1 /*
2  * Strawberry Music Player
3  * This code was part of Clementine
4  * Copyright 2010, David Sansome <me@davidsansome.com>
5  * Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
6  *
7  * Strawberry is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Strawberry is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  */
21 
22 #include "config.h"
23 
24 #include <QWidget>
25 #include <QTreeView>
26 #include <QSortFilterProxyModel>
27 #include <QAbstractItemView>
28 #include <QItemSelectionModel>
29 #include <QMap>
30 #include <QVariant>
31 #include <QString>
32 #include <QUrl>
33 #include <QPainter>
34 #include <QRect>
35 #include <QFont>
36 #include <QFontMetrics>
37 #include <QMimeData>
38 #include <QMenu>
39 #include <QAction>
40 #include <QtEvents>
41 
42 #include "core/application.h"
43 #include "core/iconloader.h"
44 #include "core/mimedata.h"
45 #include "collection/collectionbackend.h"
46 #include "collection/collectionmodel.h"
47 #include "collection/collectionfilterwidget.h"
48 #include "collection/collectionitem.h"
49 #include "collection/collectionitemdelegate.h"
50 #include "internetcollectionview.h"
51 
InternetCollectionView(QWidget * parent)52 InternetCollectionView::InternetCollectionView(QWidget *parent)
53     : AutoExpandingTreeView(parent),
54       app_(nullptr),
55       collection_backend_(nullptr),
56       collection_model_(nullptr),
57       filter_(nullptr),
58       favorite_(false),
59       total_song_count_(0),
60       total_artist_count_(0),
61       total_album_count_(0),
62       nomusic_(":/pictures/nomusic.png"),
63       context_menu_(nullptr),
64       load_(nullptr),
65       add_to_playlist_(nullptr),
66       add_to_playlist_enqueue_(nullptr),
67       add_to_playlist_enqueue_next_(nullptr),
68       open_in_new_playlist_(nullptr),
69       remove_songs_(nullptr),
70       is_in_keyboard_search_(false) {
71 
72   setItemDelegate(new CollectionItemDelegate(this));
73   setAttribute(Qt::WA_MacShowFocusRect, false);
74   setHeaderHidden(true);
75   setAllColumnsShowFocus(true);
76   setDragEnabled(true);
77   setDragDropMode(QAbstractItemView::DragOnly);
78   setSelectionMode(QAbstractItemView::ExtendedSelection);
79   SetAutoOpen(false);
80 
81   setStyleSheet("QTreeView::item{padding-top:1px;}");
82 
83 }
84 
Init(Application * app,CollectionBackend * backend,CollectionModel * model,const bool favorite)85 void InternetCollectionView::Init(Application *app, CollectionBackend *backend, CollectionModel *model, const bool favorite) {
86 
87   app_ = app;
88   collection_backend_ = backend;
89   collection_model_ = model;
90   favorite_ = favorite;
91 
92   collection_model_->set_pretty_covers(true);
93   collection_model_->set_show_dividers(true);
94 
95   ReloadSettings();
96 
97 }
98 
SetFilter(CollectionFilterWidget * filter)99 void InternetCollectionView::SetFilter(CollectionFilterWidget *filter) {
100 
101   filter_ = filter;
102 
103 }
104 
ReloadSettings()105 void InternetCollectionView::ReloadSettings() {
106 
107   if (filter_) filter_->ReloadSettings();
108 
109 }
110 
SaveFocus()111 void InternetCollectionView::SaveFocus() {
112 
113   QModelIndex current = currentIndex();
114   QVariant type = model()->data(current, CollectionModel::Role_Type);
115   if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Song || type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) {
116     return;
117   }
118 
119   last_selected_path_.clear();
120   last_selected_song_ = Song();
121   last_selected_container_ = QString();
122 
123   switch (type.toInt()) {
124     case CollectionItem::Type_Song: {
125       QModelIndex idx = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
126       SongList songs = collection_model_->GetChildSongs(idx);
127       if (!songs.isEmpty()) {
128         last_selected_song_ = songs.last();
129       }
130       break;
131     }
132 
133     case CollectionItem::Type_Container:
134     case CollectionItem::Type_Divider: {
135       QString text = model()->data(current, CollectionModel::Role_SortText).toString();
136       last_selected_container_ = text;
137       break;
138     }
139 
140     default:
141       return;
142   }
143 
144   SaveContainerPath(current);
145 
146 }
147 
SaveContainerPath(const QModelIndex & child)148 void InternetCollectionView::SaveContainerPath(const QModelIndex &child) {
149 
150   QModelIndex current = model()->parent(child);
151   QVariant type = model()->data(current, CollectionModel::Role_Type);
152   if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) {
153     return;
154   }
155 
156   QString text = model()->data(current, CollectionModel::Role_SortText).toString();
157   last_selected_path_ << text;
158   SaveContainerPath(current);
159 
160 }
161 
RestoreFocus()162 void InternetCollectionView::RestoreFocus() {
163 
164   if (last_selected_container_.isEmpty() && last_selected_song_.url().isEmpty()) {
165     return;
166   }
167   RestoreLevelFocus();
168 
169 }
170 
RestoreLevelFocus(const QModelIndex & parent)171 bool InternetCollectionView::RestoreLevelFocus(const QModelIndex &parent) {
172 
173   if (model()->canFetchMore(parent)) {
174     model()->fetchMore(parent);
175   }
176   int rows = model()->rowCount(parent);
177   for (int i = 0; i < rows; i++) {
178     QModelIndex current = model()->index(i, 0, parent);
179     QVariant type = model()->data(current, CollectionModel::Role_Type);
180     switch (type.toInt()) {
181       case CollectionItem::Type_Song:
182         if (!last_selected_song_.url().isEmpty()) {
183           QModelIndex idx = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
184           SongList songs = collection_model_->GetChildSongs(idx);
185           for (const Song &song : songs) {
186             if (song == last_selected_song_) {
187               setCurrentIndex(current);
188               return true;
189             }
190           }
191         }
192         break;
193 
194       case CollectionItem::Type_Container:
195       case CollectionItem::Type_Divider: {
196         QString text = model()->data(current, CollectionModel::Role_SortText).toString();
197         if (!last_selected_container_.isEmpty() && last_selected_container_ == text) {
198           expand(current);
199           setCurrentIndex(current);
200           return true;
201         }
202         else if (last_selected_path_.contains(text)) {
203           expand(current);
204           // If a selected container or song were not found, we've got into a wrong subtree (happens with "unknown" all the time)
205           if (!RestoreLevelFocus(current)) {
206             collapse(current);
207           }
208           else {
209             return true;
210           }
211         }
212         break;
213       }
214     }
215   }
216   return false;
217 
218 }
219 
TotalSongCountUpdated(int count)220 void InternetCollectionView::TotalSongCountUpdated(int count) {
221 
222   int old = total_song_count_;
223   total_song_count_ = count;
224   if (old != total_song_count_) update();
225 
226   if (total_song_count_ == 0) {
227     setCursor(Qt::PointingHandCursor);
228   }
229   else {
230     unsetCursor();
231   }
232 
233   emit TotalSongCountUpdated_();
234 
235 }
236 
TotalArtistCountUpdated(int count)237 void InternetCollectionView::TotalArtistCountUpdated(int count) {
238 
239   int old = total_artist_count_;
240   total_artist_count_ = count;
241   if (old != total_artist_count_) update();
242 
243   if (total_artist_count_ == 0) {
244     setCursor(Qt::PointingHandCursor);
245   }
246   else {
247     unsetCursor();
248   }
249 
250   emit TotalArtistCountUpdated_();
251 
252 }
253 
TotalAlbumCountUpdated(int count)254 void InternetCollectionView::TotalAlbumCountUpdated(int count) {
255 
256   int old = total_album_count_;
257   total_album_count_ = count;
258   if (old != total_album_count_) update();
259 
260   if (total_album_count_ == 0) {
261     setCursor(Qt::PointingHandCursor);
262   }
263   else {
264     unsetCursor();
265   }
266 
267   emit TotalAlbumCountUpdated_();
268 
269 }
270 
paintEvent(QPaintEvent * event)271 void InternetCollectionView::paintEvent(QPaintEvent *event) {
272 
273   if (total_song_count_ == 0) {
274     QPainter p(viewport());
275     QRect rect(viewport()->rect());
276 
277     // Draw the confused strawberry
278     QRect image_rect((rect.width() - nomusic_.width()) / 2, 50, nomusic_.width(), nomusic_.height());
279     p.drawPixmap(image_rect, nomusic_);
280 
281     // Draw the title text
282     QFont bold_font;
283     bold_font.setBold(true);
284     p.setFont(bold_font);
285 
286     QFontMetrics metrics(bold_font);
287 
288     QRect title_rect(0, image_rect.bottom() + 20, rect.width(), metrics.height());
289     p.drawText(title_rect, Qt::AlignHCenter, tr("The internet collection is empty!"));
290 
291     // Draw the other text
292     p.setFont(QFont());
293 
294     QRect text_rect(0, title_rect.bottom() + 5, rect.width(), metrics.height());
295     p.drawText(text_rect, Qt::AlignHCenter, tr("Click here to retrieve music"));
296   }
297   else {
298     QTreeView::paintEvent(event);
299   }
300 
301 }
302 
mouseReleaseEvent(QMouseEvent * e)303 void InternetCollectionView::mouseReleaseEvent(QMouseEvent *e) {
304 
305   QTreeView::mouseReleaseEvent(e);
306 
307   if (total_song_count_ == 0) {
308     emit GetSongs();
309   }
310 
311 }
312 
contextMenuEvent(QContextMenuEvent * e)313 void InternetCollectionView::contextMenuEvent(QContextMenuEvent *e) {
314 
315   if (!context_menu_) {
316     context_menu_ = new QMenu(this);
317     add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, &InternetCollectionView::AddToPlaylist);
318     load_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, &InternetCollectionView::Load);
319     open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, &InternetCollectionView::OpenInNewPlaylist);
320 
321     context_menu_->addSeparator();
322     add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, &InternetCollectionView::AddToPlaylistEnqueue);
323     add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue to play next"), this, &InternetCollectionView::AddToPlaylistEnqueueNext);
324 
325     context_menu_->addSeparator();
326 
327     if (favorite_) {
328       remove_songs_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Remove from favorites"), this, &InternetCollectionView::RemoveSelectedSongs);
329       context_menu_->addSeparator();
330     }
331 
332     if (filter_) context_menu_->addMenu(filter_->menu());
333 
334   }
335 
336   context_menu_index_ = indexAt(e->pos());
337   if (!context_menu_index_.isValid()) return;
338 
339   context_menu_index_ = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(context_menu_index_);
340   QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
341   int songs_selected = selected_indexes.count();;
342 
343   // In all modes
344   load_->setEnabled(songs_selected > 0);
345   add_to_playlist_->setEnabled(songs_selected > 0);
346   open_in_new_playlist_->setEnabled(songs_selected > 0);
347   add_to_playlist_enqueue_->setEnabled(songs_selected > 0);
348   if (remove_songs_) remove_songs_->setEnabled(songs_selected > 0);
349 
350   context_menu_->popup(e->globalPos());
351 
352 }
353 
Load()354 void InternetCollectionView::Load() {
355 
356   QMimeData *q_mimedata = model()->mimeData(selectedIndexes());
357   if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
358     mimedata->clear_first_ = true;
359   }
360   emit AddToPlaylistSignal(q_mimedata);
361 
362 }
363 
AddToPlaylist()364 void InternetCollectionView::AddToPlaylist() {
365 
366   emit AddToPlaylistSignal(model()->mimeData(selectedIndexes()));
367 
368 }
369 
AddToPlaylistEnqueue()370 void InternetCollectionView::AddToPlaylistEnqueue() {
371 
372   QMimeData *q_mimedata = model()->mimeData(selectedIndexes());
373   if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
374     mimedata->enqueue_now_ = true;
375   }
376   emit AddToPlaylistSignal(q_mimedata);
377 
378 }
379 
AddToPlaylistEnqueueNext()380 void InternetCollectionView::AddToPlaylistEnqueueNext() {
381 
382   QMimeData *q_mimedata = model()->mimeData(selectedIndexes());
383   if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
384     mimedata->enqueue_next_now_ = true;
385   }
386   emit AddToPlaylistSignal(q_mimedata);
387 
388 }
389 
OpenInNewPlaylist()390 void InternetCollectionView::OpenInNewPlaylist() {
391 
392   QMimeData *q_mimedata = model()->mimeData(selectedIndexes());
393   if (MimeData* mimedata = qobject_cast<MimeData*>(q_mimedata)) {
394     mimedata->open_in_new_playlist_ = true;
395   }
396   emit AddToPlaylistSignal(q_mimedata);
397 
398 }
399 
RemoveSelectedSongs()400 void InternetCollectionView::RemoveSelectedSongs() {
401 
402   emit RemoveSongs(GetSelectedSongs());
403 
404 }
405 
keyboardSearch(const QString & search)406 void InternetCollectionView::keyboardSearch(const QString &search) {
407 
408   is_in_keyboard_search_ = true;
409   QTreeView::keyboardSearch(search);
410   is_in_keyboard_search_ = false;
411 
412 }
413 
scrollTo(const QModelIndex & idx,ScrollHint hint)414 void InternetCollectionView::scrollTo(const QModelIndex &idx, ScrollHint hint) {
415 
416   if (is_in_keyboard_search_) {
417     QTreeView::scrollTo(idx, QAbstractItemView::PositionAtTop);
418   }
419   else {
420     QTreeView::scrollTo(idx, hint);
421   }
422 
423 }
424 
GetSelectedSongs() const425 SongList InternetCollectionView::GetSelectedSongs() const {
426 
427   QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
428   return collection_model_->GetChildSongs(selected_indexes);
429 
430 }
431 
FilterReturnPressed()432 void InternetCollectionView::FilterReturnPressed() {
433 
434   if (!currentIndex().isValid()) {
435     // Pick the first thing that isn't a divider
436     for (int row = 0; row < model()->rowCount(); ++row) {
437       QModelIndex idx(model()->index(row, 0));
438       if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) {
439         setCurrentIndex(idx);
440         break;
441       }
442     }
443   }
444 
445   if (!currentIndex().isValid()) return;
446 
447   emit doubleClicked(currentIndex());
448 
449 }
450 
TotalSongs() const451 int InternetCollectionView::TotalSongs() const {
452   return total_song_count_;
453 }
TotalArtists() const454 int InternetCollectionView::TotalArtists() const {
455   return total_artist_count_;
456 }
TotalAlbums() const457 int InternetCollectionView::TotalAlbums() const {
458   return total_album_count_;
459 }
460