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