From f8cfb3ec14b951b651d7ceae0de00f03247c15ef Mon Sep 17 00:00:00 2001 From: Martchus Date: Mon, 29 Feb 2016 23:59:46 +0100 Subject: [PATCH] moved parts of the MainWindow class to new TagEditorWidget class --- CMakeLists.txt | 3 + gui/initiate.cpp | 2 +- gui/mainwindow.cpp | 1337 ++++----------------------------------- gui/mainwindow.h | 94 +-- gui/mainwindow.ui | 300 +-------- gui/tageditorwidget.cpp | 1231 +++++++++++++++++++++++++++++++++++ gui/tageditorwidget.h | 187 ++++++ gui/tageditorwidget.ui | 305 +++++++++ misc/utility.h | 8 +- tageditor.pro | 9 +- 10 files changed, 1878 insertions(+), 1598 deletions(-) create mode 100644 gui/tageditorwidget.cpp create mode 100644 gui/tageditorwidget.h create mode 100644 gui/tageditorwidget.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 51ff5bf..1fe38cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ set(WIDGETS_HEADER_FILES gui/settingsdialog.h gui/tagedit.h gui/tagfieldedit.h + gui/tageditorwidget.h renamingutility/filesystemitem.h renamingutility/filesystemitemmodel.h renamingutility/filteredfilesystemitemmodel.h @@ -82,6 +83,7 @@ set(WIDGETS_SRC_FILES gui/settingsdialog.cpp gui/tagedit.cpp gui/tagfieldedit.cpp + gui/tageditorwidget.cpp renamingutility/filesystemitem.cpp renamingutility/filesystemitemmodel.cpp renamingutility/filteredfilesystemitemmodel.cpp @@ -106,6 +108,7 @@ set(WIDGETS_UI_FILES gui/attachmentsedit.ui gui/editortempoptionpage.ui gui/filelayout.ui + gui/tageditorwidget.ui ) #set(QUICK_HEADER_FILES #) diff --git a/gui/initiate.cpp b/gui/initiate.cpp index cc3afd9..69bb340 100644 --- a/gui/initiate.cpp +++ b/gui/initiate.cpp @@ -37,7 +37,7 @@ int runWidgetsGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, MainWindow w; w.show(); if(!path.isEmpty()) { - w.startParsing(path, true); + w.startParsing(path); } res = a.exec(); } diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index e81f070..9b6b07f 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -1,26 +1,14 @@ #include "./mainwindow.h" #include "./settingsdialog.h" -#include "./notificationlabel.h" #include "./renamefilesdialog.h" -#include "./tagedit.h" -#include "./attachmentsedit.h" -#include "./entertargetdialog.h" +#include "./tageditorwidget.h" #include "../application/settings.h" -#include "../misc/htmlinfo.h" #include "../misc/utility.h" #include "ui_mainwindow.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include #include @@ -30,32 +18,11 @@ #include #include -#include -#include #include -#include -#include -#include #include -#include -#include -#include #include -#include -#include -#ifdef TAGEDITOR_USE_WEBENGINE -# include -#else -# include -#endif -#include -#include -#include -#include #include -#include -#include using namespace std; using namespace Utility; @@ -77,9 +44,25 @@ enum LoadingResult : char /*! * \class QtGui::MainWindow - * \brief The MainWindow class provides the main window of the Tag Editor's Qt gui. + * \brief The MainWindow class provides the main window of the Tag Editor's Qt GUI. */ +/*! + * \brief Shortcut to access file operation mutex of TagEditorWidget. + */ +std::mutex &MainWindow::fileOperationMutex() +{ + return m_ui->tagEditorWidget->fileOperationMutex(); +} + +/*! + * \brief Shortcut to access MediaFileInfo of TagEditorWidget. + */ +MediaFileInfo &MainWindow::fileInfo() +{ + return m_ui->tagEditorWidget->fileInfo(); +} + /*! * \brief Constructs a new main window. */ @@ -87,18 +70,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent, Qt::Window), m_ui(new Ui::MainWindow()), m_aboutDlg(nullptr), - m_settingsDlg(nullptr), - m_makingResultsAvailable(false), - m_abortClicked(false) + m_settingsDlg(nullptr) { // setup UI m_ui->setupUi(this); - makeHeading(m_ui->fileNameLabel); - m_infoWebView = new WEB_VIEW_PROVIDER(m_ui->tagSplitter); - m_infoWebView->setAcceptDrops(false); - m_infoWebView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(m_infoWebView, &QWidget::customContextMenuRequested, this, &MainWindow::showInfoWebViewContextMenu); - m_ui->tagSplitter->addWidget(m_infoWebView); #ifdef Q_OS_WIN32 setStyleSheet(dialogStyle() + QStringLiteral("#rightWidget, #rightWidget QSplitter::handle { color: palette(text); background-color: palette(base); }")); #else @@ -119,75 +94,35 @@ MainWindow::MainWindow(QWidget *parent) : m_ui->filesTreeView->setColumnWidth(0, 300); // setup path line edit m_ui->pathLineEdit->setCompletionModel(m_fileModel); - // setup file watcher - m_fileWatcher = new QFileSystemWatcher(this); - m_fileChangedOnDisk = false; - // setup command link button icons - m_ui->saveButton->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton, nullptr, m_ui->saveButton)); - m_ui->deleteTagsButton->setIcon(style()->standardIcon(QStyle::SP_DialogResetButton, nullptr, m_ui->deleteTagsButton)); - m_ui->closeButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton, nullptr, m_ui->closeButton)); - // setup m_keepPreviousValuesMenu - m_keepPreviousValuesMenu = new QMenu(this); - QActionGroup *group = new QActionGroup(this); - group->addAction(m_ui->actionKeep_previous_values_never); - group->addAction(m_ui->actionKeep_previous_values_within_same_dir); - group->addAction(m_ui->actionKeep_previous_values_always); - connect(group, &QActionGroup::triggered, this, &MainWindow::handleKeepPreviousValuesActionTriggered); - m_keepPreviousValuesMenu->addActions(group->actions()); - m_ui->keepPreviousValuesPushButton->setMenu(m_keepPreviousValuesMenu); - // setup m_tagOptionsMenu, m_addTagMenu, m_removeTagMenu, m_changeTargetMenu - m_tagOptionsMenu = new QMenu(this); - m_tagOptionsMenu->addAction(m_ui->actionManage_tags_automatically_when_loading_file); - connect(m_ui->actionManage_tags_automatically_when_loading_file, &QAction::triggered, [] (bool checked) { Settings::autoTagManagement() = checked; }); - m_tagOptionsMenu->addSeparator(); - m_addTagMenu = new QMenu(tr("Add tag"), m_tagOptionsMenu); - m_addTagMenu->setEnabled(false); - m_addTagMenu->setIcon(QIcon::fromTheme(QStringLiteral("tag-add"))); - m_tagOptionsMenu->addMenu(m_addTagMenu); - m_removeTagMenu = new QMenu(tr("Remove tag"), m_tagOptionsMenu); - m_removeTagMenu->setEnabled(false); - m_removeTagMenu->setIcon(QIcon::fromTheme(QStringLiteral("tag-delete"))); - m_tagOptionsMenu->addMenu(m_removeTagMenu); - m_changeTargetMenu = new QMenu(tr("Change target"), m_tagOptionsMenu); - m_changeTargetMenu->setEnabled(false); - m_changeTargetMenu->setIcon(QIcon::fromTheme(QStringLiteral("tag-properties"))); - m_tagOptionsMenu->addMenu(m_changeTargetMenu); - m_ui->tagOptionsPushButton->setMenu(m_tagOptionsMenu); - // other widgets - updateUiStatus(); - m_ui->abortButton->setVisible(false); + // apply initial file status + handleFileStatusChange(false, false); // connect signals and slots, install event filter // menu: application connect(m_ui->actionSettings, &QAction::triggered, this, &MainWindow::showSettingsDlg); connect(m_ui->actionQuit, &QAction::triggered, this, &MainWindow::close); // menu: file connect(m_ui->actionOpen, &QAction::triggered, this, &MainWindow::showOpenFileDlg); - connect(m_ui->actionSave, &QAction::triggered, this, &MainWindow::applyEntriesAndSaveChangings); - connect(m_ui->actionDelete_all_tags, &QAction::triggered, this, &MainWindow::deleteAllTagsAndSave); + connect(m_ui->actionSave, &QAction::triggered, m_ui->tagEditorWidget, &TagEditorWidget::applyEntriesAndSaveChangings); + connect(m_ui->actionDelete_all_tags, &QAction::triggered, m_ui->tagEditorWidget, &TagEditorWidget::deleteAllTagsAndSave); connect(m_ui->actionSave_file_information, &QAction::triggered, this, &MainWindow::saveFileInformation); - connect(m_ui->actionClose, &QAction::triggered, this, &MainWindow::closeFile); - connect(m_ui->actionReload, &QAction::triggered, this, &MainWindow::reparseFile); + connect(m_ui->actionClose, &QAction::triggered, m_ui->tagEditorWidget, &TagEditorWidget::closeFile); + connect(m_ui->actionReload, &QAction::triggered, m_ui->tagEditorWidget, &TagEditorWidget::reparseFile); connect(m_ui->actionExternalPlayer, &QAction::triggered, this, &MainWindow::spawnExternalPlayer); // menu: directory - connect(m_ui->actionSelect_next_file, &QAction::triggered, this, &MainWindow::selectNextFile); - connect(m_ui->actionSelect_next_file_and_save_current, &QAction::triggered, this, &MainWindow::saveAndShowNextFile); + connect(m_ui->actionSelect_next_file, &QAction::triggered, this, static_cast(&MainWindow::selectNextFile)); + connect(m_ui->actionSelect_next_file_and_save_current, &QAction::triggered, m_ui->tagEditorWidget, &TagEditorWidget::saveAndShowNextFile); connect(m_ui->actionRename_files, &QAction::triggered, this, &MainWindow::showRenameFilesDlg); // menu: help connect(m_ui->actionAbout, &QAction::triggered, this, &MainWindow::showAboutDlg); - // buttons: save, delete, next, close - connect(m_ui->saveButton, &QPushButton::clicked, this, &MainWindow::applyEntriesAndSaveChangings); - connect(m_ui->deleteTagsButton, &QPushButton::clicked, this, &MainWindow::deleteAllTagsAndSave); - connect(m_ui->nextButton, &QPushButton::clicked, this, &MainWindow::saveAndShowNextFile); - connect(m_ui->closeButton, &QPushButton::clicked, this, &MainWindow::closeFile); + // tag editor widget + connect(m_ui->tagEditorWidget, &TagEditorWidget::nextFileSelected, this, static_cast(&MainWindow::selectNextFile)); + connect(m_ui->tagEditorWidget, &TagEditorWidget::fileStatusChange, this, &MainWindow::handleFileStatusChange); + connect(m_ui->tagEditorWidget, &TagEditorWidget::statusMessage, m_ui->statusBar, &QStatusBar::showMessage); + connect(m_ui->tagEditorWidget, &TagEditorWidget::fileSaved, this, &MainWindow::handleFileSaved); // misc connect(m_ui->pathLineEdit, &QLineEdit::textEdited, this, &MainWindow::pathEntered); connect(m_ui->filesTreeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::fileSelected); - connect(m_ui->selectNextCommandLinkButton, &QCommandLinkButton::clicked, this, &MainWindow::selectNextFile); - connect(m_ui->abortButton, &QPushButton::clicked, [this] {m_abortClicked = true; m_ui->abortButton->setEnabled(false); }); - connect(m_ui->tagSelectionComboBox, static_cast(&QComboBox::activated), m_ui->stackedWidget, &QStackedWidget::setCurrentIndex); - connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &MainWindow::fileChangedOnDisk); - // event filter - m_ui->rightWidget->installEventFilter(this); + connect(m_ui->selectNextCommandLinkButton, &QCommandLinkButton::clicked, this, static_cast(&MainWindow::selectNextFile)); // apply settings setCurrentDirectory(Settings::mainWindowCurrentFileBrowserDirectory()); applySettingsFromDialog(); @@ -216,6 +151,14 @@ void MainWindow::setCurrentDirectory(const QString &path) m_ui->pathLineEdit->editText(path); } +/*! + * \brief Starts parsing the specified file. + */ +void MainWindow::startParsing(const QString &path) +{ + m_ui->tagEditorWidget->startParsing(path, true); +} + /*! * \brief * - Saves the applications settings relating the state of the main window. @@ -224,9 +167,6 @@ void MainWindow::setCurrentDirectory(const QString &path) bool MainWindow::event(QEvent *event) { switch(event->type()) { - case QEvent::PaletteChange: - updateInfoWebView(); - break; case QEvent::Close: // save settings Settings::mainWindowGeometry() = saveGeometry(); @@ -239,47 +179,6 @@ bool MainWindow::event(QEvent *event) return QMainWindow::event(event); } -bool MainWindow::eventFilter(QObject *obj, QEvent *event) -{ - if(obj == m_ui->rightWidget) { - switch(event->type()) { - case QEvent::DragEnter: - case QEvent::Drop: - if(QDropEvent *dropEvent = static_cast(event)) { - for(const auto &url : dropEvent->mimeData()->urls()) { - if(url.scheme() == QLatin1String("file")) { - event->accept(); - if(event->type() == QEvent::Drop) { -#ifdef Q_OS_WIN32 - // remove leading slash - QString path = url.path(); - int index = 0; - for(const auto &c : path) { - if(c == QChar('/')) { - ++index; - } else { - break; - } - } - if(index) { - path = path.mid(index); - } - startParsing(path, true); -#else - startParsing(url.path(), true); -#endif - } - return true; - } - } - } - default: - ; - } - } - return QMainWindow::eventFilter(obj, event); -} - /*! * \brief This private slot is called when the entered text of m_ui->pathLineEdit which represents the current directory changes. * @@ -323,394 +222,32 @@ void MainWindow::fileSelected() } } -/*! - * \brief Sets the file to be opened next and invokes saving the current file. - */ -void MainWindow::saveAndShowNextFile() -{ - if(!m_fileOperationMutex.try_lock()) { - m_ui->statusBar->showMessage(tr("Unable to save the selected file and load the next file after saving because the current process hasn't finished yet.")); - return; - } - // clear previous value - m_nextFilePath.clear(); - // fetch next file from the file model - QModelIndexList selectedIndexes = m_ui->filesTreeView->selectionModel()->selectedRows(); - if(selectedIndexes.count() == 1) { - const QModelIndex &selected = selectedIndexes.at(0); - QModelIndex next = selected.sibling(selected.row() + 1, selected.column()); - if(next.isValid()) { - QModelIndex nextInSource = m_fileFilterModel->mapToSource(next); - // set it to m_nextFilePath - m_nextFilePath = m_fileModel->filePath(nextInSource); - m_fileOperationMutex.unlock(); - applyEntriesAndSaveChangings(); - // next file set to m_nextFilePath will be opened when changings have been applied in showSavingResult(...) - return; - } - } - m_fileOperationMutex.unlock(); - static const QString msg = tr("Unable to find the next file. The chanings of the currently opened file will be saved regardless."); - QMessageBox::warning(this, QApplication::applicationName(), msg); - m_ui->statusBar->showMessage(msg); - applyEntriesAndSaveChangings(); -} - -/*! - * \brief Updates the line edits for the document title(s). - */ -void MainWindow::updateDocumentTitleEdits() -{ - // get container, segment count and present titles - AbstractContainer *container = m_fileInfo.container(); - int segmentCount = container ? static_cast(container->segmentCount()) : 0; - const vector &titles = container ? container->titles() : vector(); - - // get layout - QLayout *docTitleLayout = m_ui->docTitleWidget->layout(); - int lineEditCount = docTitleLayout->count() - 1; - - // update existing line edits, remove unneeded line edits - int i = 0; - for(; i < lineEditCount; ++i) { - if(i < segmentCount) { - // update existing line edit - static_cast(docTitleLayout->itemAt(i + 1)->widget()) - ->setText(static_cast(i) < titles.size() ? QString::fromUtf8(titles[i].data()) : QString()); - } else { - // remove unneeded line edit - docTitleLayout->removeItem(docTitleLayout->itemAt(i + 1)); - } - } - - // add missing line edits - while(i < segmentCount) { - auto *lineEdit = new Widgets::ClearLineEdit; - if(static_cast(i) < titles.size()) { - lineEdit->setText(QString::fromUtf8(titles[i].data())); - } - lineEdit->setPlaceholderText(tr("Segment %1").arg(++i)); - docTitleLayout->addWidget(lineEdit); - } -} - -/*! - * \brief Update the tag edits and the tag selection to show the specified \a tags. - * \param tags Specifies the tags to be shown. - * \param updateUi Specifies whether the UI of the tag edits should be updated. - * \remarks The tag selection combo box should be updated after calling this method to - * ensure the updated edits can be selected properly. - */ -void MainWindow::updateTagEditsAndAttachmentEdits(bool updateUi, PreviousValueHandling previousValueHandling) -{ - // determine to previous value handling according to the settings if auto is specified - switch(previousValueHandling) { - case PreviousValueHandling::Auto: - switch(Settings::adoptFields()) { - case Settings::AdoptFields::WithinDirectory: - if(m_lastDir != m_currentDir) { - previousValueHandling = PreviousValueHandling::Clear; - } - break; - case Settings::AdoptFields::Never: - previousValueHandling = PreviousValueHandling::Clear; - break; - default: - ; - } - break; - default: - ; - } - // define helper function to fetch next edit - TagEdit *edit; // holds current edit - int widgetIndex = 0; // holds index of current edit in the stacked widget - auto fetchNextEdit = [this, &edit, &widgetIndex, &previousValueHandling] { - // reuse existing edit (assigned in if-condition!) or ... - if(!((widgetIndex < m_ui->stackedWidget->count()) - && (edit = qobject_cast(m_ui->stackedWidget->widget(widgetIndex))))) { - // ... create and add a new edit - edit = new TagEdit; - connect(m_ui->clearEntriesPushButton, &QPushButton::clicked, edit, &TagEdit::clear); - connect(m_ui->restoreEntriesPushButton, &QPushButton::clicked, edit, &TagEdit::restore); - connect(edit, &TagEdit::returnPressed, this, &MainWindow::handleReturnPressed); - m_ui->stackedWidget->insertWidget(widgetIndex, edit); - } - // apply settings - edit->setPreviousValueHandling(previousValueHandling); - }; - // add/update TagEdit widgets - if(m_tags.size()) { - // create a lists of the targets and tags - QList targets; - QList > tagsByTarget; - for(Tag *tag : m_tags) { - const TagTarget &target = tag->target(); - int index = targets.indexOf(target); - if(index < 0) { - targets << target; - tagsByTarget << (QList() << tag); - } else { - tagsByTarget[index] << tag; - } - } - // create a singe editor per target or seperate editors for each tag depending on the settings - switch(Settings::multipleTagHandling()) { - case Settings::MultipleTagHandling::SingleEditorPerTarget: - // iterate through all targets in both cases - for(int targetIndex = 0, targetCount = targets.size(); targetIndex < targetCount; ++targetIndex) { - fetchNextEdit(); - edit->setTags(tagsByTarget.at(targetIndex), updateUi); // set all tags with the same target to a single edit - ++widgetIndex; - } - break; - case Settings::MultipleTagHandling::SeparateEditors: - // iterate through all targets in both cases - for(int targetIndex = 0, targetCount = targets.size(); targetIndex < targetCount; ++targetIndex) { - for(Tag *tag : tagsByTarget.at(targetIndex)) { - fetchNextEdit(); - edit->setTag(tag, updateUi); // use a separate edit for each tag - ++widgetIndex; - } - } - break; - } - } else { - // there are no tags -> leave one edit existend but ensure no tags are assigned - fetchNextEdit(); - edit->setTag(nullptr, false); - ++widgetIndex; - } - // add/update AttachmentsEdit widget - if(m_fileInfo.areAttachmentsSupported()) { - AttachmentsEdit *edit; - // reuse existing edit (assigned in if-condition!) or ... - if((widgetIndex < m_ui->stackedWidget->count()) - && (edit = qobject_cast(m_ui->stackedWidget->widget(widgetIndex)))) { - edit->setFileInfo(&m_fileInfo, true); - } else { - // ... create and add a new edit - edit = new AttachmentsEdit(&m_fileInfo, this); - connect(m_ui->clearEntriesPushButton, &QPushButton::clicked, edit, &AttachmentsEdit::clear); - connect(m_ui->restoreEntriesPushButton, &QPushButton::clicked, edit, &AttachmentsEdit::restore); - //connect(edit, &AttachmentsEdit::returnPressed, this, &MainWindow::handleReturnPressed); - m_ui->stackedWidget->insertWidget(widgetIndex, edit); - } - ++widgetIndex; - } - // remove surplus edits - while(widgetIndex < m_ui->stackedWidget->count()) { - QWidget *toRemove = m_ui->stackedWidget->widget(widgetIndex); - m_ui->stackedWidget->removeWidget(toRemove); - delete toRemove; - } -} - -/*! - * \brief Updates the items and the visibility of the tag selection combo box. - * \remarks Tag edits should have been updated before since this method uses the - * tag edits to generate the labels. - */ -void MainWindow::updateTagSelectionComboBox() -{ - if(m_fileInfo.isOpen()) { - // memorize the index of the previously selected edit - int previouslySelectedEditIndex = m_ui->tagSelectionComboBox->currentIndex(); - // clear old entries and create new labels - m_ui->tagSelectionComboBox->clear(); - bool haveTargetInfo = false; - for(int index = 0, count = m_ui->stackedWidget->count(); index < count; ++index) { - QString label; - if(TagEdit *edit = qobject_cast(m_ui->stackedWidget->widget(index))) { - if(!edit->tags().isEmpty()) { - label = edit->generateLabel(); - haveTargetInfo |= !edit->tags().at(0)->target().isEmpty(); - } - } else if(qobject_cast(m_ui->stackedWidget->widget(index))) { - static const QString attachmentsLabel = tr("Attachments"); - label = attachmentsLabel; - } - m_ui->tagSelectionComboBox->addItem(label); - } - // set visibility - m_ui->tagSelectionComboBox->setHidden(Settings::hideTagSelectionComboBox() && m_ui->tagSelectionComboBox->count() <= 1 && !haveTargetInfo); - // restore selected index - if(previouslySelectedEditIndex >= 0 && previouslySelectedEditIndex < m_ui->tagSelectionComboBox->count()) { - m_ui->tagSelectionComboBox->setCurrentIndex(previouslySelectedEditIndex); - } - } -} - /*! * \brief Updates the status of the relevant widgets (enabled/disabled, visible/hidden) according to the * current "file status" (opened/closed, has tags/no tags). */ -void MainWindow::updateUiStatus() +void MainWindow::handleFileStatusChange(bool opened, bool hasTag) { - bool opened = m_fileInfo.isOpen(); - bool hasTag = opened && m_tags.size(); - // notification widgets - m_ui->parsingNotificationWidget->setVisible(opened); - m_ui->makingNotificationWidget->setVisible(opened && (m_makingResultsAvailable)); - // document title widget - m_ui->docTitleWidget->setVisible(opened && m_fileInfo.container() && m_fileInfo.container()->supportsTitle()); - // buttons and actions to save, delete, close - m_ui->saveButton->setEnabled(opened); - m_ui->deleteTagsButton->setEnabled(hasTag); + // actions to save, delete, close m_ui->actionSave->setEnabled(opened); m_ui->actionDelete_all_tags->setEnabled(hasTag); m_ui->actionSave_file_information->setEnabled(opened); m_ui->actionClose->setEnabled(opened); m_ui->actionReload->setEnabled(opened); m_ui->actionExternalPlayer->setEnabled(opened); - m_ui->buttonsWidget->setEnabled(opened); - // clear and restore buttons - m_ui->clearEntriesPushButton->setEnabled(hasTag); - m_ui->restoreEntriesPushButton->setEnabled(hasTag); - m_ui->clearEntriesPushButton->setEnabled(hasTag); - m_ui->restoreEntriesPushButton->setEnabled(hasTag); - // tag management button - m_ui->tagOptionsPushButton->setEnabled(opened); - // stacked widget containering edits and selection - m_ui->tagSelectionComboBox->setEnabled(hasTag); - // visibility of m_ui->tagSelectionComboBox is set within updateTagEdits - m_ui->stackedWidget->setEnabled(hasTag); - // webview - m_infoWebView->setEnabled(opened); - // next button - bool nextFileAvailable = false; - if(opened) { - const QModelIndexList selectedIndexes = m_ui->filesTreeView->selectionModel()->selectedRows(); - if(selectedIndexes.size() == 1) { - const QModelIndex &selected = selectedIndexes.at(0); - nextFileAvailable = selected.sibling(selected.row() + 1, selected.column()).isValid(); - } - } - m_ui->nextButton->setEnabled(nextFileAvailable); // window title - setWindowTitle(Dialogs::generateWindowTitle(m_fileInfo.isOpen() ? DocumentStatus::Saved : DocumentStatus::NoDocument, m_currentPath)); + setWindowTitle(Dialogs::generateWindowTitle(opened ? DocumentStatus::Saved : DocumentStatus::NoDocument, m_ui->tagEditorWidget->currentPath())); } -/*! - * \brief Updates the "tag management menu". - */ -void MainWindow::updateTagManagementMenu() +void MainWindow::handleFileSaved(const QString ¤tPath) { - m_addTagMenu->clear(); - m_removeTagMenu->clear(); - m_changeTargetMenu->clear(); - if(m_fileInfo.isOpen()) { - // add "Add tag" actions - if(m_fileInfo.container()) { - // there is a container object which might be able to create tags - QString label; - if(m_fileInfo.containerFormat() == ContainerFormat::Matroska) { - // tag format supports targets (Matroska tags are currently the only tag format supporting targets.) - label = tr("Matroska tag"); - connect(m_addTagMenu->addAction(label), &QAction::triggered, std::bind(&MainWindow::addTag, this, [this] (MediaFileInfo &file) -> Media::Tag * { - if(file.container()) { - EnterTargetDialog targetDlg(this); - targetDlg.setTarget(TagTarget(50), &this->m_fileInfo); - if(targetDlg.exec() == QDialog::Accepted) { - return file.container()->createTag(targetDlg.target()); - } - } - return nullptr; - })); - } else { - // tag format does not support targets - if(!m_fileInfo.container()->tagCount()) { - switch(m_fileInfo.containerFormat()) { - case ContainerFormat::Mp4: - label = tr("MP4/iTunes tag"); - break; - case ContainerFormat::Ogg: - label = tr("Vorbis comment"); - break; - default: - label = tr("Tag"); - } - connect(m_addTagMenu->addAction(label), &QAction::triggered, std::bind(&MainWindow::addTag, this, [] (MediaFileInfo &file) { - return file.container() ? file.container()->createTag() : nullptr; - })); - } - } - } else { - // there is no container object; creation of ID3 tags is possible - if(!m_fileInfo.hasId3v1Tag()) { - connect(m_addTagMenu->addAction(tr("ID3v1 tag")), &QAction::triggered, std::bind(&MainWindow::addTag, this, [] (MediaFileInfo &file) { - return file.createId3v1Tag(); - })); - } - if(!m_fileInfo.hasId3v2Tag()) { - connect(m_addTagMenu->addAction(tr("ID3v2 tag")), &QAction::triggered, std::bind(&MainWindow::addTag, this, [] (MediaFileInfo &file) { - return file.createId3v2Tag(); - })); - } - } - // add "Remove tag" and "Change target" actions - for(Tag *tag : m_tags) { - connect(m_removeTagMenu->addAction(QString::fromLocal8Bit(tag->toString().c_str())), &QAction::triggered, std::bind(&MainWindow::removeTag, this, tag)); - if(tag->supportsTarget()) { - connect(m_changeTargetMenu->addAction(QString::fromLocal8Bit(tag->toString().c_str())), &QAction::triggered, std::bind(&MainWindow::changeTarget, this, tag)); - } - } + // ensure the current file is still selected + const QModelIndex index = m_fileFilterModel->mapFromSource(m_fileModel->index(currentPath)); + if(index.isValid()) { + m_ui->filesTreeView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); } - m_addTagMenu->setEnabled(!m_addTagMenu->actions().empty()); - m_removeTagMenu->setEnabled(!m_removeTagMenu->actions().empty()); - m_changeTargetMenu->setEnabled(!m_changeTargetMenu->actions().empty()); -} - -/*! - * \brief Inserts the title from the filename keeping a possibly available title from the tags. - * \remarks Does nothing if there are no tags assigned and if this feature is not enabled in the settings. - */ -void MainWindow::insertTitleFromFilename() -{ - if(!m_tags.empty() && Settings::insertTitleFromFilename()) { - QString title; - int trackNum; - parseFileName(QString::fromLocal8Bit(m_fileInfo.fileName().c_str()), title, trackNum); - TagValue titleValue = qstringToTagValue(title, TagTextEncoding::Utf16LittleEndian); - foreachTagEdit([&titleValue] (TagEdit *edit) { - edit->setValue(KnownField::Title, titleValue, PreviousValueHandling::Keep); - }); - } -} - -/*! - * \brief Updates the info web view to show information about the - * currently opened file. - */ -void MainWindow::updateInfoWebView() -{ - if(m_fileInfo.isOpen()) { - m_fileInfoHtml = HtmlInfo::generateInfo(m_fileInfo, m_originalNotifications); - m_infoWebView->setContent(m_fileInfoHtml, QStringLiteral("application/xhtml+xml")); - } else { - m_infoWebView->setUrl(QStringLiteral("about:blank")); - } -} - -/*! - * \brief Shows the context menu for the info web view. - */ -void MainWindow::showInfoWebViewContextMenu(const QPoint &) -{ - QAction copyAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy"), nullptr); - copyAction.setDisabled(m_infoWebView->selectedText().isEmpty()); - connect(©Action, &QAction::triggered, this, &MainWindow::copyInfoWebViewSelection); - QMenu menu; - menu.addAction(©Action); - menu.exec(QCursor::pos()); -} - -/*! - * \brief Copies the current selection of the info web view. - */ -void MainWindow::copyInfoWebViewSelection() -{ - QApplication::clipboard()->setText(m_infoWebView->selectedText()); + // ensure this is the active window + activateWindow(); } /*! @@ -718,467 +255,14 @@ void MainWindow::copyInfoWebViewSelection() */ void MainWindow::spawnExternalPlayer() { - if(!m_currentPath.isEmpty()) { - DesktopUtils::openLocalFileOrDir(m_currentPath); + const QString ¤tPath = m_ui->tagEditorWidget->currentPath(); + if(!currentPath.isEmpty()) { + DesktopUtils::openLocalFileOrDir(currentPath); } else { m_ui->statusBar->showMessage(tr("No file opened.")); } } -/*! - * \brief Calls the specified \a function for each of the currently present tag edits. - */ -void MainWindow::foreachTagEdit(const std::function &function) -{ - for(int i = 0, count = m_ui->stackedWidget->count(); i < count; ++i) { - if(auto *edit = qobject_cast(m_ui->stackedWidget->widget(i))) { - function(edit); - } - } -} - -/*! - * \brief Opens and parses a file using another thread. - * - * Shows its tags and general information using the showFile() method. - * - * \param path Specifies the \a path of the file. - * \param forceRefresh Specifies whether the file should be reparsed if it is already opened. - */ -bool MainWindow::startParsing(const QString &path, bool forceRefresh) -{ - // check if file is current file - bool sameFile = m_currentPath == path; - if(!forceRefresh && sameFile) { - return true; - } - if(!m_fileOperationMutex.try_lock()) { - m_ui->statusBar->showMessage(tr("Unable to load the selected file \"%1\" because the current process hasn't finished yet.").arg(path)); - return false; - } - lock_guard guard(m_fileOperationMutex, adopt_lock); - // clear previous results and status - m_tags.clear(); - m_fileInfo.clearParsingResults(); - m_fileInfo.invalidateStatus(); - m_fileInfo.invalidateNotifications(); - if(!sameFile) { - // close last file if possibly open - m_fileInfo.close(); - // set path of file info - m_currentPath = path; - m_fileInfo.setPath(path.toLocal8Bit().data()); - // update directory - m_lastDir = m_currentDir; - m_currentDir = QString::fromLocal8Bit(m_fileInfo.containingDirectory().c_str()); - } - // update availability of making results - m_makingResultsAvailable &= sameFile; - if(!m_makingResultsAvailable) { - m_originalNotifications.clear(); - } - // show filename - m_ui->fileNameLabel->setText(QString::fromLocal8Bit(m_fileInfo.fileName().c_str())); - // define function to parse the file - auto startThread = [this, sameFile] { - m_fileOperationMutex.lock(); - char result; - try { - if(sameFile) { - m_fileInfo.reopen(); - } - m_fileInfo.setForceFullParse(Settings::forceFullParse()); - m_fileInfo.parseEverything(); - result = ParsingSuccessful; - } catch(Failure &) { - // the file has been opened; parsing notifications will be shown in the info box - result = FatalParsingError; - } catch(ios_base::failure &) { - // the file could not be opened because an IO error occured - m_fileInfo.close(); // ensure file is closed - result = IoError; - } - m_fileInfo.unregisterAllCallbacks(); - QMetaObject::invokeMethod(this, "showFile", Qt::QueuedConnection, Q_ARG(char, result)); - // showFile() will unlock the mutex! - }; - m_fileInfo.unregisterAllCallbacks(); - //m_fileInfo.registerCallback(showProgress); can't show progress yet - // use another thread to perform the operation - std::thread thr(startThread); - thr.detach(); - // inform user - static const QString statusMsg(tr("The file is beeing parsed ...")); - m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Progress); - m_ui->parsingNotificationWidget->setText(statusMsg); - m_ui->parsingNotificationWidget->setVisible(true); // ensure widget is visible! - m_ui->statusBar->showMessage(statusMsg); - return true; -} - -/*! - * \brief Reparses the current file. - */ -bool MainWindow::reparseFile() -{ - if(!m_fileOperationMutex.try_lock()) { - m_ui->statusBar->showMessage(tr("Unable to reload the file because the current process hasn't finished yet.")); - return false; - } - { - lock_guard guard(m_fileOperationMutex, adopt_lock); - if(!m_fileInfo.isOpen() || m_currentPath.isEmpty()) { - QMessageBox::warning(this, windowTitle(), tr("Currently is not file opened.")); - return false; - } - } - return startParsing(m_currentPath, true); -} - -/*! - * \brief Shows the current file info (technical info, tags, ...). - * This private slot is invoked from the thread which performed the - * parsing operation using Qt::QueuedConnection. - * \param result Specifies whether the file could be load sucessfully. - * \remarks Expects m_fileOperationMutex to be locked! - */ -void MainWindow::showFile(char result) -{ - lock_guard guard(m_fileOperationMutex, adopt_lock); - if(result == IoError) { - // update status - updateUiStatus(); - static const QString statusMsg(tr("The file could not be opened because an IO error occurred.")); - QMessageBox::critical(this, windowTitle(), statusMsg); - m_ui->statusBar->showMessage(statusMsg); - } else { - // update webview - updateInfoWebView(); - // show parsing status/result using parsing notification widget - auto worstNotificationType = m_fileInfo.worstNotificationTypeIncludingRelatedObjects(); - if(worstNotificationType >= Media::NotificationType::Critical) { - // we catched no exception, but there are critical notifications - // -> treat critical notifications as fatal parsing errors - result = LoadingResult::FatalParsingError; - } - switch(result) { - case ParsingSuccessful: - m_ui->parsingNotificationWidget->setNotificationType(NotificationType::TaskComplete); - m_ui->parsingNotificationWidget->setText(tr("File could be parsed correctly.")); - break; - default: - m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Critical); - m_ui->parsingNotificationWidget->setText(tr("File couldn't be parsed correctly.")); - } - switch(worstNotificationType) { - case Media::NotificationType::Critical: - m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Critical); - m_ui->parsingNotificationWidget->appendLine(tr("There are critical parsing notifications.")); - break; - case Media::NotificationType::Warning: - m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Warning); - m_ui->parsingNotificationWidget->appendLine(tr("There are warnings.")); - break; - default: - ; - } - // load existing tags - m_tags.clear(); - m_fileInfo.tags(m_tags); - // show notification if there is currently no existing tag(s) could be found - if(!m_tags.size()) { - m_ui->parsingNotificationWidget->appendLine(tr("There is no (supported) tag assigned.")); - if(!m_fileInfo.areTagsSupported()) { - m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Warning); - m_ui->parsingNotificationWidget->appendLine(tr("File format is not supported (an ID3 tag can be added anyways).")); - } - } - // create appropriate tags according to file type and user preferences when automatic tag management is enabled - if(Settings::autoTagManagement()) { - if(!m_fileInfo.createAppropriateTags(false, Settings::id3v1usage(), Settings::id3v2usage(), Settings::mergeMultipleSuccessiveId3v2Tags(), - Settings::keepVersionOfExistingId3v2Tag(), Settings::id3v2versionToBeUsed())) { - if(confirmCreationOfId3TagForUnsupportedFile()) { - m_fileInfo.createAppropriateTags(true, Settings::id3v1usage(), Settings::id3v2usage(), Settings::mergeMultipleSuccessiveId3v2Tags(), - Settings::keepVersionOfExistingId3v2Tag(), Settings::id3v2versionToBeUsed()); - } - } - } - // reload tags - m_tags.clear(); - m_fileInfo.tags(m_tags); - // update file watcher - m_fileWatcher->addPath(m_currentPath); - m_fileChangedOnDisk = false; - // update related widgets - updateDocumentTitleEdits(); - updateTagEditsAndAttachmentEdits(); - updateTagSelectionComboBox(); - updateTagManagementMenu(); - insertTitleFromFilename(); - // update status - m_ui->statusBar->showMessage(tr("The file %1 has been opened.").arg(QString::fromLocal8Bit(m_fileInfo.fileName().c_str()))); - updateUiStatus(); - } -} - -/*! - * \brief Applies all entries and starts saving. - * \sa startSaving() - */ -bool MainWindow::applyEntriesAndSaveChangings() -{ - { - if(!m_fileOperationMutex.try_lock()) { - static const QString statusMsg(tr("Unable to apply the entered tags to the file because the current process hasn't finished yet.")); - m_ui->makingNotificationWidget->setText(statusMsg); - m_ui->statusBar->showMessage(statusMsg); - return false; - } - lock_guard guard(m_fileOperationMutex, adopt_lock); - m_ui->makingNotificationWidget->setNotificationType(NotificationType::Information); - m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::Saving); - m_ui->makingNotificationWidget->setHidden(false); - m_makingResultsAvailable = true; - if(m_fileInfo.isOpen()) { - // apply titles - if(AbstractContainer *container = m_fileInfo.container()) { - if(container->supportsTitle()) { - QLayout *docTitleLayout = m_ui->docTitleWidget->layout(); - for(int i = 0, count = min(docTitleLayout->count() - 1, container->segmentCount()); i < count; ++i) { - container->setTitle(static_cast(docTitleLayout->itemAt(i + 1)->widget())->text().toUtf8().data(), i); - } - } - } - // apply all tags - foreachTagEdit([] (TagEdit *edit) {edit->apply();}); - static const QString statusMsg(tr("Saving tags ...")); - m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::None); - m_ui->makingNotificationWidget->setNotificationType(NotificationType::Progress); - m_ui->makingNotificationWidget->setText(statusMsg); - m_ui->statusBar->showMessage(statusMsg); - } else { - QString statusMsg = tr("No file has been opened."); - m_ui->makingNotificationWidget->setText(statusMsg); - QMessageBox::warning(this, QApplication::applicationName(), statusMsg); - return false; - } - } - return startSaving(); -} - -/*! - * \brief Deletes all tags and starts saving. - * \sa startSaving() - */ -bool MainWindow::deleteAllTagsAndSave() -{ - { - if(!m_fileOperationMutex.try_lock()) { - static const QString statusMsg(tr("Unable to delete all tags from the file because the current process hasn't been finished yet.")); - m_ui->makingNotificationWidget->setText(statusMsg); - m_ui->statusBar->showMessage(statusMsg); - return false; - } - lock_guard guard(m_fileOperationMutex, adopt_lock); - if(Settings::askBeforeDeleting()) { - QMessageBox msgBox(this); - msgBox.setText(tr("Do you really want to delete all tags from the file?")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - msgBox.setDefaultButton(QMessageBox::No); -#if QT_VERSION >= 0x050200 - auto *checkBox = new QCheckBox(&msgBox); - checkBox->setText(tr("don't show this message again")); - msgBox.setCheckBox(checkBox); -#endif - int res = msgBox.exec(); -#if QT_VERSION >= 0x050200 - if(checkBox->isChecked()) { - Settings::askBeforeDeleting() = false; - } -#endif - if(res != QMessageBox::Yes) { - return false; - } - } - m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::Saving); - m_ui->makingNotificationWidget->setNotificationType(NotificationType::Information); - m_ui->makingNotificationWidget->setHidden(false); - m_makingResultsAvailable = true; - if(m_fileInfo.isOpen()) { - if(m_fileInfo.hasAnyTag()) { - foreachTagEdit([] (TagEdit *edit) {edit->clear();}); - m_fileInfo.removeAllTags(); - m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::None); - m_ui->makingNotificationWidget->setNotificationType(NotificationType::Progress); - static const QString statusMsg(tr("Deleting all tags ...")); - m_ui->makingNotificationWidget->setText(statusMsg); - m_ui->statusBar->showMessage(statusMsg); - } else { - m_ui->makingNotificationWidget->setText(tr("The selected file stores no tag (at least no supported), so there is nothing to delete.")); - return false; - } - } else { - m_ui->makingNotificationWidget->setText(tr("No file has been opened, so no tags can be deleted.")); - return false; - } - } - return startSaving(); -} - -/*! - * \brief Starts saving. This method is called by applyEntriesAndSaveChangings() and deleteAllTagsAndSave(). - * The actual process is performed in another thread. - * \remarks Will start a new thread to perform the operation. Then showSavingResult() is called - * using Qt::QueuedConnection in the main thread. m_fileOperationMutex will remain locked when saving is - * finished and will be unlocked in showSavingResult(). This way any method which might be called after - * the operation thread ends and before the invokation of showSavingResult() will see a locked mutex and - * hence not mutate the current file. - */ -bool MainWindow::startSaving() -{ - if(!m_fileOperationMutex.try_lock()) { - static const QString errorMsg(tr("Unable to start saving process because there an other process hasn't finished yet.")); - m_ui->statusBar->showMessage(errorMsg); - QMessageBox::warning(this, QApplication::applicationName(), errorMsg); - return false; - } - lock_guard guard(m_fileOperationMutex, adopt_lock); - // tags might get invalidated - m_tags.clear(); - foreachTagEdit([] (TagEdit *edit) { edit->setTag(nullptr, false); }); - // show abort button - m_ui->abortButton->setHidden(false); - m_ui->abortButton->setEnabled(true); - m_abortClicked = false; - // remove current path from file watcher - m_fileWatcher->removePath(m_currentPath); - // use current configuration - m_fileInfo.setForceRewrite(Settings::forceRewrite()); - m_fileInfo.setTagPosition(Settings::preferredTagPosition()); - m_fileInfo.setForceTagPosition(Settings::forceTagPosition()); - m_fileInfo.setIndexPosition(Settings::preferredIndexPosition()); - m_fileInfo.setForceIndexPosition(Settings::forceIndexPosition()); - m_fileInfo.setMinPadding(Settings::minPadding()); - m_fileInfo.setMaxPadding(Settings::maxPadding()); - m_fileInfo.setPreferredPadding(Settings::preferredPadding()); - // define functions to show the saving progress and to actually applying the changes - auto showProgress = [this] (StatusProvider &sender) -> void { - QMetaObject::invokeMethod(m_ui->makingNotificationWidget, "setPercentage", Qt::QueuedConnection, Q_ARG(int, static_cast(sender.currentPercentage() * 100.0))); - if(m_abortClicked) { - QMetaObject::invokeMethod(m_ui->makingNotificationWidget, "setText", Qt::QueuedConnection, Q_ARG(QString, tr("Cancelling ..."))); - m_fileInfo.tryToAbort(); - } else { - QMetaObject::invokeMethod(m_ui->makingNotificationWidget, "setText", Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(sender.currentStatus()))); - } - }; - auto startThread = [this] { - m_fileOperationMutex.lock(); - bool processingError = false, ioError = false; - try { - m_fileInfo.applyChanges(); - } catch(const Failure &) { - processingError = true; - } catch(const ios_base::failure &) { - ioError = true; - } - m_fileInfo.unregisterAllCallbacks(); - QMetaObject::invokeMethod(this, "showSavingResult", Qt::QueuedConnection, Q_ARG(bool, processingError), Q_ARG(bool, ioError)); - // showSavingResult() will unlock the mutex! - }; - m_fileInfo.unregisterAllCallbacks(); - m_fileInfo.registerCallback(showProgress); - // use another thread to perform the operation - std::thread thr(startThread); - thr.detach(); - return true; -} - -/*! - * \brief Shows the saving results. - * This private slot is invoked from the thread which performed the - * saving operation using Qt::QueuedConnection. - * \param sucess Specifies whether the file could be saved sucessfully. - * \remarks Expects m_fileOperationMutex to be locked! - */ -void MainWindow::showSavingResult(bool processingError, bool ioError) -{ - m_ui->abortButton->setHidden(true); - m_ui->makingNotificationWidget->setNotificationType(NotificationType::TaskComplete); - m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::Saving); - m_ui->makingNotificationWidget->setPercentage(-1); - m_ui->makingNotificationWidget->setHidden(false); - m_makingResultsAvailable = true; - m_originalNotifications = m_fileInfo.gatherRelatedNotifications(); - if(!processingError && !ioError) { - // display status messages - QString statusMsg; - size_t critical = 0; - size_t warnings = 0; - for(const Notification ¬ification : m_originalNotifications) { - switch(notification.type()) { - case Media::NotificationType::Critical: - ++critical; - break; - case Media::NotificationType::Warning: - ++warnings; - break; - default: - ; - } - } - if(warnings || critical) { - if(critical) { - statusMsg = tr("The tags have been saved, but there is/are %1 warning(s) ", 0, warnings).arg(warnings); - statusMsg.append(tr("and %1 error(s).", 0, critical).arg(critical)); - } else { - statusMsg = tr("The tags have been saved, but there is/are %1 warning(s).", 0, warnings).arg(warnings); - } - m_ui->makingNotificationWidget->setNotificationType(critical > 0 ? NotificationType::Critical : NotificationType::Warning); - - } else { - statusMsg = tr("The tags have been saved."); - } - m_ui->makingNotificationWidget->setText(statusMsg); - m_ui->statusBar->showMessage(statusMsg); - m_fileOperationMutex.unlock(); - // ensure the current file is still selected - const QModelIndex index = m_fileFilterModel->mapFromSource(m_fileModel->index(QString::fromStdString(m_fileInfo.path()))); - if(index.isValid()) { - m_ui->filesTreeView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); - } - activateWindow(); // ensure this is the active window - // update info model/show next file - bool showNextFile = false; - if(critical < 1 && !m_nextFilePath.isEmpty()) { // do not show next file if there are critical notifications - const QModelIndex next = m_fileModel->index(m_nextFilePath); - m_nextFilePath.clear(); - if(next.isValid()) { - QItemSelectionModel *selectionModel = m_ui->filesTreeView->selectionModel(); - selectionModel->setCurrentIndex(m_fileFilterModel->mapFromSource(next), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); - showNextFile = true; - } else { - static const QString errormsg(tr("Unable to show the next file because it can't be found anymore.")); - QMessageBox::warning(this, QApplication::applicationName(), errormsg); - m_ui->statusBar->showMessage(errormsg); - } - } - if(!showNextFile) { - startParsing(m_currentPath, true); - } - } else { - static const QString processingErrorMsg(tr("The tags couldn't be saved. See the info box for detail.")); - static const QString ioErrorMsg(tr("The tags couldn't be saved because an IO error occured.")); - const auto &errorMsg = ioError ? ioErrorMsg : processingErrorMsg; - QMessageBox::warning(this, QApplication::applicationName(), errorMsg); - m_ui->statusBar->showMessage(errorMsg); - m_ui->makingNotificationWidget->setText(errorMsg); - m_ui->makingNotificationWidget->setNotificationType(NotificationType::Critical); - m_fileOperationMutex.unlock(); - startParsing(m_currentPath, true); - } -} - /*! * \brief Shows the about dialog. */ @@ -1224,64 +308,78 @@ void MainWindow::showRenameFilesDlg() } } -/*! - * \brief Asks the user whether an ID3 tag should be add to a not supported container format and returns the result. - */ -bool MainWindow::confirmCreationOfId3TagForUnsupportedFile() -{ - QMessageBox msgBox(this); - msgBox.setWindowTitle(tr("Automatic tag management")); - msgBox.setText(tr("The container format of the selected file is not supported. The file can be treated as MP3 file (an ID3 tag according to the settings will be created). This might break the file. Do you want to continue?")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.addButton(tr("Treat file as MP3 file"), QMessageBox::AcceptRole); - msgBox.addButton(tr("Abort"), QMessageBox::RejectRole); - return msgBox.exec() == 0; -} - /*! * \brief Selects the next file. + * \remarks Does nothing if there is currently no file selected. */ void MainWindow::selectNextFile() { QItemSelectionModel *selectionModel = m_ui->filesTreeView->selectionModel(); QModelIndexList selectedIndexes = selectionModel->selectedIndexes(); - if(selectedIndexes.count() >= 1) { - const QModelIndex &selectedIndex = selectedIndexes.at(0); - QModelIndex next; - if(selectionModel->model()->hasChildren(selectedIndex)) { // returns true for directories - if(m_fileFilterModel->canFetchMore(selectedIndex)) { // files and subdirectories have to be fetched - // fetchMore will return immediatly because QFileSystemModel seems to use an - // extra thread to fetch files and directories. That's why I select the next file - // in this case when the rowsInserted signal is emitted. - auto conn = make_shared(); - *conn = connect(m_fileFilterModel, &QAbstractItemModel::rowsInserted, [this, selectedIndex, conn] (const QModelIndex &parent, int, int) { - disconnect(*conn); - if(parent == selectedIndex) { - QModelIndex next = m_fileFilterModel->index(0, 0, parent); - if(next.isValid()) { - m_ui->filesTreeView->selectionModel()->setCurrentIndex(next, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); - } + if(!selectedIndexes.isEmpty()) { + selectNextFile(selectionModel, selectedIndexes.at(0), false); + } +} + +/*! + * \brief Selects the file next to the file with the specified index. + * + * If \a notDeeper is false, this method will not try to go deeper into + * the file system tree. + */ +void MainWindow::selectNextFile(QItemSelectionModel *selectionModel, const QModelIndex ¤tIndex, bool notDeeper) +{ + QModelIndex next; + if(!notDeeper && selectionModel->model()->hasChildren(currentIndex)) { + // a directory is selected -> go deeper + if(m_fileFilterModel->canFetchMore(currentIndex)) { + // files and subdirectories have to be fetched + // -> QFileSystemModel seems to use an extra thread to fetch files and directories + // -> fetchMore will return immediatly because + // -> select next file when rowsInserted is emitted + auto conn = make_shared(); + *conn = connect(m_fileFilterModel, &QAbstractItemModel::rowsInserted, [this, selectionModel, currentIndex, conn] (const QModelIndex &parent, int, int) { + disconnect(*conn); + if(parent == currentIndex) { + QModelIndex next = m_fileFilterModel->index(0, 0, parent); + if(next.isValid()) { + m_ui->filesTreeView->selectionModel()->setCurrentIndex(next, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + } else { + selectNextFile(selectionModel, currentIndex, true); } - }); - m_fileModel->fetchMore(m_fileFilterModel->mapToSource(selectedIndex)); - return; - } else { // files and subdirectories have been fetched already - next = selectedIndex.child(0, selectedIndex.column()); - } - } - if(!next.isValid()) { - next = selectedIndex.sibling(selectedIndex.row() + 1, selectedIndex.column()); - } - if(!next.isValid()) { - const QModelIndex parent = selectedIndex.parent(); - if(parent.isValid()) { - next = parent.sibling(parent.row() + 1, parent.column()); - } - } - if(next.isValid()) { - selectionModel->setCurrentIndex(next, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + } + }); + m_fileModel->fetchMore(m_fileFilterModel->mapToSource(currentIndex)); + return; + } else { + // files and subdirectories have been fetched already + next = currentIndex.child(0, currentIndex.column()); } } + if(!next.isValid()) { + // not possible to go deeper -> choose next sibling + next = currentIndex.sibling(currentIndex.row() + 1, currentIndex.column()); + } + if(!next.isValid()) { + // not possible to choose next sibling -> go higher + const QModelIndex parent = currentIndex.parent(); + if(parent.isValid()) { + selectNextFile(selectionModel, parent, true); + return; + } + } + if(next.isValid()) { + selectionModel->setCurrentIndex(next, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + } else { + showNextFileNotFound(); + } +} + +void MainWindow::showNextFileNotFound() +{ + static const QString errormsg(tr("Unable to show the next file because it can't be found anymore.")); + QMessageBox::warning(this, QApplication::applicationName(), errormsg); + m_ui->statusBar->showMessage(errormsg); } /*! @@ -1295,55 +393,23 @@ void MainWindow::showOpenFileDlg() } } -/*! - * \brief This slot is connected to the fileChanged() signal of the file info watcher. - */ -void MainWindow::fileChangedOnDisk(const QString &path) -{ - if(!m_fileChangedOnDisk && m_fileInfo.isOpen() && path == m_currentPath) { - auto ¬ifyWidget = *m_ui->parsingNotificationWidget; - notifyWidget.appendLine(tr("The currently opened file changed on the disk.")); - notifyWidget.setNotificationType(notifyWidget.notificationType() == NotificationType::Critical ? NotificationType::Critical : NotificationType::Warning); - m_fileChangedOnDisk = true; - } -} - -/*! - * \brief Closes the currently opened file and disables all related widgets. - */ -void MainWindow::closeFile() -{ - if(!m_fileOperationMutex.try_lock()) { - m_ui->statusBar->showMessage("Unable to close the file because the current process hasn't been finished yet."); - return; - } - lock_guard guard(m_fileOperationMutex, adopt_lock); - // close file - m_fileInfo.close(); - // remove current path from file watcher - m_fileWatcher->removePath(m_currentPath); - // update ui - m_ui->statusBar->showMessage("The file has been closed."); - updateUiStatus(); -} - /*! * \brief Saves the file information generated to be displayed in the info web view in a file. */ void MainWindow::saveFileInformation() { - if(!m_fileOperationMutex.try_lock()) { - m_ui->statusBar->showMessage("Unable to save file information because the current process hasn't been finished yet."); + if(!fileOperationMutex().try_lock()) { + m_ui->statusBar->showMessage(tr("Unable to save file information because the current process hasn't been finished yet.")); return; } - lock_guard guard(m_fileOperationMutex, adopt_lock); - if(m_fileInfo.isOpen() && m_fileInfoHtml.size()) { + lock_guard guard(fileOperationMutex(), adopt_lock); + if(fileInfo().isOpen() && m_ui->tagEditorWidget->fileInfoHtml().size()) { const QString path = QFileDialog::getSaveFileName(this, windowTitle()); if(!path.isEmpty()) { QFile file(path); if(file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { QTextStream stream(&file); - stream << m_fileInfoHtml; + stream << m_ui->tagEditorWidget->fileInfoHtml(); file.close(); if(file.error() != QFileDevice::NoError) { QMessageBox::critical(this, QApplication::applicationName(), tr("Unable to write to file.\n%1").arg(file.errorString())); @@ -1357,29 +423,6 @@ void MainWindow::saveFileInformation() } } -/*! - * \brief This private slot is invoked the the return key has been pressed in a tag edit. - * - * The file will be saved and then the next opened if the user selected that option. - */ -void MainWindow::handleReturnPressed() -{ - if(Settings::saveAndShowNextOnEnter() && m_fileInfo.isOpen()) { - saveAndShowNextFile(); - } -} - -void MainWindow::handleKeepPreviousValuesActionTriggered(QAction *action) -{ - if(action == m_ui->actionKeep_previous_values_never) { - Settings::adoptFields() = Settings::AdoptFields::Never; - } else if(action == m_ui->actionKeep_previous_values_within_same_dir) { - Settings::adoptFields() = Settings::AdoptFields::WithinDirectory; - } else if(action == m_ui->actionKeep_previous_values_always) { - Settings::adoptFields() = Settings::AdoptFields::Always; - } -} - /*! * \brief Applies settings from Settings namespace. Only settings configurable through the SettingsDialog * will be applied and not settings like the main window's geometry and state. @@ -1397,142 +440,6 @@ void MainWindow::applySettingsFromDialog() if(m_fileModel->isReadOnly() != Settings::fileBrowserReadOnly()) { m_fileModel->setReadOnly(Settings::fileBrowserReadOnly()); } - switch(Settings::adoptFields()) { - case Settings::AdoptFields::Never: - m_ui->actionKeep_previous_values_never->setChecked(true); - break; - case Settings::AdoptFields::WithinDirectory: - m_ui->actionKeep_previous_values_within_same_dir->setChecked(true); - break; - case Settings::AdoptFields::Always: - m_ui->actionKeep_previous_values_always->setChecked(true); - break; - } - m_ui->actionManage_tags_automatically_when_loading_file->setChecked(Settings::autoTagManagement()); -} - -/*! - * \brief Adds a tag (using the specified \a createTag function) to the currently opened file. - * - * Shows an error message if no file is opened. Tag edits, tag management menu und UI status will be updated. - */ -void MainWindow::addTag(const function &createTag) -{ - if(!m_fileOperationMutex.try_lock()) { - m_ui->statusBar->showMessage("Unable to add a tag because the current process hasn't been finished yet."); - return; - } - lock_guard guard(m_fileOperationMutex, adopt_lock); - if(!m_fileInfo.isOpen()) { - m_ui->statusBar->showMessage("Unable to add a tag because no file is opened."); - return; - } - if(Tag *tag = createTag(m_fileInfo)) { - if(std::find(m_tags.cbegin(), m_tags.cend(), tag) == m_tags.cend()) { - m_tags.push_back(tag); - updateTagEditsAndAttachmentEdits(true, m_tags.size() > 1 ? PreviousValueHandling::Keep : PreviousValueHandling::Auto); - updateTagSelectionComboBox(); - updateTagManagementMenu(); - updateUiStatus(); - insertTitleFromFilename(); - } else { - QMessageBox::warning(this, windowTitle(), tr("A tag (with the selected target) already exists.")); - } - - } else { - QMessageBox::warning(this, windowTitle(), tr("The tag can not be created.")); - } -} - -/*! - * \brief Removes the specified \a tag from the currently opened file. - * - * Shows an error message if the removal is (currently) not possible. Tag edits, tag management menu und UI status will be updated. - */ -void MainWindow::removeTag(Tag *tag) -{ - if(tag) { - if(!m_fileOperationMutex.try_lock()) { - m_ui->statusBar->showMessage(tr("Unable to remove the tag because the current process hasn't been finished yet.")); - return; - } - lock_guard guard(m_fileOperationMutex, adopt_lock); - if(!m_fileInfo.isOpen()) { - m_ui->statusBar->showMessage(tr("Unable to remove the tag because no file is opened.")); - return; - } - if(m_fileInfo.isOpen()) { - m_fileInfo.removeTag(tag); - // remove tag from m_tags - m_tags.erase(remove(m_tags.begin(), m_tags.end(), tag), m_tags.end()); - // remove tag from all TagEdit widgets - vector toRemove; - for(int index = 0, count = m_ui->stackedWidget->count(); index < count; ++index) { - TagEdit *edit = qobject_cast(m_ui->stackedWidget->widget(index)); - if(edit && edit->tags().contains(tag)) { - QList tagsOfEdit = edit->tags(); - tagsOfEdit.removeAll(tag); - if(tagsOfEdit.empty()) { - // no tags left in the edit - if(m_tags.empty()) { - // there are no other tag edits -> just disable the edit - edit->setTag(nullptr, false); - } else { - // there are still other tag edits -> remove the edit - toRemove.push_back(edit); - } - } else { - // there are still tags left, reassign remaining tags (keeping the previous values) - edit->setPreviousValueHandling(PreviousValueHandling::Keep); - edit->setTags(tagsOfEdit, true); - } - } - } - // remove TagEdit widgets - for(TagEdit *edit : toRemove) { - m_ui->tagSelectionComboBox->removeItem(m_ui->stackedWidget->indexOf(edit)); - m_ui->stackedWidget->removeWidget(edit); - delete edit; - } - // update affected widgets - updateTagSelectionComboBox(); - updateTagManagementMenu(); - updateUiStatus(); - } - } -} - -/*! - * \brief Changes the target of the specified \a tag; prompts the user to enter tag target information. - * - * Shows an error message if the change is (currently) not possible. Tag management menu and tag selection combo box will be updated. - */ -void MainWindow::changeTarget(Tag *tag) -{ - if(tag) { - if(!m_fileOperationMutex.try_lock()) { - m_ui->statusBar->showMessage(tr("Unable to change the target because the current process hasn't been finished yet.")); - return; - } - lock_guard guard(m_fileOperationMutex, adopt_lock); - if(!m_fileInfo.isOpen()) { - m_ui->statusBar->showMessage(tr("Unable to change the target because no file is opened.")); - return; - } - if(m_fileInfo.isOpen()) { - if(tag->supportsTarget()) { - EnterTargetDialog targetDlg(this); - targetDlg.setTarget(tag->target(), &m_fileInfo); - if(targetDlg.exec() == QDialog::Accepted) { - tag->setTarget(targetDlg.target()); - updateTagSelectionComboBox(); - updateTagManagementMenu(); - } - } else { - QMessageBox::warning(this, windowTitle(), tr("Can not change the target of the selected tag because the tag does not support targets.")); - } - } - } } } diff --git a/gui/mainwindow.h b/gui/mainwindow.h index 53f7af0..a888fb2 100644 --- a/gui/mainwindow.h +++ b/gui/mainwindow.h @@ -1,5 +1,5 @@ -#ifndef TAGEDITORDIALOG_H -#define TAGEDITORDIALOG_H +#ifndef TAGEDITORMAINWINDOW_H +#define TAGEDITORMAINWINDOW_H #include "./filefilterproxymodel.h" #include "./previousvaluehandling.h" @@ -10,26 +10,10 @@ #include #include -#include #include -#include -#ifdef TAGEDITOR_USE_WEBENGINE -# define WEB_VIEW_PROVIDER QWebEngineView -#else -# define WEB_VIEW_PROVIDER QWebView -#endif - -QT_BEGIN_NAMESPACE -class QLineEdit; -class QComboBox; -class QSpinBox; -class QPlainTextEdit; -class QGraphicsScene; -class QFileSystemModel; -class QFileSystemWatcher; -class WEB_VIEW_PROVIDER; -QT_END_NAMESPACE +QT_FORWARD_DECLARE_CLASS(QFileSystemModel) +QT_FORWARD_DECLARE_CLASS(QItemSelectionModel) namespace Media { DECLARE_ENUM(TagType, unsigned int) @@ -46,7 +30,7 @@ namespace Ui { class MainWindow; } -class TagEdit; +class TagEditorWidget; class RenameFilesDialog; class MainWindow : public QMainWindow @@ -60,93 +44,47 @@ public: // file browser QString currentDirectory(); void setCurrentDirectory(const QString &path); - -public slots: - // opened file: load, save, delete, close - bool startParsing(const QString &path, bool forceRefresh = false); - bool reparseFile(); - bool applyEntriesAndSaveChangings(); - bool deleteAllTagsAndSave(); - void closeFile(); + void startParsing(const QString &path); protected: bool event(QEvent *event); - bool eventFilter(QObject *obj, QEvent *event); private slots: // file selection void pathEntered(); void fileSelected(); - void saveAndShowNextFile(); void selectNextFile(); + void selectNextFile(QItemSelectionModel *selectionModel, const QModelIndex ¤tIndex, bool notDeeper); + void showNextFileNotFound(); void showOpenFileDlg(); - - // editor - void fileChangedOnDisk(const QString &path); - void showFile(char result); void saveFileInformation(); - void handleReturnPressed(); - void handleKeepPreviousValuesActionTriggered(QAction *action); - void applySettingsFromDialog(); - void addTag(const std::function &createTag); - void removeTag(Media::Tag *tag); - void changeTarget(Media::Tag *tag); + void handleFileStatusChange(bool opened, bool hasTag); + void handleFileSaved(const QString ¤tPath); - // saving - bool startSaving(); - void showSavingResult(bool processingError, bool ioError); + // settings + void showSettingsDlg(); + void applySettingsFromDialog(); // misc void showAboutDlg(); - void showSettingsDlg(); void showRenameFilesDlg(); - void updateInfoWebView(); - void showInfoWebViewContextMenu(const QPoint &); - void copyInfoWebViewSelection(); void spawnExternalPlayer(); private: - void updateDocumentTitleEdits(); - void updateTagEditsAndAttachmentEdits(bool updateUi = true, PreviousValueHandling previousValueHandling = PreviousValueHandling::Auto); - void updateTagSelectionComboBox(); - void updateUiStatus(); - void updateTagManagementMenu(); - void insertTitleFromFilename(); - void foreachTagEdit(const std::function &function); - bool confirmCreationOfId3TagForUnsupportedFile(); + std::mutex &fileOperationMutex(); + Media::MediaFileInfo &fileInfo(); // UI std::unique_ptr m_ui; - QMenu *m_keepPreviousValuesMenu; - QMenu *m_tagOptionsMenu; - QMenu *m_addTagMenu; - QMenu *m_removeTagMenu; - QMenu *m_changeTargetMenu; - WEB_VIEW_PROVIDER *m_infoWebView; // models QFileSystemModel *m_fileModel; FileFilterProxyModel *m_fileFilterModel; - // tag, file, directory management - QString m_currentPath; - QString m_currentDir; // this is the actual direcotry of the opened file and may differ from the directory selected in the tree view - QString m_lastDir; - QFileSystemWatcher *m_fileWatcher; - bool m_fileChangedOnDisk; - Media::MediaFileInfo m_fileInfo; - QString m_nextFilePath; - std::vector m_tags; - QByteArray m_fileInfoHtml; // dialogs Dialogs::AboutDialog *m_aboutDlg; Dialogs::SettingsDialog *m_settingsDlg; std::unique_ptr m_renameFilesDlg; - // status - bool m_makingResultsAvailable; - Media::NotificationList m_originalNotifications; - bool m_abortClicked; - std::mutex m_fileOperationMutex; }; } -#endif // TAGEDITORDIALOG_H +#endif // TAGEDITORMAINWINDOW_H diff --git a/gui/mainwindow.ui b/gui/mainwindow.ui index aaf910a..0904aad 100644 --- a/gui/mainwindow.ui +++ b/gui/mainwindow.ui @@ -72,261 +72,7 @@ - - - true - - - - 3 - - - 4 - - - 4 - - - - - true - - - No file selected - - - - - - - - - - Qt::Vertical - - - - - 2 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Document title - - - - - - - - - - - - - 0 - 0 - - - - - - - - Let you choose whether the values of the -previously opened file should be cleared when -opening the next file. -Keeping these values might be useful when -tagging multiple files of the same album. - - - Keep previous values - - - - - - - - 0 - 0 - - - - Let you enable or disable the automatic -creation or removal of tags (according to -the settings) when loading a file. - -You can also create or remove tags manually. - - - Tag management - - - - - - - Restores the original values read from -the file reverting all unsaved changings. - - - Restore - - - - .. - - - - 16 - 16 - - - - - - - - Clears all values. - - - Clear - - - - .. - - - - 16 - 16 - - - - - - - - - - - - - - - - - Qt::Horizontal - - - - - - - - - - - - - 0 - 0 - - - - Aborts the saving process. The tageditor will try to restore the original file from the backup. - - - Abort - - - - .. - - - - - - - - - false - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Save - - - all entered values - - - - - - - Delete - - - all tags from the file - - - - - - - Open next file - - - and save current before - - - - - - - Close - - - the file and discard changings - - - - - - - - + @@ -521,42 +267,6 @@ the file reverting all unsaved changings. F8 - - - true - - - No, disable this feature - - - - - true - - - Yes, but only if both files are in the same directory - - - - - true - - - Yes, regardless where the files are stored - - - - - true - - - - .. - - - Manage tags automatically when loading file - - false @@ -595,16 +305,14 @@ the file reverting all unsaved changings.
gui/pathlineedit.h
- NotificationLabel + TagEditorWidget QWidget -
gui/notificationlabel.h
- 1 +
gui/tageditorwidget.h
+ 0
filesTreeView - saveButton - deleteTagsButton diff --git a/gui/tageditorwidget.cpp b/gui/tageditorwidget.cpp new file mode 100644 index 0000000..2fef1e2 --- /dev/null +++ b/gui/tageditorwidget.cpp @@ -0,0 +1,1231 @@ +#include "./tageditorwidget.h" +#include "./notificationlabel.h" +#include "./tagedit.h" +#include "./attachmentsedit.h" +#include "./entertargetdialog.h" + +#include "../application/settings.h" +#include "../misc/htmlinfo.h" +#include "../misc/utility.h" + +#include "ui_tageditorwidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(TAGEDITOR_NO_WEBVIEW) +# error "not supported (yet)." +#elif defined(TAGEDITOR_USE_WEBENGINE) +# include +#else +# include +#endif + +#include +#include +#include + +using namespace std; +using namespace Utility; +using namespace Dialogs; +using namespace Widgets; +using namespace Media; + +namespace QtGui { + +/*! + * \brief The LoadingResult enum specifies whether the file could be parsed. + */ +enum LoadingResult : char +{ + ParsingSuccessful, + FatalParsingError, + IoError +}; + +/*! + * \class QtGui::TagEditorWidget + * \brief The TagEditorWidget class provides a widget for tag editing. + */ + +/*! + * \brief Constructs a new tag editor widget. + */ +TagEditorWidget::TagEditorWidget(QWidget *parent) : + QWidget(parent), + m_ui(new Ui::TagEditorWidget()), + m_nextFileAfterSaving(false), + m_makingResultsAvailable(false), + m_abortClicked(false) +{ + // setup UI + m_ui->setupUi(this); + makeHeading(m_ui->fileNameLabel); + // setup web view + m_infoWebView = new WEB_VIEW_PROVIDER(m_ui->tagSplitter); + m_infoWebView->setAcceptDrops(false); + m_infoWebView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_infoWebView, &QWidget::customContextMenuRequested, this, &TagEditorWidget::showInfoWebViewContextMenu); + m_ui->tagSplitter->addWidget(m_infoWebView); + // setup file watcher + m_fileWatcher = new QFileSystemWatcher(this); + m_fileChangedOnDisk = false; + // setup command link button icons + m_ui->saveButton->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton, nullptr, m_ui->saveButton)); + m_ui->deleteTagsButton->setIcon(style()->standardIcon(QStyle::SP_DialogResetButton, nullptr, m_ui->deleteTagsButton)); + m_ui->closeButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton, nullptr, m_ui->closeButton)); + // setup m_keepPreviousValuesMenu + m_keepPreviousValuesMenu = new QMenu(this); + QActionGroup *group = new QActionGroup(this); + group->addAction(m_ui->actionKeep_previous_values_never); + group->addAction(m_ui->actionKeep_previous_values_within_same_dir); + group->addAction(m_ui->actionKeep_previous_values_always); + connect(group, &QActionGroup::triggered, this, &TagEditorWidget::handleKeepPreviousValuesActionTriggered); + m_keepPreviousValuesMenu->addActions(group->actions()); + m_ui->keepPreviousValuesPushButton->setMenu(m_keepPreviousValuesMenu); + // setup m_tagOptionsMenu, m_addTagMenu, m_removeTagMenu, m_changeTargetMenu + m_tagOptionsMenu = new QMenu(this); + m_tagOptionsMenu->addAction(m_ui->actionManage_tags_automatically_when_loading_file); + connect(m_ui->actionManage_tags_automatically_when_loading_file, &QAction::triggered, [] (bool checked) { Settings::autoTagManagement() = checked; }); + m_tagOptionsMenu->addSeparator(); + m_addTagMenu = new QMenu(tr("Add tag"), m_tagOptionsMenu); + m_addTagMenu->setEnabled(false); + m_addTagMenu->setIcon(QIcon::fromTheme(QStringLiteral("tag-add"))); + m_tagOptionsMenu->addMenu(m_addTagMenu); + m_removeTagMenu = new QMenu(tr("Remove tag"), m_tagOptionsMenu); + m_removeTagMenu->setEnabled(false); + m_removeTagMenu->setIcon(QIcon::fromTheme(QStringLiteral("tag-delete"))); + m_tagOptionsMenu->addMenu(m_removeTagMenu); + m_changeTargetMenu = new QMenu(tr("Change target"), m_tagOptionsMenu); + m_changeTargetMenu->setEnabled(false); + m_changeTargetMenu->setIcon(QIcon::fromTheme(QStringLiteral("tag-properties"))); + m_tagOptionsMenu->addMenu(m_changeTargetMenu); + m_ui->tagOptionsPushButton->setMenu(m_tagOptionsMenu); + // other widgets + updateFileStatusStatus(); + m_ui->abortButton->setVisible(false); + // connect signals and slots, install event filter + // buttons: save, delete, next, close + connect(m_ui->saveButton, &QPushButton::clicked, this, &TagEditorWidget::applyEntriesAndSaveChangings); + connect(m_ui->deleteTagsButton, &QPushButton::clicked, this, &TagEditorWidget::deleteAllTagsAndSave); + connect(m_ui->nextButton, &QPushButton::clicked, this, &TagEditorWidget::saveAndShowNextFile); + connect(m_ui->closeButton, &QPushButton::clicked, this, &TagEditorWidget::closeFile); + // misc + connect(m_ui->abortButton, &QPushButton::clicked, [this] {m_abortClicked = true; m_ui->abortButton->setEnabled(false); }); + connect(m_ui->tagSelectionComboBox, static_cast(&QComboBox::activated), m_ui->stackedWidget, &QStackedWidget::setCurrentIndex); + connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &TagEditorWidget::fileChangedOnDisk); + // apply settings + applySettingsFromDialog(); +} + +/*! + * \brief Destroys the tag editor widget. + */ +TagEditorWidget::~TagEditorWidget() +{} + +/*! + * \brief Returns whether the file name is visible at the top of the widget. + */ +bool TagEditorWidget::isFileNameVisible() const +{ + return m_ui->fileNameLabel->isVisible(); +} + +/*! + * \brief Sets whether the file name is visible at the top of the widget. + */ +void TagEditorWidget::setFileNameVisible(bool visible) +{ + m_ui->fileNameLabel->setVisible(visible); +} + +/*! + * \brief Returns whether the buttons default save, next, delete and close buttons are visible. + */ +bool TagEditorWidget::areButtonsVisible() const +{ + return m_ui->buttonsWidget->isVisible(); +} + +/*! + * \brief Sets whether the buttons default save, next, delete and close buttons are visible. + */ +void TagEditorWidget::setButtonVisible(bool visible) +{ + m_ui->buttonsWidget->setVisible(visible); + m_ui->line->setVisible(visible); +} + +/*! + * \brief + * - Saves the applications settings relating the state of the main window. + * - Updates the info webview when the palette changed. + */ +bool TagEditorWidget::event(QEvent *event) +{ + switch(event->type()) { + case QEvent::PaletteChange: + updateInfoWebView(); + break; + case QEvent::DragEnter: + case QEvent::Drop: { + auto *dropEvent = static_cast(event); + for(const auto &url : dropEvent->mimeData()->urls()) { + if(url.scheme() == QLatin1String("file")) { + event->accept(); + if(event->type() == QEvent::Drop) { +#ifdef Q_OS_WIN32 + // remove leading slash + QString path = url.path(); + int index = 0; + for(const auto &c : path) { + if(c == QChar('/')) { + ++index; + } else { + break; + } + } + if(index) { + path = path.mid(index); + } + startParsing(path, true); +#else + startParsing(url.path(), true); +#endif + } + return true; + } + } + } default: + ; + } + return QWidget::event(event); +} + +/*! + * \brief Updates the line edits for the document title(s). + */ +void TagEditorWidget::updateDocumentTitleEdits() +{ + // get container, segment count and present titles + AbstractContainer *container = m_fileInfo.container(); + int segmentCount = container ? static_cast(container->segmentCount()) : 0; + const vector &titles = container ? container->titles() : vector(); + + // get layout + QLayout *docTitleLayout = m_ui->docTitleWidget->layout(); + int lineEditCount = docTitleLayout->count() - 1; + + // update existing line edits, remove unneeded line edits + int i = 0; + for(; i < lineEditCount; ++i) { + if(i < segmentCount) { + // update existing line edit + static_cast(docTitleLayout->itemAt(i + 1)->widget()) + ->setText(static_cast(i) < titles.size() ? QString::fromUtf8(titles[i].data()) : QString()); + } else { + // remove unneeded line edit + docTitleLayout->removeItem(docTitleLayout->itemAt(i + 1)); + } + } + + // add missing line edits + while(i < segmentCount) { + auto *lineEdit = new Widgets::ClearLineEdit; + if(static_cast(i) < titles.size()) { + lineEdit->setText(QString::fromUtf8(titles[i].data())); + } + lineEdit->setPlaceholderText(tr("Segment %1").arg(++i)); + docTitleLayout->addWidget(lineEdit); + } +} + +/*! + * \brief Update the tag edits and the tag selection to show the specified \a tags. + * \param tags Specifies the tags to be shown. + * \param updateUi Specifies whether the UI of the tag edits should be updated. + * \remarks The tag selection combo box should be updated after calling this method to + * ensure the updated edits can be selected properly. + */ +void TagEditorWidget::updateTagEditsAndAttachmentEdits(bool updateUi, PreviousValueHandling previousValueHandling) +{ + // determine to previous value handling according to the settings if auto is specified + switch(previousValueHandling) { + case PreviousValueHandling::Auto: + switch(Settings::adoptFields()) { + case Settings::AdoptFields::WithinDirectory: + if(m_lastDir != m_currentDir) { + previousValueHandling = PreviousValueHandling::Clear; + } + break; + case Settings::AdoptFields::Never: + previousValueHandling = PreviousValueHandling::Clear; + break; + default: + ; + } + break; + default: + ; + } + // define helper function to fetch next edit + TagEdit *edit; // holds current edit + int widgetIndex = 0; // holds index of current edit in the stacked widget + auto fetchNextEdit = [this, &edit, &widgetIndex, &previousValueHandling] { + // reuse existing edit (assigned in if-condition!) or ... + if(!((widgetIndex < m_ui->stackedWidget->count()) + && (edit = qobject_cast(m_ui->stackedWidget->widget(widgetIndex))))) { + // ... create and add a new edit + edit = new TagEdit; + connect(m_ui->clearEntriesPushButton, &QPushButton::clicked, edit, &TagEdit::clear); + connect(m_ui->restoreEntriesPushButton, &QPushButton::clicked, edit, &TagEdit::restore); + connect(edit, &TagEdit::returnPressed, this, &TagEditorWidget::handleReturnPressed); + m_ui->stackedWidget->insertWidget(widgetIndex, edit); + } + // apply settings + edit->setPreviousValueHandling(previousValueHandling); + }; + // add/update TagEdit widgets + if(m_tags.size()) { + // create a lists of the targets and tags + QList targets; + QList > tagsByTarget; + for(Tag *tag : m_tags) { + const TagTarget &target = tag->target(); + int index = targets.indexOf(target); + if(index < 0) { + targets << target; + tagsByTarget << (QList() << tag); + } else { + tagsByTarget[index] << tag; + } + } + // create a singe editor per target or seperate editors for each tag depending on the settings + switch(Settings::multipleTagHandling()) { + case Settings::MultipleTagHandling::SingleEditorPerTarget: + // iterate through all targets in both cases + for(int targetIndex = 0, targetCount = targets.size(); targetIndex < targetCount; ++targetIndex) { + fetchNextEdit(); + edit->setTags(tagsByTarget.at(targetIndex), updateUi); // set all tags with the same target to a single edit + ++widgetIndex; + } + break; + case Settings::MultipleTagHandling::SeparateEditors: + // iterate through all targets in both cases + for(int targetIndex = 0, targetCount = targets.size(); targetIndex < targetCount; ++targetIndex) { + for(Tag *tag : tagsByTarget.at(targetIndex)) { + fetchNextEdit(); + edit->setTag(tag, updateUi); // use a separate edit for each tag + ++widgetIndex; + } + } + break; + } + } else { + // there are no tags -> leave one edit existend but ensure no tags are assigned + fetchNextEdit(); + edit->setTag(nullptr, false); + ++widgetIndex; + } + // add/update AttachmentsEdit widget + if(m_fileInfo.areAttachmentsSupported()) { + AttachmentsEdit *edit; + // reuse existing edit (assigned in if-condition!) or ... + if((widgetIndex < m_ui->stackedWidget->count()) + && (edit = qobject_cast(m_ui->stackedWidget->widget(widgetIndex)))) { + edit->setFileInfo(&m_fileInfo, true); + } else { + // ... create and add a new edit + edit = new AttachmentsEdit(&m_fileInfo, this); + connect(m_ui->clearEntriesPushButton, &QPushButton::clicked, edit, &AttachmentsEdit::clear); + connect(m_ui->restoreEntriesPushButton, &QPushButton::clicked, edit, &AttachmentsEdit::restore); + //connect(edit, &AttachmentsEdit::returnPressed, this, &TagEditorWidget::handleReturnPressed); + m_ui->stackedWidget->insertWidget(widgetIndex, edit); + } + ++widgetIndex; + } + // remove surplus edits + while(widgetIndex < m_ui->stackedWidget->count()) { + QWidget *toRemove = m_ui->stackedWidget->widget(widgetIndex); + m_ui->stackedWidget->removeWidget(toRemove); + delete toRemove; + } +} + +/*! + * \brief Updates the items and the visibility of the tag selection combo box. + * \remarks Tag edits should have been updated before since this method uses the + * tag edits to generate the labels. + */ +void TagEditorWidget::updateTagSelectionComboBox() +{ + if(m_fileInfo.isOpen()) { + // memorize the index of the previously selected edit + int previouslySelectedEditIndex = m_ui->tagSelectionComboBox->currentIndex(); + // clear old entries and create new labels + m_ui->tagSelectionComboBox->clear(); + bool haveTargetInfo = false; + for(int index = 0, count = m_ui->stackedWidget->count(); index < count; ++index) { + QString label; + if(TagEdit *edit = qobject_cast(m_ui->stackedWidget->widget(index))) { + if(!edit->tags().isEmpty()) { + label = edit->generateLabel(); + haveTargetInfo |= !edit->tags().at(0)->target().isEmpty(); + } + } else if(qobject_cast(m_ui->stackedWidget->widget(index))) { + static const QString attachmentsLabel = tr("Attachments"); + label = attachmentsLabel; + } + m_ui->tagSelectionComboBox->addItem(label); + } + // set visibility + m_ui->tagSelectionComboBox->setHidden(Settings::hideTagSelectionComboBox() && m_ui->tagSelectionComboBox->count() <= 1 && !haveTargetInfo); + // restore selected index + if(previouslySelectedEditIndex >= 0 && previouslySelectedEditIndex < m_ui->tagSelectionComboBox->count()) { + m_ui->tagSelectionComboBox->setCurrentIndex(previouslySelectedEditIndex); + } + } +} + +/*! + * \brief Updates the status of the relevant widgets (enabled/disabled, visible/hidden) according to the + * current "file status" (opened/closed, has tags/no tags). + */ +void TagEditorWidget::updateFileStatusStatus() +{ + bool opened = m_fileInfo.isOpen(); + bool hasTag = opened && m_tags.size(); + // notification widgets + m_ui->parsingNotificationWidget->setVisible(opened); + m_ui->makingNotificationWidget->setVisible(opened && (m_makingResultsAvailable)); + // document title widget + m_ui->docTitleWidget->setVisible(opened && m_fileInfo.container() && m_fileInfo.container()->supportsTitle()); + // buttons and actions to save, delete, close + m_ui->saveButton->setEnabled(opened); + m_ui->nextButton->setEnabled(opened); + m_ui->deleteTagsButton->setEnabled(hasTag); + m_ui->buttonsWidget->setEnabled(opened); + // clear and restore buttons + m_ui->clearEntriesPushButton->setEnabled(hasTag); + m_ui->restoreEntriesPushButton->setEnabled(hasTag); + m_ui->clearEntriesPushButton->setEnabled(hasTag); + m_ui->restoreEntriesPushButton->setEnabled(hasTag); + // tag management button + m_ui->tagOptionsPushButton->setEnabled(opened); + // stacked widget containering edits and selection + m_ui->tagSelectionComboBox->setEnabled(hasTag); + // visibility of m_ui->tagSelectionComboBox is set within updateTagEdits + m_ui->stackedWidget->setEnabled(hasTag); + // webview + m_infoWebView->setEnabled(opened); + // inform the main window about the file status change as well + emit fileStatusChange(opened, hasTag); +} + +/*! + * \brief Updates the "tag management menu". + */ +void TagEditorWidget::updateTagManagementMenu() +{ + m_addTagMenu->clear(); + m_removeTagMenu->clear(); + m_changeTargetMenu->clear(); + if(m_fileInfo.isOpen()) { + // add "Add tag" actions + if(m_fileInfo.container()) { + // there is a container object which might be able to create tags + QString label; + if(m_fileInfo.containerFormat() == ContainerFormat::Matroska) { + // tag format supports targets (Matroska tags are currently the only tag format supporting targets.) + label = tr("Matroska tag"); + connect(m_addTagMenu->addAction(label), &QAction::triggered, std::bind(&TagEditorWidget::addTag, this, [this] (MediaFileInfo &file) -> Media::Tag * { + if(file.container()) { + EnterTargetDialog targetDlg(this); + targetDlg.setTarget(TagTarget(50), &this->m_fileInfo); + if(targetDlg.exec() == QDialog::Accepted) { + return file.container()->createTag(targetDlg.target()); + } + } + return nullptr; + })); + } else { + // tag format does not support targets + if(!m_fileInfo.container()->tagCount()) { + switch(m_fileInfo.containerFormat()) { + case ContainerFormat::Mp4: + label = tr("MP4/iTunes tag"); + break; + case ContainerFormat::Ogg: + label = tr("Vorbis comment"); + break; + default: + label = tr("Tag"); + } + connect(m_addTagMenu->addAction(label), &QAction::triggered, std::bind(&TagEditorWidget::addTag, this, [] (MediaFileInfo &file) { + return file.container() ? file.container()->createTag() : nullptr; + })); + } + } + } else { + // there is no container object; creation of ID3 tags is possible + if(!m_fileInfo.hasId3v1Tag()) { + connect(m_addTagMenu->addAction(tr("ID3v1 tag")), &QAction::triggered, std::bind(&TagEditorWidget::addTag, this, [] (MediaFileInfo &file) { + return file.createId3v1Tag(); + })); + } + if(!m_fileInfo.hasId3v2Tag()) { + connect(m_addTagMenu->addAction(tr("ID3v2 tag")), &QAction::triggered, std::bind(&TagEditorWidget::addTag, this, [] (MediaFileInfo &file) { + return file.createId3v2Tag(); + })); + } + } + // add "Remove tag" and "Change target" actions + for(Tag *tag : m_tags) { + connect(m_removeTagMenu->addAction(QString::fromLocal8Bit(tag->toString().c_str())), &QAction::triggered, std::bind(&TagEditorWidget::removeTag, this, tag)); + if(tag->supportsTarget()) { + connect(m_changeTargetMenu->addAction(QString::fromLocal8Bit(tag->toString().c_str())), &QAction::triggered, std::bind(&TagEditorWidget::changeTarget, this, tag)); + } + } + } + m_addTagMenu->setEnabled(!m_addTagMenu->actions().empty()); + m_removeTagMenu->setEnabled(!m_removeTagMenu->actions().empty()); + m_changeTargetMenu->setEnabled(!m_changeTargetMenu->actions().empty()); +} + +/*! + * \brief Inserts the title from the filename keeping a possibly available title from the tags. + * \remarks Does nothing if there are no tags assigned and if this feature is not enabled in the settings. + */ +void TagEditorWidget::insertTitleFromFilename() +{ + if(!m_tags.empty() && Settings::insertTitleFromFilename()) { + QString title; + int trackNum; + parseFileName(QString::fromLocal8Bit(m_fileInfo.fileName().c_str()), title, trackNum); + TagValue titleValue = qstringToTagValue(title, TagTextEncoding::Utf16LittleEndian); + foreachTagEdit([&titleValue] (TagEdit *edit) { + edit->setValue(KnownField::Title, titleValue, PreviousValueHandling::Keep); + }); + } +} + +/*! + * \brief Updates the info web view to show information about the + * currently opened file. + */ +void TagEditorWidget::updateInfoWebView() +{ + if(m_fileInfo.isOpen()) { + m_fileInfoHtml = HtmlInfo::generateInfo(m_fileInfo, m_originalNotifications); + m_infoWebView->setContent(m_fileInfoHtml, QStringLiteral("application/xhtml+xml")); + } else { + m_infoWebView->setUrl(QStringLiteral("about:blank")); + } +} + +/*! + * \brief Shows the context menu for the info web view. + */ +void TagEditorWidget::showInfoWebViewContextMenu(const QPoint &) +{ + QAction copyAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy"), nullptr); + copyAction.setDisabled(m_infoWebView->selectedText().isEmpty()); + connect(©Action, &QAction::triggered, this, &TagEditorWidget::copyInfoWebViewSelection); + QMenu menu; + menu.addAction(©Action); + menu.exec(QCursor::pos()); +} + +/*! + * \brief Copies the current selection of the info web view. + */ +void TagEditorWidget::copyInfoWebViewSelection() +{ + QApplication::clipboard()->setText(m_infoWebView->selectedText()); +} + +/*! + * \brief Calls the specified \a function for each of the currently present tag edits. + */ +void TagEditorWidget::foreachTagEdit(const std::function &function) +{ + for(int i = 0, count = m_ui->stackedWidget->count(); i < count; ++i) { + if(auto *edit = qobject_cast(m_ui->stackedWidget->widget(i))) { + function(edit); + } + } +} + +/*! + * \brief Opens and parses a file using another thread. + * + * Shows its tags and general information using the showFile() method. + * + * \param path Specifies the \a path of the file. + * \param forceRefresh Specifies whether the file should be reparsed if it is already opened. + */ +bool TagEditorWidget::startParsing(const QString &path, bool forceRefresh) +{ + // check if file is current file + bool sameFile = m_currentPath == path; + if(!forceRefresh && sameFile) { + return true; + } + if(!m_fileOperationMutex.try_lock()) { + emit statusMessage(tr("Unable to load the selected file \"%1\" because the current process hasn't finished yet.").arg(path)); + return false; + } + lock_guard guard(m_fileOperationMutex, adopt_lock); + // clear previous results and status + m_tags.clear(); + m_fileInfo.clearParsingResults(); + m_fileInfo.invalidateStatus(); + m_fileInfo.invalidateNotifications(); + if(!sameFile) { + // close last file if possibly open + m_fileInfo.close(); + // set path of file info + m_currentPath = path; + m_fileInfo.setPath(path.toLocal8Bit().data()); + // update directory + m_lastDir = m_currentDir; + m_currentDir = QString::fromLocal8Bit(m_fileInfo.containingDirectory().c_str()); + } + // update availability of making results + m_makingResultsAvailable &= sameFile; + if(!m_makingResultsAvailable) { + m_originalNotifications.clear(); + } + // show filename + m_ui->fileNameLabel->setText(QString::fromLocal8Bit(m_fileInfo.fileName().c_str())); + // define function to parse the file + auto startThread = [this, sameFile] { + m_fileOperationMutex.lock(); + char result; + try { + if(sameFile) { + m_fileInfo.reopen(); + } + m_fileInfo.setForceFullParse(Settings::forceFullParse()); + m_fileInfo.parseEverything(); + result = ParsingSuccessful; + } catch(Failure &) { + // the file has been opened; parsing notifications will be shown in the info box + result = FatalParsingError; + } catch(ios_base::failure &) { + // the file could not be opened because an IO error occured + m_fileInfo.close(); // ensure file is closed + result = IoError; + } + m_fileInfo.unregisterAllCallbacks(); + QMetaObject::invokeMethod(this, "showFile", Qt::QueuedConnection, Q_ARG(char, result)); + // showFile() will unlock the mutex! + }; + m_fileInfo.unregisterAllCallbacks(); + //m_fileInfo.registerCallback(showProgress); can't show progress yet + // use another thread to perform the operation + std::thread thr(startThread); + thr.detach(); + // inform user + static const QString statusMsg(tr("The file is beeing parsed ...")); + m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Progress); + m_ui->parsingNotificationWidget->setText(statusMsg); + m_ui->parsingNotificationWidget->setVisible(true); // ensure widget is visible! + emit statusMessage(statusMsg); + return true; +} + +/*! + * \brief Reparses the current file. + */ +bool TagEditorWidget::reparseFile() +{ + if(!m_fileOperationMutex.try_lock()) { + emit statusMessage(tr("Unable to reload the file because the current process hasn't finished yet.")); + return false; + } + { + lock_guard guard(m_fileOperationMutex, adopt_lock); + if(!m_fileInfo.isOpen() || m_currentPath.isEmpty()) { + QMessageBox::warning(this, windowTitle(), tr("Currently is not file opened.")); + return false; + } + } + return startParsing(m_currentPath, true); +} + +/*! + * \brief Shows the current file info (technical info, tags, ...). + * This private slot is invoked from the thread which performed the + * parsing operation using Qt::QueuedConnection. + * \param result Specifies whether the file could be load sucessfully. + * \remarks Expects m_fileOperationMutex to be locked! + */ +void TagEditorWidget::showFile(char result) +{ + lock_guard guard(m_fileOperationMutex, adopt_lock); + if(result == IoError) { + // update status + updateFileStatusStatus(); + static const QString statusMsg(tr("The file could not be opened because an IO error occurred.")); + QMessageBox::critical(this, windowTitle(), statusMsg); + emit statusMessage(statusMsg); + } else { + // update webview + updateInfoWebView(); + // show parsing status/result using parsing notification widget + auto worstNotificationType = m_fileInfo.worstNotificationTypeIncludingRelatedObjects(); + if(worstNotificationType >= Media::NotificationType::Critical) { + // we catched no exception, but there are critical notifications + // -> treat critical notifications as fatal parsing errors + result = LoadingResult::FatalParsingError; + } + switch(result) { + case ParsingSuccessful: + m_ui->parsingNotificationWidget->setNotificationType(NotificationType::TaskComplete); + m_ui->parsingNotificationWidget->setText(tr("File could be parsed correctly.")); + break; + default: + m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Critical); + m_ui->parsingNotificationWidget->setText(tr("File couldn't be parsed correctly.")); + } + switch(worstNotificationType) { + case Media::NotificationType::Critical: + m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Critical); + m_ui->parsingNotificationWidget->appendLine(tr("There are critical parsing notifications.")); + break; + case Media::NotificationType::Warning: + m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Warning); + m_ui->parsingNotificationWidget->appendLine(tr("There are warnings.")); + break; + default: + ; + } + // load existing tags + m_tags.clear(); + m_fileInfo.tags(m_tags); + // show notification if there is currently no existing tag(s) could be found + if(!m_tags.size()) { + m_ui->parsingNotificationWidget->appendLine(tr("There is no (supported) tag assigned.")); + if(!m_fileInfo.areTagsSupported()) { + m_ui->parsingNotificationWidget->setNotificationType(NotificationType::Warning); + m_ui->parsingNotificationWidget->appendLine(tr("File format is not supported (an ID3 tag can be added anyways).")); + } + } + // create appropriate tags according to file type and user preferences when automatic tag management is enabled + if(Settings::autoTagManagement()) { + if(!m_fileInfo.createAppropriateTags(false, Settings::id3v1usage(), Settings::id3v2usage(), Settings::mergeMultipleSuccessiveId3v2Tags(), + Settings::keepVersionOfExistingId3v2Tag(), Settings::id3v2versionToBeUsed())) { + if(confirmCreationOfId3TagForUnsupportedFile()) { + m_fileInfo.createAppropriateTags(true, Settings::id3v1usage(), Settings::id3v2usage(), Settings::mergeMultipleSuccessiveId3v2Tags(), + Settings::keepVersionOfExistingId3v2Tag(), Settings::id3v2versionToBeUsed()); + } + } + } + // reload tags + m_tags.clear(); + m_fileInfo.tags(m_tags); + // update file watcher + m_fileWatcher->addPath(m_currentPath); + m_fileChangedOnDisk = false; + // update related widgets + updateDocumentTitleEdits(); + updateTagEditsAndAttachmentEdits(); + updateTagSelectionComboBox(); + updateTagManagementMenu(); + insertTitleFromFilename(); + // update status + emit statusMessage(tr("The file %1 has been opened.").arg(m_currentPath)); + updateFileStatusStatus(); + } +} + +/*! + * \brief Invokes saving the current file and - if saving was successful loading the next file. + */ +void TagEditorWidget::saveAndShowNextFile() +{ + m_nextFileAfterSaving = true; + startSaving(); +} + +/*! + * \brief Applies all entries and starts saving. + * \sa startSaving() + */ +bool TagEditorWidget::applyEntriesAndSaveChangings() +{ + { + if(!m_fileOperationMutex.try_lock()) { + static const QString statusMsg(tr("Unable to apply the entered tags to the file because the current process hasn't finished yet.")); + m_ui->makingNotificationWidget->setText(statusMsg); + emit statusMessage(statusMsg); + return false; + } + lock_guard guard(m_fileOperationMutex, adopt_lock); + m_ui->makingNotificationWidget->setNotificationType(NotificationType::Information); + m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::Saving); + m_ui->makingNotificationWidget->setHidden(false); + m_makingResultsAvailable = true; + if(m_fileInfo.isOpen()) { + // apply titles + if(AbstractContainer *container = m_fileInfo.container()) { + if(container->supportsTitle()) { + QLayout *docTitleLayout = m_ui->docTitleWidget->layout(); + for(int i = 0, count = min(docTitleLayout->count() - 1, container->segmentCount()); i < count; ++i) { + container->setTitle(static_cast(docTitleLayout->itemAt(i + 1)->widget())->text().toUtf8().data(), i); + } + } + } + // apply all tags + foreachTagEdit([] (TagEdit *edit) {edit->apply();}); + static const QString statusMsg(tr("Saving tags ...")); + m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::None); + m_ui->makingNotificationWidget->setNotificationType(NotificationType::Progress); + m_ui->makingNotificationWidget->setText(statusMsg); + emit statusMessage(statusMsg); + } else { + QString statusMsg = tr("No file has been opened."); + m_ui->makingNotificationWidget->setText(statusMsg); + QMessageBox::warning(this, QApplication::applicationName(), statusMsg); + return false; + } + } + return startSaving(); +} + +/*! + * \brief Deletes all tags and starts saving. + * \sa startSaving() + */ +bool TagEditorWidget::deleteAllTagsAndSave() +{ + { + if(!m_fileOperationMutex.try_lock()) { + static const QString statusMsg(tr("Unable to delete all tags from the file because the current process hasn't been finished yet.")); + m_ui->makingNotificationWidget->setText(statusMsg); + emit statusMessage(statusMsg); + return false; + } + lock_guard guard(m_fileOperationMutex, adopt_lock); + if(Settings::askBeforeDeleting()) { + QMessageBox msgBox(this); + msgBox.setText(tr("Do you really want to delete all tags from the file?")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); +#if QT_VERSION >= 0x050200 + auto *checkBox = new QCheckBox(&msgBox); + checkBox->setText(tr("don't show this message again")); + msgBox.setCheckBox(checkBox); +#endif + int res = msgBox.exec(); +#if QT_VERSION >= 0x050200 + if(checkBox->isChecked()) { + Settings::askBeforeDeleting() = false; + } +#endif + if(res != QMessageBox::Yes) { + return false; + } + } + m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::Saving); + m_ui->makingNotificationWidget->setNotificationType(NotificationType::Information); + m_ui->makingNotificationWidget->setHidden(false); + m_makingResultsAvailable = true; + if(m_fileInfo.isOpen()) { + if(m_fileInfo.hasAnyTag()) { + foreachTagEdit([] (TagEdit *edit) {edit->clear();}); + m_fileInfo.removeAllTags(); + m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::None); + m_ui->makingNotificationWidget->setNotificationType(NotificationType::Progress); + static const QString statusMsg(tr("Deleting all tags ...")); + m_ui->makingNotificationWidget->setText(statusMsg); + emit statusMessage(statusMsg); + } else { + m_ui->makingNotificationWidget->setText(tr("The selected file stores no tag (at least no supported), so there is nothing to delete.")); + return false; + } + } else { + m_ui->makingNotificationWidget->setText(tr("No file has been opened, so no tags can be deleted.")); + return false; + } + } + return startSaving(); +} + +/*! + * \brief Starts saving. This method is called by applyEntriesAndSaveChangings() and deleteAllTagsAndSave(). + * The actual process is performed in another thread. + * \remarks Will start a new thread to perform the operation. Then showSavingResult() is called + * using Qt::QueuedConnection in the main thread. m_fileOperationMutex will remain locked when saving is + * finished and will be unlocked in showSavingResult(). This way any method which might be called after + * the operation thread ends and before the invokation of showSavingResult() will see a locked mutex and + * hence not mutate the current file. + */ +bool TagEditorWidget::startSaving() +{ + if(!m_fileOperationMutex.try_lock()) { + static const QString errorMsg(tr("Unable to start saving process because there an other process hasn't finished yet.")); + emit statusMessage(errorMsg); + QMessageBox::warning(this, QApplication::applicationName(), errorMsg); + return false; + } + lock_guard guard(m_fileOperationMutex, adopt_lock); + // tags might get invalidated + m_tags.clear(); + foreachTagEdit([] (TagEdit *edit) { edit->setTag(nullptr, false); }); + // show abort button + m_ui->abortButton->setHidden(false); + m_ui->abortButton->setEnabled(true); + m_abortClicked = false; + // remove current path from file watcher + m_fileWatcher->removePath(m_currentPath); + // use current configuration + m_fileInfo.setForceRewrite(Settings::forceRewrite()); + m_fileInfo.setTagPosition(Settings::preferredTagPosition()); + m_fileInfo.setForceTagPosition(Settings::forceTagPosition()); + m_fileInfo.setIndexPosition(Settings::preferredIndexPosition()); + m_fileInfo.setForceIndexPosition(Settings::forceIndexPosition()); + m_fileInfo.setMinPadding(Settings::minPadding()); + m_fileInfo.setMaxPadding(Settings::maxPadding()); + m_fileInfo.setPreferredPadding(Settings::preferredPadding()); + // define functions to show the saving progress and to actually applying the changes + auto showProgress = [this] (StatusProvider &sender) -> void { + QMetaObject::invokeMethod(m_ui->makingNotificationWidget, "setPercentage", Qt::QueuedConnection, Q_ARG(int, static_cast(sender.currentPercentage() * 100.0))); + if(m_abortClicked) { + QMetaObject::invokeMethod(m_ui->makingNotificationWidget, "setText", Qt::QueuedConnection, Q_ARG(QString, tr("Cancelling ..."))); + m_fileInfo.tryToAbort(); + } else { + QMetaObject::invokeMethod(m_ui->makingNotificationWidget, "setText", Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(sender.currentStatus()))); + } + }; + auto startThread = [this] { + m_fileOperationMutex.lock(); + bool processingError = false, ioError = false; + try { + m_fileInfo.applyChanges(); + } catch(const Failure &) { + processingError = true; + } catch(const ios_base::failure &) { + ioError = true; + } + m_fileInfo.unregisterAllCallbacks(); + QMetaObject::invokeMethod(this, "showSavingResult", Qt::QueuedConnection, Q_ARG(bool, processingError), Q_ARG(bool, ioError)); + // showSavingResult() will unlock the mutex! + }; + m_fileInfo.unregisterAllCallbacks(); + m_fileInfo.registerCallback(showProgress); + // use another thread to perform the operation + std::thread thr(startThread); + thr.detach(); + return true; +} + +/*! + * \brief Shows the saving results. + * This private slot is invoked from the thread which performed the + * saving operation using Qt::QueuedConnection. + * \param sucess Specifies whether the file could be saved sucessfully. + * \remarks Expects m_fileOperationMutex to be locked! + */ +void TagEditorWidget::showSavingResult(bool processingError, bool ioError) +{ + m_ui->abortButton->setHidden(true); + m_ui->makingNotificationWidget->setNotificationType(NotificationType::TaskComplete); + m_ui->makingNotificationWidget->setNotificationSubject(NotificationSubject::Saving); + m_ui->makingNotificationWidget->setPercentage(-1); + m_ui->makingNotificationWidget->setHidden(false); + m_makingResultsAvailable = true; + m_originalNotifications = m_fileInfo.gatherRelatedNotifications(); + if(!processingError && !ioError) { + // display status messages + QString statusMsg; + size_t critical = 0, warnings = 0; + for(const Notification ¬ification : m_originalNotifications) { + switch(notification.type()) { + case Media::NotificationType::Critical: + ++critical; + break; + case Media::NotificationType::Warning: + ++warnings; + break; + default: + ; + } + } + if(warnings || critical) { + if(critical) { + statusMsg = tr("The tags have been saved, but there is/are %1 warning(s) ", 0, warnings).arg(warnings); + statusMsg.append(tr("and %1 error(s).", 0, critical).arg(critical)); + } else { + statusMsg = tr("The tags have been saved, but there is/are %1 warning(s).", 0, warnings).arg(warnings); + } + m_ui->makingNotificationWidget->setNotificationType(critical > 0 ? NotificationType::Critical : NotificationType::Warning); + + } else { + statusMsg = tr("The tags have been saved."); + } + m_ui->makingNotificationWidget->setText(statusMsg); + emit statusMessage(statusMsg); + m_fileOperationMutex.unlock(); + // let the main window know that the current file has been saved + // -> the main window will ensure the current file is still selected + emit fileSaved(m_currentPath); + // show next file (only if there are critical notifications) + m_nextFileAfterSaving = false; + if(critical < 1 && m_nextFileAfterSaving) { + emit nextFileSelected(); + } else { + startParsing(m_currentPath, true); + } + } else { + static const QString processingErrorMsg(tr("The tags couldn't be saved. See the info box for detail.")); + static const QString ioErrorMsg(tr("The tags couldn't be saved because an IO error occured.")); + const auto &errorMsg = ioError ? ioErrorMsg : processingErrorMsg; + QMessageBox::warning(this, QApplication::applicationName(), errorMsg); + emit statusMessage(errorMsg); + m_ui->makingNotificationWidget->setText(errorMsg); + m_ui->makingNotificationWidget->setNotificationType(NotificationType::Critical); + m_fileOperationMutex.unlock(); + startParsing(m_currentPath, true); + } +} + +/*! + * \brief Asks the user whether an ID3 tag should be add to a not supported container format and returns the result. + */ +bool TagEditorWidget::confirmCreationOfId3TagForUnsupportedFile() +{ + QMessageBox msgBox(this); + msgBox.setWindowTitle(tr("Automatic tag management")); + msgBox.setText(tr("The container format of the selected file is not supported. The file can be treated as MP3 file (an ID3 tag according to the settings will be created). This might break the file. Do you want to continue?")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.addButton(tr("Treat file as MP3 file"), QMessageBox::AcceptRole); + msgBox.addButton(tr("Abort"), QMessageBox::RejectRole); + return msgBox.exec() == 0; +} + +/*! + * \brief This slot is connected to the fileChanged() signal of the file info watcher. + */ +void TagEditorWidget::fileChangedOnDisk(const QString &path) +{ + if(!m_fileChangedOnDisk && m_fileInfo.isOpen() && path == m_currentPath) { + auto ¬ifyWidget = *m_ui->parsingNotificationWidget; + notifyWidget.appendLine(tr("The currently opened file changed on the disk.")); + notifyWidget.setNotificationType(notifyWidget.notificationType() == NotificationType::Critical ? NotificationType::Critical : NotificationType::Warning); + m_fileChangedOnDisk = true; + } +} + +/*! + * \brief Closes the currently opened file and disables all related widgets. + */ +void TagEditorWidget::closeFile() +{ + if(!m_fileOperationMutex.try_lock()) { + emit statusMessage("Unable to close the file because the current process hasn't been finished yet."); + return; + } + lock_guard guard(m_fileOperationMutex, adopt_lock); + // close file + m_fileInfo.close(); + // remove current path from file watcher + m_fileWatcher->removePath(m_currentPath); + // update ui + emit statusMessage("The file has been closed."); + updateFileStatusStatus(); +} + +/*! + * \brief This private slot is invoked the the return key has been pressed in a tag edit. + * + * The file will be saved and then the next opened if the user selected that option. + */ +void TagEditorWidget::handleReturnPressed() +{ + if(Settings::saveAndShowNextOnEnter() && m_fileInfo.isOpen()) { + saveAndShowNextFile(); + } +} + +void TagEditorWidget::handleKeepPreviousValuesActionTriggered(QAction *action) +{ + if(action == m_ui->actionKeep_previous_values_never) { + Settings::adoptFields() = Settings::AdoptFields::Never; + } else if(action == m_ui->actionKeep_previous_values_within_same_dir) { + Settings::adoptFields() = Settings::AdoptFields::WithinDirectory; + } else if(action == m_ui->actionKeep_previous_values_always) { + Settings::adoptFields() = Settings::AdoptFields::Always; + } +} + +/*! + * \brief Applies settings from Settings namespace. Only settings configurable through the SettingsDialog + * will be applied and not settings like the main window's geometry and state. + */ +void TagEditorWidget::applySettingsFromDialog() +{ + switch(Settings::adoptFields()) { + case Settings::AdoptFields::Never: + m_ui->actionKeep_previous_values_never->setChecked(true); + break; + case Settings::AdoptFields::WithinDirectory: + m_ui->actionKeep_previous_values_within_same_dir->setChecked(true); + break; + case Settings::AdoptFields::Always: + m_ui->actionKeep_previous_values_always->setChecked(true); + break; + } + m_ui->actionManage_tags_automatically_when_loading_file->setChecked(Settings::autoTagManagement()); +} + +/*! + * \brief Adds a tag (using the specified \a createTag function) to the currently opened file. + * + * Shows an error message if no file is opened. Tag edits, tag management menu und UI status will be updated. + */ +void TagEditorWidget::addTag(const function &createTag) +{ + if(!m_fileOperationMutex.try_lock()) { + emit statusMessage("Unable to add a tag because the current process hasn't been finished yet."); + return; + } + lock_guard guard(m_fileOperationMutex, adopt_lock); + if(!m_fileInfo.isOpen()) { + emit statusMessage("Unable to add a tag because no file is opened."); + return; + } + if(Tag *tag = createTag(m_fileInfo)) { + if(std::find(m_tags.cbegin(), m_tags.cend(), tag) == m_tags.cend()) { + m_tags.push_back(tag); + updateTagEditsAndAttachmentEdits(true, m_tags.size() > 1 ? PreviousValueHandling::Keep : PreviousValueHandling::Auto); + updateTagSelectionComboBox(); + updateTagManagementMenu(); + updateFileStatusStatus(); + insertTitleFromFilename(); + } else { + QMessageBox::warning(this, windowTitle(), tr("A tag (with the selected target) already exists.")); + } + + } else { + QMessageBox::warning(this, windowTitle(), tr("The tag can not be created.")); + } +} + +/*! + * \brief Removes the specified \a tag from the currently opened file. + * + * Shows an error message if the removal is (currently) not possible. Tag edits, tag management menu und UI status will be updated. + */ +void TagEditorWidget::removeTag(Tag *tag) +{ + if(tag) { + if(!m_fileOperationMutex.try_lock()) { + emit statusMessage(tr("Unable to remove the tag because the current process hasn't been finished yet.")); + return; + } + lock_guard guard(m_fileOperationMutex, adopt_lock); + if(!m_fileInfo.isOpen()) { + emit statusMessage(tr("Unable to remove the tag because no file is opened.")); + return; + } + if(m_fileInfo.isOpen()) { + m_fileInfo.removeTag(tag); + // remove tag from m_tags + m_tags.erase(remove(m_tags.begin(), m_tags.end(), tag), m_tags.end()); + // remove tag from all TagEdit widgets + vector toRemove; + for(int index = 0, count = m_ui->stackedWidget->count(); index < count; ++index) { + TagEdit *edit = qobject_cast(m_ui->stackedWidget->widget(index)); + if(edit && edit->tags().contains(tag)) { + QList tagsOfEdit = edit->tags(); + tagsOfEdit.removeAll(tag); + if(tagsOfEdit.empty()) { + // no tags left in the edit + if(m_tags.empty()) { + // there are no other tag edits -> just disable the edit + edit->setTag(nullptr, false); + } else { + // there are still other tag edits -> remove the edit + toRemove.push_back(edit); + } + } else { + // there are still tags left, reassign remaining tags (keeping the previous values) + edit->setPreviousValueHandling(PreviousValueHandling::Keep); + edit->setTags(tagsOfEdit, true); + } + } + } + // remove TagEdit widgets + for(TagEdit *edit : toRemove) { + m_ui->tagSelectionComboBox->removeItem(m_ui->stackedWidget->indexOf(edit)); + m_ui->stackedWidget->removeWidget(edit); + delete edit; + } + // update affected widgets + updateTagSelectionComboBox(); + updateTagManagementMenu(); + updateFileStatusStatus(); + } + } +} + +/*! + * \brief Changes the target of the specified \a tag; prompts the user to enter tag target information. + * + * Shows an error message if the change is (currently) not possible. Tag management menu and tag selection combo box will be updated. + */ +void TagEditorWidget::changeTarget(Tag *tag) +{ + if(tag) { + if(!m_fileOperationMutex.try_lock()) { + emit statusMessage(tr("Unable to change the target because the current process hasn't been finished yet.")); + return; + } + lock_guard guard(m_fileOperationMutex, adopt_lock); + if(!m_fileInfo.isOpen()) { + emit statusMessage(tr("Unable to change the target because no file is opened.")); + return; + } + if(m_fileInfo.isOpen()) { + if(tag->supportsTarget()) { + EnterTargetDialog targetDlg(this); + targetDlg.setTarget(tag->target(), &m_fileInfo); + if(targetDlg.exec() == QDialog::Accepted) { + tag->setTarget(targetDlg.target()); + updateTagSelectionComboBox(); + updateTagManagementMenu(); + } + } else { + QMessageBox::warning(this, windowTitle(), tr("Can not change the target of the selected tag because the tag does not support targets.")); + } + } + } +} + +} diff --git a/gui/tageditorwidget.h b/gui/tageditorwidget.h new file mode 100644 index 0000000..fa5805c --- /dev/null +++ b/gui/tageditorwidget.h @@ -0,0 +1,187 @@ +#ifndef TAGEDITORWIDGET_H +#define TAGEDITORWIDGET_H + +#include "./previousvaluehandling.h" + +#include + +#include +#include + +#include +#include + +#if defined(TAGEDITOR_NO_WEBVIEW) +# error "not supported (yet)." +#elif defined(TAGEDITOR_USE_WEBENGINE) +# define WEB_VIEW_PROVIDER QWebEngineView +#else +# define WEB_VIEW_PROVIDER QWebView +#endif + +QT_FORWARD_DECLARE_CLASS(QFileSystemWatcher) +QT_FORWARD_DECLARE_CLASS(QMenu) +#ifndef TAGEDITOR_NO_WEBVIEW +QT_FORWARD_DECLARE_CLASS(WEB_VIEW_PROVIDER) +#endif + +namespace Media { +DECLARE_ENUM(TagType, unsigned int) +} + +namespace QtGui { + +namespace Ui { +class TagEditorWidget; +} + +class TagEdit; + +class TagEditorWidget : public QWidget +{ + Q_OBJECT + Q_PROPERTY(QString currentPath READ currentPath) + Q_PROPERTY(QByteArray fileInfoHtml READ fileInfoHtml) + Q_PROPERTY(bool fileNameVisible READ isFileNameVisible WRITE setFileNameVisible) + Q_PROPERTY(bool buttonsVisible READ areButtonsVisible WRITE setButtonVisible) + +public: + explicit TagEditorWidget(QWidget *parent = nullptr); + virtual ~TagEditorWidget(); + +public: + std::mutex &fileOperationMutex(); + const QString ¤tPath() const; + Media::MediaFileInfo &fileInfo(); + const QByteArray &fileInfoHtml() const; + bool isFileNameVisible() const; + void setFileNameVisible(bool visible); + bool areButtonsVisible() const; + void setButtonVisible(bool visible); + +public slots: + // operations with the currently opened file: load, save, delete, close + bool startParsing(const QString &path, bool forceRefresh = false); + bool startSaving(); + void saveAndShowNextFile(); + bool reparseFile(); + bool applyEntriesAndSaveChangings(); + bool deleteAllTagsAndSave(); + void closeFile(); + +signals: + /*! + * \brief Emitted when loading the next file has been triggered. + */ + void nextFileSelected(); + + /*! + * \brief Emitted to show a status message. + */ + void statusMessage(const QString &message, int timeout = 0); + + /*! + * \brief Emmited when the file status (opened/closed) has changed. + */ + void fileStatusChange(bool opened, bool hasTag); + + /*! + * \brief Emitted when the current path changed. + */ + void fileSaved(const QString &newPath); + +protected: + bool event(QEvent *event); + +private slots: + // editor + void fileChangedOnDisk(const QString &path); + void showFile(char result); + void handleReturnPressed(); + void handleKeepPreviousValuesActionTriggered(QAction *action); + void applySettingsFromDialog(); + void addTag(const std::function &createTag); + void removeTag(Media::Tag *tag); + void changeTarget(Media::Tag *tag); + + // saving + void showSavingResult(bool processingError, bool ioError); + + // web view + void updateInfoWebView(); + void showInfoWebViewContextMenu(const QPoint &); + void copyInfoWebViewSelection(); + +private: + void updateDocumentTitleEdits(); + void updateTagEditsAndAttachmentEdits(bool updateUi = true, PreviousValueHandling previousValueHandling = PreviousValueHandling::Auto); + void updateTagSelectionComboBox(); + void updateFileStatusStatus(); + void updateTagManagementMenu(); + void insertTitleFromFilename(); + void foreachTagEdit(const std::function &function); + bool confirmCreationOfId3TagForUnsupportedFile(); + + // UI + std::unique_ptr m_ui; + QMenu *m_keepPreviousValuesMenu; + QMenu *m_tagOptionsMenu; + QMenu *m_addTagMenu; + QMenu *m_removeTagMenu; + QMenu *m_changeTargetMenu; + WEB_VIEW_PROVIDER *m_infoWebView; + // tag, file, directory management + QString m_currentPath; + QFileSystemWatcher *m_fileWatcher; + bool m_fileChangedOnDisk; + Media::MediaFileInfo m_fileInfo; + std::vector m_tags; + QByteArray m_fileInfoHtml; + /*! + * \brief This is the actual direcotry of the opened file which may differ from the directory selected in the tree view of the main window. + */ + QString m_currentDir; + QString m_lastDir; + // status + bool m_nextFileAfterSaving; + bool m_makingResultsAvailable; + Media::NotificationList m_originalNotifications; + bool m_abortClicked; + std::mutex m_fileOperationMutex; +}; + +/*! + * \brief Returns the mutex which is internally used for thread-synchronization. + */ +inline std::mutex &TagEditorWidget::fileOperationMutex() +{ + return m_fileOperationMutex; +} + +/*! + * \brief Returns the current path. + */ +inline const QString &TagEditorWidget::currentPath() const +{ + return m_currentPath; +} + +/*! + * \brief Return file info. + */ +inline Media::MediaFileInfo &TagEditorWidget::fileInfo() +{ + return m_fileInfo; +} + +/*! + * \brief Returns the HTML source of the info website. + */ +inline const QByteArray &TagEditorWidget::fileInfoHtml() const +{ + return m_fileInfoHtml; +} + +} + +#endif // TAGEDITORWIDGET_H diff --git a/gui/tageditorwidget.ui b/gui/tageditorwidget.ui new file mode 100644 index 0000000..6c98b6e --- /dev/null +++ b/gui/tageditorwidget.ui @@ -0,0 +1,305 @@ + + + QtGui::TagEditorWidget + + + true + + + + 3 + + + 4 + + + 4 + + + + + true + + + No file selected + + + + + + + + + + Qt::Vertical + + + + + 2 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Document title + + + + + + + + + + + + + 0 + 0 + + + + + + + + Let you choose whether the values of the +previously opened file should be cleared when +opening the next file. +Keeping these values might be useful when +tagging multiple files of the same album. + + + Keep previous values + + + + + + + + 0 + 0 + + + + Let you enable or disable the automatic +creation or removal of tags (according to +the settings) when loading a file. + +You can also create or remove tags manually. + + + Tag management + + + + + + + Restores the original values read from +the file reverting all unsaved changings. + + + Restore + + + + .. + + + + 16 + 16 + + + + + + + + Clears all values. + + + Clear + + + + .. + + + + 16 + 16 + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + + + + Aborts the saving process. The tageditor will try to restore the original file from the backup. + + + Abort + + + + .. + + + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Save + + + all entered values + + + + + + + Delete + + + all tags from the file + + + + + + + Open next file + + + and save current before + + + + + + + Close + + + the file and discard changings + + + + + + + + + + true + + + No, disable this feature + + + + + true + + + Yes, but only if both files are in the same directory + + + + + true + + + Yes, regardless where the files are stored + + + + + true + + + + .. + + + Manage tags automatically when loading file + + + + + + NotificationLabel + QWidget +
gui/notificationlabel.h
+ 1 +
+
+ + +
diff --git a/misc/utility.h b/misc/utility.h index abcabe7..89db10f 100644 --- a/misc/utility.h +++ b/misc/utility.h @@ -5,11 +5,9 @@ #include -QT_BEGIN_NAMESPACE -class QDir; -class QAbstractItemModel; -class QModelIndex; -QT_END_NAMESPACE +QT_FORWARD_DECLARE_CLASS(QDir) +QT_FORWARD_DECLARE_CLASS(QAbstractItemModel) +QT_FORWARD_DECLARE_CLASS(QModelIndex) namespace Media { class MediaFileInfo; diff --git a/tageditor.pro b/tageditor.pro index faa2a27..dfe8001 100644 --- a/tageditor.pro +++ b/tageditor.pro @@ -62,7 +62,8 @@ HEADERS += \ gui/entertargetdialog.h \ gui/attachmentsmodel.h \ gui/attachmentsedit.h \ - gui/codeedit.h + gui/codeedit.h \ + gui/tageditorwidget.h SOURCES += \ application/main.cpp \ @@ -92,7 +93,8 @@ SOURCES += \ gui/entertargetdialog.cpp \ gui/attachmentsmodel.cpp \ gui/attachmentsedit.cpp \ - gui/codeedit.cpp + gui/codeedit.cpp \ + gui/tageditorwidget.cpp FORMS += \ gui/id3v2optionpage.ui \ @@ -109,7 +111,8 @@ FORMS += \ gui/entertargetdialog.ui \ gui/attachmentsedit.ui \ gui/editortempoptionpage.ui \ - gui/filelayout.ui + gui/filelayout.ui \ + gui/tageditorwidget.ui RESOURCES += \ resources/icons.qrc \