1 #include "common/common_pch.h"
2 
3 #include <QFileInfo>
4 #include <QMenu>
5 #include <QStringList>
6 
7 #include "common/bcp47.h"
8 #include "common/bitvalue.h"
9 #include "common/qt.h"
10 #include "mkvtoolnix-gui/forms/merge/tab.h"
11 #include "mkvtoolnix-gui/chapter_editor/tool.h"
12 #include "mkvtoolnix-gui/main_window/main_window.h"
13 #include "mkvtoolnix-gui/main_window/select_character_set_dialog.h"
14 #include "mkvtoolnix-gui/merge/additional_command_line_options_dialog.h"
15 #include "mkvtoolnix-gui/merge/tab.h"
16 #include "mkvtoolnix-gui/merge/tab_p.h"
17 #include "mkvtoolnix-gui/util/file.h"
18 #include "mkvtoolnix-gui/util/language_display_widget.h"
19 #include "mkvtoolnix-gui/util/message_box.h"
20 #include "mkvtoolnix-gui/util/settings.h"
21 #include "mkvtoolnix-gui/util/widget.h"
22 
23 namespace mtx::gui::Merge {
24 
25 using namespace mtx::gui;
26 
27 void
setupOutputControls()28 Tab::setupOutputControls() {
29   auto &p   = *p_func();
30   auto &cfg = Util::Settings::get();
31 
32   cfg.handleSplitterSizes(p.ui->mergeOutputSplitter);
33 
34   for (auto idx = 0; idx < 8; ++idx)
35     p.ui->splitMode->addItem(QString{}, idx);
36 
37   setupOutputFileControls();
38 
39   p.ui->chapterLanguage->enableClearingLanguage(true);
40   p.ui->chapterCharacterSetPreview->setEnabled(false);
41 
42   p.splitControls << p.ui->splitOptions << p.ui->splitOptionsLabel << p.ui->splitMaxFilesLabel << p.ui->splitMaxFiles << p.ui->linkFiles;
43 
44   auto comboBoxControls = QList<QComboBox *>{} << p.ui->splitMode << p.ui->chapterCharacterSet << p.ui->chapterGenerationMode;
45   for (auto const &control : comboBoxControls) {
46     control->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
47     Util::fixComboBoxViewWidth(*control);
48   }
49 
50   p.ui->splitOptions->lineEdit()->setClearButtonEnabled(true);
51   p.ui->splitMaxFiles->setMaximum(std::numeric_limits<int>::max());
52 
53   onSplitModeChanged(MuxConfig::DoNotSplit);
54   onChaptersChanged({});
55   onChapterGenerationModeChanged();
56 
57 #if !defined(HAVE_DVDREAD)
58   p.ui->chapterTitleNumberLabel->setVisible(false);
59   p.ui->chapterTitleNumber     ->setVisible(false);
60 #endif  // !defined(HAVE_DVDREAD)
61 
62   connect(MainWindow::get(),                 &MainWindow::preferencesChanged,                                                                  this, &Tab::setupOutputFileControls);
63 
64   connect(p.ui->additionalOptions,             &QLineEdit::textChanged,                                                                          this, &Tab::onAdditionalOptionsChanged);
65   connect(p.ui->browseChapters,                &QPushButton::clicked,                                                                            this, &Tab::onBrowseChapters);
66   connect(p.ui->browseGlobalTags,              &QPushButton::clicked,                                                                            this, &Tab::onBrowseGlobalTags);
67   connect(p.ui->browseNextSegmentUID,          &QPushButton::clicked,                                                                            this, &Tab::onBrowseNextSegmentUID);
68   connect(p.ui->browseOutput,                  &QPushButton::clicked,                                                                            this, &Tab::onBrowseOutput);
69   connect(p.ui->browsePreviousSegmentUID,      &QPushButton::clicked,                                                                            this, &Tab::onBrowsePreviousSegmentUID);
70   connect(p.ui->browseSegmentInfo,             &QPushButton::clicked,                                                                            this, &Tab::onBrowseSegmentInfo);
71   connect(p.ui->browseSegmentUID,              &QPushButton::clicked,                                                                            this, &Tab::onBrowseSegmentUID);
72   connect(p.ui->chapterCharacterSet,           &QComboBox::currentTextChanged,                                                                   this, &Tab::onChapterCharacterSetChanged);
73   connect(p.ui->chapterCharacterSetPreview,    &QPushButton::clicked,                                                                            this, &Tab::onPreviewChapterCharacterSet);
74   connect(p.ui->chapterDelay,                  &QLineEdit::textChanged,                                                                          this, &Tab::onChapterDelayChanged);
75   connect(p.ui->chapterStretchBy,              &QLineEdit::textChanged,                                                                          this, &Tab::onChapterStretchByChanged);
76   connect(p.ui->chapterCueNameFormat,          &QLineEdit::textChanged,                                                                          this, &Tab::onChapterCueNameFormatChanged);
77   connect(p.ui->chapterLanguage,               &Util::LanguageDisplayWidget::languageChanged,                                                    this, &Tab::onChapterLanguageChanged);
78   connect(p.ui->chapters,                      &QLineEdit::textChanged,                                                                          this, &Tab::onChaptersChanged);
79   connect(p.ui->globalTags,                    &QLineEdit::textChanged,                                                                          this, &Tab::onGlobalTagsChanged);
80   connect(p.ui->linkFiles,                     &QPushButton::clicked,                                                                            this, &Tab::onLinkFilesClicked);
81   connect(p.ui->nextSegmentUID,                &QLineEdit::textChanged,                                                                          this, &Tab::onNextSegmentUIDChanged);
82   connect(p.ui->output,                        &QLineEdit::textChanged,                                                                          this, &Tab::setDestination);
83   connect(p.ui->outputRecentlyUsed,            &QPushButton::clicked,                                                                            this, &Tab::showRecentlyUsedOutputDirs);
84   connect(p.ui->previousSegmentUID,            &QLineEdit::textChanged,                                                                          this, &Tab::onPreviousSegmentUIDChanged);
85   connect(p.ui->segmentInfo,                   &QLineEdit::textChanged,                                                                          this, &Tab::onSegmentInfoChanged);
86   connect(p.ui->segmentUIDs,                   &QLineEdit::textChanged,                                                                          this, &Tab::onSegmentUIDsChanged);
87   connect(p.ui->splitMaxFiles,                 static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),                                    this, &Tab::onSplitMaxFilesChanged);
88   connect(p.ui->splitMode,                     static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),                           this, &Tab::onSplitModeChanged);
89   connect(p.ui->splitOptions,                  &QComboBox::editTextChanged,                                                                      this, &Tab::onSplitOptionsChanged);
90   connect(p.ui->title,                         &QLineEdit::textChanged,                                                                          this, &Tab::onTitleChanged);
91   connect(p.ui->webmMode,                      &QPushButton::clicked,                                                                            this, &Tab::onWebmClicked);
92   connect(p.ui->chapterGenerationMode,         static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),                           this, &Tab::onChapterGenerationModeChanged);
93   connect(p.ui->chapterGenerationNameTemplate, &QLineEdit::textChanged,                                                                          this, &Tab::onChapterGenerationNameTemplateChanged);
94   connect(p.ui->chapterGenerationInterval,     &QLineEdit::textChanged,                                                                          this, &Tab::onChapterGenerationIntervalChanged);
95   connect(p.ui->chapterTitleNumber,            static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),                                    this, &Tab::onChapterTitleNumberChanged);
96 }
97 
98 void
setupOutputFileControls()99 Tab::setupOutputFileControls() {
100   if (Util::Settings::get().m_mergeAlwaysShowOutputFileControls)
101     moveOutputFileNameToGlobal();
102   else
103     moveOutputFileNameToOutputTab();
104 }
105 
106 void
moveOutputFileNameToGlobal()107 Tab::moveOutputFileNameToGlobal() {
108   auto &p = *p_func();
109 
110   if (!p.ui->hlOutput)
111     return;
112 
113   auto widgets = QList<QWidget *>{} << p.ui->outputLabel << p.ui->output << p.ui->browseOutput << p.ui->outputRecentlyUsed;
114 
115   for (auto const &widget : widgets) {
116     widget->setParent(p.ui->gbOutputFile);
117     widget->show();
118   }
119 
120   delete p.ui->hlOutput;
121   p.ui->hlOutput = nullptr;
122 
123   delete p.ui->gbOutputFile->layout();
124   auto layout = new QHBoxLayout{p.ui->gbOutputFile};
125   layout->setSpacing(6);
126   layout->setContentsMargins(6, 6, 6, 6);
127 
128   for (auto const &widget : widgets)
129     layout->addWidget(widget);
130 
131   p.ui->gbOutputFile->show();
132 }
133 
134 void
moveOutputFileNameToOutputTab()135 Tab::moveOutputFileNameToOutputTab() {
136   auto &p = *p_func();
137 
138   p.ui->gbOutputFile->hide();
139 
140   if (p.ui->hlOutput)
141     return;
142 
143   p.ui->gbOutputFile->hide();
144 
145   auto widgets = QList<QWidget *>{} << p.ui->outputLabel << p.ui->output << p.ui->browseOutput << p.ui->outputRecentlyUsed;
146 
147   for (auto const &widget : widgets) {
148     widget->setParent(p.ui->generalBox);
149     widget->show();
150   }
151 
152   p.ui->hlOutput = new QHBoxLayout{p.ui->gbOutputFile};
153   p.ui->hlOutput->setSpacing(6);
154   p.ui->hlOutput->addWidget(p.ui->output);
155   p.ui->hlOutput->addWidget(p.ui->browseOutput);
156   p.ui->hlOutput->addWidget(p.ui->outputRecentlyUsed);
157 
158   p.ui->generalGridLayout->addWidget(p.ui->outputLabel, 1, 0, 1, 1);
159   p.ui->generalGridLayout->addLayout(p.ui->hlOutput,    1, 1, 1, 1);
160 }
161 
162 void
retranslateOutputUI()163 Tab::retranslateOutputUI() {
164   auto &p = *p_func();
165 
166   Util::setComboBoxTexts(p.ui->splitMode,
167                          QStringList{} << QY("Do not split")                 << QY("After output size")                     << QY("After output duration")     << QY("After specific timestamps")
168                                        << QY("By parts based on timestamps") << QY("By parts based on frame/field numbers") << QY("After frame/field numbers") << QY("Before chapters"));
169 
170   p.ui->chapterLanguage->retranslateUi();
171 
172   setupOutputToolTips();
173   setupSplitModeLabelAndToolTips();
174 }
175 
176 void
setupOutputToolTips()177 Tab::setupOutputToolTips() {
178   auto &p = *p_func();
179 
180   Util::setToolTip(p.ui->title, QY("This is the title that players may show as the 'main title' for this movie."));
181   Util::setToolTip(p.ui->splitMode,
182                    Q("%1 %2")
183                    .arg(QY("Enables splitting of the output into more than one file."))
184                    .arg(QY("You can split based on the amount of time passed, based on timestamps, on frame/field numbers or on chapter numbers.")));
185   Util::setToolTip(p.ui->splitMaxFiles,
186                    Q("<p>%1 %2</p><p>%3</p>")
187                    .arg(QYH("The maximum number of files that will be created even if the last file might contain more bytes/time than wanted."))
188                    .arg(QYH("Useful e.g. when you want exactly two files."))
189                    .arg(QYH("If you leave this empty then there is no limit for the number of files mkvmerge might create.")));
190   Util::setToolTip(p.ui->linkFiles,
191                    Q("%1 %2")
192                    .arg(QY("Use 'segment linking' for the resulting files."))
193                    .arg(QY("For an in-depth explanantion of file/segment linking and this feature please read mkvmerge's documentation.")));
194   Util::setToolTip(p.ui->segmentUIDs,
195                    Q("<p>%1 %2</p><p>%3 %4 %5</p>")
196                    .arg(QYH("Sets the segment UIDs to use."))
197                    .arg(QYH("This is a comma-separated list of 128-bit segment UIDs in the usual UID form: hex numbers with or without the \"0x\" prefix, with or without spaces, exactly 32 digits."))
198                    .arg(QYH("Each file created contains one segment, and each segment has one segment UID."))
199                    .arg(QYH("If more segment UIDs are specified than segments are created then the surplus UIDs are ignored."))
200                    .arg(QYH("If fewer UIDs are specified than segments are created then random UIDs will be created for them.")));
201   Util::setToolTip(p.ui->previousSegmentUID, QY("For an in-depth explanantion of file/segment linking and this feature please read mkvmerge's documentation."));
202   Util::setToolTip(p.ui->nextSegmentUID,     QY("For an in-depth explanantion of file/segment linking and this feature please read mkvmerge's documentation."));
203   Util::setToolTip(p.ui->chapters,           QY("mkvmerge supports two chapter formats: The OGM like text format and the full featured XML format."));
204   Util::setToolTip(p.ui->browseChapters,     QY("mkvmerge supports two chapter formats: The OGM like text format and the full featured XML format."));
205   Util::setToolTip(p.ui->browseSegmentUID,         QY("Select an existing Matroska or WebM file and the GUI will add its segment UID to the input field on the left."));
206   Util::setToolTip(p.ui->browseNextSegmentUID,     QY("Select an existing Matroska or WebM file and the GUI will add its segment UID to the input field on the left."));
207   Util::setToolTip(p.ui->browsePreviousSegmentUID, QY("Select an existing Matroska or WebM file and the GUI will add its segment UID to the input field on the left."));
208   Util::setToolTip(p.ui->chapterLanguage,          Q("<p>%1 %2 %3</p><p>%4</p>")
209                                                    .arg(QYH("mkvmerge supports two chapter formats: The OGM like text format and the full featured XML format."))
210                                                    .arg(QYH("This option specifies the language to be associated with chapters if the OGM chapter format is used."))
211                                                    .arg(QYH("It is ignored for XML chapter files."))
212                                                    .arg(QYH("The language set here is also used when chapters are generated.")));
213   Util::setToolTip(p.ui->chapterCharacterSet,
214                    Q("%1 %2 %3")
215                    .arg(QY("mkvmerge supports two chapter formats: The OGM like text format and the full featured XML format."))
216                    .arg(QY("If the OGM format is used and the file's character set is not recognized correctly then this option can be used to correct that."))
217                    .arg(QY("It is ignored for XML chapter files.")));
218   Util::setToolTip(p.ui->chapterCueNameFormat,
219                    Q("<p>%1 %2 %3 %4</p><p>%5</p>")
220                    .arg(QYH("mkvmerge can read cue sheets for audio CDs and automatically convert them to chapters."))
221                    .arg(QYH("This option controls how the chapter names are created."))
222                    .arg(QYH("The sequence '%p' is replaced by the track's PERFORMER, the sequence '%t' by the track's TITLE, '%n' by the track's number and '%N' by the track's number padded with a leading 0 for track numbers < 10."))
223                    .arg(QYH("The rest is copied as is."))
224                    .arg(QYH("If nothing is entered then '%p - %t' will be used.")));
225   Util::setToolTip(p.ui->chapterDelay, QY("Delay the chapters' timestamps by a couple of ms."));
226   Util::setToolTip(p.ui->chapterStretchBy,
227                    Q("%1 %2")
228                    .arg(QYH("Multiply the chapters' timestamps with a factor."))
229                    .arg(QYH("The value can be given either as a floating point number (e.g. 12.345) or a fraction of numbers (e.g. 123/456.78).")));
230   Util::setToolTip(p.ui->chapterGenerationMode,
231                    Q("<p>%1 %2</p><ol><li>%3</li><li>%4</li></ol><p>%5</p>")
232                    .arg(QYH("mkvmerge can generate chapters automatically."))
233                    .arg(QYH("The following modes are supported:"))
234                    .arg(QYH("When appending: one chapter is created at the start and one whenever a file is appended."))
235                    .arg(QYH("In fixed intervals: chapters are created in fixed intervals, e.g. every 30 seconds."))
236                    .arg(QYH("The language for the newly created chapters is set via the chapter language control above.")));
237   Util::setToolTip(p.ui->chapterGenerationNameTemplate, ChapterEditor::Tool::chapterNameTemplateToolTip());
238   Util::setToolTip(p.ui->chapterGenerationInterval, QY("The format is either the form 'HH:MM:SS.nnnnnnnnn' or a number followed by one of the units 's', 'ms' or 'us'."));
239   Util::setToolTip(p.ui->webmMode,
240                    Q("<p>%1 %2</p><p>%3 %4 %5</p><p>%6<p>")
241                    .arg(QYH("Create a WebM compliant file."))
242                    .arg(QYH("mkvmerge also turns this on if the destination file name's extension is \"webm\"."))
243                    .arg(QYH("This mode enforces several restrictions."))
244                    .arg(QYH("The only allowed codecs are VP8/VP9 video and Vorbis/Opus audio tracks."))
245                    .arg(QYH("Tags are allowed, but chapters are not."))
246                    .arg(QYH("The DocType header item is changed to \"webm\".")));
247   Util::setToolTip(p.ui->additionalOptions,     QY("Any option given here will be added at the end of the mkvmerge command line."));
248   Util::setToolTip(p.ui->editAdditionalOptions, QY("Any option given here will be added at the end of the mkvmerge command line."));
249 }
250 
251 void
setupSplitModeLabelAndToolTips()252 Tab::setupSplitModeLabelAndToolTips() {
253   auto &p = *p_func();
254 
255   auto tooltip = QStringList{};
256   auto entries = QStringList{};
257   auto label   = QString{};
258 
259   if (MuxConfig::SplitAfterSize == p.config.m_splitMode) {
260     label    = QY("Size:");
261     tooltip << QY("The size after which a new destination file is started.")
262             << QY("The letters 'G', 'M' and 'K' can be used to indicate giga/mega/kilo bytes respectively.")
263             << QY("All units are based on 1024 (G = 1024^3, M = 1024^2, K = 1024).");
264     entries << Q("");
265     entries += Util::Settings::get().m_mergePredefinedSplitSizes;
266 
267   } else if (MuxConfig::SplitAfterDuration == p.config.m_splitMode) {
268     label    = QY("Duration:");
269     tooltip << QY("The duration after which a new destination file is started.")
270             << (Q("%1 %2 %3")
271                 .arg(QY("The format is either the form 'HH:MM:SS.nnnnnnnnn' or a number followed by one of the units 's', 'ms' or 'us'."))
272                 .arg(QY("You may omit the number of hours 'HH' and the number of nanoseconds 'nnnnnnnnn'."))
273                 .arg(QY("If given then you may use up to nine digits after the decimal point.")))
274             << QY("Examples: 01:00:00 (after one hour) or 1800s (after 1800 seconds).");
275     entries << Q("");
276     entries += Util::Settings::get().m_mergePredefinedSplitDurations;
277 
278   } else if (MuxConfig::SplitAfterTimestamps == p.config.m_splitMode) {
279     label    = QY("Timestamps:");
280     tooltip << (Q("%1 %2")
281                 .arg(QY("The timestamps after which a new destination file is started."))
282                 .arg(QY("The timestamps refer to the whole stream and not to each individual destination file.")))
283             << (Q("%1 %2 %3")
284                 .arg(QY("The format is either the form 'HH:MM:SS.nnnnnnnnn' or a number followed by one of the units 's', 'ms' or 'us'."))
285                 .arg(QY("You may omit the number of hours 'HH'."))
286                 .arg(QY("You can specify up to nine digits for the number of nanoseconds 'nnnnnnnnn' or none at all.")))
287             << (Q("%1 %2")
288                 .arg(QY("If two or more timestamps are used then you have to separate them with commas."))
289                 .arg(QY("The formats can be mixed, too.")))
290             << QY("Examples: 01:00:00,01:30:00 (after one hour and after one hour and thirty minutes) or 180s,300s,00:10:00 (after three, five and ten minutes).");
291 
292   } else if (MuxConfig::SplitByParts == p.config.m_splitMode) {
293     label    = QY("Parts:");
294     tooltip << QY("A comma-separated list of timestamp ranges of content to keep.")
295             << (Q("%1 %2")
296                 .arg(QY("Each range consists of a start and end timestamp with a '-' in the middle, e.g. '00:01:15-00:03:20'."))
297                 .arg(QY("If a start timestamp is left out then the previous range's end timestamp is used, or the start of the file if there was no previous range.")))
298             << QY("The format is either the form 'HH:MM:SS.nnnnnnnnn' or a number followed by one of the units 's', 'ms' or 'us'.")
299             << QY("If a range's start timestamp is prefixed with '+' then its content will be written to the same file as the previous range. Otherwise a new file will be created for this range.");
300 
301   } else if (MuxConfig::SplitByPartsFrames == p.config.m_splitMode) {
302     label    = QY("Parts:");
303     tooltip << (Q("%1 %2 %3")
304                 .arg(QY("A comma-separated list of frame/field number ranges of content to keep."))
305                 .arg(QY("Each range consists of a start and end frame/field number with a '-' in the middle, e.g. '157-238'."))
306                 .arg(QY("The numbering starts at 1.")))
307             << (Q("%1 %2")
308                 .arg(QY("This mode considers only the first video track that is output."))
309                 .arg(QY("If no video track is output no splitting will occur.")))
310             << (Q("%1 %2 %3")
311                 .arg(QY("The numbers given with this argument are interpreted based on the number of Matroska blocks that are output."))
312                 .arg(QY("A single Matroska block contains either a full frame (for progressive content) or a single field (for interlaced content)."))
313                 .arg(QY("mkvmerge does not distinguish between those two and simply counts the number of blocks.")))
314             << (Q("%1 %2")
315                 .arg(QY("If a start number is left out then the previous range's end number is used, or the start of the file if there was no previous range."))
316                 .arg(QY("If a range's start number is prefixed with '+' then its content will be written to the same file as the previous range. Otherwise a new file will be created for this range.")));
317 
318   } else if (MuxConfig::SplitByFrames == p.config.m_splitMode) {
319     label    = QY("Frames/fields:");
320     tooltip << (Q("%1 %2")
321                 .arg(QY("A comma-separated list of frame/field numbers after which to split."))
322                 .arg(QY("The numbering starts at 1.")))
323             << (Q("%1 %2")
324                 .arg(QY("This mode considers only the first video track that is output."))
325                 .arg(QY("If no video track is output no splitting will occur.")))
326             << (Q("%1 %2 %3")
327                 .arg(QY("The numbers given with this argument are interpreted based on the number of Matroska blocks that are output."))
328                 .arg(QY("A single Matroska block contains either a full frame (for progressive content) or a single field (for interlaced content)."))
329                 .arg(QY("mkvmerge does not distinguish between those two and simply counts the number of blocks.")));
330 
331   } else if (MuxConfig::SplitAfterChapters == p.config.m_splitMode) {
332     label    = QY("Chapter numbers:");
333     tooltip << (Q("%1 %2")
334                 .arg(QY("Either the word 'all' which selects all chapters or a comma-separated list of chapter numbers before which to split."))
335                 .arg(QY("The numbering starts at 1.")))
336             << QY("Splitting will occur right before the first key frame whose timestamp is equal to or bigger than the start timestamp for the chapters whose numbers are listed.")
337             << (Q("%1 %2")
338                 .arg(QY("A chapter starting at 0s is never considered for splitting and discarded silently."))
339                 .arg(QY("This mode only considers the top-most level of chapters across all edition entries.")));
340 
341   } else
342     label    = QY("Options:");
343 
344   p.ui->splitOptionsLabel->setText(label);
345 
346   if (MuxConfig::DoNotSplit == p.config.m_splitMode) {
347     p.ui->splitOptions->setToolTip({});
348     return;
349   }
350 
351   auto options = p.ui->splitOptions->currentText();
352 
353   p.ui->splitOptions->clear();
354   p.ui->splitOptions->addItems(entries);
355   p.ui->splitOptions->setCurrentText(options);
356 
357   for (auto &oneTooltip : tooltip)
358     oneTooltip = oneTooltip.toHtmlEscaped();
359 
360   Util::setToolTip(p.ui->splitOptions, Q("<p>%1</p>").arg(tooltip.join(Q("</p><p>"))));
361 }
362 
363 void
onTitleChanged(QString newValue)364 Tab::onTitleChanged(QString newValue) {
365   p_func()->config.m_title = newValue;
366 }
367 
368 void
setDestination(QString const & newValue)369 Tab::setDestination(QString const &newValue) {
370   auto &p = *p_func();
371 
372   if (newValue.isEmpty()) {
373     p.config.m_destination.clear();
374     Q_EMIT titleChanged();
375     return;
376   }
377 
378 #if defined(SYS_WINDOWS)
379   if (!newValue.contains(QRegularExpression{Q(R"(^[a-zA-Z]:[\\/]|^(?:\\\\|//).+[\\/].+)")}))
380     return;
381 #endif
382 
383   p.config.m_destination = QDir::toNativeSeparators(Util::removeInvalidPathCharacters(newValue));
384   if (!p.config.m_destination.isEmpty()) {
385     auto &settings           = Util::Settings::get();
386     settings.m_lastOutputDir = QFileInfo{ newValue }.absoluteDir();
387   }
388 
389   Q_EMIT titleChanged();
390 
391   if (p.config.m_destination == newValue)
392     return;
393 
394   auto numRemovedChars = newValue.size()                - std::min<int>(p.config.m_destination.size(), newValue.size());
395   auto newPosition     = p.ui->output->cursorPosition() - std::min<int>(numRemovedChars, p.ui->output->cursorPosition());
396 
397   p.ui->output->setText(p.config.m_destination);
398   p.ui->output->setCursorPosition(newPosition);
399 }
400 
401 void
clearDestination()402 Tab::clearDestination() {
403   auto &p = *p_func();
404 
405   p.ui->output->setText(Q(""));
406   setDestination(Q(""));
407   p.config.m_destinationAuto.clear();
408   p.config.m_destinationUniquenessSuffix.clear();
409 }
410 
411 void
clearDestinationMaybe()412 Tab::clearDestinationMaybe() {
413   if (Util::Settings::get().m_autoClearOutputFileName)
414     clearDestination();
415 }
416 
417 void
clearTitle()418 Tab::clearTitle() {
419   auto &p = *p_func();
420 
421   p.ui->title->setText(Q(""));
422   p.config.m_title.clear();
423 }
424 
425 void
clearTitleMaybe()426 Tab::clearTitleMaybe() {
427   if (Util::Settings::get().m_autoClearFileTitle)
428     clearTitle();
429 }
430 
431 void
onBrowseOutput()432 Tab::onBrowseOutput() {
433   auto &p       = *p_func();
434   auto filter   = p.config.m_webmMode ? QY("WebM files") + Q(" (*.webm)") : QY("Matroska files") + Q(" (*.mkv *.mka *.mks *.mk3d)");
435   auto ext      = !p.config.m_destination.isEmpty() ? QFileInfo{p.config.m_destination}.suffix()
436                 : p.config.m_webmMode               ? Q("webm")
437                 :                                     Q("mkv");
438   auto mkvName  = defaultFileNameForSaving(!ext.isEmpty() ? Q(".%1").arg(ext) : ext);
439   auto fileName = getSaveFileName(QY("Select destination file name"), mkvName, filter, p.ui->output, ext);
440   if (fileName.isEmpty())
441     return;
442 
443   setDestination(fileName);
444 
445   auto &settings           = Util::Settings::get();
446   settings.m_lastOutputDir = QFileInfo{ fileName }.absoluteDir();
447   settings.save();
448 }
449 
450 void
onGlobalTagsChanged(QString newValue)451 Tab::onGlobalTagsChanged(QString newValue) {
452   p_func()->config.m_globalTags = newValue;
453 }
454 
455 void
onBrowseGlobalTags()456 Tab::onBrowseGlobalTags() {
457   auto &p       = *p_func();
458   auto fileName = getOpenFileName(QY("Select tags file"), QY("XML tag files") + Q(" (*.xml)"), p.ui->globalTags);
459   if (!fileName.isEmpty())
460     p.config.m_globalTags = fileName;
461 }
462 
463 void
onSegmentInfoChanged(QString newValue)464 Tab::onSegmentInfoChanged(QString newValue) {
465   p_func()->config.m_segmentInfo = newValue;
466 }
467 
468 void
onBrowseSegmentInfo()469 Tab::onBrowseSegmentInfo() {
470   auto &p       = *p_func();
471   auto fileName = getOpenFileName(QY("Select segment info file"), QY("XML segment info files") + Q(" (*.xml)"), p.ui->segmentInfo);
472   if (!fileName.isEmpty())
473     p.config.m_segmentInfo = fileName;
474 }
475 
476 void
onSplitModeChanged(int newMode)477 Tab::onSplitModeChanged(int newMode) {
478   auto &p              = *p_func();
479   auto splitMode       = static_cast<MuxConfig::SplitMode>(newMode);
480   p.config.m_splitMode = splitMode;
481 
482   Util::enableWidgets(p.splitControls, MuxConfig::DoNotSplit != splitMode);
483   setupSplitModeLabelAndToolTips();
484 }
485 
486 void
onSplitOptionsChanged(QString newValue)487 Tab::onSplitOptionsChanged(QString newValue) {
488   p_func()->config.m_splitOptions = newValue;
489 }
490 
491 void
onLinkFilesClicked(bool newValue)492 Tab::onLinkFilesClicked(bool newValue) {
493   p_func()->config.m_linkFiles = newValue;
494 }
495 
496 void
onSplitMaxFilesChanged(int newValue)497 Tab::onSplitMaxFilesChanged(int newValue) {
498   p_func()->config.m_splitMaxFiles = newValue;
499 }
500 
501 void
onSegmentUIDsChanged(QString newValue)502 Tab::onSegmentUIDsChanged(QString newValue) {
503   p_func()->config.m_segmentUIDs = newValue;
504 }
505 
506 void
onPreviousSegmentUIDChanged(QString newValue)507 Tab::onPreviousSegmentUIDChanged(QString newValue) {
508   p_func()->config.m_previousSegmentUID = newValue;
509 }
510 
511 void
onNextSegmentUIDChanged(QString newValue)512 Tab::onNextSegmentUIDChanged(QString newValue) {
513   p_func()->config.m_nextSegmentUID = newValue;
514 }
515 
516 void
onChaptersChanged(QString newValue)517 Tab::onChaptersChanged(QString newValue) {
518   auto &p             = *p_func();
519   p.config.m_chapters = newValue;
520   auto enablePreview  = !newValue.isEmpty();
521 
522 #if defined(HAVE_DVDREAD)
523   auto isDVD = newValue.toLower().endsWith(".ifo");
524 
525   if (enablePreview && isDVD)
526     enablePreview = false;
527 
528   p.ui->chapterTitleNumberLabel->setEnabled(isDVD);
529   p.ui->chapterTitleNumber     ->setEnabled(isDVD);
530 #endif  // HAVE_DVDREAD
531 
532   p.ui->chapterCharacterSetPreview->setEnabled(enablePreview);
533 }
534 
535 void
onBrowseChapters()536 Tab::onBrowseChapters() {
537   QString ifo;
538   QStringList dvds;
539 
540 #if defined(HAVE_DVDREAD)
541   dvds << Q("%1 (*.ifo *.IFO)").arg(QY("DVDs"));
542   ifo = Q("*.ifo *.IFO ");
543 #endif  // HAVE_DVDREAD
544 
545   auto fileTypes = QStringList{} << Q("%1 (%2*.txt *.xml)").arg(QY("Supported file types")).arg(ifo)
546                                  << Q("%1 (*.xml)").arg(QY("XML chapter files"))
547                                  << Q("%1 (*.txt)").arg(QY("Simple OGM-style chapter files"));
548 
549   fileTypes += dvds;
550 
551   auto fileName = getOpenFileName(QY("Select chapter file"), fileTypes.join(Q(";;")), p_func()->ui->chapters, InitialDirMode::ContentFirstInputFileLastOpenDir);
552 
553   if (!fileName.isEmpty())
554     onChaptersChanged(fileName);
555 }
556 
557 void
onChapterTitleNumberChanged(int newValue)558 Tab::onChapterTitleNumberChanged(int newValue) {
559   p_func()->config.m_chapterTitleNumber = newValue;
560 }
561 
562 void
onChapterLanguageChanged(mtx::bcp47::language_c const & newLanguage)563 Tab::onChapterLanguageChanged(mtx::bcp47::language_c const &newLanguage) {
564   if (newLanguage.is_valid())
565     p_func()->config.m_chapterLanguage = newLanguage;
566 }
567 
568 void
onChapterCharacterSetChanged(QString newValue)569 Tab::onChapterCharacterSetChanged(QString newValue) {
570   p_func()->config.m_chapterCharacterSet = newValue;
571 }
572 
573 void
onChapterDelayChanged(QString newValue)574 Tab::onChapterDelayChanged(QString newValue) {
575   p_func()->config.m_chapterDelay = newValue;
576 }
577 
578 void
onChapterStretchByChanged(QString newValue)579 Tab::onChapterStretchByChanged(QString newValue) {
580   p_func()->config.m_chapterStretchBy = newValue;
581 }
582 
583 void
onChapterCueNameFormatChanged(QString newValue)584 Tab::onChapterCueNameFormatChanged(QString newValue) {
585   p_func()->config.m_chapterCueNameFormat = newValue;
586 }
587 
588 void
onWebmClicked(bool newValue)589 Tab::onWebmClicked(bool newValue) {
590   p_func()->config.m_webmMode = newValue;
591   setOutputFileNameMaybe();
592 }
593 
594 void
onAdditionalOptionsChanged(QString newValue)595 Tab::onAdditionalOptionsChanged(QString newValue) {
596   p_func()->config.m_additionalOptions = newValue;
597 }
598 
599 void
onEditAdditionalOptions()600 Tab::onEditAdditionalOptions() {
601   auto &p = *p_func();
602 
603   AdditionalCommandLineOptionsDialog dlg{this, p.config.m_additionalOptions};
604   if (!dlg.exec())
605     return;
606 
607   p.config.m_additionalOptions = dlg.additionalOptions();
608   p.ui->additionalOptions->setText(p.config.m_additionalOptions);
609 
610   if (dlg.saveAsDefault()) {
611     auto &settings = Util::Settings::get();
612     settings.m_defaultAdditionalMergeOptions = p.config.m_additionalOptions;
613     settings.save();
614   }
615 }
616 
617 void
setOutputControlValues()618 Tab::setOutputControlValues() {
619   auto &p = *p_func();
620 
621   p.config.m_destination = Util::removeInvalidPathCharacters(p.config.m_destination);
622 
623   p.ui->title->setText(p.config.m_title);
624   p.ui->output->setText(p.config.m_destination);
625   p.ui->globalTags->setText(p.config.m_globalTags);
626   p.ui->segmentInfo->setText(p.config.m_segmentInfo);
627   p.ui->splitMode->setCurrentIndex(p.config.m_splitMode);
628   p.ui->splitOptions->setEditText(p.config.m_splitOptions);
629   p.ui->splitMaxFiles->setValue(p.config.m_splitMaxFiles);
630   p.ui->linkFiles->setChecked(p.config.m_linkFiles);
631   p.ui->segmentUIDs->setText(p.config.m_segmentUIDs);
632   p.ui->previousSegmentUID->setText(p.config.m_previousSegmentUID);
633   p.ui->nextSegmentUID->setText(p.config.m_nextSegmentUID);
634   p.ui->chapters->setText(p.config.m_chapters);
635   p.ui->chapterTitleNumber->setValue(p.config.m_chapterTitleNumber);
636   p.ui->chapterDelay->setText(p.config.m_chapterDelay);
637   p.ui->chapterStretchBy->setText(p.config.m_chapterStretchBy);
638   p.ui->chapterCueNameFormat->setText(p.config.m_chapterCueNameFormat);
639   p.ui->additionalOptions->setText(p.config.m_additionalOptions);
640   p.ui->webmMode->setChecked(p.config.m_webmMode);
641 
642   p.ui->chapterLanguage->setLanguage(p.config.m_chapterLanguage);
643   p.ui->chapterCharacterSet->setAdditionalItems(p.config.m_chapterCharacterSet)
644     .reInitializeIfNecessary()
645     .setCurrentByData(p.config.m_chapterCharacterSet);
646   p.ui->chapterGenerationMode->setCurrentIndex(static_cast<int>(p.config.m_chapterGenerationMode));
647   p.ui->chapterGenerationNameTemplate->setText(p.config.m_chapterGenerationNameTemplate);
648   p.ui->chapterGenerationInterval->setText(p.config.m_chapterGenerationInterval);
649 }
650 
651 void
onBrowseSegmentUID()652 Tab::onBrowseSegmentUID() {
653   addSegmentUIDFromFile(*p_func()->ui->segmentUIDs, true);
654 }
655 
656 void
onBrowsePreviousSegmentUID()657 Tab::onBrowsePreviousSegmentUID() {
658   addSegmentUIDFromFile(*p_func()->ui->previousSegmentUID, false);
659 }
660 
661 void
onBrowseNextSegmentUID()662 Tab::onBrowseNextSegmentUID() {
663   addSegmentUIDFromFile(*p_func()->ui->nextSegmentUID, false);
664 }
665 
666 void
addSegmentUIDFromFile(QLineEdit & lineEdit,bool append)667 Tab::addSegmentUIDFromFile(QLineEdit &lineEdit,
668                            bool append) {
669   Util::addSegmentUIDFromFileToLineEdit(*this, lineEdit, append);
670 }
671 
672 void
onPreviewChapterCharacterSet()673 Tab::onPreviewChapterCharacterSet() {
674   auto &p = *p_func();
675 
676   if (p.config.m_chapters.isEmpty())
677     return;
678 
679   auto dlg = new SelectCharacterSetDialog{this, p.config.m_chapters, p.ui->chapterCharacterSet->currentData().toString()};
680   connect(dlg, &SelectCharacterSetDialog::characterSetSelected, this, &Tab::setChapterCharacterSet);
681 
682   dlg->show();
683 }
684 
685 void
setChapterCharacterSet(QString const & characterSet)686 Tab::setChapterCharacterSet(QString const &characterSet) {
687   Util::setComboBoxTextByData(p_func()->ui->chapterCharacterSet, characterSet);
688   onChapterCharacterSetChanged(characterSet);
689 }
690 
691 void
setChaptersFileName(QString const & fileName)692 Tab::setChaptersFileName(QString const &fileName) {
693   auto &p             = *p_func();
694   p.config.m_chapters = fileName;
695   p.ui->chapters->setText(fileName);
696 }
697 
698 void
setTagsFileName(QString const & fileName)699 Tab::setTagsFileName(QString const &fileName) {
700   auto &p               = *p_func();
701   p.config.m_globalTags = fileName;
702   p.ui->globalTags->setText(fileName);
703 }
704 
705 void
setSegmentInfoFileName(QString const & fileName)706 Tab::setSegmentInfoFileName(QString const &fileName) {
707   auto &p                = *p_func();
708   p.config.m_segmentInfo = fileName;
709   p.ui->segmentInfo->setText(fileName);
710 }
711 
712 void
onCopyFirstFileNameToTitle()713 Tab::onCopyFirstFileNameToTitle() {
714   auto &p = *p_func();
715 
716   if (hasSourceFiles())
717     p.ui->title->setText(QFileInfo{ p.config.m_files[0]->m_fileName }.completeBaseName());
718 }
719 
720 void
onCopyOutputFileNameToTitle()721 Tab::onCopyOutputFileNameToTitle() {
722   auto &p = *p_func();
723 
724   if (hasDestinationFileName())
725     p.ui->title->setText(QFileInfo{ p.config.m_destination }.completeBaseName());
726 }
727 
728 void
onCopyTitleToOutputFileName()729 Tab::onCopyTitleToOutputFileName() {
730   auto &p = *p_func();
731 
732   if (!hasTitle())
733     return;
734 
735   p.config.m_destinationUniquenessSuffix.clear();
736 
737   auto info  = QFileInfo{ p.config.m_destination };
738   auto path  = info.path();
739   auto title = Util::replaceInvalidFileNameCharacters(p.config.m_title);
740 
741   QString newFileName;
742 
743   if (Util::Settings::get().m_uniqueOutputFileNames)
744     newFileName = generateUniqueOutputFileName(title, QDir{path});
745 
746   else {
747     auto suffix = info.suffix();
748     newFileName = Q("%1.%2")
749       .arg(path.isEmpty()   ? title    : Q("%1/%2").arg(path).arg(title))
750       .arg(suffix.isEmpty() ? Q("mkv") : suffix);
751   }
752 
753   p.ui->output->setText(QDir::toNativeSeparators(newFileName));
754 }
755 
756 bool
hasTitle() const757 Tab::hasTitle()
758   const {
759   return !p_func()->config.m_title.isEmpty();
760 }
761 
762 bool
hasDestinationFileName() const763 Tab::hasDestinationFileName()
764   const {
765   return !p_func()->config.m_destination.isEmpty();
766 }
767 
768 void
onChapterGenerationModeChanged()769 Tab::onChapterGenerationModeChanged() {
770   auto &p = *p_func();
771 
772   p.config.m_chapterGenerationMode = static_cast<MuxConfig::ChapterGenerationMode>(p.ui->chapterGenerationMode->currentIndex());
773   auto isInterval                  = MuxConfig::ChapterGenerationMode::Intervals == p.config.m_chapterGenerationMode;
774 
775   p.ui->chapterGenerationInterval->setEnabled(isInterval);
776   p.ui->chapterGenerationIntervalLabel->setEnabled(isInterval);
777 }
778 
779 void
onChapterGenerationNameTemplateChanged()780 Tab::onChapterGenerationNameTemplateChanged() {
781   auto &p = *p_func();
782 
783   p.config.m_chapterGenerationNameTemplate = p.ui->chapterGenerationNameTemplate->text();
784 }
785 
786 void
onChapterGenerationIntervalChanged()787 Tab::onChapterGenerationIntervalChanged() {
788   auto &p = *p_func();
789 
790   p.config.m_chapterGenerationInterval = p.ui->chapterGenerationInterval->text();
791 }
792 
793 void
showRecentlyUsedOutputDirs()794 Tab::showRecentlyUsedOutputDirs() {
795   auto &reg   = Util::Settings::get();
796   auto &items = reg.m_mergeLastOutputDirs;
797   auto path   = QFileInfo{ p_func()->config.m_destination }.path();
798 
799   if (!path.isEmpty())
800     items.add(QDir::toNativeSeparators(path));
801 
802   if (items.isEmpty())
803     return;
804 
805   QMenu menu{this};
806 
807   for (auto const &dir : Util::Settings::get().m_mergeLastOutputDirs.items()) {
808     auto action = new QAction{&menu};
809     action->setText(dir);
810 
811     connect(action, &QAction::triggered, [this, dir]() { changeOutputDirectoryTo(dir); });
812 
813     menu.addAction(action);
814   }
815 
816   menu.exec(QCursor::pos());
817 
818 }
819 
820 void
changeOutputDirectoryTo(QString const & directory)821 Tab::changeOutputDirectoryTo(QString const &directory) {
822   auto &p = *p_func();
823 
824   auto makeUnique  = Util::Settings::get().m_uniqueOutputFileNames;
825   auto oldFileName = QFileInfo{ p.config.m_destination }.fileName();
826   auto newFileName = !oldFileName.isEmpty() ? oldFileName : Q("%1.%2").arg(QY("unnamed")).arg(suggestOutputFileNameExtension());
827   auto newFilePath = makeUnique ? generateUniqueOutputFileName(QFileInfo{newFileName}.completeBaseName(), QDir{directory}, true) : Q("%1/%2").arg(directory).arg(newFileName);
828 
829   p.ui->output->setText(QDir::toNativeSeparators(newFilePath));
830 }
831 
832 }
833