strawberry-audio-player-win.../src/collection/collectionfilterwidget.cpp

543 lines
19 KiB
C++

/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019-2022, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <memory>
#include <QApplication>
#include <QWidget>
#include <QObject>
#include <QDataStream>
#include <QIODevice>
#include <QAction>
#include <QActionGroup>
#include <QByteArray>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QRegularExpression>
#include <QInputDialog>
#include <QList>
#include <QTimer>
#include <QMenu>
#include <QSettings>
#include <QToolButton>
#include <QtEvents>
#include "core/iconloader.h"
#include "core/song.h"
#include "core/logging.h"
#include "core/settings.h"
#include "collectionfilteroptions.h"
#include "collectionmodel.h"
#include "savedgroupingmanager.h"
#include "collectionfilterwidget.h"
#include "groupbydialog.h"
#include "ui_collectionfilterwidget.h"
#include "widgets/qsearchfield.h"
#include "settings/collectionsettingspage.h"
#include "settings/appearancesettingspage.h"
CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
: QWidget(parent),
ui_(new Ui_CollectionFilterWidget),
model_(nullptr),
group_by_dialog_(new GroupByDialog(this)),
groupings_manager_(nullptr),
filter_age_menu_(nullptr),
group_by_menu_(nullptr),
collection_menu_(nullptr),
group_by_group_(nullptr),
filter_delay_(new QTimer(this)),
filter_applies_to_model_(true),
delay_behaviour_(DelayBehaviour::DelayedOnLargeLibraries) {
ui_->setupUi(this);
QString available_fields = Song::kFtsColumns.join(QStringLiteral(", ")).replace(QRegularExpression(QStringLiteral("\\bfts")), QLatin1String(""));
available_fields += QStringLiteral(", ") + Song::kNumericalColumns.join(QStringLiteral(", "));
ui_->search_field->setToolTip(
QStringLiteral("<html><head/><body><p>") +
tr("Prefix a word with a field name to limit the search to that field, e.g.:") +
QStringLiteral(" ") +
QStringLiteral("<span style=\"font-weight:600;\">") +
tr("artist") +
QStringLiteral(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
tr("searches the collection for all artists that contain the word %1. ").arg(QStringLiteral("Strawbs")) +
QStringLiteral("</p><p>") +
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
.arg(QStringLiteral(" =, !=, &lt;, &gt;, &lt;="), QStringLiteral("&gt;=")) +
QStringLiteral("<span style=\"font-weight:600;\">") +
tr("rating") +
QStringLiteral("</span>") +
QStringLiteral(":>=") +
QStringLiteral("<span style=\"font-weight:italic;\">4</span>") +
QStringLiteral("</p><p><span style=\"font-weight:600;\">") +
tr("Available fields") +
QStringLiteral(": ") +
QStringLiteral("</span>") +
QStringLiteral("<span style=\"font-style:italic;\">") +
available_fields +
QStringLiteral("</span>.") +
QStringLiteral("</p></body></html>")
);
QObject::connect(ui_->search_field, &QSearchField::returnPressed, this, &CollectionFilterWidget::ReturnPressed);
QObject::connect(filter_delay_, &QTimer::timeout, this, &CollectionFilterWidget::FilterDelayTimeout);
filter_delay_->setInterval(kFilterDelay);
filter_delay_->setSingleShot(true);
// Icons
ui_->options->setIcon(IconLoader::Load(QStringLiteral("configure")));
// Filter by age
QActionGroup *filter_age_group = new QActionGroup(this);
filter_age_group->addAction(ui_->filter_age_all);
filter_age_group->addAction(ui_->filter_age_today);
filter_age_group->addAction(ui_->filter_age_week);
filter_age_group->addAction(ui_->filter_age_month);
filter_age_group->addAction(ui_->filter_age_three_months);
filter_age_group->addAction(ui_->filter_age_year);
filter_age_menu_ = new QMenu(tr("Show"), this);
filter_age_menu_->addActions(filter_age_group->actions());
filter_ages_[ui_->filter_age_all] = -1;
filter_ages_[ui_->filter_age_today] = 60 * 60 * 24;
filter_ages_[ui_->filter_age_week] = 60 * 60 * 24 * 7;
filter_ages_[ui_->filter_age_month] = 60 * 60 * 24 * 30;
filter_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3;
filter_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365;
group_by_menu_ = new QMenu(tr("Group by"), this);
QObject::connect(ui_->save_grouping, &QAction::triggered, this, &CollectionFilterWidget::SaveGroupBy);
QObject::connect(ui_->manage_groupings, &QAction::triggered, this, &CollectionFilterWidget::ShowGroupingManager);
// Collection config menu
collection_menu_ = new QMenu(tr("Display options"), this);
collection_menu_->setIcon(ui_->options->icon());
collection_menu_->addMenu(filter_age_menu_);
collection_menu_->addMenu(group_by_menu_);
collection_menu_->addAction(ui_->save_grouping);
collection_menu_->addAction(ui_->manage_groupings);
collection_menu_->addSeparator();
ui_->options->setMenu(collection_menu_);
QObject::connect(ui_->search_field, &QSearchField::textChanged, this, &CollectionFilterWidget::FilterTextChanged);
QObject::connect(ui_->options, &QToolButton::clicked, ui_->options, &QToolButton::showMenu);
ReloadSettings();
}
CollectionFilterWidget::~CollectionFilterWidget() { delete ui_; }
void CollectionFilterWidget::Init(CollectionModel *model) {
if (model_) {
QObject::disconnect(model_, nullptr, this, nullptr);
QObject::disconnect(model_, nullptr, group_by_dialog_, nullptr);
QObject::disconnect(group_by_dialog_, nullptr, model_, nullptr);
QList<QAction*> filter_ages = filter_ages_.keys();
for (QAction *action : filter_ages) {
QObject::disconnect(action, &QAction::triggered, model_, nullptr);
}
}
model_ = model;
// Connect signals
QObject::connect(model_, &CollectionModel::GroupingChanged, group_by_dialog_, &GroupByDialog::CollectionGroupingChanged);
QObject::connect(model_, &CollectionModel::GroupingChanged, this, &CollectionFilterWidget::GroupingChanged);
QObject::connect(group_by_dialog_, &GroupByDialog::Accepted, model_, &CollectionModel::SetGroupBy);
QList<QAction*> filter_ages = filter_ages_.keys();
for (QAction *action : filter_ages) {
int age = filter_ages_[action];
QObject::connect(action, &QAction::triggered, this, [this, age]() { model_->SetFilterAge(age); } );
}
// Load settings
if (!settings_group_.isEmpty()) {
Settings s;
s.beginGroup(settings_group_);
int version = 0;
if (s.contains(group_by_version())) version = s.value(group_by_version(), 0).toInt();
if (version == 1) {
model_->SetGroupBy(CollectionModel::Grouping(
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(1), static_cast<int>(CollectionModel::GroupBy::AlbumArtist)).toInt()),
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(2), static_cast<int>(CollectionModel::GroupBy::AlbumDisc)).toInt()),
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(3), static_cast<int>(CollectionModel::GroupBy::None)).toInt())),
s.value(separate_albums_by_grouping_key(), false).toBool());
}
else {
model_->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::AlbumDisc, CollectionModel::GroupBy::None), false);
}
s.endGroup();
}
}
void CollectionFilterWidget::SetSettingsGroup(const QString &settings_group) {
settings_group_ = settings_group;
saved_groupings_settings_group_ = SavedGroupingManager::GetSavedGroupingsSettingsGroup(settings_group);
UpdateGroupByActions();
}
void CollectionFilterWidget::SetSettingsPrefix(const QString &prefix) {
settings_prefix_ = prefix;
}
void CollectionFilterWidget::ReloadSettings() {
Settings s;
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
int iconsize = s.value(AppearanceSettingsPage::kIconSizeConfigureButtons, 20).toInt();
s.endGroup();
ui_->options->setIconSize(QSize(iconsize, iconsize));
ui_->search_field->setIconSize(iconsize);
}
QString CollectionFilterWidget::group_by_version() const {
if (settings_prefix_.isEmpty()) {
return QStringLiteral("group_by_version");
}
else {
return QStringLiteral("%1_group_by_version").arg(settings_prefix_);
}
}
QString CollectionFilterWidget::group_by_key() const {
if (settings_prefix_.isEmpty()) {
return QStringLiteral("group_by");
}
else {
return QStringLiteral("%1_group_by").arg(settings_prefix_);
}
}
QString CollectionFilterWidget::group_by_key(const int number) const { return group_by_key() + QString::number(number); }
QString CollectionFilterWidget::separate_albums_by_grouping_key() const {
if (settings_prefix_.isEmpty()) {
return QStringLiteral("separate_albums_by_grouping");
}
else {
return QStringLiteral("%1_separate_albums_by_grouping").arg(settings_prefix_);
}
}
void CollectionFilterWidget::UpdateGroupByActions() {
if (group_by_group_) {
QObject::disconnect(group_by_group_, nullptr, this, nullptr);
delete group_by_group_;
}
group_by_group_ = CreateGroupByActions(saved_groupings_settings_group_, this);
group_by_menu_->clear();
group_by_menu_->addActions(group_by_group_->actions());
QObject::connect(group_by_group_, &QActionGroup::triggered, this, &CollectionFilterWidget::GroupByClicked);
if (model_) {
CheckCurrentGrouping(model_->GetGroupBy());
}
}
QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_groupings_settings_group, QObject *parent) {
QActionGroup *ret = new QActionGroup(parent);
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::AlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::YearAlbum)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::YearAlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::AlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::YearAlbum)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::YearAlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Album Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist)));
ret->addAction(CreateGroupByAction(tr("Group by Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist)));
ret->addAction(CreateGroupByAction(tr("Group by Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::Album)));
QAction *sep1 = new QAction(parent);
sep1->setSeparator(true);
ret->addAction(sep1);
// Read saved groupings
Settings s;
s.beginGroup(saved_groupings_settings_group);
int version = s.value("version").toInt();
if (version == 1) {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == QStringLiteral("version")) continue;
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
ds >> g;
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
}
}
else {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == QStringLiteral("version")) continue;
s.remove(saved.at(i));
}
}
s.endGroup();
QAction *sep2 = new QAction(parent);
sep2->setSeparator(true);
ret->addAction(sep2);
ret->addAction(CreateGroupByAction(tr("Advanced grouping..."), parent, CollectionModel::Grouping()));
return ret;
}
QAction *CollectionFilterWidget::CreateGroupByAction(const QString &text, QObject *parent, const CollectionModel::Grouping grouping) {
QAction *ret = new QAction(text, parent);
ret->setCheckable(true);
if (grouping.first != CollectionModel::GroupBy::None) {
ret->setProperty("group_by", QVariant::fromValue(grouping));
}
return ret;
}
void CollectionFilterWidget::SaveGroupBy() {
if (!model_) return;
QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
if (name.isEmpty()) return;
qLog(Debug) << "Saving current grouping to" << name;
Settings s;
if (settings_group_.isEmpty() || settings_group_ == QLatin1String(CollectionSettingsPage::kSettingsGroup)) {
s.beginGroup(SavedGroupingManager::kSavedGroupingsSettingsGroup);
}
else {
s.beginGroup(QLatin1String(SavedGroupingManager::kSavedGroupingsSettingsGroup) + QLatin1Char('_') + settings_group_);
}
QByteArray buffer;
QDataStream datastream(&buffer, QIODevice::WriteOnly);
datastream << model_->GetGroupBy();
s.setValue("version", QStringLiteral("1"));
s.setValue(name, buffer);
s.endGroup();
UpdateGroupByActions();
}
void CollectionFilterWidget::ShowGroupingManager() {
if (!groupings_manager_) {
groupings_manager_ = new SavedGroupingManager(saved_groupings_settings_group_, this);
QObject::connect(groupings_manager_, &SavedGroupingManager::UpdateGroupByActions, this, &CollectionFilterWidget::UpdateGroupByActions);
}
groupings_manager_->UpdateModel();
groupings_manager_->show();
}
bool CollectionFilterWidget::SearchFieldHasFocus() const {
return ui_->search_field->hasFocus();
}
void CollectionFilterWidget::FocusSearchField() {
ui_->search_field->setFocus();
}
void CollectionFilterWidget::FocusOnFilter(QKeyEvent *event) {
ui_->search_field->setFocus();
QApplication::sendEvent(ui_->search_field, event);
}
void CollectionFilterWidget::GroupByClicked(QAction *action) {
if (action->property("group_by").isNull()) {
group_by_dialog_->show();
return;
}
CollectionModel::Grouping g = action->property("group_by").value<CollectionModel::Grouping>();
model_->SetGroupBy(g);
}
void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping) {
if (!settings_group_.isEmpty()) {
Settings s;
s.beginGroup(settings_group_);
s.setValue(group_by_version(), 1);
s.setValue(group_by_key(1), static_cast<int>(g[0]));
s.setValue(group_by_key(2), static_cast<int>(g[1]));
s.setValue(group_by_key(3), static_cast<int>(g[2]));
s.setValue(separate_albums_by_grouping_key(), separate_albums_by_grouping);
s.endGroup();
}
// Now make sure the correct action is checked
CheckCurrentGrouping(g);
}
void CollectionFilterWidget::CheckCurrentGrouping(const CollectionModel::Grouping g) {
if (!group_by_group_) {
UpdateGroupByActions();
}
for (QAction *action : group_by_group_->actions()) {
if (action->property("group_by").isNull()) continue;
if (g == action->property("group_by").value<CollectionModel::Grouping>()) {
action->setChecked(true);
return;
}
}
// Check the advanced action
QList<QAction*> actions = group_by_group_->actions();
QAction *action = actions.last();
action->setChecked(true);
}
void CollectionFilterWidget::SetFilterHint(const QString &hint) {
ui_->search_field->setPlaceholderText(hint);
}
void CollectionFilterWidget::SetFilterMode(CollectionFilterOptions::FilterMode filter_mode) {
ui_->search_field->clear();
ui_->search_field->setEnabled(filter_mode == CollectionFilterOptions::FilterMode::All);
model_->SetFilterMode(filter_mode);
}
void CollectionFilterWidget::ShowInCollection(const QString &search) {
ui_->search_field->setText(search);
}
void CollectionFilterWidget::SetAgeFilterEnabled(bool enabled) {
filter_age_menu_->setEnabled(enabled);
}
void CollectionFilterWidget::SetGroupByEnabled(bool enabled) {
group_by_menu_->setEnabled(enabled);
}
void CollectionFilterWidget::AddMenuAction(QAction *action) {
collection_menu_->addAction(action);
}
void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) {
switch (e->key()) {
case Qt::Key_Up:
emit UpPressed();
e->accept();
break;
case Qt::Key_Down:
emit DownPressed();
e->accept();
break;
case Qt::Key_Escape:
ui_->search_field->clear();
e->accept();
break;
}
QWidget::keyReleaseEvent(e);
}
void CollectionFilterWidget::FilterTextChanged(const QString &text) {
// Searching with one or two characters can be very expensive on the database even with FTS,
// so if there are a large number of songs in the database introduce a small delay before actually filtering the model,
// so if the user is typing the first few characters of something it will be quicker.
const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
if (delay) {
filter_delay_->start();
}
else {
filter_delay_->stop();
FilterDelayTimeout();
}
}
void CollectionFilterWidget::FilterDelayTimeout() {
emit Filter(ui_->search_field->text());
if (filter_applies_to_model_) {
model_->SetFilterText(ui_->search_field->text());
}
}