Выделение областью, рез всех дорожек, курсор не улетает

Рамка выделения: протяжка по пустому месту дорожек обтягивает клипы,
Delete убирает всё, что она задела. Щелчок без движения по-прежнему
ставит курсор.

Рез по курсору (S) режет все дорожки, а не ту, что под указателем мыши:
стоило указателю оказаться над пустой дорожкой, и клавиша молча не
делала ничего. Теперь рез отвечает числом разрезанных клипов.

Курсор после правки монтажа улетал в конец: при перезагрузке списка
кусков mpv сбрасывает отсчёт времени, программа ждала перемотки, не
дожидалась и подтягивала курсор к плееру. На паузе курсор теперь
остаётся там, куда его поставил человек. Это и делало Ctrl+Shift+R
неудобным: после каждого схлопывания приходилось целиться заново.

Схлопывание дыры ищет её с допуском и по всем дорожкам, а не только под
мышью, и называет дорожку в строке состояния.

Ответ на команду держится в строке состояния несколько секунд: раньше
его через долю секунды затирало сообщение о пересборке предпросмотра.

Проверки: рамка, рез всех дорожек, допуск при схлопывании (lfmcheck).
This commit is contained in:
4aiman 2026-09-14 15:27:43 +03:00
commit 86c28a3308
4 changed files with 537 additions and 98 deletions

View file

@ -209,7 +209,7 @@ begin
Timeline.Project := Proj;
Timeline.PixelsPerSecond := 100;
Timeline.Playhead := 4;
Timeline.SplitAtPlayhead(False, False);
Timeline.SplitAtPlayhead(False);
CheckTrue('клипов стало два', Proj.Tracks[0].Count = 2);
if Proj.Tracks[0].Count <> 2 then
@ -374,6 +374,196 @@ begin
end;
end;
{ Общая заготовка: проект с одним файлом и заданной сеткой кадров. }
function TestInfo: TMediaInfo;
begin
Result := Default(TMediaInfo);
Result.Valid := True;
Result.Kind := mkVideo;
Result.Duration := 600;
Result.HasVideo := True;
Result.VideoCodec := 'hevc';
Result.Width := 1920;
Result.Height := 1080;
Result.FpsNum := 30;
Result.FpsDen := 1;
Result.PixFmt := 'yuv420p';
end;
{ Выделение областью.
Проверяется расчётом, а не мышью: сама протяжка проверена на невидимом
экране, а вот что попадает в рамку — числами, иначе первая же правка
захвата пройдёт незамеченной. }
procedure CheckRubberBand;
var
Proj: TProject;
Info: TMediaInfo;
Media: TMediaItem;
Timeline: TTimelineControl;
begin
WriteLn;
WriteLn('Выделение областью');
WriteLn;
Info := TestInfo;
Proj := TProject.Create;
Timeline := TTimelineControl.Create(nil);
try
Proj.Clear;
Proj.AddTrack('V1');
Proj.AddTrack('V2');
Proj.AddTrack('V3');
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Proj.Master := Info;
{ V1: клип 0..5 и клип 20..25. V2: длинный клип 0..30.
V3: клип 0..5 — дорожка вне рамки. }
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
Proj.AddClip(Proj.Tracks[0], Media, 100, 5, 20);
Proj.AddClip(Proj.Tracks[1], Media, 0, 30, 0);
Proj.AddClip(Proj.Tracks[2], Media, 0, 5, 0);
Timeline.Project := Proj;
Timeline.PixelsPerSecond := 100;
{ Рамка по дорожкам 0 и 1, время 2..10. }
Timeline.SelectInRange(2, 10, 0, 1, False);
CheckTrue('задетый клип выделен', Proj.Tracks[0][0].Selected);
CheckTrue('клип за рамкой не выделен', not Proj.Tracks[0][1].Selected);
CheckTrue('длинный клип задет и выделен', Proj.Tracks[1][0].Selected);
CheckTrue('чужая дорожка не тронута', not Proj.Tracks[2][0].Selected);
{ Рамку тянут и справа налево — это та же рамка. }
Timeline.SelectInRange(25, 18, 0, 0, False);
CheckTrue('обратная протяжка выделила второй клип',
Proj.Tracks[0][1].Selected);
CheckTrue('прежнее выделение снято', not Proj.Tracks[0][0].Selected);
{ С Ctrl прежнее выделение остаётся. }
Timeline.SelectInRange(2, 4, 0, 0, True);
CheckTrue('добавление сохранило прежнее', Proj.Tracks[0][1].Selected);
CheckTrue('добавление выделило новое', Proj.Tracks[0][0].Selected);
{ Рамка, не задевшая ничего, просто снимает выделение. }
Timeline.SelectInRange(6, 8, 0, 0, False);
CheckTrue('пустая рамка ничего не выделила',
Proj.SelectedCount = 0);
finally
Timeline.Free;
Proj.Free;
end;
end;
{ Рез по курсору режет все дорожки.
Раньше резалась только дорожка под указателем мыши, и стоило указателю
оказаться над пустым местом, как клавиша S молча не делала ничего. }
procedure CheckSplitAllTracks;
var
Proj: TProject;
Info: TMediaInfo;
Media: TMediaItem;
Timeline: TTimelineControl;
begin
WriteLn;
WriteLn('Рез по курсору на всех дорожках');
WriteLn;
Info := TestInfo;
Proj := TProject.Create;
Timeline := TTimelineControl.Create(nil);
try
Proj.Clear;
Proj.AddTrack('V1');
Proj.AddTrack('V2');
Proj.AddTrack('A1');
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Proj.Master := Info;
{ Клипы 0..10 на первой и третьей дорожках, на второй — 20..30:
под курсором её нет, и резать там нечего. }
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 0);
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, 20);
Proj.AddClip(Proj.Tracks[2], Media, 0, 10, 0);
Timeline.Project := Proj;
Timeline.PixelsPerSecond := 100;
Timeline.ActiveTrack := 1;
Timeline.Playhead := 4;
CheckTrue('разрезаны обе задетые дорожки',
Timeline.SplitAtPlayhead(False) = 2);
CheckTrue('на первой дорожке два клипа', Proj.Tracks[0].Count = 2);
CheckTrue('на звуковой дорожке два клипа', Proj.Tracks[2].Count = 2);
CheckTrue('нетронутая дорожка осталась с одним клипом',
Proj.Tracks[1].Count = 1);
{ Курсор ровно на стыке — резать нечего, и клипов нулевой длины
появиться не должно. }
CheckTrue('на стыке рез отвечает нулём',
Timeline.SplitAtPlayhead(False) = 0);
CheckTrue('лишних клипов не появилось',
(Proj.Tracks[0].Count = 2) and (Proj.Tracks[2].Count = 2));
finally
Timeline.Free;
Proj.Free;
end;
end;
{ Схлопывание дыры не требует попадать в тайминг.
Курсор может стоять не точно на краю дыры: во время воспроизведения он
останавливается там, где успел, а после перезагрузки предпросмотра —
где оказался плеер. Раньше дыра при этом не находилась. }
procedure CheckGapTolerance;
var
Proj: TProject;
Info: TMediaInfo;
Media: TMediaItem;
Timeline: TTimelineControl;
begin
WriteLn;
WriteLn('Схлопывание дыры с допуском');
WriteLn;
Info := TestInfo;
Proj := TProject.Create;
Timeline := TTimelineControl.Create(nil);
try
Proj.Clear;
Proj.AddTrack('V1');
Proj.AddTrack('A1');
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Proj.Master := Info;
{ Дыра на картинке с 5 по 12. У звука дыры нет вовсе. }
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 12);
Proj.AddClip(Proj.Tracks[1], Media, 0, 17, 0);
Timeline.Project := Proj;
Timeline.PixelsPerSecond := 100;
{ Указатель и выбранная дорожка — звуковая, дыры на ней нет. }
Timeline.ActiveTrack := 1;
{ Курсор не в дыре, а на четыре сотых раньше её начала. }
Timeline.Playhead := 4.96;
CheckTrue('дыра найдена, хотя курсор чуть раньше края',
Timeline.RippleCloseGap);
CheckTrue('названа дорожка, где схлопнули',
Timeline.LastGapTrack = 'V1');
CheckTrue('клип придвинут вплотную',
Abs(Proj.Tracks[0][1].TlStart - 5) < 0.001);
{ Курсор далеко от дыры — ничего не двигаем. }
Proj.Tracks[0][1].TlStart := 12;
Timeline.Playhead := 2;
CheckTrue('далеко от дыры — не схлопываем',
not Timeline.RippleCloseGap);
finally
Timeline.Free;
Proj.Free;
end;
end;
begin
WriteLn('Свойства из файлов форм');
WriteLn;
@ -404,6 +594,9 @@ begin
CheckSplitKeepsSettings;
CheckRippleWithoutMouse;
CheckClipCopy;
CheckRubberBand;
CheckSplitAllTracks;
CheckGapTolerance;
WriteLn;
if Failed = 0 then