1 /* This file is part of Clementine.
2    Copyright 2010, David Sansome <me@davidsansome.com>
3 
4    Clementine is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    Clementine is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
16 */
17 
18 #include "querygenerator.h"
19 #include "querywizardplugin.h"
20 #include "searchtermwidget.h"
21 #include "ui_querysearchpage.h"
22 #include "ui_querysortpage.h"
23 #include "core/logging.h"
24 
25 #include <QScrollBar>
26 #include <QWizardPage>
27 
28 namespace smart_playlists {
29 
30 class QueryWizardPlugin::SearchPage : public QWizardPage {
31   friend class QueryWizardPlugin;
32 
33  public:
SearchPage(QWidget * parent=0)34   SearchPage(QWidget* parent = 0)
35       : QWizardPage(parent), ui_(new Ui_SmartPlaylistQuerySearchPage) {
36     ui_->setupUi(this);
37   }
38 
isComplete() const39   bool isComplete() const {
40     if (ui_->type->currentIndex() == 2)  // All songs
41       return true;
42 
43     for (SearchTermWidget* widget : terms_) {
44       if (!widget->Term().is_valid()) return false;
45     }
46     return true;
47   }
48 
49   QVBoxLayout* layout_;
50   QList<SearchTermWidget*> terms_;
51   SearchTermWidget* new_term_;
52 
53   SearchPreview* preview_;
54 
55   std::unique_ptr<Ui_SmartPlaylistQuerySearchPage> ui_;
56 };
57 
58 class QueryWizardPlugin::SortPage : public QWizardPage {
59  public:
SortPage(QueryWizardPlugin * plugin,QWidget * parent,int next_id)60   SortPage(QueryWizardPlugin* plugin, QWidget* parent, int next_id)
61       : QWizardPage(parent), next_id_(next_id), plugin_(plugin) {}
62 
showEvent(QShowEvent *)63   void showEvent(QShowEvent*) { plugin_->UpdateSortPreview(); }
64 
nextId() const65   int nextId() const { return next_id_; }
66   int next_id_;
67 
68   QueryWizardPlugin* plugin_;
69 };
70 
QueryWizardPlugin(Application * app,LibraryBackend * library,QObject * parent)71 QueryWizardPlugin::QueryWizardPlugin(Application* app, LibraryBackend* library,
72                                      QObject* parent)
73     : WizardPlugin(app, library, parent),
74       search_page_(nullptr),
75       previous_scrollarea_max_(0) {}
76 
~QueryWizardPlugin()77 QueryWizardPlugin::~QueryWizardPlugin() {}
78 
name() const79 QString QueryWizardPlugin::name() const { return tr("Library search"); }
80 
description() const81 QString QueryWizardPlugin::description() const {
82   return tr("Find songs in your library that match the criteria you specify.");
83 }
84 
CreatePages(QWizard * wizard,int finish_page_id)85 int QueryWizardPlugin::CreatePages(QWizard* wizard, int finish_page_id) {
86   // Create the UI
87   search_page_ = new SearchPage(wizard);
88 
89   QWizardPage* sort_page = new SortPage(this, wizard, finish_page_id);
90   sort_ui_.reset(new Ui_SmartPlaylistQuerySortPage);
91   sort_ui_->setupUi(sort_page);
92 
93   sort_ui_->limit_value->setValue(Generator::kDefaultLimit);
94 
95   connect(search_page_->ui_->type, SIGNAL(currentIndexChanged(int)),
96           SLOT(SearchTypeChanged()));
97 
98   // Create the new search term widget
99   search_page_->new_term_ = new SearchTermWidget(library_, search_page_);
100   search_page_->new_term_->SetActive(false);
101   connect(search_page_->new_term_, SIGNAL(Clicked()), SLOT(AddSearchTerm()));
102 
103   // Add an empty initial term
104   search_page_->layout_ = static_cast<QVBoxLayout*>(
105       search_page_->ui_->terms_scroll_area_content->layout());
106   search_page_->layout_->addWidget(search_page_->new_term_);
107   AddSearchTerm();
108 
109   // Ensure that the terms are scrolled to the bottom when a new one is added
110   connect(search_page_->ui_->terms_scroll_area->verticalScrollBar(),
111           SIGNAL(rangeChanged(int, int)), this,
112           SLOT(MoveTermListToBottom(int, int)));
113 
114   // Add the preview widget at the bottom of the search terms page
115   QVBoxLayout* terms_page_layout =
116       static_cast<QVBoxLayout*>(search_page_->layout());
117   terms_page_layout->addStretch();
118   search_page_->preview_ = new SearchPreview(search_page_);
119   search_page_->preview_->set_application(app_);
120   search_page_->preview_->set_library(library_);
121   terms_page_layout->addWidget(search_page_->preview_);
122 
123   // Add sort field texts
124   for (int i = 0; i < SearchTerm::FieldCount; ++i) {
125     const SearchTerm::Field field = SearchTerm::Field(i);
126     const QString field_name = SearchTerm::FieldName(field);
127     sort_ui_->field_value->addItem(field_name);
128   }
129   connect(sort_ui_->field_value, SIGNAL(currentIndexChanged(int)),
130           SLOT(UpdateSortOrder()));
131   UpdateSortOrder();
132 
133   // Set the sort and limit radio buttons back to their defaults - they would
134   // have been changed by setupUi
135   sort_ui_->random->setChecked(true);
136   sort_ui_->limit_none->setChecked(true);
137 
138   // Set up the preview widget that's already at the bottom of the sort page
139   sort_ui_->preview->set_application(app_);
140   sort_ui_->preview->set_library(library_);
141   connect(sort_ui_->field, SIGNAL(toggled(bool)), SLOT(UpdateSortPreview()));
142   connect(sort_ui_->field_value, SIGNAL(currentIndexChanged(int)),
143           SLOT(UpdateSortPreview()));
144   connect(sort_ui_->limit_limit, SIGNAL(toggled(bool)),
145           SLOT(UpdateSortPreview()));
146   connect(sort_ui_->limit_none, SIGNAL(toggled(bool)),
147           SLOT(UpdateSortPreview()));
148   connect(sort_ui_->limit_value, SIGNAL(valueChanged(QString)),
149           SLOT(UpdateSortPreview()));
150   connect(sort_ui_->order, SIGNAL(currentIndexChanged(int)),
151           SLOT(UpdateSortPreview()));
152   connect(sort_ui_->random, SIGNAL(toggled(bool)), SLOT(UpdateSortPreview()));
153 
154   // Configure the page text
155   search_page_->setTitle(tr("Search terms"));
156   search_page_->setSubTitle(
157       tr("A song will be included in the playlist if it matches these "
158          "conditions."));
159   sort_page->setTitle(tr("Search options"));
160   sort_page->setSubTitle(tr(
161       "Choose how the playlist is sorted and how many songs it will contain."));
162 
163   // Add the pages
164   const int first_page = wizard->addPage(search_page_);
165   wizard->addPage(sort_page);
166   return first_page;
167 }
168 
SetGenerator(GeneratorPtr g)169 void QueryWizardPlugin::SetGenerator(GeneratorPtr g) {
170   std::shared_ptr<QueryGenerator> gen =
171       std::dynamic_pointer_cast<QueryGenerator>(g);
172   if (!gen) return;
173   Search search = gen->search();
174 
175   // Search type
176   search_page_->ui_->type->setCurrentIndex(search.search_type_);
177 
178   // Search terms
179   qDeleteAll(search_page_->terms_);
180   search_page_->terms_.clear();
181 
182   for (const SearchTerm& term : search.terms_) {
183     AddSearchTerm();
184     search_page_->terms_.last()->SetTerm(term);
185   }
186 
187   // Sort order
188   if (search.sort_type_ == Search::Sort_Random) {
189     sort_ui_->random->setChecked(true);
190   } else {
191     sort_ui_->field->setChecked(true);
192     sort_ui_->order->setCurrentIndex(
193         search.sort_type_ == Search::Sort_FieldAsc ? 0 : 1);
194     sort_ui_->field_value->setCurrentIndex(search.sort_field_);
195   }
196 
197   // Limit
198   if (search.limit_ == -1) {
199     sort_ui_->limit_none->setChecked(true);
200   } else {
201     sort_ui_->limit_limit->setChecked(true);
202     sort_ui_->limit_value->setValue(search.limit_);
203   }
204 }
205 
CreateGenerator() const206 GeneratorPtr QueryWizardPlugin::CreateGenerator() const {
207   std::shared_ptr<QueryGenerator> gen(new QueryGenerator);
208   gen->Load(MakeSearch());
209 
210   return std::static_pointer_cast<Generator>(gen);
211 }
212 
UpdateSortOrder()213 void QueryWizardPlugin::UpdateSortOrder() {
214   const SearchTerm::Field field =
215       SearchTerm::Field(sort_ui_->field_value->currentIndex());
216   const SearchTerm::Type type = SearchTerm::TypeOf(field);
217   const QString asc = SearchTerm::FieldSortOrderText(type, true);
218   const QString desc = SearchTerm::FieldSortOrderText(type, false);
219 
220   const int old_current_index = sort_ui_->order->currentIndex();
221   sort_ui_->order->clear();
222   sort_ui_->order->addItem(asc);
223   sort_ui_->order->addItem(desc);
224   sort_ui_->order->setCurrentIndex(old_current_index);
225 }
226 
AddSearchTerm()227 void QueryWizardPlugin::AddSearchTerm() {
228   SearchTermWidget* widget = new SearchTermWidget(library_, search_page_);
229   connect(widget, SIGNAL(RemoveClicked()), SLOT(RemoveSearchTerm()));
230   connect(widget, SIGNAL(Changed()), SLOT(UpdateTermPreview()));
231 
232   search_page_->layout_->insertWidget(search_page_->terms_.count(), widget);
233   search_page_->terms_ << widget;
234 
235   UpdateTermPreview();
236 }
237 
RemoveSearchTerm()238 void QueryWizardPlugin::RemoveSearchTerm() {
239   SearchTermWidget* widget = qobject_cast<SearchTermWidget*>(sender());
240   if (!widget) return;
241 
242   const int index = search_page_->terms_.indexOf(widget);
243   if (index == -1) return;
244 
245   search_page_->terms_.takeAt(index)->deleteLater();
246   UpdateTermPreview();
247 }
248 
UpdateTermPreview()249 void QueryWizardPlugin::UpdateTermPreview() {
250   Search search = MakeSearch();
251   emit search_page_->completeChanged();
252   // When removing last term, update anyway the search
253   if (!search.is_valid() && !search_page_->terms_.isEmpty()) return;
254 
255   // Don't apply limits in the term page
256   search.limit_ = -1;
257 
258   search_page_->preview_->Update(search);
259 }
260 
UpdateSortPreview()261 void QueryWizardPlugin::UpdateSortPreview() {
262   Search search = MakeSearch();
263   if (!search.is_valid()) return;
264 
265   sort_ui_->preview->Update(search);
266 }
267 
MakeSearch() const268 Search QueryWizardPlugin::MakeSearch() const {
269   Search ret;
270 
271   // Search type
272   ret.search_type_ =
273       Search::SearchType(search_page_->ui_->type->currentIndex());
274 
275   // Search terms
276   for (SearchTermWidget* widget : search_page_->terms_) {
277     SearchTerm term = widget->Term();
278     if (term.is_valid()) ret.terms_ << term;
279   }
280 
281   // Sort order
282   if (sort_ui_->random->isChecked()) {
283     ret.sort_type_ = Search::Sort_Random;
284   } else {
285     const bool ascending = sort_ui_->order->currentIndex() == 0;
286     ret.sort_type_ = ascending ? Search::Sort_FieldAsc : Search::Sort_FieldDesc;
287     ret.sort_field_ = SearchTerm::Field(sort_ui_->field_value->currentIndex());
288   }
289 
290   // Limit
291   if (sort_ui_->limit_none->isChecked())
292     ret.limit_ = -1;
293   else
294     ret.limit_ = sort_ui_->limit_value->value();
295 
296   return ret;
297 }
298 
SearchTypeChanged()299 void QueryWizardPlugin::SearchTypeChanged() {
300   const bool all = search_page_->ui_->type->currentIndex() == 2;
301   search_page_->ui_->terms_scroll_area_content->setEnabled(!all);
302 
303   UpdateTermPreview();
304 }
305 
MoveTermListToBottom(int min,int max)306 void QueryWizardPlugin::MoveTermListToBottom(int min, int max) {
307   Q_UNUSED(min);
308   // Only scroll to the bottom if a new term is added
309   if (previous_scrollarea_max_ < max)
310     search_page_->ui_->terms_scroll_area->verticalScrollBar()->setValue(max);
311 
312   previous_scrollarea_max_ = max;
313 }
314 
315 }  // namespace smart_playlists
316