1 /* $BEGIN_LICENSE
2 
3 This file is part of Musique.
4 Copyright 2013, Flavio Tordini <flavio.tordini@gmail.com>
5 
6 Musique 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 Musique 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 Musique.  If not, see <http://www.gnu.org/licenses/>.
18 
19 $END_LICENSE */
20 
21 #include "finderwidget.h"
22 #include "finderitemdelegate.h"
23 
24 #include "segmentedcontrol.h"
25 
26 #include "breadcrumb.h"
27 
28 #include "playlistmodel.h"
29 #include "playlistview.h"
30 
31 #include "model/album.h"
32 #include "model/artist.h"
33 #include "model/genre.h"
34 
35 #include "artistlistview.h"
36 #include "artistsqlmodel.h"
37 
38 #include "albumlistview.h"
39 #include "albumsqlmodel.h"
40 
41 #include "genresmodel.h"
42 
43 #include "tracklistview.h"
44 #include "tracksqlmodel.h"
45 
46 #include "filesystemfinderview.h"
47 #include "filesystemmodel.h"
48 #include "filteringfilesystemmodel.h"
49 
50 #include "searchmodel.h"
51 #include "searchview.h"
52 
53 #include "database.h"
54 #include <QtSql>
55 
56 namespace {
57 const QString finderViewKey = QStringLiteral("finderView");
58 }
59 
FinderWidget(QWidget * parent)60 FinderWidget::FinderWidget(QWidget *parent) : QWidget(parent) {
61     fileSystemView = nullptr;
62     artistListView = nullptr;
63     albumListView = nullptr;
64     trackListView = nullptr;
65     genresListView = nullptr;
66 
67     fileSystemModel = nullptr;
68     artistListModel = nullptr;
69     albumListModel = nullptr;
70     trackListModel = nullptr;
71     genresModel = nullptr;
72 
73     searchView = nullptr;
74 
75     // colors
76     QPalette p = palette();
77     QColor backgroundColor(0x20, 0x20, 0x20);
78     p.setBrush(QPalette::Window, backgroundColor);
79     p.setBrush(QPalette::Base, backgroundColor);
80     p.setBrush(QPalette::Text, Qt::white);
81     p.setBrush(QPalette::WindowText, Qt::white);
82 
83     QBoxLayout *layout = new QVBoxLayout();
84     layout->setMargin(0);
85     layout->setSpacing(0);
86 
87     setupBar();
88     layout->addWidget(finderBar);
89 
90     breadcrumb = new Breadcrumb(this);
91     breadcrumb->setPalette(p);
92     breadcrumb->hide();
93     connect(breadcrumb, SIGNAL(goneBack()), SLOT(goBack()));
94     layout->addWidget(breadcrumb);
95 
96     folderBreadcrumb = new Breadcrumb(this);
97     folderBreadcrumb->setPalette(p);
98     folderBreadcrumb->hide();
99     connect(folderBreadcrumb, SIGNAL(goneBack()), SLOT(folderGoBack()));
100     layout->addWidget(folderBreadcrumb);
101 
102     stackedWidget = new QStackedWidget(this);
103     stackedWidget->setPalette(p);
104 
105     layout->addWidget(stackedWidget);
106     setLayout(layout);
107 
108     setMinimumWidth(FinderItemDelegate::ITEM_WIDTH * 3 + 4 +
109                     style()->pixelMetric(QStyle::PM_ScrollBarExtent));
110     setMinimumHeight(FinderItemDelegate::ITEM_HEIGHT + finderBar->minimumHeight());
111 
112     restoreSavedView();
113 }
114 
restoreSavedView()115 void FinderWidget::restoreSavedView() {
116     QSettings settings;
117     QString currentViewName = settings.value(finderViewKey).toString();
118 
119     if (currentViewName == "folders")
120         QTimer::singleShot(0, this, SLOT(showFolders()));
121     else if (currentViewName == "albums")
122         QTimer::singleShot(0, this, SLOT(showAlbums()));
123     else if (currentViewName == "genres")
124         QTimer::singleShot(0, this, SLOT(showGenres()));
125     else
126         QTimer::singleShot(0, this, SLOT(showArtists()));
127 
128     /*
129     if (currentViewName == "folders") QTimer::singleShot(0, foldersAction, SLOT(trigger()));
130     else if (currentViewName == "albums") QTimer::singleShot(0, albumsAction, SLOT(trigger()));
131     else QTimer::singleShot(0, artistsAction, SLOT(trigger()));
132     */
133 }
134 
setupBar()135 void FinderWidget::setupBar() {
136     finderBar = new SegmentedControl(this);
137 
138     artistsAction = new QAction(tr("Artists"), this);
139     artistsAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_1));
140     connect(artistsAction, SIGNAL(triggered()), SLOT(showArtists()));
141     finderBar->addAction(artistsAction);
142 
143     albumsAction = new QAction(tr("Albums"), this);
144     albumsAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_2));
145     connect(albumsAction, SIGNAL(triggered()), SLOT(showAlbums()));
146     finderBar->addAction(albumsAction);
147 
148     genresAction = new QAction(tr("Genres"), this);
149     genresAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_3));
150     connect(genresAction, SIGNAL(triggered()), SLOT(showGenres()));
151     finderBar->addAction(genresAction);
152 
153     foldersAction = new QAction(tr("Folders"), this);
154     foldersAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_4));
155     connect(foldersAction, SIGNAL(triggered()), SLOT(showFolders()));
156     finderBar->addAction(foldersAction);
157 
158     for (QAction *action : finderBar->actions()) {
159         addAction(action);
160         action->setAutoRepeat(false);
161         if (!action->shortcut().isEmpty())
162             action->setStatusTip(action->text() + " (" +
163                                  action->shortcut().toString(QKeySequence::NativeText) + ")");
164     }
165 
166     finderBar->setCheckedAction(0);
167 }
168 
setupArtists()169 void FinderWidget::setupArtists() {
170     artistListModel = new ArtistSqlModel(this);
171     artistListView = new ArtistListView(stackedWidget);
172     artistListView->setEnabled(false);
173     connect(artistListView, SIGNAL(activated(const QModelIndex &)),
174             SLOT(artistActivated(const QModelIndex &)));
175     connect(artistListView, SIGNAL(play(const QModelIndex &)),
176             SLOT(artistPlayed(const QModelIndex &)));
177     artistListView->setModel(artistListModel);
178     stackedWidget->addWidget(artistListView);
179 }
180 
setupAlbums()181 void FinderWidget::setupAlbums() {
182     albumListModel = new AlbumSqlModel(this);
183     albumListView = new AlbumListView(stackedWidget);
184     albumListView->setEnabled(false);
185     connect(albumListView, SIGNAL(activated(const QModelIndex &)),
186             SLOT(albumActivated(const QModelIndex &)));
187     connect(albumListView, SIGNAL(play(const QModelIndex &)),
188             SLOT(albumPlayed(const QModelIndex &)));
189     albumListView->setModel(albumListModel);
190     stackedWidget->addWidget(albumListView);
191 }
192 
setupGenres()193 void FinderWidget::setupGenres() {
194     genresModel = new GenresModel(this);
195     genresListView = new FinderListView(stackedWidget);
196     genresListView->setEnabled(false);
197     connect(genresListView, SIGNAL(activated(const QModelIndex &)),
198             SLOT(genreActivated(const QModelIndex &)));
199     connect(genresListView, SIGNAL(play(const QModelIndex &)),
200             SLOT(genrePlayed(const QModelIndex &)));
201     genresListView->setModel(genresModel);
202     stackedWidget->addWidget(genresListView);
203 }
204 
setupTracks()205 void FinderWidget::setupTracks() {
206     trackListModel = new TrackSqlModel(this);
207     trackListView = new TrackListView(stackedWidget);
208     connect(trackListView, SIGNAL(activated(const QModelIndex &)),
209             SLOT(trackActivated(const QModelIndex &)));
210     trackListView->setModel(trackListModel);
211     stackedWidget->addWidget(trackListView);
212     trackListView->setPalette(stackedWidget->palette());
213 }
214 
setupFolders()215 void FinderWidget::setupFolders() {
216     fileSystemModel = new FileSystemModel(this);
217     fileSystemModel->setResolveSymlinks(true);
218     fileSystemModel->setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
219     filteringFileSystemModel = new FilteringFileSystemModel(this);
220     filteringFileSystemModel->setSourceModel(fileSystemModel);
221 
222     fileSystemView = new FileSystemFinderView(stackedWidget);
223     connect(fileSystemView, SIGNAL(activated(const QModelIndex &)),
224             SLOT(folderActivated(const QModelIndex &)));
225     connect(fileSystemView, SIGNAL(play(const QModelIndex &)),
226             SLOT(folderPlayed(const QModelIndex &)));
227     fileSystemView->setModel(filteringFileSystemModel);
228     fileSystemView->setFileSystemModel(fileSystemModel);
229     stackedWidget->addWidget(fileSystemView);
230 }
231 
setupSearch()232 void FinderWidget::setupSearch() {
233     searchModel = new SearchModel(this);
234     searchView = new SearchView(stackedWidget);
235 
236     connect(searchView, SIGNAL(activated(const QModelIndex &)), searchModel,
237             SLOT(itemActivated(const QModelIndex &)));
238     connect(searchView, SIGNAL(play(const QModelIndex &)), searchModel,
239             SLOT(itemPlayed(const QModelIndex &)));
240 
241     searchView->setEnabled(false);
242     searchView->setModel(searchModel);
243     stackedWidget->addWidget(searchView);
244 }
245 
showArtists()246 void FinderWidget::showArtists() {
247     if (!artistListView) setupArtists();
248     artistListView->updateQuery();
249     showWidget(artistListView, true);
250     finderBar->setCheckedAction(artistsAction);
251     QSettings settings;
252     settings.setValue(finderViewKey, "artists");
253 }
254 
showAlbums()255 void FinderWidget::showAlbums() {
256     if (!albumListView) setupAlbums();
257     albumListView->updateQuery();
258     albumListView->setShowToolBar(true);
259     showWidget(albumListView, true);
260     finderBar->setCheckedAction(albumsAction);
261     QSettings settings;
262     settings.setValue(finderViewKey, "albums");
263 }
264 
showGenres()265 void FinderWidget::showGenres() {
266     if (!genresListView) setupGenres();
267     if (genresListView->rootIndex().isValid()) genresListView->setRootIndex(QModelIndex());
268     showWidget(genresListView, true);
269     finderBar->setCheckedAction(genresAction);
270     QSettings settings;
271     settings.setValue(finderViewKey, "genres");
272 }
273 
showFolders()274 void FinderWidget::showFolders() {
275     if (!fileSystemView) setupFolders();
276 
277     const QString path = Database::instance().collectionRoot();
278     fileSystemModel->setRootPath(path);
279     QSortFilterProxyModel *proxyModel =
280             static_cast<QSortFilterProxyModel *>(fileSystemView->model());
281     fileSystemView->setRootIndex(proxyModel->mapFromSource(fileSystemModel->index(path)));
282 
283     showWidget(fileSystemView, true);
284     finderBar->setCheckedAction(foldersAction);
285     QSettings settings;
286     settings.setValue(finderViewKey, "folders");
287 }
288 
showSearch(const QString & query)289 void FinderWidget::showSearch(const QString &query) {
290     if (query.isEmpty()) {
291         if (stackedWidget->currentWidget() == searchView) restoreSavedView();
292         return;
293     }
294 
295     if (!searchView) setupSearch();
296 
297     showWidget(searchView, true);
298     finderBar->setCheckedAction(-1);
299     searchModel->search(query);
300 }
301 
showWidget(QWidget * widget,bool isRoot)302 void FinderWidget::showWidget(QWidget *widget, bool isRoot) {
303     // breadcrumb behaviour
304     if (isRoot) {
305         history.clear();
306         breadcrumb->clear();
307         folderBreadcrumb->clear();
308         folderBreadcrumb->hide();
309     } else {
310         breadcrumb->addItem(widget->windowTitle());
311     }
312     breadcrumb->setVisible(!isRoot);
313 
314     // call disappear() on previous widget
315     QWidget *currentWidget = stackedWidget->currentWidget();
316     if (currentWidget && currentWidget != widget) {
317         bool ret = QMetaObject::invokeMethod(currentWidget, "disappear", Qt::DirectConnection);
318         if (!ret) qDebug() << "FinderWidget::showWidget invokeMethod failed for" << currentWidget;
319     }
320 
321     // call appear() on new widget
322     bool ret = QMetaObject::invokeMethod(widget, "appear", Qt::DirectConnection);
323     if (!ret) qDebug() << "FinderWidget::showWidget invokeMethod failed for" << widget;
324 
325     stackedWidget->setCurrentWidget(widget);
326     history.push(widget);
327 
328     widget->setFocus();
329 }
330 
goBack()331 void FinderWidget::goBack() {
332     if (history.size() > 1) {
333         breadcrumb->goBack();
334         breadcrumb->goBack();
335         history.pop();
336         QWidget *widget = history.pop();
337         bool isRoot = history.isEmpty();
338         showWidget(widget, isRoot);
339         if (genresListView && widget == genresListView) {
340             genresListView->setRootIndex(genresListView->rootIndex().parent());
341         }
342     }
343 }
344 
folderGoBack()345 void FinderWidget::folderGoBack() {
346     folderBreadcrumb->goBack();
347     QModelIndex index = fileSystemView->rootIndex();
348     QSortFilterProxyModel *proxyModel =
349             static_cast<QSortFilterProxyModel *>(fileSystemView->model());
350     if (proxyModel) {
351         index = proxyModel->mapToSource(index);
352         QString path = fileSystemModel->filePath(index);
353         QDir dir(path);
354         dir.cdUp();
355         // qDebug() << d << folderListModel->rootPath();
356         if (dir.absolutePath() == fileSystemModel->rootDirectory().absolutePath()) {
357             folderBreadcrumb->clear();
358             folderBreadcrumb->hide();
359         }
360         index = fileSystemModel->index(dir.absolutePath(), 0);
361         qDebug() << dir.absolutePath() << index.isValid();
362         index = proxyModel->mapFromSource(index);
363         fileSystemView->setRootIndex(index);
364     }
365 }
366 
appear()367 void FinderWidget::appear() {
368     QWidget *currentWidget = stackedWidget->currentWidget();
369     if (currentWidget) {
370         bool success = QMetaObject::invokeMethod(stackedWidget->currentWidget(), "appear",
371                                                  Qt::DirectConnection);
372         if (!success) qDebug() << "Error invoking appear() on" << stackedWidget->currentWidget();
373     }
374 }
375 
disappear()376 void FinderWidget::disappear() {
377     QWidget *currentWidget = stackedWidget->currentWidget();
378     if (currentWidget) {
379         bool success = QMetaObject::invokeMethod(stackedWidget->currentWidget(), "disappear",
380                                                  Qt::DirectConnection);
381         if (!success) qDebug() << "Error invoking disappear() on" << stackedWidget->currentWidget();
382     }
383 }
384 
artistActivated(const QModelIndex & index)385 void FinderWidget::artistActivated(const QModelIndex &index) {
386     // get the data object
387     const ArtistPointer artistPointer = index.data(Finder::DataObjectRole).value<ArtistPointer>();
388     Artist *artist = artistPointer.data();
389     if (artist) artistActivated(artist);
390 }
391 
artistActivated(Artist * artist)392 void FinderWidget::artistActivated(Artist *artist) {
393     if (!albumListView) setupAlbums();
394 
395     QString qry("select id from albums where artist=%1 and trackCount>0 order by year desc, "
396                 "trackCount desc");
397     qry = qry.arg(artist->getId());
398     qDebug() << qry;
399     albumListModel->setQuery(qry, Database::instance().getConnection());
400     if (albumListModel->lastError().isValid()) qDebug() << albumListModel->lastError();
401 
402     albumListView->setWindowTitle(artist->getName());
403     albumListView->scrollToTop();
404     albumListView->setShowToolBar(false);
405     showWidget(albumListView, false);
406 }
407 
artistPlayed(const QModelIndex & index)408 void FinderWidget::artistPlayed(const QModelIndex &index) {
409     const ArtistPointer artistPointer = index.data(Finder::DataObjectRole).value<ArtistPointer>();
410     Artist *artist = artistPointer.data();
411     if (artist) {
412         QVector<Track *> tracks = artist->getTracks();
413         addTracksAndPlay(tracks);
414     }
415 }
416 
albumActivated(Album * album)417 void FinderWidget::albumActivated(Album *album) {
418     if (!trackListView) setupTracks();
419 
420     QString qry("select id from tracks where album=%1 order by disk, track, path");
421     qry = qry.arg(album->getId());
422     qDebug() << qry;
423     trackListModel->setQuery(qry, Database::instance().getConnection());
424     if (trackListModel->lastError().isValid()) qDebug() << trackListModel->lastError();
425 
426     trackListView->setWindowTitle(album->getName());
427     trackListView->scrollToTop();
428     showWidget(trackListView, false);
429 }
430 
albumActivated(const QModelIndex & index)431 void FinderWidget::albumActivated(const QModelIndex &index) {
432     const AlbumPointer albumPointer = index.data(Finder::DataObjectRole).value<AlbumPointer>();
433     Album *album = albumPointer.data();
434     if (album) albumActivated(album);
435 }
436 
albumPlayed(const QModelIndex & index)437 void FinderWidget::albumPlayed(const QModelIndex &index) {
438     const AlbumPointer albumPointer = index.data(Finder::DataObjectRole).value<AlbumPointer>();
439     Album *album = albumPointer.data();
440     if (album) {
441         QVector<Track *> tracks = album->getTracks();
442         addTracksAndPlay(tracks);
443     }
444 }
445 
trackActivated(Track * track)446 void FinderWidget::trackActivated(Track *track) {
447     playlistModel->addTrack(track);
448     playlistModel->setActiveRow(playlistModel->rowForTrack(track));
449 }
450 
trackActivated(const QModelIndex & index)451 void FinderWidget::trackActivated(const QModelIndex &index) {
452     const TrackPointer trackPointer = index.data(Finder::DataObjectRole).value<TrackPointer>();
453     Track *track = trackPointer.data();
454     if (track) trackActivated(track);
455 }
456 
folderActivated(const QModelIndex & index)457 void FinderWidget::folderActivated(const QModelIndex &index) {
458     const TrackPointer trackPointer = index.data(Finder::DataObjectRole).value<TrackPointer>();
459     Track *track = trackPointer.data();
460     if (track) {
461         addTracksAndPlay(track->getTracks());
462     } else {
463         fileSystemView->setRootIndex(index);
464         const FolderPointer folderPointer =
465                 index.data(Finder::DataObjectRole).value<FolderPointer>();
466         Folder *folder = folderPointer.data();
467         if (folder) {
468             folderBreadcrumb->addItem(folder->getName());
469             folderBreadcrumb->setVisible(true);
470         }
471     }
472 }
473 
folderPlayed(const QModelIndex & index)474 void FinderWidget::folderPlayed(const QModelIndex &index) {
475     const FolderPointer folderPointer = index.data(Finder::DataObjectRole).value<FolderPointer>();
476     Folder *folder = folderPointer.data();
477     if (folder) {
478         QVector<Track *> tracks = folder->getTracks();
479         addTracksAndPlay(tracks);
480     } else {
481         const TrackPointer trackPointer = index.data(Finder::DataObjectRole).value<TrackPointer>();
482         Track *track = trackPointer.data();
483         if (track) {
484             addTracksAndPlay(track->getTracks());
485         }
486     }
487 }
488 
genreActivated(const QModelIndex & index)489 void FinderWidget::genreActivated(const QModelIndex &index) {
490     qDebug() << "Activating index" << index;
491     Genre *genre = index.data(Finder::DataObjectRole).value<GenrePointer>().data();
492     if (genre && !genre->getChildren().isEmpty()) {
493         genresListView->setWindowTitle(genre->getName());
494         genresListView->setRootIndex(index);
495         showWidget(genresListView, false);
496     }
497 }
498 
genrePlayed(const QModelIndex & index)499 void FinderWidget::genrePlayed(const QModelIndex &index) {
500     Item *item = index.data(Finder::ItemObjectRole).value<ItemPointer>().data();
501     if (item) addTracksAndPlay(item->getTracks());
502 }
503 
addTracksAndPlay(const QVector<Track * > & tracks)504 void FinderWidget::addTracksAndPlay(const QVector<Track *> &tracks) {
505     if (tracks.isEmpty()) return;
506     playlistModel->addTracks(tracks);
507 
508     Track *trackToPlay = nullptr;
509 
510     QSettings settings;
511     const bool shuffle = settings.value("shuffle").toBool();
512     if (shuffle) {
513         int nextRow = (int)((float)qrand() / (float)RAND_MAX * tracks.size());
514         trackToPlay = tracks.at(nextRow);
515     } else {
516         trackToPlay = tracks.first();
517     }
518     playlistModel->setActiveRow(playlistModel->rowForTrack(trackToPlay));
519     playlistView->scrollTo(playlistModel->indexForTrack(trackToPlay),
520                            QAbstractItemView::PositionAtCenter);
521 }
522