1 #include "common/common_pch.h"
2 
3 #include <QCheckBox>
4 #include <QDir>
5 #include <QFileInfo>
6 #include <QLineEdit>
7 #include <QMenu>
8 #include <QMessageBox>
9 
10 #include <matroska/KaxAttached.h>
11 #include <matroska/KaxAttachments.h>
12 #include <matroska/KaxInfoData.h>
13 #include <matroska/KaxSemantic.h>
14 
15 #include "common/construct.h"
16 #include "common/doc_type_version_handler.h"
17 #include "common/ebml.h"
18 #include "common/list_utils.h"
19 #include "common/mime.h"
20 #include "common/mm_io_x.h"
21 #include "common/mm_file_io.h"
22 #include "common/property_element.h"
23 #include "common/qt.h"
24 #include "common/segmentinfo.h"
25 #include "common/strings/formatting.h"
26 #include "common/unique_numbers.h"
27 #include "mkvtoolnix-gui/app.h"
28 #include "mkvtoolnix-gui/forms/header_editor/tab.h"
29 #include "mkvtoolnix-gui/header_editor/action_for_dropped_files_dialog.h"
30 #include "mkvtoolnix-gui/header_editor/ascii_string_value_page.h"
31 #include "mkvtoolnix-gui/header_editor/attached_file_page.h"
32 #include "mkvtoolnix-gui/header_editor/attachments_page.h"
33 #include "mkvtoolnix-gui/header_editor/bit_value_page.h"
34 #include "mkvtoolnix-gui/header_editor/bool_value_page.h"
35 #include "mkvtoolnix-gui/header_editor/float_value_page.h"
36 #include "mkvtoolnix-gui/header_editor/language_ietf_value_page.h"
37 #include "mkvtoolnix-gui/header_editor/language_value_page.h"
38 #include "mkvtoolnix-gui/header_editor/page_model.h"
39 #include "mkvtoolnix-gui/header_editor/string_value_page.h"
40 #include "mkvtoolnix-gui/header_editor/tab.h"
41 #include "mkvtoolnix-gui/header_editor/time_value_page.h"
42 #include "mkvtoolnix-gui/header_editor/tool.h"
43 #include "mkvtoolnix-gui/header_editor/top_level_page.h"
44 #include "mkvtoolnix-gui/header_editor/track_name_page.h"
45 #include "mkvtoolnix-gui/header_editor/track_type_page.h"
46 #include "mkvtoolnix-gui/header_editor/unsigned_integer_value_page.h"
47 #include "mkvtoolnix-gui/main_window/main_window.h"
48 #include "mkvtoolnix-gui/util/basic_tree_view.h"
49 #include "mkvtoolnix-gui/util/file.h"
50 #include "mkvtoolnix-gui/util/file_dialog.h"
51 #include "mkvtoolnix-gui/util/header_view_manager.h"
52 #include "mkvtoolnix-gui/util/model.h"
53 #include "mkvtoolnix-gui/util/message_box.h"
54 #include "mkvtoolnix-gui/util/settings.h"
55 #include "mkvtoolnix-gui/util/tree.h"
56 #include "mkvtoolnix-gui/util/widget.h"
57 
58 using namespace libmatroska;
59 using namespace mtx::gui;
60 
61 namespace mtx::gui::HeaderEditor {
62 
Tab(QWidget * parent,QString const & fileName)63 Tab::Tab(QWidget *parent,
64          QString const &fileName)
65   : QWidget{parent}
66   , ui{new Ui::Tab}
67   , m_fileName{fileName}
68   , m_model{new PageModel{this}}
69   , m_treeContextMenu{new QMenu{this}}
70   , m_modifySelectedTrackMenu{new QMenu{this}}
71   , m_languageShortcutsMenu{new QMenu{this}}
72   , m_expandAllAction{new QAction{this}}
73   , m_collapseAllAction{new QAction{this}}
74   , m_addAttachmentsAction{new QAction{this}}
75   , m_removeAttachmentAction{new QAction{this}}
76   , m_removeAllAttachmentsAction{new QAction{this}}
77   , m_saveAttachmentContentAction{new QAction{this}}
78   , m_replaceAttachmentContentAction{new QAction{this}}
79   , m_replaceAttachmentContentSetValuesAction{new QAction{this}}
80 {
81   // Setup UI controls.
82   ui->setupUi(this);
83 
84   setupUi();
85 
86   retranslateUi();
87 }
88 
~Tab()89 Tab::~Tab() {
90 }
91 
92 void
resetData()93 Tab::resetData() {
94   m_analyzer.reset();
95   m_eSegmentInfo.reset();
96   m_eTracks.reset();
97   m_model->reset();
98   m_segmentinfoPage = nullptr;
99   m_tracksReordered = false;
100 }
101 
102 void
load()103 Tab::load() {
104   QVector<int> selectedRows;
105 
106   auto selectedIdx = ui->elements->selectionModel()->currentIndex();
107   if (!selectedIdx.isValid()) {
108     auto rowIndexes = ui->elements->selectionModel()->selectedRows();
109     if (!rowIndexes.isEmpty())
110       selectedIdx = rowIndexes.first();
111   }
112 
113   while (selectedIdx.isValid()) {
114     selectedRows.insert(0, selectedIdx.row());
115     selectedIdx = selectedIdx.sibling(selectedIdx.row(), 0).parent();
116   }
117 
118   QHash<QString, bool> expansionStatus;
119 
120   for (auto const &page : m_model->allExpandablePages()) {
121     auto key = dynamic_cast<TopLevelPage &>(*page).internalIdentifier();
122     expansionStatus[key] = ui->elements->isExpanded(page->m_pageIdx);
123   }
124 
125   resetData();
126 
127   if (!kax_analyzer_c::probe(to_utf8(m_fileName))) {
128     auto text = Q("%1 %2")
129       .arg(QY("The file you tried to open (%1) is not recognized as a valid Matroska/WebM file.").arg(m_fileName))
130       .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."));
131     Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(text).exec();
132     Q_EMIT removeThisTab();
133     return;
134   }
135 
136   m_analyzer = std::make_unique<Util::KaxAnalyzer>(this, m_fileName);
137   bool ok    = false;
138   QString error;
139 
140   try {
141     ok = m_analyzer->set_parse_mode(kax_analyzer_c::parse_mode_fast)
142       .set_open_mode(MODE_READ)
143       .set_throw_on_error(true)
144       .process();
145 
146   } catch (mtx::kax_analyzer_x &ex) {
147     error = QY("Error details: %1.").arg(Q(ex.what()));
148   }
149 
150   if (!ok) {
151     if (error.isEmpty())
152       error = 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.");
153 
154     auto text = Q("%1 %2")
155       .arg(QY("The file you tried to open (%1) could not be read successfully.").arg(m_fileName))
156       .arg(error);
157     Util::MessageBox::critical(this)->title(QY("File parsing failed")).text(text).exec();
158     Q_EMIT removeThisTab();
159     return;
160   }
161 
162   populateTree();
163 
164   m_analyzer->close_file();
165 
166   for (auto const &page : m_model->allExpandablePages()) {
167     auto key = dynamic_cast<TopLevelPage &>(*page).internalIdentifier();
168     ui->elements->setExpanded(page->m_pageIdx, expansionStatus[key]);
169   }
170 
171   if (selectedRows.isEmpty())
172     return;
173 
174   selectedIdx = m_model->index(selectedRows.takeFirst(), 0);
175   for (auto row : selectedRows)
176     selectedIdx = m_model->index(row, 0, selectedIdx);
177 
178   auto selection = QItemSelection{selectedIdx, selectedIdx.sibling(selectedIdx.row(), m_model->columnCount() - 1)};
179   ui->elements->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
180   selectionChanged(selectedIdx, QModelIndex{});
181 }
182 
183 void
save()184 Tab::save() {
185   auto segmentinfoModified = false;
186   auto tracksModified      = false;
187   auto attachmentsModified = false;
188 
189   for (auto const &page : m_model->topLevelPages()) {
190     if (!page->hasBeenModified())
191       continue;
192 
193     if (page == m_segmentinfoPage)
194       segmentinfoModified = true;
195 
196     else if (page == m_attachmentsPage)
197       attachmentsModified = true;
198 
199     else
200       tracksModified      = true;
201   }
202 
203   if (!segmentinfoModified && !tracksModified && !attachmentsModified && !m_tracksReordered) {
204     Util::MessageBox::information(this)->title(QY("File has not been modified")).text(QY("The header values have not been modified. There is nothing to save.")).exec();
205     return;
206   }
207 
208   auto pageIdx = m_model->validate();
209   if (pageIdx.isValid()) {
210     reportValidationFailure(false, pageIdx);
211     return;
212   }
213 
214   auto trackUIDChanges = determineTrackUIDChanges();
215 
216   doModifications();
217 
218   bool ok = true;
219 
220   try {
221     mtx::doc_type_version_handler_c doc_type_version_handler;
222 
223     m_analyzer->set_doc_type_version_handler(&doc_type_version_handler);
224 
225     if (segmentinfoModified && m_eSegmentInfo) {
226       auto result = m_analyzer->update_element(m_eSegmentInfo, true);
227       if (kax_analyzer_c::uer_success != result) {
228         Util::KaxAnalyzer::displayUpdateElementResult(this, result, QY("Saving the modified segment information header failed."));
229         ok = false;
230       }
231     }
232 
233     if (ok && m_eTracks && (tracksModified || m_tracksReordered)) {
234       updateTracksElementToMatchTrackOrder();
235 
236       auto result = m_analyzer->update_element(m_eTracks, true);
237       if (kax_analyzer_c::uer_success != result) {
238         Util::KaxAnalyzer::displayUpdateElementResult(this, result, QY("Saving the modified track headers failed."));
239         ok = false;
240       }
241     }
242 
243     if (ok && attachmentsModified) {
244       auto attachments = std::make_shared<KaxAttachments>();
245 
246       for (auto const &attachedFilePage : m_attachmentsPage->m_children)
247         attachments->PushElement(*dynamic_cast<AttachedFilePage &>(*attachedFilePage).m_attachment.get());
248 
249       auto result = attachments->ListSize() ? m_analyzer->update_element(attachments.get(), true)
250                   :                           m_analyzer->remove_elements(KaxAttachments::ClassInfos.GlobalId);
251 
252       attachments->RemoveAll();
253 
254       if (kax_analyzer_c::uer_success != result) {
255         Util::KaxAnalyzer::displayUpdateElementResult(this, result, QY("Saving the modified attachments failed."));
256         ok = false;
257       }
258     }
259 
260     if (ok && !trackUIDChanges.empty()) {
261       auto result = m_analyzer->update_uid_referrals(trackUIDChanges);
262 
263       if (kax_analyzer_c::uer_success != result) {
264         Util::KaxAnalyzer::displayUpdateElementResult(this, result, QY("Saving the modified attachments failed."));
265         ok = false;
266       }
267     }
268 
269     if (ok) {
270       auto result = doc_type_version_handler.update_ebml_head(m_analyzer->get_file());
271       if (!mtx::included_in(result, mtx::doc_type_version_handler_c::update_result_e::ok_updated, mtx::doc_type_version_handler_c::update_result_e::ok_no_update_needed)) {
272         ok           = false;
273         auto details = mtx::doc_type_version_handler_c::update_result_e::err_no_head_found    == result ? QY("No 'EBML head' element was found.")
274                      : mtx::doc_type_version_handler_c::update_result_e::err_not_enough_space == result ? QY("There's not enough space at the beginning of the file to fit the updated 'EBML head' element in.")
275                      :                                                                                    QY("A generic read or write failure occurred.");
276         auto message = Q("%1 %2").arg(QY("Updating the 'document type version' or 'document type read version' header fields failed.")).arg(details);
277 
278         QMessageBox::warning(this, QY("Error writing Matroska file"), message);
279       }
280     }
281 
282   } catch (mtx::kax_analyzer_x &ex) {
283     QMessageBox::critical(this, QY("Error writing Matroska file"), QY("Error details: %1.").arg(Q(ex.what())));
284     ok = false;
285   }
286 
287   m_analyzer->close_file();
288 
289   load();
290 
291   if (ok)
292     MainWindow::get()->setStatusBarMessage(QY("The file has been saved successfully."));
293 }
294 
295 void
setupUi()296 Tab::setupUi() {
297   setupModifyTracksMenu();
298 
299   Util::Settings::get().handleSplitterSizes(ui->headerEditorSplitter);
300 
301   auto info = QFileInfo{m_fileName};
302   ui->fileName->setText(info.fileName());
303   ui->directory->setText(QDir::toNativeSeparators(info.path()));
304 
305   ui->elements->setModel(m_model);
306   ui->elements->acceptDroppedFiles(true);
307 
308   Util::HeaderViewManager::create(*ui->elements, "HeaderEditor::Elements").setDefaultSizes({ { Q("type"), 250 }, { Q("codec"), 100 }, { Q("language"), 120 }, { Q("properties"), 120 } });
309   Util::preventScrollingWithoutFocus(this);
310 
311   auto &mts = m_modifyTracksSubmenu;
312 
313   connect(ui->elements,                              &Util::BasicTreeView::customContextMenuRequested,    this, &Tab::showTreeContextMenu);
314   connect(ui->elements,                              &Util::BasicTreeView::filesDropped,                  this, &Tab::handleDroppedFiles);
315   connect(ui->elements,                              &Util::BasicTreeView::deletePressed,                 this, &Tab::removeSelectedAttachment);
316   connect(ui->elements,                              &Util::BasicTreeView::insertPressed,                 this, &Tab::selectAttachmentsAndAdd);
317   connect(ui->elements,                              &Util::BasicTreeView::ctrlDownPressed,               this, [this]() { moveElementUpOrDown(false); });
318   connect(ui->elements,                              &Util::BasicTreeView::ctrlUpPressed,                 this, [this]() { moveElementUpOrDown(true); });
319   connect(ui->elements->selectionModel(),            &QItemSelectionModel::currentChanged,                this, &Tab::selectionChanged);
320   connect(m_expandAllAction,                         &QAction::triggered,                                 this, &Tab::expandAll);
321   connect(m_collapseAllAction,                       &QAction::triggered,                                 this, &Tab::collapseAll);
322   connect(m_addAttachmentsAction,                    &QAction::triggered,                                 this, &Tab::selectAttachmentsAndAdd);
323   connect(m_removeAttachmentAction,                  &QAction::triggered,                                 this, &Tab::removeSelectedAttachment);
324   connect(m_removeAllAttachmentsAction,              &QAction::triggered,                                 this, &Tab::removeAllAttachments);
325   connect(m_saveAttachmentContentAction,             &QAction::triggered,                                 this, &Tab::saveAttachmentContent);
326   connect(mts.m_toggleTrackEnabledFlag,              &QAction::triggered,                                 this, &Tab::toggleTrackFlag);
327   connect(mts.m_toggleDefaultTrackFlag,              &QAction::triggered,                                 this, &Tab::toggleTrackFlag);
328   connect(mts.m_toggleForcedDisplayFlag,             &QAction::triggered,                                 this, &Tab::toggleTrackFlag);
329   connect(mts.m_toggleCommentaryFlag,                &QAction::triggered,                                 this, &Tab::toggleTrackFlag);
330   connect(mts.m_toggleOriginalFlag,                  &QAction::triggered,                                 this, &Tab::toggleTrackFlag);
331   connect(mts.m_toggleHearingImpairedFlag,           &QAction::triggered,                                 this, &Tab::toggleTrackFlag);
332   connect(mts.m_toggleVisualImpairedFlag,            &QAction::triggered,                                 this, &Tab::toggleTrackFlag);
333   connect(mts.m_toggleTextDescriptionsFlag,          &QAction::triggered,                                 this, &Tab::toggleTrackFlag);
334   connect(&mts,                                      &Util::ModifyTracksSubmenu::languageChangeRequested, this, &Tab::changeTrackLanguage);
335   connect(m_replaceAttachmentContentAction,          &QAction::triggered,                                 [this]() { replaceAttachmentContent(false); });
336   connect(m_replaceAttachmentContentSetValuesAction, &QAction::triggered,                                 [this]() { replaceAttachmentContent(true); });
337   connect(m_model,                                   &PageModel::attachmentsReordered,                    [this]() { m_attachmentsPage->rereadChildren(*m_model); });
338   connect(m_model,                                   &PageModel::tracksReordered,                         this, &Tab::handleReorderedTracks);
339 }
340 
341 void
setupModifyTracksMenu()342 Tab::setupModifyTracksMenu() {
343   m_modifySelectedTrackMenu->addMenu(m_languageShortcutsMenu);
344   m_modifySelectedTrackMenu->addSeparator();
345 
346   m_modifyTracksSubmenu.setupTrack(*m_modifySelectedTrackMenu);
347   m_modifyTracksSubmenu.setupLanguage(*m_languageShortcutsMenu);
348 }
349 
350 void
handleReorderedTracks()351 Tab::handleReorderedTracks() {
352   m_tracksReordered = true;
353   m_model->rereadTopLevelPageIndexes();
354 }
355 
356 void
appendPage(PageBase * page,QModelIndex const & parentIdx)357 Tab::appendPage(PageBase *page,
358                 QModelIndex const &parentIdx) {
359   ui->pageContainer->addWidget(page);
360   m_model->appendPage(page, parentIdx);
361 }
362 
363 PageModel *
model() const364 Tab::model()
365   const {
366   return m_model;
367 }
368 
369 PageBase *
currentlySelectedPage() const370 Tab::currentlySelectedPage()
371   const {
372   return m_model->selectedPage(ui->elements->selectionModel()->currentIndex());
373 }
374 
375 void
retranslateUi()376 Tab::retranslateUi() {
377   ui->fileNameLabel->setText(QY("File name:"));
378   ui->directoryLabel->setText(QY("Directory:"));
379 
380   m_expandAllAction->setText(QY("&Expand all"));
381   m_collapseAllAction->setText(QY("&Collapse all"));
382   m_addAttachmentsAction->setText(QY("&Add attachments"));
383   m_removeAttachmentAction->setText(QY("&Remove selected attachment"));
384   m_removeAllAttachmentsAction->setText(QY("Remove a&ll attachments"));
385   m_saveAttachmentContentAction->setText(QY("&Save attachment content to a file"));
386   m_replaceAttachmentContentAction->setText(QY("Re&place attachment with a new file"));
387   m_replaceAttachmentContentSetValuesAction->setText(QY("Replace attachment with a new file and &derive name && MIME type from it"));
388   m_modifySelectedTrackMenu->setTitle(QY("Modif&y selected track"));
389 
390   m_addAttachmentsAction->setIcon(QIcon{Q(":/icons/16x16/list-add.png")});
391   m_removeAttachmentAction->setIcon(QIcon{Q(":/icons/16x16/list-remove.png")});
392   m_saveAttachmentContentAction->setIcon(QIcon{Q(":/icons/16x16/document-save.png")});
393   m_replaceAttachmentContentAction->setIcon(QIcon{Q(":/icons/16x16/document-open.png")});
394 
395   m_modifyTracksSubmenu.retranslateUi();
396   m_languageShortcutsMenu->setTitle(QY("Set &language"));
397 
398   setupToolTips();
399 
400   for (auto const &page : m_model->pages())
401     page->retranslateUi();
402 
403   m_model->retranslateUi();
404 }
405 
406 void
setupToolTips()407 Tab::setupToolTips() {
408   Util::setToolTip(ui->elements, QY("Right-click for actions for header elements and attachments"));
409 }
410 
411 void
populateTree()412 Tab::populateTree() {
413   m_analyzer->with_elements(KaxInfo::ClassInfos.GlobalId, [this](kax_analyzer_data_c const &data) {
414     handleSegmentInfo(data);
415   });
416 
417   m_analyzer->with_elements(KaxTracks::ClassInfos.GlobalId, [this](kax_analyzer_data_c const &data) {
418     handleTracks(data);
419   });
420 
421   handleAttachments();
422 }
423 
424 void
selectionChanged(QModelIndex const & current,QModelIndex const &)425 Tab::selectionChanged(QModelIndex const &current,
426                       QModelIndex const &) {
427   if (m_ignoreSelectionChanges)
428     return;
429 
430   m_model->rememberLastSelectedIndex(current);
431 
432   auto selectedPage = m_model->selectedPage(current);
433   if (selectedPage)
434     ui->pageContainer->setCurrentWidget(selectedPage);
435 }
436 
437 bool
isTrackSelected()438 Tab::isTrackSelected() {
439   auto topLevelIdx = ui->elements->selectionModel()->currentIndex();
440 
441   while (topLevelIdx.parent().isValid())
442     topLevelIdx = topLevelIdx.parent();
443 
444   return !!dynamic_cast<TrackTypePage *>(m_model->selectedPage(topLevelIdx));
445 }
446 
447 QString const &
fileName() const448 Tab::fileName()
449   const {
450   return m_fileName;
451 }
452 
453 QString
title() const454 Tab::title()
455   const {
456   return QFileInfo{m_fileName}.fileName();
457 }
458 
459 PageBase *
hasBeenModified()460 Tab::hasBeenModified() {
461   for (auto const &page : m_model->topLevelPages()) {
462     auto modifiedPage = page->hasBeenModified();
463     if (modifiedPage)
464       return modifiedPage;
465   }
466 
467   return nullptr;
468 }
469 
470 void
pruneEmptyMastersForTrack(TrackTypePage & page)471 Tab::pruneEmptyMastersForTrack(TrackTypePage &page) {
472   auto trackType = FindChildValue<KaxTrackType>(page.m_master);
473 
474   if (!mtx::included_in(trackType, track_video, track_audio))
475     return;
476 
477   std::unordered_map<EbmlMaster *, bool> handled;
478 
479   if (trackType == track_video) {
480     auto trackVideo            = &GetChildEmptyIfNew<KaxTrackVideo>(page.m_master);
481     auto videoColour           = &GetChildEmptyIfNew<KaxVideoColour>(trackVideo);
482     auto videoColourMasterMeta = &GetChildEmptyIfNew<KaxVideoColourMasterMeta>(videoColour);
483     auto videoProjection       = &GetChildEmptyIfNew<KaxVideoProjection>(trackVideo);
484 
485     remove_master_from_parent_if_empty_or_only_defaults(videoColour,    videoColourMasterMeta, handled);
486     remove_master_from_parent_if_empty_or_only_defaults(trackVideo,     videoColour,           handled);
487     remove_master_from_parent_if_empty_or_only_defaults(trackVideo,     videoProjection,       handled);
488     remove_master_from_parent_if_empty_or_only_defaults(&page.m_master, trackVideo,            handled);
489 
490   } else
491     // trackType is track_audio
492     remove_master_from_parent_if_empty_or_only_defaults(&page.m_master, &GetChildEmptyIfNew<KaxTrackAudio>(page.m_master), handled);
493 }
494 
495 void
pruneEmptyMastersForAllTracks()496 Tab::pruneEmptyMastersForAllTracks() {
497   for (auto const &page : m_model->topLevelPages())
498     if (dynamic_cast<TrackTypePage *>(page))
499       pruneEmptyMastersForTrack(static_cast<TrackTypePage &>(*page));
500 }
501 
502 std::unordered_map<uint64_t, uint64_t>
determineTrackUIDChanges()503 Tab::determineTrackUIDChanges() {
504   std::unordered_map<uint64_t, uint64_t> changes;
505 
506   for (auto const &topLevelPage : m_model->topLevelPages()) {
507     for (auto const &childPage : topLevelPage->m_children) {
508       auto uiValuePage = dynamic_cast<UnsignedIntegerValuePage *>(childPage);
509       if (!uiValuePage)
510         continue;
511 
512       if (uiValuePage->m_callbacks.GlobalId != KaxTrackUID::ClassInfos.GlobalId)
513         continue;
514 
515       if (uiValuePage->m_cbAddOrRemove->isChecked())
516         continue;
517 
518       auto currentValue = uiValuePage->m_leValue->text().toULongLong();
519       if (uiValuePage->m_originalValue != currentValue)
520         changes[uiValuePage->m_originalValue] = currentValue;
521     }
522   }
523 
524   return changes;
525 }
526 
527 void
doModifications()528 Tab::doModifications() {
529   for (auto const &page : m_model->topLevelPages())
530     page->doModifications();
531 
532   pruneEmptyMastersForAllTracks();
533 
534   if (m_eSegmentInfo) {
535     fix_mandatory_elements(m_eSegmentInfo.get());
536     m_eSegmentInfo->UpdateSize(true, true);
537   }
538 
539   if (m_eTracks) {
540     fix_mandatory_elements(m_eTracks.get());
541     m_eTracks->UpdateSize(true, true);
542   }
543 }
544 
545 ValuePage *
createValuePage(TopLevelPage & parentPage,EbmlMaster & parentMaster,property_element_c const & element)546 Tab::createValuePage(TopLevelPage &parentPage,
547                      EbmlMaster &parentMaster,
548                      property_element_c const &element) {
549   ValuePage *page{};
550   auto const type = element.m_type;
551 
552   page = element.m_callbacks == &KaxTrackLanguage::ClassInfos     ? new LanguageValuePage{       *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
553        : element.m_callbacks == &KaxLanguageIETF::ClassInfos      ? new LanguageIETFValuePage{   *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
554        : element.m_callbacks == &KaxTrackName::ClassInfos         ? new TrackNamePage{           *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
555        : type                == property_element_c::EBMLT_BOOL    ? new BoolValuePage{           *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
556        : type                == property_element_c::EBMLT_BINARY  ? new BitValuePage{            *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description, element.m_bit_length}
557        : type                == property_element_c::EBMLT_FLOAT   ? new FloatValuePage{          *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
558        : type                == property_element_c::EBMLT_INT     ? new UnsignedIntegerValuePage{*this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
559        : type                == property_element_c::EBMLT_UINT    ? new UnsignedIntegerValuePage{*this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
560        : type                == property_element_c::EBMLT_STRING  ? new AsciiStringValuePage{    *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
561        : type                == property_element_c::EBMLT_USTRING ? new StringValuePage{         *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
562        : type                == property_element_c::EBMLT_DATE    ? new TimeValuePage{           *this, parentPage, parentMaster, *element.m_callbacks, element.m_title, element.m_description}
563        :                                                             static_cast<ValuePage *>(nullptr);
564 
565   if (page)
566     page->init();
567 
568   return page;
569 }
570 
571 void
handleSegmentInfo(kax_analyzer_data_c const & data)572 Tab::handleSegmentInfo(kax_analyzer_data_c const &data) {
573   m_eSegmentInfo = m_analyzer->read_element(data);
574   if (!m_eSegmentInfo)
575     return;
576 
577   auto &info = dynamic_cast<KaxInfo &>(*m_eSegmentInfo.get());
578   auto page  = new TopLevelPage{*this, YT("Segment information")};
579   page->setInternalIdentifier("segmentInfo");
580   page->init();
581 
582   auto &propertyElements = property_element_c::get_table_for(KaxInfo::ClassInfos, nullptr, true);
583   for (auto const &element : propertyElements)
584     createValuePage(*page, info, element);
585 
586   m_segmentinfoPage = page;
587 }
588 
589 void
handleTracks(kax_analyzer_data_c const & data)590 Tab::handleTracks(kax_analyzer_data_c const &data) {
591   m_eTracks = m_analyzer->read_element(data);
592   if (!m_eTracks)
593     return;
594 
595   auto trackIdxMkvmerge  = 0u;
596   auto &propertyElements = property_element_c::get_table_for(KaxTracks::ClassInfos, nullptr, true);
597 
598   for (auto const &element : dynamic_cast<EbmlMaster &>(*m_eTracks)) {
599     auto kTrackEntry = dynamic_cast<KaxTrackEntry *>(element);
600     if (!kTrackEntry)
601       continue;
602 
603     auto kTrackType = FindChild<KaxTrackType>(kTrackEntry);
604     if (!kTrackType)
605       continue;
606 
607     auto trackType = kTrackType->GetValue();
608     auto page      = new TrackTypePage{*this, *kTrackEntry, trackIdxMkvmerge++};
609     page->init();
610 
611     QHash<EbmlCallbacks const *, EbmlMaster *> parentMastersByCallback;
612     QHash<EbmlCallbacks const *, TopLevelPage *> parentPagesByCallback;
613 
614     parentMastersByCallback[nullptr] = kTrackEntry;
615     parentPagesByCallback[nullptr]   = page;
616 
617     if (track_video == trackType) {
618       auto colourPage = new TopLevelPage{*this, YT("Colour information")};
619       colourPage->setInternalIdentifier(Q("videoColour %1").arg(trackIdxMkvmerge - 1));
620       colourPage->setParentPage(*page);
621       colourPage->init();
622 
623       auto colourMasterMetaPage = new TopLevelPage{*this, YT("Colour mastering meta information")};
624       colourMasterMetaPage->setInternalIdentifier(Q("videoColourMasterMeta %1").arg(trackIdxMkvmerge - 1));
625       colourMasterMetaPage->setParentPage(*page);
626       colourMasterMetaPage->init();
627 
628       auto projectionPage = new TopLevelPage{*this, YT("Video projection information")};
629       projectionPage->setInternalIdentifier(Q("videoProjection %1").arg(trackIdxMkvmerge - 1));
630       projectionPage->setParentPage(*page);
631       projectionPage->init();
632 
633       parentMastersByCallback[&KaxTrackVideo::ClassInfos]            = &GetChildEmptyIfNew<KaxTrackVideo>(kTrackEntry);
634       parentMastersByCallback[&KaxVideoColour::ClassInfos]           = &GetChildEmptyIfNew<KaxVideoColour>(parentMastersByCallback[&KaxTrackVideo::ClassInfos]);
635       parentMastersByCallback[&KaxVideoColourMasterMeta::ClassInfos] = &GetChildEmptyIfNew<KaxVideoColourMasterMeta>(parentMastersByCallback[&KaxVideoColour::ClassInfos]);
636       parentMastersByCallback[&KaxVideoProjection::ClassInfos]       = &GetChildEmptyIfNew<KaxVideoProjection>(parentMastersByCallback[&KaxTrackVideo::ClassInfos]);
637 
638       parentPagesByCallback[&KaxTrackVideo::ClassInfos]              = page;
639       parentPagesByCallback[&KaxVideoColour::ClassInfos]             = colourPage;
640       parentPagesByCallback[&KaxVideoColourMasterMeta::ClassInfos]   = colourMasterMetaPage;
641       parentPagesByCallback[&KaxVideoProjection::ClassInfos]         = projectionPage;
642 
643     } else if (track_audio == trackType) {
644       parentMastersByCallback[&KaxTrackAudio::ClassInfos]            = &GetChildEmptyIfNew<KaxTrackAudio>(kTrackEntry);
645       parentPagesByCallback[&KaxTrackAudio::ClassInfos]              = page;
646     }
647 
648     for (auto const &propElement : propertyElements) {
649       auto parentMasterCallbacks = propElement.m_sub_sub_sub_master_callbacks ? propElement.m_sub_sub_sub_master_callbacks
650                                  : propElement.m_sub_sub_master_callbacks     ? propElement.m_sub_sub_master_callbacks
651                                  :                                              propElement.m_sub_master_callbacks;
652       auto parentPage            = parentPagesByCallback[parentMasterCallbacks];
653       auto parentMaster          = parentMastersByCallback[parentMasterCallbacks];
654 
655       if (parentPage && parentMaster)
656         createValuePage(*parentPage, *parentMaster, propElement);
657     }
658   }
659 }
660 
661 void
handleAttachments()662 Tab::handleAttachments() {
663   auto attachments = KaxAttachedList{};
664 
665   m_analyzer->with_elements(KaxAttachments::ClassInfos.GlobalId, [this, &attachments](kax_analyzer_data_c const &data) {
666     auto master = std::dynamic_pointer_cast<KaxAttachments>(m_analyzer->read_element(data));
667     if (!master)
668       return;
669 
670     auto idx = 0u;
671     while (idx < master->ListSize()) {
672       auto attached = dynamic_cast<KaxAttached *>((*master)[idx]);
673       if (attached) {
674         attachments << KaxAttachedPtr{attached};
675         master->Remove(idx);
676       } else
677         ++idx;
678     }
679   });
680 
681   m_attachmentsPage = new AttachmentsPage{*this, attachments};
682   m_attachmentsPage->init();
683 }
684 
685 void
validate()686 Tab::validate() {
687   auto pageIdx = m_model->validate();
688   // TODO: Tab::validate: handle attachments
689 
690   if (!pageIdx.isValid()) {
691     Util::MessageBox::information(this)->title(QY("Header validation")).text(QY("All header values are OK.")).exec();
692     return;
693   }
694 
695   reportValidationFailure(false, pageIdx);
696 }
697 
698 void
reportValidationFailure(bool isCritical,QModelIndex const & pageIdx)699 Tab::reportValidationFailure(bool isCritical,
700                              QModelIndex const &pageIdx) {
701   ui->elements->selectionModel()->setCurrentIndex(pageIdx, QItemSelectionModel::ClearAndSelect);
702   ui->elements->selectionModel()->select(pageIdx, QItemSelectionModel::ClearAndSelect);
703   selectionChanged(pageIdx, QModelIndex{});
704 
705   if (isCritical)
706     Util::MessageBox::critical(this)->title(QY("Header validation")).text(QY("There were errors in the header values preventing the headers from being saved. The first error has been selected.")).exec();
707   else
708     Util::MessageBox::warning(this)->title(QY("Header validation")).text(QY("There were errors in the header values preventing the headers from being saved. The first error has been selected.")).exec();
709 }
710 
711 void
expandAll()712 Tab::expandAll() {
713   Util::expandCollapseAll(ui->elements, true);
714 }
715 
716 void
collapseAll()717 Tab::collapseAll() {
718   Util::expandCollapseAll(ui->elements, false);
719 }
720 
721 void
showTreeContextMenu(QPoint const & pos)722 Tab::showTreeContextMenu(QPoint const &pos) {
723   auto selectedPage       = currentlySelectedPage();
724   auto isAttachmentsPage  = !!dynamic_cast<AttachmentsPage *>(selectedPage);
725   auto isAttachedFilePage = !!dynamic_cast<AttachedFilePage *>(selectedPage);
726   auto isAttachments      = isAttachmentsPage || isAttachedFilePage;
727   auto isTrack            = isTrackSelected();
728   auto actions            = m_treeContextMenu->actions();
729 
730   for (auto const &action : actions)
731     if (!action->isSeparator())
732       m_treeContextMenu->removeAction(action);
733 
734   m_treeContextMenu->clear();
735 
736   m_treeContextMenu->addAction(m_expandAllAction);
737   m_treeContextMenu->addAction(m_collapseAllAction);
738 
739   if (isTrack) {
740     m_treeContextMenu->addSeparator();
741     m_treeContextMenu->addMenu(m_modifySelectedTrackMenu);
742   }
743 
744   m_treeContextMenu->addSeparator();
745   m_treeContextMenu->addAction(m_addAttachmentsAction);
746 
747   if (isAttachments) {
748     m_treeContextMenu->addAction(m_removeAttachmentAction);
749     m_treeContextMenu->addAction(m_removeAllAttachmentsAction);
750     m_treeContextMenu->addSeparator();
751     m_treeContextMenu->addAction(m_saveAttachmentContentAction);
752     m_treeContextMenu->addAction(m_replaceAttachmentContentAction);
753     m_treeContextMenu->addAction(m_replaceAttachmentContentSetValuesAction);
754 
755     m_removeAttachmentAction->setEnabled(isAttachedFilePage);
756     m_removeAllAttachmentsAction->setEnabled(!m_attachmentsPage->m_children.isEmpty());
757     m_saveAttachmentContentAction->setEnabled(isAttachedFilePage);
758     m_replaceAttachmentContentAction->setEnabled(isAttachedFilePage);
759     m_replaceAttachmentContentSetValuesAction->setEnabled(isAttachedFilePage);
760   }
761 
762   m_treeContextMenu->exec(ui->elements->viewport()->mapToGlobal(pos));
763 }
764 
765 void
selectAttachmentsAndAdd()766 Tab::selectAttachmentsAndAdd() {
767   auto &settings = Util::Settings::get();
768   auto fileNames = Util::getOpenFileNames(this, QY("Add attachments"), settings.lastOpenDirPath(), QY("All files") + Q(" (*)"));
769 
770   if (fileNames.isEmpty())
771     return;
772 
773   settings.m_lastOpenDir.setPath(QFileInfo{fileNames[0]}.path());
774   settings.save();
775 
776   addAttachments(fileNames);
777 }
778 
779 void
addAttachment(KaxAttachedPtr const & attachment)780 Tab::addAttachment(KaxAttachedPtr const &attachment) {
781   if (!attachment)
782     return;
783 
784   auto page = new AttachedFilePage{*this, *m_attachmentsPage, attachment};
785   page->init();
786 }
787 
788 void
addAttachments(QStringList const & fileNames)789 Tab::addAttachments(QStringList const &fileNames) {
790   for (auto const &fileName : fileNames)
791     addAttachment(createAttachmentFromFile(fileName));
792 
793   ui->elements->setExpanded(m_attachmentsPage->m_pageIdx, true);
794 }
795 
796 void
removeSelectedAttachment()797 Tab::removeSelectedAttachment() {
798   auto selectedPage = dynamic_cast<AttachedFilePage *>(currentlySelectedPage());
799   if (!selectedPage)
800     return;
801 
802   auto idx = m_model->indexFromPage(selectedPage);
803   if (idx.isValid())
804     m_model->removeRow(idx.row(), idx.parent());
805 
806   m_attachmentsPage->m_children.removeAll(selectedPage);
807   m_model->deletePage(selectedPage);
808 }
809 
810 void
removeAllAttachments()811 Tab::removeAllAttachments() {
812   auto attachmentsItem = m_model->itemFromIndex(m_attachmentsPage->m_pageIdx);
813 
814   m_model->removeRows(0, attachmentsItem->rowCount(), m_attachmentsPage->m_pageIdx);
815 
816   for (auto const &attachmentPage : m_attachmentsPage->m_children)
817     m_model->deletePage(attachmentPage);
818 
819   m_attachmentsPage->m_children.clear();
820 }
821 
822 memory_cptr
readFileData(QWidget * parent,QString const & fileName)823 Tab::readFileData(QWidget *parent,
824                   QString const &fileName) {
825   auto info = QFileInfo{fileName};
826   if (info.size() > 0x7fffffff) {
827     Util::MessageBox::critical(parent)
828       ->title(QY("Reading failed"))
829       .text(Q("%1 %2")
830             .arg(QY("The file (%1) is too big (%2).").arg(fileName).arg(Q(mtx::string::format_file_size(info.size()))))
831             .arg(QY("Only files smaller than 2 GiB are supported.")))
832       .exec();
833     return {};
834   }
835 
836   try {
837     return mm_file_io_c::slurp(to_utf8(fileName));
838 
839   } catch (mtx::mm_io::end_of_file_x &) {
840     Util::MessageBox::critical(parent)->title(QY("Reading failed")).text(QY("The file you tried to open (%1) could not be read successfully.").arg(fileName)).exec();
841   }
842 
843   return {};
844 }
845 
846 KaxAttachedPtr
createAttachmentFromFile(QString const & fileName)847 Tab::createAttachmentFromFile(QString const &fileName) {
848   auto content = readFileData(this, fileName);
849   if (!content)
850     return {};
851 
852   auto mimeType   = Util::detectMIMEType(fileName);
853   auto uid        = create_unique_number(UNIQUE_ATTACHMENT_IDS);
854   auto fileData   = new KaxFileData;
855   auto attachment = KaxAttachedPtr{
856     mtx::construct::cons<KaxAttached>(new KaxFileName, to_wide(QFileInfo{fileName}.fileName()),
857                                       new KaxMimeType, to_utf8(mimeType),
858                                       new KaxFileUID,  uid)
859   };
860 
861   fileData->SetBuffer(content->get_buffer(), content->get_size());
862   content->lock();
863   attachment->PushElement(*fileData);
864 
865   return attachment;
866 }
867 
868 void
saveAttachmentContent()869 Tab::saveAttachmentContent() {
870   auto page = dynamic_cast<AttachedFilePage *>(currentlySelectedPage());
871   if (page)
872     page->saveContent();
873 }
874 
875 void
replaceAttachmentContent(bool deriveNameAndMimeType)876 Tab::replaceAttachmentContent(bool deriveNameAndMimeType) {
877   auto page = dynamic_cast<AttachedFilePage *>(currentlySelectedPage());
878   if (page)
879     page->replaceContent(deriveNameAndMimeType);
880 }
881 
882 void
handleDroppedFiles(QStringList const & fileNames,Qt::MouseButtons mouseButtons)883 Tab::handleDroppedFiles(QStringList const &fileNames,
884                         Qt::MouseButtons mouseButtons) {
885   if (fileNames.isEmpty())
886     return;
887 
888   auto &settings = Util::Settings::get();
889   auto decision  = settings.m_headerEditorDroppedFilesPolicy;
890 
891   if (   (Util::Settings::HeaderEditorDroppedFilesPolicy::Ask == decision)
892       || ((mouseButtons & Qt::RightButton)                    == Qt::RightButton)) {
893     ActionForDroppedFilesDialog dlg{this};
894     if (!dlg.exec())
895       return;
896 
897     decision = dlg.decision();
898 
899     if (dlg.alwaysUseThisDecision()) {
900       settings.m_headerEditorDroppedFilesPolicy = decision;
901       settings.save();
902     }
903   }
904 
905   if (Util::Settings::HeaderEditorDroppedFilesPolicy::Open == decision)
906     MainWindow::get()->headerEditorTool()->openFiles(fileNames);
907 
908   else
909     addAttachments(fileNames);
910 }
911 
912 void
focusPage(PageBase * page)913 Tab::focusPage(PageBase *page) {
914   auto idx = m_model->indexFromPage(page);
915   if (!idx.isValid())
916     return;
917 
918   auto selection = QItemSelection{idx.sibling(idx.row(), 0), idx.sibling(idx.row(), m_model->columnCount() - 1)};
919 
920   m_ignoreSelectionChanges = true;
921 
922   ui->elements->selectionModel()->setCurrentIndex(idx.sibling(idx.row(), 0), QItemSelectionModel::ClearAndSelect);
923   ui->elements->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect);
924   ui->pageContainer->setCurrentWidget(page);
925 
926   m_ignoreSelectionChanges = false;
927 }
928 
929 bool
isClosingOrReloadingOkIfModified(ModifiedConfirmationMode mode)930 Tab::isClosingOrReloadingOkIfModified(ModifiedConfirmationMode mode) {
931   if (!Util::Settings::get().m_warnBeforeClosingModifiedTabs)
932     return true;
933 
934   auto modifiedPage = hasBeenModified();
935   if (!modifiedPage && !m_tracksReordered)
936     return true;
937 
938   auto tool = MainWindow::headerEditorTool();
939   MainWindow::get()->switchToTool(tool);
940   tool->showTab(*this);
941 
942   if (modifiedPage)
943     focusPage(modifiedPage);
944 
945   auto closing  = mode == ModifiedConfirmationMode::Closing;
946   auto text     = closing ? QY("The file \"%1\" has been modified. Do you really want to close? All changes will be lost.")
947                 :           QY("The file \"%1\" has been modified. Do you really want to reload it? All changes will be lost.");
948   auto title    = closing ? QY("Close modified file") : QY("Reload modified file");
949   auto yesLabel = closing ? QY("&Close file")         : QY("&Reload file");
950 
951   auto answer   = Util::MessageBox::question(this)
952     ->title(title)
953     .text(text.arg(QFileInfo{fileName()}.fileName()))
954     .buttonLabel(QMessageBox::Yes, yesLabel)
955     .buttonLabel(QMessageBox::No,  QY("Cancel"))
956     .exec();
957 
958   return answer == QMessageBox::Yes;
959 }
960 
961 void
updateTracksElementToMatchTrackOrder()962 Tab::updateTracksElementToMatchTrackOrder() {
963   auto &tracks = static_cast<libebml::EbmlMaster &>(*m_eTracks);
964 
965   RemoveChildren<libmatroska::KaxTrackEntry>(tracks);
966 
967   for (auto page : m_model->topLevelPages())
968     if (dynamic_cast<TrackTypePage *>(page))
969       tracks.PushElement(static_cast<TrackTypePage &>(*page).m_master);
970 }
971 
972 void
walkPagesOfSelectedTopLevelNode(std::function<bool (PageBase *)> worker)973 Tab::walkPagesOfSelectedTopLevelNode(std::function<bool(PageBase *)> worker) {
974   auto topLevelIdx = ui->elements->selectionModel()->currentIndex();
975 
976   if (!topLevelIdx.isValid())
977     return;
978 
979   while (topLevelIdx.parent().isValid())
980     topLevelIdx = topLevelIdx.parent();
981 
982   std::function<void(QModelIndex const &)> walkTree;
983   walkTree = [this, &worker, &walkTree](QModelIndex const &parentIdx) {
984     for (auto row = 0, numRows = m_model->rowCount(parentIdx); row < numRows; ++row) {
985       auto idx  = m_model->index(row, 0, parentIdx);
986 
987       if (!worker(m_model->selectedPage(idx)))
988         return;
989 
990       walkTree(idx);
991     }
992   };
993 
994   walkTree(topLevelIdx);
995 }
996 
997 void
toggleSpecificTrackFlag(unsigned int wantedId)998 Tab::toggleSpecificTrackFlag(unsigned int wantedId) {
999   walkPagesOfSelectedTopLevelNode([wantedId](auto *pageBase) -> bool {
1000     auto page = dynamic_cast<BoolValuePage *>(pageBase);
1001 
1002     if (page && (page->m_callbacks.GlobalId.GetValue() == wantedId)) {
1003       page->toggleFlag();
1004       return false;
1005     }
1006 
1007     return true;
1008   });
1009 }
1010 
1011 void
toggleTrackFlag()1012 Tab::toggleTrackFlag() {
1013   auto action = dynamic_cast<QAction *>(sender());
1014 
1015   if (action)
1016     toggleSpecificTrackFlag(action->data().toUInt());
1017 }
1018 
1019 void
changeTrackLanguage(QString const & formattedLanguage)1020 Tab::changeTrackLanguage(QString const &formattedLanguage) {
1021   auto language = mtx::bcp47::language_c::parse(to_utf8(formattedLanguage));
1022   if (!language.is_valid())
1023     return;
1024 
1025   auto languageFound  = false,
1026     languageIETFFound = false;
1027 
1028   walkPagesOfSelectedTopLevelNode([&language, &languageFound, &languageIETFFound](auto *pageBase) -> bool {
1029     auto languagePage = dynamic_cast<LanguageValuePage *>(pageBase);
1030 
1031     if (languagePage) {
1032       languagePage->setLanguage(language);
1033       languageFound = true;
1034     }
1035 
1036     auto languageIETFPage = dynamic_cast<LanguageIETFValuePage *>(pageBase);
1037 
1038     if (languageIETFPage) {
1039       languageIETFPage->setLanguage(language);
1040       languageIETFFound = true;
1041     }
1042 
1043     return !languageFound || !languageIETFFound;
1044   });
1045 }
1046 
1047 void
moveElementUpOrDown(bool up)1048 Tab::moveElementUpOrDown(bool up) {
1049   auto focus        = App::instance()->focusWidget();
1050   auto selectedIdx  = ui->elements->selectionModel()->currentIndex();
1051   auto selectedPage = m_model->selectedPage(selectedIdx);
1052 
1053   if (!selectedIdx.isValid() || !selectedPage)
1054     return;
1055 
1056   auto idxToMove = m_model->trackOrAttachedFileIndexForSelectedIndex(selectedIdx);
1057   if (!idxToMove.isValid())
1058     return;
1059 
1060   auto wasExpanded = ui->elements->isExpanded(selectedIdx);
1061 
1062   m_model->moveElementUpOrDown(idxToMove, up);
1063 
1064   auto newIdx = m_model->indexFromPage(selectedPage);
1065 
1066   ui->elements->setExpanded(newIdx, wasExpanded);
1067 
1068   auto expandIdx = newIdx.parent();
1069 
1070   while (expandIdx.isValid()) {
1071     ui->elements->setExpanded(expandIdx, true);
1072     expandIdx = expandIdx.parent();
1073   }
1074 
1075   auto selection = QItemSelection{newIdx, newIdx.sibling(newIdx.row(), m_model->columnCount() - 1)};
1076   ui->elements->selectionModel()->setCurrentIndex(newIdx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
1077   ui->elements->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
1078 
1079   if (focus)
1080     focus->setFocus();
1081 }
1082 
1083 }
1084