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 ® = 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