1 /*
2  * Strawberry Music Player
3  * This file was part of Clementine.
4  * Copyright 2010, David Sansome <me@davidsansome.com>
5  *
6  * Strawberry is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Strawberry is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 #include "config.h"
22 
23 #include <memory>
24 
25 #include <QApplication>
26 #include <QWidget>
27 #include <QObject>
28 #include <QDataStream>
29 #include <QIODevice>
30 #include <QAction>
31 #include <QActionGroup>
32 #include <QByteArray>
33 #include <QVariant>
34 #include <QString>
35 #include <QStringList>
36 #include <QRegularExpression>
37 #include <QInputDialog>
38 #include <QList>
39 #include <QTimer>
40 #include <QMenu>
41 #include <QSettings>
42 #include <QToolButton>
43 #include <QtEvents>
44 
45 #include "core/iconloader.h"
46 #include "core/song.h"
47 #include "core/logging.h"
48 #include "collectionmodel.h"
49 #include "collectionquery.h"
50 #include "savedgroupingmanager.h"
51 #include "collectionfilterwidget.h"
52 #include "groupbydialog.h"
53 #include "ui_collectionfilterwidget.h"
54 #include "widgets/qsearchfield.h"
55 #include "settings/appearancesettingspage.h"
56 
57 CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
58     : QWidget(parent),
59       ui_(new Ui_CollectionFilterWidget),
60       model_(nullptr),
61       group_by_dialog_(new GroupByDialog),
62       filter_delay_(new QTimer(this)),
63       filter_applies_to_model_(true),
64       delay_behaviour_(DelayedOnLargeLibraries) {
65 
66   ui_->setupUi(this);
67 
68   QString available_fields = Song::kFtsColumns.join(", ").replace(QRegularExpression("\\bfts"), "");
69 
70   ui_->search_field->setToolTip(
71   QString("<html><head/><body><p>") +
72   tr("Prefix a word with a field name to limit the search to that field, e.g.:") +
73   QString(" ") +
74   QString("<span style=\"font-weight:600;\">") +
75   tr("artist") +
76   QString(":") +
77   QString("</span><span style=\"font-style:italic;\">Strawbs</span>") +
78   QString(" ") +
79   tr("searches the collection for all artists that contain the word") +
80   QString(" Strawbs.") +
81   QString("</p><p><span style=\"font-weight:600;\">") +
82   tr("Available fields") +
83   QString(": ") +
84   "</span><span style=\"font-style:italic;\">" +
85   available_fields +
86   QString("</span>.") +
87   QString("</p></body></html>")
88   );
89 
90   QObject::connect(ui_->search_field, &QSearchField::returnPressed, this, &CollectionFilterWidget::ReturnPressed);
91   QObject::connect(filter_delay_, &QTimer::timeout, this, &CollectionFilterWidget::FilterDelayTimeout);
92 
93   filter_delay_->setInterval(kFilterDelay);
94   filter_delay_->setSingleShot(true);
95 
96   // Icons
97   ui_->options->setIcon(IconLoader::Load("configure"));
98 
99   // Filter by age
100   QActionGroup *filter_age_group = new QActionGroup(this);
101   filter_age_group->addAction(ui_->filter_age_all);
102   filter_age_group->addAction(ui_->filter_age_today);
103   filter_age_group->addAction(ui_->filter_age_week);
104   filter_age_group->addAction(ui_->filter_age_month);
105   filter_age_group->addAction(ui_->filter_age_three_months);
106   filter_age_group->addAction(ui_->filter_age_year);
107 
108   filter_age_menu_ = new QMenu(tr("Show"), this);
109   filter_age_menu_->addActions(filter_age_group->actions());
110 
111   filter_ages_[ui_->filter_age_all] = -1;
112   filter_ages_[ui_->filter_age_today] = 60 * 60 * 24;
113   filter_ages_[ui_->filter_age_week] = 60 * 60 * 24 * 7;
114   filter_ages_[ui_->filter_age_month] = 60 * 60 * 24 * 30;
115   filter_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3;
116   filter_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365;
117 
118   // "Group by ..."
119   group_by_group_ = CreateGroupByActions(this);
120 
121   group_by_menu_ = new QMenu(tr("Group by"), this);
122   group_by_menu_->addActions(group_by_group_->actions());
123 
124   QObject::connect(group_by_group_, &QActionGroup::triggered, this, &CollectionFilterWidget::GroupByClicked);
125   QObject::connect(ui_->save_grouping, &QAction::triggered, this, &CollectionFilterWidget::SaveGroupBy);
126   QObject::connect(ui_->manage_groupings, &QAction::triggered, this, &CollectionFilterWidget::ShowGroupingManager);
127 
128   // Collection config menu
129   collection_menu_ = new QMenu(tr("Display options"), this);
130   collection_menu_->setIcon(ui_->options->icon());
131   collection_menu_->addMenu(filter_age_menu_);
132   collection_menu_->addMenu(group_by_menu_);
133   collection_menu_->addAction(ui_->save_grouping);
134   collection_menu_->addAction(ui_->manage_groupings);
135   collection_menu_->addSeparator();
136   ui_->options->setMenu(collection_menu_);
137 
138   QObject::connect(ui_->search_field, &QSearchField::textChanged, this, &CollectionFilterWidget::FilterTextChanged);
139 
140   ReloadSettings();
141 
142 }
143 
144 CollectionFilterWidget::~CollectionFilterWidget() { delete ui_; }
145 
146 void CollectionFilterWidget::Init(CollectionModel *model) {
147 
148   if (model_) {
149     QObject::disconnect(model_, nullptr, this, nullptr);
150     QObject::disconnect(model_, nullptr, group_by_dialog_.get(), nullptr);
151     QObject::disconnect(group_by_dialog_.get(), nullptr, model_, nullptr);
152     QList<QAction*> filter_ages = filter_ages_.keys();
153     for (QAction *action : filter_ages) {
154       QObject::disconnect(action, &QAction::triggered, model_, nullptr);
155     }
156   }
157 
158   model_ = model;
159 
160   // Connect signals
161   QObject::connect(model_, &CollectionModel::GroupingChanged, group_by_dialog_.get(), &GroupByDialog::CollectionGroupingChanged);
162   QObject::connect(model_, &CollectionModel::GroupingChanged, this, &CollectionFilterWidget::GroupingChanged);
163   QObject::connect(group_by_dialog_.get(), &GroupByDialog::Accepted, model_, &CollectionModel::SetGroupBy);
164 
165   QList<QAction*> filter_ages = filter_ages_.keys();
166   for (QAction *action : filter_ages) {
167     int age = filter_ages_[action];
168     QObject::connect(action, &QAction::triggered, this, [this, age]() { model_->SetFilterAge(age); } );
169   }
170 
171   // Load settings
172   if (!settings_group_.isEmpty()) {
173     QSettings s;
174     s.beginGroup(settings_group_);
175     int version = 0;
176     if (s.contains(group_by_version())) version = s.value(group_by_version(), 0).toInt();
177     if (version == 1) {
178       model_->SetGroupBy(CollectionModel::Grouping(
179           CollectionModel::GroupBy(s.value(group_by(1), static_cast<int>(CollectionModel::GroupBy_AlbumArtist)).toInt()),
180           CollectionModel::GroupBy(s.value(group_by(2), static_cast<int>(CollectionModel::GroupBy_AlbumDisc)).toInt()),
181           CollectionModel::GroupBy(s.value(group_by(3), static_cast<int>(CollectionModel::GroupBy_None)).toInt())));
182     }
183     else {
184       model_->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_AlbumDisc, CollectionModel::GroupBy_None));
185     }
186     s.endGroup();
187   }
188 
189 }
190 
191 void CollectionFilterWidget::ReloadSettings() {
192 
193   QSettings s;
194   s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
195   int iconsize = s.value(AppearanceSettingsPage::kIconSizeConfigureButtons, 20).toInt();
196   s.endGroup();
197   ui_->options->setIconSize(QSize(iconsize, iconsize));
198   ui_->search_field->setIconSize(iconsize);
199 
200 }
201 
202 QString CollectionFilterWidget::group_by() {
203 
204   if (settings_prefix_.isEmpty()) {
205     return QString("group_by");
206   }
207   else {
208     return QString("%1_group_by").arg(settings_prefix_);
209   }
210 
211 }
212 
213 QString CollectionFilterWidget::group_by_version() {
214 
215   if (settings_prefix_.isEmpty()) {
216     return QString("group_by_version");
217   }
218   else {
219     return QString("%1_group_by_version").arg(settings_prefix_);
220   }
221 
222 }
223 
224 QString CollectionFilterWidget::group_by(const int number) { return group_by() + QString::number(number); }
225 
226 void CollectionFilterWidget::UpdateGroupByActions() {
227 
228   if (group_by_group_) {
229     QObject::disconnect(group_by_group_, nullptr, this, nullptr);
230     delete group_by_group_;
231   }
232 
233   group_by_group_ = CreateGroupByActions(this);
234   group_by_menu_->clear();
235   group_by_menu_->addActions(group_by_group_->actions());
236   QObject::connect(group_by_group_, &QActionGroup::triggered, this, &CollectionFilterWidget::GroupByClicked);
237   if (model_) {
238     CheckCurrentGrouping(model_->GetGroupBy());
239   }
240 
241 }
242 
243 
244 QActionGroup *CollectionFilterWidget::CreateGroupByActions(QObject *parent) {
245 
246   QActionGroup *ret = new QActionGroup(parent);
247 
248   ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_Album)));
249   ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_AlbumDisc)));
250   ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_YearAlbum)));
251   ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_YearAlbumDisc)));
252 
253   ret->addAction(CreateGroupByAction(tr("Group by Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
254   ret->addAction(CreateGroupByAction(tr("Group by Artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_AlbumDisc)));
255   ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_YearAlbum)));
256   ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_YearAlbumDisc)));
257 
258   ret->addAction(CreateGroupByAction(tr("Group by Genre/Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_Album)));
259   ret->addAction(CreateGroupByAction(tr("Group by Genre/Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
260 
261   ret->addAction(CreateGroupByAction(tr("Group by Album Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist)));
262   ret->addAction(CreateGroupByAction(tr("Group by Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist)));
263 
264   ret->addAction(CreateGroupByAction(tr("Group by Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Album)));
265   ret->addAction(CreateGroupByAction(tr("Group by Genre/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Album)));
266 
267   QAction *sep1 = new QAction(parent);
268   sep1->setSeparator(true);
269   ret->addAction(sep1);
270 
271   // read saved groupings
272   QSettings s;
273   s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
274   int version = s.value("version").toInt();
275   if (version == 1) {
276     QStringList saved = s.childKeys();
277     for (int i = 0; i < saved.size(); ++i) {
278       if (saved.at(i) == "version") continue;
279       QByteArray bytes = s.value(saved.at(i)).toByteArray();
280       QDataStream ds(&bytes, QIODevice::ReadOnly);
281       CollectionModel::Grouping g;
282       ds >> g;
283       ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
284     }
285   }
286   else {
287     QStringList saved = s.childKeys();
288     for (int i = 0; i < saved.size(); ++i) {
289       if (saved.at(i) == "version") continue;
290       s.remove(saved.at(i));
291     }
292   }
293   s.endGroup();
294 
295   QAction *sep2 = new QAction(parent);
296   sep2->setSeparator(true);
297   ret->addAction(sep2);
298 
299   ret->addAction(CreateGroupByAction(tr("Advanced grouping..."), parent, CollectionModel::Grouping()));
300 
301   return ret;
302 
303 }
304 
305 QAction *CollectionFilterWidget::CreateGroupByAction(const QString &text, QObject *parent, const CollectionModel::Grouping grouping) {
306 
307   QAction *ret = new QAction(text, parent);
308   ret->setCheckable(true);
309 
310   if (grouping.first != CollectionModel::GroupBy_None) {
311     ret->setProperty("group_by", QVariant::fromValue(grouping));
312   }
313 
314   return ret;
315 
316 }
317 
318 void CollectionFilterWidget::SaveGroupBy() {
319 
320   QString text = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
321   if (!text.isEmpty() && model_) {
322     model_->SaveGrouping(text);
323     UpdateGroupByActions();
324   }
325 
326 }
327 
328 void CollectionFilterWidget::ShowGroupingManager() {
329 
330   if (!groupings_manager_) {
331     groupings_manager_ = std::make_unique<SavedGroupingManager>();
332   }
333   groupings_manager_->SetFilter(this);
334   groupings_manager_->UpdateModel();
335   groupings_manager_->show();
336 
337 }
338 
339 bool CollectionFilterWidget::SearchFieldHasFocus() const {
340 
341   return ui_->search_field->hasFocus();
342 
343 }
344 
345 void CollectionFilterWidget::FocusSearchField() {
346 
347   ui_->search_field->setFocus();
348 
349 }
350 
351 void CollectionFilterWidget::FocusOnFilter(QKeyEvent *event) {
352 
353   ui_->search_field->setFocus();
354   QApplication::sendEvent(ui_->search_field, event);
355 
356 }
357 
358 void CollectionFilterWidget::GroupByClicked(QAction *action) {
359 
360   if (action->property("group_by").isNull()) {
361     group_by_dialog_->show();
362     return;
363   }
364 
365   CollectionModel::Grouping g = action->property("group_by").value<CollectionModel::Grouping>();
366   model_->SetGroupBy(g);
367 
368 }
369 
370 void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping g) {
371 
372   if (!settings_group_.isEmpty()) {
373     // Save the settings
374     QSettings s;
375     s.beginGroup(settings_group_);
376     s.setValue(group_by_version(), 1);
377     s.setValue(group_by(1), static_cast<int>(g[0]));
378     s.setValue(group_by(2), static_cast<int>(g[1]));
379     s.setValue(group_by(3), static_cast<int>(g[2]));
380     s.endGroup();
381   }
382 
383   // Now make sure the correct action is checked
384   CheckCurrentGrouping(g);
385 
386 }
387 
388 void CollectionFilterWidget::CheckCurrentGrouping(const CollectionModel::Grouping g) {
389 
390   for (QAction *action : group_by_group_->actions()) {
391     if (action->property("group_by").isNull()) continue;
392 
393     if (g == action->property("group_by").value<CollectionModel::Grouping>()) {
394       action->setChecked(true);
395       return;
396     }
397   }
398 
399   // Check the advanced action
400   QList<QAction*> actions = group_by_group_->actions();
401   QAction *action = actions.last();
402   action->setChecked(true);
403 
404 }
405 
406 void CollectionFilterWidget::SetFilterHint(const QString &hint) {
407   ui_->search_field->setPlaceholderText(hint);
408 }
409 
410 void CollectionFilterWidget::SetQueryMode(QueryOptions::QueryMode query_mode) {
411 
412   ui_->search_field->clear();
413   ui_->search_field->setEnabled(query_mode == QueryOptions::QueryMode_All);
414 
415   model_->SetFilterQueryMode(query_mode);
416 
417 }
418 
419 void CollectionFilterWidget::ShowInCollection(const QString &search) {
420   ui_->search_field->setText(search);
421 }
422 
423 void CollectionFilterWidget::SetAgeFilterEnabled(bool enabled) {
424   filter_age_menu_->setEnabled(enabled);
425 }
426 
427 void CollectionFilterWidget::SetGroupByEnabled(bool enabled) {
428   group_by_menu_->setEnabled(enabled);
429 }
430 
431 void CollectionFilterWidget::AddMenuAction(QAction *action) {
432   collection_menu_->addAction(action);
433 }
434 
435 void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) {
436 
437   switch (e->key()) {
438     case Qt::Key_Up:
439       emit UpPressed();
440       e->accept();
441       break;
442 
443     case Qt::Key_Down:
444       emit DownPressed();
445       e->accept();
446       break;
447 
448     case Qt::Key_Escape:
449       ui_->search_field->clear();
450       e->accept();
451       break;
452   }
453 
454   QWidget::keyReleaseEvent(e);
455 
456 }
457 
458 void CollectionFilterWidget::FilterTextChanged(const QString &text) {
459 
460   // Searching with one or two characters can be very expensive on the database even with FTS,
461   // so if there are a large number of songs in the database introduce a small delay before actually filtering the model,
462   // so if the user is typing the first few characters of something it will be quicker.
463   const bool delay = (delay_behaviour_ == AlwaysDelayed) || (delay_behaviour_ == DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
464 
465   if (delay) {
466     filter_delay_->start();
467   }
468   else {
469     filter_delay_->stop();
470     FilterDelayTimeout();
471   }
472 
473 }
474 
475 void CollectionFilterWidget::FilterDelayTimeout() {
476 
477   emit Filter(ui_->search_field->text());
478   if (filter_applies_to_model_) {
479     model_->SetFilterText(ui_->search_field->text());
480   }
481 
482 }
483