Выделение по времени, рез по краям промежутка, курсор идёт за резом

Режимов выделения стало два, переключаются клавишей D и кнопкой внизу,
режим показан её значком.

Рамкой по клипам — как было. По времени — протяжка отмечает промежуток по
всем дорожкам сразу, он остаётся виден пунктиром, и S режет все дорожки по
обоим его краям. Выделенной после этого остаётся ровно середина: следующим
действием её обычно удаляют, а выделение на старых клипах убрало бы не то.

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

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

Выделение рамкой требует настоящего перекрытия, а не касания краями:
сосед, кончающийся ровно на краю рамки, попадал в выделение из-за разницы
в последнем знаке дробей.

Кнопка журнала и строка состояния сдвинуты вправо: переключатель выделения
встал в один ряд с прилипанием и схлопыванием.

Значки — icons8, набор ios-glyphs, авторство дописано в attribution.txt.
Набора «ios26» на их раздаче нет (404), взят ближайший по виду к нынешним.

Проверки: рез по курсору и по краям промежутка, кадровая сетка, выделение
середины, курсор после удаления и после схлопывания.
This commit is contained in:
4aiman 2026-09-29 23:58:30 +03:00
commit 6617e29514
11 changed files with 538 additions and 31 deletions

View file

@ -631,6 +631,128 @@ begin
end;
end;
{ Рез и курсор: где режет S и куда после этого встаёт курсор.
Раньше курсор оставался там, где был, а рез мог попасть на долю кадра.
Теперь рез кладётся на кадровую сетку, а курсор встаёт ровно на место
реза — с ним и работают дальше. }
procedure CheckSplitAndPlayhead;
var
Proj: TProject;
Info: TMediaInfo;
Media: TMediaItem;
Timeline: TTimelineControl;
Step: Double;
I, N: Integer;
begin
WriteLn;
WriteLn('Рез, промежуток и курсор');
WriteLn;
Info := TestInfo;
Info.FpsNum := 60000;
Info.FpsDen := 1001;
Step := Info.FpsDen / Info.FpsNum;
Proj := TProject.Create;
Timeline := TTimelineControl.Create(nil);
try
Proj.Clear;
Proj.AddTrack('V1');
Proj.AddTrack('A1');
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Proj.Master := Info;
{ По два клипа на каждой дорожке, встык: 0..10 и 10..20. }
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 0);
Proj.AddClip(Proj.Tracks[0], Media, 20, 10, 10);
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, 0);
Proj.AddClip(Proj.Tracks[1], Media, 20, 10, 10);
Timeline.Project := Proj;
Timeline.PixelsPerSecond := 100;
{ Рез по курсору: место кладётся на кадр, курсор встаёт на него. }
Timeline.Playhead := 3.3177;
CheckTrue('разрезаны обе дорожки', Timeline.SplitAtPlayhead(False) = 2);
CheckTrue('курсор встал на место реза',
Abs(Timeline.Playhead - Round(3.3177 / Step) * Step) < 0.0001);
CheckTrue('курсор на кадровой сетке',
Abs(Timeline.Playhead / Step - Round(Timeline.Playhead / Step)) < 0.001);
N := 0;
for I := 0 to Proj.Tracks[0].Count - 1 do
if Abs(Proj.Tracks[0][I].TlStart - Timeline.Playhead) < 0.0001 then
Inc(N);
CheckTrue('рез пришёлся ровно туда, где курсор', N = 1);
{ Промежуток по времени: рез по обоим краям, курсор — на начало. }
Timeline.SelectionMode := smRange;
Timeline.SetSelectionRange(12.13, 16.77);
CheckTrue('промежуток отмечен', Timeline.HasSelectionRange);
CheckTrue('разрезано по два клипа на дорожке',
Timeline.SplitAtPlayhead(False) = 4);
CheckTrue('курсор — на начале промежутка',
Abs(Timeline.Playhead - Timeline.RangeFrom) < 0.0001);
N := 0;
for I := 0 to Proj.Tracks[1].Count - 1 do
if (Abs(Proj.Tracks[1][I].TlStart - Timeline.RangeFrom) < 0.0001) or
(Abs(Proj.Tracks[1][I].TlStart - Timeline.RangeTo) < 0.0001) then
Inc(N);
CheckTrue('на звуковой дорожке резы по обоим краям', N = 2);
{ После реза выделена середина — то, что между краями: следующим
действием её обычно удаляют. }
N := 0;
for I := 0 to Proj.Tracks[0].Count - 1 do
if Proj.Tracks[0][I].Selected then
begin
Inc(N);
CheckTrue('выделенное лежит внутри промежутка',
(Proj.Tracks[0][I].TlStart >= Timeline.RangeFrom - 0.0001) and
(Proj.Tracks[0][I].TlEnd <= Timeline.RangeTo + 0.0001));
end;
CheckTrue('на дорожке картинки выделена одна середина', N = 1);
{ Смена режима снимает промежуток: резать по невидимой метке нельзя. }
Timeline.SelectionMode := smClips;
CheckTrue('смена режима сняла промежуток',
not Timeline.HasSelectionRange);
{ Удаление со схлопыванием: курсор встаёт на место стыка. }
Proj.Clear;
Proj.AddTrack('V1');
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Proj.Master := Info;
Proj.RippleEnabled := True;
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
Proj.AddClip(Proj.Tracks[0], Media, 10, 5, 5);
Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 10);
Timeline.Playhead := 0;
Proj.Tracks[0][1].Selected := True;
Timeline.DeleteSelected;
CheckTrue('после удаления клипов стало два', Proj.Tracks[0].Count = 2);
CheckTrue('курсор — на месте вырезанного куска',
Abs(Timeline.Playhead - 5) < 0.0001);
CheckTrue('хвост придвинут к стыку',
Abs(Proj.Tracks[0][1].TlStart - 5) < 0.0001);
{ Схлопывание дыры с клавиатуры: курсор тоже на стык. }
Proj.Clear;
Proj.AddTrack('V1');
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Proj.Master := Info;
Proj.AddClip(Proj.Tracks[0], Media, 0, 5, 0);
Proj.AddClip(Proj.Tracks[0], Media, 20, 5, 12);
Timeline.ActiveTrack := 0;
Timeline.Playhead := 8;
CheckTrue('дыра схлопнута', Timeline.RippleCloseGap);
CheckTrue('курсор — на стыке', Abs(Timeline.Playhead - 5) < 0.0001);
finally
Timeline.Free;
Proj.Free;
end;
end;
begin
WriteLn('Свойства из файлов форм');
WriteLn;
@ -663,6 +785,7 @@ begin
CheckClipCopy;
CheckRubberBand;
CheckFrameGridRepair;
CheckSplitAndPlayhead;
CheckSplitAllTracks;
CheckGapTolerance;