Отметки участка стали видимыми уголками на линейке — жёлтые треугольники по краям, за них можно тянуть. Клавишами это выходило долго: поставил, посмотрел, промахнулся, ставь заново. Край прилипает к границам клипов, как и курсор, и края не пропускаются друг за друга. Клавиша \ и кнопка с целью переключают, держать ли курсор посередине монтажа при воспроизведении. Раньше монтаж ехал под курсором всегда, а это мешает, когда разглядываешь конкретное место. В окне сборки вместо надписи про участок появилась галка «только отмеченный участок». Надпись легко проглядеть, а вопрос из тех, где ошибка стоит часа работы: соберёшь кусок вместо ролика или наоборот. Снятая галка собирает весь монтаж, отметки при этом остаются. Журнал сборки пишется рядом с готовым файлом и под тем же именем. Общий журнал программы перезаписывается следующим запуском, а разбираться, почему в ролике не то, приходится потом. В журнале: время, проект, участок, кадр и битрейт, что с текстом, и полный список кусков — с пометками, где слои, где текст и что пересобирается. Строка пишется сразу, чтобы после падения осталась последняя.
322 lines
12 KiB
ObjectPascal
322 lines
12 KiB
ObjectPascal
{
|
||
uExportForm — окно сборки ролика.
|
||
|
||
Здесь выбирается не только имя файла, но и то, что от него зависит:
|
||
контейнер и способ попадания текста в ролик. Разделять их нельзя —
|
||
выбор одного меняет допустимое в другом.
|
||
|
||
Почему при MP4 вжигание становится обязательным. Отдельной дорожкой
|
||
субтитров со всем оформлением умеет только Matroska. В MP4 их кладут
|
||
как mov_text, а он не хранит ни цвета, ни обводки, ни положения — от
|
||
надписи остаётся голый текст в углу, и это хуже, чем ничего. Поэтому
|
||
при MP4 галка ставится и запирается: выбора всё равно нет.
|
||
|
||
Запертой она остаётся ровно пока выбран MP4. Переключение на MKV
|
||
возвращает и галку, и прежний ответ человека — тот, что он давал до
|
||
того, как MP4 всё за него решил.
|
||
}
|
||
unit uExportForm;
|
||
|
||
{$mode objfpc}{$H+}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, Controls, Forms, Graphics, StdCtrls, Dialogs,
|
||
uTheme;
|
||
|
||
type
|
||
{ Что человек выбрал в окне. }
|
||
TExportChoice = record
|
||
Path: string;
|
||
{ Вжигать ли текст в картинку. При MP4 всегда True. }
|
||
BurnText: Boolean;
|
||
{ Собирать только отмеченный участок. Имеет смысл, только когда
|
||
участок отмечен. }
|
||
RegionOnly: Boolean;
|
||
end;
|
||
|
||
{ Показывает окно.
|
||
|
||
AHasText — есть ли в проекте надписи: без них про текст и спрашивать
|
||
незачем. APreferMkv — в монтаже есть точные резы, и MKV надёжнее.
|
||
ABurnDefault — ответ, данный в прошлый раз.
|
||
|
||
ARegion — описание отмеченного участка. Не пусто — в окне появляется
|
||
галка «только отмеченный участок». Галка, а не надпись: надпись легко
|
||
проглядеть, а тут вопрос из тех, где ошибка стоит часа работы — соберёшь
|
||
кусок вместо ролика или наоборот. }
|
||
function AskExport(var AChoice: TExportChoice; AHasText, APreferMkv: Boolean;
|
||
const ADefaultDir: string; const ARegion: string = ''): Boolean;
|
||
|
||
implementation
|
||
|
||
type
|
||
TExportDialog = class(TComponent)
|
||
public
|
||
Form: TForm;
|
||
edPath: TEdit;
|
||
BtnMp4: TThemedButton;
|
||
BtnMkv: TThemedButton;
|
||
cbBurn: TCheckBox;
|
||
cbRegion: TCheckBox;
|
||
lblNote: TLabel;
|
||
HasText: Boolean;
|
||
{ Ответ человека про вжигание, сохранённый до того, как MP4 навязал
|
||
своё. К нему возвращаемся при переходе на MKV. }
|
||
BurnWanted: Boolean;
|
||
{ Идёт ли согласование: пока идёт, ответы человека не запоминаются.
|
||
Установка галки кодом вызывает тот же обработчик, что и щелчок, и
|
||
без этого признака навязанное при MP4 значение записывалось бы как
|
||
осознанный выбор — при возврате на MKV галка так и оставалась
|
||
стоять. }
|
||
Applying: Boolean;
|
||
procedure FormatClick(Sender: TObject);
|
||
procedure BrowseClick(Sender: TObject);
|
||
procedure BurnClick(Sender: TObject);
|
||
procedure RegionClick(Sender: TObject);
|
||
function IsMp4: Boolean;
|
||
procedure ApplyFormat;
|
||
end;
|
||
|
||
function TExportDialog.IsMp4: Boolean;
|
||
begin
|
||
Result := BtnMp4.Down;
|
||
end;
|
||
|
||
{ Приводит имя файла и галку в согласие с выбранным контейнером. }
|
||
procedure TExportDialog.ApplyFormat;
|
||
var
|
||
Ext: string;
|
||
begin
|
||
Applying := True;
|
||
try
|
||
if IsMp4 then
|
||
Ext := '.mp4'
|
||
else
|
||
Ext := '.mkv';
|
||
if Trim(edPath.Text) <> '' then
|
||
edPath.Text := ChangeFileExt(edPath.Text, Ext);
|
||
|
||
if not HasText then
|
||
begin
|
||
lblNote.Caption := '';
|
||
Exit;
|
||
end;
|
||
|
||
if IsMp4 then
|
||
begin
|
||
{ Сначала запираем, потом ставим: наоборот обработчик успел бы счесть
|
||
навязанное значение осознанным. }
|
||
cbBurn.Enabled := False;
|
||
cbBurn.Checked := True;
|
||
lblNote.Caption := 'В MP4 отдельная дорожка субтитров теряет цвет, ' +
|
||
'обводку и положение, поэтому текст вжигается. Участки с текстом ' +
|
||
'придётся пересобрать.';
|
||
end
|
||
else
|
||
begin
|
||
cbBurn.Enabled := True;
|
||
cbBurn.Checked := BurnWanted;
|
||
if cbBurn.Checked then
|
||
lblNote.Caption := 'Участки с текстом придётся пересобрать. ' +
|
||
'Текст будет виден в любом проигрывателе.'
|
||
else
|
||
lblNote.Caption := 'Текст ляжет отдельной дорожкой субтитров: ' +
|
||
'сборка останется мгновенной, но показывать его будет не всякий ' +
|
||
'проигрыватель.';
|
||
end;
|
||
finally
|
||
Applying := False;
|
||
end;
|
||
end;
|
||
|
||
procedure TExportDialog.FormatClick(Sender: TObject);
|
||
begin
|
||
BtnMp4.Down := Sender = BtnMp4;
|
||
BtnMkv.Down := Sender = BtnMkv;
|
||
ApplyFormat;
|
||
end;
|
||
|
||
procedure TExportDialog.BurnClick(Sender: TObject);
|
||
begin
|
||
if Applying then
|
||
Exit;
|
||
{ Запоминаем только осознанный ответ: при MP4 галка стоит не по воле
|
||
человека, и запоминать её значило бы соврать ему в следующий раз. }
|
||
if cbBurn.Enabled then
|
||
BurnWanted := cbBurn.Checked;
|
||
ApplyFormat;
|
||
end;
|
||
|
||
{ Галка участка ничего не пересчитывает: она только запоминает ответ.
|
||
Всё, что от неё зависит, решается уже после закрытия окна. }
|
||
procedure TExportDialog.RegionClick(Sender: TObject);
|
||
begin
|
||
end;
|
||
|
||
procedure TExportDialog.BrowseClick(Sender: TObject);
|
||
var
|
||
Dlg: TSaveDialog;
|
||
begin
|
||
Dlg := TSaveDialog.Create(nil);
|
||
try
|
||
Dlg.Title := 'Куда собрать ролик';
|
||
Dlg.Options := Dlg.Options + [ofOverwritePrompt, ofPathMustExist];
|
||
if IsMp4 then
|
||
begin
|
||
Dlg.DefaultExt := '.mp4';
|
||
Dlg.Filter := 'MP4|*.mp4';
|
||
end
|
||
else
|
||
begin
|
||
Dlg.DefaultExt := '.mkv';
|
||
Dlg.Filter := 'Matroska|*.mkv';
|
||
end;
|
||
Dlg.FileName := edPath.Text;
|
||
if Dlg.Execute then
|
||
edPath.Text := Dlg.FileName;
|
||
finally
|
||
Dlg.Free;
|
||
end;
|
||
end;
|
||
|
||
function AskExport(var AChoice: TExportChoice; AHasText, APreferMkv: Boolean;
|
||
const ADefaultDir: string; const ARegion: string): Boolean;
|
||
var
|
||
Dlg: TExportDialog;
|
||
Lbl: TLabel;
|
||
BtnBrowse, BtnOk, BtnCancel: TThemedButton;
|
||
Dir: string;
|
||
begin
|
||
Result := False;
|
||
Dlg := TExportDialog.Create(nil);
|
||
try
|
||
Dlg.HasText := AHasText;
|
||
Dlg.BurnWanted := AChoice.BurnText;
|
||
|
||
Dlg.Form := TForm.CreateNew(nil);
|
||
Dlg.Form.Caption := 'Сборка ролика';
|
||
Dlg.Form.Position := poScreenCenter;
|
||
Dlg.Form.ClientWidth := 620;
|
||
Dlg.Form.ClientHeight := 272;
|
||
|
||
Lbl := TLabel.Create(Dlg.Form);
|
||
Lbl.Parent := Dlg.Form;
|
||
Lbl.SetBounds(12, 12, 200, 20);
|
||
Lbl.Caption := 'Куда собрать';
|
||
|
||
Dlg.edPath := TEdit.Create(Dlg.Form);
|
||
Dlg.edPath.Parent := Dlg.Form;
|
||
Dlg.edPath.SetBounds(12, 34, 490, 26);
|
||
Dlg.edPath.Anchors := [akTop, akLeft, akRight];
|
||
|
||
BtnBrowse := TThemedButton.Create(Dlg.Form);
|
||
BtnBrowse.Parent := Dlg.Form;
|
||
BtnBrowse.SetBounds(510, 33, 96, 28);
|
||
BtnBrowse.Anchors := [akTop, akRight];
|
||
BtnBrowse.Caption := 'Обзор...';
|
||
BtnBrowse.OnClick := @Dlg.BrowseClick;
|
||
|
||
Lbl := TLabel.Create(Dlg.Form);
|
||
Lbl.Parent := Dlg.Form;
|
||
Lbl.SetBounds(12, 76, 100, 20);
|
||
Lbl.Caption := 'Контейнер';
|
||
|
||
Dlg.BtnMp4 := TThemedButton.Create(Dlg.Form);
|
||
Dlg.BtnMp4.Parent := Dlg.Form;
|
||
Dlg.BtnMp4.SetBounds(110, 72, 90, 28);
|
||
Dlg.BtnMp4.Caption := 'MP4';
|
||
Dlg.BtnMp4.TabStop := False;
|
||
Dlg.BtnMp4.OnClick := @Dlg.FormatClick;
|
||
|
||
Dlg.BtnMkv := TThemedButton.Create(Dlg.Form);
|
||
Dlg.BtnMkv.Parent := Dlg.Form;
|
||
Dlg.BtnMkv.SetBounds(206, 72, 90, 28);
|
||
Dlg.BtnMkv.Caption := 'MKV';
|
||
Dlg.BtnMkv.TabStop := False;
|
||
Dlg.BtnMkv.OnClick := @Dlg.FormatClick;
|
||
|
||
if ARegion <> '' then
|
||
begin
|
||
Lbl := TLabel.Create(Dlg.Form);
|
||
Lbl.Parent := Dlg.Form;
|
||
Lbl.SetBounds(310, 76, 300, 20);
|
||
Lbl.Anchors := [akTop, akLeft, akRight];
|
||
Lbl.Caption := 'Только участок: ' + ARegion;
|
||
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, 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, 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, 230, 100, 30);
|
||
BtnOk.Anchors := [akRight, akBottom];
|
||
BtnOk.Caption := 'Собрать';
|
||
BtnOk.ModalResult := mrOk;
|
||
BtnOk.Default := True;
|
||
|
||
BtnCancel := TThemedButton.Create(Dlg.Form);
|
||
BtnCancel.Parent := Dlg.Form;
|
||
BtnCancel.SetBounds(508, 230, 100, 30);
|
||
BtnCancel.Anchors := [akRight, akBottom];
|
||
BtnCancel.Caption := 'Отмена';
|
||
BtnCancel.ModalResult := mrCancel;
|
||
BtnCancel.Cancel := True;
|
||
|
||
ApplyDarkTheme(Dlg.Form);
|
||
|
||
{ Начальное имя: где были в прошлый раз, с расширением по контейнеру. }
|
||
Dir := ADefaultDir;
|
||
if Dir = '' then
|
||
Dir := GetCurrentDir;
|
||
if AChoice.Path <> '' then
|
||
Dlg.edPath.Text := AChoice.Path
|
||
else
|
||
Dlg.edPath.Text := IncludeTrailingPathDelimiter(Dir) + 'ролик.mkv';
|
||
|
||
{ Точные резы просят Matroska: в MP4 перекодированный стык может
|
||
воспроизводиться неправильно. }
|
||
Dlg.BtnMkv.Down := APreferMkv or
|
||
(LowerCase(ExtractFileExt(Dlg.edPath.Text)) = '.mkv');
|
||
Dlg.BtnMp4.Down := not Dlg.BtnMkv.Down;
|
||
Dlg.ApplyFormat;
|
||
|
||
if Dlg.Form.ShowModal <> mrOk then
|
||
Exit;
|
||
|
||
AChoice.Path := Trim(Dlg.edPath.Text);
|
||
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;
|
||
Dlg.Free;
|
||
end;
|
||
end;
|
||
|
||
end.
|