1 #include "common/common_pch.h"
2
3 #include <QDebug>
4 #include <QDir>
5 #include <QFileInfo>
6 #include <QMenu>
7 #include <QMessageBox>
8 #include <QTimer>
9
10 #include <matroska/KaxSemantic.h>
11
12 #include "common/bitvalue.h"
13 #include "common/bluray/mpls.h"
14 #include "common/chapters/bluray.h"
15 #include "common/chapters/chapters.h"
16 #include "common/chapters/dvd.h"
17 #include "common/ebml.h"
18 #include "common/kax_file.h"
19 #include "common/math.h"
20 #include "common/mm_io_x.h"
21 #include "common/mm_file_io.h"
22 #include "common/path.h"
23 #include "common/qt.h"
24 #include "common/segmentinfo.h"
25 #include "common/strings/formatting.h"
26 #include "common/strings/parsing.h"
27 #include "common/translation.h"
28 #include "common/xml/ebml_chapters_converter.h"
29 #include "mkvtoolnix-gui/forms/chapter_editor/tab.h"
30 #include "mkvtoolnix-gui/chapter_editor/generate_sub_chapters_parameters_dialog.h"
31 #include "mkvtoolnix-gui/chapter_editor/name_model.h"
32 #include "mkvtoolnix-gui/chapter_editor/mass_modification_dialog.h"
33 #include "mkvtoolnix-gui/chapter_editor/tab.h"
34 #include "mkvtoolnix-gui/chapter_editor/tab_p.h"
35 #include "mkvtoolnix-gui/chapter_editor/tool.h"
36 #include "mkvtoolnix-gui/main_window/main_window.h"
37 #include "mkvtoolnix-gui/main_window/select_character_set_dialog.h"
38 #include "mkvtoolnix-gui/util/file.h"
39 #include "mkvtoolnix-gui/util/file_dialog.h"
40 #include "mkvtoolnix-gui/util/header_view_manager.h"
41 #include "mkvtoolnix-gui/util/message_box.h"
42 #include "mkvtoolnix-gui/util/model.h"
43 #include "mkvtoolnix-gui/util/settings.h"
44 #include "mkvtoolnix-gui/util/tree.h"
45 #include "mkvtoolnix-gui/util/widget.h"
46
47 using namespace libmatroska;
48 using namespace mtx::gui;
49
50 namespace mtx::bcp47 {
51
52 uint
qHash(language_c const & language)53 qHash(language_c const &language) {
54 return qHash(Q(language.format()));
55 }
56
57 }
58
59 namespace mtx::gui::ChapterEditor {
60
61 namespace {
62
63 bool
removeLanguageControlRow(QVector<std::tuple<QBoxLayout *,QWidget *,QPushButton * >> & controls,QObject * sender)64 removeLanguageControlRow(QVector<std::tuple<QBoxLayout *, QWidget *, QPushButton *>> &controls,
65 QObject *sender) {
66 for (int idx = 0, numControls = controls.size(); idx < numControls; ++idx) {
67 auto &[layout, widget, button] = controls[idx];
68
69 if (button != sender)
70 continue;
71
72 delete layout;
73 delete widget;
74 delete button;
75
76 controls.removeAt(idx);
77
78 return true;
79 }
80
81 return false;
82 }
83
84 }
85
TabPrivate(Tab & tab,QString const & pFileName)86 TabPrivate::TabPrivate(Tab &tab,
87 QString const &pFileName)
88 : ui{new Ui::Tab}
89 , fileName{pFileName}
90 , chapterModel{new ChapterModel{&tab}}
91 , nameModel{new NameModel{&tab}}
92 , expandAllAction{new QAction{&tab}}
93 , collapseAllAction{new QAction{&tab}}
94 , addEditionBeforeAction{new QAction{&tab}}
95 , addEditionAfterAction{new QAction{&tab}}
96 , addChapterBeforeAction{new QAction{&tab}}
97 , addChapterAfterAction{new QAction{&tab}}
98 , addSubChapterAction{new QAction{&tab}}
99 , removeElementAction{new QAction{&tab}}
100 , duplicateAction{new QAction{&tab}}
101 , massModificationAction{new QAction{&tab}}
102 , generateSubChaptersAction{new QAction{&tab}}
103 , renumberSubChaptersAction{new QAction{&tab}}
104 , copyToOtherTabMenu{new QMenu{&tab}}
105 {
106 }
107
Tab(QWidget * parent,QString const & fileName)108 Tab::Tab(QWidget *parent,
109 QString const &fileName)
110 : QWidget{parent}
111 , p_ptr{new TabPrivate{*this, fileName}}
112 {
113 setup();
114 }
115
Tab(QWidget * parent,TabPrivate & p)116 Tab::Tab(QWidget *parent,
117 TabPrivate &p)
118 : QWidget{parent}
119 , p_ptr{&p}
120 {
121 setup();
122 }
123
~Tab()124 Tab::~Tab() {
125 }
126
127 void
setup()128 Tab::setup() {
129 auto p = p_func();
130
131 // Setup UI controls.
132 p->ui->setupUi(this);
133
134 setupUi();
135
136 retranslateUi();
137 }
138
139 void
setupUi()140 Tab::setupUi() {
141 auto p = p_func();
142
143 Util::Settings::get().handleSplitterSizes(p->ui->chapterEditorSplitter);
144
145 p->ui->elements->setModel(p->chapterModel);
146 p->ui->tvChNames->setModel(p->nameModel);
147
148 p->ui->elements->acceptDroppedFiles(true);
149
150 p->languageControls.push_back({ nullptr, p->ui->ldwChNameLanguage1, p->ui->pbChNameLanguageAdd });
151
152 p->nameWidgets << p->ui->pbChRemoveName << p->ui->lChName << p->ui->leChName;
153
154 Util::fixScrollAreaBackground(p->ui->scrollArea);
155 Util::HeaderViewManager::create(*p->ui->elements, "ChapterEditor::Elements") .setDefaultSizes({ { Q("editionChapter"), 200 }, { Q("start"), 130 }, { Q("end"), 130 } });
156 Util::HeaderViewManager::create(*p->ui->tvChNames, "ChapterEditor::ChapterNames").setDefaultSizes({ { Q("name"), 200 }, { Q("language"), 150 } });
157
158 p->addEditionBeforeAction->setIcon(QIcon{Q(":/icons/16x16/edit-table-insert-row-above.png")});
159 p->addEditionAfterAction->setIcon(QIcon{Q(":/icons/16x16/edit-table-insert-row-below.png")});
160 p->addChapterBeforeAction->setIcon(QIcon{Q(":/icons/16x16/edit-table-insert-row-above.png")});
161 p->addChapterAfterAction->setIcon(QIcon{Q(":/icons/16x16/edit-table-insert-row-below.png")});
162 p->addSubChapterAction->setIcon(QIcon{Q(":/icons/16x16/edit-table-insert-row-under.png")});
163 p->duplicateAction->setIcon(QIcon{Q(":/icons/16x16/tab-duplicate.png")});
164 p->removeElementAction->setIcon(QIcon{Q(":/icons/16x16/list-remove.png")});
165 p->renumberSubChaptersAction->setIcon(QIcon{Q(":/icons/16x16/format-list-ordered.png")});
166 p->massModificationAction->setIcon(QIcon{Q(":/icons/16x16/tools-wizard.png")});
167 p->copyToOtherTabMenu->setIcon(QIcon{Q(":/icons/16x16/edit-copy.png")});
168
169 auto tool = MainWindow::chapterEditorTool();
170 connect(p->ui->elements, &Util::BasicTreeView::customContextMenuRequested, this, &Tab::showChapterContextMenu);
171 connect(p->ui->elements, &Util::BasicTreeView::deletePressed, this, &Tab::removeElement);
172 connect(p->ui->elements, &Util::BasicTreeView::insertPressed, this, &Tab::addEditionOrChapterAfter);
173 connect(p->ui->elements, &Util::BasicTreeView::filesDropped, tool, &Tool::openFiles);
174 connect(p->ui->elements->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Tab::chapterSelectionChanged);
175 connect(p->ui->tvChNames->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Tab::nameSelectionChanged);
176 connect(p->ui->leChName, &QLineEdit::textEdited, this, &Tab::chapterNameEdited);
177 connect(p->ui->ldwChNameLanguage1, &Util::LanguageDisplayWidget::languageChanged, this, &Tab::chapterNameLanguageChanged);
178 connect(p->ui->pbChAddName, &QPushButton::clicked, this, &Tab::addChapterName);
179 connect(p->ui->pbChRemoveName, &QPushButton::clicked, this, &Tab::removeChapterName);
180 connect(p->ui->pbBrowseSegmentUID, &QPushButton::clicked, this, &Tab::addSegmentUIDFromFile);
181 connect(p->ui->pbChNameLanguageAdd, &QPushButton::clicked, this, &Tab::addChapterNameLanguage);
182
183 connect(p->expandAllAction, &QAction::triggered, this, &Tab::expandAll);
184 connect(p->collapseAllAction, &QAction::triggered, this, &Tab::collapseAll);
185 connect(p->addEditionBeforeAction, &QAction::triggered, this, &Tab::addEditionBefore);
186 connect(p->addEditionAfterAction, &QAction::triggered, this, &Tab::addEditionAfter);
187 connect(p->addChapterBeforeAction, &QAction::triggered, this, &Tab::addChapterBefore);
188 connect(p->addChapterAfterAction, &QAction::triggered, this, &Tab::addChapterAfter);
189 connect(p->addSubChapterAction, &QAction::triggered, this, &Tab::addSubChapter);
190 connect(p->removeElementAction, &QAction::triggered, this, &Tab::removeElement);
191 connect(p->duplicateAction, &QAction::triggered, this, &Tab::duplicateElement);
192 connect(p->massModificationAction, &QAction::triggered, this, &Tab::massModify);
193 connect(p->generateSubChaptersAction, &QAction::triggered, this, &Tab::generateSubChapters);
194 connect(p->renumberSubChaptersAction, &QAction::triggered, this, &Tab::renumberSubChapters);
195
196 for (auto &lineEdit : findChildren<Util::BasicLineEdit *>()) {
197 lineEdit->acceptDroppedFiles(false).setTextToDroppedFileName(false);
198 connect(lineEdit, &Util::BasicLineEdit::returnPressed, this, &Tab::focusOtherControlInNextChapterElement);
199 connect(lineEdit, &Util::BasicLineEdit::shiftReturnPressed, this, &Tab::focusSameControlInNextChapterElement);
200 }
201 }
202
203 void
updateFileNameDisplay()204 Tab::updateFileNameDisplay() {
205 auto p = p_func();
206
207 if (!p->fileName.isEmpty()) {
208 auto info = QFileInfo{p->fileName};
209 p->ui->fileName->setText(info.fileName());
210 p->ui->directory->setText(QDir::toNativeSeparators(info.path()));
211
212 } else {
213 p->ui->fileName->setText(QY("<Unsaved file>"));
214 p->ui->directory->setText(Q(""));
215
216 }
217 }
218
219 void
retranslateUi()220 Tab::retranslateUi() {
221 auto p = p_func();
222
223 p->ui->retranslateUi(this);
224
225 updateFileNameDisplay();
226
227 p->expandAllAction->setText(QY("&Expand all"));
228 p->collapseAllAction->setText(QY("&Collapse all"));
229 p->addEditionBeforeAction->setText(QY("Add new e&dition before"));
230 p->addEditionAfterAction->setText(QY("Add new ed&ition after"));
231 p->addChapterBeforeAction->setText(QY("Add new c&hapter before"));
232 p->addChapterAfterAction->setText(QY("Add new ch&apter after"));
233 p->addSubChapterAction->setText(QY("Add new &sub-chapter inside"));
234 p->removeElementAction->setText(QY("&Remove selected edition or chapter"));
235 p->duplicateAction->setText(QY("D&uplicate selected edition or chapter"));
236 p->massModificationAction->setText(QY("Additional &modifications"));
237 p->generateSubChaptersAction->setText(QY("&Generate sub-chapters"));
238 p->renumberSubChaptersAction->setText(QY("Re&number sub-chapters"));
239 p->copyToOtherTabMenu->setTitle(QY("Cop&y to other tab"));
240
241 setupToolTips();
242
243 p->chapterModel->retranslateUi();
244 p->nameModel->retranslateUi();
245
246 Q_EMIT titleChanged();
247 }
248
249 void
setupToolTips()250 Tab::setupToolTips() {
251 auto p = p_func();
252
253 Util::setToolTip(p->ui->elements, QY("Right-click for actions for editions and chapters"));
254 Util::setToolTip(p->ui->pbBrowseSegmentUID, QY("Select an existing Matroska or WebM file and the GUI will add its segment UID to the input field on the left."));
255 }
256
257 QString
title() const258 Tab::title()
259 const {
260 auto p = p_func();
261
262 if (p->fileName.isEmpty())
263 return QY("<Unsaved file>");
264 return QFileInfo{p->fileName}.fileName();
265 }
266
267 QString const &
fileName() const268 Tab::fileName()
269 const {
270 auto p = p_func();
271
272 return p->fileName;
273 }
274
275 void
newFile()276 Tab::newFile() {
277 auto p = p_func();
278
279 addEdition(false);
280
281 auto selectionModel = p->ui->elements->selectionModel();
282 auto selection = QItemSelection{p->chapterModel->index(0, 0), p->chapterModel->index(0, p->chapterModel->columnCount() - 1)};
283 selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
284
285 addSubChapter();
286
287 auto parentIdx = p->chapterModel->index(0, 0);
288 selection = QItemSelection{p->chapterModel->index(0, 0, parentIdx), p->chapterModel->index(0, p->chapterModel->columnCount() - 1, parentIdx)};
289 selectionModel->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
290
291 p->ui->leChStart->selectAll();
292 p->ui->leChStart->setFocus();
293
294 p->savedState = currentState();
295 }
296
297 void
resetData()298 Tab::resetData() {
299 auto p = p_func();
300
301 p->analyzer.reset();
302 p->nameModel->reset();
303 p->chapterModel->reset();
304 }
305
306 bool
readFileEndTimestampForMatroska(kax_analyzer_c & analyzer)307 Tab::readFileEndTimestampForMatroska(kax_analyzer_c &analyzer) {
308 auto p = p_func();
309
310 p->fileEndTimestamp.reset();
311
312 auto idx = analyzer.find(KaxInfo::ClassInfos.GlobalId);
313 if (-1 == idx) {
314 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(QY("The file you tried to open (%1) could not be read successfully.").arg(p->fileName)).exec();
315 return false;
316 }
317
318 auto info = analyzer.read_element(idx);
319 if (!info) {
320 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(QY("The file you tried to open (%1) could not be read successfully.").arg(p->fileName)).exec();
321 return false;
322 }
323
324 auto durationKax = FindChild<KaxDuration>(*info);
325 if (!durationKax) {
326 qDebug() << "readFileEndTimestampForMatroska: no duration found";
327 return true;
328 }
329
330 auto timestampScale = FindChildValue<KaxTimecodeScale, uint64_t>(static_cast<KaxInfo &>(*info), TIMESTAMP_SCALE);
331 auto duration = timestamp_c::ns(durationKax->GetValue() * timestampScale);
332
333 qDebug() << "readFileEndTimestampForMatroska: duration is" << Q(mtx::string::format_timestamp(duration));
334
335 auto &fileIo = analyzer.get_file();
336 fileIo.setFilePointer(analyzer.get_segment_data_start_pos());
337
338 kax_file_c fileKax{fileIo};
339 fileKax.enable_reporting(false);
340
341 auto cluster = fileKax.read_next_cluster();
342 if (!cluster) {
343 qDebug() << "readFileEndTimestampForMatroska: no cluster found";
344 return true;
345 }
346
347 cluster->InitTimecode(FindChildValue<KaxClusterTimecode>(*cluster), timestampScale);
348
349 auto minBlockTimestamp = timestamp_c::ns(0);
350
351 for (auto const &child : *cluster) {
352 timestamp_c blockTimestamp;
353
354 if (Is<KaxBlockGroup>(child)) {
355 auto &group = static_cast<KaxBlockGroup &>(*child);
356 auto block = FindChild<KaxBlock>(group);
357
358 if (block) {
359 block->SetParent(*cluster);
360 blockTimestamp = timestamp_c::ns(mtx::math::to_signed(block->GlobalTimecode()));
361 }
362
363 } else if (Is<KaxSimpleBlock>(child)) {
364 auto &block = static_cast<KaxSimpleBlock &>(*child);
365 block.SetParent(*cluster);
366 blockTimestamp = timestamp_c::ns(mtx::math::to_signed(block.GlobalTimecode()));
367
368 }
369
370 if ( blockTimestamp.valid()
371 && ( !minBlockTimestamp.valid()
372 || (blockTimestamp < minBlockTimestamp)))
373 minBlockTimestamp = blockTimestamp;
374 }
375
376 p->fileEndTimestamp = minBlockTimestamp + duration;
377
378 qDebug() << "readFileEndTimestampForMatroska: minBlockTimestamp" << Q(mtx::string::format_timestamp(minBlockTimestamp)) << "result" << Q(mtx::string::format_timestamp(p->fileEndTimestamp));
379
380 return true;
381 }
382
383 Tab::LoadResult
loadFromMatroskaFile(QString const & fileName,bool append)384 Tab::loadFromMatroskaFile(QString const &fileName,
385 bool append) {
386 auto p = p_func();
387 auto analyzer = std::make_unique<Util::KaxAnalyzer>(this, fileName);
388
389 if (!analyzer->set_parse_mode(kax_analyzer_c::parse_mode_fast).set_open_mode(MODE_READ).process()) {
390 auto text = Q("%1 %2")
391 .arg(QY("The file you tried to open (%1) could not be read successfully.").arg(fileName))
392 .arg(QY("Possible reasons are: the file is not a Matroska file; the file is write-protected; the file is locked by another process; you do not have permission to access the file."));
393 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(text).exec();
394
395 if (!append)
396 Q_EMIT removeThisTab();
397 return {};
398 }
399
400 if (!append && !readFileEndTimestampForMatroska(*analyzer)) {
401 Q_EMIT removeThisTab();
402 return {};
403 }
404
405 auto idx = analyzer->find(KaxChapters::ClassInfos.GlobalId);
406 if (-1 == idx) {
407 analyzer->close_file();
408
409 if (!append)
410 p->analyzer = std::move(analyzer);
411
412 return { std::make_shared<KaxChapters>(), true };
413 }
414
415 auto chapters = analyzer->read_element(idx);
416 if (!chapters) {
417 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(QY("The file you tried to open (%1) could not be read successfully.").arg(fileName)).exec();
418 if (!append)
419 Q_EMIT removeThisTab();
420 return {};
421 }
422
423 analyzer->close_file();
424
425 if (!append)
426 p->analyzer = std::move(analyzer);
427
428 return { std::static_pointer_cast<KaxChapters>(chapters), true };
429 }
430
431 Tab::LoadResult
checkSimpleFormatForBomAndNonAscii(ChaptersPtr const & chapters,QString const & fileName,bool append)432 Tab::checkSimpleFormatForBomAndNonAscii(ChaptersPtr const &chapters,
433 QString const &fileName,
434 bool append) {
435 auto p = p_func();
436 auto result = Util::checkForBomAndNonAscii(fileName);
437
438 if ( (byte_order_mark_e::none != result.byteOrderMark)
439 || !result.containsNonAscii
440 || !Util::Settings::get().m_ceTextFileCharacterSet.isEmpty())
441 return { chapters, false };
442
443 Util::enableChildren(this, false);
444
445 p->originalFileName = fileName;
446 auto dlg = new SelectCharacterSetDialog{this, fileName};
447
448 if (append)
449 connect(dlg, &SelectCharacterSetDialog::characterSetSelected, this, &Tab::appendSimpleChaptersWithCharacterSet);
450
451 else {
452 connect(dlg, &SelectCharacterSetDialog::characterSetSelected, this, &Tab::reloadSimpleChaptersWithCharacterSet);
453 connect(dlg, &SelectCharacterSetDialog::rejected, this, &Tab::closeTab);
454 }
455
456 dlg->show();
457
458 return {};
459 }
460
461 Tab::LoadResult
loadFromChapterFile(QString const & fileName,bool append)462 Tab::loadFromChapterFile(QString const &fileName,
463 bool append) {
464 auto p = p_func();
465 auto format = mtx::chapters::format_e::xml;
466 auto chapters = ChaptersPtr{};
467 auto error = QString{};
468
469 try {
470 chapters = mtx::chapters::parse(to_utf8(fileName), 0, -1, 0, {}, to_utf8(Util::Settings::get().m_ceTextFileCharacterSet), true, &format);
471 // log_it(ebml_dumper_c::dump_to_string(chapters.get()));
472
473 } catch (mtx::mm_io::exception &ex) {
474 error = Q(ex.what());
475
476 } catch (mtx::chapters::parser_x &ex) {
477 error = Q(ex.what());
478 }
479
480 if (!chapters) {
481 auto message = QY("The file you tried to open (%1) is recognized as neither a valid Matroska nor a valid chapter file.").arg(fileName);
482 if (!error.isEmpty())
483 message = Q("%1 %2").arg(message).arg(QY("Error message from the parser: %1").arg(error));
484
485 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(message).exec();
486
487 if (!append)
488 Q_EMIT removeThisTab();
489
490 } else if (format != mtx::chapters::format_e::xml) {
491 auto result = checkSimpleFormatForBomAndNonAscii(chapters, fileName, append);
492
493 if (!append) {
494 p->fileName.clear();
495 Q_EMIT titleChanged();
496 }
497
498 return result;
499 }
500
501 return { chapters, format == mtx::chapters::format_e::xml };
502 }
503
504 void
appendSimpleChaptersWithCharacterSet(QString const & characterSet)505 Tab::appendSimpleChaptersWithCharacterSet(QString const &characterSet) {
506 reloadOrAppendSimpleChaptersWithCharacterSet(characterSet, true);
507 }
508
509 void
reloadSimpleChaptersWithCharacterSet(QString const & characterSet)510 Tab::reloadSimpleChaptersWithCharacterSet(QString const &characterSet) {
511 reloadOrAppendSimpleChaptersWithCharacterSet(characterSet, false);
512 }
513
514 void
reloadOrAppendSimpleChaptersWithCharacterSet(QString const & characterSet,bool append)515 Tab::reloadOrAppendSimpleChaptersWithCharacterSet(QString const &characterSet,
516 bool append) {
517 auto p = p_func();
518 auto error = QString{};
519
520 try {
521 auto chapters = mtx::chapters::parse(to_utf8(p->originalFileName), 0, -1, 0, {}, to_utf8(characterSet), true);
522
523 if (!append)
524 chaptersLoaded(chapters, false);
525 else
526 appendTheseChapters(chapters);
527
528 Util::enableChildren(this, true);
529
530 MainWindow::chapterEditorTool()->enableMenuActions();
531
532 return;
533
534 } catch (mtx::mm_io::exception &ex) {
535 error = Q(ex.what());
536 } catch (mtx::chapters::parser_x &ex) {
537 error = Q(ex.what());
538 }
539
540 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(QY("Error message from the parser: %1").arg(error)).exec();
541
542 Q_EMIT removeThisTab();
543 }
544
545 bool
areWidgetsEnabled() const546 Tab::areWidgetsEnabled()
547 const {
548 auto p = p_func();
549
550 return p->ui->elements->isEnabled();
551 }
552
553 ChaptersPtr
mplsChaptersToMatroskaChapters(std::vector<mtx::bluray::mpls::chapter_t> const & mplsChapters) const554 Tab::mplsChaptersToMatroskaChapters(std::vector<mtx::bluray::mpls::chapter_t> const &mplsChapters)
555 const {
556 auto &cfg = Util::Settings::get();
557 return mtx::chapters::convert_mpls_chapters_kax_chapters(mplsChapters, cfg.m_defaultChapterLanguage, to_utf8(cfg.m_chapterNameTemplate));
558 }
559
560 Tab::LoadResult
loadFromMplsFile(QString const & fileName,bool append)561 Tab::loadFromMplsFile(QString const &fileName,
562 bool append) {
563 auto p = p_func();
564 auto chapters = ChaptersPtr{};
565 auto error = QString{};
566
567 try {
568 mm_file_io_c in{to_utf8(fileName)};
569 auto parser = ::mtx::bluray::mpls::parser_c{};
570
571 parser.enable_dropping_last_entry_if_at_end(Util::Settings::get().m_dropLastChapterFromBlurayPlaylist);
572
573 if (parser.parse(in))
574 chapters = mplsChaptersToMatroskaChapters(parser.get_chapters());
575
576 } catch (mtx::mm_io::exception &ex) {
577 error = Q(ex.what());
578
579 } catch (mtx::bluray::mpls::exception &ex) {
580 error = Q(ex.what());
581
582 }
583
584 if (!chapters) {
585 auto message = QY("The file you tried to open (%1) is recognized as neither a valid Matroska nor a valid chapter file.").arg(fileName);
586 if (!error.isEmpty())
587 message = Q("%1 %2").arg(message).arg(QY("Error message from the parser: %1").arg(error));
588
589 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(message).exec();
590 if (!append)
591 Q_EMIT removeThisTab();
592
593 } else if (!append) {
594 p->fileName.clear();
595 Q_EMIT titleChanged();
596 }
597
598 return { chapters, false };
599 }
600
601 Tab::LoadResult
loadFromDVD(QString const & fileName,bool append)602 Tab::loadFromDVD([[maybe_unused]] QString const &fileName,
603 [[maybe_unused]] bool append) {
604 #if defined(HAVE_DVDREAD)
605 auto p = p_func();
606 auto &cfg = Util::Settings::get();
607 auto chapters = ChaptersPtr{};
608 auto error = QString{};
609
610 auto dvdDirectory = mtx::fs::to_path(fileName).parent_path();
611
612 try {
613 auto titlesAndTimestamps = mtx::chapters::parse_dvd(dvdDirectory.u8string());
614 chapters = mtx::chapters::create_editions_and_chapters(titlesAndTimestamps, cfg.m_defaultChapterLanguage, to_utf8(cfg.m_chapterNameTemplate));
615
616 } catch (mtx::chapters::parser_x const &ex) {
617 error = Q(ex.what());
618 }
619
620 if (!chapters) {
621 auto message = QY("The file you tried to open (%1) is recognized as neither a valid Matroska nor a valid chapter file.").arg(fileName);
622 if (!error.isEmpty())
623 message = Q("%1 %2").arg(message).arg(QY("Error message from the parser: %1").arg(error));
624
625 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(message).exec();
626 if (!append)
627 Q_EMIT removeThisTab();
628
629 } else if (!append) {
630 p->fileName.clear();
631 Q_EMIT titleChanged();
632 }
633
634 return { chapters, false };
635
636 #else // HAVE_DVDREAD
637 if (!append)
638 Q_EMIT removeThisTab();
639
640 return { {}, false };
641 #endif // HAVE_DVDREAD
642 }
643
644 void
load()645 Tab::load() {
646 auto p = p_func();
647
648 resetData();
649
650 p->savedState = currentState();
651 auto result = kax_analyzer_c::probe(to_utf8(p->fileName)) ? loadFromMatroskaFile(p->fileName, false)
652 : p->fileName.toLower().endsWith(Q(".mpls")) ? loadFromMplsFile(p->fileName, false)
653 : p->fileName.toLower().endsWith(Q(".ifo")) ? loadFromDVD(p->fileName, false)
654 : loadFromChapterFile(p->fileName, false);
655
656 if (result.first)
657 chaptersLoaded(result.first, result.second);
658 }
659
660 void
append(QString const & fileName)661 Tab::append(QString const &fileName) {
662 auto result = kax_analyzer_c::probe(to_utf8(fileName)) ? loadFromMatroskaFile(fileName, true)
663 : fileName.toLower().endsWith(Q(".mpls")) ? loadFromMplsFile(fileName, true)
664 : fileName.toLower().endsWith(Q(".ifo")) ? loadFromDVD(fileName, true)
665 : loadFromChapterFile(fileName, true);
666
667 if (result.first)
668 appendTheseChapters(result.first);
669 }
670
671 void
appendTheseChapters(ChaptersPtr const & chapters)672 Tab::appendTheseChapters(ChaptersPtr const &chapters) {
673 auto p = p_func();
674
675 disconnect(p->chapterModel, &QStandardItemModel::rowsInserted, this, &Tab::expandInsertedElements);
676
677 p->chapterModel->populate(*chapters, true);
678
679 expandAll();
680
681 connect(p->chapterModel, &QStandardItemModel::rowsInserted, this, &Tab::expandInsertedElements);
682
683 MainWindow::chapterEditorTool()->enableMenuActions();
684 }
685
686 void
chaptersLoaded(ChaptersPtr const & chapters,bool canBeWritten)687 Tab::chaptersLoaded(ChaptersPtr const &chapters,
688 bool canBeWritten) {
689 auto p = p_func();
690
691 if (!p->fileName.isEmpty())
692 p->fileModificationTime = QFileInfo{p->fileName}.lastModified();
693
694 disconnect(p->chapterModel, &QStandardItemModel::rowsInserted, this, &Tab::expandInsertedElements);
695
696 p->chapterModel->reset();
697 p->chapterModel->populate(*chapters, false);
698
699 if (canBeWritten)
700 p->savedState = currentState();
701
702 expandAll();
703
704 connect(p->chapterModel, &QStandardItemModel::rowsInserted, this, &Tab::expandInsertedElements);
705
706 MainWindow::chapterEditorTool()->enableMenuActions();
707 }
708
709 void
save()710 Tab::save() {
711 auto p = p_func();
712
713 if (!p->analyzer)
714 saveAsXmlImpl(false);
715
716 else
717 saveToMatroskaImpl(false);
718 }
719
720 void
saveAsImpl(bool requireNewFileName,std::function<bool (bool,QString &)> const & worker)721 Tab::saveAsImpl(bool requireNewFileName,
722 std::function<bool(bool, QString &)> const &worker) {
723 auto p = p_func();
724
725 if (!copyControlsToStorage())
726 return;
727
728 p->chapterModel->fixMandatoryElements();
729 setControlsFromStorage();
730
731 auto newFileName = p->fileName;
732 if (p->fileName.isEmpty())
733 requireNewFileName = true;
734
735 if (!worker(requireNewFileName, newFileName))
736 return;
737
738 p->savedState = currentState();
739
740 if (newFileName != p->fileName) {
741 p->fileName = newFileName;
742
743 auto &settings = Util::Settings::get();
744 settings.m_lastOpenDir.setPath(QFileInfo{newFileName}.path());
745 settings.save();
746
747 updateFileNameDisplay();
748 Q_EMIT titleChanged();
749 }
750
751 MainWindow::get()->setStatusBarMessage(QY("The file has been saved successfully."));
752 }
753
754 void
saveAsXml()755 Tab::saveAsXml() {
756 saveAsXmlImpl(true);
757 }
758
759 void
saveAsXmlImpl(bool requireNewFileName)760 Tab::saveAsXmlImpl(bool requireNewFileName) {
761 auto p = p_func();
762
763 saveAsImpl(requireNewFileName, [this, p](bool doRequireNewFileName, QString &newFileName) -> bool {
764 if (doRequireNewFileName) {
765 auto defaultFilePath = !p->fileName.isEmpty() ? Util::dirPath(QFileInfo{p->fileName}.path()) : Util::Settings::get().lastOpenDirPath();
766 newFileName = Util::getSaveFileName(this, QY("Save chapters as XML"), defaultFilePath, {}, QY("XML chapter files") + Q(" (*.xml);;") + QY("All files") + Q(" (*)"), Q("xml"));
767
768 if (newFileName.isEmpty())
769 return false;
770 }
771
772 try {
773 auto chapters = p->chapterModel->allChapters();
774 mm_file_io_c out{to_utf8(newFileName), MODE_CREATE};
775 mtx::xml::ebml_chapters_converter_c::write_xml(*chapters, out);
776
777 } catch (mtx::mm_io::exception &) {
778 Util::MessageBox::critical(this)->title(QY("Saving failed")).text(QY("Creating the file failed. Check to make sure you have permission to write to that directory and that the drive is not full.")).exec();
779 return false;
780
781 } catch (mtx::xml::conversion_x &ex) {
782 Util::MessageBox::critical(this)->title(QY("Saving failed")).text(QY("Converting the chapters to XML failed: %1").arg(ex.what())).exec();
783 return false;
784 }
785
786 return true;
787 });
788 }
789
790 void
saveToMatroska()791 Tab::saveToMatroska() {
792 saveToMatroskaImpl(true);
793 }
794
795 void
saveToMatroskaImpl(bool requireNewFileName)796 Tab::saveToMatroskaImpl(bool requireNewFileName) {
797 auto p = p_func();
798
799 saveAsImpl(requireNewFileName, [this, p](bool doRequireNewFileName, QString &newFileName) -> bool {
800 if (!p->analyzer)
801 doRequireNewFileName = true;
802
803 if (doRequireNewFileName) {
804 auto defaultFilePath = !p->fileName.isEmpty() ? QFileInfo{p->fileName}.path() : Util::Settings::get().lastOpenDirPath();
805 newFileName = Util::getSaveFileName(this, QY("Save chapters to Matroska or WebM file"), defaultFilePath, {},
806 QY("Supported file types") + Q(" (*.mkv *.mka *.mks *.mk3d *.webm);;") +
807 QY("Matroska files") + Q(" (*.mkv *.mka *.mks *.mk3d);;") +
808 QY("WebM files") + Q(" (*.webm);;") +
809 QY("All files") + Q(" (*)"),
810 {}, {}, QFileDialog::DontConfirmOverwrite, QFileDialog::ExistingFile);
811
812 if (newFileName.isEmpty())
813 return false;
814 }
815
816 if (doRequireNewFileName || (QFileInfo{newFileName}.lastModified() != p->fileModificationTime)) {
817 p->analyzer = std::make_unique<Util::KaxAnalyzer>(this, newFileName);
818 if (!p->analyzer->set_parse_mode(kax_analyzer_c::parse_mode_fast).process()) {
819 auto text = Q("%1 %2")
820 .arg(QY("The file you tried to open (%1) could not be read successfully.").arg(newFileName))
821 .arg(QY("Possible reasons are: the file is not a Matroska file; the file is write-protected; the file is locked by another process; you do not have permission to access the file."));
822 Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(text).exec();
823 return false;
824 }
825
826 p->fileName = newFileName;
827 }
828
829 auto chapters = p->chapterModel->allChapters();
830 auto result = kax_analyzer_c::uer_success;
831
832 if (chapters && (0 != chapters->ListSize())) {
833 fix_mandatory_elements(chapters.get());
834 if (p->analyzer->is_webm())
835 mtx::chapters::remove_elements_unsupported_by_webm(*chapters);
836
837 remove_mandatory_elements_set_to_their_default(*chapters);
838
839 result = p->analyzer->update_element(chapters, false, false);
840
841 } else
842 result = p->analyzer->remove_elements(EBML_ID(KaxChapters));
843
844 p->analyzer->close_file();
845
846 if (kax_analyzer_c::uer_success != result) {
847 Util::KaxAnalyzer::displayUpdateElementResult(this, result, QY("Saving the chapters failed."));
848 return false;
849 }
850
851 p->fileModificationTime = QFileInfo{p->fileName}.lastModified();
852
853 return true;
854 });
855 }
856
857 void
selectChapterRow(QModelIndex const & idx,bool ignoreSelectionChanges)858 Tab::selectChapterRow(QModelIndex const &idx,
859 bool ignoreSelectionChanges) {
860 auto p = p_func();
861 auto selection = QItemSelection{idx.sibling(idx.row(), 0), idx.sibling(idx.row(), p->chapterModel->columnCount() - 1)};
862
863 p->ignoreChapterSelectionChanges = ignoreSelectionChanges;
864 p->ui->elements->selectionModel()->setCurrentIndex(idx.sibling(idx.row(), 0), QItemSelectionModel::ClearAndSelect);
865 p->ui->elements->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect);
866 p->ignoreChapterSelectionChanges = false;
867 }
868
869 bool
copyControlsToStorage()870 Tab::copyControlsToStorage() {
871 auto p = p_func();
872 auto idx = Util::selectedRowIdx(p->ui->elements);
873 return idx.isValid() ? copyControlsToStorage(idx) : true;
874 }
875
876 bool
copyControlsToStorage(QModelIndex const & idx)877 Tab::copyControlsToStorage(QModelIndex const &idx) {
878 auto p = p_func();
879 auto result = copyControlsToStorageImpl(idx);
880
881 if (result.first) {
882 p->chapterModel->updateRow(idx);
883 return true;
884 }
885
886 selectChapterRow(idx, true);
887
888 Util::MessageBox::critical(this)->title(QY("Validation failed")).text(result.second).exec();
889
890 return false;
891 }
892
893 Tab::ValidationResult
copyControlsToStorageImpl(QModelIndex const & idx)894 Tab::copyControlsToStorageImpl(QModelIndex const &idx) {
895 auto p = p_func();
896 auto stdItem = p->chapterModel->itemFromIndex(idx);
897
898 if (!stdItem)
899 return { true, QString{} };
900
901 if (!idx.parent().isValid())
902 return copyEditionControlsToStorage(p->chapterModel->editionFromItem(stdItem));
903
904 return copyChapterControlsToStorage(p->chapterModel->chapterFromItem(stdItem));
905 }
906
907 QString
fixAndGetTimestampString(QLineEdit & lineEdit)908 Tab::fixAndGetTimestampString(QLineEdit &lineEdit) {
909 auto timestampText = lineEdit.text();
910
911 if (timestampText.contains(Q(','))) {
912 timestampText.replace(Q(','), Q('.'));
913 lineEdit.setText(timestampText);
914 }
915
916 return timestampText;
917 }
918
919 Tab::ValidationResult
copyChapterControlsToStorage(ChapterPtr const & chapter)920 Tab::copyChapterControlsToStorage(ChapterPtr const &chapter) {
921 auto p = p_func();
922
923 if (!chapter)
924 return { true, QString{} };
925
926 auto uid = uint64_t{};
927
928 if (!p->ui->leChUid->text().isEmpty()) {
929 auto ok = false;
930 uid = p->ui->leChUid->text().toULongLong(&ok);
931 if (!ok)
932 return { false, QY("The chapter UID must be a number if given.") };
933 }
934
935 if (uid)
936 GetChild<KaxChapterUID>(*chapter).SetValue(uid);
937 else
938 DeleteChildren<KaxChapterUID>(*chapter);
939
940 if (!p->ui->cbChFlagEnabled->isChecked())
941 GetChild<KaxChapterFlagEnabled>(*chapter).SetValue(0);
942 else
943 DeleteChildren<KaxChapterFlagEnabled>(*chapter);
944
945 if (p->ui->cbChFlagHidden->isChecked())
946 GetChild<KaxChapterFlagHidden>(*chapter).SetValue(1);
947 else
948 DeleteChildren<KaxChapterFlagHidden>(*chapter);
949
950 auto startTimestamp = int64_t{};
951 if (!mtx::string::parse_timestamp(to_utf8(fixAndGetTimestampString(*p->ui->leChStart)), startTimestamp))
952 return { false, QY("The start time could not be parsed: %1").arg(Q(mtx::string::timestamp_parser_error)) };
953 GetChild<KaxChapterTimeStart>(*chapter).SetValue(startTimestamp);
954
955 if (!p->ui->leChEnd->text().isEmpty()) {
956 auto endTimestamp = int64_t{};
957 if (!mtx::string::parse_timestamp(to_utf8(fixAndGetTimestampString(*p->ui->leChEnd)), endTimestamp))
958 return { false, QY("The end time could not be parsed: %1").arg(Q(mtx::string::timestamp_parser_error)) };
959
960 if (endTimestamp <= startTimestamp)
961 return { false, QY("The end time must be greater than the start time.") };
962
963 GetChild<KaxChapterTimeEnd>(*chapter).SetValue(endTimestamp);
964
965 } else
966 DeleteChildren<KaxChapterTimeEnd>(*chapter);
967
968 if (!p->ui->leChSegmentUid->text().isEmpty()) {
969 try {
970 auto value = mtx::bits::value_c{to_utf8(p->ui->leChSegmentUid->text())};
971 GetChild<KaxChapterSegmentUID>(*chapter).CopyBuffer(value.data(), value.byte_size());
972
973 } catch (mtx::bits::value_parser_x const &ex) {
974 return { false, QY("The segment UID could not be parsed: %1").arg(ex.what()) };
975 }
976
977 } else
978 DeleteChildren<KaxChapterSegmentUID>(*chapter);
979
980 if (!p->ui->leChSegmentEditionUid->text().isEmpty()) {
981 auto ok = false;
982 uid = p->ui->leChSegmentEditionUid->text().toULongLong(&ok);
983 if (!ok || !uid)
984 return { false, QY("The segment edition UID must be a positive number if given.") };
985
986 GetChild<KaxChapterSegmentEditionUID>(*chapter).SetValue(uid);
987 }
988
989 RemoveChildren<KaxChapterDisplay>(*chapter);
990 for (auto row = 0, numRows = p->nameModel->rowCount(); row < numRows; ++row)
991 chapter->PushElement(*p->nameModel->displayFromIndex(p->nameModel->index(row, 0)));
992
993 return { true, QString{} };
994 }
995
996 Tab::ValidationResult
copyEditionControlsToStorage(EditionPtr const & edition)997 Tab::copyEditionControlsToStorage(EditionPtr const &edition) {
998 auto p = p_func();
999
1000 if (!edition)
1001 return { true, QString{} };
1002
1003 auto uid = uint64_t{};
1004
1005 if (!p->ui->leEdUid->text().isEmpty()) {
1006 auto ok = false;
1007 uid = p->ui->leEdUid->text().toULongLong(&ok);
1008 if (!ok)
1009 return { false, QY("The edition UID must be a number if given.") };
1010 }
1011
1012 if (uid)
1013 GetChild<KaxEditionUID>(*edition).SetValue(uid);
1014 else
1015 DeleteChildren<KaxEditionUID>(*edition);
1016
1017 if (p->ui->cbEdFlagDefault->isChecked())
1018 GetChild<KaxEditionFlagDefault>(*edition).SetValue(1);
1019 else
1020 DeleteChildren<KaxEditionFlagDefault>(*edition);
1021
1022 if (p->ui->cbEdFlagHidden->isChecked())
1023 GetChild<KaxEditionFlagHidden>(*edition).SetValue(1);
1024 else
1025 DeleteChildren<KaxEditionFlagHidden>(*edition);
1026
1027 if (p->ui->cbEdFlagOrdered->isChecked())
1028 GetChild<KaxEditionFlagOrdered>(*edition).SetValue(1);
1029 else
1030 DeleteChildren<KaxEditionFlagOrdered>(*edition);
1031
1032 return { true, QString{} };
1033 }
1034
1035 bool
setControlsFromStorage()1036 Tab::setControlsFromStorage() {
1037 auto p = p_func();
1038 auto idx = Util::selectedRowIdx(p->ui->elements);
1039
1040 return idx.isValid() ? setControlsFromStorage(idx) : true;
1041 }
1042
1043 bool
setControlsFromStorage(QModelIndex const & idx)1044 Tab::setControlsFromStorage(QModelIndex const &idx) {
1045 auto p = p_func();
1046 auto stdItem = p->chapterModel->itemFromIndex(idx);
1047
1048 if (!stdItem)
1049 return false;
1050
1051 if (!idx.parent().isValid())
1052 return setEditionControlsFromStorage(p->chapterModel->editionFromItem(stdItem));
1053 return setChapterControlsFromStorage(p->chapterModel->chapterFromItem(stdItem));
1054 }
1055
1056 bool
setChapterControlsFromStorage(ChapterPtr const & chapter)1057 Tab::setChapterControlsFromStorage(ChapterPtr const &chapter) {
1058 auto p = p_func();
1059
1060 if (!chapter)
1061 return true;
1062
1063 auto uid = FindChildValue<KaxChapterUID>(*chapter);
1064 auto end = FindChild<KaxChapterTimeEnd>(*chapter);
1065 auto segmentEditionUid = FindChild<KaxChapterSegmentEditionUID>(*chapter);
1066
1067 p->ui->lChapter->setText(p->chapterModel->chapterDisplayName(*chapter));
1068 p->ui->leChStart->setText(Q(mtx::string::format_timestamp(FindChildValue<KaxChapterTimeStart>(*chapter))));
1069 p->ui->leChEnd->setText(end ? Q(mtx::string::format_timestamp(end->GetValue())) : Q(""));
1070 p->ui->cbChFlagEnabled->setChecked(!!FindChildValue<KaxChapterFlagEnabled>(*chapter, 1));
1071 p->ui->cbChFlagHidden->setChecked(!!FindChildValue<KaxChapterFlagHidden>(*chapter));
1072 p->ui->leChUid->setText(uid ? QString::number(uid) : Q(""));
1073 p->ui->leChSegmentUid->setText(formatEbmlBinary(FindChild<KaxChapterSegmentUID>(*chapter)));
1074 p->ui->leChSegmentEditionUid->setText(segmentEditionUid ? QString::number(segmentEditionUid->GetValue()) : Q(""));
1075
1076 auto nameSelectionModel = p->ui->tvChNames->selectionModel();
1077 auto previouslySelectedNameIdx = nameSelectionModel->currentIndex();
1078 p->nameModel->populate(*chapter);
1079 enableNameWidgets(false);
1080
1081 if (p->nameModel->rowCount()) {
1082 auto oldBlocked = nameSelectionModel->blockSignals(true);
1083 auto rowToSelect = std::min(previouslySelectedNameIdx.isValid() ? previouslySelectedNameIdx.row() : 0, p->nameModel->rowCount());
1084 auto selection = QItemSelection{ p->nameModel->index(rowToSelect, 0), p->nameModel->index(rowToSelect, p->nameModel->columnCount() - 1) };
1085
1086 nameSelectionModel->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
1087
1088 setNameControlsFromStorage(p->nameModel->index(rowToSelect, 0));
1089 enableNameWidgets(true);
1090
1091 nameSelectionModel->blockSignals(oldBlocked);
1092 }
1093
1094 p->ui->pageContainer->setCurrentWidget(p->ui->chapterPage);
1095
1096 return true;
1097 }
1098
1099 bool
setEditionControlsFromStorage(EditionPtr const & edition)1100 Tab::setEditionControlsFromStorage(EditionPtr const &edition) {
1101 auto p = p_func();
1102
1103 if (!edition)
1104 return true;
1105
1106 auto uid = FindChildValue<KaxEditionUID>(*edition);
1107
1108 p->ui->leEdUid->setText(uid ? QString::number(uid) : Q(""));
1109 p->ui->cbEdFlagDefault->setChecked(!!FindChildValue<KaxEditionFlagDefault>(*edition));
1110 p->ui->cbEdFlagHidden->setChecked(!!FindChildValue<KaxEditionFlagHidden>(*edition));
1111 p->ui->cbEdFlagOrdered->setChecked(!!FindChildValue<KaxEditionFlagOrdered>(*edition));
1112
1113 p->ui->pageContainer->setCurrentWidget(p->ui->editionPage);
1114
1115 return true;
1116 }
1117
1118 bool
handleChapterDeselection(QItemSelection const & deselected)1119 Tab::handleChapterDeselection(QItemSelection const &deselected) {
1120 if (deselected.isEmpty())
1121 return true;
1122
1123 auto indexes = deselected.at(0).indexes();
1124 return indexes.isEmpty() ? true : copyControlsToStorage(indexes.at(0));
1125 }
1126
1127 void
chapterSelectionChanged(QItemSelection const & selected,QItemSelection const & deselected)1128 Tab::chapterSelectionChanged(QItemSelection const &selected,
1129 QItemSelection const &deselected) {
1130 auto p = p_func();
1131 auto selectedIdx = QModelIndex{};
1132
1133 if (!selected.isEmpty()) {
1134 auto indexes = selected.at(0).indexes();
1135 if (!indexes.isEmpty())
1136 selectedIdx = indexes.at(0);
1137 }
1138
1139 p->chapterModel->setSelectedIdx(selectedIdx);
1140
1141 if (p->ignoreChapterSelectionChanges)
1142 return;
1143
1144 if (!handleChapterDeselection(deselected))
1145 return;
1146
1147 if (selectedIdx.isValid() && setControlsFromStorage(selectedIdx.sibling(selectedIdx.row(), 0)))
1148 return;
1149
1150 p->ui->pageContainer->setCurrentWidget(p->ui->emptyPage);
1151 }
1152
1153 bool
setNameControlsFromStorage(QModelIndex const & idx)1154 Tab::setNameControlsFromStorage(QModelIndex const &idx) {
1155 auto &p = *p_func();
1156 auto display = p.nameModel->displayFromIndex(idx);
1157
1158 if (!display)
1159 return false;
1160
1161 // qDebug() << "setNameControlsFromStorage start";
1162
1163 // log_it(ebml_dumper_c::dump_to_string(display));
1164
1165 p.ignoreChapterNameChanges = true;
1166
1167 p.ui->leChName->setText(Q(GetChildValue<KaxChapterString>(display)));
1168
1169 auto lists = NameModel::effectiveLanguagesForDisplay(*display);
1170 auto usedLanguageCodes = usedNameLanguages();
1171
1172 std::sort(lists.languageCodes.begin(), lists.languageCodes.end(), [](auto const &a, auto const &b) { return a.format_long() < b.format_long(); });
1173
1174 for (int languageIdx = 1, numLanguages = p.languageControls.size(); languageIdx < numLanguages; ++languageIdx) {
1175 auto &[layout, language, button] = p.languageControls[languageIdx];
1176 delete layout;
1177 delete language;
1178 delete button;
1179 }
1180
1181 p.languageControls.remove(1, p.languageControls.size() - 1);
1182
1183 p.ui->ldwChNameLanguage1->setAdditionalLanguages(usedLanguageCodes);
1184 p.ui->ldwChNameLanguage1->setLanguage(lists.languageCodes.isEmpty() ? mtx::bcp47::language_c{} : lists.languageCodes[0]);
1185
1186 for (int languageIdx = 1, numLanguages = lists.languageCodes.size(); languageIdx < numLanguages; ++languageIdx)
1187 addOneChapterNameLanguage(lists.languageCodes[languageIdx], usedLanguageCodes);
1188
1189 p.ignoreChapterNameChanges = false;
1190
1191 // qDebug() << "setNameControlsFromStorage end";
1192
1193 return true;
1194 }
1195
1196 void
nameSelectionChanged(QItemSelection const & selected,QItemSelection const &)1197 Tab::nameSelectionChanged(QItemSelection const &selected,
1198 QItemSelection const &) {
1199 auto p = p_func();
1200
1201 if (!selected.isEmpty()) {
1202 auto indexes = selected.at(0).indexes();
1203 if (!indexes.isEmpty() && setNameControlsFromStorage(indexes.at(0))) {
1204 enableNameWidgets(true);
1205
1206 p->ui->leChName->selectAll();
1207 QTimer::singleShot(0, p->ui->leChName, [p]() { p->ui->leChName->setFocus(); });
1208
1209 return;
1210 }
1211 }
1212
1213 enableNameWidgets(false);
1214 }
1215
1216 void
withSelectedName(std::function<void (QModelIndex const &,KaxChapterDisplay &)> const & worker)1217 Tab::withSelectedName(std::function<void(QModelIndex const &, KaxChapterDisplay &)> const &worker) {
1218 auto p = p_func();
1219 auto selectedRows = p->ui->tvChNames->selectionModel()->selectedRows();
1220
1221 if (selectedRows.isEmpty())
1222 return;
1223
1224 auto idx = selectedRows.at(0);
1225 auto display = p->nameModel->displayFromIndex(idx);
1226 if (display)
1227 worker(idx, *display);
1228 }
1229
1230 void
chapterNameEdited(QString const & text)1231 Tab::chapterNameEdited(QString const &text) {
1232 auto p = p_func();
1233
1234 withSelectedName([p, &text](QModelIndex const &idx, KaxChapterDisplay &display) {
1235 GetChild<KaxChapterString>(display).SetValueUTF8(to_utf8(text));
1236 p->nameModel->updateRow(idx.row());
1237 });
1238 }
1239
1240 void
chapterNameLanguageChanged()1241 Tab::chapterNameLanguageChanged() {
1242 auto &p = *p_func();
1243
1244 // qDebug() << "chapterNameLanguageChanged start";
1245
1246 if (p.ignoreChapterNameChanges) {
1247 // qDebug() << "chapterNameLanguageChanged ignoring";
1248 return;
1249 }
1250
1251 std::vector<mtx::bcp47::language_c> languageCodes;
1252
1253 for (auto const &control : p.languageControls) {
1254 auto languageCode = static_cast<mtx::gui::Util::LanguageDisplayWidget *>(std::get<1>(control))->language();
1255 if (languageCode.is_valid())
1256 languageCodes.emplace_back(languageCode);
1257 }
1258
1259 std::sort(languageCodes.begin(), languageCodes.end());
1260
1261 withSelectedName([&p, &languageCodes](QModelIndex const &idx, KaxChapterDisplay &display) {
1262 mtx::chapters::set_languages_in_display(display, languageCodes);
1263
1264 p.nameModel->updateRow(idx.row());
1265 });
1266
1267 // qDebug() << "chapterNameLanguageChanged end";
1268 }
1269
1270 void
addChapterName()1271 Tab::addChapterName() {
1272 auto p = p_func();
1273
1274 p->nameModel->addNew();
1275 }
1276
1277 void
removeChapterName()1278 Tab::removeChapterName() {
1279 auto p = p_func();
1280 auto idx = Util::selectedRowIdx(p->ui->tvChNames);
1281
1282 if (idx.isValid())
1283 p->nameModel->remove(idx);
1284 }
1285
1286 void
addChapterNameLanguage()1287 Tab::addChapterNameLanguage() {
1288 addOneChapterNameLanguage({}, usedNameLanguages());
1289 }
1290
1291 void
addOneChapterNameLanguage(mtx::bcp47::language_c const & languageCode,QStringList const & usedLanguageCodes)1292 Tab::addOneChapterNameLanguage(mtx::bcp47::language_c const &languageCode,
1293 QStringList const &usedLanguageCodes) {
1294 auto &p = *p_func();
1295
1296 auto language = new mtx::gui::Util::LanguageDisplayWidget{p.ui->wChNameLanguages};
1297 auto removeButton = new QPushButton{p.ui->wChNameLanguages};
1298 auto layout = new QHBoxLayout;
1299
1300 language->setAdditionalLanguages(usedLanguageCodes);
1301 language->setLanguage(languageCode);
1302
1303 QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
1304 language->setSizePolicy(sizePolicy);
1305
1306 QIcon removeIcon;
1307 removeIcon.addFile(QString::fromUtf8(":/icons/16x16/list-remove.png"), QSize(), QIcon::Normal, QIcon::Off);
1308
1309 removeButton->setIcon(removeIcon);
1310
1311 layout->addWidget(language);
1312 layout->addWidget(removeButton);
1313
1314 static_cast<QBoxLayout *>(p.ui->wChNameLanguages->layout())->addLayout(layout);
1315
1316 p.languageControls.push_back({ layout, language, removeButton });
1317
1318 connect(language, &Util::LanguageDisplayWidget::languageChanged, this, &Tab::chapterNameLanguageChanged);
1319 }
1320
1321 void
removeChapterNameLanguage()1322 Tab::removeChapterNameLanguage() {
1323 auto &p = *p_func();
1324
1325 if (removeLanguageControlRow(p.languageControls, sender()))
1326 chapterNameLanguageChanged();
1327 }
1328
1329 void
enableNameWidgets(bool enable)1330 Tab::enableNameWidgets(bool enable) {
1331 auto p = p_func();
1332
1333 for (auto const &widget : p->nameWidgets)
1334 widget->setEnabled(enable);
1335
1336 for (auto const &widget : p->ui->wChNameLanguages->findChildren<QWidget *>())
1337 widget->setEnabled(enable);
1338 }
1339
1340 void
expandAll()1341 Tab::expandAll() {
1342 expandCollapseAll(true);
1343 }
1344
1345 void
collapseAll()1346 Tab::collapseAll() {
1347 expandCollapseAll(false);
1348 }
1349
1350 void
addEditionBefore()1351 Tab::addEditionBefore() {
1352 addEdition(true);
1353 }
1354
1355 void
addEditionAfter()1356 Tab::addEditionAfter() {
1357 addEdition(false);
1358 }
1359
1360 QModelIndex
addEdition(bool before)1361 Tab::addEdition(bool before) {
1362 auto p = p_func();
1363 auto edition = std::make_shared<KaxEditionEntry>();
1364 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1365 auto row = 0;
1366
1367 if (selectedIdx.isValid()) {
1368 while (selectedIdx.parent().isValid())
1369 selectedIdx = selectedIdx.parent();
1370
1371 row = selectedIdx.row() + (before ? 0 : 1);
1372 }
1373
1374 GetChild<KaxEditionUID>(*edition).SetValue(0);
1375
1376 p->chapterModel->insertEdition(row, edition);
1377
1378 Q_EMIT numberOfEntriesChanged();
1379
1380 return p->chapterModel->index(row, 0);
1381 }
1382
1383 void
addEditionOrChapterAfter()1384 Tab::addEditionOrChapterAfter() {
1385 auto p = p_func();
1386 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1387 auto hasSelection = selectedIdx.isValid();
1388 auto chapterSelected = hasSelection && selectedIdx.parent().isValid();
1389
1390 if (!hasSelection)
1391 return;
1392
1393 auto newEntryIdx = chapterSelected ? addChapter(false) : addEdition(false);
1394 selectChapterRow(newEntryIdx, false);
1395 }
1396
1397 ChapterPtr
createEmptyChapter(int64_t startTime,int chapterNumber,std::optional<QString> const & nameTemplate,mtx::bcp47::language_c const & language)1398 Tab::createEmptyChapter(int64_t startTime,
1399 int chapterNumber,
1400 std::optional<QString> const &nameTemplate,
1401 mtx::bcp47::language_c const &language) {
1402 auto &cfg = Util::Settings::get();
1403 auto chapter = std::make_shared<KaxChapterAtom>();
1404 auto name = formatChapterName(nameTemplate ? *nameTemplate : cfg.m_chapterNameTemplate, chapterNumber, timestamp_c::ns(startTime));
1405
1406 GetChild<KaxChapterUID>(*chapter).SetValue(0);
1407 GetChild<KaxChapterTimeStart>(*chapter).SetValue(startTime);
1408 if (!name.isEmpty()) {
1409 auto &display = GetChild<KaxChapterDisplay>(*chapter);
1410 auto actual_language = language.is_valid() ? language : cfg.m_defaultChapterLanguage;
1411
1412 GetChild<KaxChapterString>(display).SetValue(to_wide(name));
1413 mtx::chapters::set_languages_in_display(display, actual_language);
1414 }
1415
1416 return chapter;
1417 }
1418
1419 void
addChapterBefore()1420 Tab::addChapterBefore() {
1421 addChapter(true);
1422 }
1423
1424 void
addChapterAfter()1425 Tab::addChapterAfter() {
1426 addChapter(false);
1427 }
1428
1429 QModelIndex
addChapter(bool before)1430 Tab::addChapter(bool before) {
1431 auto p = p_func();
1432 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1433
1434 if (!selectedIdx.isValid() || !selectedIdx.parent().isValid())
1435 return {};
1436
1437 // TODO: Tab::addChapter: start time
1438 auto row = selectedIdx.row() + (before ? 0 : 1);
1439 auto chapter = createEmptyChapter(0, row + 1);
1440
1441 p->chapterModel->insertChapter(row, chapter, selectedIdx.parent());
1442
1443 Q_EMIT numberOfEntriesChanged();
1444
1445 return p->chapterModel->index(row, 0, selectedIdx.parent());
1446 }
1447
1448 void
addSubChapter()1449 Tab::addSubChapter() {
1450 auto p = p_func();
1451 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1452
1453 if (!selectedIdx.isValid())
1454 return;
1455
1456 // TODO: Tab::addSubChapter: start time
1457 auto selectedItem = p->chapterModel->itemFromIndex(selectedIdx);
1458 auto chapter = createEmptyChapter(0, (selectedItem ? selectedItem->rowCount() : 0) + 1);
1459
1460 p->chapterModel->appendChapter(chapter, selectedIdx);
1461 expandCollapseAll(true, selectedIdx);
1462
1463 Q_EMIT numberOfEntriesChanged();
1464 }
1465
1466 void
removeElement()1467 Tab::removeElement() {
1468 auto p = p_func();
1469
1470 p->chapterModel->removeTree(Util::selectedRowIdx(p->ui->elements));
1471 Q_EMIT numberOfEntriesChanged();
1472 }
1473
1474 void
applyModificationToTimestamps(QStandardItem * item,std::function<int64_t (int64_t)> const & unaryOp)1475 Tab::applyModificationToTimestamps(QStandardItem *item,
1476 std::function<int64_t(int64_t)> const &unaryOp) {
1477 auto p = p_func();
1478
1479 if (!item)
1480 return;
1481
1482 if (item->parent()) {
1483 auto chapter = p->chapterModel->chapterFromItem(item);
1484 if (chapter) {
1485 auto kStart = FindChild<KaxChapterTimeStart>(*chapter);
1486 auto kEnd = FindChild<KaxChapterTimeEnd>(*chapter);
1487
1488 if (kStart)
1489 kStart->SetValue(std::max<int64_t>(unaryOp(static_cast<int64_t>(kStart->GetValue())), 0));
1490 if (kEnd)
1491 kEnd->SetValue(std::max<int64_t>(unaryOp(static_cast<int64_t>(kEnd->GetValue())), 0));
1492
1493 if (kStart || kEnd)
1494 p->chapterModel->updateRow(item->index());
1495 }
1496 }
1497
1498 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row)
1499 applyModificationToTimestamps(item->child(row), unaryOp);
1500 }
1501
1502 void
multiplyTimestamps(QStandardItem * item,double factor)1503 Tab::multiplyTimestamps(QStandardItem *item,
1504 double factor) {
1505 applyModificationToTimestamps(item, [=](int64_t timestamp) { return static_cast<int64_t>((timestamp * factor) * 10.0 + 5) / 10ll; });
1506 }
1507
1508 void
shiftTimestamps(QStandardItem * item,int64_t delta)1509 Tab::shiftTimestamps(QStandardItem *item,
1510 int64_t delta) {
1511 applyModificationToTimestamps(item, [=](int64_t timestamp) { return timestamp + delta; });
1512 }
1513
1514 void
constrictTimestamps(QStandardItem * item,std::optional<uint64_t> const & constrictStart,std::optional<uint64_t> const & constrictEnd)1515 Tab::constrictTimestamps(QStandardItem *item,
1516 std::optional<uint64_t> const &constrictStart,
1517 std::optional<uint64_t> const &constrictEnd) {
1518 auto p = p_func();
1519
1520 if (!item)
1521 return;
1522
1523 auto chapter = item->parent() ? p->chapterModel->chapterFromItem(item) : ChapterPtr{};
1524 if (!chapter) {
1525 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row)
1526 constrictTimestamps(item->child(row), {}, {});
1527 return;
1528 }
1529
1530 auto kStart = &GetChild<KaxChapterTimeStart>(*chapter);
1531 auto kEnd = FindChild<KaxChapterTimeEnd>(*chapter);
1532 auto newStart = !constrictStart ? kStart->GetValue()
1533 : !constrictEnd ? std::max(*constrictStart, kStart->GetValue())
1534 : std::min(*constrictEnd, std::max(*constrictStart, kStart->GetValue()));
1535 auto newEnd = !kEnd ? std::optional<uint64_t>{}
1536 : !constrictEnd ? std::max(newStart, kEnd->GetValue())
1537 : std::max(newStart, std::min(*constrictEnd, kEnd->GetValue()));
1538
1539 kStart->SetValue(newStart);
1540 if (newEnd)
1541 GetChild<KaxChapterTimeEnd>(*chapter).SetValue(*newEnd);
1542
1543 p->chapterModel->updateRow(item->index());
1544
1545 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row)
1546 constrictTimestamps(item->child(row), newStart, newEnd);
1547 }
1548
1549 std::pair<std::optional<uint64_t>, std::optional<uint64_t>>
expandTimestamps(QStandardItem * item)1550 Tab::expandTimestamps(QStandardItem *item) {
1551 auto p = p_func();
1552
1553 if (!item)
1554 return {};
1555
1556 auto chapter = item->parent() ? p->chapterModel->chapterFromItem(item) : ChapterPtr{};
1557 if (!chapter) {
1558 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row)
1559 expandTimestamps(item->child(row));
1560 return {};
1561 }
1562
1563 auto kStart = chapter ? FindChild<KaxChapterTimeStart>(*chapter) : nullptr;
1564 auto kEnd = chapter ? FindChild<KaxChapterTimeEnd>(*chapter) : nullptr;
1565 auto newStart = kStart ? std::optional<uint64_t>{kStart->GetValue()} : std::optional<uint64_t>{};
1566 auto newEnd = kEnd ? std::optional<uint64_t>{kEnd->GetValue()} : std::optional<uint64_t>{};
1567 auto modified = false;
1568
1569 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row) {
1570 auto startAndEnd = expandTimestamps(item->child(row));
1571
1572 if (!newStart || (startAndEnd.first && (*startAndEnd.first < *newStart)))
1573 newStart = startAndEnd.first;
1574
1575 if (!newEnd || (startAndEnd.second && (*startAndEnd.second > *newEnd)))
1576 newEnd = startAndEnd.second;
1577 }
1578
1579 if (newStart && (!kStart || (kStart->GetValue() > *newStart))) {
1580 GetChild<KaxChapterTimeStart>(*chapter).SetValue(*newStart);
1581 modified = true;
1582 }
1583
1584 if (newEnd && (!kEnd || (kEnd->GetValue() < *newEnd))) {
1585 GetChild<KaxChapterTimeEnd>(*chapter).SetValue(*newEnd);
1586 modified = true;
1587 }
1588
1589 if (modified)
1590 p->chapterModel->updateRow(item->index());
1591
1592 return std::make_pair(newStart, newEnd);
1593 }
1594
1595 void
setLanguages(QStandardItem * item,mtx::bcp47::language_c const & language)1596 Tab::setLanguages(QStandardItem *item,
1597 mtx::bcp47::language_c const &language) {
1598 auto p = p_func();
1599
1600 if (!item)
1601 return;
1602
1603 auto chapter = p->chapterModel->chapterFromItem(item);
1604 if (chapter)
1605 for (auto const &element : *chapter) {
1606 auto kDisplay = dynamic_cast<KaxChapterDisplay *>(element);
1607 if (kDisplay)
1608 mtx::chapters::set_languages_in_display(*kDisplay, language);
1609 }
1610
1611 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row)
1612 setLanguages(item->child(row), language);
1613 }
1614
1615 void
setEndTimestamps(QStandardItem * startItem)1616 Tab::setEndTimestamps(QStandardItem *startItem) {
1617 auto p = p_func();
1618
1619 if (!startItem)
1620 return;
1621
1622 auto allAtomData = collectChapterAtomDataForEdition(startItem);
1623
1624 std::function<void(QStandardItem *)> setter = [p, &allAtomData, &setter](QStandardItem *item) {
1625 if (item->parent()) {
1626 auto chapter = p->chapterModel->chapterFromItem(item);
1627 if (chapter) {
1628 auto data = allAtomData[chapter.get()];
1629 if (data && data->calculatedEnd.valid()) {
1630 GetChild<KaxChapterTimeEnd>(*chapter).SetValue(data->calculatedEnd.to_ns());
1631 p->chapterModel->updateRow(item->index());
1632 }
1633 }
1634 }
1635
1636 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row)
1637 setter(item->child(row));
1638 };
1639
1640 setter(startItem);
1641
1642 setControlsFromStorage();
1643 }
1644
1645 void
removeEndTimestamps(QStandardItem * startItem)1646 Tab::removeEndTimestamps(QStandardItem *startItem) {
1647 auto chapterModel = p_func()->chapterModel;
1648
1649 if (!startItem)
1650 return;
1651
1652 std::function<void(QStandardItem *)> setter = [chapterModel, &setter](QStandardItem *item) {
1653 auto chapter = chapterModel->chapterFromItem(item);
1654 if (chapter) {
1655 DeleteChildren<KaxChapterTimeEnd>(*chapter);
1656 chapterModel->updateRow(item->index());
1657 }
1658
1659 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row)
1660 setter(item->child(row));
1661 };
1662
1663 setter(startItem);
1664
1665 setControlsFromStorage();
1666 }
1667
1668 void
removeNames(QStandardItem * startItem)1669 Tab::removeNames(QStandardItem *startItem) {
1670 auto chapterModel = p_func()->chapterModel;
1671
1672 if (!startItem)
1673 return;
1674
1675 std::function<void(QStandardItem *)> worker = [chapterModel, &worker](QStandardItem *item) {
1676 auto chapter = chapterModel->chapterFromItem(item);
1677 if (chapter) {
1678 DeleteChildren<KaxChapterDisplay>(*chapter);
1679 chapterModel->updateRow(item->index());
1680 }
1681
1682 for (auto row = 0, numRows = item->rowCount(); row < numRows; ++row)
1683 worker(item->child(row));
1684 };
1685
1686 worker(startItem);
1687
1688 setControlsFromStorage();
1689 }
1690
1691 void
massModify()1692 Tab::massModify() {
1693 auto p = p_func();
1694
1695 if (!copyControlsToStorage())
1696 return;
1697
1698 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1699 auto item = selectedIdx.isValid() ? p->chapterModel->itemFromIndex(selectedIdx) : p->chapterModel->invisibleRootItem();
1700
1701 MassModificationDialog dlg{this, selectedIdx.isValid(), usedNameLanguages()};
1702 if (!dlg.exec())
1703 return;
1704
1705 auto actions = dlg.actions();
1706
1707 if (actions & MassModificationDialog::Shift)
1708 shiftTimestamps(item, dlg.shiftBy());
1709
1710 if (actions & MassModificationDialog::Multiply)
1711 multiplyTimestamps(item, dlg.multiplyBy());
1712
1713 if (actions & MassModificationDialog::Constrict)
1714 constrictTimestamps(item, {}, {});
1715
1716 if (actions & MassModificationDialog::Expand)
1717 expandTimestamps(item);
1718
1719 if (actions & MassModificationDialog::SetLanguage)
1720 setLanguages(item, dlg.language());
1721
1722 if (actions & MassModificationDialog::Sort)
1723 item->sortChildren(1);
1724
1725 if (actions & MassModificationDialog::SetEndTimestamps)
1726 setEndTimestamps(item);
1727
1728 if (actions & MassModificationDialog::RemoveEndTimestamps)
1729 removeEndTimestamps(item);
1730
1731 if (actions & MassModificationDialog::RemoveNames)
1732 removeNames(item);
1733
1734 setControlsFromStorage();
1735 }
1736
1737 void
duplicateElement()1738 Tab::duplicateElement() {
1739 auto p = p_func();
1740 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1741 auto newElementIdx = p->chapterModel->duplicateTree(selectedIdx);
1742
1743 if (newElementIdx.isValid())
1744 expandCollapseAll(true, newElementIdx);
1745
1746 Q_EMIT numberOfEntriesChanged();
1747 }
1748
1749 void
copyElementToOtherTab()1750 Tab::copyElementToOtherTab() {
1751 auto p = p_func();
1752 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1753 auto tabIdx = static_cast<QAction *>(sender())->data().toInt();
1754 auto otherTabs = MainWindow::chapterEditorTool()->tabs();
1755
1756 otherTabs.removeOne(this);
1757
1758 if ( !selectedIdx.isValid()
1759 || (tabIdx < 0)
1760 || (tabIdx > otherTabs.size()))
1761 return;
1762
1763 auto newChapters = p->chapterModel->cloneSubtreeForRetrieval(selectedIdx);
1764
1765 otherTabs[tabIdx]->appendTheseChapters(newChapters);
1766 }
1767
1768 QString
formatChapterName(QString const & nameTemplate,int chapterNumber,timestamp_c const & startTimestamp) const1769 Tab::formatChapterName(QString const &nameTemplate,
1770 int chapterNumber,
1771 timestamp_c const &startTimestamp)
1772 const {
1773 return Q(mtx::chapters::format_name_template(to_utf8(nameTemplate), chapterNumber, startTimestamp));
1774 }
1775
1776 void
generateSubChapters()1777 Tab::generateSubChapters() {
1778 auto p = p_func();
1779 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1780
1781 if (!selectedIdx.isValid())
1782 return;
1783
1784 if (!copyControlsToStorage())
1785 return;
1786
1787 auto selectedItem = p->chapterModel->itemFromIndex(selectedIdx);
1788 auto selectedChapter = p->chapterModel->chapterFromItem(selectedItem);
1789 auto maxEndTimestamp = selectedChapter ? FindChildValue<KaxChapterTimeStart>(*selectedChapter, 0ull) : 0ull;
1790 auto numRows = selectedItem->rowCount();
1791
1792 for (auto row = 0; row < numRows; ++row) {
1793 auto chapter = p->chapterModel->chapterFromItem(selectedItem->child(row));
1794 if (chapter)
1795 maxEndTimestamp = std::max(maxEndTimestamp, std::max(FindChildValue<KaxChapterTimeStart>(*chapter, 0ull), FindChildValue<KaxChapterTimeEnd>(*chapter, 0ull)));
1796 }
1797
1798 GenerateSubChaptersParametersDialog dlg{this, numRows + 1, maxEndTimestamp, usedNameLanguages()};
1799 if (!dlg.exec())
1800 return;
1801
1802 auto toCreate = dlg.numberOfEntries();
1803 auto chapterNumber = dlg.firstChapterNumber();
1804 auto timestamp = dlg.startTimestamp();
1805 auto duration = dlg.durationInNs();
1806 auto nameTemplate = dlg.nameTemplate();
1807 auto language = dlg.language();
1808
1809 while (toCreate > 0) {
1810 auto chapter = createEmptyChapter(timestamp, chapterNumber, nameTemplate, language);
1811 timestamp += duration;
1812
1813 ++chapterNumber;
1814 --toCreate;
1815
1816 p->chapterModel->appendChapter(chapter, selectedIdx);
1817 }
1818
1819 expandCollapseAll(true, selectedIdx);
1820
1821 Q_EMIT numberOfEntriesChanged();
1822 }
1823
1824 bool
changeChapterName(QModelIndex const & parentIdx,int row,int chapterNumber,QString const & nameTemplate,RenumberSubChaptersParametersDialog::NameMatch nameMatchingMode,mtx::bcp47::language_c const & languageOfNamesToReplace,bool skipHidden)1825 Tab::changeChapterName(QModelIndex const &parentIdx,
1826 int row,
1827 int chapterNumber,
1828 QString const &nameTemplate,
1829 RenumberSubChaptersParametersDialog::NameMatch nameMatchingMode,
1830 mtx::bcp47::language_c const &languageOfNamesToReplace,
1831 bool skipHidden) {
1832 auto p = p_func();
1833 auto idx = p->chapterModel->index(row, 0, parentIdx);
1834 auto item = p->chapterModel->itemFromIndex(idx);
1835 auto chapter = p->chapterModel->chapterFromItem(item);
1836
1837 if (!chapter)
1838 return false;
1839
1840 if (skipHidden) {
1841 auto flagHidden = FindChild<KaxChapterFlagHidden>(*chapter);
1842 if (flagHidden && flagHidden->GetValue())
1843 return false;
1844 }
1845
1846 auto startTimestamp = FindChildValue<KaxChapterTimeStart>(*chapter);
1847 auto name = to_wide(formatChapterName(nameTemplate, chapterNumber, timestamp_c::ns(startTimestamp)));
1848
1849 if (RenumberSubChaptersParametersDialog::NameMatch::First == nameMatchingMode) {
1850 GetChild<KaxChapterString>(GetChild<KaxChapterDisplay>(*chapter)).SetValue(name);
1851 p->chapterModel->updateRow(idx);
1852
1853 return true;
1854 }
1855
1856 for (auto const &element : *chapter) {
1857 auto kDisplay = dynamic_cast<KaxChapterDisplay *>(element);
1858 if (!kDisplay)
1859 continue;
1860
1861 auto lists = NameModel::effectiveLanguagesForDisplay(*kDisplay);
1862
1863 if ( (RenumberSubChaptersParametersDialog::NameMatch::All == nameMatchingMode)
1864 || (std::find_if(lists.languageCodes.begin(), lists.languageCodes.end(), [&languageOfNamesToReplace](auto const &actualLanguage) {
1865 return (languageOfNamesToReplace == actualLanguage);
1866 }) != lists.languageCodes.end()))
1867 GetChild<KaxChapterString>(*kDisplay).SetValue(name);
1868 }
1869
1870 p->chapterModel->updateRow(idx);
1871
1872 return true;
1873 }
1874
1875 void
renumberSubChapters()1876 Tab::renumberSubChapters() {
1877 auto p = p_func();
1878 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1879
1880 if (!selectedIdx.isValid())
1881 return;
1882
1883 if (!copyControlsToStorage())
1884 return;
1885
1886 auto selectedItem = p->chapterModel->itemFromIndex(selectedIdx);
1887 auto selectedChapter = p->chapterModel->chapterFromItem(selectedItem);
1888 auto numRows = selectedItem->rowCount();
1889 auto chapterTitles = QStringList{};
1890 auto firstName = QString{};
1891
1892 for (auto row = 0; row < numRows; ++row) {
1893 auto chapter = p->chapterModel->chapterFromItem(selectedItem->child(row));
1894 if (!chapter)
1895 continue;
1896
1897 auto start = GetChild<KaxChapterTimeStart>(*chapter).GetValue();
1898 auto end = FindChild<KaxChapterTimeEnd>(*chapter);
1899 auto name = ChapterModel::chapterDisplayName(*chapter);
1900
1901 if (firstName.isEmpty())
1902 firstName = name;
1903
1904 if (end)
1905 chapterTitles << Q("%1 (%2 – %3)").arg(name).arg(Q(mtx::string::format_timestamp(start))).arg(Q(mtx::string::format_timestamp(end->GetValue())));
1906 else
1907 chapterTitles << Q("%1 (%2)").arg(name).arg(Q(mtx::string::format_timestamp(start)));
1908 }
1909
1910 auto matches = QRegularExpression{Q("(\\d+)$")}.match(firstName);
1911 auto firstNumber = matches.hasMatch() ? matches.captured(0).toInt() : 1;
1912 auto usedLanguages = usedNameLanguages(selectedItem);
1913
1914 RenumberSubChaptersParametersDialog dlg{this, firstNumber, chapterTitles, usedLanguages};
1915 if (!dlg.exec())
1916 return;
1917
1918 auto row = dlg.firstEntryToRenumber();
1919 auto toRenumber = dlg.numberOfEntries() ? dlg.numberOfEntries() : numRows;
1920 auto chapterNumber = dlg.firstChapterNumber();
1921 auto nameTemplate = dlg.nameTemplate();
1922 auto nameMatchingMode = dlg.nameMatchingMode();
1923 auto languageToReplace = dlg.languageOfNamesToReplace();
1924 auto skipHidden = dlg.skipHidden();
1925
1926 while ((row < numRows) && (0 < toRenumber)) {
1927 auto renumbered = changeChapterName(selectedIdx, row, chapterNumber, nameTemplate, nameMatchingMode, languageToReplace, skipHidden);
1928
1929 if (renumbered)
1930 ++chapterNumber;
1931 ++row;
1932 }
1933 }
1934
1935 void
expandCollapseAll(bool expand,QModelIndex const & parentIdx)1936 Tab::expandCollapseAll(bool expand,
1937 QModelIndex const &parentIdx) {
1938 Util::expandCollapseAll(p_func()->ui->elements, expand, parentIdx);
1939 }
1940
1941 void
expandInsertedElements(QModelIndex const & parentIdx,int,int)1942 Tab::expandInsertedElements(QModelIndex const &parentIdx,
1943 int,
1944 int) {
1945 expandCollapseAll(true, parentIdx);
1946 }
1947
1948 QString
formatEbmlBinary(EbmlBinary * binary)1949 Tab::formatEbmlBinary(EbmlBinary *binary) {
1950 auto value = std::string{};
1951 auto data = static_cast<unsigned char const *>(binary ? binary->GetBuffer() : nullptr);
1952
1953 if (data)
1954 for (auto end = data + binary->GetSize(); data < end; ++data)
1955 value += fmt::format("{0:02x}", static_cast<unsigned int>(*data));
1956
1957 return Q(value);
1958 }
1959
1960 void
setupCopyToOtherTabMenu()1961 Tab::setupCopyToOtherTabMenu() {
1962 auto p = p_func();
1963 auto idx = 0;
1964 auto otherTabs = MainWindow::chapterEditorTool()->tabs();
1965
1966 otherTabs.removeOne(this);
1967 p->copyToOtherTabMenu->setEnabled(!otherTabs.isEmpty());
1968
1969 p->copyToOtherTabMenu->clear();
1970
1971 for (auto const &otherTab : otherTabs) {
1972 auto action = new QAction{p->copyToOtherTabMenu};
1973 action->setText(otherTab->title());
1974 action->setData(idx);
1975
1976 p->copyToOtherTabMenu->addAction(action);
1977
1978 connect(action, &QAction::triggered, this, &Tab::copyElementToOtherTab);
1979
1980 ++idx;
1981 }
1982 }
1983
1984 void
showChapterContextMenu(QPoint const & pos)1985 Tab::showChapterContextMenu(QPoint const &pos) {
1986 auto p = p_func();
1987 auto selectedIdx = Util::selectedRowIdx(p->ui->elements);
1988 auto hasSelection = selectedIdx.isValid();
1989 auto chapterSelected = hasSelection && selectedIdx.parent().isValid();
1990 auto hasEntries = !!p->chapterModel->rowCount();
1991 auto hasSubEntries = selectedIdx.isValid() ? !!p->chapterModel->rowCount(selectedIdx) : false;
1992
1993 p->addChapterBeforeAction->setEnabled(chapterSelected);
1994 p->addChapterAfterAction->setEnabled(chapterSelected);
1995 p->addSubChapterAction->setEnabled(hasSelection);
1996 p->generateSubChaptersAction->setEnabled(hasSelection);
1997 p->renumberSubChaptersAction->setEnabled(hasSelection && hasSubEntries);
1998 p->removeElementAction->setEnabled(hasSelection);
1999 p->duplicateAction->setEnabled(hasSelection);
2000 p->expandAllAction->setEnabled(hasEntries);
2001 p->collapseAllAction->setEnabled(hasEntries);
2002
2003 setupCopyToOtherTabMenu();
2004
2005 QMenu menu{this};
2006
2007 menu.addAction(p->addEditionBeforeAction);
2008 menu.addAction(p->addEditionAfterAction);
2009 menu.addSeparator();
2010 menu.addAction(p->addChapterBeforeAction);
2011 menu.addAction(p->addChapterAfterAction);
2012 menu.addAction(p->addSubChapterAction);
2013 menu.addAction(p->generateSubChaptersAction);
2014 menu.addSeparator();
2015 menu.addAction(p->duplicateAction);
2016 menu.addMenu(p->copyToOtherTabMenu);
2017 menu.addSeparator();
2018 menu.addAction(p->removeElementAction);
2019 menu.addSeparator();
2020 menu.addAction(p->renumberSubChaptersAction);
2021 menu.addAction(p->massModificationAction);
2022 menu.addSeparator();
2023 menu.addAction(p->expandAllAction);
2024 menu.addAction(p->collapseAllAction);
2025
2026 menu.exec(p->ui->elements->viewport()->mapToGlobal(pos));
2027 }
2028
2029 bool
isSourceMatroska() const2030 Tab::isSourceMatroska()
2031 const {
2032 auto p = p_func();
2033
2034 return !!p->analyzer;
2035 }
2036
2037 bool
hasChapters() const2038 Tab::hasChapters()
2039 const {
2040 auto p = p_func();
2041
2042 for (auto idx = 0, numEditions = p->chapterModel->rowCount(); idx < numEditions; ++idx)
2043 if (p->chapterModel->item(idx)->rowCount())
2044 return true;
2045 return false;
2046 }
2047
2048 QString
currentState() const2049 Tab::currentState()
2050 const {
2051 auto p = p_func();
2052 auto chapters = p->chapterModel->allChapters();
2053
2054 return chapters ? Q(ebml_dumper_c::dump_to_string(chapters.get(), static_cast<ebml_dumper_c::dump_style_e>(ebml_dumper_c::style_with_values | ebml_dumper_c::style_with_indexes))) : QString{};
2055 }
2056
2057 bool
hasBeenModified() const2058 Tab::hasBeenModified()
2059 const {
2060 auto p = p_func();
2061
2062 return currentState() != p->savedState;
2063 }
2064
2065 bool
focusNextChapterName()2066 Tab::focusNextChapterName() {
2067 auto p = p_func();
2068 auto selectedRows = p->ui->tvChNames->selectionModel()->selectedRows();
2069
2070 if (selectedRows.isEmpty())
2071 return false;
2072
2073 auto nextRow = selectedRows.at(0).row() + 1;
2074 if (nextRow >= p->nameModel->rowCount())
2075 return false;
2076
2077 Util::selectRow(p->ui->tvChNames, nextRow);
2078
2079 return true;
2080 }
2081
2082 bool
focusNextChapterAtom(FocusElementType toFocus)2083 Tab::focusNextChapterAtom(FocusElementType toFocus) {
2084 auto p = p_func();
2085 auto doSelect = [p, toFocus](QModelIndex const &idx) -> bool {
2086 Util::selectRow(p->ui->elements, idx.row(), idx.parent());
2087
2088 auto lineEdit = FocusChapterStartTime == toFocus ? p->ui->leChStart : p->ui->leChName;
2089 lineEdit->selectAll();
2090 lineEdit->setFocus();
2091
2092 return true;
2093 };
2094
2095 auto selectedRows = p->ui->elements->selectionModel()->selectedRows();
2096 if (selectedRows.isEmpty())
2097 return false;
2098
2099 auto selectedIdx = selectedRows.at(0);
2100 selectedIdx = selectedIdx.sibling(selectedIdx.row(), 0);
2101 auto selectedItem = p->chapterModel->itemFromIndex(selectedIdx);
2102
2103 if (selectedItem->rowCount())
2104 return doSelect(p->chapterModel->index(0, 0, selectedIdx));
2105
2106 auto parentIdx = selectedIdx.parent();
2107 auto parentItem = p->chapterModel->itemFromIndex(parentIdx);
2108 auto nextRow = selectedIdx.row() + 1;
2109
2110 if (nextRow < parentItem->rowCount())
2111 return doSelect(p->chapterModel->index(nextRow, 0, parentIdx));
2112
2113 while (parentIdx.parent().isValid()) {
2114 nextRow = parentIdx.row() + 1;
2115 parentIdx = parentIdx.parent();
2116 parentItem = p->chapterModel->itemFromIndex(parentIdx);
2117
2118 if (nextRow < parentItem->rowCount())
2119 return doSelect(p->chapterModel->index(nextRow, 0, parentIdx));
2120 }
2121
2122 auto numEditions = p->chapterModel->rowCount();
2123 auto editionIdx = parentIdx.sibling((parentIdx.row() + 1) % numEditions, 0);
2124
2125 while (numEditions) {
2126 if (p->chapterModel->itemFromIndex(editionIdx)->rowCount())
2127 return doSelect(p->chapterModel->index(0, 0, editionIdx));
2128
2129 editionIdx = editionIdx.sibling((editionIdx.row() + 1) % numEditions, 0);
2130 }
2131
2132 return false;
2133 }
2134
2135 void
focusNextChapterElement(bool keepSameElement)2136 Tab::focusNextChapterElement(bool keepSameElement) {
2137 auto p = p_func();
2138
2139 if (!copyControlsToStorage())
2140 return;
2141
2142 if (QObject::sender() == p->ui->leChName) {
2143 if (focusNextChapterName())
2144 return;
2145
2146 focusNextChapterAtom(keepSameElement ? FocusChapterName : FocusChapterStartTime);
2147 return;
2148 }
2149
2150 if (!keepSameElement) {
2151 p->ui->leChName->selectAll();
2152 p->ui->leChName->setFocus();
2153 return;
2154 }
2155
2156 focusNextChapterAtom(FocusChapterStartTime);
2157 }
2158
2159 void
focusOtherControlInNextChapterElement()2160 Tab::focusOtherControlInNextChapterElement() {
2161 focusNextChapterElement(false);
2162 }
2163
2164 void
focusSameControlInNextChapterElement()2165 Tab::focusSameControlInNextChapterElement() {
2166 focusNextChapterElement(true);
2167 }
2168
2169 void
closeTab()2170 Tab::closeTab() {
2171 Q_EMIT removeThisTab();
2172 }
2173
2174 void
addSegmentUIDFromFile()2175 Tab::addSegmentUIDFromFile() {
2176 auto p = p_func();
2177
2178 Util::addSegmentUIDFromFileToLineEdit(*this, *p->ui->leChSegmentUid, false);
2179 }
2180
2181 QStringList
usedNameLanguages(QStandardItem * rootItem)2182 Tab::usedNameLanguages(QStandardItem *rootItem) {
2183 auto p = p_func();
2184
2185 if (!rootItem)
2186 rootItem = p->chapterModel->invisibleRootItem();
2187
2188 auto languages = QSet<QString>{};
2189
2190 std::function<void(QStandardItem *)> collector = [p, &collector, &languages](auto *currentItem) {
2191 if (!currentItem)
2192 return;
2193
2194 auto chapter = p->chapterModel->chapterFromItem(currentItem);
2195 if (chapter)
2196 for (auto const &element : *chapter) {
2197 auto kDisplay = dynamic_cast<KaxChapterDisplay *>(element);
2198 if (!kDisplay)
2199 continue;
2200
2201 auto lists = NameModel::effectiveLanguagesForDisplay(*static_cast<libmatroska::KaxChapterDisplay *>(kDisplay));
2202 for (auto const &languageCodeHere : lists.languageCodes)
2203 languages << Q(languageCodeHere.format());
2204 }
2205
2206 for (int row = 0, numRows = currentItem->rowCount(); row < numRows; ++row)
2207 collector(currentItem->child(row));
2208 };
2209
2210 collector(rootItem);
2211
2212 return languages.values();
2213 }
2214
2215 QHash<KaxChapterAtom *, ChapterAtomDataPtr>
collectChapterAtomDataForEdition(QStandardItem * item)2216 Tab::collectChapterAtomDataForEdition(QStandardItem *item) {
2217 auto p = p_func();
2218
2219 if (!item)
2220 return {};
2221
2222 QHash<KaxChapterAtom *, ChapterAtomDataPtr> allAtoms;
2223 QHash<KaxChapterAtom *, QVector<ChapterAtomDataPtr>> atomsByParent;
2224 QVector<ChapterAtomDataPtr> atomList;
2225
2226 // Collect all existing start and end timestamps.
2227 std::function<void(QStandardItem *, KaxChapterAtom *, int)> collector = [p, &collector, &allAtoms, &atomsByParent, &atomList](auto *currentItem, auto *parentAtom, int level) {
2228 if (!currentItem)
2229 return;
2230
2231 QVector<ChapterAtomDataPtr> currentAtoms;
2232
2233 auto chapter = p->chapterModel->chapterFromItem(currentItem);
2234 if (chapter && currentItem->parent()) {
2235 auto timeEndKax = FindChild<KaxChapterTimeEnd>(*chapter);
2236 auto displayKax = FindChild<KaxChapterDisplay>(*chapter);
2237
2238 auto data = std::make_shared<ChapterAtomData>();
2239 data->atom = chapter.get();
2240 data->parentAtom = parentAtom;
2241 data->level = level - 1;
2242 data->start = timestamp_c::ns(GetChildValue<KaxChapterTimeStart>(*chapter));
2243
2244 if (timeEndKax)
2245 data->end = timestamp_c::ns(timeEndKax->GetValue());
2246
2247 if (displayKax)
2248 data->primaryName = Q(FindChildValue<KaxChapterString>(*displayKax));
2249
2250 allAtoms.insert(chapter.get(), data);
2251 atomsByParent[parentAtom].append(data);
2252 atomList.append(data);
2253 }
2254
2255 for (int row = 0, numRows = currentItem->rowCount(); row < numRows; ++row)
2256 collector(currentItem->child(row), chapter.get(), level + 1);
2257 };
2258
2259 std::function<void(QStandardItem *)> calculator = [p, &calculator, &allAtoms, &atomsByParent](auto *parentItem) {
2260 if (!parentItem || !parentItem->rowCount())
2261 return;
2262
2263 auto parentAtom = p->chapterModel->chapterFromItem(parentItem);
2264 auto parentData = allAtoms[parentAtom.get()];
2265 auto &sortedData = atomsByParent[parentAtom.get()];
2266 auto parentEndTimestamp = parentData ? parentData->calculatedEnd : p->fileEndTimestamp;
2267
2268 for (int row = 0, numRows = parentItem->rowCount(); row < numRows; ++row) {
2269 auto atom = p->chapterModel->chapterFromItem(parentItem->child(row));
2270 auto data = allAtoms[atom.get()];
2271
2272 if (!data)
2273 continue;
2274
2275 auto itr = std::lower_bound(sortedData.begin(), sortedData.end(), data, [](auto const &a, auto const &b) { return a->start <= b->start; });
2276 data->calculatedEnd = itr == sortedData.end() ? parentEndTimestamp : (*itr)->start;
2277 }
2278
2279 for (int row = 0, numRows = parentItem->rowCount(); row < numRows; ++row)
2280 calculator(parentItem->child(row));
2281 };
2282
2283 // Determine edition we're working in.
2284 while (item->parent())
2285 item = item->parent();
2286
2287 // Collect existing tree structure & basic data for each atom.
2288 collector(item, nullptr, 0);
2289
2290 // Sort all levels by their start times.
2291 for (auto const &key : atomsByParent.keys())
2292 std::sort(atomsByParent[key].begin(), atomsByParent[key].end(), [](ChapterAtomDataPtr const &a, ChapterAtomDataPtr const &b) { return a->start < b->start; });
2293
2294 // Calculate end timestamps.
2295 calculator(item);
2296
2297 // Output debug info.
2298 for (auto const &data : atomList)
2299 qDebug() <<
2300 Q(fmt::format("collectChapterAtomData: data {0}{1} start {2} end {3} [{4}] atom {5} parent {6}",
2301 std::string(data->level * 2, ' '),
2302 to_utf8(data->primaryName),
2303 data->start,
2304 data->end,
2305 data->calculatedEnd,
2306 static_cast<void *>(data->atom),
2307 static_cast<void *>(data->parentAtom)));
2308
2309 return allAtoms;
2310 }
2311
2312 }
2313