#include "./dbquerywidget.h" #include "./tagedit.h" #include "./tageditorwidget.h" #include "../application/knownfieldmodel.h" #include "../application/settings.h" #include "../dbquery/dbquery.h" #include "../misc/utility.h" #include "ui_dbquerywidget.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace std::placeholders; using namespace ConversionUtilities; using namespace Dialogs; using namespace Models; using namespace Settings; using namespace Utility; using namespace TagParser; namespace QtGui { DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent) : QWidget(parent) , m_ui(new Ui::DbQueryWidget) , m_tagEditorWidget(tagEditorWidget) , m_model(nullptr) , m_coverIndex(-1) , m_lyricsIndex(-1) , m_menu(new QMenu(parent)) { m_ui->setupUi(this); #ifdef Q_OS_WIN32 setStyleSheet(dialogStyle()); #else setStyleSheet(QStringLiteral("QGroupBox { color: palette(text); background-color: palette(base); }")); #endif m_ui->notificationLabel->setText(tr("Search hasn't been started")); m_ui->notificationLabel->setContext(tr("MusicBrainz/LyricsWikia notifications")); m_ui->notificationLabel->setMaxLineCount(5); m_ui->searchGroupBox->installEventFilter(this); // initialize buttons m_ui->abortPushButton->setIcon(style()->standardIcon(QStyle::SP_DialogCancelButton, nullptr, m_ui->abortPushButton)); m_ui->abortPushButton->setVisible(false); m_ui->applyPushButton->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton, nullptr, m_ui->applyPushButton)); // initialize fields model m_ui->fieldsListView->setModel(&values().dbQuery.fields); // initialize search terms form insertSearchTermsFromTagEdit(m_tagEditorWidget->activeTagEdit()); // restore settings m_ui->overrideCheckBox->setChecked(values().dbQuery.override); // setup menu m_insertPresentDataAction = m_menu->addAction(tr("Insert present data")); m_insertPresentDataAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); m_insertPresentDataAction->setEnabled(m_tagEditorWidget->activeTagEdit()); connect(m_insertPresentDataAction, &QAction::triggered, this, &DbQueryWidget::insertSearchTermsFromActiveTagEdit); QAction *clearSearchCriteria = m_menu->addAction(tr("Clear search criteria")); clearSearchCriteria->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear"))); connect(clearSearchCriteria, &QAction::triggered, this, &DbQueryWidget::clearSearchCriteria); m_ui->menuPushButton->setMenu(m_menu); // ensure fieldsGroupBox takes only minimal space (initially) m_ui->splitter->setSizes({ 1000, 1 }); // connect signals and slots connect(m_ui->abortPushButton, &QPushButton::clicked, this, &DbQueryWidget::abortSearch); connect(m_ui->searchMusicBrainzPushButton, &QPushButton::clicked, this, &DbQueryWidget::searchMusicBrainz); connect(m_ui->searchLyricsWikiaPushButton, &QPushButton::clicked, this, &DbQueryWidget::searchLyricsWikia); connect(m_ui->applyPushButton, &QPushButton::clicked, this, static_cast(&DbQueryWidget::applySelectedResults)); connect(m_tagEditorWidget, &TagEditorWidget::fileStatusChanged, this, &DbQueryWidget::fileStatusChanged); connect(m_ui->resultsTreeView, &QTreeView::customContextMenuRequested, this, &DbQueryWidget::showResultsContextMenu); } DbQueryWidget::~DbQueryWidget() { values().dbQuery.override = m_ui->overrideCheckBox->isChecked(); } void DbQueryWidget::insertSearchTermsFromTagEdit(TagEdit *tagEdit) { if (!tagEdit) { return; } // set title, album and artist m_ui->titleLineEdit->setText(tagValueToQString(tagEdit->value(KnownField::Title))); m_ui->albumLineEdit->setText(tagValueToQString(tagEdit->value(KnownField::Album))); m_ui->artistLineEdit->setText(tagValueToQString(tagEdit->value(KnownField::Artist))); // set track number, or if not available part number bool trackValueOk = false; try { TagValue trackValue = tagEdit->value(KnownField::TrackPosition); if (!trackValue.isEmpty()) { m_ui->trackSpinBox->setValue(trackValue.toPositionInSet().position()); trackValueOk = true; } } catch (const ConversionException &) { } if (!trackValueOk) { TagValue trackValue = tagEdit->value(KnownField::PartNumber); if (!trackValue.isEmpty()) { m_ui->trackSpinBox->setValue(trackValue.toInteger()); trackValueOk = true; } } if (!trackValueOk) { m_ui->trackSpinBox->clear(); } } SongDescription DbQueryWidget::currentSongDescription() const { SongDescription desc; desc.title = m_ui->titleLineEdit->text(); desc.album = m_ui->albumLineEdit->text(); desc.artist = m_ui->artistLineEdit->text(); desc.track = m_ui->trackSpinBox->value(); return desc; } void DbQueryWidget::searchMusicBrainz() { // check whether enough search terms are supplied if (m_ui->titleLineEdit->text().isEmpty() && m_ui->albumLineEdit->text().isEmpty() && m_ui->artistLineEdit->text().isEmpty()) { m_ui->notificationLabel->setNotificationType(NotificationType::Critical); m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied - at least title, album or artist must be specified")); return; } // delete current model m_ui->resultsTreeView->setModel(nullptr); delete m_model; // show status m_ui->notificationLabel->setNotificationType(NotificationType::Progress); m_ui->notificationLabel->setText(tr("Retrieving meta data from MusicBrainz ...")); setStatus(false); // do actual query useQueryResults(queryMusicBrainz(currentSongDescription())); } void DbQueryWidget::searchLyricsWikia() { // check whether enough search terms are supplied if (m_ui->artistLineEdit->text().isEmpty()) { m_ui->notificationLabel->setNotificationType(NotificationType::Critical); m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied - artist is mandatory")); return; } // delete current model m_ui->resultsTreeView->setModel(nullptr); delete m_model; // show status m_ui->notificationLabel->setNotificationType(NotificationType::Progress); m_ui->notificationLabel->setText(tr("Retrieving meta data from LyricsWikia ...")); setStatus(false); // do actual query useQueryResults(queryLyricsWikia(currentSongDescription())); } void DbQueryWidget::abortSearch() { if (!m_model) { return; } if (m_model->isFetchingCover()) { // call abort to abort fetching cover m_model->abort(); } else if (!m_model->areResultsAvailable()) { // delete model to abort search m_ui->resultsTreeView->setModel(nullptr); delete m_model; m_model = nullptr; // update status m_ui->notificationLabel->setNotificationType(NotificationType::Information); m_ui->notificationLabel->setText(tr("Aborted")); setStatus(true); } } void DbQueryWidget::showResults() { if (!m_model) { return; } if (m_model->errorList().isEmpty()) { m_ui->notificationLabel->setNotificationType(NotificationType::TaskComplete); if (m_model->results().isEmpty()) { m_ui->notificationLabel->setText(tr("No results available")); } else { m_ui->notificationLabel->setText(tr("%1 result(s) available", nullptr, m_model->results().size()).arg(m_model->results().size())); } } else { m_ui->notificationLabel->setNotificationType(NotificationType::Critical); m_ui->notificationLabel->clearText(); for (const QString &error : m_model->errorList()) { m_ui->notificationLabel->appendLine(error); } } if (m_model->results().isEmpty()) { m_ui->applyPushButton->setEnabled(false); } else { m_ui->resultsTreeView->selectionModel()->setCurrentIndex( m_model->index(0, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); m_ui->applyPushButton->setEnabled(m_tagEditorWidget->activeTagEdit()); } setStatus(true); } void DbQueryWidget::setStatus(bool aborted) { m_ui->abortPushButton->setVisible(!aborted); m_ui->searchMusicBrainzPushButton->setVisible(aborted); m_ui->searchLyricsWikiaPushButton->setVisible(aborted); m_ui->applyPushButton->setVisible(aborted); } void DbQueryWidget::fileStatusChanged(bool, bool hasTags) { m_ui->applyPushButton->setEnabled(hasTags && m_ui->resultsTreeView->selectionModel() && m_ui->resultsTreeView->selectionModel()->hasSelection()); insertSearchTermsFromActiveTagEdit(); m_insertPresentDataAction->setEnabled(hasTags); } /*! * \brief Applies the selected results for the selected fields to the active tag edit. * \sa applyResults() */ void DbQueryWidget::applySelectedResults() { // check whether model, tag edit and current selection exist if (auto *const tagEdit = m_tagEditorWidget->activeTagEdit()) { if (const auto *const selectionModel = m_ui->resultsTreeView->selectionModel()) { const auto selection = selectionModel->selection().indexes(); if (!selection.isEmpty()) { applyResults(tagEdit, selection.front()); } } } } /*! * \brief Completes all present tag edits with the best matching result row. * \remarks * - Does nothing if no result row matches. * - Only the selected fields are applied. * \sa applyResults() */ void DbQueryWidget::applyMatchingResults() { m_tagEditorWidget->foreachTagEdit(bind(static_cast(&DbQueryWidget::applyMatchingResults), this, _1)); } /*! * \brief Completes the specified \a tagEdit with the best matching result row. * \remarks * - Does nothing if no result row matches. * - Only the selected fields are applied. * \sa applyResults() */ void DbQueryWidget::applyMatchingResults(TagEdit *tagEdit) { if (!m_model) { return; } // determine already present title, album and artist const auto givenTitle = tagEdit->value(KnownField::Title); const auto givenAlbum = tagEdit->value(KnownField::Album); const auto givenArtist = tagEdit->value(KnownField::Artist); // also determine already present track number (which is a little bit more complex -> TODO: improve backend API) int givenTrack; try { givenTrack = tagEdit->value(KnownField::TrackPosition).toPositionInSet().position(); } catch (const ConversionException &) { givenTrack = 0; } if (!givenTrack) { for (const Tag *tag : tagEdit->tags()) { if (!tag->supportsTarget() || tag->targetLevel() == TagTargetLevel::Track) { try { givenTrack = tagEdit->value(KnownField::PartNumber).toInteger(); } catch (const ConversionException &) { } break; } } } if (givenTitle.isEmpty() || !givenTrack) { return; } // find row matching already present data for (int row = 0, rowCount = m_model->rowCount(); row != rowCount; ++row) { if ((!givenTitle.isEmpty() && givenTitle != m_model->fieldValue(row, KnownField::Title)) || (!givenAlbum.isEmpty() && givenAlbum != m_model->fieldValue(row, KnownField::Album)) || (!givenArtist.isEmpty() && givenArtist != m_model->fieldValue(row, KnownField::Artist)) || (givenTrack && givenTrack != m_model->fieldValue(row, KnownField::PartNumber).toInteger())) { continue; } // apply results for matching row applyResults(tagEdit, m_model->index(row, 0)); // just take the first matching row for now break; } } /*! * \brief The same as applyMatchingResults() but checks whether auto-insert is enabled before (and does nothing if not). */ void DbQueryWidget::autoInsertMatchingResults() { if (m_ui->autoInsertCheckBox->isChecked()) { applyMatchingResults(); } } /*! * \brief Applies the results at the specified \a resultIndex for the selected fields to the specified \a tagEdit. * \remarks * - Returns instantly. If cover/lyrics need to be retrieved, this is done asynchronously. * - Does nothing if no results are available. */ void DbQueryWidget::applyResults(TagEdit *tagEdit, const QModelIndex &resultIndex) { if (!m_model) { return; } // determine previous value handling const auto previousValueHandling = m_ui->overrideCheckBox->isChecked() ? PreviousValueHandling::Update : PreviousValueHandling::Keep; // loop through all fields for (const ChecklistItem &item : values().dbQuery.fields.items()) { if (!item.isChecked()) { continue; } // determine the field to be used and its value const auto field = static_cast(item.id().toInt()); const auto row = resultIndex.row(); const auto value = m_model->fieldValue(row, field); // set the value if available if (!value.isEmpty()) { tagEdit->setValue(field, value, previousValueHandling); continue; } // cover and lyrics might be fetched asynchronously switch (field) { case KnownField::Cover: if (m_model->fetchCover(resultIndex)) { // cover is available now tagEdit->setValue(KnownField::Cover, m_model->fieldValue(row, KnownField::Cover), previousValueHandling); continue; } // cover is fetched asynchronously // -> show status m_ui->notificationLabel->setNotificationType(NotificationType::Progress); m_ui->notificationLabel->appendLine(tr("Retrieving cover art to be applied ...")); setStatus(false); // -> apply cover when available connect(m_model, &QueryResultsModel::coverAvailable, [this, row, previousValueHandling](const QModelIndex &index) { if (row != index.row()) { return; } if (auto *const tagEdit = m_tagEditorWidget->activeTagEdit()) { tagEdit->setValue(KnownField::Cover, m_model->fieldValue(row, KnownField::Cover), previousValueHandling); } }); break; case KnownField::Lyrics: if (m_model->fetchLyrics(resultIndex)) { // lyrics are available now tagEdit->setValue(KnownField::Lyrics, m_model->fieldValue(row, KnownField::Lyrics), previousValueHandling); continue; } // lyrics are fetched asynchronously // -> show status m_ui->notificationLabel->setNotificationType(NotificationType::Progress); m_ui->notificationLabel->appendLine(tr("Retrieving lyrics to be applied ...")); setStatus(false); // -> apply cover when available connect(m_model, &QueryResultsModel::lyricsAvailable, [this, row, previousValueHandling](const QModelIndex &index) { if (row != index.row()) { return; } if (auto *const tagEdit = m_tagEditorWidget->activeTagEdit()) { tagEdit->setValue(KnownField::Lyrics, m_model->fieldValue(row, KnownField::Lyrics), previousValueHandling); } }); break; default:; } } } void DbQueryWidget::insertSearchTermsFromActiveTagEdit() { insertSearchTermsFromTagEdit(m_tagEditorWidget->activeTagEdit()); } void DbQueryWidget::showResultsContextMenu() { const auto *const selectionModel = m_ui->resultsTreeView->selectionModel(); if (!selectionModel) { return; } const auto selection = selectionModel->selection().indexes(); if (selection.isEmpty()) { return; } QMenu contextMenu; if (m_ui->applyPushButton->isEnabled()) { contextMenu.addAction(m_ui->applyPushButton->icon(), tr("Use selected row"), this, static_cast(&DbQueryWidget::applySelectedResults)); } if (m_model && m_model->areResultsAvailable()) { if (!contextMenu.isEmpty()) { contextMenu.addSeparator(); } contextMenu.addAction( QIcon::fromTheme(QStringLiteral("view-preview")), tr("Show cover"), this, &DbQueryWidget::fetchAndShowCoverForSelection); contextMenu.addAction( QIcon::fromTheme(QStringLiteral("view-media-lyrics")), tr("Show lyrics"), this, &DbQueryWidget::fetchAndShowLyricsForSelection); contextMenu.addAction( QIcon::fromTheme(QStringLiteral("internet-web-browser")), tr("Show in browser"), this, &DbQueryWidget::openSelectionInBrowser); } contextMenu.exec(QCursor::pos()); } void DbQueryWidget::fetchAndShowCoverForSelection() { const auto selectedIndex = this->selectedIndex(); if (!selectedIndex.isValid()) { return; } if (const QByteArray *const cover = m_model->cover(selectedIndex)) { showCover(*cover); return; } if (m_model->fetchCover(selectedIndex)) { if (const QByteArray *const cover = m_model->cover(selectedIndex)) { showCover(*cover); } else { // cover couldn't be fetched, error tracked via resultsAvailable() signal so nothing to do } } else { // cover is fetched asynchronously // -> memorize index to be shown m_coverIndex = selectedIndex.row(); // -> show status m_ui->notificationLabel->setNotificationType(NotificationType::Progress); m_ui->notificationLabel->setText(tr("Retrieving cover art ...")); setStatus(false); } } void DbQueryWidget::fetchAndShowLyricsForSelection() { const auto selectedIndex = this->selectedIndex(); if (!selectedIndex.isValid()) { return; } if (const QString *const lyrics = m_model->lyrics(selectedIndex)) { showLyrics(*lyrics); return; } if (m_model->fetchLyrics(selectedIndex)) { if (const QByteArray *cover = m_model->cover(selectedIndex)) { showLyrics(*cover); } else { // lyrics couldn't be fetched, error tracked via resultsAvailable() signal so nothing to do } } else { // lyrics are fetched asynchronously // -> memorize index to be shown m_lyricsIndex = selectedIndex.row(); // -> show status m_ui->notificationLabel->setNotificationType(NotificationType::Progress); m_ui->notificationLabel->setText(tr("Retrieving lyrics ...")); setStatus(false); } } void DbQueryWidget::openSelectionInBrowser() { const auto selectedIndex = this->selectedIndex(); if (!selectedIndex.isValid()) { return; } const auto url = m_model->webUrl(selectedIndex); if (url.isEmpty()) { m_ui->notificationLabel->appendLine(tr("No web URL available.")); return; } if (!QDesktopServices::openUrl(url)) { m_ui->notificationLabel->appendLine(tr("Unable to open URL: ") + url.toString()); } } void DbQueryWidget::showCover(const QByteArray &data) { QDialog dlg; dlg.setWindowFlags(Qt::Tool); dlg.setWindowTitle(tr("Cover - %1").arg(QApplication::applicationName())); QBoxLayout layout(QBoxLayout::Up); layout.setMargin(0); QGraphicsView view(&dlg); QGraphicsScene scene; layout.addWidget(&view); scene.addItem(new QGraphicsPixmapItem(QPixmap::fromImage(QImage::fromData(data)))); view.setScene(&scene); view.show(); dlg.setLayout(&layout); dlg.exec(); } void DbQueryWidget::showCoverFromIndex(const QModelIndex &index) { if (m_coverIndex == index.row()) { m_coverIndex = -1; showCover(*m_model->cover(index)); } } void DbQueryWidget::showLyrics(const QString &data) { QDialog dlg; dlg.setWindowFlags(Qt::Tool); dlg.setWindowTitle(tr("Lyrics - %1").arg(QApplication::applicationName())); QBoxLayout layout(QBoxLayout::Up); layout.setMargin(0); QTextBrowser textBrowser; layout.addWidget(&textBrowser); textBrowser.setText(data); dlg.setLayout(&layout); dlg.resize(400, 400); dlg.exec(); } void DbQueryWidget::showLyricsFromIndex(const QModelIndex &index) { if (m_lyricsIndex == index.row()) { m_lyricsIndex = -1; showLyrics(*m_model->lyrics(index)); } } void DbQueryWidget::clearSearchCriteria() { m_ui->titleLineEdit->clear(); m_ui->albumLineEdit->clear(); m_ui->artistLineEdit->clear(); m_ui->trackSpinBox->setValue(0); } bool DbQueryWidget::eventFilter(QObject *obj, QEvent *event) { if (obj == m_ui->searchGroupBox) { switch (event->type()) { case QEvent::KeyRelease: switch (static_cast(event)->key()) { case Qt::Key_Return: searchMusicBrainz(); break; default:; } break; default:; } } return QWidget::eventFilter(obj, event); } void DbQueryWidget::useQueryResults(QueryResultsModel *queryResults) { m_ui->resultsTreeView->setModel(m_model = queryResults); connect(queryResults, &QueryResultsModel::resultsAvailable, this, &DbQueryWidget::showResults); connect(queryResults, &QueryResultsModel::lyricsAvailable, this, &DbQueryWidget::showLyricsFromIndex); connect(queryResults, &QueryResultsModel::coverAvailable, this, &DbQueryWidget::showCoverFromIndex); } QModelIndex DbQueryWidget::selectedIndex() const { if (!m_model) { return QModelIndex(); } const auto *const selectionModel = m_ui->resultsTreeView->selectionModel(); if (!selectionModel) { return QModelIndex(); } const auto selection = selectionModel->selectedRows(); if (selection.size() != 1) { return QModelIndex(); } return selection.at(0); } } // namespace QtGui