From 537130c4ecefc8f66172f81d44146ccf563d3336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Wed, 21 Jan 2026 22:17:10 +0100 Subject: [PATCH 01/22] Move track filtering widgets to a separate .h/.cpp Introduce files "trackfilterwidgets.h" and "trackfilterwidgets.cpp" and move the following classes there: - FilterPickerWidget - FilterLineWidget - FiltersListWidget --- src/CMakeLists.txt | 2 + src/desktop-remote/collectionwidget.cpp | 234 +------------------ src/desktop-remote/collectionwidget.h | 65 ------ src/desktop-remote/trackfilterwidgets.cpp | 259 ++++++++++++++++++++++ src/desktop-remote/trackfilterwidgets.h | 96 ++++++++ 5 files changed, 358 insertions(+), 298 deletions(-) create mode 100644 src/desktop-remote/trackfilterwidgets.cpp create mode 100644 src/desktop-remote/trackfilterwidgets.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6f9ae0fc..49492995 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -234,6 +234,7 @@ set(PMP_DESKTOP_REMOTE_SOURCES desktop-remote/scrobblingauthenticationdialog.cpp desktop-remote/searchdata.cpp desktop-remote/searchdialog.cpp + desktop-remote/trackfilterwidgets.cpp desktop-remote/trackinfodialog.cpp desktop-remote/trackjudge.cpp desktop-remote/trackprogresswidget.cpp @@ -267,6 +268,7 @@ set(PMP_DESKTOP_REMOTE_HEADERS desktop-remote/scrobblingauthenticationdialog.h desktop-remote/searchdata.h desktop-remote/searchdialog.h + desktop-remote/trackfilterwidgets.h desktop-remote/trackinfodialog.h desktop-remote/trackprogresswidget.h desktop-remote/useraccountcreationwidget.h diff --git a/src/desktop-remote/collectionwidget.cpp b/src/desktop-remote/collectionwidget.cpp index 68da1749..76e106db 100644 --- a/src/desktop-remote/collectionwidget.cpp +++ b/src/desktop-remote/collectionwidget.cpp @@ -20,8 +20,6 @@ #include "collectionwidget.h" #include "ui_collectionwidget.h" -#include "common/unicodechars.h" - #include "client/collectionwatcher.h" #include "client/queuecontroller.h" #include "client/serverinterface.h" @@ -29,6 +27,7 @@ #include "collectiontablemodel.h" #include "colors.h" #include "colorswitcher.h" +#include "trackfilterwidgets.h" #include "trackinfodialog.h" #include "waitingspinnerwidget.h" @@ -336,235 +335,4 @@ namespace PMP _colorSwitcher->setColors(colors.itemBackgroundHighlightColors); _usingColorsForDarkMode = darkMode; } - - // =============================================================== // - - FilterPickerWidget::FilterPickerWidget(PredefinedTrackCriterium criteriumForEmpty, - QString captionForEmpty) - : _predefinedCriterium(criteriumForEmpty), - _criterium(convertToTrackCriterium(criteriumForEmpty)) - { - _comboBox = new QComboBox(); - - QHBoxLayout* layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(_comboBox); - - fillTrackCriteriaComboBox(_comboBox, criteriumForEmpty, captionForEmpty); - - connect( - _comboBox, qOverload(&QComboBox::currentIndexChanged), - this, - [this]() - { - auto predefinedCriterium = - _comboBox->currentData().value(); - - if (_predefinedCriterium == predefinedCriterium) - return; - - _predefinedCriterium = predefinedCriterium; - _criterium = convertToTrackCriterium(predefinedCriterium); - Q_EMIT criteriumChanged(); - } - ); - } - - void FilterPickerWidget::clearCriterium() - { - _comboBox->setCurrentIndex(0); - } - - void FilterPickerWidget::fillTrackCriteriaComboBox(QComboBox* comboBox, - PredefinedTrackCriterium criteriumForEmpty, - QString captionForEmpty) - { - auto addItem = - [comboBox](QString text, PredefinedTrackCriterium mode) - { - text.replace(">=", UnicodeChars::greaterThanOrEqual) - .replace("<=", UnicodeChars::lessThanOrEqual); - - comboBox->addItem(text, QVariant::fromValue(mode)); - }; - - addItem(captionForEmpty, criteriumForEmpty); - - addItem(tr("never heard"), PredefinedTrackCriterium::NeverHeard); - addItem(tr("not heard in the last 5 years"), - PredefinedTrackCriterium::NotHeardInLast5Years); - addItem(tr("not heard in the last 3 years"), - PredefinedTrackCriterium::NotHeardInLast3Years); - addItem(tr("not heard in the last 2 years"), - PredefinedTrackCriterium::NotHeardInLast2Years); - addItem(tr("not heard in the last year"), - PredefinedTrackCriterium::NotHeardInLastYear); - addItem(tr("not heard in the last 180 days"), - PredefinedTrackCriterium::NotHeardInLast180Days); - addItem(tr("not heard in the last 90 days"), - PredefinedTrackCriterium::NotHeardInLast90Days); - addItem(tr("not heard in the last 30 days"), - PredefinedTrackCriterium::NotHeardInLast30Days); - addItem(tr("not heard in the last 10 days"), - PredefinedTrackCriterium::NotHeardInLast10Days); - addItem(tr("heard at least once"), PredefinedTrackCriterium::HeardAtLeastOnce); - - addItem(tr("without score"), PredefinedTrackCriterium::WithoutScore); - addItem(tr("with score"), PredefinedTrackCriterium::WithScore); - addItem(tr("score < 30"), PredefinedTrackCriterium::ScoreLessThan30); - addItem(tr("score < 50"), PredefinedTrackCriterium::ScoreLessThan50); - addItem(tr("score >= 80"), PredefinedTrackCriterium::ScoreAtLeast80); - addItem(tr("score >= 85"), PredefinedTrackCriterium::ScoreAtLeast85); - addItem(tr("score >= 90"), PredefinedTrackCriterium::ScoreAtLeast90); - addItem(tr("score >= 95"), PredefinedTrackCriterium::ScoreAtLeast95); - - addItem(tr("length < 1 min."), PredefinedTrackCriterium::LengthLessThanOneMinute); - addItem(tr("length >= 1 min."), PredefinedTrackCriterium::LengthAtLeastOneMinute); - addItem(tr("length < 2 min."), PredefinedTrackCriterium::LengthLessThanTwoMinutes); - addItem(tr("length >= 2 min."), PredefinedTrackCriterium::LengthAtLeastTwoMinutes); - addItem(tr("length < 3 min."), PredefinedTrackCriterium::LengthLessThanThreeMinutes); - addItem(tr("length >= 3 min."), PredefinedTrackCriterium::LengthAtLeastThreeMinutes); - addItem(tr("length < 4 min."), PredefinedTrackCriterium::LengthLessThanFourMinutes); - addItem(tr("length >= 4 min."), PredefinedTrackCriterium::LengthAtLeastFourMinutes); - addItem(tr("length < 5 min."), PredefinedTrackCriterium::LengthLessThanFiveMinutes); - addItem(tr("length >= 5 min."), PredefinedTrackCriterium::LengthAtLeastFiveMinutes); - - addItem(tr("not in the queue"), PredefinedTrackCriterium::NotInTheQueue); - addItem(tr("in the queue"), PredefinedTrackCriterium::InTheQueue); - - addItem(tr("without title"), PredefinedTrackCriterium::WithoutTitle); - addItem(tr("without artist"), PredefinedTrackCriterium::WithoutArtist); - addItem(tr("without album"), PredefinedTrackCriterium::WithoutAlbum); - - addItem(tr("no longer available"), PredefinedTrackCriterium::NoLongerAvailable); - - comboBox->setCurrentIndex(0); - } - - // =============================================================== // - - FilterLineWidget::FilterLineWidget() - { - _filterPicker = new FilterPickerWidget(PredefinedTrackCriterium::AllTracks, - tr("(empty)")); - _criterium = _filterPicker->criterium().clone(); - _deleteButton = new QPushButton(); - _resetButton = new QPushButton(); - - QHBoxLayout* layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(_filterPicker, 1); - layout->addWidget(_deleteButton, 0); - layout->addWidget(_resetButton, 0); - - _deleteButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); - _deleteButton->setToolTip(tr("Remove filter")); - - _resetButton->setIcon(style()->standardIcon(QStyle::SP_LineEditClearButton)); - _resetButton->setToolTip(tr("Clear filter")); - - connect( - _filterPicker, &FilterPickerWidget::criteriumChanged, - [this]() - { - _criterium = _filterPicker->criterium().clone(); - Q_EMIT criteriumChanged(); - } - ); - - connect( - _deleteButton, &QPushButton::clicked, - this, [this]() { Q_EMIT deleteClicked(); } - ); - - connect( - _resetButton, &QPushButton::clicked, - this, [this]() { _filterPicker->clearCriterium(); } - ); - } - - // =============================================================== // - - FiltersListWidget::FiltersListWidget() - : _criterium(ConstantTrackCriterium::allTracksMatch()) - { - _verticalLayout = new QVBoxLayout(this); - _verticalLayout->setContentsMargins(0, 0, 0, 0); - - addFilterLine(); - - auto* buttonsLayout = new QHBoxLayout(); - _verticalLayout->addLayout(buttonsLayout); - - _addButton = new QPushButton(tr("Add")); - _addButton->setToolTip(tr("Add filter")); - - buttonsLayout->addWidget(_addButton); - buttonsLayout->addStretch(); - - connect( - _addButton, &QPushButton::clicked, - this, - [this]() - { - addFilterLine(); - - // emit is not necessary because the new filter is "none" - //Q_EMIT criteriaChanged(); - } - ); - } - - void FiltersListWidget::addFilterLine() - { - auto* filter = new FilterLineWidget(); - - auto index = _filters.size(); - _verticalLayout->insertWidget(index, filter); - - _filters.append(filter); - rebuildCriterium(); - - connect( - filter, &FilterLineWidget::criteriumChanged, - this, - [this]() - { - rebuildCriterium(); - Q_EMIT criteriumChanged(); - } - ); - - connect( - filter, &FilterLineWidget::deleteClicked, - this, - [this, filter]() - { - auto index = _filters.indexOf(filter); - Q_ASSERT_X( - index >= 0, - "FiltersListWidget::addFilterLine", - "filter to be deleted not found" - ); - - _filters.removeAt(index); - filter->deleteLater(); - - rebuildCriterium(); - Q_EMIT criteriumChanged(); - } - ); - } - - void FiltersListWidget::rebuildCriterium() - { - auto compositeCriterium = std::make_unique(); - - for (auto const* filterLine : _filters) - { - compositeCriterium->add(filterLine->criterium().clone()); - } - - _criterium = std::move(compositeCriterium); - } } diff --git a/src/desktop-remote/collectionwidget.h b/src/desktop-remote/collectionwidget.h index dce33f92..4aaa8bfc 100644 --- a/src/desktop-remote/collectionwidget.h +++ b/src/desktop-remote/collectionwidget.h @@ -24,10 +24,7 @@ #include -QT_FORWARD_DECLARE_CLASS(QComboBox) QT_FORWARD_DECLARE_CLASS(QMenu) -QT_FORWARD_DECLARE_CLASS(QPushButton) -QT_FORWARD_DECLARE_CLASS(QVBoxLayout) namespace Ui { @@ -90,67 +87,5 @@ namespace PMP QMenu* _collectionContextMenu; bool _usingColorsForDarkMode { false }; }; - - class FilterPickerWidget : public QWidget - { - Q_OBJECT - public: - FilterPickerWidget(PredefinedTrackCriterium criteriumForEmpty, QString captionForEmpty); - - void clearCriterium(); - const TrackCriterium& criterium() const { return *_criterium; } - - Q_SIGNALS: - void criteriumChanged(); - - private: - void fillTrackCriteriaComboBox(QComboBox* comboBox, - PredefinedTrackCriterium criteriumForEmpty, - QString captionForEmpty); - - QComboBox* _comboBox; - PredefinedTrackCriterium _predefinedCriterium; - std::unique_ptr _criterium; - }; - - class FilterLineWidget : public QWidget - { - Q_OBJECT - public: - FilterLineWidget(); - - const TrackCriterium& criterium() const { return *_criterium; } - - Q_SIGNALS: - void criteriumChanged(); - void deleteClicked(); - - private: - FilterPickerWidget* _filterPicker; - QPushButton* _deleteButton; - QPushButton* _resetButton; - std::unique_ptr _criterium; - }; - - class FiltersListWidget : public QWidget - { - Q_OBJECT - public: - FiltersListWidget(); - - const TrackCriterium& criterium() const { return *_criterium; } - - Q_SIGNALS: - void criteriumChanged(); - - private: - void addFilterLine(); - void rebuildCriterium(); - - QPushButton* _addButton; - QVBoxLayout* _verticalLayout; - QList _filters; - std::unique_ptr _criterium; - }; } #endif diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp new file mode 100644 index 00000000..99ca0dc6 --- /dev/null +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -0,0 +1,259 @@ +/* + Copyright (C) 2025-2026, Kevin André + + This file is part of PMP (Party Music Player). + + PMP is free software: you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation, either version 3 of the License, or (at your option) any later + version. + + PMP is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along + with PMP. If not, see . +*/ + +#include "trackfilterwidgets.h" + +#include "common/unicodechars.h" + +#include +#include +#include +#include + +namespace PMP +{ + FilterPickerWidget::FilterPickerWidget(PredefinedTrackCriterium criteriumForEmpty, + QString captionForEmpty) + : _predefinedCriterium(criteriumForEmpty), + _criterium(convertToTrackCriterium(criteriumForEmpty)) + { + _comboBox = new QComboBox(); + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_comboBox); + + fillTrackCriteriaComboBox(_comboBox, criteriumForEmpty, captionForEmpty); + + connect( + _comboBox, qOverload(&QComboBox::currentIndexChanged), + this, + [this]() + { + auto predefinedCriterium = + _comboBox->currentData().value(); + + if (_predefinedCriterium == predefinedCriterium) + return; + + _predefinedCriterium = predefinedCriterium; + _criterium = convertToTrackCriterium(predefinedCriterium); + Q_EMIT criteriumChanged(); + } + ); + } + + void FilterPickerWidget::clearCriterium() + { + _comboBox->setCurrentIndex(0); + } + + void FilterPickerWidget::fillTrackCriteriaComboBox(QComboBox* comboBox, + PredefinedTrackCriterium criteriumForEmpty, + QString captionForEmpty) + { + auto addItem = + [comboBox](QString text, PredefinedTrackCriterium mode) + { + text.replace(">=", UnicodeChars::greaterThanOrEqual) + .replace("<=", UnicodeChars::lessThanOrEqual); + + comboBox->addItem(text, QVariant::fromValue(mode)); + }; + + addItem(captionForEmpty, criteriumForEmpty); + + addItem(tr("never heard"), PredefinedTrackCriterium::NeverHeard); + addItem(tr("not heard in the last 5 years"), + PredefinedTrackCriterium::NotHeardInLast5Years); + addItem(tr("not heard in the last 3 years"), + PredefinedTrackCriterium::NotHeardInLast3Years); + addItem(tr("not heard in the last 2 years"), + PredefinedTrackCriterium::NotHeardInLast2Years); + addItem(tr("not heard in the last year"), + PredefinedTrackCriterium::NotHeardInLastYear); + addItem(tr("not heard in the last 180 days"), + PredefinedTrackCriterium::NotHeardInLast180Days); + addItem(tr("not heard in the last 90 days"), + PredefinedTrackCriterium::NotHeardInLast90Days); + addItem(tr("not heard in the last 30 days"), + PredefinedTrackCriterium::NotHeardInLast30Days); + addItem(tr("not heard in the last 10 days"), + PredefinedTrackCriterium::NotHeardInLast10Days); + addItem(tr("heard at least once"), PredefinedTrackCriterium::HeardAtLeastOnce); + + addItem(tr("without score"), PredefinedTrackCriterium::WithoutScore); + addItem(tr("with score"), PredefinedTrackCriterium::WithScore); + addItem(tr("score < 30"), PredefinedTrackCriterium::ScoreLessThan30); + addItem(tr("score < 50"), PredefinedTrackCriterium::ScoreLessThan50); + addItem(tr("score >= 80"), PredefinedTrackCriterium::ScoreAtLeast80); + addItem(tr("score >= 85"), PredefinedTrackCriterium::ScoreAtLeast85); + addItem(tr("score >= 90"), PredefinedTrackCriterium::ScoreAtLeast90); + addItem(tr("score >= 95"), PredefinedTrackCriterium::ScoreAtLeast95); + + addItem(tr("length < 1 min."), PredefinedTrackCriterium::LengthLessThanOneMinute); + addItem(tr("length >= 1 min."), PredefinedTrackCriterium::LengthAtLeastOneMinute); + addItem(tr("length < 2 min."), PredefinedTrackCriterium::LengthLessThanTwoMinutes); + addItem(tr("length >= 2 min."), PredefinedTrackCriterium::LengthAtLeastTwoMinutes); + addItem(tr("length < 3 min."), PredefinedTrackCriterium::LengthLessThanThreeMinutes); + addItem(tr("length >= 3 min."), PredefinedTrackCriterium::LengthAtLeastThreeMinutes); + addItem(tr("length < 4 min."), PredefinedTrackCriterium::LengthLessThanFourMinutes); + addItem(tr("length >= 4 min."), PredefinedTrackCriterium::LengthAtLeastFourMinutes); + addItem(tr("length < 5 min."), PredefinedTrackCriterium::LengthLessThanFiveMinutes); + addItem(tr("length >= 5 min."), PredefinedTrackCriterium::LengthAtLeastFiveMinutes); + + addItem(tr("not in the queue"), PredefinedTrackCriterium::NotInTheQueue); + addItem(tr("in the queue"), PredefinedTrackCriterium::InTheQueue); + + addItem(tr("without title"), PredefinedTrackCriterium::WithoutTitle); + addItem(tr("without artist"), PredefinedTrackCriterium::WithoutArtist); + addItem(tr("without album"), PredefinedTrackCriterium::WithoutAlbum); + + addItem(tr("no longer available"), PredefinedTrackCriterium::NoLongerAvailable); + + comboBox->setCurrentIndex(0); + } + + // =============================================================== // + + FilterLineWidget::FilterLineWidget() + { + _filterPicker = new FilterPickerWidget(PredefinedTrackCriterium::AllTracks, + tr("(empty)")); + _criterium = _filterPicker->criterium().clone(); + _deleteButton = new QPushButton(); + _resetButton = new QPushButton(); + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_filterPicker, 1); + layout->addWidget(_deleteButton, 0); + layout->addWidget(_resetButton, 0); + + _deleteButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); + _deleteButton->setToolTip(tr("Remove filter")); + + _resetButton->setIcon(style()->standardIcon(QStyle::SP_LineEditClearButton)); + _resetButton->setToolTip(tr("Clear filter")); + + connect( + _filterPicker, &FilterPickerWidget::criteriumChanged, + [this]() + { + _criterium = _filterPicker->criterium().clone(); + Q_EMIT criteriumChanged(); + } + ); + + connect( + _deleteButton, &QPushButton::clicked, + this, [this]() { Q_EMIT deleteClicked(); } + ); + + connect( + _resetButton, &QPushButton::clicked, + this, [this]() { _filterPicker->clearCriterium(); } + ); + } + + // =============================================================== // + + FiltersListWidget::FiltersListWidget() + : _criterium(ConstantTrackCriterium::allTracksMatch()) + { + _verticalLayout = new QVBoxLayout(this); + _verticalLayout->setContentsMargins(0, 0, 0, 0); + + addFilterLine(); + + auto* buttonsLayout = new QHBoxLayout(); + _verticalLayout->addLayout(buttonsLayout); + + _addButton = new QPushButton(tr("Add")); + _addButton->setToolTip(tr("Add filter")); + + buttonsLayout->addWidget(_addButton); + buttonsLayout->addStretch(); + + connect( + _addButton, &QPushButton::clicked, + this, + [this]() + { + addFilterLine(); + + // emit is not necessary because the new filter is "none" + //Q_EMIT criteriaChanged(); + } + ); + } + + void FiltersListWidget::addFilterLine() + { + auto* filter = new FilterLineWidget(); + + auto index = _filters.size(); + _verticalLayout->insertWidget(index, filter); + + _filters.append(filter); + rebuildCriterium(); + + connect( + filter, &FilterLineWidget::criteriumChanged, + this, + [this]() + { + rebuildCriterium(); + Q_EMIT criteriumChanged(); + } + ); + + connect( + filter, &FilterLineWidget::deleteClicked, + this, + [this, filter]() + { + auto index = _filters.indexOf(filter); + Q_ASSERT_X( + index >= 0, + "FiltersListWidget::addFilterLine", + "filter to be deleted not found" + ); + + _filters.removeAt(index); + filter->deleteLater(); + + rebuildCriterium(); + Q_EMIT criteriumChanged(); + } + ); + } + + void FiltersListWidget::rebuildCriterium() + { + auto compositeCriterium = std::make_unique(); + + for (auto const* filterLine : _filters) + { + compositeCriterium->add(filterLine->criterium().clone()); + } + + _criterium = std::move(compositeCriterium); + } +} diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h new file mode 100644 index 00000000..be1d597b --- /dev/null +++ b/src/desktop-remote/trackfilterwidgets.h @@ -0,0 +1,96 @@ +/* + Copyright (C) 2025-2026, Kevin André + + This file is part of PMP (Party Music Player). + + PMP is free software: you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation, either version 3 of the License, or (at your option) any later + version. + + PMP is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along + with PMP. If not, see . +*/ + +#ifndef PMP_TRACKFILTERWIDGETS_H +#define PMP_TRACKFILTERWIDGETS_H + +#include "common/trackcriteria.h" + +#include + +QT_FORWARD_DECLARE_CLASS(QComboBox) +QT_FORWARD_DECLARE_CLASS(QPushButton) +QT_FORWARD_DECLARE_CLASS(QVBoxLayout) + +namespace PMP +{ + class FilterPickerWidget : public QWidget + { + Q_OBJECT + public: + FilterPickerWidget(PredefinedTrackCriterium criteriumForEmpty, + QString captionForEmpty); + + void clearCriterium(); + const TrackCriterium& criterium() const { return *_criterium; } + + Q_SIGNALS: + void criteriumChanged(); + + private: + void fillTrackCriteriaComboBox(QComboBox* comboBox, + PredefinedTrackCriterium criteriumForEmpty, + QString captionForEmpty); + + QComboBox* _comboBox; + PredefinedTrackCriterium _predefinedCriterium; + std::unique_ptr _criterium; + }; + + class FilterLineWidget : public QWidget + { + Q_OBJECT + public: + FilterLineWidget(); + + const TrackCriterium& criterium() const { return *_criterium; } + + Q_SIGNALS: + void criteriumChanged(); + void deleteClicked(); + + private: + FilterPickerWidget* _filterPicker; + QPushButton* _deleteButton; + QPushButton* _resetButton; + std::unique_ptr _criterium; + }; + + class FiltersListWidget : public QWidget + { + Q_OBJECT + public: + FiltersListWidget(); + + const TrackCriterium& criterium() const { return *_criterium; } + + Q_SIGNALS: + void criteriumChanged(); + + private: + void addFilterLine(); + void rebuildCriterium(); + + QPushButton* _addButton; + QVBoxLayout* _verticalLayout; + QList _filters; + std::unique_ptr _criterium; + }; +} +#endif From 264f91d16cd53755a17fb43f69498cc66ab4734a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Mon, 2 Feb 2026 22:05:29 +0100 Subject: [PATCH 02/22] Add foundation for track criterium editor widgets Add the foundation for editing/customizing the track criteria that are used for filtering the music collection. For now, only the track score comparison criterium supports editing. A predefined criterium like "score < 50" can be changed to an edit mode where both the operator and the number can be changed. The reset button can be used to revert to the list of predefined criteria. --- src/common/commonmetatypes.cpp | 1 + src/common/trackcriteria.h | 1 + src/common/unicodechars.h | 5 +- src/desktop-remote/collectiontablemodel.cpp | 9 + src/desktop-remote/collectionwidget.cpp | 13 +- src/desktop-remote/trackfilterwidgets.cpp | 386 ++++++++++++++++++-- src/desktop-remote/trackfilterwidgets.h | 102 +++++- 7 files changed, 475 insertions(+), 42 deletions(-) diff --git a/src/common/commonmetatypes.cpp b/src/common/commonmetatypes.cpp index 0e3f97b3..38597438 100644 --- a/src/common/commonmetatypes.cpp +++ b/src/common/commonmetatypes.cpp @@ -45,6 +45,7 @@ namespace PMP CommonMetatypesInit() { qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); diff --git a/src/common/trackcriteria.h b/src/common/trackcriteria.h index 15cbf220..24de0716 100644 --- a/src/common/trackcriteria.h +++ b/src/common/trackcriteria.h @@ -662,5 +662,6 @@ namespace PMP } Q_DECLARE_METATYPE(PMP::PredefinedTrackCriterium) +Q_DECLARE_METATYPE(PMP::ComparisonOperator) #endif diff --git a/src/common/unicodechars.h b/src/common/unicodechars.h index 5b1d764d..b07b695a 100644 --- a/src/common/unicodechars.h +++ b/src/common/unicodechars.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2016-2022, Kevin Andre + Copyright (C) 2016-2026, Kevin Andre This file is part of PMP (Party Music Player). @@ -50,6 +50,9 @@ namespace PMP /*! The "LESS-THAN OR EQUAL TO" symbol (U+2264) */ [[maybe_unused]] constexpr QChar lessThanOrEqual = QChar(0x2264); + /*! The inequality operator (U+2260) */ + [[maybe_unused]] constexpr QChar notEqual = QChar(0x2260); + /*! Pause symbol (U+23F8) */ [[maybe_unused]] constexpr QChar pauseSymbol = QChar(0x23F8); diff --git a/src/desktop-remote/collectiontablemodel.cpp b/src/desktop-remote/collectiontablemodel.cpp index c7a8ba16..b4df9d1c 100644 --- a/src/desktop-remote/collectiontablemodel.cpp +++ b/src/desktop-remote/collectiontablemodel.cpp @@ -934,10 +934,19 @@ namespace PMP void FilteredCollectionTableModel::setTrackFilters(const TrackCriterium& criterium) { + qDebug() << "Music collection: got filters changed signal"; + bool changed = _filteringTrackJudge.setCriterium(criterium); if (changed) + { + qDebug() << "filters were really changed"; invalidateFilter(); + } + else + { + qDebug() << "filters did not really change"; + } } void FilteredCollectionTableModel::sort(int column, Qt::SortOrder order) diff --git a/src/desktop-remote/collectionwidget.cpp b/src/desktop-remote/collectionwidget.cpp index 76e106db..ad0e18b6 100644 --- a/src/desktop-remote/collectionwidget.cpp +++ b/src/desktop-remote/collectionwidget.cpp @@ -149,21 +149,20 @@ namespace PMP void CollectionWidget::onFiltersChanged() { - _collectionDisplayModel->setTrackFilters(_filtersListWidget->criterium()); + auto criterium = _filtersListWidget->createCriterium(); + _collectionDisplayModel->setTrackFilters(*criterium); } void CollectionWidget::onHighlightCriteriumChanged() { + auto criterium = _highlightingCriteriumPicker->createCriterium(); + bool nothingToHighlight = - _highlightingCriteriumPicker->criterium().equals( - *ConstantTrackCriterium::noTracksMatch() - ); + criterium->equals(*ConstantTrackCriterium::noTracksMatch()); _colorSwitcher->setVisible(!nothingToHighlight); - _collectionSourceModel->setHighlightCriterium( - _highlightingCriteriumPicker->criterium() - ); + _collectionSourceModel->setHighlightCriterium(*criterium); } void CollectionWidget::highlightColorIndexChanged() diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 99ca0dc6..783779ff 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -19,19 +19,21 @@ #include "trackfilterwidgets.h" +#include "common/nullable.h" #include "common/unicodechars.h" #include #include +#include #include +#include #include namespace PMP { FilterPickerWidget::FilterPickerWidget(PredefinedTrackCriterium criteriumForEmpty, QString captionForEmpty) - : _predefinedCriterium(criteriumForEmpty), - _criterium(convertToTrackCriterium(criteriumForEmpty)) + : _predefinedCriterium(criteriumForEmpty) { _comboBox = new QComboBox(); @@ -53,7 +55,6 @@ namespace PMP return; _predefinedCriterium = predefinedCriterium; - _criterium = convertToTrackCriterium(predefinedCriterium); Q_EMIT criteriumChanged(); } ); @@ -64,6 +65,11 @@ namespace PMP _comboBox->setCurrentIndex(0); } + std::unique_ptr FilterPickerWidget::createCriterium() const + { + return convertToTrackCriterium(_predefinedCriterium); + } + void FilterPickerWidget::fillTrackCriteriaComboBox(QComboBox* comboBox, PredefinedTrackCriterium criteriumForEmpty, QString captionForEmpty) @@ -72,7 +78,7 @@ namespace PMP [comboBox](QString text, PredefinedTrackCriterium mode) { text.replace(">=", UnicodeChars::greaterThanOrEqual) - .replace("<=", UnicodeChars::lessThanOrEqual); + .replace("<=", UnicodeChars::lessThanOrEqual); comboBox->addItem(text, QVariant::fromValue(mode)); }; @@ -132,20 +138,295 @@ namespace PMP // =============================================================== // + namespace + { + void fillComboBoxWithComparisonOperators(QComboBox* comboBox) + { + comboBox->addItem("=", QVariant::fromValue(ComparisonOperator::Equal)); + + comboBox->addItem(UnicodeChars::notEqual, + QVariant::fromValue(ComparisonOperator::NotEqual)); + + comboBox->addItem("<", QVariant::fromValue(ComparisonOperator::LessThan)); + + comboBox->addItem(UnicodeChars::lessThanOrEqual, + QVariant::fromValue(ComparisonOperator::LessThanOrEqual)); + + comboBox->addItem(">", QVariant::fromValue(ComparisonOperator::GreaterThan)); + + comboBox->addItem(UnicodeChars::greaterThanOrEqual, + QVariant::fromValue(ComparisonOperator::GreaterThanOrEqual)); + } + + void selectValue(QComboBox* comboBox, ComparisonOperator op) + { + int index = -1; + switch (op) + { + case ComparisonOperator::Equal: + index = 0; + break; + case ComparisonOperator::NotEqual: + index = 1; + break; + case ComparisonOperator::LessThan: + index = 2; + break; + case ComparisonOperator::LessThanOrEqual: + index = 3; + break; + case ComparisonOperator::GreaterThan: + index = 4; + break; + case ComparisonOperator::GreaterThanOrEqual: + index = 5; + break; + } + + comboBox->setCurrentIndex(index); + } + + Nullable getSelectedComparisonOperator(QComboBox* comboBox) + { + if (comboBox->currentIndex() < 0) + return null; + + auto comparisonOperator = + comboBox->currentData().value(); + + return comparisonOperator; + } + } + + // =============================================================== // + + ScoreComparisonEditorWidget::ScoreComparisonEditorWidget(QWidget* parent) + : FilterEditorWidget(parent) + { + auto* scoreLabel = new QLabel(tr("score")); + _operatorComboBox = new QComboBox(); + _scoreSpinBox = new QSpinBox(); + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(scoreLabel); + layout->addWidget(_operatorComboBox); + layout->addWidget(_scoreSpinBox); + layout->addStretch(); + + fillComboBoxWithComparisonOperators(_operatorComboBox); + + connect( + _operatorComboBox, &QComboBox::currentIndexChanged, + this, &ScoreComparisonEditorWidget::criteriumChanged + ); + + connect( + _scoreSpinBox, &QSpinBox::valueChanged, + this, &ScoreComparisonEditorWidget::criteriumChanged + ); + } + + void ScoreComparisonEditorWidget::setOperator(ComparisonOperator comparisonOperator) + { + selectValue(_operatorComboBox, comparisonOperator); + } + + void ScoreComparisonEditorWidget::setScore(int score) + { + _scoreSpinBox->setValue(score); + } + + std::unique_ptr ScoreComparisonEditorWidget::createCriterium() const + { + auto comparisonOperator = getSelectedComparisonOperator(_operatorComboBox); + + if (comparisonOperator == null) + return ConstantTrackCriterium::noTracksMatch(); + + auto score = _scoreSpinBox->value(); + + return std::make_unique(comparisonOperator.value(), + score); + } + + // =============================================================== // + + bool FilterEditorFactory::isEditable(const TrackCriterium& criterium) + { + IsEditableVisitor visitor; + criterium.accept(visitor); + return visitor.isCriteriumEditable(); + } + + FilterEditorWidget* FilterEditorFactory::createEditor(QWidget* parent, + const TrackCriterium& criterium) + { + EditorWidgetCreationVisitor visitor(parent); + criterium.accept(visitor); + return visitor.editorWidget(); + } + + void FilterEditorFactory::IsEditableVisitor::visit(const ConstantTrackCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit( + const TrackLengthPresenceCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit( + const TrackLengthComparisonCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit(const TrackScorePresenceCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit( + const TrackScoreComparisonCriterium&) + { + _isEditable = true; + } + + void FilterEditorFactory::IsEditableVisitor::visit( + const TrackLastHeardPresenceCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit( + const TrackLastHeardRecentlyCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit(const TrackQueuePresenceCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit(const TrackAvailabilityCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit( + const TrackMetaDataPresenceCriterium&) + { + _isEditable = false; + } + + void FilterEditorFactory::IsEditableVisitor::visit(const CompositeTrackCriterium&) + { + _isEditable = false; + } + + FilterEditorFactory::EditorWidgetCreationVisitor::EditorWidgetCreationVisitor( + QWidget* parent) + : _parent(parent), + _editorWidget(nullptr) + { + // + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const ConstantTrackCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackLengthPresenceCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackLengthComparisonCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackScorePresenceCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackScoreComparisonCriterium& scoreComparisonCriterium) + { + auto* editor = new ScoreComparisonEditorWidget(_parent); + editor->setOperator(scoreComparisonCriterium.comparisonOperator()); + editor->setScore(scoreComparisonCriterium.score()); + + _editorWidget = editor; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackLastHeardPresenceCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackLastHeardRecentlyCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackQueuePresenceCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackAvailabilityCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const TrackMetaDataPresenceCriterium&) + { + _editorWidget = nullptr; + } + + void FilterEditorFactory::EditorWidgetCreationVisitor::visit( + const CompositeTrackCriterium&) + { + _editorWidget = nullptr; + } + + // =============================================================== // + FilterLineWidget::FilterLineWidget() + : _editorWidget(nullptr) { _filterPicker = new FilterPickerWidget(PredefinedTrackCriterium::AllTracks, tr("(empty)")); - _criterium = _filterPicker->criterium().clone(); + _editButton = new QPushButton(); _deleteButton = new QPushButton(); _resetButton = new QPushButton(); QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(_filterPicker, 1); + layout->addWidget(_editButton, 0); layout->addWidget(_deleteButton, 0); layout->addWidget(_resetButton, 0); + _editButton->setText(tr("Edit")); + _editButton->setEnabled(false); + _deleteButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); _deleteButton->setToolTip(tr("Remove filter")); @@ -154,11 +435,12 @@ namespace PMP connect( _filterPicker, &FilterPickerWidget::criteriumChanged, - [this]() - { - _criterium = _filterPicker->criterium().clone(); - Q_EMIT criteriumChanged(); - } + this, &FilterLineWidget::onPickerCriteriumChanged + ); + + connect( + _editButton, &QPushButton::clicked, + this, &FilterLineWidget::onEditClicked ); connect( @@ -168,14 +450,67 @@ namespace PMP connect( _resetButton, &QPushButton::clicked, - this, [this]() { _filterPicker->clearCriterium(); } + this, &FilterLineWidget::onResetClicked ); } + std::unique_ptr FilterLineWidget::createCriterium() const + { + if (_editorWidget) + return _editorWidget->createCriterium(); + + return _filterPicker->createCriterium(); + } + + void FilterLineWidget::onPickerCriteriumChanged() + { + if (_editorWidget) + return; + + auto criterium = _filterPicker->createCriterium(); + bool isEditable = FilterEditorFactory::isEditable(*criterium); + _editButton->setEnabled(isEditable); + + Q_EMIT criteriumChanged(); + } + + void FilterLineWidget::onEditClicked() + { + Q_ASSERT_X(_editorWidget == nullptr, + "FilterLineWidget::onEditClicked", + "editor already present!"); + + auto criterium = _filterPicker->createCriterium(); + + _editorWidget = FilterEditorFactory::createEditor(nullptr, *criterium); + + connect( + _editorWidget, &FilterEditorWidget::criteriumChanged, + this, &FilterLineWidget::criteriumChanged + ); + + layout()->replaceWidget(_filterPicker, _editorWidget); + _filterPicker->setVisible(false); + + _editButton->setEnabled(false); + } + + void FilterLineWidget::onResetClicked() + { + if (_editorWidget) + { + layout()->replaceWidget(_editorWidget, _filterPicker); + _editorWidget->deleteLater(); + _editorWidget = nullptr; + _filterPicker->setVisible(true); + } + + _filterPicker->clearCriterium(); + } + // =============================================================== // FiltersListWidget::FiltersListWidget() - : _criterium(ConstantTrackCriterium::allTracksMatch()) { _verticalLayout = new QVBoxLayout(this); _verticalLayout->setContentsMargins(0, 0, 0, 0); @@ -204,6 +539,18 @@ namespace PMP ); } + std::unique_ptr FiltersListWidget::createCriterium() const + { + auto compositeCriterium = std::make_unique(); + + for (auto const* filterLine : _filters) + { + compositeCriterium->add(filterLine->createCriterium()); + } + + return compositeCriterium; + } + void FiltersListWidget::addFilterLine() { auto* filter = new FilterLineWidget(); @@ -212,14 +559,12 @@ namespace PMP _verticalLayout->insertWidget(index, filter); _filters.append(filter); - rebuildCriterium(); connect( filter, &FilterLineWidget::criteriumChanged, this, [this]() { - rebuildCriterium(); Q_EMIT criteriumChanged(); } ); @@ -239,21 +584,8 @@ namespace PMP _filters.removeAt(index); filter->deleteLater(); - rebuildCriterium(); Q_EMIT criteriumChanged(); } ); } - - void FiltersListWidget::rebuildCriterium() - { - auto compositeCriterium = std::make_unique(); - - for (auto const* filterLine : _filters) - { - compositeCriterium->add(filterLine->criterium().clone()); - } - - _criterium = std::move(compositeCriterium); - } } diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index be1d597b..c596bc41 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -26,6 +26,7 @@ QT_FORWARD_DECLARE_CLASS(QComboBox) QT_FORWARD_DECLARE_CLASS(QPushButton) +QT_FORWARD_DECLARE_CLASS(QSpinBox) QT_FORWARD_DECLARE_CLASS(QVBoxLayout) namespace PMP @@ -38,7 +39,7 @@ namespace PMP QString captionForEmpty); void clearCriterium(); - const TrackCriterium& criterium() const { return *_criterium; } + std::unique_ptr createCriterium() const; Q_SIGNALS: void criteriumChanged(); @@ -50,7 +51,90 @@ namespace PMP QComboBox* _comboBox; PredefinedTrackCriterium _predefinedCriterium; - std::unique_ptr _criterium; + }; + + class FilterEditorWidget : public QWidget + { + Q_OBJECT + public: + virtual ~FilterEditorWidget() = default; + + virtual std::unique_ptr createCriterium() const = 0; + + Q_SIGNALS: + void criteriumChanged(); + + protected: + explicit FilterEditorWidget(QWidget* parent = nullptr) : QWidget(parent) {} + }; + + class ScoreComparisonEditorWidget : public FilterEditorWidget + { + Q_OBJECT + public: + explicit ScoreComparisonEditorWidget(QWidget* parent); + void setOperator(ComparisonOperator comparisonOperator); + void setScore(int score); + + std::unique_ptr createCriterium() const override; + + private: + QComboBox* _operatorComboBox; + QSpinBox* _scoreSpinBox; + }; + + class FilterEditorFactory + { + public: + static bool isEditable(TrackCriterium const& criterium); + static FilterEditorWidget* createEditor(QWidget* parent, + TrackCriterium const& criterium); + + private: + class IsEditableVisitor final : public TrackCriteriumVisitor + { + public: + bool isCriteriumEditable() const { return _isEditable; } + + void visit(const ConstantTrackCriterium&) override; + void visit(const TrackLengthPresenceCriterium&) override; + void visit(const TrackLengthComparisonCriterium&) override; + void visit(const TrackScorePresenceCriterium&) override; + void visit(const TrackScoreComparisonCriterium&) override; + void visit(const TrackLastHeardPresenceCriterium&) override; + void visit(const TrackLastHeardRecentlyCriterium&) override; + void visit(const TrackQueuePresenceCriterium&) override; + void visit(const TrackAvailabilityCriterium&) override; + void visit(const TrackMetaDataPresenceCriterium&) override; + void visit(const CompositeTrackCriterium&) override; + + private: + bool _isEditable { false }; + }; + + class EditorWidgetCreationVisitor final : public TrackCriteriumVisitor + { + public: + explicit EditorWidgetCreationVisitor(QWidget* parent); + + FilterEditorWidget* editorWidget() { return _editorWidget; } + + void visit(const ConstantTrackCriterium&) override; + void visit(const TrackLengthPresenceCriterium&) override; + void visit(const TrackLengthComparisonCriterium&) override; + void visit(const TrackScorePresenceCriterium&) override; + void visit(const TrackScoreComparisonCriterium&) override; + void visit(const TrackLastHeardPresenceCriterium&) override; + void visit(const TrackLastHeardRecentlyCriterium&) override; + void visit(const TrackQueuePresenceCriterium&) override; + void visit(const TrackAvailabilityCriterium&) override; + void visit(const TrackMetaDataPresenceCriterium&) override; + void visit(const CompositeTrackCriterium&) override; + + private: + QWidget* _parent; + FilterEditorWidget* _editorWidget; + }; }; class FilterLineWidget : public QWidget @@ -59,17 +143,23 @@ namespace PMP public: FilterLineWidget(); - const TrackCriterium& criterium() const { return *_criterium; } + std::unique_ptr createCriterium() const; Q_SIGNALS: void criteriumChanged(); void deleteClicked(); + private Q_SLOTS: + void onPickerCriteriumChanged(); + void onEditClicked(); + void onResetClicked(); + private: FilterPickerWidget* _filterPicker; + FilterEditorWidget* _editorWidget; + QPushButton* _editButton; QPushButton* _deleteButton; QPushButton* _resetButton; - std::unique_ptr _criterium; }; class FiltersListWidget : public QWidget @@ -78,19 +168,17 @@ namespace PMP public: FiltersListWidget(); - const TrackCriterium& criterium() const { return *_criterium; } + std::unique_ptr createCriterium() const; Q_SIGNALS: void criteriumChanged(); private: void addFilterLine(); - void rebuildCriterium(); QPushButton* _addButton; QVBoxLayout* _verticalLayout; QList _filters; - std::unique_ptr _criterium; }; } #endif From 62f03c143d5b326d100cb6cb7f2ac6bf6369a379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Tue, 3 Feb 2026 13:09:32 +0100 Subject: [PATCH 03/22] Add editor widget for track length comparisons Add an editor widget for track length comparisons. A predefined criterium such as "length > 3 min." can now be edited. The editor makes it possible to change both the comparison operator and the number. And it is possible to enter hours, minutes, and seconds, not just an amount of minutes. --- src/common/trackcriteria.cpp | 12 +-- src/common/trackcriteria.h | 21 +++- src/common/trackcriteriumevaluation.cpp | 7 +- src/common/util.cpp | 17 ++- src/common/util.h | 4 +- src/desktop-remote/trackfilterwidgets.cpp | 113 +++++++++++++++++++- src/desktop-remote/trackfilterwidgets.h | 18 ++++ tests/test_trackcriteriumsimplification.cpp | 3 +- tests/test_util.cpp | 52 ++++++++- tests/test_util.h | 3 +- 10 files changed, 230 insertions(+), 20 deletions(-) diff --git a/src/common/trackcriteria.cpp b/src/common/trackcriteria.cpp index f21f898c..6cbd96c9 100644 --- a/src/common/trackcriteria.cpp +++ b/src/common/trackcriteria.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2023-2025, Kevin André + Copyright (C) 2023-2026, Kevin André This file is part of PMP (Party Music Player). @@ -29,14 +29,14 @@ namespace PMP int lengthMinutesCeiling) { return std::make_unique( - ComparisonOperator::LessThan, lengthMinutesCeiling); + ComparisonOperator::LessThan, 0, lengthMinutesCeiling, 0); } std::unique_ptr createLengthAtLeastCriterium( int minimumLengthMinutes) { return std::make_unique( - ComparisonOperator::GreaterThanOrEqual, minimumLengthMinutes); + ComparisonOperator::GreaterThanOrEqual, 0, minimumLengthMinutes, 0); } std::unique_ptr createScoreLessThanCriterium(short scoreCeiling) @@ -201,15 +201,15 @@ namespace PMP TrackLengthComparisonCriterium::TrackLengthComparisonCriterium() : _operator(ComparisonOperator::GreaterThanOrEqual), - _minutes(0) + _hours(0), _minutes(0), _seconds(0) { // } TrackLengthComparisonCriterium::TrackLengthComparisonCriterium( - ComparisonOperator comparisonOperator, int minutes) + ComparisonOperator comparisonOperator, int hours, int minutes, int seconds) : _operator(comparisonOperator), - _minutes(minutes) + _hours(hours), _minutes(minutes), _seconds(seconds) { // } diff --git a/src/common/trackcriteria.h b/src/common/trackcriteria.h index 24de0716..e846e22b 100644 --- a/src/common/trackcriteria.h +++ b/src/common/trackcriteria.h @@ -238,10 +238,18 @@ namespace PMP public: TrackLengthComparisonCriterium(); TrackLengthComparisonCriterium(ComparisonOperator comparisonOperator, - int minutes); + int hours, int minutes, int seconds); - void setLengthMinutes(int minutes) { _minutes = minutes; } - int lengthMinutes() const { return _minutes; } + void setLength(int hours, int minutes, int seconds) + { + _hours = hours; + _minutes = minutes; + _seconds = seconds; + } + + int hours() const { return _hours; } + int minutes() const { return _minutes; } + int seconds() const { return _seconds; } void setComparisonOperator(ComparisonOperator comparisonOperator) { @@ -267,12 +275,17 @@ namespace PMP { auto* o = dynamic_cast(&other); - return o && _operator == o->_operator && _minutes == o->_minutes; + return o && _operator == o->_operator + && _hours == o->hours() + && _minutes == o->minutes() + && _seconds == o->seconds(); } private: ComparisonOperator _operator; + int _hours; int _minutes; + int _seconds; }; class TrackScorePresenceCriterium final : public TrackCriterium diff --git a/src/common/trackcriteriumevaluation.cpp b/src/common/trackcriteriumevaluation.cpp index 2941e114..0b04f435 100644 --- a/src/common/trackcriteriumevaluation.cpp +++ b/src/common/trackcriteriumevaluation.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2025, Kevin André + Copyright (C) 2025-2026, Kevin André This file is part of PMP (Party Music Player). @@ -79,7 +79,10 @@ namespace PMP } auto op = criterium.comparisonOperator(); - auto criteriumMs = criterium.lengthMinutes() * 60 * 1000; + auto criteriumMs = + criterium.hours() * 60 * 60 * 1000 + + criterium.minutes() * 60 * 1000 + + criterium.seconds() * 1000; _result = evaluateComparison(trackLengthMs.value(), op, criteriumMs); } diff --git a/src/common/util.cpp b/src/common/util.cpp index bfb73b1e..2ec2f6e7 100644 --- a/src/common/util.cpp +++ b/src/common/util.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2016-2024, Kevin Andre + Copyright (C) 2016-2026, Kevin André This file is part of PMP (Party Music Player). @@ -52,6 +52,21 @@ namespace PMP return result; } + void Util::normalizeDuration(int& hours, int& minutes, int& seconds) + { + int extraMinutes = seconds / 60; + int normalizedSeconds = seconds % 60; + + seconds = normalizedSeconds; + minutes += extraMinutes; + + int extraHours = minutes / 60; + int normalizedMinutes = minutes % 60; + + minutes = normalizedMinutes; + hours += extraHours; + } + QString Util::secondsToHoursMinuteSecondsText(qint32 totalSeconds) { if (totalSeconds < 0) { return "?"; } diff --git a/src/common/util.h b/src/common/util.h index 02bdde4d..5deaf143 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2016-2024, Kevin Andre + Copyright (C) 2016-2026, Kevin André This file is part of PMP (Party Music Player). @@ -79,6 +79,8 @@ namespace PMP public: static unsigned getRandomSeed(); + static void normalizeDuration(int& hours, int& minutes, int& seconds); + static QString secondsToHoursMinuteSecondsText(qint32 totalSeconds); static QString millisecondsToShortDisplayTimeText(qint64 milliseconds); static QString millisecondsToLongDisplayTimeText(qint64 milliseconds); diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 783779ff..a00d21f7 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -21,6 +21,7 @@ #include "common/nullable.h" #include "common/unicodechars.h" +#include "common/util.h" #include #include @@ -252,6 +253,102 @@ namespace PMP // =============================================================== // + LengthComparisonEditorWidget::LengthComparisonEditorWidget(QWidget* parent) + : FilterEditorWidget(parent) + { + auto* lengthLabel = new QLabel(tr("length")); + _operatorComboBox = new QComboBox(); + _hoursSpinBox = new QSpinBox(); + auto* hoursLabel = new QLabel(tr("h")); + _minutesSpinBox = new QSpinBox(); + auto* minutesLabel = new QLabel(tr("min.")); + _secondsSpinBox = new QSpinBox(); + auto* secondsLabel = new QLabel(tr("s")); + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(lengthLabel); + layout->addWidget(_operatorComboBox); + layout->addWidget(_hoursSpinBox); + layout->addWidget(hoursLabel); + layout->addWidget(_minutesSpinBox); + layout->addWidget(minutesLabel); + layout->addWidget(_secondsSpinBox); + layout->addWidget(secondsLabel); + layout->addStretch(); + + fillComboBoxWithComparisonOperators(_operatorComboBox); + + connect( + _operatorComboBox, &QComboBox::currentIndexChanged, + this, &ScoreComparisonEditorWidget::criteriumChanged + ); + + connect( + _hoursSpinBox, &QSpinBox::valueChanged, + this, [this]() { if (!_suspendChangeSignal) Q_EMIT criteriumChanged(); } + ); + + connect( + _minutesSpinBox, &QSpinBox::valueChanged, + this, [this]() { if (!_suspendChangeSignal) Q_EMIT criteriumChanged(); } + ); + + connect( + _secondsSpinBox, &QSpinBox::valueChanged, + this, [this]() { if (!_suspendChangeSignal) Q_EMIT criteriumChanged(); } + ); + } + + void LengthComparisonEditorWidget::setOperator(ComparisonOperator comparisonOperator) + { + selectValue(_operatorComboBox, comparisonOperator); + } + + void LengthComparisonEditorWidget::setLength(int hours, int minutes, int seconds) + { + Util::normalizeDuration(hours, minutes, seconds); + + Q_ASSERT_X(hours >= 0 && hours < 100, + "LengthComparisonEditorWidget::setLength", + "hours out of range"); + + Q_ASSERT_X(minutes >= 0 && minutes < 100, + "LengthComparisonEditorWidget::setLength", + "minutes out of range"); + + Q_ASSERT_X(seconds >= 0 && seconds < 100, + "LengthComparisonEditorWidget::setLength", + "seconds out of range"); + + _suspendChangeSignal++; + + _hoursSpinBox->setValue(hours); + _minutesSpinBox->setValue(minutes); + _secondsSpinBox->setValue(seconds); + + _suspendChangeSignal--; + + Q_EMIT criteriumChanged(); + } + + std::unique_ptr LengthComparisonEditorWidget::createCriterium() const + { + auto comparisonOperator = getSelectedComparisonOperator(_operatorComboBox); + + if (comparisonOperator == null) + return ConstantTrackCriterium::noTracksMatch(); + + int hours = _hoursSpinBox->value(); + int minutes = _minutesSpinBox->value(); + int seconds = _secondsSpinBox->value(); + + return std::make_unique( + comparisonOperator.value(), hours, minutes, seconds); + } + + // =============================================================== // + bool FilterEditorFactory::isEditable(const TrackCriterium& criterium) { IsEditableVisitor visitor; @@ -281,7 +378,7 @@ namespace PMP void FilterEditorFactory::IsEditableVisitor::visit( const TrackLengthComparisonCriterium&) { - _isEditable = false; + _isEditable = true; } void FilterEditorFactory::IsEditableVisitor::visit(const TrackScorePresenceCriterium&) @@ -349,9 +446,15 @@ namespace PMP } void FilterEditorFactory::EditorWidgetCreationVisitor::visit( - const TrackLengthComparisonCriterium&) + const TrackLengthComparisonCriterium& lengthCriterium) { - _editorWidget = nullptr; + auto* editor = new LengthComparisonEditorWidget(_parent); + editor->setOperator(lengthCriterium.comparisonOperator()); + editor->setLength(lengthCriterium.hours(), + lengthCriterium.minutes(), + lengthCriterium.seconds()); + + _editorWidget = editor; } void FilterEditorFactory::EditorWidgetCreationVisitor::visit( @@ -484,6 +587,10 @@ namespace PMP _editorWidget = FilterEditorFactory::createEditor(nullptr, *criterium); + Q_ASSERT_X(_editorWidget != nullptr, + "FilterLineWidget::onEditClicked", + "failed to obtain editor for criterium"); + connect( _editorWidget, &FilterEditorWidget::criteriumChanged, this, &FilterLineWidget::criteriumChanged diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index c596bc41..8b332ea5 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -83,6 +83,24 @@ namespace PMP QSpinBox* _scoreSpinBox; }; + class LengthComparisonEditorWidget : public FilterEditorWidget + { + Q_OBJECT + public: + explicit LengthComparisonEditorWidget(QWidget* parent); + void setOperator(ComparisonOperator comparisonOperator); + void setLength(int hours, int minutes, int seconds); + + std::unique_ptr createCriterium() const override; + + private: + QComboBox* _operatorComboBox; + QSpinBox* _hoursSpinBox; + QSpinBox* _minutesSpinBox; + QSpinBox* _secondsSpinBox; + quint8 _suspendChangeSignal { 0 }; + }; + class FilterEditorFactory { public: diff --git a/tests/test_trackcriteriumsimplification.cpp b/tests/test_trackcriteriumsimplification.cpp index c6ac9b63..1e90fb92 100644 --- a/tests/test_trackcriteriumsimplification.cpp +++ b/tests/test_trackcriteriumsimplification.cpp @@ -106,7 +106,8 @@ void TestTrackCriteriumSimplification::compositeContainingOneNoTracksSimplifiedT input->add(createAllTracks()); input->add(TrackQueuePresenceCriterium::mustBeAbsentInQueue()); input->add(createNoTracks()); - input->add(ComparisonOperator::GreaterThanOrEqual, 3); + input->add(ComparisonOperator::GreaterThanOrEqual, + 0, 3, 0); auto simplified = TrackCriteriumSimplifier::simplify(*input); diff --git a/tests/test_util.cpp b/tests/test_util.cpp index 360f9008..aad45b49 100644 --- a/tests/test_util.cpp +++ b/tests/test_util.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2018-2022, Kevin Andre + Copyright (C) 2018-2026, Kevin André This file is part of PMP (Party Music Player). @@ -227,6 +227,56 @@ void TestUtil::getRandomSeed() QVERIFY(seed3 != seed4); } +void TestUtil::normalizeDuration() +{ + int hours, minutes, seconds; + + { + hours = 0; minutes = 0; seconds = 0; + Util::normalizeDuration(hours, minutes, seconds); + + QCOMPARE(hours, 0); + QCOMPARE(minutes, 0); + QCOMPARE(seconds, 0); + } + + { + hours = 1; minutes = 30; seconds = 8; + Util::normalizeDuration(hours, minutes, seconds); + + QCOMPARE(hours, 1); + QCOMPARE(minutes, 30); + QCOMPARE(seconds, 8); + } + + { + hours = 1; minutes = 30; seconds = 60; + Util::normalizeDuration(hours, minutes, seconds); + + QCOMPARE(hours, 1); + QCOMPARE(minutes, 31); + QCOMPARE(seconds, 0); + } + + { + hours = 1; minutes = 70; seconds = 5; + Util::normalizeDuration(hours, minutes, seconds); + + QCOMPARE(hours, 2); + QCOMPARE(minutes, 10); + QCOMPARE(seconds, 5); + } + + { + hours = 1; minutes = 59; seconds = 137; + Util::normalizeDuration(hours, minutes, seconds); + + QCOMPARE(hours, 2); + QCOMPARE(minutes, 1); + QCOMPARE(seconds, 17); + } +} + void TestUtil::generateZeroedMemory() { auto a = Util::generateZeroedMemory(29); diff --git a/tests/test_util.h b/tests/test_util.h index ae582ab7..c2d35111 100644 --- a/tests/test_util.h +++ b/tests/test_util.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2018-2021, Kevin Andre + Copyright (C) 2018-2026, Kevin André This file is part of PMP (Party Music Player). @@ -28,6 +28,7 @@ class TestUtil : public QObject private Q_SLOTS: void getCopyrightLine(); void getRandomSeed(); + void normalizeDuration(); void secondsToHoursMinuteSecondsText(); void millisecondsToShortDisplayTimeText(); void millisecondsToLongDisplayTimeText(); From 5143690c0d7fe092a253e638234896f435c1467d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Sat, 7 Feb 2026 13:43:55 +0100 Subject: [PATCH 04/22] Add "OK" button and read-only view to filter line Add a read-only view for filters. This will display the filter as a label, which is much easier to read than a line containing multiple spin boxes. For now, an "OK" button is added that allows switching from edit mode to read-only mode. The "Edit" button can be used to switch back from read-only mode to edit mode. I am not fond of the "OK" button as a solution for ending edit mode, and I will try to find something more elegant later. --- src/common/trackcriteria.h | 2 + src/desktop-remote/trackfilterwidgets.cpp | 373 ++++++++++++++++++++-- src/desktop-remote/trackfilterwidgets.h | 50 ++- 3 files changed, 401 insertions(+), 24 deletions(-) diff --git a/src/common/trackcriteria.h b/src/common/trackcriteria.h index e846e22b..984aad08 100644 --- a/src/common/trackcriteria.h +++ b/src/common/trackcriteria.h @@ -426,6 +426,8 @@ namespace PMP int years { 0 }; int days { 0 }; + bool isZero() const { return years == 0 && days == 0; } + bool operator==(const CompositeDuration&) const = default; }; diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index a00d21f7..04422f26 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -32,6 +32,239 @@ namespace PMP { + FilterLabelWidget::FilterLabelWidget(QWidget *parent) + : QWidget(parent) + { + _label = new QLabel(); + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_label); + } + + void FilterLabelWidget::setCriterium(std::unique_ptr criterium) + { + _criterium = std::move(criterium); + + if (!_criterium) + { + _label->clear(); + return; + } + + CriteriumCaptionGenerator visitor; + _criterium->accept(visitor); + auto caption = visitor.caption(); + + _label->setText(caption); + } + + std::unique_ptr FilterLabelWidget::createCriterium() const + { + return _criterium->clone(); + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const ConstantTrackCriterium& criterium) + { + if (criterium.value()) + _caption = tr("match any track"); + else + _caption = tr("match no tracks"); + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackLengthPresenceCriterium& criterium) + { + if (criterium.presence()) + _caption = tr("length is known"); + else + _caption = tr("length is unknown"); + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackLengthComparisonCriterium& criterium) + { + auto caption = + tr("length %1 %2:%3:%4") + .arg(toString(criterium.comparisonOperator())) + .arg(criterium.hours(), 2, 10, QChar('0')) + .arg(criterium.minutes(), 2, 10, QChar('0')) + .arg(criterium.seconds(), 2, 10, QChar('0')); + + _caption = caption; + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackScorePresenceCriterium& criterium) + { + if (criterium.presence()) + _caption = tr("with score"); + else + _caption = tr("without score"); + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackScoreComparisonCriterium& criterium) + { + auto caption = + tr("score %1 %2") + .arg(toString(criterium.comparisonOperator())) + .arg(criterium.score()); + + _caption = caption; + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackLastHeardPresenceCriterium& criterium) + { + if (criterium.presence()) + _caption = tr("heard at least once"); + else + _caption = tr("never heard"); + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackLastHeardRecentlyCriterium& criterium) + { + auto isInverted = criterium.isInverted(); + auto duration = criterium.duration(); + + if (duration.isZero()) + { + if (isInverted) + { + _caption = tr("not heard in the last 0 seconds"); + } + else + { + _caption = tr("heard in the last 0 seconds"); + } + } + else if (duration.years > 0 && duration.days == 0) + { + if (isInverted) + { + _caption = tr("not heard in the last %1 year(s)").arg(duration.years); + } + else + { + _caption = tr("heard in the last %1 year(s)").arg(duration.years); + } + } + else if (duration.days > 0 && duration.years == 0) + { + if (isInverted) + { + _caption = tr("not heard in the last %1 day(s)").arg(duration.days); + } + else + { + _caption = tr("heard in the last %1 day(s)").arg(duration.days); + } + } + else // catch-all case + { + if (isInverted) + { + _caption = + tr("not heard in the last %1 year(s) %2 day(s)") + .arg(duration.years) + .arg(duration.days); + } + else + { + _caption = + tr("heard in the last %1 year(s) %2 day(s)") + .arg(duration.years) + .arg(duration.days); + } + } + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackQueuePresenceCriterium& criterium) + { + if (criterium.presence()) + _caption = tr("in the queue"); + else + _caption = tr("not in the queue"); + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackAvailabilityCriterium& criterium) + { + if (criterium.availability()) + _caption = tr("available"); + else + _caption = tr("no longer available"); + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const TrackMetaDataPresenceCriterium& criterium) + { + switch (criterium.metaDataKind()) + { + case TrackMetaDataKind::Title: + if (criterium.presence()) + _caption = tr("with title"); + else + _caption = tr("without title"); + break; + case TrackMetaDataKind::Artist: + if (criterium.presence()) + _caption = tr("with artist"); + else + _caption = tr("without artist"); + break; + case TrackMetaDataKind::Album: + if (criterium.presence()) + _caption = tr("with album"); + else + _caption = tr("without album"); + break; + } + } + + void FilterLabelWidget::CriteriumCaptionGenerator::visit( + const CompositeTrackCriterium& criterium) + { + // This function should not be called, because the composite criterium cannot be + // put inside a FilterLineWidget yet + Q_UNREACHABLE(); + } + + QString FilterLabelWidget::CriteriumCaptionGenerator::toString( + ComparisonOperator comparisonOperator) + { + QString operatorString; + switch (comparisonOperator) + { + case ComparisonOperator::Equal: + operatorString = "="; + break; + case ComparisonOperator::NotEqual: + operatorString = UnicodeChars::notEqual; + break; + case ComparisonOperator::LessThan: + operatorString = "<"; + break; + case ComparisonOperator::LessThanOrEqual: + operatorString = UnicodeChars::lessThanOrEqual; + break; + case ComparisonOperator::GreaterThan: + operatorString = ">"; + break; + case ComparisonOperator::GreaterThanOrEqual: + operatorString = UnicodeChars::greaterThanOrEqual; + break; + } + + return operatorString; + } + + // =============================================================== // + FilterPickerWidget::FilterPickerWidget(PredefinedTrackCriterium criteriumForEmpty, QString captionForEmpty) : _predefinedCriterium(criteriumForEmpty) @@ -58,7 +291,7 @@ namespace PMP _predefinedCriterium = predefinedCriterium; Q_EMIT criteriumChanged(); } - ); + ); } void FilterPickerWidget::clearCriterium() @@ -512,11 +745,13 @@ namespace PMP // =============================================================== // FilterLineWidget::FilterLineWidget() - : _editorWidget(nullptr) + : _labelWidget(nullptr), + _editorWidget(nullptr) { _filterPicker = new FilterPickerWidget(PredefinedTrackCriterium::AllTracks, tr("(empty)")); _editButton = new QPushButton(); + _okButton = new QPushButton(); _deleteButton = new QPushButton(); _resetButton = new QPushButton(); @@ -524,12 +759,16 @@ namespace PMP layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(_filterPicker, 1); layout->addWidget(_editButton, 0); + layout->addWidget(_okButton, 0); layout->addWidget(_deleteButton, 0); layout->addWidget(_resetButton, 0); _editButton->setText(tr("Edit")); _editButton->setEnabled(false); + _okButton->setText(tr("OK")); + _okButton->setVisible(false); + _deleteButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); _deleteButton->setToolTip(tr("Remove filter")); @@ -546,6 +785,11 @@ namespace PMP this, &FilterLineWidget::onEditClicked ); + connect( + _okButton, &QPushButton::clicked, + this, [this]() { switchEditorToLabel(); } + ); + connect( _deleteButton, &QPushButton::clicked, this, [this]() { Q_EMIT deleteClicked(); } @@ -559,15 +803,19 @@ namespace PMP std::unique_ptr FilterLineWidget::createCriterium() const { + if (_labelWidget) + return _labelWidget->createCriterium(); + if (_editorWidget) return _editorWidget->createCriterium(); return _filterPicker->createCriterium(); } + void FilterLineWidget::onPickerCriteriumChanged() { - if (_editorWidget) + if (_labelWidget || _editorWidget) return; auto criterium = _filterPicker->createCriterium(); @@ -579,16 +827,106 @@ namespace PMP void FilterLineWidget::onEditClicked() { - Q_ASSERT_X(_editorWidget == nullptr, - "FilterLineWidget::onEditClicked", - "editor already present!"); + if (_labelWidget) + { + switchLabelToEditor(); + } + else + { + switchPickerToEditor(); + } + } + + void FilterLineWidget::onResetClicked() + { + if (_labelWidget) + { + layout()->replaceWidget(_labelWidget, _filterPicker); + _labelWidget->setVisible(false); + _labelWidget->deleteLater(); + _labelWidget = nullptr; + } + + if (_editorWidget) + { + layout()->replaceWidget(_editorWidget, _filterPicker); + _editorWidget->setVisible(false); + _editorWidget->deleteLater(); + _editorWidget = nullptr; + } + + _filterPicker->clearCriterium(); + _filterPicker->setVisible(true); + } + + void FilterLineWidget::switchEditorToLabel() + { + Q_ASSERT_X(_editorWidget != nullptr, + "FilterLineWidget::switchEditorToLabel", + "editor not present!"); + + auto criterium = _editorWidget->createCriterium(); + + switchToLabel(_editorWidget, std::move(criterium)); + + _editorWidget->deleteLater(); + _editorWidget = nullptr; + } + + void FilterLineWidget::switchToLabel(QWidget* widgetToReplace, + std::unique_ptr criterium) + { + Q_ASSERT_X(_labelWidget == nullptr, + "FilterLineWidget::switchToLabel", + "label widget already present!"); + + _labelWidget = new FilterLabelWidget(nullptr); + _labelWidget->setCriterium(std::move(criterium)); + + layout()->replaceWidget(widgetToReplace, _labelWidget); + widgetToReplace->setVisible(false); + + _editButton->setEnabled(true); + _editButton->setVisible(true); + _okButton->setVisible(false); + } + + void FilterLineWidget::switchLabelToEditor() + { + Q_ASSERT_X(_labelWidget != nullptr, + "FilterLineWidget::switchLabelToEditor", + "label widget not present!"); + + auto criterium = _labelWidget->createCriterium(); + + switchToEditor(_labelWidget, *criterium); + + _labelWidget->deleteLater(); + _labelWidget = nullptr; + } + + void FilterLineWidget::switchPickerToEditor() + { + Q_ASSERT_X(_labelWidget == nullptr, + "FilterLineWidget::switchPickerToEditor", + "label widget present!"); auto criterium = _filterPicker->createCriterium(); - _editorWidget = FilterEditorFactory::createEditor(nullptr, *criterium); + switchToEditor(_filterPicker, *criterium); + } + + void FilterLineWidget::switchToEditor(QWidget* widgetToReplace, + const TrackCriterium& criterium) + { + Q_ASSERT_X(_editorWidget == nullptr, + "FilterLineWidget::switchToEditor", + "editor already present!"); + + _editorWidget = FilterEditorFactory::createEditor(nullptr, criterium); Q_ASSERT_X(_editorWidget != nullptr, - "FilterLineWidget::onEditClicked", + "FilterLineWidget::switchToEditor", "failed to obtain editor for criterium"); connect( @@ -596,23 +934,12 @@ namespace PMP this, &FilterLineWidget::criteriumChanged ); - layout()->replaceWidget(_filterPicker, _editorWidget); - _filterPicker->setVisible(false); + layout()->replaceWidget(widgetToReplace, _editorWidget); + widgetToReplace->setVisible(false); _editButton->setEnabled(false); - } - - void FilterLineWidget::onResetClicked() - { - if (_editorWidget) - { - layout()->replaceWidget(_editorWidget, _filterPicker); - _editorWidget->deleteLater(); - _editorWidget = nullptr; - _filterPicker->setVisible(true); - } - - _filterPicker->clearCriterium(); + _editButton->setVisible(false); + _okButton->setVisible(true); } // =============================================================== // diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index 8b332ea5..84e6dbe3 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -25,12 +25,51 @@ #include QT_FORWARD_DECLARE_CLASS(QComboBox) +QT_FORWARD_DECLARE_CLASS(QLabel) QT_FORWARD_DECLARE_CLASS(QPushButton) QT_FORWARD_DECLARE_CLASS(QSpinBox) QT_FORWARD_DECLARE_CLASS(QVBoxLayout) namespace PMP { + class FilterLabelWidget : public QWidget + { + Q_OBJECT + public: + explicit FilterLabelWidget(QWidget* parent); + + void setCriterium(std::unique_ptr criterium); + + std::unique_ptr createCriterium() const; + + private: + class CriteriumCaptionGenerator : public TrackCriteriumVisitor + { + public: + QString caption() const { return _caption; } + + void visit(const ConstantTrackCriterium&) override; + void visit(const TrackLengthPresenceCriterium&) override; + void visit(const TrackLengthComparisonCriterium&) override; + void visit(const TrackScorePresenceCriterium&) override; + void visit(const TrackScoreComparisonCriterium&) override; + void visit(const TrackLastHeardPresenceCriterium&) override; + void visit(const TrackLastHeardRecentlyCriterium&) override; + void visit(const TrackQueuePresenceCriterium&) override; + void visit(const TrackAvailabilityCriterium&) override; + void visit(const TrackMetaDataPresenceCriterium&) override; + void visit(const CompositeTrackCriterium&) override; + + private: + QString toString(ComparisonOperator comparisonOperator); + + QString _caption; + }; + + std::unique_ptr _criterium; + QLabel* _label; + }; + class FilterPickerWidget : public QWidget { Q_OBJECT @@ -135,7 +174,7 @@ namespace PMP public: explicit EditorWidgetCreationVisitor(QWidget* parent); - FilterEditorWidget* editorWidget() { return _editorWidget; } + FilterEditorWidget* editorWidget() const { return _editorWidget; } void visit(const ConstantTrackCriterium&) override; void visit(const TrackLengthPresenceCriterium&) override; @@ -173,9 +212,18 @@ namespace PMP void onResetClicked(); private: + void switchEditorToLabel(); + void switchToLabel(QWidget* widgetToReplace, + std::unique_ptr criterium); + void switchLabelToEditor(); + void switchPickerToEditor(); + void switchToEditor(QWidget* widgetToReplace, TrackCriterium const& criterium); + + FilterLabelWidget* _labelWidget; FilterPickerWidget* _filterPicker; FilterEditorWidget* _editorWidget; QPushButton* _editButton; + QPushButton* _okButton; QPushButton* _deleteButton; QPushButton* _resetButton; }; From c89b69109fbd2973b5be5a98a0fe5910302e96a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Sat, 7 Feb 2026 15:31:47 +0100 Subject: [PATCH 05/22] Add editor widget for "(not) heard in the last X" Add an editor widget for criteria like "not heard in the last 180 days" or "not heard in the last year". The editor also supports the reverse, like "heard in the last 10 days", i.e. without the "not". --- src/common/util.cpp | 24 ++++++ src/common/util.h | 1 + src/desktop-remote/trackfilterwidgets.cpp | 96 ++++++++++++++++++++++- src/desktop-remote/trackfilterwidgets.h | 18 +++++ tests/test_util.cpp | 69 ++++++++++++++++ tests/test_util.h | 1 + 6 files changed, 206 insertions(+), 3 deletions(-) diff --git a/src/common/util.cpp b/src/common/util.cpp index 2ec2f6e7..514a67ca 100644 --- a/src/common/util.cpp +++ b/src/common/util.cpp @@ -67,6 +67,30 @@ namespace PMP hours += extraHours; } + void Util::normalizeLongDuration(int& years, int& days) + { + const int daysInFourYears = 3 * 365 + 366; + + int fourYearInstances = days / daysInFourYears; + int daysLeft = days % daysInFourYears; + + days = daysLeft; + years += 4 * fourYearInstances; + + int extraYears = days / 365; + int normalizedDays = days % 365; + + // fix the edge case of 4 * 365 + if (extraYears == 4) + { + extraYears = 3; + normalizedDays += 365; + } + + days = normalizedDays; + years += extraYears; + } + QString Util::secondsToHoursMinuteSecondsText(qint32 totalSeconds) { if (totalSeconds < 0) { return "?"; } diff --git a/src/common/util.h b/src/common/util.h index 5deaf143..39b6ca3b 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -80,6 +80,7 @@ namespace PMP static unsigned getRandomSeed(); static void normalizeDuration(int& hours, int& minutes, int& seconds); + static void normalizeLongDuration(int& years, int& days); static QString secondsToHoursMinuteSecondsText(qint32 totalSeconds); static QString millisecondsToShortDisplayTimeText(qint64 milliseconds); diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 04422f26..e46ce275 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -486,6 +486,90 @@ namespace PMP // =============================================================== // + LastHeardEditorWidget::LastHeardEditorWidget(QWidget* parent) + : FilterEditorWidget(parent) + { + _inversionComboBox = new QComboBox(); + _yearsSpinBox = new QSpinBox(); + auto* yearsLabel = new QLabel(tr("years")); + _daysSpinBox = new QSpinBox(); + auto* daysLabel = new QLabel(tr("days")); + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_inversionComboBox); + layout->addWidget(_yearsSpinBox); + layout->addWidget(yearsLabel); + layout->addWidget(_daysSpinBox); + layout->addWidget(daysLabel); + layout->addStretch(); + + _inversionComboBox->addItem(tr("heard within")); + _inversionComboBox->addItem(tr("not heard within")); + + _daysSpinBox->setMaximum(999); + + connect( + _inversionComboBox, &QComboBox::currentIndexChanged, + this, &ScoreComparisonEditorWidget::criteriumChanged + ); + + connect( + _yearsSpinBox, &QSpinBox::valueChanged, + this, [this]() { if (!_suspendChangeSignal) Q_EMIT criteriumChanged(); } + ); + + connect( + _daysSpinBox, &QSpinBox::valueChanged, + this, [this]() { if (!_suspendChangeSignal) Q_EMIT criteriumChanged(); } + ); + } + + void LastHeardEditorWidget::setInverted(bool isInverted) + { + _inversionComboBox->setCurrentIndex(isInverted ? 1 : 0); + } + + void LastHeardEditorWidget::setPeriod(int years, int days) + { + Util::normalizeLongDuration(years, days); + + Q_ASSERT_X(years >= 0 && years < 100, + "LastHeardEditorWidget::setPeriod", + "years out of range"); + + Q_ASSERT_X(days >= 0 && days < 1000, + "LastHeardEditorWidget::setPeriod", + "days out of range"); + + _suspendChangeSignal++; + + _yearsSpinBox->setValue(years); + _daysSpinBox->setValue(days); + + _suspendChangeSignal--; + + Q_EMIT criteriumChanged(); + } + + std::unique_ptr LastHeardEditorWidget::createCriterium() const + { + auto inversionIndex = _inversionComboBox->currentIndex(); + + if (inversionIndex < 0) + return ConstantTrackCriterium::noTracksMatch(); + + bool isInverted = inversionIndex == 1; + + int years = _yearsSpinBox->value(); + int days = _daysSpinBox->value(); + + return std::make_unique( + CompositeDuration { .years = years, .days = days }, isInverted); + } + + // =============================================================== // + LengthComparisonEditorWidget::LengthComparisonEditorWidget(QWidget* parent) : FilterEditorWidget(parent) { @@ -634,7 +718,7 @@ namespace PMP void FilterEditorFactory::IsEditableVisitor::visit( const TrackLastHeardRecentlyCriterium&) { - _isEditable = false; + _isEditable = true; } void FilterEditorFactory::IsEditableVisitor::visit(const TrackQueuePresenceCriterium&) @@ -713,9 +797,15 @@ namespace PMP } void FilterEditorFactory::EditorWidgetCreationVisitor::visit( - const TrackLastHeardRecentlyCriterium&) + const TrackLastHeardRecentlyCriterium& criterium) { - _editorWidget = nullptr; + auto period = criterium.duration(); + + auto* editor = new LastHeardEditorWidget(_parent); + editor->setPeriod(period.years, period.days); + editor->setInverted(criterium.isInverted()); + + _editorWidget = editor; } void FilterEditorFactory::EditorWidgetCreationVisitor::visit( diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index 84e6dbe3..c2c99609 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -122,6 +122,24 @@ namespace PMP QSpinBox* _scoreSpinBox; }; + class LastHeardEditorWidget : public FilterEditorWidget + { + Q_OBJECT + public: + explicit LastHeardEditorWidget(QWidget* parent); + + void setInverted(bool isInverted); + void setPeriod(int years, int days); + + std::unique_ptr createCriterium() const override; + + private: + QComboBox* _inversionComboBox; + QSpinBox* _yearsSpinBox; + QSpinBox* _daysSpinBox; + quint8 _suspendChangeSignal { 0 }; + }; + class LengthComparisonEditorWidget : public FilterEditorWidget { Q_OBJECT diff --git a/tests/test_util.cpp b/tests/test_util.cpp index aad45b49..70bd1605 100644 --- a/tests/test_util.cpp +++ b/tests/test_util.cpp @@ -277,6 +277,75 @@ void TestUtil::normalizeDuration() } } +void TestUtil::normalizeLongDuration() +{ + int years, days; + + { + years = 0; days = 0; + Util::normalizeLongDuration(years, days); + + QCOMPARE(years, 0); + QCOMPARE(days, 0); + } + + { + years = 3; days = 0; + Util::normalizeLongDuration(years, days); + + QCOMPARE(years, 3); + QCOMPARE(days, 0); + } + + { + years = 0; days = 90; + Util::normalizeLongDuration(years, days); + + QCOMPARE(years, 0); + QCOMPARE(days, 90); + } + + { + years = 1; days = 7; + Util::normalizeLongDuration(years, days); + + QCOMPARE(years, 1); + QCOMPARE(days, 7); + } + + { + years = 2; days = 370; + Util::normalizeLongDuration(years, days); + + QCOMPARE(years, 3); + QCOMPARE(days, 5); + } + + { + years = 0; days = 999; + Util::normalizeLongDuration(years, days); + + QCOMPARE(years, 2); + QCOMPARE(days, 269); + } + + { + years = 0; days = 365 + 365 + 365 + 365; + Util::normalizeLongDuration(years, days); + + QCOMPARE(years, 3); + QCOMPARE(days, 365); + } + + { + years = 0; days = 365 + 365 + 365 + 366; + Util::normalizeLongDuration(years, days); + + QCOMPARE(years, 4); + QCOMPARE(days, 0); + } +} + void TestUtil::generateZeroedMemory() { auto a = Util::generateZeroedMemory(29); diff --git a/tests/test_util.h b/tests/test_util.h index c2d35111..6ee2a097 100644 --- a/tests/test_util.h +++ b/tests/test_util.h @@ -29,6 +29,7 @@ private Q_SLOTS: void getCopyrightLine(); void getRandomSeed(); void normalizeDuration(); + void normalizeLongDuration(); void secondsToHoursMinuteSecondsText(); void millisecondsToShortDisplayTimeText(); void millisecondsToLongDisplayTimeText(); From 65d878bb8493e73acbb7611e48f0f00ab755d1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Sun, 8 Feb 2026 16:30:48 +0100 Subject: [PATCH 06/22] FilterLabelWidget: add click-to-edit Switch to edit mode when a read-only filter is clicked. --- src/desktop-remote/trackfilterwidgets.cpp | 15 ++++++++++++++- src/desktop-remote/trackfilterwidgets.h | 7 ++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index e46ce275..cb7be42d 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -23,6 +23,8 @@ #include "common/unicodechars.h" #include "common/util.h" +#include "clickablelabel.h" + #include #include #include @@ -35,11 +37,17 @@ namespace PMP FilterLabelWidget::FilterLabelWidget(QWidget *parent) : QWidget(parent) { - _label = new QLabel(); + _label = new ClickableLabel(); QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(_label); + layout->addStretch(); + + connect( + _label, &ClickableLabel::clicked, + this, &FilterLabelWidget::editingRequested + ); } void FilterLabelWidget::setCriterium(std::unique_ptr criterium) @@ -973,6 +981,11 @@ namespace PMP _labelWidget = new FilterLabelWidget(nullptr); _labelWidget->setCriterium(std::move(criterium)); + connect( + _labelWidget, &FilterLabelWidget::editingRequested, + this, [this] { switchLabelToEditor(); } + ); + layout()->replaceWidget(widgetToReplace, _labelWidget); widgetToReplace->setVisible(false); diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index c2c99609..5c5e2dfc 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -32,6 +32,8 @@ QT_FORWARD_DECLARE_CLASS(QVBoxLayout) namespace PMP { + class ClickableLabel; + class FilterLabelWidget : public QWidget { Q_OBJECT @@ -42,6 +44,9 @@ namespace PMP std::unique_ptr createCriterium() const; + Q_SIGNALS: + void editingRequested(); + private: class CriteriumCaptionGenerator : public TrackCriteriumVisitor { @@ -67,7 +72,7 @@ namespace PMP }; std::unique_ptr _criterium; - QLabel* _label; + ClickableLabel* _label; }; class FilterPickerWidget : public QWidget From ea5caae13038d5f01f9e1c43364d6119410f3db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Sun, 8 Feb 2026 16:49:39 +0100 Subject: [PATCH 07/22] Improve read-only view of track length comparison Instead of the generic "00:00:00" time view, use words when only one time unit is specified. Examples: - display "01:00:00" as "1 hour(s)" - display "00:03:00" as "3 minute(s)" - display "00:00:55" as "55 second(s)" The generic view is still used when more than one time unit is used, as in "00:03:30". --- src/desktop-remote/trackfilterwidgets.cpp | 33 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index cb7be42d..2c42a815 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -93,14 +93,33 @@ namespace PMP void FilterLabelWidget::CriteriumCaptionGenerator::visit( const TrackLengthComparisonCriterium& criterium) { - auto caption = - tr("length %1 %2:%3:%4") - .arg(toString(criterium.comparisonOperator())) - .arg(criterium.hours(), 2, 10, QChar('0')) - .arg(criterium.minutes(), 2, 10, QChar('0')) - .arg(criterium.seconds(), 2, 10, QChar('0')); + auto opStr = toString(criterium.comparisonOperator()); - _caption = caption; + auto hours = criterium.hours(); + auto minutes = criterium.minutes(); + auto seconds = criterium.seconds(); + + if (hours > 0 && minutes == 0 && seconds == 0) + { + _caption = tr("length %1 %2 hour(s)").arg(opStr).arg(hours); + } + else if (hours == 0 && minutes > 0 && seconds == 0) + { + _caption = tr("length %1 %2 minute(s)").arg(opStr).arg(minutes); + } + else if (hours == 0 && minutes == 0 && seconds > 0) + { + _caption = tr("length %1 %2 second(s)").arg(opStr).arg(seconds); + } + else + { + _caption = + tr("length %1 %2:%3:%4") + .arg(opStr) + .arg(hours, 2, 10, QChar('0')) + .arg(minutes, 2, 10, QChar('0')) + .arg(seconds, 2, 10, QChar('0')); + } } void FilterLabelWidget::CriteriumCaptionGenerator::visit( From 0d23e0e16fa61717335b4a255bb3b7b5b538aae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Sun, 8 Feb 2026 17:00:49 +0100 Subject: [PATCH 08/22] Normalize hours/minutes/seconds before displaying If the track length comparison editor produces 3 minutes and 60 seconds, and then a switch to read-only view is done, make sure to display this as "00:04:00" instead of "00:03:60". --- src/desktop-remote/trackfilterwidgets.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 2c42a815..a4da34ac 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -99,6 +99,8 @@ namespace PMP auto minutes = criterium.minutes(); auto seconds = criterium.seconds(); + Util::normalizeDuration(hours, minutes, seconds); + if (hours > 0 && minutes == 0 && seconds == 0) { _caption = tr("length %1 %2 hour(s)").arg(opStr).arg(hours); From a4cc3f14d581c1aa522d90d2d2c99d6be048a02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Wed, 11 Feb 2026 15:59:40 +0100 Subject: [PATCH 09/22] Switch to icons for the "Edit" and "OK" button --- src/desktop-remote/trackfilterwidgets.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index a4da34ac..cc8eca69 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -882,11 +883,16 @@ namespace PMP layout->addWidget(_deleteButton, 0); layout->addWidget(_resetButton, 0); - _editButton->setText(tr("Edit")); _editButton->setEnabled(false); + _editButton->setIcon( + QIcon::fromTheme("document-edit", + style()->standardIcon(QStyle::SP_FileDialogDetailedView))); + _editButton->setToolTip(tr("Edit filter")); - _okButton->setText(tr("OK")); _okButton->setVisible(false); + _okButton->setIcon(QIcon::fromTheme("dialog-ok-apply", + style()->standardIcon(QStyle::SP_DialogApplyButton))); + _okButton->setToolTip(tr("Done editing")); _deleteButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); _deleteButton->setToolTip(tr("Remove filter")); From 2bc13f47645f798fd0235bcc73724967cb3fec29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Wed, 11 Feb 2026 16:32:52 +0100 Subject: [PATCH 10/22] Rename "_okButton" to "_doneButton" --- src/desktop-remote/trackfilterwidgets.cpp | 16 ++++++++-------- src/desktop-remote/trackfilterwidgets.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index cc8eca69..f9eabbf0 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -871,7 +871,7 @@ namespace PMP _filterPicker = new FilterPickerWidget(PredefinedTrackCriterium::AllTracks, tr("(empty)")); _editButton = new QPushButton(); - _okButton = new QPushButton(); + _doneButton = new QPushButton(); _deleteButton = new QPushButton(); _resetButton = new QPushButton(); @@ -879,7 +879,7 @@ namespace PMP layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(_filterPicker, 1); layout->addWidget(_editButton, 0); - layout->addWidget(_okButton, 0); + layout->addWidget(_doneButton, 0); layout->addWidget(_deleteButton, 0); layout->addWidget(_resetButton, 0); @@ -889,10 +889,10 @@ namespace PMP style()->standardIcon(QStyle::SP_FileDialogDetailedView))); _editButton->setToolTip(tr("Edit filter")); - _okButton->setVisible(false); - _okButton->setIcon(QIcon::fromTheme("dialog-ok-apply", + _doneButton->setVisible(false); + _doneButton->setIcon(QIcon::fromTheme("dialog-ok-apply", style()->standardIcon(QStyle::SP_DialogApplyButton))); - _okButton->setToolTip(tr("Done editing")); + _doneButton->setToolTip(tr("Done editing")); _deleteButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); _deleteButton->setToolTip(tr("Remove filter")); @@ -911,7 +911,7 @@ namespace PMP ); connect( - _okButton, &QPushButton::clicked, + _doneButton, &QPushButton::clicked, this, [this]() { switchEditorToLabel(); } ); @@ -1018,7 +1018,7 @@ namespace PMP _editButton->setEnabled(true); _editButton->setVisible(true); - _okButton->setVisible(false); + _doneButton->setVisible(false); } void FilterLineWidget::switchLabelToEditor() @@ -1069,7 +1069,7 @@ namespace PMP _editButton->setEnabled(false); _editButton->setVisible(false); - _okButton->setVisible(true); + _doneButton->setVisible(true); } // =============================================================== // diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index 5c5e2dfc..5283f453 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -246,7 +246,7 @@ namespace PMP FilterPickerWidget* _filterPicker; FilterEditorWidget* _editorWidget; QPushButton* _editButton; - QPushButton* _okButton; + QPushButton* _doneButton; QPushButton* _deleteButton; QPushButton* _resetButton; }; From 873caa748fe1b92c6f00cf837103766c49e33b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Wed, 11 Feb 2026 17:38:22 +0100 Subject: [PATCH 11/22] Bugfix: wrong button visible after click on reset --- src/desktop-remote/trackfilterwidgets.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index f9eabbf0..c7802e02 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -982,6 +982,10 @@ namespace PMP _filterPicker->clearCriterium(); _filterPicker->setVisible(true); + + _editButton->setVisible(true); + _editButton->setEnabled(false); + _doneButton->setVisible(false); } void FilterLineWidget::switchEditorToLabel() From cdbf7d00f3f26a337258bd7e499a2a851cd6324a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Sun, 15 Feb 2026 20:27:01 +0100 Subject: [PATCH 12/22] Add hours to "(not) heard in the last X" filter Add support for a number of hours to the "(not) heard in the last X years/days" filter. This makes it possible to specify filters like: - heard in the last 2 hours - not heard in the last 4 hours - not heard in the last 3 days 12 hours Any number of hours larger than 23 will be normalized to an equivalent amount of days and hours whenever starting or finishing editing mode. For example, 30 hours will be normalized to 1 day and 6 hours. --- src/common/trackcriteria.h | 3 +- src/common/trackcriteriumevaluation.cpp | 3 +- src/common/util.cpp | 8 +- src/common/util.h | 2 +- src/desktop-remote/trackfilterwidgets.cpp | 89 +++++++++++++++++++---- src/desktop-remote/trackfilterwidgets.h | 3 +- tests/test_util.cpp | 69 +++++++++++++----- 7 files changed, 142 insertions(+), 35 deletions(-) diff --git a/src/common/trackcriteria.h b/src/common/trackcriteria.h index 984aad08..2bc43359 100644 --- a/src/common/trackcriteria.h +++ b/src/common/trackcriteria.h @@ -425,8 +425,9 @@ namespace PMP { int years { 0 }; int days { 0 }; + int hours { 0 }; - bool isZero() const { return years == 0 && days == 0; } + bool isZero() const { return years == 0 && days == 0 && hours == 0; } bool operator==(const CompositeDuration&) const = default; }; diff --git a/src/common/trackcriteriumevaluation.cpp b/src/common/trackcriteriumevaluation.cpp index 0b04f435..8e9560ab 100644 --- a/src/common/trackcriteriumevaluation.cpp +++ b/src/common/trackcriteriumevaluation.cpp @@ -153,7 +153,8 @@ namespace PMP auto startOfDuration = _context.currentDateTimeUtc() .addYears(-duration.years) - .addDays(-duration.days); + .addDays(-duration.days) + .addSecs(-duration.hours * 60 * 60); _result = criterium.isInverted() ? trackLastHeard.value() <= startOfDuration diff --git a/src/common/util.cpp b/src/common/util.cpp index 514a67ca..90b849a8 100644 --- a/src/common/util.cpp +++ b/src/common/util.cpp @@ -67,8 +67,14 @@ namespace PMP hours += extraHours; } - void Util::normalizeLongDuration(int& years, int& days) + void Util::normalizeLongDuration(int& years, int& days, int& hours) { + int extraDays = hours / 24; + int normalizedHours = hours % 24; + + hours = normalizedHours; + days += extraDays; + const int daysInFourYears = 3 * 365 + 366; int fourYearInstances = days / daysInFourYears; diff --git a/src/common/util.h b/src/common/util.h index 39b6ca3b..20d805b6 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -80,7 +80,7 @@ namespace PMP static unsigned getRandomSeed(); static void normalizeDuration(int& hours, int& minutes, int& seconds); - static void normalizeLongDuration(int& years, int& days); + static void normalizeLongDuration(int& years, int& days, int& hours); static QString secondsToHoursMinuteSecondsText(qint32 totalSeconds); static QString millisecondsToShortDisplayTimeText(qint64 milliseconds); diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index c7802e02..98f977f7 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -160,6 +160,8 @@ namespace PMP auto isInverted = criterium.isInverted(); auto duration = criterium.duration(); + Util::normalizeLongDuration(duration.years, duration.days, duration.hours); + if (duration.isZero()) { if (isInverted) @@ -171,7 +173,7 @@ namespace PMP _caption = tr("heard in the last 0 seconds"); } } - else if (duration.years > 0 && duration.days == 0) + else if (duration.years > 0 && duration.days == 0 && duration.hours == 0) { if (isInverted) { @@ -182,7 +184,7 @@ namespace PMP _caption = tr("heard in the last %1 year(s)").arg(duration.years); } } - else if (duration.days > 0 && duration.years == 0) + else if (duration.days > 0 && duration.years == 0 && duration.hours == 0) { if (isInverted) { @@ -193,21 +195,64 @@ namespace PMP _caption = tr("heard in the last %1 day(s)").arg(duration.days); } } + else if (duration.hours > 0 && duration.years == 0 && duration.days == 0) + { + if (isInverted) + { + _caption = tr("not heard in the last %1 hour(s)").arg(duration.hours); + } + else + { + _caption = tr("heard in the last %1 hour(s)").arg(duration.hours); + } + } + else if (duration.days > 0 && duration.hours > 0 && duration.years == 0) + { + if (isInverted) + { + _caption = tr("not heard in the last %1 day(s) %2 hour(s)") + .arg(duration.days) + .arg(duration.hours); + } + else + { + _caption = tr("heard in the last %1 day(s) %2 hour(s)") + .arg(duration.days) + .arg(duration.hours); + } + } + else if (duration.years > 0 && duration.days > 0 && duration.hours == 0) + { + if (isInverted) + { + _caption = tr("not heard in the last %1 year(s) %2 day(s)") + .arg(duration.years) + .arg(duration.days); + } + else + { + _caption = tr("heard in the last %1 year(s) %2 day(s)") + .arg(duration.years) + .arg(duration.days); + } + } else // catch-all case { if (isInverted) { _caption = - tr("not heard in the last %1 year(s) %2 day(s)") + tr("not heard in the last %1 year(s) %2 day(s) %3 hour(s)") .arg(duration.years) - .arg(duration.days); + .arg(duration.days) + .arg(duration.hours); } else { _caption = - tr("heard in the last %1 year(s) %2 day(s)") + tr("heard in the last %1 year(s) %2 day(s) %3 hour(s)") .arg(duration.years) - .arg(duration.days); + .arg(duration.days) + .arg(duration.hours); } } } @@ -524,6 +569,8 @@ namespace PMP auto* yearsLabel = new QLabel(tr("years")); _daysSpinBox = new QSpinBox(); auto* daysLabel = new QLabel(tr("days")); + _hoursSpinBox = new QSpinBox(); + auto* hoursLabel = new QLabel(tr("hours")); QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -532,6 +579,8 @@ namespace PMP layout->addWidget(yearsLabel); layout->addWidget(_daysSpinBox); layout->addWidget(daysLabel); + layout->addWidget(_hoursSpinBox); + layout->addWidget(hoursLabel); layout->addStretch(); _inversionComboBox->addItem(tr("heard within")); @@ -553,6 +602,11 @@ namespace PMP _daysSpinBox, &QSpinBox::valueChanged, this, [this]() { if (!_suspendChangeSignal) Q_EMIT criteriumChanged(); } ); + + connect( + _hoursSpinBox, &QSpinBox::valueChanged, + this, [this]() { if (!_suspendChangeSignal) Q_EMIT criteriumChanged(); } + ); } void LastHeardEditorWidget::setInverted(bool isInverted) @@ -560,9 +614,9 @@ namespace PMP _inversionComboBox->setCurrentIndex(isInverted ? 1 : 0); } - void LastHeardEditorWidget::setPeriod(int years, int days) + void LastHeardEditorWidget::setPeriod(int years, int days, int hours) { - Util::normalizeLongDuration(years, days); + Util::normalizeLongDuration(years, days, hours); Q_ASSERT_X(years >= 0 && years < 100, "LastHeardEditorWidget::setPeriod", @@ -572,10 +626,15 @@ namespace PMP "LastHeardEditorWidget::setPeriod", "days out of range"); + Q_ASSERT_X(hours >= 0 && hours < 100, + "LastHeardEditorWidget::setPeriod", + "hours out of range"); + _suspendChangeSignal++; _yearsSpinBox->setValue(years); _daysSpinBox->setValue(days); + _hoursSpinBox->setValue(hours); _suspendChangeSignal--; @@ -591,11 +650,15 @@ namespace PMP bool isInverted = inversionIndex == 1; - int years = _yearsSpinBox->value(); - int days = _daysSpinBox->value(); + auto duration = + CompositeDuration + { + .years = _yearsSpinBox->value(), + .days = _daysSpinBox->value(), + .hours = _hoursSpinBox->value() + }; - return std::make_unique( - CompositeDuration { .years = years, .days = days }, isInverted); + return std::make_unique(duration, isInverted); } // =============================================================== // @@ -832,7 +895,7 @@ namespace PMP auto period = criterium.duration(); auto* editor = new LastHeardEditorWidget(_parent); - editor->setPeriod(period.years, period.days); + editor->setPeriod(period.years, period.days, period.hours); editor->setInverted(criterium.isInverted()); _editorWidget = editor; diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index 5283f453..f7994ac2 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -134,7 +134,7 @@ namespace PMP explicit LastHeardEditorWidget(QWidget* parent); void setInverted(bool isInverted); - void setPeriod(int years, int days); + void setPeriod(int years, int days, int hours); std::unique_ptr createCriterium() const override; @@ -142,6 +142,7 @@ namespace PMP QComboBox* _inversionComboBox; QSpinBox* _yearsSpinBox; QSpinBox* _daysSpinBox; + QSpinBox* _hoursSpinBox; quint8 _suspendChangeSignal { 0 }; }; diff --git a/tests/test_util.cpp b/tests/test_util.cpp index 70bd1605..178a797c 100644 --- a/tests/test_util.cpp +++ b/tests/test_util.cpp @@ -279,70 +279,105 @@ void TestUtil::normalizeDuration() void TestUtil::normalizeLongDuration() { - int years, days; + int years, days, hours; { - years = 0; days = 0; - Util::normalizeLongDuration(years, days); + years = 0; days = 0; hours = 0; + Util::normalizeLongDuration(years, days, hours); QCOMPARE(years, 0); QCOMPARE(days, 0); + QCOMPARE(hours, 0); + } + + { + years = 0; days = 0; hours = 4; + Util::normalizeLongDuration(years, days, hours); + + QCOMPARE(years, 0); + QCOMPARE(days, 0); + QCOMPARE(hours, 4); } { - years = 3; days = 0; - Util::normalizeLongDuration(years, days); + years = 0; days = 1; hours = 30; + Util::normalizeLongDuration(years, days, hours); + + QCOMPARE(years, 0); + QCOMPARE(days, 2); + QCOMPARE(hours, 6); + } + + { + years = 0; days = 0; hours = 365 * 24 + 15; + Util::normalizeLongDuration(years, days, hours); + + QCOMPARE(years, 1); + QCOMPARE(days, 0); + QCOMPARE(hours, 15); + } + + { + years = 3; days = 0; hours = 0; + Util::normalizeLongDuration(years, days, hours); QCOMPARE(years, 3); QCOMPARE(days, 0); + QCOMPARE(hours, 0); } { - years = 0; days = 90; - Util::normalizeLongDuration(years, days); + years = 0; days = 90; hours = 0; + Util::normalizeLongDuration(years, days, hours); QCOMPARE(years, 0); QCOMPARE(days, 90); + QCOMPARE(hours, 0); } { - years = 1; days = 7; - Util::normalizeLongDuration(years, days); + years = 1; days = 7; hours = 0; + Util::normalizeLongDuration(years, days, hours); QCOMPARE(years, 1); QCOMPARE(days, 7); + QCOMPARE(hours, 0); } { - years = 2; days = 370; - Util::normalizeLongDuration(years, days); + years = 2; days = 370; hours = 0; + Util::normalizeLongDuration(years, days, hours); QCOMPARE(years, 3); QCOMPARE(days, 5); + QCOMPARE(hours, 0); } { - years = 0; days = 999; - Util::normalizeLongDuration(years, days); + years = 0; days = 999; hours = 0; + Util::normalizeLongDuration(years, days, hours); QCOMPARE(years, 2); QCOMPARE(days, 269); + QCOMPARE(hours, 0); } { - years = 0; days = 365 + 365 + 365 + 365; - Util::normalizeLongDuration(years, days); + years = 0; days = 365 + 365 + 365 + 365; hours = 0; + Util::normalizeLongDuration(years, days, hours); QCOMPARE(years, 3); QCOMPARE(days, 365); + QCOMPARE(hours, 0); } { - years = 0; days = 365 + 365 + 365 + 366; - Util::normalizeLongDuration(years, days); + years = 0; days = 365 + 365 + 365 + 366; hours = 0; + Util::normalizeLongDuration(years, days, hours); QCOMPARE(years, 4); QCOMPARE(days, 0); + QCOMPARE(hours, 0); } } From 313f31ab448ea031fc2cbb9a0de7946237dc5831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Sun, 8 Mar 2026 20:37:25 +0100 Subject: [PATCH 13/22] Change how an extra filter is added The "Add" button to add a new filter now no longer adds an "(empty)" filter. Clicking the button will now open a context menu providing several categories: - score - length - last heard - metadata - status Each category opens a submenu. Clicking an item in the submenu, like "no score" from the "Score" category, will add the filter to the list. As before, some filters can then be edited by clicking on their description or by clicking the edit button on the right. This new system adds a small number of new filters. These are logical opposites of existing predefined filters: - available (opposite of "unavailable" or "no longer available") - with title (opposite of "without title") - with artist (opposite of "without artist") - with album (opposite of "without album") The dropdown of predefined filters is still available for now, by clicking the "reset" button on a filter line. But it will soon be removed. --- src/common/trackcriteria.cpp | 105 +++----- src/common/trackcriteria.h | 112 ++++++++ src/common/trackcriteriumevaluation.cpp | 23 +- src/desktop-remote/clickablelabel.cpp | 23 +- src/desktop-remote/clickablelabel.h | 12 +- src/desktop-remote/trackfilterwidgets.cpp | 298 +++++++++++++++++++--- src/desktop-remote/trackfilterwidgets.h | 16 +- 7 files changed, 468 insertions(+), 121 deletions(-) diff --git a/src/common/trackcriteria.cpp b/src/common/trackcriteria.cpp index 6cbd96c9..10e2a9b1 100644 --- a/src/common/trackcriteria.cpp +++ b/src/common/trackcriteria.cpp @@ -23,42 +23,6 @@ namespace PMP { - namespace - { - std::unique_ptr createLengthLessThanCriterium( - int lengthMinutesCeiling) - { - return std::make_unique( - ComparisonOperator::LessThan, 0, lengthMinutesCeiling, 0); - } - - std::unique_ptr createLengthAtLeastCriterium( - int minimumLengthMinutes) - { - return std::make_unique( - ComparisonOperator::GreaterThanOrEqual, 0, minimumLengthMinutes, 0); - } - - std::unique_ptr createScoreLessThanCriterium(short scoreCeiling) - { - return std::make_unique( - ComparisonOperator::LessThan, scoreCeiling); - } - - std::unique_ptr createScoreAtLeastCriterium(short minimumScore) - { - return std::make_unique( - ComparisonOperator::GreaterThanOrEqual, minimumScore); - } - - std::unique_ptr createNotRecentlyHeardCriterium( - CompositeDuration duration) - { - return std::make_unique( - duration, /* isInverted: */ true); - } - } - std::unique_ptr convertToTrackCriterium( PredefinedTrackCriterium criterium) { @@ -71,107 +35,106 @@ namespace PMP return ConstantTrackCriterium::noTracksMatch(); case PredefinedTrackCriterium::NeverHeard: - return TrackLastHeardPresenceCriterium::lastHeardMustBeAbsent(); + return TrackCriteriumFactory::neverHeard(); case PredefinedTrackCriterium::NotHeardInLast5Years: - return createNotRecentlyHeardCriterium(CompositeDuration { .years = 5 }); + return TrackCriteriumFactory::notRecentlyHeard({ .years = 5 }); case PredefinedTrackCriterium::NotHeardInLast3Years: - return createNotRecentlyHeardCriterium(CompositeDuration { .years = 3 }); + return TrackCriteriumFactory::notRecentlyHeard({ .years = 3 }); case PredefinedTrackCriterium::NotHeardInLast2Years: - return createNotRecentlyHeardCriterium(CompositeDuration { .years = 2 }); + return TrackCriteriumFactory::notRecentlyHeard({ .years = 2 }); case PredefinedTrackCriterium::NotHeardInLastYear: - return createNotRecentlyHeardCriterium(CompositeDuration { .years = 1 }); + return TrackCriteriumFactory::notRecentlyHeard({ .years = 1 }); case PredefinedTrackCriterium::NotHeardInLast180Days: - return createNotRecentlyHeardCriterium(CompositeDuration { .days = 180 }); + return TrackCriteriumFactory::notRecentlyHeard({ .days = 180 }); case PredefinedTrackCriterium::NotHeardInLast90Days: - return createNotRecentlyHeardCriterium(CompositeDuration { .days = 90 }); + return TrackCriteriumFactory::notRecentlyHeard({ .days = 90 }); case PredefinedTrackCriterium::NotHeardInLast30Days: - return createNotRecentlyHeardCriterium(CompositeDuration { .days = 30 }); + return TrackCriteriumFactory::notRecentlyHeard({ .days = 30 }); case PredefinedTrackCriterium::NotHeardInLast10Days: - return createNotRecentlyHeardCriterium(CompositeDuration { .days = 10 }); + return TrackCriteriumFactory::notRecentlyHeard({ .days = 10 }); case PredefinedTrackCriterium::HeardAtLeastOnce: - return TrackLastHeardPresenceCriterium::lastHeardMustBePresent(); + return TrackCriteriumFactory::heardAtLeastOnce(); case PredefinedTrackCriterium::WithoutScore: - return TrackScorePresenceCriterium::scoreMustBeAbsent(); + return TrackCriteriumFactory::scoreMustBeAbsent(); case PredefinedTrackCriterium::WithScore: - return TrackScorePresenceCriterium::scoreMustBePresent(); + return TrackCriteriumFactory::scoreMustBePresent(); case PredefinedTrackCriterium::ScoreLessThan30: - return createScoreLessThanCriterium(30); + return TrackCriteriumFactory::scoreLessThanXPercent(30); case PredefinedTrackCriterium::ScoreLessThan50: - return createScoreLessThanCriterium(50); + return TrackCriteriumFactory::scoreLessThanXPercent(50); case PredefinedTrackCriterium::ScoreAtLeast80: - return createScoreAtLeastCriterium(80); + return TrackCriteriumFactory::scoreAtLeastXPercent(80); case PredefinedTrackCriterium::ScoreAtLeast85: - return createScoreAtLeastCriterium(85); + return TrackCriteriumFactory::scoreAtLeastXPercent(85); case PredefinedTrackCriterium::ScoreAtLeast90: - return createScoreAtLeastCriterium(90); + return TrackCriteriumFactory::scoreAtLeastXPercent(90); case PredefinedTrackCriterium::ScoreAtLeast95: - return createScoreAtLeastCriterium(95); + return TrackCriteriumFactory::scoreAtLeastXPercent(95); case PredefinedTrackCriterium::LengthLessThanOneMinute: - return createLengthLessThanCriterium(1); + return TrackCriteriumFactory::lengthLessThanXMinutes(1); case PredefinedTrackCriterium::LengthAtLeastOneMinute: - return createLengthAtLeastCriterium(1); + return TrackCriteriumFactory::lengthAtLeastXMinutes(1); case PredefinedTrackCriterium::LengthLessThanTwoMinutes: - return createLengthLessThanCriterium(2); + return TrackCriteriumFactory::lengthLessThanXMinutes(2); case PredefinedTrackCriterium::LengthAtLeastTwoMinutes: - return createLengthAtLeastCriterium(2); + return TrackCriteriumFactory::lengthAtLeastXMinutes(2); case PredefinedTrackCriterium::LengthLessThanThreeMinutes: - return createLengthLessThanCriterium(3); + return TrackCriteriumFactory::lengthLessThanXMinutes(3); case PredefinedTrackCriterium::LengthAtLeastThreeMinutes: - return createLengthAtLeastCriterium(3); + return TrackCriteriumFactory::lengthAtLeastXMinutes(3); case PredefinedTrackCriterium::LengthLessThanFourMinutes: - return createLengthLessThanCriterium(4); + return TrackCriteriumFactory::lengthLessThanXMinutes(4); case PredefinedTrackCriterium::LengthAtLeastFourMinutes: - return createLengthAtLeastCriterium(4); + return TrackCriteriumFactory::lengthAtLeastXMinutes(4); case PredefinedTrackCriterium::LengthLessThanFiveMinutes: - return createLengthLessThanCriterium(5); + return TrackCriteriumFactory::lengthLessThanXMinutes(5); case PredefinedTrackCriterium::LengthAtLeastFiveMinutes: - return createLengthAtLeastCriterium(5); + return TrackCriteriumFactory::lengthAtLeastXMinutes(5); case PredefinedTrackCriterium::NotInTheQueue: - return TrackQueuePresenceCriterium::mustBeAbsentInQueue(); + return TrackCriteriumFactory::notInTheQueue(); case PredefinedTrackCriterium::InTheQueue: - return TrackQueuePresenceCriterium::mustBePresentInQueue(); + return TrackCriteriumFactory::inTheQueue(); case PredefinedTrackCriterium::WithoutTitle: - return TrackMetaDataPresenceCriterium::mustBeAbsent(TrackMetaDataKind::Title); + return TrackCriteriumFactory::withoutTitle(); case PredefinedTrackCriterium::WithoutArtist: - return TrackMetaDataPresenceCriterium::mustBeAbsent( - TrackMetaDataKind::Artist); + return TrackCriteriumFactory::withoutArtist(); case PredefinedTrackCriterium::WithoutAlbum: - return TrackMetaDataPresenceCriterium::mustBeAbsent(TrackMetaDataKind::Album); + return TrackCriteriumFactory::withoutAlbum(); case PredefinedTrackCriterium::NoLongerAvailable: - return TrackAvailabilityCriterium::mustBeUnavailable(); + return TrackCriteriumFactory::unavailable(); } /* should be unreachable because we handled all enum values */ diff --git a/src/common/trackcriteria.h b/src/common/trackcriteria.h index 2bc43359..c4e1b210 100644 --- a/src/common/trackcriteria.h +++ b/src/common/trackcriteria.h @@ -675,6 +675,118 @@ namespace PMP private: std::vector> _criteria; }; + + class TrackCriteriumFactory + { + public: + static std::unique_ptr lengthLessThanXMinutes(int minutes) + { + return std::make_unique( + ComparisonOperator::LessThan, 0, minutes, 0); + } + + static std::unique_ptr lengthAtLeastXMinutes(int minutes) + { + return std::make_unique( + ComparisonOperator::GreaterThanOrEqual, 0, minutes, 0); + } + + static std::unique_ptr scoreMustBePresent() + { + return TrackScorePresenceCriterium::scoreMustBePresent(); + } + + static std::unique_ptr scoreMustBeAbsent() + { + return TrackScorePresenceCriterium::scoreMustBeAbsent(); + } + + static std::unique_ptr scoreLessThanXPercent(short percent) + { + return std::make_unique( + ComparisonOperator::LessThan, percent); + } + + static std::unique_ptr scoreAtLeastXPercent(short percent) + { + return std::make_unique( + ComparisonOperator::GreaterThanOrEqual, percent); + } + + static std::unique_ptr notRecentlyHeard( + CompositeDuration duration) + { + return std::make_unique( + duration, /* isInverted: */ true); + } + + static std::unique_ptr neverHeard() + { + return TrackLastHeardPresenceCriterium::lastHeardMustBeAbsent(); + } + + static std::unique_ptr heardAtLeastOnce() + { + return TrackLastHeardPresenceCriterium::lastHeardMustBePresent(); + } + + static std::unique_ptr inTheQueue() + { + return TrackQueuePresenceCriterium::mustBePresentInQueue(); + } + + static std::unique_ptr notInTheQueue() + { + return TrackQueuePresenceCriterium::mustBeAbsentInQueue(); + } + + static std::unique_ptr available() + { + return TrackAvailabilityCriterium::mustBeAvailable(); + } + + static std::unique_ptr unavailable() + { + return TrackAvailabilityCriterium::mustBeUnavailable(); + } + + static std::unique_ptr withTitle() + { + return TrackMetaDataPresenceCriterium::mustBePresent( + TrackMetaDataKind::Title); + } + + static std::unique_ptr withoutTitle() + { + return TrackMetaDataPresenceCriterium::mustBeAbsent(TrackMetaDataKind::Title); + } + + static std::unique_ptr withArtist() + { + return TrackMetaDataPresenceCriterium::mustBePresent( + TrackMetaDataKind::Artist); + } + + static std::unique_ptr withoutArtist() + { + return TrackMetaDataPresenceCriterium::mustBeAbsent( + TrackMetaDataKind::Artist); + } + + static std::unique_ptr withAlbum() + { + return TrackMetaDataPresenceCriterium::mustBePresent( + TrackMetaDataKind::Album); + } + + static std::unique_ptr withoutAlbum() + { + return TrackMetaDataPresenceCriterium::mustBeAbsent(TrackMetaDataKind::Album); + } + + private: + TrackCriteriumFactory() = delete; + }; } Q_DECLARE_METATYPE(PMP::PredefinedTrackCriterium) diff --git a/src/common/trackcriteriumevaluation.cpp b/src/common/trackcriteriumevaluation.cpp index 8e9560ab..c8a82e20 100644 --- a/src/common/trackcriteriumevaluation.cpp +++ b/src/common/trackcriteriumevaluation.cpp @@ -46,6 +46,14 @@ namespace PMP return s.value().trimmed().isEmpty(); } + + TriBool stringIsNotEmpty(Nullable s) + { + if (s.isNull()) + return TriBool::unknown; + + return !s.value().trimmed().isEmpty(); + } } TriBool TrackCriteriumEvaluator::evaluate(const TrackCriterium& criterium, @@ -176,15 +184,24 @@ namespace PMP switch (criterium.metaDataKind()) { case TrackMetaDataKind::Title: - _result = stringIsEmpty(_context.title()); + _result = + criterium.presence() + ? stringIsNotEmpty(_context.title()) + : stringIsEmpty(_context.title()); return; case TrackMetaDataKind::Artist: - _result = stringIsEmpty(_context.artist()); + _result = + criterium.presence() + ? stringIsNotEmpty(_context.artist()) + : stringIsEmpty(_context.artist()); return; case TrackMetaDataKind::Album: - _result = stringIsEmpty(_context.album()); + _result = + criterium.presence() + ? stringIsNotEmpty(_context.album()) + : stringIsEmpty(_context.album()); return; } diff --git a/src/desktop-remote/clickablelabel.cpp b/src/desktop-remote/clickablelabel.cpp index d71d304d..f664cf4d 100644 --- a/src/desktop-remote/clickablelabel.cpp +++ b/src/desktop-remote/clickablelabel.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2021, Kevin Andre + Copyright (C) 2021-2026, Kevin André This file is part of PMP (Party Music Player). @@ -23,16 +23,25 @@ namespace PMP { - ClickableLabel::ClickableLabel(QWidget* parent, Qt::WindowFlags f) : QLabel(parent, f) { - setCursor(QCursor(Qt::PointingHandCursor)); + setCursor(Qt::PointingHandCursor); } - ClickableLabel::~ClickableLabel() + void ClickableLabel::setClickable(bool clickable) { - // + if (_clickable == clickable) + return; + + _clickable = clickable; + + if (_clickable) + setCursor(Qt::PointingHandCursor); + else + unsetCursor(); + + Q_EMIT clickableChanged(); } ClickableLabel* ClickableLabel::replace(QLabel*& existingLabel) @@ -55,7 +64,9 @@ namespace PMP { Q_UNUSED(event) + if (!_clickable) + return; + Q_EMIT clicked(); } - } diff --git a/src/desktop-remote/clickablelabel.h b/src/desktop-remote/clickablelabel.h index ce845d13..64debd61 100644 --- a/src/desktop-remote/clickablelabel.h +++ b/src/desktop-remote/clickablelabel.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2021, Kevin Andre + Copyright (C) 2021-2026, Kevin André This file is part of PMP (Party Music Player). @@ -24,22 +24,28 @@ namespace PMP { - class ClickableLabel : public QLabel { Q_OBJECT public: explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); - ~ClickableLabel(); + ~ClickableLabel() = default; + + bool clickable() const { return _clickable; } + void setClickable(bool clickable); static ClickableLabel* replace(QLabel*& existingLabel); Q_SIGNALS: + void clickableChanged(); void clicked(); protected: void mousePressEvent(QMouseEvent* event) override; + + private: + bool _clickable { true }; }; } #endif diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 98f977f7..4bf65b21 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,7 @@ namespace PMP : QWidget(parent) { _label = new ClickableLabel(); + _label->setClickable(false); QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -58,9 +60,13 @@ namespace PMP if (!_criterium) { _label->clear(); + _label->setClickable(false); return; } + bool isEditable = FilterEditorFactory::isEditable(*_criterium); + _label->setClickable(isEditable); + CriteriumCaptionGenerator visitor; _criterium->accept(visitor); auto caption = visitor.caption(); @@ -129,9 +135,9 @@ namespace PMP const TrackScorePresenceCriterium& criterium) { if (criterium.presence()) - _caption = tr("with score"); + _caption = tr("has score"); else - _caption = tr("without score"); + _caption = tr("no score"); } void FilterLabelWidget::CriteriumCaptionGenerator::visit( @@ -766,7 +772,7 @@ namespace PMP return visitor.isCriteriumEditable(); } - FilterEditorWidget* FilterEditorFactory::createEditor(QWidget* parent, + FilterEditorWidget* FilterEditorFactory::createFromCriterium(QWidget* parent, const TrackCriterium& criterium) { EditorWidgetCreationVisitor visitor(parent); @@ -928,11 +934,30 @@ namespace PMP // =============================================================== // FilterLineWidget::FilterLineWidget() - : _labelWidget(nullptr), - _editorWidget(nullptr) { + init(); // default initialization with picker + } + + FilterLineWidget::FilterLineWidget(std::unique_ptr criterium) + { + init(); // default initialization with picker + + switchToLabel(_filterPicker, std::move(criterium)); + } + + FilterLineWidget::FilterLineWidget(FilterEditorWidget* editor) + { + init(); // default initialization with picker + + switchToEditor(_filterPicker, editor); + } + + void FilterLineWidget::init() + { + _labelWidget = nullptr; _filterPicker = new FilterPickerWidget(PredefinedTrackCriterium::AllTracks, tr("(empty)")); + _editorWidget = nullptr; _editButton = new QPushButton(); _doneButton = new QPushButton(); _deleteButton = new QPushButton(); @@ -946,15 +971,14 @@ namespace PMP layout->addWidget(_deleteButton, 0); layout->addWidget(_resetButton, 0); - _editButton->setEnabled(false); _editButton->setIcon( QIcon::fromTheme("document-edit", style()->standardIcon(QStyle::SP_FileDialogDetailedView))); _editButton->setToolTip(tr("Edit filter")); - _doneButton->setVisible(false); - _doneButton->setIcon(QIcon::fromTheme("dialog-ok-apply", - style()->standardIcon(QStyle::SP_DialogApplyButton))); + _doneButton->setIcon( + QIcon::fromTheme("dialog-ok-apply", + style()->standardIcon(QStyle::SP_DialogApplyButton))); _doneButton->setToolTip(tr("Done editing")); _deleteButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); @@ -963,6 +987,9 @@ namespace PMP _resetButton->setIcon(style()->standardIcon(QStyle::SP_LineEditClearButton)); _resetButton->setToolTip(tr("Clear filter")); + _editButton->setEnabled(false); + _doneButton->setVisible(false); + connect( _filterPicker, &FilterPickerWidget::criteriumChanged, this, &FilterLineWidget::onPickerCriteriumChanged @@ -1049,6 +1076,8 @@ namespace PMP _editButton->setVisible(true); _editButton->setEnabled(false); _doneButton->setVisible(false); + + Q_EMIT criteriumChanged(); } void FilterLineWidget::switchEditorToLabel() @@ -1072,6 +1101,8 @@ namespace PMP "FilterLineWidget::switchToLabel", "label widget already present!"); + bool isEditable = FilterEditorFactory::isEditable(*criterium); + _labelWidget = new FilterLabelWidget(nullptr); _labelWidget->setCriterium(std::move(criterium)); @@ -1083,7 +1114,7 @@ namespace PMP layout()->replaceWidget(widgetToReplace, _labelWidget); widgetToReplace->setVisible(false); - _editButton->setEnabled(true); + _editButton->setEnabled(isEditable); _editButton->setVisible(true); _doneButton->setVisible(false); } @@ -1115,12 +1146,19 @@ namespace PMP void FilterLineWidget::switchToEditor(QWidget* widgetToReplace, const TrackCriterium& criterium) + { + switchToEditor(widgetToReplace, + FilterEditorFactory::createFromCriterium(nullptr, criterium)); + } + + void FilterLineWidget::switchToEditor(QWidget *widgetToReplace, + FilterEditorWidget* editor) { Q_ASSERT_X(_editorWidget == nullptr, "FilterLineWidget::switchToEditor", "editor already present!"); - _editorWidget = FilterEditorFactory::createEditor(nullptr, criterium); + _editorWidget = editor; Q_ASSERT_X(_editorWidget != nullptr, "FilterLineWidget::switchToEditor", @@ -1146,27 +1184,20 @@ namespace PMP _verticalLayout = new QVBoxLayout(this); _verticalLayout->setContentsMargins(0, 0, 0, 0); - addFilterLine(); + addFilterLine(new FilterLineWidget()); auto* buttonsLayout = new QHBoxLayout(); _verticalLayout->addLayout(buttonsLayout); - _addButton = new QPushButton(tr("Add")); - _addButton->setToolTip(tr("Add filter")); + _addMenuButton = new QPushButton(tr("Add…")); + _addMenuButton->setToolTip(tr("Add filter")); - buttonsLayout->addWidget(_addButton); + buttonsLayout->addWidget(_addMenuButton); buttonsLayout->addStretch(); connect( - _addButton, &QPushButton::clicked, - this, - [this]() - { - addFilterLine(); - - // emit is not necessary because the new filter is "none" - //Q_EMIT criteriaChanged(); - } + _addMenuButton, &QPushButton::clicked, + this, &FiltersListWidget::showAddMenu ); } @@ -1182,17 +1213,214 @@ namespace PMP return compositeCriterium; } - void FiltersListWidget::addFilterLine() + void FiltersListWidget::showAddMenu() { - auto* filter = new FilterLineWidget(); + QMenu menu(this); + + // Category: Score + QMenu* scoreMenu = menu.addMenu(tr("Score")); + + scoreMenu->addAction( + tr("Less than 30"), + [this]() { addFilterLine(TrackCriteriumFactory::scoreLessThanXPercent(30)); } + ); + + scoreMenu->addAction( + tr("Less than 50"), + [this]() { addFilterLine(TrackCriteriumFactory::scoreLessThanXPercent(50)); } + ); + + scoreMenu->addAction( + tr("At least 80"), + [this]() { addFilterLine(TrackCriteriumFactory::scoreAtLeastXPercent(80)); } + ); + + scoreMenu->addAction( + tr("At least 85"), + [this]() { addFilterLine(TrackCriteriumFactory::scoreAtLeastXPercent(85)); } + ); + + scoreMenu->addAction( + tr("At least 90"), + [this]() { addFilterLine(TrackCriteriumFactory::scoreAtLeastXPercent(90)); } + ); + + scoreMenu->addSeparator(); + + scoreMenu->addAction( + tr("Has score"), + [this]() { addFilterLine(TrackCriteriumFactory::scoreMustBePresent()); } + ); + + scoreMenu->addAction( + tr("No score"), + [this]() { addFilterLine(TrackCriteriumFactory::scoreMustBeAbsent()); } + ); + + // Category: Length + QMenu* lengthMenu = menu.addMenu(tr("Length")); + lengthMenu->addAction( + tr("Less than 3 minutes"), + [this]() { addFilterLine(TrackCriteriumFactory::lengthLessThanXMinutes(3)); } + ); + + lengthMenu->addAction( + tr("At least 3 minutes"), + [this]() { addFilterLine(TrackCriteriumFactory::lengthAtLeastXMinutes(3)); } + ); + + lengthMenu->addAction( + tr("Less than 4 minutes"), + [this]() { addFilterLine(TrackCriteriumFactory::lengthLessThanXMinutes(4)); } + ); + + lengthMenu->addAction( + tr("At least 4 minutes"), + [this]() { addFilterLine(TrackCriteriumFactory::lengthAtLeastXMinutes(4)); } + ); + + lengthMenu->addAction( + tr("Less than 5 minutes"), + [this]() { addFilterLine(TrackCriteriumFactory::lengthLessThanXMinutes(5)); } + ); + + lengthMenu->addAction( + tr("At least 5 minutes"), + [this]() { addFilterLine(TrackCriteriumFactory::lengthAtLeastXMinutes(5)); } + ); + + // Category: last heard + QMenu* lastHeardMenu = menu.addMenu(tr("Last heard")); + + lastHeardMenu->addAction( + tr("More than 2 years ago"), + [this]() { addFilterLine( + TrackCriteriumFactory::notRecentlyHeard({ .years = 2 })); } + ); + + lastHeardMenu->addAction( + tr("More than a year ago"), + [this]() { addFilterLine( + TrackCriteriumFactory::notRecentlyHeard({ .years = 1 })); } + ); + + lastHeardMenu->addAction( + tr("More than 90 days ago"), + [this]() { addFilterLine( + TrackCriteriumFactory::notRecentlyHeard({ .days = 90 })); } + ); + + lastHeardMenu->addAction( + tr("More than 7 days ago"), + [this]() { addFilterLine( + TrackCriteriumFactory::notRecentlyHeard({ .days = 7 })); } + ); + + lastHeardMenu->addAction( + tr("More than 8 hours ago"), + [this]() { addFilterLine( + TrackCriteriumFactory::notRecentlyHeard({ .hours = 8 })); } + ); + + lastHeardMenu->addAction( + tr("More than an hour ago"), + [this]() { addFilterLine( + TrackCriteriumFactory::notRecentlyHeard({ .hours = 1 })); } + ); + + lastHeardMenu->addSeparator(); + + lastHeardMenu->addAction( + tr("Never"), + [this]() { addFilterLine(TrackCriteriumFactory::neverHeard()); } + ); + + lastHeardMenu->addAction( + tr("At least once"), + [this]() { addFilterLine(TrackCriteriumFactory::heardAtLeastOnce()); } + ); + + + // Category: Metadata + QMenu* metadataMenu = menu.addMenu(tr("Metadata")); + + metadataMenu->addAction( + tr("With title"), + [this]() { addFilterLine(TrackCriteriumFactory::withTitle()); } + ); + + metadataMenu->addAction( + tr("With artist"), + [this]() { addFilterLine(TrackCriteriumFactory::withArtist()); } + ); + + metadataMenu->addAction( + tr("With album"), + [this]() { addFilterLine(TrackCriteriumFactory::withAlbum()); } + ); + + metadataMenu->addSeparator(); + + metadataMenu->addAction( + tr("Without title"), + [this]() { addFilterLine(TrackCriteriumFactory::withoutTitle()); } + ); + + metadataMenu->addAction( + tr("Without artist"), + [this]() { addFilterLine(TrackCriteriumFactory::withoutArtist()); } + ); + + metadataMenu->addAction( + tr("Without album"), + [this]() { addFilterLine(TrackCriteriumFactory::withoutAlbum()); } + ); + + // Category: Status + QMenu* statusMenu = menu.addMenu(tr("Status")); + + statusMenu->addAction( + tr("In queue"), + [this]() { addFilterLine(TrackCriteriumFactory::inTheQueue()); } + ); + + statusMenu->addAction( + tr("Not in queue"), + [this]() { addFilterLine(TrackCriteriumFactory::notInTheQueue()); } + ); + + statusMenu->addSeparator(); + + statusMenu->addAction( + tr("Available"), + [this]() { addFilterLine(TrackCriteriumFactory::available()); } + ); + + statusMenu->addAction( + tr("Unavailable"), + [this]() { addFilterLine(TrackCriteriumFactory::unavailable()); } + ); + + // Show menu below the button + QPoint pos = _addMenuButton->mapToGlobal(QPoint(0, _addMenuButton->height())); + menu.exec(pos); + } + + void FiltersListWidget::addFilterLine(std::unique_ptr criterium) + { + addFilterLine(new FilterLineWidget(std::move(criterium))); + } + + void FiltersListWidget::addFilterLine(FilterLineWidget* filterLine) + { auto index = _filters.size(); - _verticalLayout->insertWidget(index, filter); + _verticalLayout->insertWidget(index, filterLine); - _filters.append(filter); + _filters.append(filterLine); connect( - filter, &FilterLineWidget::criteriumChanged, + filterLine, &FilterLineWidget::criteriumChanged, this, [this]() { @@ -1201,22 +1429,24 @@ namespace PMP ); connect( - filter, &FilterLineWidget::deleteClicked, + filterLine, &FilterLineWidget::deleteClicked, this, - [this, filter]() + [this, filterLine]() { - auto index = _filters.indexOf(filter); + auto index = _filters.indexOf(filterLine); Q_ASSERT_X( index >= 0, "FiltersListWidget::addFilterLine", "filter to be deleted not found" - ); + ); _filters.removeAt(index); - filter->deleteLater(); + filterLine->deleteLater(); Q_EMIT criteriumChanged(); } ); + + Q_EMIT criteriumChanged(); } } diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index f7994ac2..bd59e8ec 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -168,8 +168,8 @@ namespace PMP { public: static bool isEditable(TrackCriterium const& criterium); - static FilterEditorWidget* createEditor(QWidget* parent, - TrackCriterium const& criterium); + static FilterEditorWidget* createFromCriterium(QWidget* parent, + TrackCriterium const& criterium); private: class IsEditableVisitor final : public TrackCriteriumVisitor @@ -223,6 +223,8 @@ namespace PMP Q_OBJECT public: FilterLineWidget(); + FilterLineWidget(std::unique_ptr criterium); + FilterLineWidget(FilterEditorWidget* editor); std::unique_ptr createCriterium() const; @@ -236,12 +238,14 @@ namespace PMP void onResetClicked(); private: + void init(); void switchEditorToLabel(); void switchToLabel(QWidget* widgetToReplace, std::unique_ptr criterium); void switchLabelToEditor(); void switchPickerToEditor(); void switchToEditor(QWidget* widgetToReplace, TrackCriterium const& criterium); + void switchToEditor(QWidget* widgetToReplace, FilterEditorWidget* editor); FilterLabelWidget* _labelWidget; FilterPickerWidget* _filterPicker; @@ -263,10 +267,14 @@ namespace PMP Q_SIGNALS: void criteriumChanged(); + private Q_SLOTS: + void showAddMenu(); + private: - void addFilterLine(); + void addFilterLine(std::unique_ptr criterium); + void addFilterLine(FilterLineWidget* filterLine); - QPushButton* _addButton; + QPushButton* _addMenuButton; QVBoxLayout* _verticalLayout; QList _filters; }; From 76f00b623e343aeb06d792aa998621b9b1bd402b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Wed, 18 Mar 2026 16:24:53 +0100 Subject: [PATCH 14/22] Do not add an empty filter line at startup --- src/desktop-remote/trackfilterwidgets.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 4bf65b21..2fdfda4b 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -1184,8 +1184,6 @@ namespace PMP _verticalLayout = new QVBoxLayout(this); _verticalLayout->setContentsMargins(0, 0, 0, 0); - addFilterLine(new FilterLineWidget()); - auto* buttonsLayout = new QHBoxLayout(); _verticalLayout->addLayout(buttonsLayout); From 64e8d7a37e11fe214769992675db6d2c02e6b92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Mon, 23 Mar 2026 21:28:18 +0100 Subject: [PATCH 15/22] Eliminate the combobox from the filter line UI Remove the combobox with predefined track filter criteria from the filter line UI. This combobox was only still used for the empty filter line state (after clicking the reset button). Replace this combobox with an empty state displaying a label with the text "(empty)". Clicking the label will open a context menu that allows selecting a filter criterium again. Note that a combobox is still used for the track highlighting criterium. --- src/desktop-remote/clickablelabel.cpp | 3 +- src/desktop-remote/clickablelabel.h | 2 +- src/desktop-remote/trackfilterwidgets.cpp | 528 ++++++++++++---------- src/desktop-remote/trackfilterwidgets.h | 8 +- 4 files changed, 294 insertions(+), 247 deletions(-) diff --git a/src/desktop-remote/clickablelabel.cpp b/src/desktop-remote/clickablelabel.cpp index f664cf4d..81575a6c 100644 --- a/src/desktop-remote/clickablelabel.cpp +++ b/src/desktop-remote/clickablelabel.cpp @@ -20,6 +20,7 @@ #include "clickablelabel.h" #include +#include namespace PMP { @@ -67,6 +68,6 @@ namespace PMP if (!_clickable) return; - Q_EMIT clicked(); + Q_EMIT clicked(event->pos()); } } diff --git a/src/desktop-remote/clickablelabel.h b/src/desktop-remote/clickablelabel.h index 64debd61..5cbea610 100644 --- a/src/desktop-remote/clickablelabel.h +++ b/src/desktop-remote/clickablelabel.h @@ -39,7 +39,7 @@ namespace PMP Q_SIGNALS: void clickableChanged(); - void clicked(); + void clicked(QPoint position); protected: void mousePressEvent(QMouseEvent* event) override; diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 2fdfda4b..3de52d7d 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -26,6 +26,7 @@ #include "clickablelabel.h" #include +#include #include #include #include @@ -34,6 +35,8 @@ #include #include +#include + namespace PMP { FilterLabelWidget::FilterLabelWidget(QWidget *parent) @@ -511,6 +514,217 @@ namespace PMP return comparisonOperator; } + + inline QString filtersMenuTr(const char* text) + { + return QCoreApplication::translate("TrackFilterMenu", text); + } + + void displayFiltersPopupMenu(QWidget* parent, QPoint globalPopupPosition, + std::function)> setFilter) + { + QMenu menu(parent); + + // Category: Score + QMenu* scoreMenu = menu.addMenu(filtersMenuTr("Score")); + + scoreMenu->addAction( + filtersMenuTr("Less than 30"), + [setFilter]() { setFilter( + TrackCriteriumFactory::scoreLessThanXPercent(30)); } + ); + + scoreMenu->addAction( + filtersMenuTr("Less than 50"), + [setFilter]() { setFilter( + TrackCriteriumFactory::scoreLessThanXPercent(50)); } + ); + + scoreMenu->addAction( + filtersMenuTr("At least 80"), + [setFilter]() { setFilter( + TrackCriteriumFactory::scoreAtLeastXPercent(80)); } + ); + + scoreMenu->addAction( + filtersMenuTr("At least 85"), + [setFilter]() { setFilter( + TrackCriteriumFactory::scoreAtLeastXPercent(85)); } + ); + + scoreMenu->addAction( + filtersMenuTr("At least 90"), + [setFilter]() { setFilter( + TrackCriteriumFactory::scoreAtLeastXPercent(90)); } + ); + + scoreMenu->addSeparator(); + + scoreMenu->addAction( + filtersMenuTr("Has score"), + [setFilter]() { setFilter( + TrackCriteriumFactory::scoreMustBePresent()); } + ); + + scoreMenu->addAction( + filtersMenuTr("No score"), + [setFilter]() { setFilter( + TrackCriteriumFactory::scoreMustBeAbsent()); } + ); + + // Category: Length + QMenu* lengthMenu = menu.addMenu(filtersMenuTr("Length")); + + lengthMenu->addAction( + filtersMenuTr("Less than 3 minutes"), + [setFilter]() { setFilter( + TrackCriteriumFactory::lengthLessThanXMinutes(3)); } + ); + + lengthMenu->addAction( + filtersMenuTr("At least 3 minutes"), + [setFilter]() { setFilter( + TrackCriteriumFactory::lengthAtLeastXMinutes(3)); } + ); + + lengthMenu->addAction( + filtersMenuTr("Less than 4 minutes"), + [setFilter]() { setFilter( + TrackCriteriumFactory::lengthLessThanXMinutes(4)); } + ); + + lengthMenu->addAction( + filtersMenuTr("At least 4 minutes"), + [setFilter]() { setFilter( + TrackCriteriumFactory::lengthAtLeastXMinutes(4)); } + ); + + lengthMenu->addAction( + filtersMenuTr("Less than 5 minutes"), + [setFilter]() { setFilter( + TrackCriteriumFactory::lengthLessThanXMinutes(5)); } + ); + + lengthMenu->addAction( + filtersMenuTr("At least 5 minutes"), + [setFilter]() { setFilter( + TrackCriteriumFactory::lengthAtLeastXMinutes(5)); } + ); + + // Category: last heard + QMenu* lastHeardMenu = menu.addMenu(filtersMenuTr("Last heard")); + + lastHeardMenu->addAction( + filtersMenuTr("More than 2 years ago"), + [setFilter]() { setFilter( + TrackCriteriumFactory::notRecentlyHeard({ .years = 2 })); } + ); + + lastHeardMenu->addAction( + filtersMenuTr("More than a year ago"), + [setFilter]() { setFilter( + TrackCriteriumFactory::notRecentlyHeard({ .years = 1 })); } + ); + + lastHeardMenu->addAction( + filtersMenuTr("More than 90 days ago"), + [setFilter]() { setFilter( + TrackCriteriumFactory::notRecentlyHeard({ .days = 90 })); } + ); + + lastHeardMenu->addAction( + filtersMenuTr("More than 7 days ago"), + [setFilter]() { setFilter( + TrackCriteriumFactory::notRecentlyHeard({ .days = 7 })); } + ); + + lastHeardMenu->addAction( + filtersMenuTr("More than 8 hours ago"), + [setFilter]() { setFilter( + TrackCriteriumFactory::notRecentlyHeard({ .hours = 8 })); } + ); + + lastHeardMenu->addAction( + filtersMenuTr("More than an hour ago"), + [setFilter]() { setFilter( + TrackCriteriumFactory::notRecentlyHeard({ .hours = 1 })); } + ); + + lastHeardMenu->addSeparator(); + + lastHeardMenu->addAction( + filtersMenuTr("Never"), + [setFilter]() { setFilter(TrackCriteriumFactory::neverHeard()); } + ); + + lastHeardMenu->addAction( + filtersMenuTr("At least once"), + [setFilter]() { setFilter(TrackCriteriumFactory::heardAtLeastOnce()); } + ); + + + // Category: Metadata + QMenu* metadataMenu = menu.addMenu(filtersMenuTr("Metadata")); + + metadataMenu->addAction( + filtersMenuTr("With title"), + [setFilter]() { setFilter(TrackCriteriumFactory::withTitle()); } + ); + + metadataMenu->addAction( + filtersMenuTr("With artist"), + [setFilter]() { setFilter(TrackCriteriumFactory::withArtist()); } + ); + + metadataMenu->addAction( + filtersMenuTr("With album"), + [setFilter]() { setFilter(TrackCriteriumFactory::withAlbum()); } + ); + + metadataMenu->addSeparator(); + + metadataMenu->addAction( + filtersMenuTr("Without title"), + [setFilter]() { setFilter(TrackCriteriumFactory::withoutTitle()); } + ); + + metadataMenu->addAction( + filtersMenuTr("Without artist"), + [setFilter]() { setFilter(TrackCriteriumFactory::withoutArtist()); } + ); + + metadataMenu->addAction( + filtersMenuTr("Without album"), + [setFilter]() { setFilter(TrackCriteriumFactory::withoutAlbum()); } + ); + + // Category: Status + QMenu* statusMenu = menu.addMenu(filtersMenuTr("Status")); + + statusMenu->addAction( + filtersMenuTr("In queue"), + [setFilter]() { setFilter(TrackCriteriumFactory::inTheQueue()); } + ); + + statusMenu->addAction( + filtersMenuTr("Not in queue"), + [setFilter]() { setFilter(TrackCriteriumFactory::notInTheQueue()); } + ); + + statusMenu->addSeparator(); + + statusMenu->addAction( + filtersMenuTr("Available"), + [setFilter]() { setFilter(TrackCriteriumFactory::available()); } + ); + + statusMenu->addAction( + filtersMenuTr("Unavailable"), + [setFilter]() { setFilter(TrackCriteriumFactory::unavailable()); } + ); + + menu.exec(globalPopupPosition); + } } // =============================================================== // @@ -935,42 +1149,44 @@ namespace PMP FilterLineWidget::FilterLineWidget() { - init(); // default initialization with picker + init(); // default initialization to empty filter } FilterLineWidget::FilterLineWidget(std::unique_ptr criterium) { - init(); // default initialization with picker + init(); // default initialization to empty filter - switchToLabel(_filterPicker, std::move(criterium)); + switchToLabel(_emptyFilterLabel, std::move(criterium)); } FilterLineWidget::FilterLineWidget(FilterEditorWidget* editor) { - init(); // default initialization with picker + init(); // default initialization to empty filter - switchToEditor(_filterPicker, editor); + switchToEditor(_emptyFilterLabel, editor); } void FilterLineWidget::init() { + _emptyFilterLabel = new ClickableLabel(); _labelWidget = nullptr; - _filterPicker = new FilterPickerWidget(PredefinedTrackCriterium::AllTracks, - tr("(empty)")); _editorWidget = nullptr; _editButton = new QPushButton(); _doneButton = new QPushButton(); _deleteButton = new QPushButton(); _resetButton = new QPushButton(); + _isEmpty = true; QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(_filterPicker, 1); + layout->addWidget(_emptyFilterLabel, 1); layout->addWidget(_editButton, 0); layout->addWidget(_doneButton, 0); layout->addWidget(_deleteButton, 0); layout->addWidget(_resetButton, 0); + _emptyFilterLabel->setText(tr("(empty)")); + _editButton->setIcon( QIcon::fromTheme("document-edit", style()->standardIcon(QStyle::SP_FileDialogDetailedView))); @@ -987,12 +1203,12 @@ namespace PMP _resetButton->setIcon(style()->standardIcon(QStyle::SP_LineEditClearButton)); _resetButton->setToolTip(tr("Clear filter")); - _editButton->setEnabled(false); + _editButton->setVisible(false); _doneButton->setVisible(false); connect( - _filterPicker, &FilterPickerWidget::criteriumChanged, - this, &FilterLineWidget::onPickerCriteriumChanged + _emptyFilterLabel, &ClickableLabel::clicked, + this, &FilterLineWidget::onEmptyLabelClicked ); connect( @@ -1024,60 +1240,76 @@ namespace PMP if (_editorWidget) return _editorWidget->createCriterium(); - return _filterPicker->createCriterium(); - } + Q_ASSERT_X(_isEmpty, + "FilterLineWidget::createCriterium", + "should be empty at his point"); + return ConstantTrackCriterium::noTracksMatch(); + } - void FilterLineWidget::onPickerCriteriumChanged() + void FilterLineWidget::onEmptyLabelClicked(QPoint position) { - if (_labelWidget || _editorWidget) - return; + QPoint globalPosition = _emptyFilterLabel->mapToGlobal(position); - auto criterium = _filterPicker->createCriterium(); - bool isEditable = FilterEditorFactory::isEditable(*criterium); - _editButton->setEnabled(isEditable); + displayFiltersPopupMenu( + this, + globalPosition, + [this](auto criterium) + { + switchToLabel(_emptyFilterLabel, std::move(criterium)); - Q_EMIT criteriumChanged(); + Q_EMIT criteriumChanged(); + } + ); } void FilterLineWidget::onEditClicked() { - if (_labelWidget) - { - switchLabelToEditor(); - } - else - { - switchPickerToEditor(); - } + Q_ASSERT_X(_labelWidget != nullptr, + "FilterLineWidget::onEditClicked", + "label widget must be present"); + + switchLabelToEditor(); } void FilterLineWidget::onResetClicked() { + if (_isEmpty) + return; + if (_labelWidget) { - layout()->replaceWidget(_labelWidget, _filterPicker); - _labelWidget->setVisible(false); + switchToEmpty(_labelWidget); + _labelWidget->deleteLater(); _labelWidget = nullptr; } - - if (_editorWidget) + else if (_editorWidget) { - layout()->replaceWidget(_editorWidget, _filterPicker); - _editorWidget->setVisible(false); + switchToEmpty(_editorWidget); + _editorWidget->deleteLater(); _editorWidget = nullptr; } + else + { + Q_UNREACHABLE(); + } - _filterPicker->clearCriterium(); - _filterPicker->setVisible(true); + Q_EMIT criteriumChanged(); + } - _editButton->setVisible(true); - _editButton->setEnabled(false); - _doneButton->setVisible(false); + void FilterLineWidget::switchToEmpty(QWidget* widgetToReplace) + { + layout()->replaceWidget(widgetToReplace, _emptyFilterLabel); + widgetToReplace->setVisible(false); - Q_EMIT criteriumChanged(); + _isEmpty = true; + + _emptyFilterLabel->setVisible(true); + _editButton->setVisible(false); + _doneButton->setVisible(false); + _resetButton->setEnabled(false); } void FilterLineWidget::switchEditorToLabel() @@ -1114,9 +1346,11 @@ namespace PMP layout()->replaceWidget(widgetToReplace, _labelWidget); widgetToReplace->setVisible(false); - _editButton->setEnabled(isEditable); - _editButton->setVisible(true); + _isEmpty = false; + + _editButton->setVisible(isEditable); _doneButton->setVisible(false); + _resetButton->setEnabled(true); } void FilterLineWidget::switchLabelToEditor() @@ -1133,17 +1367,6 @@ namespace PMP _labelWidget = nullptr; } - void FilterLineWidget::switchPickerToEditor() - { - Q_ASSERT_X(_labelWidget == nullptr, - "FilterLineWidget::switchPickerToEditor", - "label widget present!"); - - auto criterium = _filterPicker->createCriterium(); - - switchToEditor(_filterPicker, *criterium); - } - void FilterLineWidget::switchToEditor(QWidget* widgetToReplace, const TrackCriterium& criterium) { @@ -1172,9 +1395,11 @@ namespace PMP layout()->replaceWidget(widgetToReplace, _editorWidget); widgetToReplace->setVisible(false); - _editButton->setEnabled(false); + _isEmpty = false; + _editButton->setVisible(false); _doneButton->setVisible(true); + _resetButton->setEnabled(true); } // =============================================================== // @@ -1205,6 +1430,9 @@ namespace PMP for (auto const* filterLine : _filters) { + if (filterLine->isEmpty()) + continue; // skip empty filter + compositeCriterium->add(filterLine->createCriterium()); } @@ -1213,196 +1441,12 @@ namespace PMP void FiltersListWidget::showAddMenu() { - QMenu menu(this); - - // Category: Score - QMenu* scoreMenu = menu.addMenu(tr("Score")); - - scoreMenu->addAction( - tr("Less than 30"), - [this]() { addFilterLine(TrackCriteriumFactory::scoreLessThanXPercent(30)); } - ); - - scoreMenu->addAction( - tr("Less than 50"), - [this]() { addFilterLine(TrackCriteriumFactory::scoreLessThanXPercent(50)); } - ); - - scoreMenu->addAction( - tr("At least 80"), - [this]() { addFilterLine(TrackCriteriumFactory::scoreAtLeastXPercent(80)); } - ); - - scoreMenu->addAction( - tr("At least 85"), - [this]() { addFilterLine(TrackCriteriumFactory::scoreAtLeastXPercent(85)); } - ); - - scoreMenu->addAction( - tr("At least 90"), - [this]() { addFilterLine(TrackCriteriumFactory::scoreAtLeastXPercent(90)); } - ); - - scoreMenu->addSeparator(); - - scoreMenu->addAction( - tr("Has score"), - [this]() { addFilterLine(TrackCriteriumFactory::scoreMustBePresent()); } - ); - - scoreMenu->addAction( - tr("No score"), - [this]() { addFilterLine(TrackCriteriumFactory::scoreMustBeAbsent()); } - ); - - // Category: Length - QMenu* lengthMenu = menu.addMenu(tr("Length")); - - lengthMenu->addAction( - tr("Less than 3 minutes"), - [this]() { addFilterLine(TrackCriteriumFactory::lengthLessThanXMinutes(3)); } - ); - - lengthMenu->addAction( - tr("At least 3 minutes"), - [this]() { addFilterLine(TrackCriteriumFactory::lengthAtLeastXMinutes(3)); } - ); - - lengthMenu->addAction( - tr("Less than 4 minutes"), - [this]() { addFilterLine(TrackCriteriumFactory::lengthLessThanXMinutes(4)); } - ); - - lengthMenu->addAction( - tr("At least 4 minutes"), - [this]() { addFilterLine(TrackCriteriumFactory::lengthAtLeastXMinutes(4)); } - ); - - lengthMenu->addAction( - tr("Less than 5 minutes"), - [this]() { addFilterLine(TrackCriteriumFactory::lengthLessThanXMinutes(5)); } - ); - - lengthMenu->addAction( - tr("At least 5 minutes"), - [this]() { addFilterLine(TrackCriteriumFactory::lengthAtLeastXMinutes(5)); } - ); - - // Category: last heard - QMenu* lastHeardMenu = menu.addMenu(tr("Last heard")); - - lastHeardMenu->addAction( - tr("More than 2 years ago"), - [this]() { addFilterLine( - TrackCriteriumFactory::notRecentlyHeard({ .years = 2 })); } - ); - - lastHeardMenu->addAction( - tr("More than a year ago"), - [this]() { addFilterLine( - TrackCriteriumFactory::notRecentlyHeard({ .years = 1 })); } - ); - - lastHeardMenu->addAction( - tr("More than 90 days ago"), - [this]() { addFilterLine( - TrackCriteriumFactory::notRecentlyHeard({ .days = 90 })); } - ); - - lastHeardMenu->addAction( - tr("More than 7 days ago"), - [this]() { addFilterLine( - TrackCriteriumFactory::notRecentlyHeard({ .days = 7 })); } - ); - - lastHeardMenu->addAction( - tr("More than 8 hours ago"), - [this]() { addFilterLine( - TrackCriteriumFactory::notRecentlyHeard({ .hours = 8 })); } - ); - - lastHeardMenu->addAction( - tr("More than an hour ago"), - [this]() { addFilterLine( - TrackCriteriumFactory::notRecentlyHeard({ .hours = 1 })); } - ); - - lastHeardMenu->addSeparator(); - - lastHeardMenu->addAction( - tr("Never"), - [this]() { addFilterLine(TrackCriteriumFactory::neverHeard()); } - ); - - lastHeardMenu->addAction( - tr("At least once"), - [this]() { addFilterLine(TrackCriteriumFactory::heardAtLeastOnce()); } - ); - - - // Category: Metadata - QMenu* metadataMenu = menu.addMenu(tr("Metadata")); - - metadataMenu->addAction( - tr("With title"), - [this]() { addFilterLine(TrackCriteriumFactory::withTitle()); } - ); - - metadataMenu->addAction( - tr("With artist"), - [this]() { addFilterLine(TrackCriteriumFactory::withArtist()); } - ); - - metadataMenu->addAction( - tr("With album"), - [this]() { addFilterLine(TrackCriteriumFactory::withAlbum()); } - ); - - metadataMenu->addSeparator(); - - metadataMenu->addAction( - tr("Without title"), - [this]() { addFilterLine(TrackCriteriumFactory::withoutTitle()); } - ); - - metadataMenu->addAction( - tr("Without artist"), - [this]() { addFilterLine(TrackCriteriumFactory::withoutArtist()); } - ); - - metadataMenu->addAction( - tr("Without album"), - [this]() { addFilterLine(TrackCriteriumFactory::withoutAlbum()); } - ); - - // Category: Status - QMenu* statusMenu = menu.addMenu(tr("Status")); - - statusMenu->addAction( - tr("In queue"), - [this]() { addFilterLine(TrackCriteriumFactory::inTheQueue()); } - ); - - statusMenu->addAction( - tr("Not in queue"), - [this]() { addFilterLine(TrackCriteriumFactory::notInTheQueue()); } - ); - - statusMenu->addSeparator(); - - statusMenu->addAction( - tr("Available"), - [this]() { addFilterLine(TrackCriteriumFactory::available()); } - ); - - statusMenu->addAction( - tr("Unavailable"), - [this]() { addFilterLine(TrackCriteriumFactory::unavailable()); } - ); - // Show menu below the button QPoint pos = _addMenuButton->mapToGlobal(QPoint(0, _addMenuButton->height())); - menu.exec(pos); + + displayFiltersPopupMenu( + this, pos, [this](auto criterium) { addFilterLine(std::move(criterium)); } + ); } void FiltersListWidget::addFilterLine(std::unique_ptr criterium) diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index bd59e8ec..75c285df 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -226,6 +226,7 @@ namespace PMP FilterLineWidget(std::unique_ptr criterium); FilterLineWidget(FilterEditorWidget* editor); + bool isEmpty() const { return _isEmpty; } std::unique_ptr createCriterium() const; Q_SIGNALS: @@ -233,27 +234,28 @@ namespace PMP void deleteClicked(); private Q_SLOTS: - void onPickerCriteriumChanged(); + void onEmptyLabelClicked(QPoint position); void onEditClicked(); void onResetClicked(); private: void init(); + void switchToEmpty(QWidget* widgetToReplace); void switchEditorToLabel(); void switchToLabel(QWidget* widgetToReplace, std::unique_ptr criterium); void switchLabelToEditor(); - void switchPickerToEditor(); void switchToEditor(QWidget* widgetToReplace, TrackCriterium const& criterium); void switchToEditor(QWidget* widgetToReplace, FilterEditorWidget* editor); + ClickableLabel* _emptyFilterLabel; FilterLabelWidget* _labelWidget; - FilterPickerWidget* _filterPicker; FilterEditorWidget* _editorWidget; QPushButton* _editButton; QPushButton* _doneButton; QPushButton* _deleteButton; QPushButton* _resetButton; + bool _isEmpty; }; class FiltersListWidget : public QWidget From 12de476219831f9740c67b6025f52c666c702113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Tue, 24 Mar 2026 22:31:25 +0100 Subject: [PATCH 16/22] Use a FilterLineWidget for the highlighting UI Remove the combobox with predefined track criteria from the track highlighting UI, and replace it with the UI used for a track filter. It is not possible yet to have multiple conditions with a logical AND for track highlighting. --- src/desktop-remote/collectionwidget.cpp | 13 ++++++------ src/desktop-remote/collectionwidget.h | 4 ++-- src/desktop-remote/trackfilterwidgets.cpp | 25 +++++++++++++++++++++++ src/desktop-remote/trackfilterwidgets.h | 7 +++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/desktop-remote/collectionwidget.cpp b/src/desktop-remote/collectionwidget.cpp index ad0e18b6..a8678e55 100644 --- a/src/desktop-remote/collectionwidget.cpp +++ b/src/desktop-remote/collectionwidget.cpp @@ -155,7 +155,7 @@ namespace PMP void CollectionWidget::onHighlightCriteriumChanged() { - auto criterium = _highlightingCriteriumPicker->createCriterium(); + auto criterium = _highlightingCriteriumWidget->createCriterium(); bool nothingToHighlight = criterium->equals(*ConstantTrackCriterium::noTracksMatch()); @@ -278,13 +278,14 @@ namespace PMP void CollectionWidget::initTrackHighlightingWidgets() { - _highlightingCriteriumPicker = - new FilterPickerWidget(PredefinedTrackCriterium::NoTracks, tr("(none)")); + _highlightingCriteriumWidget = new FilterLineWidget(); + _highlightingCriteriumWidget->setDeleteButtonVisible(false); + _highlightingCriteriumWidget->setResetButtonVisible(false); { auto layoutItem = this->layout()->replaceWidget(_ui->highlightTracksComboBox, - _highlightingCriteriumPicker); + _highlightingCriteriumWidget); delete layoutItem; /* we cannot delete the placeholder because of retranslateUi() so we hide it*/ @@ -292,7 +293,7 @@ namespace PMP } connect( - _highlightingCriteriumPicker, &FilterPickerWidget::criteriumChanged, + _highlightingCriteriumWidget, &FilterLineWidget::criteriumChanged, this, [this]() { onHighlightCriteriumChanged(); } ); @@ -316,7 +317,7 @@ namespace PMP connect( resetButton, &QPushButton::clicked, - this, [this]() { _highlightingCriteriumPicker->clearCriterium(); } + this, [this]() { _highlightingCriteriumWidget->clearCriterium(); } ); updateColors(/* force: */ true); diff --git a/src/desktop-remote/collectionwidget.h b/src/desktop-remote/collectionwidget.h index 4aaa8bfc..5816f7a1 100644 --- a/src/desktop-remote/collectionwidget.h +++ b/src/desktop-remote/collectionwidget.h @@ -41,7 +41,7 @@ namespace PMP { class ColorSwitcher; class FilteredCollectionTableModel; - class FilterPickerWidget; + class FilterLineWidget; class FiltersListWidget; class SearchData; class SortedCollectionTableModel; @@ -78,7 +78,7 @@ namespace PMP Ui::CollectionWidget* _ui; WaitingSpinnerWidget* _spinner { nullptr }; FiltersListWidget* _filtersListWidget { nullptr }; - FilterPickerWidget* _highlightingCriteriumPicker { nullptr }; + FilterLineWidget* _highlightingCriteriumWidget { nullptr }; ColorSwitcher* _colorSwitcher; Client::ServerInterface* _serverInterface; UserForStatisticsDisplay* _userStatisticsDisplay; diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 3de52d7d..a52fdcc5 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -1197,9 +1197,11 @@ namespace PMP style()->standardIcon(QStyle::SP_DialogApplyButton))); _doneButton->setToolTip(tr("Done editing")); + _deleteVisible = true; _deleteButton->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton)); _deleteButton->setToolTip(tr("Remove filter")); + _resetVisible = true; _resetButton->setIcon(style()->standardIcon(QStyle::SP_LineEditClearButton)); _resetButton->setToolTip(tr("Clear filter")); @@ -1247,6 +1249,24 @@ namespace PMP return ConstantTrackCriterium::noTracksMatch(); } + void FilterLineWidget::setDeleteButtonVisible(bool visible) + { + if (_deleteVisible == visible) + return; + + _deleteVisible = visible; + _deleteButton->setVisible(_deleteVisible); + } + + void FilterLineWidget::setResetButtonVisible(bool visible) + { + if (_resetVisible == visible) + return; + + _resetVisible = visible; + _resetButton->setVisible(_resetVisible); + } + void FilterLineWidget::onEmptyLabelClicked(QPoint position) { QPoint globalPosition = _emptyFilterLabel->mapToGlobal(position); @@ -1273,6 +1293,11 @@ namespace PMP } void FilterLineWidget::onResetClicked() + { + clearCriterium(); + } + + void FilterLineWidget::clearCriterium() { if (_isEmpty) return; diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index 75c285df..49f621f2 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -226,9 +226,14 @@ namespace PMP FilterLineWidget(std::unique_ptr criterium); FilterLineWidget(FilterEditorWidget* editor); + void clearCriterium(); bool isEmpty() const { return _isEmpty; } + std::unique_ptr createCriterium() const; + void setDeleteButtonVisible(bool visible); + void setResetButtonVisible(bool visible); + Q_SIGNALS: void criteriumChanged(); void deleteClicked(); @@ -256,6 +261,8 @@ namespace PMP QPushButton* _deleteButton; QPushButton* _resetButton; bool _isEmpty; + bool _deleteVisible; + bool _resetVisible; }; class FiltersListWidget : public QWidget From 4dbe0cbdaaa6d1d80a7fb93e1eb315739630983b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Tue, 24 Mar 2026 22:47:15 +0100 Subject: [PATCH 17/22] Delete unused class FilterPickerWidget --- src/desktop-remote/trackfilterwidgets.cpp | 107 ---------------------- src/desktop-remote/trackfilterwidgets.h | 22 ----- 2 files changed, 129 deletions(-) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index a52fdcc5..475107f7 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -349,113 +349,6 @@ namespace PMP // =============================================================== // - FilterPickerWidget::FilterPickerWidget(PredefinedTrackCriterium criteriumForEmpty, - QString captionForEmpty) - : _predefinedCriterium(criteriumForEmpty) - { - _comboBox = new QComboBox(); - - QHBoxLayout* layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(_comboBox); - - fillTrackCriteriaComboBox(_comboBox, criteriumForEmpty, captionForEmpty); - - connect( - _comboBox, qOverload(&QComboBox::currentIndexChanged), - this, - [this]() - { - auto predefinedCriterium = - _comboBox->currentData().value(); - - if (_predefinedCriterium == predefinedCriterium) - return; - - _predefinedCriterium = predefinedCriterium; - Q_EMIT criteriumChanged(); - } - ); - } - - void FilterPickerWidget::clearCriterium() - { - _comboBox->setCurrentIndex(0); - } - - std::unique_ptr FilterPickerWidget::createCriterium() const - { - return convertToTrackCriterium(_predefinedCriterium); - } - - void FilterPickerWidget::fillTrackCriteriaComboBox(QComboBox* comboBox, - PredefinedTrackCriterium criteriumForEmpty, - QString captionForEmpty) - { - auto addItem = - [comboBox](QString text, PredefinedTrackCriterium mode) - { - text.replace(">=", UnicodeChars::greaterThanOrEqual) - .replace("<=", UnicodeChars::lessThanOrEqual); - - comboBox->addItem(text, QVariant::fromValue(mode)); - }; - - addItem(captionForEmpty, criteriumForEmpty); - - addItem(tr("never heard"), PredefinedTrackCriterium::NeverHeard); - addItem(tr("not heard in the last 5 years"), - PredefinedTrackCriterium::NotHeardInLast5Years); - addItem(tr("not heard in the last 3 years"), - PredefinedTrackCriterium::NotHeardInLast3Years); - addItem(tr("not heard in the last 2 years"), - PredefinedTrackCriterium::NotHeardInLast2Years); - addItem(tr("not heard in the last year"), - PredefinedTrackCriterium::NotHeardInLastYear); - addItem(tr("not heard in the last 180 days"), - PredefinedTrackCriterium::NotHeardInLast180Days); - addItem(tr("not heard in the last 90 days"), - PredefinedTrackCriterium::NotHeardInLast90Days); - addItem(tr("not heard in the last 30 days"), - PredefinedTrackCriterium::NotHeardInLast30Days); - addItem(tr("not heard in the last 10 days"), - PredefinedTrackCriterium::NotHeardInLast10Days); - addItem(tr("heard at least once"), PredefinedTrackCriterium::HeardAtLeastOnce); - - addItem(tr("without score"), PredefinedTrackCriterium::WithoutScore); - addItem(tr("with score"), PredefinedTrackCriterium::WithScore); - addItem(tr("score < 30"), PredefinedTrackCriterium::ScoreLessThan30); - addItem(tr("score < 50"), PredefinedTrackCriterium::ScoreLessThan50); - addItem(tr("score >= 80"), PredefinedTrackCriterium::ScoreAtLeast80); - addItem(tr("score >= 85"), PredefinedTrackCriterium::ScoreAtLeast85); - addItem(tr("score >= 90"), PredefinedTrackCriterium::ScoreAtLeast90); - addItem(tr("score >= 95"), PredefinedTrackCriterium::ScoreAtLeast95); - - addItem(tr("length < 1 min."), PredefinedTrackCriterium::LengthLessThanOneMinute); - addItem(tr("length >= 1 min."), PredefinedTrackCriterium::LengthAtLeastOneMinute); - addItem(tr("length < 2 min."), PredefinedTrackCriterium::LengthLessThanTwoMinutes); - addItem(tr("length >= 2 min."), PredefinedTrackCriterium::LengthAtLeastTwoMinutes); - addItem(tr("length < 3 min."), PredefinedTrackCriterium::LengthLessThanThreeMinutes); - addItem(tr("length >= 3 min."), PredefinedTrackCriterium::LengthAtLeastThreeMinutes); - addItem(tr("length < 4 min."), PredefinedTrackCriterium::LengthLessThanFourMinutes); - addItem(tr("length >= 4 min."), PredefinedTrackCriterium::LengthAtLeastFourMinutes); - addItem(tr("length < 5 min."), PredefinedTrackCriterium::LengthLessThanFiveMinutes); - addItem(tr("length >= 5 min."), PredefinedTrackCriterium::LengthAtLeastFiveMinutes); - - addItem(tr("not in the queue"), PredefinedTrackCriterium::NotInTheQueue); - addItem(tr("in the queue"), PredefinedTrackCriterium::InTheQueue); - - addItem(tr("without title"), PredefinedTrackCriterium::WithoutTitle); - addItem(tr("without artist"), PredefinedTrackCriterium::WithoutArtist); - addItem(tr("without album"), PredefinedTrackCriterium::WithoutAlbum); - - addItem(tr("no longer available"), PredefinedTrackCriterium::NoLongerAvailable); - - comboBox->setCurrentIndex(0); - } - - // =============================================================== // - namespace { void fillComboBoxWithComparisonOperators(QComboBox* comboBox) diff --git a/src/desktop-remote/trackfilterwidgets.h b/src/desktop-remote/trackfilterwidgets.h index 49f621f2..0c8620d6 100644 --- a/src/desktop-remote/trackfilterwidgets.h +++ b/src/desktop-remote/trackfilterwidgets.h @@ -75,28 +75,6 @@ namespace PMP ClickableLabel* _label; }; - class FilterPickerWidget : public QWidget - { - Q_OBJECT - public: - FilterPickerWidget(PredefinedTrackCriterium criteriumForEmpty, - QString captionForEmpty); - - void clearCriterium(); - std::unique_ptr createCriterium() const; - - Q_SIGNALS: - void criteriumChanged(); - - private: - void fillTrackCriteriaComboBox(QComboBox* comboBox, - PredefinedTrackCriterium criteriumForEmpty, - QString captionForEmpty); - - QComboBox* _comboBox; - PredefinedTrackCriterium _predefinedCriterium; - }; - class FilterEditorWidget : public QWidget { Q_OBJECT From e71db78dc5d1b617ee2fab4e72fbd7a73deee75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Tue, 24 Mar 2026 22:53:37 +0100 Subject: [PATCH 18/22] Delete enum PredefinedTrackCriterium --- src/common/commonmetatypes.cpp | 1 - src/common/trackcriteria.cpp | 139 --------------------------------- src/common/trackcriteria.h | 46 ----------- 3 files changed, 186 deletions(-) diff --git a/src/common/commonmetatypes.cpp b/src/common/commonmetatypes.cpp index 38597438..392beb0c 100644 --- a/src/common/commonmetatypes.cpp +++ b/src/common/commonmetatypes.cpp @@ -50,7 +50,6 @@ namespace PMP qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); diff --git a/src/common/trackcriteria.cpp b/src/common/trackcriteria.cpp index 10e2a9b1..983d8094 100644 --- a/src/common/trackcriteria.cpp +++ b/src/common/trackcriteria.cpp @@ -23,145 +23,6 @@ namespace PMP { - std::unique_ptr convertToTrackCriterium( - PredefinedTrackCriterium criterium) - { - switch (criterium) - { - case PredefinedTrackCriterium::AllTracks: - return ConstantTrackCriterium::allTracksMatch(); - - case PredefinedTrackCriterium::NoTracks: - return ConstantTrackCriterium::noTracksMatch(); - - case PredefinedTrackCriterium::NeverHeard: - return TrackCriteriumFactory::neverHeard(); - - case PredefinedTrackCriterium::NotHeardInLast5Years: - return TrackCriteriumFactory::notRecentlyHeard({ .years = 5 }); - - case PredefinedTrackCriterium::NotHeardInLast3Years: - return TrackCriteriumFactory::notRecentlyHeard({ .years = 3 }); - - case PredefinedTrackCriterium::NotHeardInLast2Years: - return TrackCriteriumFactory::notRecentlyHeard({ .years = 2 }); - - case PredefinedTrackCriterium::NotHeardInLastYear: - return TrackCriteriumFactory::notRecentlyHeard({ .years = 1 }); - - case PredefinedTrackCriterium::NotHeardInLast180Days: - return TrackCriteriumFactory::notRecentlyHeard({ .days = 180 }); - - case PredefinedTrackCriterium::NotHeardInLast90Days: - return TrackCriteriumFactory::notRecentlyHeard({ .days = 90 }); - - case PredefinedTrackCriterium::NotHeardInLast30Days: - return TrackCriteriumFactory::notRecentlyHeard({ .days = 30 }); - - case PredefinedTrackCriterium::NotHeardInLast10Days: - return TrackCriteriumFactory::notRecentlyHeard({ .days = 10 }); - - case PredefinedTrackCriterium::HeardAtLeastOnce: - return TrackCriteriumFactory::heardAtLeastOnce(); - - case PredefinedTrackCriterium::WithoutScore: - return TrackCriteriumFactory::scoreMustBeAbsent(); - - case PredefinedTrackCriterium::WithScore: - return TrackCriteriumFactory::scoreMustBePresent(); - - case PredefinedTrackCriterium::ScoreLessThan30: - return TrackCriteriumFactory::scoreLessThanXPercent(30); - - case PredefinedTrackCriterium::ScoreLessThan50: - return TrackCriteriumFactory::scoreLessThanXPercent(50); - - case PredefinedTrackCriterium::ScoreAtLeast80: - return TrackCriteriumFactory::scoreAtLeastXPercent(80); - - case PredefinedTrackCriterium::ScoreAtLeast85: - return TrackCriteriumFactory::scoreAtLeastXPercent(85); - - case PredefinedTrackCriterium::ScoreAtLeast90: - return TrackCriteriumFactory::scoreAtLeastXPercent(90); - - case PredefinedTrackCriterium::ScoreAtLeast95: - return TrackCriteriumFactory::scoreAtLeastXPercent(95); - - case PredefinedTrackCriterium::LengthLessThanOneMinute: - return TrackCriteriumFactory::lengthLessThanXMinutes(1); - - case PredefinedTrackCriterium::LengthAtLeastOneMinute: - return TrackCriteriumFactory::lengthAtLeastXMinutes(1); - - case PredefinedTrackCriterium::LengthLessThanTwoMinutes: - return TrackCriteriumFactory::lengthLessThanXMinutes(2); - - case PredefinedTrackCriterium::LengthAtLeastTwoMinutes: - return TrackCriteriumFactory::lengthAtLeastXMinutes(2); - - case PredefinedTrackCriterium::LengthLessThanThreeMinutes: - return TrackCriteriumFactory::lengthLessThanXMinutes(3); - - case PredefinedTrackCriterium::LengthAtLeastThreeMinutes: - return TrackCriteriumFactory::lengthAtLeastXMinutes(3); - - case PredefinedTrackCriterium::LengthLessThanFourMinutes: - return TrackCriteriumFactory::lengthLessThanXMinutes(4); - - case PredefinedTrackCriterium::LengthAtLeastFourMinutes: - return TrackCriteriumFactory::lengthAtLeastXMinutes(4); - - case PredefinedTrackCriterium::LengthLessThanFiveMinutes: - return TrackCriteriumFactory::lengthLessThanXMinutes(5); - - case PredefinedTrackCriterium::LengthAtLeastFiveMinutes: - return TrackCriteriumFactory::lengthAtLeastXMinutes(5); - - case PredefinedTrackCriterium::NotInTheQueue: - return TrackCriteriumFactory::notInTheQueue(); - - case PredefinedTrackCriterium::InTheQueue: - return TrackCriteriumFactory::inTheQueue(); - - case PredefinedTrackCriterium::WithoutTitle: - return TrackCriteriumFactory::withoutTitle(); - - case PredefinedTrackCriterium::WithoutArtist: - return TrackCriteriumFactory::withoutArtist(); - - case PredefinedTrackCriterium::WithoutAlbum: - return TrackCriteriumFactory::withoutAlbum(); - - case PredefinedTrackCriterium::NoLongerAvailable: - return TrackCriteriumFactory::unavailable(); - } - - /* should be unreachable because we handled all enum values */ - Q_UNREACHABLE(); - } - - std::unique_ptr convertToTrackCriterium( - const QList& criteria) - { - if (criteria.isEmpty()) - return ConstantTrackCriterium::allTracksMatch(); - - if (criteria.size() == 1) - return convertToTrackCriterium(criteria.front()); - - auto composite = std::make_unique(); - - for (PredefinedTrackCriterium c : criteria) - { - composite->add(convertToTrackCriterium(c)); - } - - return composite; - } - - /* ============================================================================ */ - TrackLengthComparisonCriterium::TrackLengthComparisonCriterium() : _operator(ComparisonOperator::GreaterThanOrEqual), _hours(0), _minutes(0), _seconds(0) diff --git a/src/common/trackcriteria.h b/src/common/trackcriteria.h index c4e1b210..0c9bb3e8 100644 --- a/src/common/trackcriteria.h +++ b/src/common/trackcriteria.h @@ -29,52 +29,7 @@ namespace PMP { - enum class PredefinedTrackCriterium - { - AllTracks = 0, - NoTracks, - NeverHeard, - NotHeardInLast5Years, - NotHeardInLast3Years, - NotHeardInLast2Years, - NotHeardInLastYear, - NotHeardInLast180Days, - NotHeardInLast90Days, - NotHeardInLast30Days, - NotHeardInLast10Days, - HeardAtLeastOnce, - WithoutScore, - WithScore, - ScoreLessThan30, - ScoreLessThan50, - ScoreAtLeast80, - ScoreAtLeast85, - ScoreAtLeast90, - ScoreAtLeast95, - LengthLessThanOneMinute, - LengthAtLeastOneMinute, - LengthLessThanTwoMinutes, - LengthAtLeastTwoMinutes, - LengthLessThanThreeMinutes, - LengthAtLeastThreeMinutes, - LengthLessThanFourMinutes, - LengthAtLeastFourMinutes, - LengthLessThanFiveMinutes, - LengthAtLeastFiveMinutes, - NotInTheQueue, - InTheQueue, - WithoutTitle, - WithoutArtist, - WithoutAlbum, - NoLongerAvailable, - }; - class TrackCriterium; - - std::unique_ptr convertToTrackCriterium(PredefinedTrackCriterium); - std::unique_ptr convertToTrackCriterium( - const QList&); - class TrackCriteriumVisitor; class TrackCriterium @@ -789,7 +744,6 @@ namespace PMP }; } -Q_DECLARE_METATYPE(PMP::PredefinedTrackCriterium) Q_DECLARE_METATYPE(PMP::ComparisonOperator) #endif From c1265c857ca5eaf10aa6b5c84e52821a9b3d79b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Tue, 24 Mar 2026 23:04:55 +0100 Subject: [PATCH 19/22] Disable the reset highlighting button after reset Only enable the 'reset highlighting' button when the highlighting criterium is not empty. --- src/desktop-remote/collectionwidget.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/desktop-remote/collectionwidget.cpp b/src/desktop-remote/collectionwidget.cpp index a8678e55..f0001770 100644 --- a/src/desktop-remote/collectionwidget.cpp +++ b/src/desktop-remote/collectionwidget.cpp @@ -161,6 +161,7 @@ namespace PMP criterium->equals(*ConstantTrackCriterium::noTracksMatch()); _colorSwitcher->setVisible(!nothingToHighlight); + _ui->highlightTracksResetButton->setEnabled(!nothingToHighlight); _collectionSourceModel->setHighlightCriterium(*criterium); } @@ -314,6 +315,7 @@ namespace PMP auto* resetButton = _ui->highlightTracksResetButton; resetButton->setIcon(style()->standardIcon(QStyle::SP_LineEditClearButton)); resetButton->setToolTip(tr("Reset highlighting")); + resetButton->setEnabled(false); connect( resetButton, &QPushButton::clicked, From fb1e20eb0ccaddcd4c502a0af573e07601b4216f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Fri, 27 Mar 2026 20:42:57 +0100 Subject: [PATCH 20/22] Add context menu option to add empty filter slot --- src/desktop-remote/trackfilterwidgets.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index 475107f7..db603b69 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -414,7 +414,8 @@ namespace PMP } void displayFiltersPopupMenu(QWidget* parent, QPoint globalPopupPosition, - std::function)> setFilter) + std::function)> setFilter, + Nullable> emptyAction) { QMenu menu(parent); @@ -555,7 +556,6 @@ namespace PMP [setFilter]() { setFilter(TrackCriteriumFactory::heardAtLeastOnce()); } ); - // Category: Metadata QMenu* metadataMenu = menu.addMenu(filtersMenuTr("Metadata")); @@ -616,6 +616,16 @@ namespace PMP [setFilter]() { setFilter(TrackCriteriumFactory::unavailable()); } ); + // The empty entry + if (emptyAction.hasValue()) + { + menu.addSeparator(); + menu.addAction( + filtersMenuTr("(empty)"), + [emptyAction]() { emptyAction.value()(); } + ); + } + menu.exec(globalPopupPosition); } } @@ -1100,6 +1110,7 @@ namespace PMP _editButton->setVisible(false); _doneButton->setVisible(false); + _resetButton->setEnabled(false); connect( _emptyFilterLabel, &ClickableLabel::clicked, @@ -1172,7 +1183,8 @@ namespace PMP switchToLabel(_emptyFilterLabel, std::move(criterium)); Q_EMIT criteriumChanged(); - } + }, + null /* do not display 'empty' */ ); } @@ -1363,7 +1375,10 @@ namespace PMP QPoint pos = _addMenuButton->mapToGlobal(QPoint(0, _addMenuButton->height())); displayFiltersPopupMenu( - this, pos, [this](auto criterium) { addFilterLine(std::move(criterium)); } + this, + pos, + [this](auto criterium) { addFilterLine(std::move(criterium)); }, + { [this]() { addFilterLine(new FilterLineWidget()); } } /* add empty filter */ ); } From 53759d72988df4f07bf1e824d4a5ca3a31a887ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Fri, 27 Mar 2026 21:18:49 +0100 Subject: [PATCH 21/22] Filters: add shorter length display without hours In the read-only filter view, display length as 00:00 if hours is zero and minutes and seconds are non-zero. --- src/desktop-remote/trackfilterwidgets.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index db603b69..f077bc03 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -123,6 +123,14 @@ namespace PMP { _caption = tr("length %1 %2 second(s)").arg(opStr).arg(seconds); } + else if (hours == 0) + { + _caption = + tr("length %1 %2:%3") + .arg(opStr) + .arg(minutes, 2, 10, QChar('0')) + .arg(seconds, 2, 10, QChar('0')); + } else { _caption = From 56b2f890e6664769b02bf7f18f89d1a5f0b86408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Andr=C3=A9?= Date: Fri, 27 Mar 2026 21:22:08 +0100 Subject: [PATCH 22/22] Improve consistency between view and context menu Improve consistency between the descriptions in the read-only filter view and the context menu for adding/setting filters. --- src/desktop-remote/trackfilterwidgets.cpp | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/desktop-remote/trackfilterwidgets.cpp b/src/desktop-remote/trackfilterwidgets.cpp index f077bc03..9a130517 100644 --- a/src/desktop-remote/trackfilterwidgets.cpp +++ b/src/desktop-remote/trackfilterwidgets.cpp @@ -289,7 +289,7 @@ namespace PMP if (criterium.availability()) _caption = tr("available"); else - _caption = tr("no longer available"); + _caption = tr("unavailable"); } void FilterLabelWidget::CriteriumCaptionGenerator::visit( @@ -299,21 +299,21 @@ namespace PMP { case TrackMetaDataKind::Title: if (criterium.presence()) - _caption = tr("with title"); + _caption = tr("has title"); else - _caption = tr("without title"); + _caption = tr("no title"); break; case TrackMetaDataKind::Artist: if (criterium.presence()) - _caption = tr("with artist"); + _caption = tr("has artist"); else - _caption = tr("without artist"); + _caption = tr("no artist"); break; case TrackMetaDataKind::Album: if (criterium.presence()) - _caption = tr("with album"); + _caption = tr("has album"); else - _caption = tr("without album"); + _caption = tr("no album"); break; } } @@ -568,34 +568,34 @@ namespace PMP QMenu* metadataMenu = menu.addMenu(filtersMenuTr("Metadata")); metadataMenu->addAction( - filtersMenuTr("With title"), + filtersMenuTr("Has title"), [setFilter]() { setFilter(TrackCriteriumFactory::withTitle()); } ); metadataMenu->addAction( - filtersMenuTr("With artist"), + filtersMenuTr("Has artist"), [setFilter]() { setFilter(TrackCriteriumFactory::withArtist()); } ); metadataMenu->addAction( - filtersMenuTr("With album"), + filtersMenuTr("Has album"), [setFilter]() { setFilter(TrackCriteriumFactory::withAlbum()); } ); metadataMenu->addSeparator(); metadataMenu->addAction( - filtersMenuTr("Without title"), + filtersMenuTr("No title"), [setFilter]() { setFilter(TrackCriteriumFactory::withoutTitle()); } ); metadataMenu->addAction( - filtersMenuTr("Without artist"), + filtersMenuTr("No artist"), [setFilter]() { setFilter(TrackCriteriumFactory::withoutArtist()); } ); metadataMenu->addAction( - filtersMenuTr("Without album"), + filtersMenuTr("No album"), [setFilter]() { setFilter(TrackCriteriumFactory::withoutAlbum()); } );