From 1ac25bc3c56639c1a1dccafad8916ad29e43f130 Mon Sep 17 00:00:00 2001 From: Moritz Bunkus Date: Thu, 31 Dec 2015 20:27:56 +0100 Subject: [PATCH] GUI: edit attachments inside the header editor Implements #1533. --- .../forms/header_editor/attached_file_page.ui | 161 ++++++++++++ .../forms/header_editor/attachments_page.ui | 133 ++++++++++ src/mkvtoolnix-gui/forms/header_editor/tab.ui | 12 +- .../header_editor/attached_file_page.cpp | 229 ++++++++++++++++++ .../header_editor/attached_file_page.h | 62 +++++ .../header_editor/attachments_page.cpp | 79 ++++++ .../header_editor/attachments_page.h | 51 ++++ .../header_editor/page_model.cpp | 24 +- src/mkvtoolnix-gui/header_editor/page_model.h | 1 + src/mkvtoolnix-gui/header_editor/tab.cpp | 221 ++++++++++++++++- src/mkvtoolnix-gui/header_editor/tab.h | 21 +- .../header_editor/top_level_page.cpp | 11 + .../header_editor/top_level_page.h | 4 + .../header_editor/track_type_page.cpp | 6 + .../header_editor/track_type_page.h | 2 + src/mkvtoolnix-gui/mkvtoolnix-gui.pro | 4 +- src/mkvtoolnix-gui/util/model.cpp | 16 ++ src/mkvtoolnix-gui/util/model.h | 1 + 18 files changed, 1015 insertions(+), 23 deletions(-) create mode 100644 src/mkvtoolnix-gui/forms/header_editor/attached_file_page.ui create mode 100644 src/mkvtoolnix-gui/forms/header_editor/attachments_page.ui create mode 100644 src/mkvtoolnix-gui/header_editor/attached_file_page.cpp create mode 100644 src/mkvtoolnix-gui/header_editor/attached_file_page.h create mode 100644 src/mkvtoolnix-gui/header_editor/attachments_page.cpp create mode 100644 src/mkvtoolnix-gui/header_editor/attachments_page.h diff --git a/src/mkvtoolnix-gui/forms/header_editor/attached_file_page.ui b/src/mkvtoolnix-gui/forms/header_editor/attached_file_page.ui new file mode 100644 index 000000000..1bab6e639 --- /dev/null +++ b/src/mkvtoolnix-gui/forms/header_editor/attached_file_page.ui @@ -0,0 +1,161 @@ + + + mtx::gui::HeaderEditor::AttachedFilePage + + + + 0 + 0 + 546 + 361 + + + + Form + + + + 0 + + + 0 + + + 0 + + + + + Attachment + + + + + + + Qt::Horizontal + + + + + + + + + + + + + + + Fi&le name: + + + name + + + + + + + &Description: + + + description + + + + + + + TextLabel + + + + + + + true + + + + + + + Size: + + + + + + + &MIME type: + + + mimeType + + + + + + + &UID: + + + uid + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Reset + + + + + + + + + name + description + mimeType + uid + + + + diff --git a/src/mkvtoolnix-gui/forms/header_editor/attachments_page.ui b/src/mkvtoolnix-gui/forms/header_editor/attachments_page.ui new file mode 100644 index 000000000..a8676ae0d --- /dev/null +++ b/src/mkvtoolnix-gui/forms/header_editor/attachments_page.ui @@ -0,0 +1,133 @@ + + + mtx::gui::HeaderEditor::AttachmentsPage + + + + 0 + 0 + 457 + 472 + + + + true + + + Form + + + + + + Attachments + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + + 20 + 136 + + + + + + + + You can add attachments by clicking on the button below, by right-clicking on a node in the tree and selecting "Add attachments" from the popup menu or by dragging & dropping files here or onto the tree. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + &Add attachments + + + + :/icons/16x16/list-add.png:/icons/16x16/list-add.png + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 135 + + + + + + + + + + + diff --git a/src/mkvtoolnix-gui/forms/header_editor/tab.ui b/src/mkvtoolnix-gui/forms/header_editor/tab.ui index d900882bb..a4ad05bce 100644 --- a/src/mkvtoolnix-gui/forms/header_editor/tab.ui +++ b/src/mkvtoolnix-gui/forms/header_editor/tab.ui @@ -85,7 +85,7 @@ false - + 0 @@ -99,7 +99,10 @@ - Qt::ActionsContextMenu + Qt::CustomContextMenu + + + true QAbstractItemView::NoEditTriggers @@ -130,6 +133,11 @@ QLabel
mkvtoolnix-gui/util/elide_label.h
+ + mtx::gui::Util::BasicTreeView + QTreeView +
mkvtoolnix-gui/util/basic_tree_view.h
+
diff --git a/src/mkvtoolnix-gui/header_editor/attached_file_page.cpp b/src/mkvtoolnix-gui/header_editor/attached_file_page.cpp new file mode 100644 index 000000000..931f18c8e --- /dev/null +++ b/src/mkvtoolnix-gui/header_editor/attached_file_page.cpp @@ -0,0 +1,229 @@ +#include "common/common_pch.h" + +#include +#include +#include + +#include + +#include "common/ebml.h" +#include "common/extern_data.h" +#include "common/qt.h" +#include "common/strings/formatting.h" +#include "mkvtoolnix-gui/forms/header_editor/attached_file_page.h" +#include "mkvtoolnix-gui/forms/header_editor/tab.h" +#include "mkvtoolnix-gui/header_editor/attached_file_page.h" +#include "mkvtoolnix-gui/util/file_dialog.h" +#include "mkvtoolnix-gui/util/message_box.h" +#include "mkvtoolnix-gui/util/settings.h" +#include "mkvtoolnix-gui/util/widget.h" + +namespace mtx { namespace gui { namespace HeaderEditor { + +using namespace mtx::gui; + +AttachedFilePage::AttachedFilePage(Tab &parent, + PageBase &topLevelPage, + KaxAttachedPtr const &attachment) + : PageBase{parent, "todo"} + , ui{new Ui::AttachedFilePage} + , m_filesDDHandler{Util::FilesDragDropHandler::Mode::Remember} + , m_topLevelPage(topLevelPage) + , m_attachment{attachment} +{ + ui->setupUi(this); + + connect(ui->reset, &QPushButton::clicked, this, &AttachedFilePage::setControlsFromAttachment); +} + +AttachedFilePage::~AttachedFilePage() { +} + +void +AttachedFilePage::retranslateUi() { + ui->retranslateUi(this); + + ui->size->setText(formatSize()); + + Util::setToolTip(ui->name, Q("%1 %2").arg(QY("Other parts of the file (e.g. a subtitle track) may refer to this attachment via this name.")).arg(QY("The name must not be left empty."))); + Util::setToolTip(ui->description, Q("%1 %2").arg(QY("An arbitrary description meant for the user.")).arg(QY("The description can be left empty."))); + Util::setToolTip(ui->mimeType, Q("%1 %2").arg(QY("The MIME type determines which program can be used for handling its content.")).arg(QY("The MIME type must not be left empty."))); + Util::setToolTip(ui->uid, Q("%1 %2").arg(QY("A unique, positive number unambiguously identifying the attachment within the Matroska file.")).arg(QY("The UID must not be left empty."))); + Util::setToolTip(ui->reset, QY("Reset the attachment values on this page to how they're saved in the file.")); +} + +void +AttachedFilePage::init() { + for (auto &mimeType : mime_types) + ui->mimeType->addItem(Q(mimeType.name), Q(mimeType.name)); + + retranslateUi(); + + setControlsFromAttachment(); + + m_parent.appendPage(this, m_topLevelPage.m_pageIdx); + + m_topLevelPage.m_children << this; +} + +void +AttachedFilePage::setControlsFromAttachment() { + auto mimeType = Q(FindChildValue(*m_attachment)); + + ui->name->setText(Q(FindChildValue(*m_attachment))); + ui->description->setText(Q(FindChildValue(*m_attachment))); + ui->mimeType->setEditText(mimeType); + ui->uid->setText(QString::number(FindChildValue(*m_attachment))); + + Util::setComboBoxTextByData(ui->mimeType, mimeType); + + ui->size->setText(formatSize()); + + m_newFileContent.reset(); +} + +void +AttachedFilePage::dragEnterEvent(QDragEnterEvent *event) { + m_filesDDHandler.handle(event, false); +} + +void +AttachedFilePage::dropEvent(QDropEvent *event) { + if (m_filesDDHandler.handle(event, true)) + emit filesDropped(m_filesDDHandler.fileNames()); +} + +QString +AttachedFilePage::title() + const { + return Q(FindChildValue(*m_attachment, to_wide(Y("")))); +} + +void +AttachedFilePage::setItems(QList const &items) + const { + PageBase::setItems(items); + + items.at(1)->setText(Q(FindChildValue(*m_attachment))); + items.at(3)->setText(Q(FindChildValue(*m_attachment))); + items.at(4)->setText(QString::number(FindChildValue(*m_attachment))); + items.at(7)->setText(formatSize()); +} + +QString +AttachedFilePage::formatSize() + const { + if (m_newFileContent) + return QNY("%1 byte (%2)", "%1 bytes (%2)", m_newFileContent->get_size()).arg(m_newFileContent->get_size()).arg(Q(format_file_size(m_newFileContent->get_size()))); + + auto content = FindChild(*m_attachment); + if (content) + return QNY("%1 byte (%2)", "%1 bytes (%2)", content->GetSize()).arg(content->GetSize()).arg(Q(format_file_size(content->GetSize()))); + + return {}; +} + +bool +AttachedFilePage::hasThisBeenModified() + const { + return m_newFileContent + || (Q(FindChildValue(*m_attachment)) != ui->name->text()) + || (Q(FindChildValue(*m_attachment)) != ui->description->text()) + || (Q(FindChildValue(*m_attachment)) != ui->mimeType->currentText()) + || (QString::number(FindChildValue(*m_attachment)) != ui->uid->text()); +} + +void +AttachedFilePage::modifyThis() { + auto description = ui->description->text(); + + GetChild(*m_attachment).SetValueUTF8(to_utf8(ui->name->text())); + GetChild(*m_attachment).SetValue(to_utf8(ui->mimeType->currentText())); + GetChild(*m_attachment).SetValue(ui->uid->text().toULongLong()); + + if (description.isEmpty()) + DeleteChildren(*m_attachment); + else + GetChild(*m_attachment).SetValueUTF8(to_utf8(description)); + + if (m_newFileContent) + GetChild(*m_attachment).CopyBuffer(m_newFileContent->get_buffer(), m_newFileContent->get_size()); +} + +bool +AttachedFilePage::validateThis() + const { + auto ok = false; + + ui->uid->text().toULongLong(&ok); + + ok = ok + && !ui->name->text().isEmpty() + && !ui->mimeType->currentText().isEmpty(); + + return ok; +} + +void +AttachedFilePage::saveContent() { + auto content = FindChild(*m_attachment); + if (!content) + return; + + auto &settings = Util::Settings::get(); + auto fileName = Util::getSaveFileName(this, QY("Save attachment"), Q("%1/%2").arg(Util::dirPath(settings.m_lastOutputDir.path())).arg(ui->name->text()), QY("All files") + Q(" (*)")); + + if (fileName.isEmpty()) + return; + + settings.m_lastOutputDir = QFileInfo{ fileName }.absoluteDir(); + settings.save(); + + QSaveFile file{fileName}; + auto ok = true; + + if (file.open(QIODevice::WriteOnly)) { + file.write(reinterpret_cast(content->GetBuffer()), content->GetSize()); + ok = file.commit(); + } else + ok = false; + + if (!ok) + 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(); +} + +void +AttachedFilePage::replaceContent(bool deriveNameAndMimeType) { + auto &settings = Util::Settings::get(); + auto fileName = Util::getOpenFileName(this, QY("Replace attachment"), Util::dirPath(settings.m_lastOpenDir.path()), QY("All files") + Q(" (*)")); + + if (fileName.isEmpty()) + return; + + auto fileInfo = QFileInfo{ fileName }; + settings.m_lastOpenDir = fileInfo.absoluteDir(); + settings.save(); + + QFile file{fileName}; + + if (!file.open(QIODevice::ReadOnly)) { + Util::MessageBox::critical(this)->title(QY("Reading failed")).text(QY("The file you tried to open (%1) could not be read successfully.").arg(fileName)).exec(); + return; + } + + auto newContent = file.readAll(); + m_newFileContent = memory_c::clone(newContent.data(), newContent.count()); + + ui->size->setText(formatSize()); + + if (!deriveNameAndMimeType) + return; + + auto mimeType = Q(guess_mime_type(to_utf8(fileName), true)); + + ui->name->setText(fileInfo.fileName()); + ui->mimeType->setEditText(mimeType); + Util::setComboBoxTextByData(ui->mimeType, mimeType); +} + +}}} diff --git a/src/mkvtoolnix-gui/header_editor/attached_file_page.h b/src/mkvtoolnix-gui/header_editor/attached_file_page.h new file mode 100644 index 000000000..63ef5fde0 --- /dev/null +++ b/src/mkvtoolnix-gui/header_editor/attached_file_page.h @@ -0,0 +1,62 @@ +#ifndef MTX_MKVTOOLNIX_GUI_HEADER_EDITOR_ATTACHED_FILE_PAGE_H +#define MTX_MKVTOOLNIX_GUI_HEADER_EDITOR_ATTACHED_FILE_PAGE_H + +#include "common/common_pch.h" + +#include "mkvtoolnix-gui/header_editor/page_base.h" +#include "mkvtoolnix-gui/util/files_drag_drop_handler.h" + +namespace libmatroska { +class KaxAttached; +}; + +namespace mtx { namespace gui { namespace HeaderEditor { + +namespace Ui { +class AttachedFilePage; +} + +using KaxAttachedPtr = std::shared_ptr; + +class AttachedFilePage: public PageBase { + Q_OBJECT; + +public: + std::unique_ptr ui; + mtx::gui::Util::FilesDragDropHandler m_filesDDHandler; + PageBase &m_topLevelPage; + KaxAttachedPtr m_attachment; + memory_cptr m_newFileContent; + +public: + AttachedFilePage(Tab &parent, PageBase &topLevelPage, KaxAttachedPtr const &attachment); + virtual ~AttachedFilePage(); + + virtual void init(); + + virtual void retranslateUi() override; + + virtual QString title() const override; + virtual void setItems(QList const &items) const override; + + virtual bool hasThisBeenModified() const; + virtual void modifyThis(); + virtual bool validateThis() const; + + virtual void setControlsFromAttachment(); + virtual void saveContent(); + virtual void replaceContent(bool deriveNameAndMimeType); + +signals: + void filesDropped(QStringList const &fileNames); + +protected: + virtual void dragEnterEvent(QDragEnterEvent *event) override; + virtual void dropEvent(QDropEvent *event) override; + + virtual QString formatSize() const; +}; + +}}} + +#endif // MTX_MKVTOOLNIX_GUI_HEADER_EDITOR_ATTACHED_FILE_PAGE_H diff --git a/src/mkvtoolnix-gui/header_editor/attachments_page.cpp b/src/mkvtoolnix-gui/header_editor/attachments_page.cpp new file mode 100644 index 000000000..252d6768b --- /dev/null +++ b/src/mkvtoolnix-gui/header_editor/attachments_page.cpp @@ -0,0 +1,79 @@ +#include "common/common_pch.h" + +#include +#include + +#include + +#include "common/qt.h" +#include "mkvtoolnix-gui/forms/header_editor/attachments_page.h" +#include "mkvtoolnix-gui/forms/header_editor/tab.h" +#include "mkvtoolnix-gui/header_editor/attached_file_page.h" +#include "mkvtoolnix-gui/header_editor/attachments_page.h" + +namespace mtx { namespace gui { namespace HeaderEditor { + +using namespace mtx::gui; + +AttachmentsPage::AttachmentsPage(Tab &parent, + KaxAttachedList const &attachments) + : TopLevelPage{parent, YT("Attachments"), true} + , ui{new Ui::AttachmentsPage} + , m_filesDDHandler{Util::FilesDragDropHandler::Mode::Remember} + , m_initialAttachments{attachments} +{ + ui->setupUi(this); + + connect(ui->addAttachments, &QPushButton::clicked, &parent, &Tab::selectAttachmentsAndAdd); + connect(this, &AttachmentsPage::filesDropped, &parent, &Tab::addAttachments); +} + +AttachmentsPage::~AttachmentsPage() { +} + +void +AttachmentsPage::init() { + TopLevelPage::init(); + + for (auto const &attachment : m_initialAttachments) + m_parent.addAttachment(attachment); +} + +bool +AttachmentsPage::hasThisBeenModified() + const { + auto numChildren = m_children.count(); + + if (m_initialAttachments.count() != numChildren) + return true; + + for (auto idx = 0; idx < numChildren; ++idx) + if (m_initialAttachments[idx] != dynamic_cast(*m_children[idx]).m_attachment) + return true; + + return false; +} + +void +AttachmentsPage::retranslateUi() { + ui->retranslateUi(this); +} + +void +AttachmentsPage::dragEnterEvent(QDragEnterEvent *event) { + m_filesDDHandler.handle(event, false); +} + +void +AttachmentsPage::dropEvent(QDropEvent *event) { + if (m_filesDDHandler.handle(event, true)) + emit filesDropped(m_filesDDHandler.fileNames()); +} + +QString +AttachmentsPage::internalIdentifier() + const { + return Q("attachments"); +} + +}}} diff --git a/src/mkvtoolnix-gui/header_editor/attachments_page.h b/src/mkvtoolnix-gui/header_editor/attachments_page.h new file mode 100644 index 000000000..fc882177d --- /dev/null +++ b/src/mkvtoolnix-gui/header_editor/attachments_page.h @@ -0,0 +1,51 @@ +#ifndef MTX_MKVTOOLNIX_GUI_HEADER_EDITOR_ATTACHMENTS_PAGE_H +#define MTX_MKVTOOLNIX_GUI_HEADER_EDITOR_ATTACHMENTS_PAGE_H + +#include "common/common_pch.h" + +#include "mkvtoolnix-gui/header_editor/top_level_page.h" +#include "mkvtoolnix-gui/util/files_drag_drop_handler.h" + +namespace libmatroska { +class KaxAttached; +}; + +namespace mtx { namespace gui { namespace HeaderEditor { + +namespace Ui { +class AttachmentsPage; +} + +using KaxAttachedPtr = std::shared_ptr; +using KaxAttachedList = QList< std::shared_ptr >; + +class AttachmentsPage: public TopLevelPage { + Q_OBJECT; + +protected: + std::unique_ptr ui; + mtx::gui::Util::FilesDragDropHandler m_filesDDHandler; + KaxAttachedList m_initialAttachments; + +public: + AttachmentsPage(Tab &parent, KaxAttachedList const &attachments); + virtual ~AttachmentsPage(); + + virtual void init() override; + virtual bool hasThisBeenModified() const override; + virtual QString internalIdentifier() const override; + +signals: + void filesDropped(QStringList const &fileNames); + +public slots: + virtual void retranslateUi() override; + +protected: + virtual void dragEnterEvent(QDragEnterEvent *event) override; + virtual void dropEvent(QDropEvent *event) override; +}; + +}}} + +#endif // MTX_MKVTOOLNIX_GUI_HEADER_EDITOR_ATTACHMENTS_PAGE_H diff --git a/src/mkvtoolnix-gui/header_editor/page_model.cpp b/src/mkvtoolnix-gui/header_editor/page_model.cpp index 2b8d129a4..c8db83ef5 100644 --- a/src/mkvtoolnix-gui/header_editor/page_model.cpp +++ b/src/mkvtoolnix-gui/header_editor/page_model.cpp @@ -96,17 +96,25 @@ PageModel::itemsForIndex(QModelIndex const &idx) { return items; } +QModelIndex +PageModel::indexFromPage(PageBase *page) + const { + return Util::findIndex(*this, [this, page](QModelIndex const &idx) -> bool { + return selectedPage(idx) == page; + }); +} + void PageModel::retranslateUi() { Util::setDisplayableAndSymbolicColumnNames(*this, { - { QY("Type"), Q("type") }, - { QY("Codec"), Q("codec") }, - { QY("Language"), Q("language") }, - { QY("Name"), Q("name") }, - { QY("UID"), Q("uid") }, - { QY("Default track"), Q("defaultTrackFlag") }, - { QY("Forced track"), Q("forcedTrackFlag") }, - { QY("Properties"), Q("properties") }, + { QY("Type"), Q("type") }, + { QY("Codec/MIME type"), Q("codec") }, + { QY("Language"), Q("language") }, + { QY("Name/Description"), Q("name") }, + { QY("UID"), Q("uid") }, + { QY("Default track"), Q("defaultTrackFlag") }, + { QY("Forced track"), Q("forcedTrackFlag") }, + { QY("Properties"), Q("properties") }, }); horizontalHeaderItem(4)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); diff --git a/src/mkvtoolnix-gui/header_editor/page_model.h b/src/mkvtoolnix-gui/header_editor/page_model.h index 5e7e17456..a84cf17e0 100644 --- a/src/mkvtoolnix-gui/header_editor/page_model.h +++ b/src/mkvtoolnix-gui/header_editor/page_model.h @@ -35,6 +35,7 @@ public: void retranslateUi(); QList itemsForIndex(QModelIndex const &idx); + QModelIndex indexFromPage(PageBase *page) const; }; }}} diff --git a/src/mkvtoolnix-gui/header_editor/tab.cpp b/src/mkvtoolnix-gui/header_editor/tab.cpp index c7b9c29ca..1948bbce5 100644 --- a/src/mkvtoolnix-gui/header_editor/tab.cpp +++ b/src/mkvtoolnix-gui/header_editor/tab.cpp @@ -1,17 +1,25 @@ #include "common/common_pch.h" #include +#include #include +#include +#include #include #include +#include "common/construct.h" #include "common/ebml.h" +#include "common/extern_data.h" #include "common/qt.h" #include "common/segmentinfo.h" #include "common/segment_tracks.h" +#include "common/unique_numbers.h" #include "mkvtoolnix-gui/forms/header_editor/tab.h" #include "mkvtoolnix-gui/header_editor/ascii_string_value_page.h" +#include "mkvtoolnix-gui/header_editor/attached_file_page.h" +#include "mkvtoolnix-gui/header_editor/attachments_page.h" #include "mkvtoolnix-gui/header_editor/bit_value_page.h" #include "mkvtoolnix-gui/header_editor/bool_value_page.h" #include "mkvtoolnix-gui/header_editor/float_value_page.h" @@ -23,6 +31,8 @@ #include "mkvtoolnix-gui/header_editor/track_type_page.h" #include "mkvtoolnix-gui/header_editor/unsigned_integer_value_page.h" #include "mkvtoolnix-gui/main_window/main_window.h" +#include "mkvtoolnix-gui/util/basic_tree_view.h" +#include "mkvtoolnix-gui/util/file_dialog.h" #include "mkvtoolnix-gui/util/header_view_manager.h" #include "mkvtoolnix-gui/util/model.h" #include "mkvtoolnix-gui/util/message_box.h" @@ -39,8 +49,14 @@ Tab::Tab(QWidget *parent, , ui{new Ui::Tab} , m_fileName{fileName} , m_model{new PageModel{this}} + , m_treeContextMenu{new QMenu{this}} , m_expandAllAction{new QAction{this}} , m_collapseAllAction{new QAction{this}} + , m_addAttachmentsAction{new QAction{this}} + , m_removeAttachmentAction{new QAction{this}} + , m_saveAttachmentContentAction{new QAction{this}} + , m_replaceAttachmentContentAction{new QAction{this}} + , m_replaceAttachmentContentSetValuesAction{new QAction{this}} { // Setup UI controls. ui->setupUi(this); @@ -75,7 +91,7 @@ Tab::load() { auto expansionStatus = QHash{}; for (auto const &page : m_model->topLevelPages()) { - auto key = page == m_segmentinfoPage ? Q("segmentinfo") : QString::number(static_cast(*page).m_trackNumber); + auto key = dynamic_cast(*page).internalIdentifier(); expansionStatus[key] = ui->elements->isExpanded(page->m_pageIdx); } @@ -102,7 +118,7 @@ Tab::load() { m_analyzer->close_file(); for (auto const &page : m_model->topLevelPages()) { - auto key = page == m_segmentinfoPage ? Q("segmentinfo") : QString::number(static_cast(*page).m_trackNumber); + auto key = dynamic_cast(*page).internalIdentifier(); ui->elements->setExpanded(page->m_pageIdx, expansionStatus[key]); } @@ -115,7 +131,8 @@ Tab::load() { if (-1 != selected2ndLevelRow) selectedIdx = m_model->index(selected2ndLevelRow, 0, selectedIdx); - ui->elements->selectionModel()->select(selectedIdx, QItemSelectionModel::ClearAndSelect); + auto selection = QItemSelection{selectedIdx, selectedIdx.sibling(selectedIdx.row(), m_model->columnCount() - 1)}; + ui->elements->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); selectionChanged(selectedIdx, QModelIndex{}); } @@ -123,6 +140,7 @@ void Tab::save() { auto segmentinfoModified = false; auto tracksModified = false; + auto attachmentsModified = false; for (auto const &page : m_model->topLevelPages()) { if (!page->hasBeenModified()) @@ -130,11 +148,15 @@ Tab::save() { if (page == m_segmentinfoPage) segmentinfoModified = true; + + else if (page == m_attachmentsPage) + attachmentsModified = true; + else tracksModified = true; } - if (!segmentinfoModified && !tracksModified) { + if (!segmentinfoModified && !tracksModified && !attachmentsModified) { 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(); return; } @@ -167,6 +189,21 @@ Tab::save() { QtKaxAnalyzer::displayUpdateElementResult(this, result, QY("Saving the modified track headers failed.")); } + if (attachmentsModified) { + auto attachments = std::make_shared(); + + for (auto const &attachedFilePage : m_attachmentsPage->m_children) + attachments->PushElement(*dynamic_cast(*attachedFilePage).m_attachment.get()); + + auto result = attachments->ListSize() ? m_analyzer->update_element(attachments.get(), true) + : m_analyzer->remove_elements(KaxAttachments::ClassInfos.GlobalId); + + attachments->RemoveAll(); + + if (kax_analyzer_c::uer_success != result) + QtKaxAnalyzer::displayUpdateElementResult(this, result, QY("Saving the modified attachments failed.")); + } + m_analyzer->close_file(); load(); @@ -183,15 +220,21 @@ Tab::setupUi() { ui->directory->setText(info.path()); ui->elements->setModel(m_model); - ui->elements->addAction(m_expandAllAction); - ui->elements->addAction(m_collapseAllAction); + ui->elements->acceptDroppedFiles(true); Util::HeaderViewManager::create(*ui->elements, "HeaderEditor::Elements"); Util::preventScrollingWithoutFocus(this); - connect(ui->elements->selectionModel(), &QItemSelectionModel::currentChanged, this, &Tab::selectionChanged); - connect(m_expandAllAction, &QAction::triggered, this, &Tab::expandAll); - connect(m_collapseAllAction, &QAction::triggered, this, &Tab::collapseAll); + connect(ui->elements, &Util::BasicTreeView::customContextMenuRequested, this, &Tab::showTreeContextMenu); + connect(ui->elements, &Util::BasicTreeView::filesDropped, this, &Tab::addAttachments); + connect(ui->elements->selectionModel(), &QItemSelectionModel::currentChanged, this, &Tab::selectionChanged); + connect(m_expandAllAction, &QAction::triggered, this, &Tab::expandAll); + connect(m_collapseAllAction, &QAction::triggered, this, &Tab::collapseAll); + connect(m_addAttachmentsAction, &QAction::triggered, this, &Tab::selectAttachmentsAndAdd); + connect(m_removeAttachmentAction, &QAction::triggered, this, &Tab::removeSelectedAttachment); + connect(m_saveAttachmentContentAction, &QAction::triggered, this, &Tab::saveAttachmentContent); + connect(m_replaceAttachmentContentAction, &QAction::triggered, [this]() { replaceAttachmentContent(false); }); + connect(m_replaceAttachmentContentSetValuesAction, &QAction::triggered, [this]() { replaceAttachmentContent(true); }); } void @@ -207,6 +250,12 @@ Tab::model() return m_model; } +PageBase * +Tab::currentlySelectedPage() + const { + return m_model->selectedPage(ui->elements->selectionModel()->currentIndex()); +} + void Tab::retranslateUi() { ui->fileNameLabel->setText(QY("File name:")); @@ -214,6 +263,11 @@ Tab::retranslateUi() { m_expandAllAction->setText(QY("&Expand all")); m_collapseAllAction->setText(QY("&Collapse all")); + m_addAttachmentsAction->setText(QY("&Add attachments")); + m_removeAttachmentAction->setText(QY("&Remove selected attachment")); + m_saveAttachmentContentAction->setText(QY("Save attachment content to a &file")); + m_replaceAttachmentContentAction->setText(QY("Replace attachment with a new &file")); + m_replaceAttachmentContentSetValuesAction->setText(QY("Replace attachment with new a file and &derive name && MIME type from it")); auto &pages = m_model->pages(); for (auto const &page : pages) @@ -233,6 +287,8 @@ Tab::populateTree() { m_analyzer->with_elements(KaxTracks::ClassInfos.GlobalId, [this](kax_analyzer_data_c const &data) { handleTracks(data); }); + + handleAttachments(); } void @@ -288,8 +344,9 @@ Tab::handleSegmentInfo(kax_analyzer_data_c const &data) { if (!m_eSegmentInfo) return; - auto &info = static_cast(*m_eSegmentInfo.get()); + auto &info = dynamic_cast(*m_eSegmentInfo.get()); auto page = new TopLevelPage{*this, YT("Segment information")}; + page->setInternalIdentifier("segmentInfo"); page->init(); (new StringValuePage{*this, *page, info, KaxTitle::ClassInfos, YT("Title"), YT("The title for the whole movie.")})->init(); @@ -311,7 +368,7 @@ Tab::handleTracks(kax_analyzer_data_c const &data) { auto trackIdxMkvmerge = 0u; - for (auto const &element : static_cast(*m_eTracks)) { + for (auto const &element : dynamic_cast(*m_eTracks)) { auto kTrackEntry = dynamic_cast(element); if (!kTrackEntry) continue; @@ -426,9 +483,34 @@ Tab::handleTracks(kax_analyzer_data_c const &data) { } } +void +Tab::handleAttachments() { + auto attachments = KaxAttachedList{}; + + m_analyzer->with_elements(KaxAttachments::ClassInfos.GlobalId, [this, &attachments](kax_analyzer_data_c const &data) { + auto master = std::dynamic_pointer_cast(m_analyzer->read_element(data)); + if (!master) + return; + + auto idx = 0u; + while (idx < master->ListSize()) { + auto attached = dynamic_cast((*master)[idx]); + if (attached) { + attachments << KaxAttachedPtr{attached}; + master->Remove(idx); + } else + ++idx; + } + }); + + m_attachmentsPage = new AttachmentsPage{*this, attachments}; + m_attachmentsPage->init(); +} + void Tab::validate() { auto pageIdx = m_model->validate(); + // TODO: Tab::validate: handle attachments if (!pageIdx.isValid()) { Util::MessageBox::information(this)->title(QY("Header validation")).text(QY("All header values are OK.")).exec(); @@ -467,4 +549,121 @@ Tab::expandCollapseAll(bool expand) { ui->elements->setExpanded(page->m_pageIdx, expand); } +void +Tab::showTreeContextMenu(QPoint const &pos) { + auto selectedPage = currentlySelectedPage(); + auto isAttachmentsPage = !!dynamic_cast(selectedPage); + auto isAttachedFilePage = !!dynamic_cast(selectedPage); + auto isAttachments = isAttachmentsPage || isAttachedFilePage; + auto actions = m_treeContextMenu->actions(); + + for (auto const &action : actions) + if (!action->isSeparator()) + m_treeContextMenu->removeAction(action); + + m_treeContextMenu->clear(); + + m_treeContextMenu->addAction(m_expandAllAction); + m_treeContextMenu->addAction(m_collapseAllAction); + m_treeContextMenu->addSeparator(); + m_treeContextMenu->addAction(m_addAttachmentsAction); + + if (isAttachments) { + m_treeContextMenu->addAction(m_removeAttachmentAction); + m_treeContextMenu->addSeparator(); + m_treeContextMenu->addAction(m_saveAttachmentContentAction); + m_treeContextMenu->addAction(m_replaceAttachmentContentAction); + m_treeContextMenu->addAction(m_replaceAttachmentContentSetValuesAction); + + m_removeAttachmentAction->setEnabled(isAttachedFilePage); + m_saveAttachmentContentAction->setEnabled(isAttachedFilePage); + m_replaceAttachmentContentAction->setEnabled(isAttachedFilePage); + m_replaceAttachmentContentSetValuesAction->setEnabled(isAttachedFilePage); + } + + m_treeContextMenu->exec(ui->elements->viewport()->mapToGlobal(pos)); +} + +void +Tab::selectAttachmentsAndAdd() { + auto &settings = Util::Settings::get(); + auto fileNames = Util::getOpenFileNames(this, QY("Add attachments"), Util::dirPath(settings.lastOpenDirPath()), QY("All files") + Q(" (*)")); + + if (fileNames.isEmpty()) + return; + + settings.m_lastOpenDir = QFileInfo{fileNames[0]}.path(); + settings.save(); + + addAttachments(fileNames); +} + +void +Tab::addAttachment(KaxAttachedPtr const &attachment) { + if (!attachment) + return; + + auto page = new AttachedFilePage{*this, *m_attachmentsPage, attachment}; + page->init(); +} + +void +Tab::addAttachments(QStringList const &fileNames) { + for (auto const &fileName : fileNames) + addAttachment(createAttachmentFromFile(fileName)); + + ui->elements->setExpanded(m_attachmentsPage->m_pageIdx, true); +} + +void +Tab::removeSelectedAttachment() { + auto selectedPage = currentlySelectedPage(); + if (!selectedPage) + return; + + auto idx = m_model->indexFromPage(selectedPage); + if (idx.isValid()) + m_model->removeRow(idx.row(), idx.parent()); + + m_attachmentsPage->m_children.removeAll(selectedPage); + + delete selectedPage; +} + +KaxAttachedPtr +Tab::createAttachmentFromFile(QString const &fileName) { + QByteArray content; + QFile file{fileName}; + + if (!file.open(QIODevice::ReadOnly)) { + Util::MessageBox::critical(this)->title(QY("Reading failed")).text(QY("The file you tried to open (%1) could not be read successfully.").arg(fileName)).exec(); + return {}; + } + + auto mimeType = guess_mime_type(to_utf8(fileName), true); + auto contentAsMem = std::make_shared(content.data(), content.count(), false); + auto uid = create_unique_number(UNIQUE_ATTACHMENT_IDS); + + return KaxAttachedPtr{ + mtx::construct::cons(new KaxFileName, to_wide(QFileInfo{fileName}.fileName()), + new KaxMimeType, mimeType, + new KaxFileUID, uid, + new KaxFileData, contentAsMem) + }; +} + +void +Tab::saveAttachmentContent() { + auto page = dynamic_cast(currentlySelectedPage()); + if (page) + page->saveContent(); +} + +void +Tab::replaceAttachmentContent(bool deriveNameAndMimeType) { + auto page = dynamic_cast(currentlySelectedPage()); + if (page) + page->replaceContent(deriveNameAndMimeType); +} + }}} diff --git a/src/mkvtoolnix-gui/header_editor/tab.h b/src/mkvtoolnix-gui/header_editor/tab.h index c189f6734..fdc209481 100644 --- a/src/mkvtoolnix-gui/header_editor/tab.h +++ b/src/mkvtoolnix-gui/header_editor/tab.h @@ -9,6 +9,7 @@ #include "mkvtoolnix-gui/header_editor/page_model.h" class QAction; +class QMenu; namespace mtx { namespace gui { namespace HeaderEditor { @@ -16,6 +17,10 @@ namespace Ui { class Tab; } +using KaxAttachedPtr = std::shared_ptr; + +class AttachmentsPage; + class Tab : public QWidget { Q_OBJECT; @@ -29,8 +34,10 @@ protected: PageModel *m_model; PageBase *m_segmentinfoPage{}; + AttachmentsPage *m_attachmentsPage{}; - QAction *m_expandAllAction, *m_collapseAllAction; + QMenu *m_treeContextMenu; + QAction *m_expandAllAction, *m_collapseAllAction, *m_addAttachmentsAction, *m_removeAttachmentAction, *m_saveAttachmentContentAction, *m_replaceAttachmentContentAction, *m_replaceAttachmentContentSetValuesAction; std::shared_ptr m_eSegmentInfo, m_eTracks; @@ -46,26 +53,38 @@ public: virtual QString const &fileName() const; virtual QString title() const; virtual void validate(); + virtual void addAttachment(KaxAttachedPtr const &attachment); signals: void removeThisTab(); public slots: + virtual void showTreeContextMenu(QPoint const &pos); virtual void selectionChanged(QModelIndex const ¤t, QModelIndex const &previous); virtual void load(); virtual void save(); virtual void expandAll(); virtual void collapseAll(); + virtual void selectAttachmentsAndAdd(); + virtual void addAttachments(QStringList const &fileNames); + virtual void removeSelectedAttachment(); + virtual void saveAttachmentContent(); + virtual void replaceAttachmentContent(bool deriveNameAndMimeType); protected: void setupUi(); void handleSegmentInfo(kax_analyzer_data_c const &data); void handleTracks(kax_analyzer_data_c const &data); + void handleAttachments(); void populateTree(); void resetData(); void doModifications(); void expandCollapseAll(bool expand); void reportValidationFailure(bool isCritical, QModelIndex const &pageIdx); + + PageBase *currentlySelectedPage() const; + + KaxAttachedPtr createAttachmentFromFile(QString const &fileName); }; }}} diff --git a/src/mkvtoolnix-gui/header_editor/top_level_page.cpp b/src/mkvtoolnix-gui/header_editor/top_level_page.cpp index ebf089c74..566a617cb 100644 --- a/src/mkvtoolnix-gui/header_editor/top_level_page.cpp +++ b/src/mkvtoolnix-gui/header_editor/top_level_page.cpp @@ -21,4 +21,15 @@ TopLevelPage::init() { m_parent.appendPage(this); } +QString +TopLevelPage::internalIdentifier() + const { + return m_internalIdentifier; +} + +void +TopLevelPage::setInternalIdentifier(QString const &identifier) { + m_internalIdentifier = identifier; +} + }}} diff --git a/src/mkvtoolnix-gui/header_editor/top_level_page.h b/src/mkvtoolnix-gui/header_editor/top_level_page.h index a469542a2..aa1fdd443 100644 --- a/src/mkvtoolnix-gui/header_editor/top_level_page.h +++ b/src/mkvtoolnix-gui/header_editor/top_level_page.h @@ -9,11 +9,15 @@ namespace mtx { namespace gui { namespace HeaderEditor { class TopLevelPage: public EmptyPage { Q_OBJECT; + QString m_internalIdentifier; + public: TopLevelPage(Tab &parent, translatable_string_c const &title, bool customLayout = false); virtual ~TopLevelPage(); virtual void init(); + virtual QString internalIdentifier() const; + virtual void setInternalIdentifier(QString const &identifier); }; }}} diff --git a/src/mkvtoolnix-gui/header_editor/track_type_page.cpp b/src/mkvtoolnix-gui/header_editor/track_type_page.cpp index 381326e4d..009a992c9 100644 --- a/src/mkvtoolnix-gui/header_editor/track_type_page.cpp +++ b/src/mkvtoolnix-gui/header_editor/track_type_page.cpp @@ -135,4 +135,10 @@ TrackTypePage::summarizeProperties() { m_properties = properties.join(Q(", ")); } +QString +TrackTypePage::internalIdentifier() + const { + return Q("track %1").arg(m_trackIdxMkvmerge); +} + }}} diff --git a/src/mkvtoolnix-gui/header_editor/track_type_page.h b/src/mkvtoolnix-gui/header_editor/track_type_page.h index 10fc8c419..aa9d949a3 100644 --- a/src/mkvtoolnix-gui/header_editor/track_type_page.h +++ b/src/mkvtoolnix-gui/header_editor/track_type_page.h @@ -29,6 +29,8 @@ public: TrackTypePage(Tab &parent, EbmlMaster &master, uint64_t trackIdxMkvmerge); virtual ~TrackTypePage(); + virtual QString internalIdentifier() const override; + protected: virtual void setItems(QList const &items) const override; virtual void summarizeProperties(); diff --git a/src/mkvtoolnix-gui/mkvtoolnix-gui.pro b/src/mkvtoolnix-gui/mkvtoolnix-gui.pro index f50ce8dfc..f74230fd3 100644 --- a/src/mkvtoolnix-gui/mkvtoolnix-gui.pro +++ b/src/mkvtoolnix-gui/mkvtoolnix-gui.pro @@ -40,7 +40,9 @@ FORMS += \ forms/watch_jobs/tab.ui \ forms/watch_jobs/tool.ui \ forms/merge/executable_location_dialog.ui \ - forms/main_window/prefs_run_program_widget.ui + forms/main_window/prefs_run_program_widget.ui \ + forms/header_editor/attachments_page.ui \ + forms/header_editor/attached_file_page.ui RESOURCES += \ qt_resources.qrc diff --git a/src/mkvtoolnix-gui/util/model.cpp b/src/mkvtoolnix-gui/util/model.cpp index 26dfbbab4..21c038d8e 100644 --- a/src/mkvtoolnix-gui/util/model.cpp +++ b/src/mkvtoolnix-gui/util/model.cpp @@ -128,4 +128,20 @@ walkTree(QAbstractItemModel &model, walkTree(model, model.index(row, 0, idx), worker); } +QModelIndex +findIndex(QAbstractItemModel const &model, + std::function const &predicate, + QModelIndex const &idx) { + if (idx.isValid() && predicate(idx)) + return idx; + + for (auto row = 0, numRows = model.rowCount(idx); row < numRows; ++row) { + auto result = findIndex(model, predicate, model.index(row, 0, idx)); + if (result.isValid()) + return result; + } + + return {}; +} + }}} diff --git a/src/mkvtoolnix-gui/util/model.h b/src/mkvtoolnix-gui/util/model.h index 1e9623034..9437f9d02 100644 --- a/src/mkvtoolnix-gui/util/model.h +++ b/src/mkvtoolnix-gui/util/model.h @@ -36,6 +36,7 @@ void withSelectedIndexes(QAbstractItemView *view, std::function const &worker); +QModelIndex findIndex(QAbstractItemModel const &model, std::function const &predicate, QModelIndex const &idx = QModelIndex{}); }}}