From d0ec869743434313d41ebac0cd3de07f419f5696 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Wed, 30 Sep 2026 20:45:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B0=D1=81=D1=88=D1=82=D0=B0=D0=B1=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B9=D0=BC=D0=BB=D0=B0=D0=B9=D0=BD=D0=B0=20=D0=B1?= =?UTF-8?q?=D0=B5=D1=80=D1=91=D1=82=D1=81=D1=8F=20=D0=BE=D1=82=20=D1=82?= =?UTF-8?q?=D0=BE=D1=87=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=B4=20=D0=BC=D1=8B?= =?UTF-8?q?=D1=88=D0=BA=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ctrl с колесом должен оставлять на месте то, на что смотрит мышь, а оставлял точку левее — ровно на левый край таймлайна. При мыши у левого края опора и вовсе уходила за пределы окна. Причина: MousePos у DoMouseWheel по описанию экранный, но под gtk2 в нём уже клиентские координаты, и ScreenToClient переводил их второй раз. Берём Mouse.CursorPos — он экранный в любой системе. Замерено на невидимом экране: до правки мышь в 700, 1000 и 1300 давала опору в 260, 560 и 860; после — ровно 700, 1000 и 1300. Проверка оформлена в tests/zoomanchor.py и прогнана на обеих сборках: на прежней она падает, на новой проходит. --- .gitignore | 3 ++ README.md | 10 +++- src/uTimelineCtrl.pas | 12 ++++- tests/zoomanchor.py | 107 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100755 tests/zoomanchor.py diff --git a/.gitignore b/.gitignore index a13d191..dc3df30 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ shot*.png /tests/lfmcheck/lfmcheck /tests/lfmcheck/units/ +# Снимок, который делает проверка масштаба таймлайна. +/tests/кадры-зума.png + # Проверка хода полоски: двоичный файл и промежуточные модули. /tests/progress/progtest /tests/progress/*.o diff --git a/README.md b/README.md index 9e67f6b..e6dfb90 100644 --- a/README.md +++ b/README.md @@ -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` — у формы. Компилятор ловит это как diff --git a/src/uTimelineCtrl.pas b/src/uTimelineCtrl.pas index 9f51c17..9409da4 100644 --- a/src/uTimelineCtrl.pas +++ b/src/uTimelineCtrl.pas @@ -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 diff --git a/tests/zoomanchor.py b/tests/zoomanchor.py new file mode 100755 index 0000000..1bcf126 --- /dev/null +++ b/tests/zoomanchor.py @@ -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())