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 ¤t,
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