Копирование клипов: Ctrl+C и Ctrl+V

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

Копируются выделенные клипы, вставляются по курсору. Связанные берутся
вместе с парой — у картинки со звуком иначе уехала бы половина, — но
связка у копий своя: копия и подлинник не должны ездить вместе. Взаимное
расположение сохраняется, набор сдвигается к курсору.

Дорожка выбирается сама: сначала та, откуда копировали, если это время на
ней свободно; потом любая подходящая по виду; в крайнем случае заводится
новая. Наложить копию на соседа по дорожке нельзя — это означало бы
переход, которого никто не заказывал.

В модели появился TClip.CopyAllFrom: полная копия, кроме места на
монтаже. Проверка в lfmcheck перебирает перенесённые свойства и отдельно
убеждается, что место и связка не переносятся.
This commit is contained in:
4aiman 2026-09-11 21:14:52 +03:00
commit ab719dfd1e
4 changed files with 318 additions and 1 deletions

View file

@ -90,6 +90,8 @@ xdg-mime query default application/x-nlve-project
| Выключить дорожку / оставить только её | кнопки М и С в заголовке |
| Громкость клипа | тянуть ручку на клипе вверх-вниз |
| Автосхлопывание вкл/выкл | Ctrl+K |
| Копировать и вставить клипы | Ctrl+C, Ctrl+V |
| Скопировать и применить оформление | Ctrl+Shift+C, Ctrl+Shift+V |
| Связать или развязать клипы | Ctrl+G |
| Отменить прилипание на время | Shift при перетаскивании |
| Вписать проект целиком | Ctrl+0 |
@ -182,6 +184,20 @@ xdg-mime query default application/x-nlve-project
переименовать, удалить. Удаление дорожки с клипами спрашивает
подтверждение и называет число клипов.
**Копирование клипов.** Ctrl+C копирует выделенные клипы, Ctrl+V кладёт
копии по курсору. Отбивку или текстовый блок так размножить проще, чем
заводить заново и настраивать оформление с нуля.
Связанные клипы копируются с парой: у картинки со звуком иначе уехала бы
половина. Связка у копий своя — копия и подлинник не ездят вместе.
Взаимное расположение сохраняется: несколько клипов встают тем же узором,
сдвинутым к курсору.
Дорожка выбирается сама: сначала та, откуда копировали, если это время на
ней свободно; потом любая подходящая по виду; в крайнем случае заводится
новая. Класть копию поверх соседа нельзя — на одной дорожке это означает
переход, а его никто не заказывал.
**Копирование свойств клипа.** Ctrl+Shift+C берёт с выделенного клипа всё
оформление — кадрирование, поворот, увеличение, смещение, прозрачность,
цветовой ключ, затухания, громкость, — Ctrl+Shift+V применяет их ко всем

View file

@ -22,7 +22,8 @@ unit uMainForm;
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls,
Classes, SysUtils, contnrs, Forms, Controls, Graphics, Dialogs, ExtCtrls,
StdCtrls,
ComCtrls, Menus, Buttons, LCLType, LMessages,
uAppTypes, uEdl, uMpvLib, uMpvPlayer, uProject, uMediaProbe, uTimelineCtrl,
uFlatten, uGenerate, uExport, uEncodeForm, uAboutForm, uProgressForm,
@ -106,6 +107,13 @@ type
{ Клип, с которого скопированы свойства. Хранится копией, а не
ссылкой: исходный клип могут удалить, а свойства должны остаться. }
FLookClip: TClip;
{ Скопированные клипы: сами копии, имя дорожки каждого, смещение от
начала выделения и прежняя связка. Хранятся копиями по той же
причине — исходные могут исчезнуть. }
FClipboard: TObjectList;
FClipTrackNames: array of string;
FClipOffsets: array of Double;
FClipGroups: array of string;
{ Проект открыт из командной строки: спросить про клипы на чужих
дорожках можно только когда окно уже показано. }
FCheckMisplacedOnShow: Boolean;
@ -335,6 +343,11 @@ type
procedure MenuColorKey(Sender: TObject);
procedure MenuCopyLook(Sender: TObject);
procedure MenuPasteLook(Sender: TObject);
procedure MenuCopyClips(Sender: TObject);
procedure MenuPasteClips(Sender: TObject);
{ Куда положить вставляемый клип: дорожка того же вида, где это время
свободно. }
function TrackForPaste(AClip: TClip; const AName: string): TTrack;
function FirstSelectedClip: TClip;
procedure MenuRegionIn(Sender: TObject);
procedure MenuRegionOut(Sender: TObject);
@ -463,6 +476,9 @@ begin
BuildPoolMenu;
BuildTrackMenu;
{ Буфер владеет копиями: подлинники могут исчезнуть, а вставлять надо
по-прежнему. }
FClipboard := TObjectList.Create(True);
RefreshRecentMenu;
AllowDropFiles := True;
OnDropFiles := @FormFilesDropped;
@ -490,6 +506,7 @@ procedure TMainForm.FormDestroy(Sender: TObject);
begin
Log('Закрытие: останавливаю предпросмотр.');
FreeAndNil(FLookClip);
FreeAndNil(FClipboard);
FPreviewTimer.Enabled := False;
if Assigned(FThumbThread) then
begin
@ -713,6 +730,10 @@ begin
AddItem(MEdit, 'Выделить всё', @MenuSelectAll, ShortCut(VK_A, [ssCtrl]));
AddItem(MEdit, 'Цветовой ключ клипа...', @MenuColorKey);
AddSep(MEdit);
AddItem(MEdit, 'Копировать клипы', @MenuCopyClips, ShortCut(VK_C, [ssCtrl]));
AddItem(MEdit, 'Вставить клипы по курсору', @MenuPasteClips,
ShortCut(VK_V, [ssCtrl]));
AddSep(MEdit);
AddItem(MEdit, 'Скопировать свойства клипа', @MenuCopyLook,
ShortCut(VK_C, [ssCtrl, ssShift]));
AddItem(MEdit, 'Применить свойства к выделенным', @MenuPasteLook,
@ -3508,6 +3529,193 @@ end;
Отметки ставятся по курсору, как в монтажных программах. Перепутанные
местами меняются сами: требовать от человека помнить порядок — лишняя
строгость там, где всё однозначно. }
{ Копирование клипов.
Отбивку или текстовый блок проще размножить копией, чем заводить заново
через меню и настраивать заново оформление. Связанные клипы копируются
вместе с парой: у картинки со звуком иначе уехала бы половина.
Взаимное расположение сохраняется: несколько клипов вставляются тем же
узором, сдвинутым к курсору. }
procedure TMainForm.MenuCopyClips(Sender: TObject);
var
T, C, I: Integer;
Clip, Copy: TClip;
Track: TTrack;
Base: Double;
Taken: TFPList;
{ Берём и связанные: они двигаются, режутся и удаляются вместе, значит
и копироваться должны вместе. }
procedure TakeWithGroup(ASource: TClip);
var
Ti, Ci: Integer;
Other: TClip;
begin
if Taken.IndexOf(ASource) >= 0 then
Exit;
Taken.Add(ASource);
if ASource.GroupId = '' then
Exit;
for Ti := 0 to FProject.TrackCount - 1 do
for Ci := 0 to FProject.Tracks[Ti].Count - 1 do
begin
Other := FProject.Tracks[Ti][Ci];
if (Other.GroupId = ASource.GroupId) and (Taken.IndexOf(Other) < 0) then
Taken.Add(Other);
end;
end;
begin
Taken := TFPList.Create;
try
for T := 0 to FProject.TrackCount - 1 do
for C := 0 to FProject.Tracks[T].Count - 1 do
if FProject.Tracks[T][C].Selected then
TakeWithGroup(FProject.Tracks[T][C]);
if Taken.Count = 0 then
begin
SetStatus('Сначала выделите клипы, которые копировать.');
Exit;
end;
FClipboard.Clear;
FClipTrackNames := nil;
FClipOffsets := nil;
FClipGroups := nil;
{ Смещения считаем от самого раннего клипа: вставка кладёт его под
курсор, остальные — как лежали относительно него. }
Base := TClip(Taken[0]).TlStart;
for I := 1 to Taken.Count - 1 do
Base := Min(Base, TClip(Taken[I]).TlStart);
SetLength(FClipTrackNames, Taken.Count);
SetLength(FClipOffsets, Taken.Count);
SetLength(FClipGroups, Taken.Count);
for I := 0 to Taken.Count - 1 do
begin
Clip := TClip(Taken[I]);
Track := FProject.FindClipTrack(Clip);
Copy := TClip.Create;
Copy.CopyAllFrom(Clip);
FClipboard.Add(Copy);
if Track <> nil then
FClipTrackNames[I] := Track.Name
else
FClipTrackNames[I] := '';
FClipOffsets[I] := Clip.TlStart - Base;
FClipGroups[I] := Clip.GroupId;
end;
SetStatus(Format('Скопировано %d %s. Вставить: Ctrl+V.',
[Taken.Count, Plural(Taken.Count, 'клип', 'клипа', 'клипов')]));
finally
Taken.Free;
end;
end;
{ Дорожка для вставки: сначала та, откуда копировали, если она есть и
время свободно; потом любая подходящая по виду; в крайнем случае новая.
Класть клип поверх соседа нельзя: на одной дорожке это означает переход,
а его никто не заказывал. }
function TMainForm.TrackForPaste(AClip: TClip; const AName: string): TTrack;
var
I: Integer;
Need: TTrackKind;
function Fits(ATrack: TTrack): Boolean;
var
C: Integer;
begin
Result := True;
for C := 0 to ATrack.Count - 1 do
if (AClip.TlStart < ATrack[C].TlEnd - 0.001) and
(AClip.TlEnd > ATrack[C].TlStart + 0.001) then
Exit(False);
end;
begin
Need := TProject.TrackKindFor(AClip.Kind);
for I := 0 to FProject.TrackCount - 1 do
if (FProject.Tracks[I].Name = AName) and (FProject.Tracks[I].Kind = Need) and
Fits(FProject.Tracks[I]) then
Exit(FProject.Tracks[I]);
for I := 0 to FProject.TrackCount - 1 do
if (FProject.Tracks[I].Kind = Need) and Fits(FProject.Tracks[I]) then
Exit(FProject.Tracks[I]);
case Need of
tkAudio: Result := FProject.AddTrackOfKind(FProject.UniqueTrackName('A'),
tkAudio);
tkText: Result := FProject.AddTrackOfKind(FProject.UniqueTrackName('Т'),
tkText);
else
Result := FProject.AddTrack(FProject.UniqueTrackName('V'));
end;
end;
procedure TMainForm.MenuPasteClips(Sender: TObject);
var
I, J: Integer;
Clip: TClip;
Track: TTrack;
NewGroups: TStringList;
begin
if FClipboard.Count = 0 then
begin
SetStatus('Сначала скопируйте клипы: Ctrl+C.');
Exit;
end;
FProject.BeginChange;
FProject.ClearSelection;
{ Связки у вставленных свои: иначе копия и подлинник ездили бы и
удалялись вместе. Прежние связки при этом сохраняются как связки
внутри вставленного набора. }
NewGroups := TStringList.Create;
try
NewGroups.NameValueSeparator := '=';
for I := 0 to FClipboard.Count - 1 do
begin
Clip := TClip.Create;
Clip.CopyAllFrom(TClip(FClipboard[I]));
Clip.Id := FProject.NewId('c');
Clip.TlStart := Max(0, FTimeline.Playhead + FClipOffsets[I]);
Clip.Selected := True;
{ Старая связка отображается в новую: клипы, связанные между собой,
останутся связанными и после вставки, но с подлинником общего у
них больше не будет. }
if FClipGroups[I] <> '' then
begin
J := NewGroups.IndexOfName(FClipGroups[I]);
if J < 0 then
J := NewGroups.Add(FClipGroups[I] + '=' + FProject.NewId('g'));
Clip.GroupId := NewGroups.ValueFromIndex[J];
end;
Track := TrackForPaste(Clip, FClipTrackNames[I]);
Track.Add(Clip);
Track.Sort;
end;
finally
NewGroups.Free;
end;
FTimeline.Invalidate;
FLastLoadedEdl := '';
SchedulePreview;
UpdateCaption;
SetStatus(Format('Вставлено %d %s.',
[FClipboard.Count,
Plural(FClipboard.Count, 'клип', 'клипа', 'клипов')]));
end;
{ Первый выделенный клип: с него берут свойства. }
function TMainForm.FirstSelectedClip: TClip;
var

View file

@ -164,6 +164,13 @@ type
картинку в надпись при этом незачем. Оформление надписи переносится
только между надписями. }
procedure CopyLookFrom(ASource: TClip);
{ Полная копия клипа, кроме его места на монтаже.
Переносится всё, что делает клип этим куском материала: файл, откуда
и сколько брать, как сделаны края, и всё оформление. Не переносится
только то, что определяет, где он лежит: имя, время начала, связка и
выделение — их задаёт тот, кто вставляет. }
procedure CopyAllFrom(ASource: TClip);
function TlEnd: Double;
function SrcOut: Double;
{ Попадает ли момент внутрь клипа. Правый край не включается: там уже
@ -481,6 +488,21 @@ begin
FTextStyle := ASource.TextStyle;
end;
procedure TClip.CopyAllFrom(ASource: TClip);
begin
if ASource = nil then
Exit;
FMediaId := ASource.MediaId;
FSrcIn := ASource.SrcIn;
FDuration := ASource.Duration;
FEdgeIn := ASource.EdgeIn;
FEdgeOut := ASource.EdgeOut;
FKind := ASource.Kind;
FText := ASource.Text;
FTextStyle := ASource.TextStyle;
CopyLookFrom(ASource);
end;
procedure TClip.CopyLookFrom(ASource: TClip);
begin
if ASource = nil then

View file

@ -304,6 +304,76 @@ begin
end;
end;
{ Копирование клипа: модель должна переносить всё, кроме места.
Само сочетание клавиш проверяется руками, а вот перенос свойств —
числами: именно из-за него копия отбивки или текстового блока была бы
бесполезной. }
procedure CheckClipCopy;
var
Proj: TProject;
Info: TMediaInfo;
Media: TMediaItem;
Src, Dst: TClip;
Key: TColorKey;
begin
WriteLn;
WriteLn('Копия клипа');
WriteLn;
Info := Default(TMediaInfo);
Info.Valid := True;
Info.Kind := mkVideo;
Info.Duration := 600;
Info.HasVideo := True;
Info.VideoCodec := 'hevc';
Info.Width := 1920;
Info.Height := 1080;
Info.FpsNum := 30;
Info.FpsDen := 1;
Info.PixFmt := 'yuv420p';
Proj := TProject.Create;
try
Proj.Clear;
Proj.AddTrack('V1');
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Src := Proj.AddClip(Proj.Tracks[0], Media, 30, 7, 5);
Src.Kind := ckText;
Src.Text := 'Отбивка';
Src.Volume := 0.4;
Src.Opacity := 0.6;
Key := DefaultColorKey;
Key.Enabled := True;
Src.ColorKey := Key;
Src.FadeIn := 1.25;
Src.EdgeOut := edPrecise;
Src.GroupId := 'g7';
Dst := TClip.Create;
try
Dst.CopyAllFrom(Src);
CheckTrue('вид клипа перенесён', Dst.Kind = ckText);
CheckTrue('текст перенесён', Dst.Text = 'Отбивка');
CheckTrue('файл и кусок исходника перенесены',
(Dst.MediaId = Src.MediaId) and (Abs(Dst.SrcIn - 30) < 0.001) and
(Abs(Dst.Duration - 7) < 0.001));
CheckTrue('края перенесены', Dst.EdgeOut = edPrecise);
CheckTrue('громкость и прозрачность перенесены',
(Abs(Dst.Volume - 0.4) < 0.001) and (Abs(Dst.Opacity - 0.6) < 0.001));
CheckTrue('ключ перенесён', Dst.ColorKey.Enabled);
CheckTrue('затухание перенесено', Abs(Dst.FadeIn - 1.25) < 0.001);
{ Место и связка — дело того, кто вставляет. }
CheckTrue('место не перенесено', Abs(Dst.TlStart) < 0.001);
CheckTrue('связка не перенесена', Dst.GroupId = '');
finally
Dst.Free;
end;
finally
Proj.Free;
end;
end;
begin
WriteLn('Свойства из файлов форм');
WriteLn;
@ -333,6 +403,7 @@ begin
CheckPlayheadSnap;
CheckSplitKeepsSettings;
CheckRippleWithoutMouse;
CheckClipCopy;
WriteLn;
if Failed = 0 then