diff --git a/src/widget/wsearchrelatedtracksmenu.cpp b/src/widget/wsearchrelatedtracksmenu.cpp index 04c2c1791f15..866cbc943072 100644 --- a/src/widget/wsearchrelatedtracksmenu.cpp +++ b/src/widget/wsearchrelatedtracksmenu.cpp @@ -1,10 +1,15 @@ #include "widget/wsearchrelatedtracksmenu.h" +#include +#include #include +#include +#include #include "moc_wsearchrelatedtracksmenu.cpp" #include "track/track.h" #include "util/math.h" +#include "util/parented_ptr.h" #include "util/qt.h" #include "util/widgethelper.h" @@ -78,12 +83,29 @@ void WSearchRelatedTracksMenu::addTriggerSearchAction( elideActionText( actionTextPrefix, elidableTextSuffix); - addAction( + + auto pCheckBox = make_parented( mixxx::escapeTextPropertyWithoutShortcuts(elidedActionText), + this); + pCheckBox->setProperty("query", searchQuery); + connect(pCheckBox.get(), + &QCheckBox::toggled, + this, + &WSearchRelatedTracksMenu::updateSearchButton); + // Use the event filter to capture clicks on the checkbox label + pCheckBox.get()->installEventFilter(this); + + auto pAction = make_parented(this); + pAction->setDefaultWidget(pCheckBox.get()); + // While the checkbox is selected (via keyboard, not hovered by pointer) + // pressing Space will toggle it whereas pressing Return triggers the action. + connect(pAction.get(), + &QAction::triggered, this, [this, searchQuery]() { emit triggerSearch(searchQuery); }); + addAction(pAction.get()); } QString WSearchRelatedTracksMenu::elideActionText( @@ -124,6 +146,13 @@ void WSearchRelatedTracksMenu::addActionsForTrack( // string concatenation will fail at runtime! // Mixing actions + // TODO(ronso0) Nice to have: remember & restore checked state of filters. + // This would simplify repeating the previous filter combo, e.g. search again + // for genre and BPM. + // For good UX (in decks) all track menu widgets of a certain deck should + // share a track menu (track menu features are/should be the same anyway for + // WTrackProperty, WTrackText & WTrackWidgetGroup. + // LegacySkinParser might take care of this :| bool addSeparatorBeforeNextAction = !isEmpty(); { const auto keyText = track.getKeyText(); @@ -332,4 +361,84 @@ void WSearchRelatedTracksMenu::addActionsForTrack( locationPathWithTerminator); } } + + addSeparator(); + + m_pSearchAction = make_parented(tr("&Search"), this); + // TODO(ronso0) Add 'looking glass' icon? + addAction(m_pSearchAction.get()); + m_pSearchAction.get()->setDisabled(true); + connect(m_pSearchAction.get(), + &QAction::triggered, + this, + &WSearchRelatedTracksMenu::combineQueriesTriggerSearch); +} + +bool WSearchRelatedTracksMenu::eventFilter(QObject* obj, QEvent* e) { + if (e->type() == QEvent::MouseButtonPress) { + // Clicking the text of a checkbox triggers the search, ignoring other + // checked boxes. + // Clicks in other places are passed on to the event filter so toggling + // the checkbox is happening as usual. + QCheckBox* box = qobject_cast(obj); + if (box) { + QMouseEvent* me = static_cast(e); + VERIFY_OR_DEBUG_ASSERT(me) { + return true; + } + QStyleOptionButton option; + option.initFrom(box); + auto pStyle = box->style(); + if (!pStyle) { + return true; + } + const QRect labelRect = pStyle->subElementRect(QStyle::SE_CheckBoxContents, + &option, + box); + if (labelRect.contains(me->pos())) { + // Text was clicked, trigger the search. + const QString query = box->property("query").toString(); + emit triggerSearch(query); + // Note that this click will not emit QAction::triggered like + // when pressing Return on a selected action, hence we need to + // make sure WTrackMenu closes when receiving triggerSearch(). + } + } + } + return QObject::eventFilter(obj, e); +} + +void WSearchRelatedTracksMenu::updateSearchButton() { + // Enable the Search button if at least one box is checked. + VERIFY_OR_DEBUG_ASSERT(m_pSearchAction) { + return; + } + m_pSearchAction->setDisabled(true); + for (const auto* child : std::as_const(children())) { + const auto* box = qobject_cast(child); + if (box && box->isChecked()) { + m_pSearchAction->setEnabled(true); + break; + } + } +} + +void WSearchRelatedTracksMenu::combineQueriesTriggerSearch() { + // collect queries of all checked checkboxes + QStringList queries; + for (const auto* child : std::as_const(children())) { + const auto* box = qobject_cast(child); + if (box && box->isChecked()) { + QString query = box->property("query").toString(); + if (!query.isEmpty()) { + queries.append(query); + } + } + } + if (queries.isEmpty()) { + return; + } else { + QString queryCombo = queries.join(QStringLiteral(" ")); + emit triggerSearch(queryCombo); + } } diff --git a/src/widget/wsearchrelatedtracksmenu.h b/src/widget/wsearchrelatedtracksmenu.h index 206123547108..88de42c552be 100644 --- a/src/widget/wsearchrelatedtracksmenu.h +++ b/src/widget/wsearchrelatedtracksmenu.h @@ -2,6 +2,8 @@ #include +#include "util/parented_ptr.h" + class Track; class WSearchRelatedTracksMenu : public QMenu { @@ -13,11 +15,16 @@ class WSearchRelatedTracksMenu : public QMenu { void addActionsForTrack( const Track& track); + bool eventFilter(QObject* obj, QEvent* e) override; signals: void triggerSearch( const QString& searchQuery); + private slots: + void updateSearchButton(); + void combineQueriesTriggerSearch(); + private: void addTriggerSearchAction( bool* /*in/out*/ pAddSeparatorBeforeNextAction, @@ -27,4 +34,6 @@ class WSearchRelatedTracksMenu : public QMenu { QString elideActionText( const QString& actionTextPrefix, const QString& elidableTextSuffix) const; + + parented_ptr m_pSearchAction; }; diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index a4aa90a63ee0..d65c482aea45 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -192,6 +192,7 @@ void WTrackMenu::createMenus() { this, [this](const QString& searchQuery) { m_pLibrary->searchTracksInCollection(searchQuery); + hide(); }); }