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