Масштаб таймлайна берётся от точки под мышкой

Ctrl с колесом должен оставлять на месте то, на что смотрит мышь, а
оставлял точку левее — ровно на левый край таймлайна. При мыши у левого
края опора и вовсе уходила за пределы окна.

Причина: MousePos у DoMouseWheel по описанию экранный, но под gtk2 в нём
уже клиентские координаты, и ScreenToClient переводил их второй раз.
Берём Mouse.CursorPos — он экранный в любой системе.

Замерено на невидимом экране: до правки мышь в 700, 1000 и 1300 давала
опору в 260, 560 и 860; после — ровно 700, 1000 и 1300. Проверка
оформлена в tests/zoomanchor.py и прогнана на обеих сборках: на прежней
она падает, на новой проходит.
This commit is contained in:
4aiman 2026-09-30 20:45:32 +03:00
commit d0ec869743
4 changed files with 130 additions and 2 deletions

3
.gitignore vendored
View file

@ -51,6 +51,9 @@ shot*.png
/tests/lfmcheck/lfmcheck
/tests/lfmcheck/units/
# Снимок, который делает проверка масштаба таймлайна.
/tests/кадры-зума.png
# Проверка хода полоски: двоичный файл и промежуточные модули.
/tests/progress/progtest
/tests/progress/*.o

View file

@ -84,7 +84,7 @@ xdg-mime query default application/x-nlve-project
| Режим выделения: рамкой или по времени | D |
| Схлопнуть дыру под курсором | Ctrl+Shift+R |
| Отменить, повторить | Ctrl+Z, Ctrl+Y |
| Масштаб таймлайна | Ctrl + колесо мыши |
| Масштаб таймлайна, от точки под мышкой | Ctrl + колесо мыши |
| Прокрутка по дорожкам | колесо мыши |
| Прокрутка по времени | Shift + колесо, средняя кнопка |
| Прокрутка журнала вбок | Shift + колесо над журналом |
@ -829,6 +829,14 @@ Wine с новой библиотекой не грузилось ничего.
привычный RGB. Записанный по привычке код даёт цвет с переставленными
красным и синим.
**MousePos у колеса мыши — не экранный.** По описанию `DoMouseWheel`
получает точку в координатах экрана, но под gtk2 в ней уже клиентские:
`ScreenToClient` переводит их второй раз, и точка уезжает влево ровно на
левый край элемента. У масштаба таймлайна это выглядело так, будто он
берётся не от мыши: замерено на невидимом экране — мышь в 700, 1000 и
1300, опора в 260, 560 и 860, всегда на 440 левее. Лечится обращением к
`Mouse.CursorPos`: он экранный в любой системе, и перевод остаётся один.
**Имена, занятые предками.** Локальная переменная перекрывает одноимённое
свойство класса: `Text` и `Position` заняты у `TControl`, `Finished` — у
`TThread`, `Name` и `Caption` — у формы. Компилятор ловит это как

View file

@ -2758,7 +2758,17 @@ function TTimelineControl.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
var
Pt: TPoint;
begin
Pt := ScreenToClient(MousePos);
{ Точка берётся у самой мыши, а не из MousePos.
MousePos по описанию приходит в координатах экрана, но под gtk2 в нём
уже клиентские: ScreenToClient переводил их второй раз, и точка опоры
уезжала влево ровно на левый край таймлайна. Замерено на невидимом
экране: мышь в 700, 1000 и 1300 — опора в 260, 560 и 860, то есть
всегда на 440 левее, а 440 — это и есть левый край. При мыши слева
опора и вовсе уходила за пределы окна.
Mouse.CursorPos экранный в любой системе, и перевод остаётся один. }
Pt := ScreenToClient(Mouse.CursorPos);
if ssCtrl in Shift then
begin

107
tests/zoomanchor.py Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Проверка того, что масштаб таймлайна берётся от точки под мышкой.
Ctrl с колесом меняет масштаб. Точка монтажа, на которую смотрит мышь,
обязана остаться на месте: иначе прицелиться масштабом нельзя — картинка
уезжает из-под курсора.
Как меряется. Курсор воспроизведения ставится в известное место и служит
меткой: он привязан ко времени, а не к пикселям, и при смене масштаба
едет вместе с монтажом. Зная, что один щелчок колеса меняет масштаб ровно
в 1.25 раза, неподвижную точку считаем по двум положениям метки:
p1 - a = k * (p0 - a) => a = (k * p0 - p1) / (k - 1)
Ошибку это не прячет: если опора уедет, a разойдётся с положением мыши.
Запуск (редактор уже должен идти на невидимом экране :99):
tests/zoomanchor.py <окно>
где окно — из `DISPLAY=:99 xdotool search --name nlve`.
"""
import os
import subprocess
import sys
import time
from PIL import Image
DISP = os.environ.get('ЭКРАН', ':99')
ENV = {'DISPLAY': DISP, 'PATH': '/usr/bin:/bin'}
# Один щелчок колеса. Должен совпадать с ZoomStep в uTimelineCtrl.
STEP = 1.25
# Полоса окна, по которой всё и меряется: линейка и первая дорожка.
BAND = '1500x120+0+450'
SHOT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'кадры-зума.png')
def run(*args):
subprocess.run(list(args), env=ENV, check=True)
def playhead(win):
"""Где сейчас курсор воспроизведения. Он единственный красный."""
run('import', '-window', win, '-crop', BAND, '+repage', SHOT)
im = Image.open(SHOT).convert('RGB')
px = im.load()
w, h = im.size
best = (0, None)
for x in range(10, w - 10):
red = sum(1 for y in range(h)
if px[x, y][0] > 120
and px[x, y][0] - px[x, y][1] > 40
and px[x, y][0] - px[x, y][2] > 40)
if red > best[0]:
best = (red, x)
return best[1] if best[0] > 60 else None
def measure(win, mouse_x):
before = playhead(win)
run('xdotool', 'mousemove', str(mouse_x), '520')
time.sleep(0.4)
run('xdotool', 'keydown', 'ctrl', 'click', '4', 'keyup', 'ctrl')
time.sleep(0.9)
after = playhead(win)
run('xdotool', 'keydown', 'ctrl', 'click', '5', 'keyup', 'ctrl')
time.sleep(0.9)
if before is None or after is None:
return None
return (STEP * before - after) / (STEP - 1)
def main():
if len(sys.argv) < 2:
print(__doc__)
return 2
win = sys.argv[1]
# Метку ставим щелчком по пустому месту под дорожками.
run('xdotool', 'mousemove', '900', '600', 'click', '1')
time.sleep(0.8)
bad = 0
for mouse_x in (700, 1000, 1300):
anchor = measure(win, mouse_x)
if anchor is None:
print('мышь %4d: метку не видно, замер не вышел' % mouse_x)
bad += 1
continue
off = anchor - mouse_x
ok = abs(off) <= 2
print('мышь %4d: опора %6.0f, промах %+.0f точек — %s'
% (mouse_x, anchor, off, 'ok' if ok else 'ПЛОХО'))
if not ok:
bad += 1
print()
if bad:
print('ВЕРДИКТ: масштаб берётся не от мыши, случаев: %d' % bad)
return 1
print('ВЕРДИКТ: точка под мышкой остаётся на месте')
return 0
if __name__ == '__main__':
sys.exit(main())