Уголки участка, курсор посередине и журнал сборки рядом с роликом
Отметки участка стали видимыми уголками на линейке — жёлтые треугольники по краям, за них можно тянуть. Клавишами это выходило долго: поставил, посмотрел, промахнулся, ставь заново. Край прилипает к границам клипов, как и курсор, и края не пропускаются друг за друга. Клавиша \ и кнопка с целью переключают, держать ли курсор посередине монтажа при воспроизведении. Раньше монтаж ехал под курсором всегда, а это мешает, когда разглядываешь конкретное место. В окне сборки вместо надписи про участок появилась галка «только отмеченный участок». Надпись легко проглядеть, а вопрос из тех, где ошибка стоит часа работы: соберёшь кусок вместо ролика или наоборот. Снятая галка собирает весь монтаж, отметки при этом остаются. Журнал сборки пишется рядом с готовым файлом и под тем же именем. Общий журнал программы перезаписывается следующим запуском, а разбираться, почему в ролике не то, приходится потом. В журнале: время, проект, участок, кадр и битрейт, что с текстом, и полный список кусков — с пометками, где слои, где текст и что пересобирается. Строка пишется сразу, чтобы после падения осталась последняя.
This commit is contained in:
parent
71381bb774
commit
96672341d2
8 changed files with 434 additions and 61 deletions
|
|
@ -115,6 +115,7 @@ type
|
|||
FStageText: string;
|
||||
FFraction: Double;
|
||||
FPendingLog: string;
|
||||
FLogPath: string;
|
||||
FReport: TExportReport;
|
||||
|
||||
FOnProgress: TNotifyEvent;
|
||||
|
|
@ -142,6 +143,7 @@ type
|
|||
procedure SyncProgress;
|
||||
procedure SyncLog;
|
||||
procedure Log(const AText: string);
|
||||
procedure WriteLogLine(const AText: string);
|
||||
procedure SetStage(const AText: string);
|
||||
procedure PublishProgress;
|
||||
|
||||
|
|
@ -187,6 +189,14 @@ type
|
|||
дорожкой. Пусто — либо текста нет, либо он вжигается в картинку.
|
||||
Годится только для Matroska: MP4 хранит субтитры без оформления. }
|
||||
property SubtitlePath: string read FSubtitlePath write FSubtitlePath;
|
||||
{ Куда писать журнал сборки. Пусто — никуда.
|
||||
|
||||
Журнал кладётся рядом с готовым файлом и под тем же именем: когда
|
||||
через неделю выяснится, что в ролике не то, разбираться придётся
|
||||
именно по нему, а общий журнал программы к тому времени перезапишется
|
||||
следующим запуском. Строка пишется сразу, а не копится в памяти:
|
||||
если сборка упадёт, важна как раз последняя строка. }
|
||||
property LogPath: string read FLogPath write FLogPath;
|
||||
end;
|
||||
|
||||
{ Каталог для временных кусков рядом с готовым файлом. }
|
||||
|
|
@ -272,8 +282,38 @@ begin
|
|||
FOnLog(Self);
|
||||
end;
|
||||
|
||||
{ Строка в журнал сборки рядом с готовым файлом.
|
||||
|
||||
Файл открывается и закрывается на каждой строке. Это медленно, но строк
|
||||
за сборку десятки, а взамен журнал остаётся целым даже если программа
|
||||
упадёт на следующем шаге — ради этого он и заводится. }
|
||||
procedure TExportThread.WriteLogLine(const AText: string);
|
||||
var
|
||||
F: TextFile;
|
||||
begin
|
||||
if FLogPath = '' then
|
||||
Exit;
|
||||
try
|
||||
AssignFile(F, FLogPath);
|
||||
if FileExists(FLogPath) then
|
||||
Append(F)
|
||||
else
|
||||
Rewrite(F);
|
||||
try
|
||||
WriteLn(F, AText);
|
||||
finally
|
||||
CloseFile(F);
|
||||
end;
|
||||
except
|
||||
{ Журнал не должен ронять сборку: не записалось — и ладно. }
|
||||
on E: Exception do
|
||||
FLogPath := '';
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TExportThread.Log(const AText: string);
|
||||
begin
|
||||
WriteLogLine(AText);
|
||||
FPendingLog := AText;
|
||||
Synchronize(@SyncLog);
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ type
|
|||
Path: string;
|
||||
{ Вжигать ли текст в картинку. При MP4 всегда True. }
|
||||
BurnText: Boolean;
|
||||
{ Собирать только отмеченный участок. Имеет смысл, только когда
|
||||
участок отмечен. }
|
||||
RegionOnly: Boolean;
|
||||
end;
|
||||
|
||||
{ Показывает окно.
|
||||
|
|
@ -39,9 +42,10 @@ type
|
|||
незачем. APreferMkv — в монтаже есть точные резы, и MKV надёжнее.
|
||||
ABurnDefault — ответ, данный в прошлый раз.
|
||||
|
||||
ARegion — описание отмеченного участка. Не пусто — окно прямо говорит,
|
||||
что собирается не весь ролик: это легко забыть и потом долго искать,
|
||||
почему в файле не хватает половины. }
|
||||
ARegion — описание отмеченного участка. Не пусто — в окне появляется
|
||||
галка «только отмеченный участок». Галка, а не надпись: надпись легко
|
||||
проглядеть, а тут вопрос из тех, где ошибка стоит часа работы — соберёшь
|
||||
кусок вместо ролика или наоборот. }
|
||||
function AskExport(var AChoice: TExportChoice; AHasText, APreferMkv: Boolean;
|
||||
const ADefaultDir: string; const ARegion: string = ''): Boolean;
|
||||
|
||||
|
|
@ -55,6 +59,7 @@ type
|
|||
BtnMp4: TThemedButton;
|
||||
BtnMkv: TThemedButton;
|
||||
cbBurn: TCheckBox;
|
||||
cbRegion: TCheckBox;
|
||||
lblNote: TLabel;
|
||||
HasText: Boolean;
|
||||
{ Ответ человека про вжигание, сохранённый до того, как MP4 навязал
|
||||
|
|
@ -69,6 +74,7 @@ type
|
|||
procedure FormatClick(Sender: TObject);
|
||||
procedure BrowseClick(Sender: TObject);
|
||||
procedure BurnClick(Sender: TObject);
|
||||
procedure RegionClick(Sender: TObject);
|
||||
function IsMp4: Boolean;
|
||||
procedure ApplyFormat;
|
||||
end;
|
||||
|
|
@ -143,6 +149,12 @@ begin
|
|||
ApplyFormat;
|
||||
end;
|
||||
|
||||
{ Галка участка ничего не пересчитывает: она только запоминает ответ.
|
||||
Всё, что от неё зависит, решается уже после закрытия окна. }
|
||||
procedure TExportDialog.RegionClick(Sender: TObject);
|
||||
begin
|
||||
end;
|
||||
|
||||
procedure TExportDialog.BrowseClick(Sender: TObject);
|
||||
var
|
||||
Dlg: TSaveDialog;
|
||||
|
|
@ -187,7 +199,7 @@ begin
|
|||
Dlg.Form.Caption := 'Сборка ролика';
|
||||
Dlg.Form.Position := poScreenCenter;
|
||||
Dlg.Form.ClientWidth := 620;
|
||||
Dlg.Form.ClientHeight := 250;
|
||||
Dlg.Form.ClientHeight := 272;
|
||||
|
||||
Lbl := TLabel.Create(Dlg.Form);
|
||||
Lbl.Parent := Dlg.Form;
|
||||
|
|
@ -235,23 +247,32 @@ begin
|
|||
Lbl.Font.Color := TColor($30A0D0);
|
||||
end;
|
||||
|
||||
Dlg.cbRegion := TCheckBox.Create(Dlg.Form);
|
||||
Dlg.cbRegion.Parent := Dlg.Form;
|
||||
Dlg.cbRegion.SetBounds(12, 112, 596, 22);
|
||||
Dlg.cbRegion.Anchors := [akTop, akLeft, akRight];
|
||||
Dlg.cbRegion.Caption := 'Только отмеченный участок: ' + ARegion;
|
||||
Dlg.cbRegion.Visible := ARegion <> '';
|
||||
Dlg.cbRegion.Checked := ARegion <> '';
|
||||
Dlg.cbRegion.OnClick := @Dlg.RegionClick;
|
||||
|
||||
Dlg.cbBurn := TCheckBox.Create(Dlg.Form);
|
||||
Dlg.cbBurn.Parent := Dlg.Form;
|
||||
Dlg.cbBurn.SetBounds(12, 116, 300, 22);
|
||||
Dlg.cbBurn.SetBounds(12, 138, 300, 22);
|
||||
Dlg.cbBurn.Caption := 'Вжечь текст в картинку';
|
||||
Dlg.cbBurn.Visible := AHasText;
|
||||
Dlg.cbBurn.OnClick := @Dlg.BurnClick;
|
||||
|
||||
Dlg.lblNote := TLabel.Create(Dlg.Form);
|
||||
Dlg.lblNote.Parent := Dlg.Form;
|
||||
Dlg.lblNote.SetBounds(12, 144, 596, 52);
|
||||
Dlg.lblNote.SetBounds(12, 164, 596, 52);
|
||||
Dlg.lblNote.Anchors := [akTop, akLeft, akRight];
|
||||
Dlg.lblNote.AutoSize := False;
|
||||
Dlg.lblNote.WordWrap := True;
|
||||
|
||||
BtnOk := TThemedButton.Create(Dlg.Form);
|
||||
BtnOk.Parent := Dlg.Form;
|
||||
BtnOk.SetBounds(400, 208, 100, 30);
|
||||
BtnOk.SetBounds(400, 230, 100, 30);
|
||||
BtnOk.Anchors := [akRight, akBottom];
|
||||
BtnOk.Caption := 'Собрать';
|
||||
BtnOk.ModalResult := mrOk;
|
||||
|
|
@ -259,7 +280,7 @@ begin
|
|||
|
||||
BtnCancel := TThemedButton.Create(Dlg.Form);
|
||||
BtnCancel.Parent := Dlg.Form;
|
||||
BtnCancel.SetBounds(508, 208, 100, 30);
|
||||
BtnCancel.SetBounds(508, 230, 100, 30);
|
||||
BtnCancel.Anchors := [akRight, akBottom];
|
||||
BtnCancel.Caption := 'Отмена';
|
||||
BtnCancel.ModalResult := mrCancel;
|
||||
|
|
@ -290,6 +311,7 @@ begin
|
|||
if AChoice.Path = '' then
|
||||
Exit;
|
||||
AChoice.BurnText := AHasText and Dlg.cbBurn.Checked;
|
||||
AChoice.RegionOnly := (ARegion <> '') and Dlg.cbRegion.Checked;
|
||||
Result := True;
|
||||
finally
|
||||
Dlg.Form.Free;
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ const
|
|||
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
|
||||
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
|
||||
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1E, $15, $14, $31, $9E,
|
||||
$20, $4F, $DD, $00, $00, $01, $3E, $49, $44, $41, $54, $38, $CB, $BD, $92, $31,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
|
||||
$2C, $48, $09, $00, $00, $01, $3E, $49, $44, $41, $54, $38, $CB, $BD, $92, $31,
|
||||
$4B, $C3, $40, $14, $C7, $7F, $97, $44, $29, $68, $03, $5A, $45, $EA, $D0, $A5,
|
||||
$6B, $41, $44, $5C, $0A, $0E, $3A, $15, $DC, $1D, $DD, $FB, $51, $74, $71, $29,
|
||||
$F8, $39, $EC, $62, $06, $17, $5D, $EA, $22, $D2, $88, $93, $E0, $22, $68, $A1,
|
||||
|
|
@ -66,8 +66,8 @@ const
|
|||
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
|
||||
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
|
||||
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1E, $15, $14, $31, $9E,
|
||||
$20, $4F, $DD, $00, $00, $01, $2F, $49, $44, $41, $54, $38, $CB, $CD, $D2, $3D,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
|
||||
$2C, $48, $09, $00, $00, $01, $2F, $49, $44, $41, $54, $38, $CB, $CD, $D2, $3D,
|
||||
$2F, $83, $51, $18, $C6, $F1, $DF, $C3, $63, $31, $34, $11, $B5, $35, $D1, $A9,
|
||||
$C1, $E0, $25, $4C, $66, $49, $F9, $16, $24, $96, $C6, $37, $A8, $18, $7D, $02,
|
||||
$31, $98, $18, $99, $25, $44, $F8, $02, $26, $B1, $30, $30, $78, $D9, $90, $18,
|
||||
|
|
@ -99,8 +99,8 @@ const
|
|||
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
|
||||
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
|
||||
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1E, $15, $14, $31, $9E,
|
||||
$20, $4F, $DD, $00, $00, $01, $0F, $49, $44, $41, $54, $38, $CB, $B5, $D3, $BF,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
|
||||
$2C, $48, $09, $00, $00, $01, $0F, $49, $44, $41, $54, $38, $CB, $B5, $D3, $BF,
|
||||
$2E, $83, $51, $18, $C7, $F1, $CF, $5B, $15, $0C, $74, $93, $88, $91, $0B, $E8,
|
||||
$68, $60, $33, $34, $B1, $1A, $F4, $1E, $0C, $66, $8B, $C5, $05, $B8, $01, $89,
|
||||
$2B, $30, $18, $24, $4C, $0D, $8B, $41, $13, $46, $62, $90, $18, $A4, $4B, $55,
|
||||
|
|
@ -130,8 +130,8 @@ const
|
|||
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
|
||||
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
|
||||
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1E, $15, $14, $31, $9E,
|
||||
$20, $4F, $DD, $00, $00, $01, $67, $49, $44, $41, $54, $38, $CB, $B5, $D3, $3D,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
|
||||
$2C, $48, $09, $00, $00, $01, $67, $49, $44, $41, $54, $38, $CB, $B5, $D3, $3D,
|
||||
$4F, $93, $61, $14, $C6, $F1, $5F, $AB, $80, $9D, $A8, $A1, $71, $95, $08, $14,
|
||||
$3F, $80, $71, $D2, $8D, $C5, $D8, $A4, $A6, $61, $31, $74, $32, $31, $1A, $99,
|
||||
$89, $B3, $8B, $71, $B0, $7C, $0F, $37, $42, $4B, $07, $07, $D0, $98, $30, $E0,
|
||||
|
|
@ -158,6 +158,59 @@ const
|
|||
$60, $82
|
||||
);
|
||||
|
||||
{ icons8-цель-30.png, 24 на 24 точек }
|
||||
IconTargetData: array[0..779] of Byte = (
|
||||
$89, $50, $4E, $47, $0D, $0A, $1A, $0A, $00, $00, $00, $0D, $49, $48, $44, $52,
|
||||
$00, $00, $00, $18, $00, $00, $00, $18, $08, $04, $00, $00, $00, $4A, $7E, $F5,
|
||||
$73, $00, $00, $00, $20, $63, $48, $52, $4D, $00, $00, $7A, $26, $00, $00, $80,
|
||||
$84, $00, $00, $FA, $00, $00, $00, $80, $E8, $00, $00, $75, $30, $00, $00, $EA,
|
||||
$60, $00, $00, $3A, $98, $00, $00, $17, $70, $9C, $BA, $51, $3C, $00, $00, $00,
|
||||
$02, $62, $4B, $47, $44, $00, $00, $AA, $8D, $23, $32, $00, $00, $00, $09, $70,
|
||||
$48, $59, $73, $00, $00, $0B, $13, $00, $00, $0B, $13, $01, $00, $9A, $9C, $18,
|
||||
$00, $00, $00, $07, $74, $49, $4D, $45, $07, $EA, $08, $1F, $07, $2E, $1B, $C7,
|
||||
$2C, $48, $09, $00, $00, $02, $71, $49, $44, $41, $54, $38, $CB, $95, $D4, $41,
|
||||
$68, $DB, $65, $18, $06, $F0, $5F, $E2, $DA, $2A, $2E, $59, $B2, $65, $29, $4B,
|
||||
$B4, $A7, $C9, $B0, $DB, $A0, $8C, $49, $63, $55, $50, $0F, $56, $31, $ED, $68,
|
||||
$BA, $CB, $70, $3B, $C8, $F0, $24, $08, $0A, $1E, $44, $99, $4E, $AA, $B2, $5D,
|
||||
$44, $86, $D9, $4D, $9B, $0E, $3C, $4D, $58, $87, $14, $75, $22, $C8, $DC, $9C,
|
||||
$2B, $78, $4A, $D9, $84, $04, $6B, $93, $B5, $34, $15, $BB, $FE, $6B, $65, $5D,
|
||||
$B5, $F1, $F0, $67, $AC, $DD, $40, $D8, $73, $FB, $E0, $7B, $78, $DF, $E7, $7D,
|
||||
$DE, $E7, $E5, $1E, $11, $B9, $EB, $9D, $B0, $43, $46, $1C, $81, $69, $57, $2D,
|
||||
$68, $AE, $FD, $B0, $01, $3C, $67, $97, $1F, $3C, $E1, $8C, $59, $AF, $3A, $20,
|
||||
$23, $26, $22, $30, $E3, $8A, $AF, $9D, $31, $77, $9B, $70, $1F, $D8, $E7, $4D,
|
||||
$3D, $0E, $DA, $62, $DC, $92, $55, $C3, $4E, $18, $71, $C1, $8A, $27, $E5, $ED,
|
||||
$35, $A9, $BE, $9E, $70, $53, $8F, $9C, $16, $BB, $B4, $29, $B9, $EC, $2F, $AB,
|
||||
$96, $D5, $7C, $67, $C4, $8A, $17, $F4, $FA, $5D, $65, $6D, $6B, $AF, $59, $D2,
|
||||
$D4, $D4, $54, $13, $D7, $6F, $D2, $8A, $9B, $AA, $4E, $D9, $2F, $29, $AF, $6C,
|
||||
$42, $4F, $A8, $37, $AC, $30, $23, $65, $B7, $28, $56, $7C, $26, $EA, $5F, $A3,
|
||||
$CE, $F9, $D3, $D3, $06, $6D, $74, $DA, $55, $03, $1E, $F1, $AD, $BF, $43, $42,
|
||||
$C4, $0D, $E3, $5A, $65, $FD, $E3, $41, $71, $A3, $C6, $FC, $EC, $8A, $F3, $C6,
|
||||
$A4, $1C, $D2, $EA, $0B, $6D, $06, $5C, $F3, $4B, $D8, $50, $D2, $3B, $9E, $F1,
|
||||
$80, $B8, $AC, $93, $16, $14, $6D, $B5, $DB, $59, $25, $DD, $D2, $4A, $1A, $06,
|
||||
$75, $A8, $38, $2D, $11, $56, $E8, $F2, $B6, $55, $97, $3D, $25, $6A, $D4, $66,
|
||||
$07, $6C, $31, $66, $D2, $CB, $FA, $9D, $F7, $A3, $7D, $36, $3A, $27, $E3, $31,
|
||||
$17, $5C, $83, $82, $79, $07, $ED, $F4, $9B, $8F, $B4, $48, $F9, $D4, $BC, $A2,
|
||||
$94, $01, $93, $86, $B5, $2B, $AA, $EA, $74, $C8, $BC, $42, $68, $5C, $4C, $4C,
|
||||
$5D, $9B, $AC, $39, $4D, $DB, $BC, $2F, $E2, $25, $81, $0F, $8C, $7B, $D6, $B2,
|
||||
$8A, $C3, $EE, $57, $17, $13, $BB, $E5, $74, $B8, $22, $4D, $4D, $31, $43, $4E,
|
||||
$38, $22, $B0, $24, $22, $72, $F7, $F2, $6C, $C0, $A2, $45, $59, $B3, $EA, $B6,
|
||||
$6B, $71, $DD, $C7, $8E, $18, $42, $C1, $5E, $DF, $6B, $B5, $5D, $CD, $0D, $3B,
|
||||
$2D, $5A, $24, $8A, $69, $33, $72, $16, $5C, $D4, $2B, $A3, $88, $4F, $7C, $6E,
|
||||
$D8, $90, $C0, $49, $DB, $F4, $BA, $E8, $BA, $9C, $E9, $50, $32, $09, $5F, $AA,
|
||||
$78, $D8, $7E, $73, $4A, $D2, $BA, $95, $D4, $D4, $0C, $EB, $96, $36, $A2, $A1,
|
||||
$A0, $43, $35, $1C, $6B, $88, $57, $34, $BC, $2B, $E9, $B8, $40, $49, $97, $B4,
|
||||
$84, $A4, $B4, $2E, $25, $81, $63, $12, $8E, $9A, $75, $F8, $B6, $92, $AD, $BE,
|
||||
$32, $25, $AF, $DD, $71, $0D, $55, $45, $AF, $7B, $43, $51, $55, $C3, $31, $ED,
|
||||
$FA, $4C, $39, $2B, $B5, $36, $36, $3D, $26, $94, $F5, $49, $1A, $74, $4A, $C5,
|
||||
$B2, $65, $15, $23, $0A, $12, $FA, $94, $95, $3D, $BE, $7E, $5E, $51, $79, $13,
|
||||
$A6, $BC, $A7, $43, $46, $A7, $3D, $F6, $E8, $94, $D1, $E1, $A8, $9A, $B2, $17,
|
||||
$45, $EF, $8C, $68, $44, $CE, $5B, $72, $02, $DF, $B8, $A4, $8E, $87, $E4, $3C,
|
||||
$6F, $93, $9F, $7C, $E8, $D2, $AD, $34, $AC, $B7, $65, $B3, $7E, $79, $8F, $CA,
|
||||
$88, $B1, $26, $A2, $7F, $FC, $DF, $11, $D8, $64, $87, $AC, $B8, $A6, $C0, $B4,
|
||||
$5F, $EF, $3C, $02, $F7, $8C, $FF, $00, $D3, $AE, $CE, $9E, $6E, $47, $CD, $DC,
|
||||
$00, $00, $00, $00, $49, $45, $4E, $44, $AE, $42, $60, $82
|
||||
);
|
||||
|
||||
{ icons8-показ-видеокадров-30.png, значок окна, 32 на 32 точек, формат ico }
|
||||
IconAppData: array[0..4285] of Byte = (
|
||||
$00, $00, $01, $00, $01, $00, $20, $20, $00, $00, $01, $00, $20, $00, $A8, $10,
|
||||
|
|
|
|||
|
|
@ -67,8 +67,19 @@ object MainForm: TMainForm
|
|||
ShowHint = True
|
||||
TabStop = False
|
||||
end
|
||||
object btnCenter: TThemedButton
|
||||
Left = 350
|
||||
Height = 28
|
||||
Top = 6
|
||||
Width = 28
|
||||
Down = True
|
||||
Icon = tiTarget
|
||||
OnClick = btnCenterClick
|
||||
ShowHint = True
|
||||
TabStop = False
|
||||
end
|
||||
object btnRipple: TThemedButton
|
||||
Left = 356
|
||||
Left = 386
|
||||
Height = 28
|
||||
Top = 6
|
||||
Width = 28
|
||||
|
|
@ -78,7 +89,7 @@ object MainForm: TMainForm
|
|||
TabStop = False
|
||||
end
|
||||
object btnRippleMenu: TThemedButton
|
||||
Left = 386
|
||||
Left = 416
|
||||
Height = 28
|
||||
Top = 6
|
||||
Width = 22
|
||||
|
|
@ -87,10 +98,10 @@ object MainForm: TMainForm
|
|||
TabStop = False
|
||||
end
|
||||
object lblStatus: TLabel
|
||||
Left = 458
|
||||
Left = 488
|
||||
Height = 20
|
||||
Top = 10
|
||||
Width = 810
|
||||
Width = 780
|
||||
Anchors = [akTop, akLeft, akRight]
|
||||
AutoSize = False
|
||||
Caption = 'Готов.'
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ type
|
|||
|
||||
btnSnap: TThemedButton;
|
||||
btnGlue: TThemedButton;
|
||||
btnCenter: TThemedButton;
|
||||
btnRipple: TThemedButton;
|
||||
btnRippleMenu: TThemedButton;
|
||||
lblPoolInfo: TLabel;
|
||||
|
|
@ -67,6 +68,7 @@ type
|
|||
splitVideo: TSplitter;
|
||||
procedure btnSnapClick(Sender: TObject);
|
||||
procedure btnGlueClick(Sender: TObject);
|
||||
procedure btnCenterClick(Sender: TObject);
|
||||
procedure btnRippleClick(Sender: TObject);
|
||||
procedure btnRippleMenuClick(Sender: TObject);
|
||||
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
|
||||
|
|
@ -97,6 +99,8 @@ type
|
|||
FLastLoadedEdl: string;
|
||||
FLastLoadedAudioEdl: string;
|
||||
{ Наложение для плеера: список кусков оверлея и окна показа. }
|
||||
{ Журнал текущей сборки: файл рядом с готовым роликом. }
|
||||
FExportLogPath: string;
|
||||
FPreviewOverlayUrl: string;
|
||||
FPreviewOverlayWindows: TOverlayWindows;
|
||||
{ Списки кусков звуковых дорожек для плеера. }
|
||||
|
|
@ -198,6 +202,8 @@ type
|
|||
procedure UpdateRippleButton;
|
||||
procedure UpdateSnapButton;
|
||||
procedure UpdateGlueButton;
|
||||
procedure UpdateCenterButton;
|
||||
procedure ToggleCenterPlayhead;
|
||||
procedure RippleModeChosen(Sender: TObject);
|
||||
procedure ToggleRipple;
|
||||
procedure BuildPoolMenu;
|
||||
|
|
@ -330,6 +336,8 @@ type
|
|||
procedure ExportDone(Sender: TObject);
|
||||
procedure ExportCancel(Sender: TObject);
|
||||
procedure ShowExportReport(const AReport: TExportReport);
|
||||
{ Строка и в общий журнал программы, и в журнал этой сборки. }
|
||||
procedure LogExport(const AText: string);
|
||||
procedure NormalizeProgress(Sender: TObject);
|
||||
procedure NormalizeLog(Sender: TObject);
|
||||
procedure NormalizeDone(Sender: TObject);
|
||||
|
|
@ -433,6 +441,7 @@ begin
|
|||
UpdateRippleButton;
|
||||
UpdateSnapButton;
|
||||
UpdateGlueButton;
|
||||
UpdateCenterButton;
|
||||
UpdateCaption;
|
||||
SetStatus('Готов. Добавьте файлы в проект.');
|
||||
|
||||
|
|
@ -790,7 +799,7 @@ procedure TMainForm.BuildJobIndicator;
|
|||
const
|
||||
JobWidth = 330;
|
||||
LogButtonWidth = 28;
|
||||
LogButtonLeft = 418;
|
||||
LogButtonLeft = 448;
|
||||
begin
|
||||
FJobLabel := TLabel.Create(Self);
|
||||
FJobLabel.Parent := pnlBottom;
|
||||
|
|
@ -2269,6 +2278,43 @@ begin
|
|||
UpdateGlueButton;
|
||||
end;
|
||||
|
||||
{ Держать ли курсор посередине при воспроизведении.
|
||||
|
||||
Включено — монтаж едет под курсором, и всегда видно, что будет дальше.
|
||||
Выключено — картинка стоит на месте, пока курсор не уйдёт за край; так
|
||||
спокойнее, когда разглядываешь конкретное место, а ролик при этом
|
||||
играет. }
|
||||
procedure TMainForm.UpdateCenterButton;
|
||||
var
|
||||
State: string;
|
||||
begin
|
||||
btnCenter.Down := FSettings.CenterPlayhead;
|
||||
if FSettings.CenterPlayhead then
|
||||
State := 'включено'
|
||||
else
|
||||
State := 'выключено';
|
||||
btnCenter.Hint := 'Курсор посередине при воспроизведении: ' + State + '.' +
|
||||
LineEnding + 'Клавиша \ переключает.';
|
||||
end;
|
||||
|
||||
procedure TMainForm.ToggleCenterPlayhead;
|
||||
begin
|
||||
FSettings.CenterPlayhead := not FSettings.CenterPlayhead;
|
||||
UpdateCenterButton;
|
||||
if FSettings.CenterPlayhead then
|
||||
begin
|
||||
FTimeline.ScrollToPlayhead(True);
|
||||
SetStatus('Курсор держится посередине монтажа.');
|
||||
end
|
||||
else
|
||||
SetStatus('Монтаж стоит на месте, курсор идёт по нему.');
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnCenterClick(Sender: TObject);
|
||||
begin
|
||||
ToggleCenterPlayhead;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ToggleRipple;
|
||||
begin
|
||||
FProject.RippleEnabled := not FProject.RippleEnabled;
|
||||
|
|
@ -2811,8 +2857,11 @@ begin
|
|||
FTimeline.Playhead := FPlayer.TimePos;
|
||||
{ Во время воспроизведения держим курсор посередине: так видно, что
|
||||
будет дальше, и не приходится гоняться за ним прокруткой. }
|
||||
{ Держать курсор посередине или дать монтажу стоять — решает человек:
|
||||
на слабой машине и на мелком масштабе постоянная прокрутка мешает
|
||||
разглядывать. }
|
||||
if not FPlayer.Paused then
|
||||
FTimeline.ScrollToPlayhead(True);
|
||||
FTimeline.ScrollToPlayhead(FSettings.CenterPlayhead);
|
||||
finally
|
||||
FUpdatingFromPlayer := False;
|
||||
end;
|
||||
|
|
@ -2890,6 +2939,15 @@ begin
|
|||
begin
|
||||
Key := 0;
|
||||
TogglePlayback;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
{ Обратная косая: пункта меню для неё нет намеренно — сочетание из
|
||||
одной такой клавиши в меню выглядит как опечатка, а работать должно. }
|
||||
if (Key = VK_OEM_5) and (Shift = []) then
|
||||
begin
|
||||
Key := 0;
|
||||
ToggleCenterPlayhead;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
|
@ -3335,7 +3393,7 @@ var
|
|||
HasPrecise: Boolean;
|
||||
T, C: Integer;
|
||||
Rebuilt, RegionFrom, RegionTo: Double;
|
||||
SubsPath, RegionCaption: string;
|
||||
SubsPath, RegionCaption, Extra: string;
|
||||
Master: TMediaInfo;
|
||||
begin
|
||||
if not FlattenProject(FProject, Segments, AudioSegs, Missing) then
|
||||
|
|
@ -3354,32 +3412,16 @@ begin
|
|||
Exit;
|
||||
end;
|
||||
|
||||
{ Отмеченный участок: от списка кусков остаётся только он. Куски,
|
||||
задетые краем, подрезаются — картинка и звук режутся по одному и тому
|
||||
же времени, иначе они разъедутся. }
|
||||
DescribeRegion(RegionFrom, RegionTo);
|
||||
RegionCaption := '';
|
||||
if FProject.HasRegion then
|
||||
begin
|
||||
RegionCaption := SecondsToStr(RegionFrom) + ' — ' + SecondsToStr(RegionTo);
|
||||
ClipSegmentsToRange(Segments, RegionFrom, RegionTo);
|
||||
ClipSegmentsToRange(AudioSegs, RegionFrom, RegionTo);
|
||||
if Length(Segments) = 0 then
|
||||
begin
|
||||
ThemedMessageDlg('nlve',
|
||||
'В отмеченном участке нечего собирать.', mtInformation, [mbOK], 0);
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Сколько материала придётся пересобрать. Наложение, переход,
|
||||
полупрозрачность и чужие параметры файла — всё это перекодирование, и
|
||||
человек должен знать цену заранее, а не обнаружить её по времени
|
||||
сборки. }
|
||||
Rebuilt := 0;
|
||||
for T := 0 to High(Segments) do
|
||||
if Segments[T].Transcode or (Length(Segments[T].Layers) > 0) then
|
||||
Rebuilt := Rebuilt + Segments[T].Duration;
|
||||
|
||||
{ Если есть хоть один точный рез, предупреждаем про контейнер: в MP4
|
||||
перекодированный кусок и скопированный уживаются не всегда, а MKV
|
||||
|
|
@ -3398,6 +3440,25 @@ begin
|
|||
Exit;
|
||||
FSettings.LastExportDir := ExtractFilePath(FExportChoice.Path);
|
||||
|
||||
{ Участок режется после вопроса: человек мог снять галку и собрать всё.
|
||||
Картинка и звук подрезаются по одному времени, иначе они разъедутся. }
|
||||
if not (FProject.HasRegion and FExportChoice.RegionOnly) then
|
||||
begin
|
||||
RegionFrom := 0;
|
||||
RegionTo := TotalDuration(Segments);
|
||||
end
|
||||
else
|
||||
begin
|
||||
ClipSegmentsToRange(Segments, RegionFrom, RegionTo);
|
||||
ClipSegmentsToRange(AudioSegs, RegionFrom, RegionTo);
|
||||
if Length(Segments) = 0 then
|
||||
begin
|
||||
ThemedMessageDlg('nlve',
|
||||
'В отмеченном участке нечего собирать.', mtInformation, [mbOK], 0);
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
if HasPrecise and
|
||||
(LowerCase(ExtractFileExt(FExportChoice.Path)) <> '.mkv') then
|
||||
if ThemedMessageDlg('nlve',
|
||||
|
|
@ -3407,6 +3468,13 @@ begin
|
|||
mtWarning, [mbYes, mbNo], 0) <> mrYes then
|
||||
Exit;
|
||||
|
||||
{ Сколько материала придётся пересобрать — считаем уже по тому списку,
|
||||
который пойдёт в сборку: с участком он короче, и обещать человеку
|
||||
работу на весь ролик было бы неправдой. }
|
||||
for T := 0 to High(Segments) do
|
||||
if Segments[T].Transcode or (Length(Segments[T].Layers) > 0) then
|
||||
Rebuilt := Rebuilt + Segments[T].Duration;
|
||||
|
||||
{ Вжигание меняет расклад: куски с текстом перестают копироваться.
|
||||
Готовим их до подсчёта того, сколько придётся пересобрать. }
|
||||
SubsPath := '';
|
||||
|
|
@ -3441,23 +3509,64 @@ begin
|
|||
mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
|
||||
Exit;
|
||||
|
||||
if FProject.HasRegion then
|
||||
Log(Format('Собирается отмеченный участок: %s — %s',
|
||||
[SecondsToStr(RegionFrom), SecondsToStr(RegionTo)]));
|
||||
Log('Сборка в ' + FExportChoice.Path);
|
||||
Log(Format('%d %s, длительность %s с',
|
||||
[Length(Segments), Plural(Length(Segments), 'кусок', 'куска', 'кусков'),
|
||||
SecondsToStr(TotalDuration(Segments))]));
|
||||
|
||||
{ Пересобираемые куски целятся в битрейт самого щедрого исходника, а не
|
||||
в то, что кодировщик выберет сам: его выбор скромнее в разы, и
|
||||
пересобранный кусок оказывается заметно хуже соседних скопированных. }
|
||||
Master := FProject.Master;
|
||||
Master.VideoBitrate := FProject.MaxVideoBitrate;
|
||||
|
||||
{ Журнал этой сборки: рядом с готовым файлом, под тем же именем.
|
||||
Прежний одноимённый убираем — иначе две сборки слиплись бы в один
|
||||
файл, и разобрать, где чья, было бы нельзя. }
|
||||
FExportLogPath := ChangeFileExt(FExportChoice.Path, '.log');
|
||||
if FileExists(FExportLogPath) then
|
||||
SysUtils.DeleteFile(FExportLogPath);
|
||||
|
||||
LogExport(Format('nlve, сборка от %s',
|
||||
[FormatDateTime('yyyy-mm-dd hh:nn:ss', Now)]));
|
||||
LogExport('Проект: ' + FProject.FileName);
|
||||
LogExport('Готовый файл: ' + FExportChoice.Path);
|
||||
if FProject.HasRegion and FExportChoice.RegionOnly then
|
||||
LogExport(Format('Собирается отмеченный участок: %s — %s',
|
||||
[SecondsToStr(RegionFrom), SecondsToStr(RegionTo)]))
|
||||
else
|
||||
LogExport('Собирается весь монтаж.');
|
||||
LogExport(Format('Кадр проекта: %dx%d, %d/%d к/с, %s, %s',
|
||||
[Master.Width, Master.Height, Master.FpsNum, Master.FpsDen,
|
||||
Master.VideoCodec, Master.PixFmt]));
|
||||
LogExport(Format('Целевой битрейт: %d кбит/с (наибольший среди файлов проекта)',
|
||||
[Master.VideoBitrate div 1000]));
|
||||
if FExportChoice.BurnText then
|
||||
LogExport('Текст вжигается в картинку.')
|
||||
else if SubsPath <> '' then
|
||||
LogExport('Текст идёт отдельной дорожкой субтитров.')
|
||||
else if HasText(FProject) then
|
||||
LogExport('ВНИМАНИЕ: текст в проекте есть, но в сборку не попадает.');
|
||||
LogExport(Format('%d %s, длительность %s с',
|
||||
[Length(Segments), Plural(Length(Segments), 'кусок', 'куска', 'кусков'),
|
||||
SecondsToStr(TotalDuration(Segments))]));
|
||||
|
||||
{ Что именно собирается — по кускам. Без этого списка по журналу нельзя
|
||||
понять, попал ли в сборку тот клип, которого потом не нашли в ролике. }
|
||||
for T := 0 to High(Segments) do
|
||||
begin
|
||||
Extra := '';
|
||||
if Length(Segments[T].Layers) > 0 then
|
||||
Extra := Extra + ', слоёв ' + IntToStr(Length(Segments[T].Layers));
|
||||
if Segments[T].SubFile <> '' then
|
||||
Extra := Extra + ', с текстом';
|
||||
if Segments[T].Transcode then
|
||||
Extra := Extra + ', пересобирается';
|
||||
LogExport(Format(' кусок %d: %s, из %s, длина %s%s',
|
||||
[T + 1, ExtractFileName(Segments[T].FilePath),
|
||||
SecondsToStr(Segments[T].SrcStart), SecondsToStr(Segments[T].Duration),
|
||||
Extra]));
|
||||
end;
|
||||
|
||||
FExport := TExportThread.Create(Segments, AudioSegs,
|
||||
FExportChoice.Path, True, Master, AProfile);
|
||||
FExport.SubtitlePath := SubsPath;
|
||||
FExport.LogPath := FExportLogPath;
|
||||
FExport.OnProgress := @ExportProgress;
|
||||
FExport.OnLog := @ExportLog;
|
||||
FExport.OnTerminate := @ExportDone;
|
||||
|
|
@ -3515,6 +3624,36 @@ begin
|
|||
FExportUi.AddLog('Файл: ' + Rep.OutputPath);
|
||||
end;
|
||||
|
||||
{ Журнал сборки лежит рядом с готовым файлом и называется так же.
|
||||
|
||||
Зачем отдельно от общего журнала программы: тот перезаписывается при
|
||||
следующем запуске, а разбираться, почему в ролике не то, приходится
|
||||
потом — иногда через неделю. Рядом с файлом журнал никуда не денется и
|
||||
не перепутается с чужим. }
|
||||
procedure TMainForm.LogExport(const AText: string);
|
||||
var
|
||||
F: TextFile;
|
||||
begin
|
||||
Log(AText);
|
||||
if FExportLogPath = '' then
|
||||
Exit;
|
||||
try
|
||||
AssignFile(F, FExportLogPath);
|
||||
if FileExists(FExportLogPath) then
|
||||
Append(F)
|
||||
else
|
||||
Rewrite(F);
|
||||
try
|
||||
WriteLn(F, AText);
|
||||
finally
|
||||
CloseFile(F);
|
||||
end;
|
||||
except
|
||||
on E: Exception do
|
||||
FExportLogPath := '';
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ShowExportReport(const AReport: TExportReport);
|
||||
var
|
||||
I, Encoded: Integer;
|
||||
|
|
@ -3522,17 +3661,17 @@ var
|
|||
begin
|
||||
if AReport.Cancelled then
|
||||
begin
|
||||
Log('Сборка отменена.');
|
||||
LogExport('Сборка отменена.');
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if not AReport.Succeeded then
|
||||
begin
|
||||
Log('Сборка не удалась: ' + AReport.Message);
|
||||
LogExport('Сборка не удалась: ' + AReport.Message);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
Log('--- Отчёт о сборке ---');
|
||||
LogExport('--- Отчёт о сборке ---');
|
||||
Encoded := 0;
|
||||
for I := 0 to High(AReport.Segments) do
|
||||
begin
|
||||
|
|
@ -3540,14 +3679,14 @@ begin
|
|||
if AReport.Segments[I].Encoded then
|
||||
begin
|
||||
Inc(Encoded);
|
||||
Log(Format(' часть %d (кусок %d, ПЕРЕКОДИРОВАНА): старт %s, длина %s, пакетов %d',
|
||||
LogExport(Format(' часть %d (кусок %d, ПЕРЕКОДИРОВАНА): старт %s, длина %s, пакетов %d',
|
||||
[I + 1, AReport.Segments[I].Index + 1,
|
||||
SecondsToStr(AReport.Segments[I].ActualStart),
|
||||
SecondsToStr(AReport.Segments[I].Duration),
|
||||
AReport.Segments[I].Packets]));
|
||||
end
|
||||
else
|
||||
Log(Format(' часть %d (кусок %d): старт %s (просили %s, сдвиг %s), длина %s, пакетов %d',
|
||||
LogExport(Format(' часть %d (кусок %d): старт %s (просили %s, сдвиг %s), длина %s, пакетов %d',
|
||||
[I + 1, AReport.Segments[I].Index + 1,
|
||||
SecondsToStr(AReport.Segments[I].ActualStart),
|
||||
SecondsToStr(AReport.Segments[I].RequestedStart),
|
||||
|
|
@ -3556,17 +3695,17 @@ begin
|
|||
AReport.Segments[I].Packets]));
|
||||
end;
|
||||
if Encoded > 0 then
|
||||
Log(Format(' перекодировано частей: %d, остальное скопировано без потерь',
|
||||
LogExport(Format(' перекодировано частей: %d, остальное скопировано без потерь',
|
||||
[Encoded]));
|
||||
|
||||
Log(Format(' сумма пакетов кусков: %d', [AReport.ExpectedPackets]));
|
||||
Log(Format(' пакетов в готовом файле: %d', [AReport.ActualPackets]));
|
||||
LogExport(Format(' сумма пакетов кусков: %d', [AReport.ExpectedPackets]));
|
||||
LogExport(Format(' пакетов в готовом файле: %d', [AReport.ActualPackets]));
|
||||
if AReport.ExpectedPackets = AReport.ActualPackets then
|
||||
Log(' СВЕРКА ПРОЙДЕНА: склейка не потеряла и не продублировала ни одного кадра.')
|
||||
LogExport(' СВЕРКА ПРОЙДЕНА: склейка не потеряла и не продублировала ни одного кадра.')
|
||||
else
|
||||
Log(Format(' РАСХОЖДЕНИЕ на %d кадров.',
|
||||
LogExport(Format(' РАСХОЖДЕНИЕ на %d кадров.',
|
||||
[AReport.ActualPackets - AReport.ExpectedPackets]));
|
||||
Log(' файл: ' + AReport.OutputPath);
|
||||
LogExport(' файл: ' + AReport.OutputPath);
|
||||
SetStatus('Готово: ' + AReport.OutputPath);
|
||||
end;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ type
|
|||
FLogVisible: Boolean;
|
||||
FPlayheadSnap: Boolean;
|
||||
FPreviewKey: Boolean;
|
||||
FCenterPlayhead: Boolean;
|
||||
FDepsReminder: Boolean;
|
||||
FRecentProjects: TStringList;
|
||||
function FileName: string;
|
||||
|
|
@ -73,6 +74,8 @@ type
|
|||
property PlayheadSnap: Boolean read FPlayheadSnap write FPlayheadSnap;
|
||||
{ Применять ли ключ и прозрачность прямо в предпросмотре. }
|
||||
property PreviewKey: Boolean read FPreviewKey write FPreviewKey;
|
||||
{ Держать ли курсор посередине монтажа при воспроизведении. }
|
||||
property CenterPlayhead: Boolean read FCenterPlayhead write FCenterPlayhead;
|
||||
{ Напоминать ли о недостающих зависимостях при запуске. Гасится
|
||||
кнопкой «Не напоминать» в самом напоминании. }
|
||||
property DepsReminder: Boolean read FDepsReminder write FDepsReminder;
|
||||
|
|
@ -107,6 +110,7 @@ begin
|
|||
FLogVisible := True;
|
||||
FPlayheadSnap := True;
|
||||
FPreviewKey := True;
|
||||
FCenterPlayhead := True;
|
||||
FDepsReminder := True;
|
||||
end;
|
||||
|
||||
|
|
@ -189,6 +193,7 @@ begin
|
|||
FLogVisible := Obj.Get('log_visible', FLogVisible);
|
||||
FPlayheadSnap := Obj.Get('playhead_snap', FPlayheadSnap);
|
||||
FPreviewKey := Obj.Get('preview_key', FPreviewKey);
|
||||
FCenterPlayhead := Obj.Get('center_playhead', FCenterPlayhead);
|
||||
FDepsReminder := Obj.Get('deps_reminder', FDepsReminder);
|
||||
|
||||
Arr := Obj.Find('recent_projects', jtArray) as TJSONArray;
|
||||
|
|
@ -242,6 +247,7 @@ begin
|
|||
Root.Add('log_visible', FLogVisible);
|
||||
Root.Add('playhead_snap', FPlayheadSnap);
|
||||
Root.Add('preview_key', FPreviewKey);
|
||||
Root.Add('center_playhead', FCenterPlayhead);
|
||||
Root.Add('deps_reminder', FDepsReminder);
|
||||
|
||||
Arr := TJSONArray.Create;
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ type
|
|||
tiSnap, // прилипание: края притягиваются к общей линии
|
||||
tiRipple, // схлопывание: дыра между кусками закрывается
|
||||
tiLog, // журнал: полоса с сообщениями
|
||||
tiGlue // курсор липнет к границам клипов
|
||||
tiGlue, // курсор липнет к границам клипов
|
||||
tiTarget // курсор держится середины монтажа
|
||||
);
|
||||
|
||||
{ Полоска выполнения, которая рисует себя сама.
|
||||
|
|
@ -453,6 +454,7 @@ begin
|
|||
tiRipple: Stream.WriteBuffer(IconRippleData[0], Length(IconRippleData));
|
||||
tiLog: Stream.WriteBuffer(IconLogData[0], Length(IconLogData));
|
||||
tiGlue: Stream.WriteBuffer(IconGlueData[0], Length(IconGlueData));
|
||||
tiTarget: Stream.WriteBuffer(IconTargetData[0], Length(IconTargetData));
|
||||
else
|
||||
Exit;
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ type
|
|||
taPan, // прокрутка средней кнопкой
|
||||
taVolume, // тянем ручку громкости клипа
|
||||
taFadeIn, // тянем появление за верхний левый угол
|
||||
taFadeOut // тянем уход за верхний правый угол
|
||||
taFadeOut, // тянем уход за верхний правый угол
|
||||
taRegionIn, // тянем начало отмеченного участка
|
||||
taRegionOut // тянем конец отмеченного участка
|
||||
);
|
||||
|
||||
{ Что находится под указателем в полосе заголовков дорожек. }
|
||||
|
|
@ -121,6 +123,8 @@ type
|
|||
FOnPlayheadChange: TNotifyEvent;
|
||||
FOnStructureChange: TNotifyEvent;
|
||||
FOnSelectionChange: TNotifyEvent;
|
||||
{ Отмеченный участок подвинули мышью. }
|
||||
FOnRegionChange: TNotifyEvent;
|
||||
FOnMediaDrop: TMediaDropEvent;
|
||||
FOnClipTransform: TClipEvent;
|
||||
FOnTrackAdded: TNotifyEvent;
|
||||
|
|
@ -144,6 +148,10 @@ type
|
|||
|
||||
procedure DrawRuler(ACanvas: TCanvas);
|
||||
procedure DrawRegion(ACanvas: TCanvas);
|
||||
procedure DrawRegionHandle(ACanvas: TCanvas; AX: Integer;
|
||||
AIsStart: Boolean);
|
||||
function RegionHandleAt(AX, AY: Integer): Integer;
|
||||
procedure DragRegionEdge(AX: Integer; AIsStart: Boolean);
|
||||
procedure DrawTracks(ACanvas: TCanvas);
|
||||
procedure DrawClip(ACanvas: TCanvas; AClip: TClip; ATop: Integer);
|
||||
procedure DrawPlayhead(ACanvas: TCanvas);
|
||||
|
|
@ -284,6 +292,8 @@ type
|
|||
write FOnStructureChange;
|
||||
property OnSelectionChange: TNotifyEvent read FOnSelectionChange
|
||||
write FOnSelectionChange;
|
||||
property OnRegionChange: TNotifyEvent read FOnRegionChange
|
||||
write FOnRegionChange;
|
||||
property OnMediaDrop: TMediaDropEvent read FOnMediaDrop write FOnMediaDrop;
|
||||
{ Нажата кнопка кадрирования в углу клипа. }
|
||||
property OnClipTransform: TClipEvent read FOnClipTransform
|
||||
|
|
@ -302,6 +312,8 @@ const
|
|||
|
||||
{ На сколько пикселей примагничиваются края при перетаскивании. }
|
||||
SnapPixels = 8;
|
||||
{ Уголок отметки участка: сторона треугольника в точках. }
|
||||
RegionHandleWidth = 9;
|
||||
|
||||
{ Сдвиг мыши, до которого нажатие считается щелчком, а не перетаскиванием.
|
||||
Совсем без запаса нельзя: рука дрожит, и попасть кнопкой в один и тот же
|
||||
|
|
@ -354,6 +366,7 @@ const
|
|||
ColorPlayhead = TColor($4040FF); // красный: привычный цвет курсора
|
||||
ColorRegion = TColor($30A0D0); // отмеченный участок: янтарная полоса
|
||||
ColorRegionEdge = TColor($1878B0);
|
||||
ColorRegionHandle = TColor($40E0F0); // уголки отметок: жёлтые, как в Vegas
|
||||
ColorEdgeSnap = TColor($909090); // серый: рез притянут к опорному кадру
|
||||
ColorEdgePrecise = TColor($FFC030); // голубой: рез потребует перекодирования
|
||||
ColorWave = TColor($96C8D2); // пастельно-жёлтый: форма звука
|
||||
|
|
@ -740,6 +753,80 @@ begin
|
|||
ACanvas.Line(L, 0, L, ClientHeight);
|
||||
if TimeToX(FProject.RegionEnd) <= ClientWidth then
|
||||
ACanvas.Line(R - 1, 0, R - 1, ClientHeight);
|
||||
|
||||
{ Уголки, за которые тянут. Отметки должны не только показывать, где
|
||||
участок, но и позволять его двигать: клавишами это выходит долго —
|
||||
поставил, посмотрел, промахнулся, ставь заново. }
|
||||
DrawRegionHandle(ACanvas, TimeToX(FProject.RegionStart), True);
|
||||
DrawRegionHandle(ACanvas, TimeToX(FProject.RegionEnd), False);
|
||||
end;
|
||||
|
||||
{ Уголок отметки: прямоугольный треугольник, прижатый к своей стороне
|
||||
участка. Так видно, какой край он держит, — у симметричной стрелки это
|
||||
пришлось бы угадывать. }
|
||||
procedure TTimelineControl.DrawRegionHandle(ACanvas: TCanvas; AX: Integer;
|
||||
AIsStart: Boolean);
|
||||
var
|
||||
Pts: array[0..2] of TPoint;
|
||||
begin
|
||||
if (AX < FHeaderWidth - RegionHandleWidth) or
|
||||
(AX > ClientWidth + RegionHandleWidth) then
|
||||
Exit;
|
||||
|
||||
if AIsStart then
|
||||
begin
|
||||
Pts[0] := Point(AX, 0);
|
||||
Pts[1] := Point(AX + RegionHandleWidth, 0);
|
||||
Pts[2] := Point(AX, RegionHandleWidth);
|
||||
end
|
||||
else
|
||||
begin
|
||||
Pts[0] := Point(AX, 0);
|
||||
Pts[1] := Point(AX - RegionHandleWidth, 0);
|
||||
Pts[2] := Point(AX, RegionHandleWidth);
|
||||
end;
|
||||
|
||||
ACanvas.Brush.Style := bsSolid;
|
||||
ACanvas.Brush.Color := ColorRegionHandle;
|
||||
ACanvas.Pen.Color := ColorRegionEdge;
|
||||
ACanvas.Polygon(Pts);
|
||||
end;
|
||||
|
||||
{ Что под указателем на линейке: 1 — уголок начала, 2 — уголок конца. }
|
||||
{ Двигает край участка за уголком.
|
||||
|
||||
Край прилипает к границам клипов тем же способом, что и курсор: участок
|
||||
чаще всего отмечают по кускам, а попасть мышью в стык на общем плане
|
||||
нельзя. Края не пропускаются друг за друга: перевёрнутый участок — это
|
||||
не участок. }
|
||||
procedure TTimelineControl.DragRegionEdge(AX: Integer; AIsStart: Boolean);
|
||||
var
|
||||
T: Double;
|
||||
begin
|
||||
if FProject = nil then
|
||||
Exit;
|
||||
T := Max(0, SnapPlayhead(XToTime(AX)));
|
||||
if AIsStart then
|
||||
FProject.RegionStart := Min(T, FProject.RegionEnd - 0.001)
|
||||
else
|
||||
FProject.RegionEnd := Max(T, FProject.RegionStart + 0.001);
|
||||
Invalidate;
|
||||
if Assigned(FOnRegionChange) then
|
||||
FOnRegionChange(Self);
|
||||
end;
|
||||
|
||||
function TTimelineControl.RegionHandleAt(AX, AY: Integer): Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
if (FProject = nil) or (not FProject.HasRegion) then
|
||||
Exit;
|
||||
if (AY < 0) or (AY > RegionHandleWidth + 2) then
|
||||
Exit;
|
||||
|
||||
if Abs(AX - TimeToX(FProject.RegionStart)) <= RegionHandleWidth then
|
||||
Exit(1);
|
||||
if Abs(AX - TimeToX(FProject.RegionEnd)) <= RegionHandleWidth then
|
||||
Exit(2);
|
||||
end;
|
||||
|
||||
procedure TTimelineControl.DrawClip(ACanvas: TCanvas; AClip: TClip;
|
||||
|
|
@ -1684,9 +1771,16 @@ begin
|
|||
Exit;
|
||||
end;
|
||||
|
||||
{ Щелчок по линейке ставит курсор воспроизведения. }
|
||||
{ Щелчок по линейке: сперва уголки отмеченного участка, потом курсор.
|
||||
|
||||
Приоритет у уголков: они мелкие, промахнуться мимо них легко, а
|
||||
поставить курсор можно где угодно рядом. }
|
||||
if Y < FRulerHeight then
|
||||
begin
|
||||
case RegionHandleAt(X, Y) of
|
||||
1: begin FAction := taRegionIn; Exit; end;
|
||||
2: begin FAction := taRegionOut; Exit; end;
|
||||
end;
|
||||
FAction := taScrub;
|
||||
Playhead := Max(0, SnapPlayhead(XToTime(X)));
|
||||
Exit;
|
||||
|
|
@ -1869,6 +1963,12 @@ begin
|
|||
на стык, соскакивал бы с него от малейшего движения мыши. }
|
||||
Playhead := Max(0, SnapPlayhead(XToTime(X)));
|
||||
|
||||
taRegionIn:
|
||||
DragRegionEdge(X, True);
|
||||
|
||||
taRegionOut:
|
||||
DragRegionEdge(X, False);
|
||||
|
||||
taVolume:
|
||||
if FDragClip <> nil then
|
||||
begin
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue