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