GUI: edit attachments inside the header editor

Implements #1533.
This commit is contained in:
Moritz Bunkus 2015-12-31 20:27:56 +01:00
parent 8badffee79
commit 1ac25bc3c5
18 changed files with 1015 additions and 23 deletions

View File

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>mtx::gui::HeaderEditor::AttachedFilePage</class>
<widget class="QWidget" name="mtx::gui::HeaderEditor::AttachedFilePage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>546</width>
<height>361</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="heading">
<property name="text">
<string>Attachment</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="1">
<widget class="QLineEdit" name="description"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="name"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Fi&amp;le name:</string>
</property>
<property name="buddy">
<cstring>name</cstring>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="descriptionLabel">
<property name="text">
<string>&amp;Description:</string>
</property>
<property name="buddy">
<cstring>description</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="size">
<property name="text">
<string notr="true">TextLabel</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="mimeType">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="sizeLabel">
<property name="text">
<string>Size:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="mimeTypeLabel">
<property name="text">
<string>&amp;MIME type:</string>
</property>
<property name="buddy">
<cstring>mimeType</cstring>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;UID:</string>
</property>
<property name="buddy">
<cstring>uid</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="uid"/>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="reset">
<property name="text">
<string notr="true">&amp;Reset</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>name</tabstop>
<tabstop>description</tabstop>
<tabstop>mimeType</tabstop>
<tabstop>uid</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>mtx::gui::HeaderEditor::AttachmentsPage</class>
<widget class="QWidget" name="mtx::gui::HeaderEditor::AttachmentsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>457</width>
<height>472</height>
</rect>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="windowTitle">
<string notr="true">Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="heading">
<property name="text">
<string>Attachments</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>136</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="instructions">
<property name="text">
<string>You can add attachments by clicking on the button below, by right-clicking on a node in the tree and selecting &quot;Add attachments&quot; from the popup menu or by dragging &amp; dropping files here or onto the tree.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="addAttachments">
<property name="text">
<string>&amp;Add attachments</string>
</property>
<property name="icon">
<iconset resource="../../qt_resources.qrc">
<normaloff>:/icons/16x16/list-add.png</normaloff>:/icons/16x16/list-add.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>135</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>
<include location="../../qt_resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -85,7 +85,7 @@
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTreeView" name="elements">
<widget class="mtx::gui::Util::BasicTreeView" name="elements">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
@ -99,7 +99,10 @@
</size>
</property>
<property name="contextMenuPolicy">
<enum>Qt::ActionsContextMenu</enum>
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
@ -130,6 +133,11 @@
<extends>QLabel</extends>
<header>mkvtoolnix-gui/util/elide_label.h</header>
</customwidget>
<customwidget>
<class>mtx::gui::Util::BasicTreeView</class>
<extends>QTreeView</extends>
<header>mkvtoolnix-gui/util/basic_tree_view.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@ -0,0 +1,229 @@
#include "common/common_pch.h"
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QSaveFile>
#include <matroska/KaxAttached.h>
#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<KaxMimeType>(*m_attachment));
ui->name->setText(Q(FindChildValue<KaxFileName>(*m_attachment)));
ui->description->setText(Q(FindChildValue<KaxFileDescription>(*m_attachment)));
ui->mimeType->setEditText(mimeType);
ui->uid->setText(QString::number(FindChildValue<KaxFileUID>(*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<KaxFileName>(*m_attachment, to_wide(Y("<unnamed>"))));
}
void
AttachedFilePage::setItems(QList<QStandardItem *> const &items)
const {
PageBase::setItems(items);
items.at(1)->setText(Q(FindChildValue<KaxMimeType>(*m_attachment)));
items.at(3)->setText(Q(FindChildValue<KaxFileDescription>(*m_attachment)));
items.at(4)->setText(QString::number(FindChildValue<KaxFileUID>(*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<KaxFileData>(*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<KaxFileName>(*m_attachment)) != ui->name->text())
|| (Q(FindChildValue<KaxFileDescription>(*m_attachment)) != ui->description->text())
|| (Q(FindChildValue<KaxMimeType>(*m_attachment)) != ui->mimeType->currentText())
|| (QString::number(FindChildValue<KaxFileUID>(*m_attachment)) != ui->uid->text());
}
void
AttachedFilePage::modifyThis() {
auto description = ui->description->text();
GetChild<KaxFileName>(*m_attachment).SetValueUTF8(to_utf8(ui->name->text()));
GetChild<KaxMimeType>(*m_attachment).SetValue(to_utf8(ui->mimeType->currentText()));
GetChild<KaxFileUID>(*m_attachment).SetValue(ui->uid->text().toULongLong());
if (description.isEmpty())
DeleteChildren<KaxFileDescription>(*m_attachment);
else
GetChild<KaxFileDescription>(*m_attachment).SetValueUTF8(to_utf8(description));
if (m_newFileContent)
GetChild<KaxFileData>(*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<KaxFileData>(*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<char *>(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);
}
}}}

View File

@ -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<KaxAttached>;
class AttachedFilePage: public PageBase {
Q_OBJECT;
public:
std::unique_ptr<Ui::AttachedFilePage> 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<QStandardItem *> 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

View File

@ -0,0 +1,79 @@
#include "common/common_pch.h"
#include <QDragEnterEvent>
#include <QDropEvent>
#include <matroska/KaxAttached.h>
#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<AttachedFilePage &>(*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");
}
}}}

View File

@ -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<KaxAttached>;
using KaxAttachedList = QList< std::shared_ptr<KaxAttached> >;
class AttachmentsPage: public TopLevelPage {
Q_OBJECT;
protected:
std::unique_ptr<Ui::AttachmentsPage> 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

View File

@ -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);

View File

@ -35,6 +35,7 @@ public:
void retranslateUi();
QList<QStandardItem *> itemsForIndex(QModelIndex const &idx);
QModelIndex indexFromPage(PageBase *page) const;
};
}}}

View File

@ -1,17 +1,25 @@
#include "common/common_pch.h"
#include <QFileInfo>
#include <QMenu>
#include <QMessageBox>
#include <matroska/KaxAttached.h>
#include <matroska/KaxAttachments.h>
#include <matroska/KaxInfoData.h>
#include <matroska/KaxSemantic.h>
#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<QString, bool>{};
for (auto const &page : m_model->topLevelPages()) {
auto key = page == m_segmentinfoPage ? Q("segmentinfo") : QString::number(static_cast<TrackTypePage &>(*page).m_trackNumber);
auto key = dynamic_cast<TopLevelPage &>(*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<TrackTypePage &>(*page).m_trackNumber);
auto key = dynamic_cast<TopLevelPage &>(*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<KaxAttachments>();
for (auto const &attachedFilePage : m_attachmentsPage->m_children)
attachments->PushElement(*dynamic_cast<AttachedFilePage &>(*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<KaxInfo &>(*m_eSegmentInfo.get());
auto &info = dynamic_cast<KaxInfo &>(*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<EbmlMaster &>(*m_eTracks)) {
for (auto const &element : dynamic_cast<EbmlMaster &>(*m_eTracks)) {
auto kTrackEntry = dynamic_cast<KaxTrackEntry *>(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<KaxAttachments>(m_analyzer->read_element(data));
if (!master)
return;
auto idx = 0u;
while (idx < master->ListSize()) {
auto attached = dynamic_cast<KaxAttached *>((*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<AttachmentsPage *>(selectedPage);
auto isAttachedFilePage = !!dynamic_cast<AttachedFilePage *>(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<memory_c>(content.data(), content.count(), false);
auto uid = create_unique_number(UNIQUE_ATTACHMENT_IDS);
return KaxAttachedPtr{
mtx::construct::cons<KaxAttached>(new KaxFileName, to_wide(QFileInfo{fileName}.fileName()),
new KaxMimeType, mimeType,
new KaxFileUID, uid,
new KaxFileData, contentAsMem)
};
}
void
Tab::saveAttachmentContent() {
auto page = dynamic_cast<AttachedFilePage *>(currentlySelectedPage());
if (page)
page->saveContent();
}
void
Tab::replaceAttachmentContent(bool deriveNameAndMimeType) {
auto page = dynamic_cast<AttachedFilePage *>(currentlySelectedPage());
if (page)
page->replaceContent(deriveNameAndMimeType);
}
}}}

View File

@ -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<KaxAttached>;
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<EbmlElement> 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 &current, 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);
};
}}}

View File

@ -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;
}
}}}

View File

@ -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);
};
}}}

View File

@ -135,4 +135,10 @@ TrackTypePage::summarizeProperties() {
m_properties = properties.join(Q(", "));
}
QString
TrackTypePage::internalIdentifier()
const {
return Q("track %1").arg(m_trackIdxMkvmerge);
}
}}}

View File

@ -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<QStandardItem *> const &items) const override;
virtual void summarizeProperties();

View File

@ -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

View File

@ -128,4 +128,20 @@ walkTree(QAbstractItemModel &model,
walkTree(model, model.index(row, 0, idx), worker);
}
QModelIndex
findIndex(QAbstractItemModel const &model,
std::function<bool(QModelIndex const &)> 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 {};
}
}}}

View File

@ -36,6 +36,7 @@ void withSelectedIndexes(QAbstractItemView *view, std::function<void(QModelIndex
void selectRow(QAbstractItemView *view, int row, QModelIndex const &parentIdx = QModelIndex{});
QModelIndex toTopLevelIdx(QModelIndex const &idx);
void walkTree(QAbstractItemModel &model, QModelIndex const &idx, std::function<void(QModelIndex const &)> const &worker);
QModelIndex findIndex(QAbstractItemModel const &model, std::function<bool(QModelIndex const &)> const &predicate, QModelIndex const &idx = QModelIndex{});
}}}