Копирование клипов: Ctrl+C и Ctrl+V
Отбивку или текстовый блок проще размножить копией, чем заводить заново через меню и настраивать оформление с нуля. Копируются выделенные клипы, вставляются по курсору. Связанные берутся вместе с парой — у картинки со звуком иначе уехала бы половина, — но связка у копий своя: копия и подлинник не должны ездить вместе. Взаимное расположение сохраняется, набор сдвигается к курсору. Дорожка выбирается сама: сначала та, откуда копировали, если это время на ней свободно; потом любая подходящая по виду; в крайнем случае заводится новая. Наложить копию на соседа по дорожке нельзя — это означало бы переход, которого никто не заказывал. В модели появился TClip.CopyAllFrom: полная копия, кроме места на монтаже. Проверка в lfmcheck перебирает перенесённые свойства и отдельно убеждается, что место и связка не переносятся.
This commit is contained in:
parent
407eb02f2d
commit
ab719dfd1e
4 changed files with 318 additions and 1 deletions
16
README.md
16
README.md
|
|
@ -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 применяет их ко всем
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue