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

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

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())