1 /**
2  * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
3  * Copyright (C) 2021      Michael Pyne  <mpyne@kde.org>
4  *
5  * This program is free software; you can redistribute it and/or modify it under
6  * the terms of the GNU General Public License as published by the Free Software
7  * Foundation; either version 2 of the License, or (at your option) any later
8  * version.
9  *
10  * This program is distributed in the hope that it will be useful, but WITHOUT ANY
11  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12  * PARTICULAR PURPOSE. See the GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along with
15  * this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "playlistbox.h"
19 
20 #include <kmessagebox.h>
21 #include <kactioncollection.h>
22 #include <ktoggleaction.h>
23 #include <kselectaction.h>
24 #include <kconfiggroup.h>
25 #include <KSharedConfig>
26 
27 #include <QAction>
28 #include <QIcon>
29 #include <QMenu>
30 #include <QPainter>
31 #include <QTimer>
32 #include <QDragLeaveEvent>
33 #include <QList>
34 #include <QDragMoveEvent>
35 #include <QKeyEvent>
36 #include <QDropEvent>
37 #include <QMouseEvent>
38 #include <QFileInfo>
39 #include <QTime>
40 #include <QApplication>
41 #include <QClipboard>
42 #include <QHeaderView>
43 #include <QElapsedTimer>
44 
45 #include "actioncollection.h"
46 #include "cache.h"
47 #include "collectionlist.h"
48 #include "dbuscollectionproxy.h"
49 #include "dynamicplaylist.h"
50 #include "historyplaylist.h"
51 #include "iconsupport.h"
52 #include "juk_debug.h"
53 #include "playermanager.h"
54 #include "playlist.h"
55 #include "searchplaylist.h"
56 #include "tagtransactionmanager.h"
57 #include "treeviewitemplaylist.h"
58 #include "upcomingplaylist.h"
59 #include "viewmode.h"
60 
61 using namespace ActionCollection; // ""_act and others
62 using namespace IconSupport;      // ""_icon
63 
64 ////////////////////////////////////////////////////////////////////////////////
65 // PlaylistBox public methods
66 ////////////////////////////////////////////////////////////////////////////////
67 
PlaylistBox(PlayerManager * player,QWidget * parent,QStackedWidget * playlistStack)68 PlaylistBox::PlaylistBox(PlayerManager *player, QWidget *parent, QStackedWidget *playlistStack)
69   : QTreeWidget(parent)
70   , PlaylistCollection(player, playlistStack)
71 {
72     readConfig();
73     setHeaderLabel("Playlists");
74     setRootIsDecorated(false);
75     setContextMenuPolicy(Qt::CustomContextMenu);
76     viewport()->setAcceptDrops(true);
77     setDragDropMode(QAbstractItemView::DropOnly);
78     setDropIndicatorShown(true);
79 
80     setColumnCount(2); // Use fake column for sorting
81     setColumnHidden(1, true);
82     setSortingEnabled(true);
83     sortByColumn(1, Qt::AscendingOrder);
84 
85     header()->blockSignals(true);
86     header()->hide();
87     header()->blockSignals(false);
88 
89     setSelectionMode(QAbstractItemView::ExtendedSelection);
90 
91     m_contextMenu = new QMenu(this);
92 
93     m_contextMenu->addAction(action("file_new"));
94     m_contextMenu->addAction(action("renamePlaylist"));
95     m_contextMenu->addAction(action("editSearch"));
96     m_contextMenu->addAction(action("duplicatePlaylist"));
97     m_contextMenu->addAction(action("reloadPlaylist"));
98     m_contextMenu->addAction(action("deleteItemPlaylist"));
99     m_contextMenu->addAction(action("file_save"));
100     m_contextMenu->addAction(action("file_save_as"));
101 
102     m_contextMenu->addSeparator();
103 
104     // add the view modes stuff
105 
106     KSelectAction *viewModeAction =
107         new KSelectAction("view-choose"_icon, i18n("View Modes"), ActionCollection::actions());
108     ActionCollection::actions()->addAction("viewModeMenu", viewModeAction);
109 
110     ViewMode* viewmode = new ViewMode(this);
111     m_viewModes.append(viewmode);
112     viewModeAction->addAction("view-list-details"_icon, viewmode->name());
113 
114     CompactViewMode* compactviewmode = new CompactViewMode(this);
115     m_viewModes.append(compactviewmode);
116     viewModeAction->addAction("view-list-text"_icon, compactviewmode->name());
117 
118     // TODO: Fix the broken tree view mode
119 #if 0
120     TreeViewMode* treeviewmode = new TreeViewMode(this);
121     m_viewModes.append(treeviewmode);
122     viewModeAction->addAction("view-list-tree"_icon, treeviewmode->name());
123 #endif
124 
125     CollectionList::initialize(this);
126 
127     viewModeAction->setCurrentItem(m_viewModeIndex);
128     m_viewModes[m_viewModeIndex]->setShown(true);
129 
130     raise(CollectionList::instance());
131 
132     m_contextMenu->addAction(viewModeAction);
133     connect(viewModeAction, &QAction::triggered,
134             this, &PlaylistBox::slotSetViewMode);
135 
136     connect(this, &PlaylistBox::itemSelectionChanged,
137             this, &PlaylistBox::slotPlaylistChanged);
138 
139     connect(this, &PlaylistBox::itemDoubleClicked,
140             this, &PlaylistBox::slotDoubleClicked);
141 
142     connect(this, &PlaylistBox::customContextMenuRequested,
143             this, &PlaylistBox::slotShowContextMenu);
144 
145     connect(this, &PlaylistBox::signalPlayFile,
146             player, qOverload<const FileHandle &>(&PlayerManager::play));
147 
148     const auto *tagManager = TagTransactionManager::instance();
149     connect(tagManager, &TagTransactionManager::signalAboutToModifyTags,
150             this,       &PlaylistBox::slotFreezePlaylists);
151     connect(tagManager, &TagTransactionManager::signalDoneModifyingTags,
152             this,       &PlaylistBox::slotUnfreezePlaylists);
153 
154     setupUpcomingPlaylist();
155 
156     const auto *collectionList = CollectionList::instance();
157     connect(collectionList, &CollectionList::signalNewTag,
158             this,           &PlaylistBox::slotAddItem);
159     connect(collectionList, &CollectionList::signalRemovedTag,
160             this,           &PlaylistBox::slotRemoveItem);
161     connect(collectionList, &CollectionList::cachedItemsLoaded,
162             this,           &PlaylistBox::slotLoadCachedPlaylists);
163 
164     KToggleAction *historyAction =
165         new KToggleAction("view-history"_icon, i18n("Show &History"), ActionCollection::actions());
166     ActionCollection::actions()->addAction("showHistory", historyAction);
167     connect(historyAction, &KToggleAction::triggered,
168             this,          &PlaylistBox::slotSetHistoryPlaylistEnabled);
169 
170     m_showTimer = new QTimer(this);
171     m_showTimer->setSingleShot(true);
172     m_showTimer->setInterval(500);
173     connect(m_showTimer, &QTimer::timeout,
174             this,        &PlaylistBox::slotShowDropTarget);
175 
176     // hook up to the D-Bus
177     (void) new DBusCollectionProxy(this, this);
178 }
179 
~PlaylistBox()180 PlaylistBox::~PlaylistBox()
181 {
182     PlaylistList l;
183     CollectionList *collection = CollectionList::instance();
184     for(QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it) {
185         Item *item = static_cast<Item *>(*it);
186         if(item->playlist() && item->playlist() != collection)
187             l.append(item->playlist());
188     }
189 
190     Cache::savePlaylists(l);
191     saveConfig();
192 
193     // Some view modes use event filters onto sibling widgets which may be
194     // destroyed before the view mode.
195     // Manually delete the view modes instead.
196     qDeleteAll(m_viewModes);
197     m_viewModes.clear();
198 }
199 
raise(Playlist * playlist)200 void PlaylistBox::raise(Playlist *playlist)
201 {
202     if(!playlist)
203         return;
204 
205     Item *i = m_playlistDict.value(playlist, 0);
206 
207     if(i) {
208         clearSelection();
209         setCurrentItem(i);
210 
211         setSingleItem(i);
212         scrollToItem(currentItem());
213     }
214     else
215         PlaylistCollection::raise(playlist);
216 
217     slotPlaylistChanged();
218 }
219 
duplicate()220 void PlaylistBox::duplicate()
221 {
222     Item *item = static_cast<Item *>(currentItem());
223     if(!item || !item->playlist())
224         return;
225 
226     QString name = playlistNameDialog(i18nc("verb, copy the playlist", "Duplicate"), item->text(0));
227 
228     if(name.isNull())
229         return;
230 
231     Playlist *p = new Playlist(this, name);
232     p->createItems(item->playlist()->items());
233 }
234 
scanFolders()235 void PlaylistBox::scanFolders()
236 {
237     PlaylistCollection::scanFolders();
238     emit startupComplete();
239 }
240 
requestPlaybackFor(const FileHandle & file)241 bool PlaylistBox::requestPlaybackFor(const FileHandle &file)
242 {
243     emit signalPlayFile(file);
244     return true;
245 }
246 
247 ////////////////////////////////////////////////////////////////////////////////
248 // PlaylistBox public slots
249 ////////////////////////////////////////////////////////////////////////////////
250 
paste()251 void PlaylistBox::paste()
252 {
253     // TODO: Reimplement
254 }
255 
256 ////////////////////////////////////////////////////////////////////////////////
257 // PlaylistBox protected methods
258 ////////////////////////////////////////////////////////////////////////////////
259 
slotFreezePlaylists()260 void PlaylistBox::slotFreezePlaylists()
261 {
262     setDynamicListsFrozen(true);
263 }
264 
slotUnfreezePlaylists()265 void PlaylistBox::slotUnfreezePlaylists()
266 {
267     setDynamicListsFrozen(false);
268 }
269 
slotPlaylistDataChanged()270 void PlaylistBox::slotPlaylistDataChanged()
271 {
272     if(m_savePlaylistTimer)
273         m_savePlaylistTimer->start(); // Restarts the timer if it's already running.
274 }
275 
slotSetHistoryPlaylistEnabled(bool enable)276 void PlaylistBox::slotSetHistoryPlaylistEnabled(bool enable)
277 {
278     setHistoryPlaylistEnabled(enable);
279 }
280 
setupPlaylist(Playlist * playlist,const QString & iconName)281 void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName)
282 {
283     setupPlaylist(playlist, iconName, nullptr);
284 }
285 
setupPlaylist(Playlist * playlist,const QString & iconName,Item * parentItem)286 void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName, Item *parentItem)
287 {
288     connect(playlist, &Playlist::signalPlaylistItemsDropped,
289             this,     &PlaylistBox::slotPlaylistItemsDropped);
290     connect(playlist, &Playlist::signalMoveFocusAway,
291             this,     &PlaylistBox::signalMoveFocusAway);
292 
293     PlaylistCollection::setupPlaylist(playlist, iconName);
294 
295     if(parentItem)
296         new Item(parentItem, iconName, playlist->name(), playlist);
297     else
298         new Item(this, iconName, playlist->name(), playlist);
299 }
300 
removePlaylist(Playlist * playlist)301 void PlaylistBox::removePlaylist(Playlist *playlist)
302 {
303     // Could be false if setup() wasn't run yet.
304     if(m_playlistDict.contains(playlist)) {
305         removeNameFromDict(m_playlistDict[playlist]->text(0));
306         delete m_playlistDict[playlist]; // Delete the Item*
307     }
308 
309     removeFileFromDict(playlist->fileName());
310     m_playlistDict.remove(playlist);
311 }
312 
supportedDropActions() const313 Qt::DropActions PlaylistBox::supportedDropActions() const
314 {
315     return Qt::CopyAction;
316 }
317 
dropMimeData(QTreeWidgetItem * parent,int index,const QMimeData * data,Qt::DropAction action)318 bool PlaylistBox::dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action)
319 {
320     Q_UNUSED(index);
321 
322     // The *parent* item won't be null, but index should be zero except in the
323     // still-broken "tree view" mode.
324 
325     if(!parent || action != Qt::CopyAction || !data->hasUrls()) {
326         return false;
327     }
328 
329     auto *playlistItem = static_cast<Item *>(parent);
330     if(!playlistItem) {
331         return false;
332     }
333 
334     auto *playlist = playlistItem->playlist();
335     const auto droppedUrls = data->urls();
336     PlaylistItem *lastItem = nullptr;
337 
338     for(const auto &url : droppedUrls) {
339         lastItem = playlist->createItem(FileHandle(url.toLocalFile()), lastItem);
340     }
341 
342     return true;
343 }
344 
mimeTypes() const345 QStringList PlaylistBox::mimeTypes() const
346 {
347     auto result = QTreeWidget::mimeTypes();
348 
349     // Need to add Playlists's mime type to convince QTreeWidget to allow it as
350     // a drop option.
351     result.append(QLatin1String("text/uri-list"));
352 
353     return result;
354 }
355 
356 ////////////////////////////////////////////////////////////////////////////////
357 // PlaylistBox private methods
358 ////////////////////////////////////////////////////////////////////////////////
359 
readConfig()360 void PlaylistBox::readConfig()
361 {
362     KConfigGroup config(KSharedConfig::openConfig(), "PlaylistBox");
363     m_viewModeIndex = config.readEntry("ViewMode", 0);
364 
365     // TODO Restore ability to use Tree View once fixed.
366     if(m_viewModeIndex == 2) {
367         m_viewModeIndex = 0;
368     }
369 }
370 
saveConfig()371 void PlaylistBox::saveConfig()
372 {
373     KConfigGroup config(KSharedConfig::openConfig(), "PlaylistBox");
374     config.writeEntry("ViewMode", action<KSelectAction>("viewModeMenu")->currentItem());
375     KSharedConfig::openConfig()->sync();
376 }
377 
remove()378 void PlaylistBox::remove()
379 {
380     const ItemList items = selectedBoxItems();
381 
382     if(items.isEmpty())
383         return;
384 
385     QStringList files;
386     QStringList names;
387 
388     for(const auto &item : items) {
389         if(!item || !item->playlist()) {
390             qFatal("Ran into an empty item or item playlist when removing playlists!");
391         }
392 
393         if (!item->playlist()->fileName().isEmpty() &&
394             QFileInfo::exists(item->playlist()->fileName()))
395         {
396             files.append(item->playlist()->fileName());
397         }
398 
399         names.append(item->playlist()->name());
400     }
401 
402     if(!files.isEmpty()) {
403         int remove = KMessageBox::warningYesNoCancelList(
404             this, i18n("Do you want to delete these files from the disk as well?"), files, QString(), KStandardGuiItem::del(), KGuiItem(i18n("Keep")));
405 
406         if(remove == KMessageBox::Yes) {
407             QStringList couldNotDelete;
408             for(const auto &playlistFile : qAsConst(files)) {
409                 if(!QFile::remove(playlistFile))
410                     couldNotDelete.append(playlistFile);
411             }
412 
413             if(!couldNotDelete.isEmpty())
414                 KMessageBox::errorList(this, i18n("Could not delete these files."), couldNotDelete);
415         }
416         else if(remove == KMessageBox::Cancel)
417             return;
418     }
419     else if(items.count() > 1 || items.front()->playlist() != upcomingPlaylist()) {
420         if(KMessageBox::Cancel == KMessageBox::warningContinueCancelList(
421             this,
422             i18n("Are you sure you want to remove these "
423                "playlists from your collection?"),
424             names,
425             i18n("Remove Items?"),
426             KGuiItem(i18n("&Remove"), "user-trash")))
427         {
428             return;
429         }
430     }
431 
432     for(const auto &item : items) {
433         if(item != Item::collectionItem() &&
434            !item->playlist()->readOnly())
435         {
436             if(item->playlist() != upcomingPlaylist())
437                 delete item;
438             else {
439                 action<KToggleAction>("showUpcoming")->setChecked(false);
440                 setUpcomingPlaylistEnabled(false);
441             }
442         }
443     }
444 
445     setSingleItem(Item::collectionItem());
446 }
447 
setDynamicListsFrozen(bool frozen)448 void PlaylistBox::setDynamicListsFrozen(bool frozen)
449 {
450     for(auto &playlistBoxItem : qAsConst(m_viewModes)) {
451         playlistBoxItem->setDynamicListsFrozen(frozen);
452     }
453 }
454 
slotSavePlaylists()455 void PlaylistBox::slotSavePlaylists()
456 {
457     qCDebug(JUK_LOG) << "Auto-saving playlists.";
458 
459     PlaylistList l;
460     CollectionList *collection = CollectionList::instance();
461     for(QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it) {
462         Item *item = static_cast<Item *>(*it);
463         if(item->playlist() && item->playlist() != collection)
464             l.append(item->playlist());
465     }
466 
467     Cache::savePlaylists(l);
468 }
469 
slotShowDropTarget()470 void PlaylistBox::slotShowDropTarget()
471 {
472     if(m_dropItem) raise(m_dropItem->playlist());
473 }
474 
slotAddItem(const QString & tag,unsigned column)475 void PlaylistBox::slotAddItem(const QString &tag, unsigned column)
476 {
477     for(auto &viewMode : qAsConst(m_viewModes)) {
478         viewMode->addItems(QStringList(tag), column);
479     }
480 }
481 
slotRemoveItem(const QString & tag,unsigned column)482 void PlaylistBox::slotRemoveItem(const QString &tag, unsigned column)
483 {
484     for(auto &viewMode : qAsConst(m_viewModes)) {
485         viewMode->removeItem(tag, column);
486     }
487 }
488 
mousePressEvent(QMouseEvent * e)489 void PlaylistBox::mousePressEvent(QMouseEvent *e)
490 {
491     if(e->button() == Qt::LeftButton)
492         m_doingMultiSelect = true;
493     QTreeWidget::mousePressEvent(e);
494 }
495 
mouseReleaseEvent(QMouseEvent * e)496 void PlaylistBox::mouseReleaseEvent(QMouseEvent *e)
497 {
498     if(e->button() == Qt::LeftButton) {
499         m_doingMultiSelect = false;
500         slotPlaylistChanged();
501     }
502     QTreeWidget::mouseReleaseEvent(e);
503 }
504 
keyPressEvent(QKeyEvent * e)505 void PlaylistBox::keyPressEvent(QKeyEvent *e)
506 {
507     if((e->key() == Qt::Key_Up || e->key() == Qt::Key_Down) && e->modifiers() == Qt::ShiftModifier)
508         m_doingMultiSelect = true;
509     QTreeWidget::keyPressEvent(e);
510 }
511 
keyReleaseEvent(QKeyEvent * e)512 void PlaylistBox::keyReleaseEvent(QKeyEvent *e)
513 {
514     if(m_doingMultiSelect && e->key() == Qt::Key_Shift) {
515         m_doingMultiSelect = false;
516         slotPlaylistChanged();
517     }
518     QTreeWidget::keyReleaseEvent(e);
519 }
520 
selectedBoxItems()521 PlaylistBox::ItemList PlaylistBox::selectedBoxItems()
522 {
523     ItemList l;
524 
525     for(QTreeWidgetItemIterator it(this, QTreeWidgetItemIterator::Selected)
526             ; *it
527             ; ++it)
528     {
529         l.append(static_cast<Item *>(*it));
530     }
531 
532     return l;
533 }
534 
setSingleItem(QTreeWidgetItem * item)535 void PlaylistBox::setSingleItem(QTreeWidgetItem *item)
536 {
537     setSelectionMode(QAbstractItemView::SingleSelection);
538     setCurrentItem(item);
539     setSelectionMode(QAbstractItemView::ExtendedSelection);
540 }
541 
dragMoveEvent(QDragMoveEvent * event)542 void PlaylistBox::dragMoveEvent(QDragMoveEvent* event)
543 {
544     QTreeWidget::dragMoveEvent(event);
545 
546     Item* hovered_item = static_cast<Item*>(itemAt(event->pos()));
547     if(hovered_item != m_dropItem){
548         m_dropItem = hovered_item;
549         if(m_dropItem) m_showTimer->start();
550         else m_showTimer->stop();
551     };
552 }
553 
dragLeaveEvent(QDragLeaveEvent * event)554 void PlaylistBox::dragLeaveEvent(QDragLeaveEvent* event)
555 {
556     QTreeWidget::dragLeaveEvent(event);
557     m_showTimer->stop();
558 }
559 
560 ////////////////////////////////////////////////////////////////////////////////
561 // PlaylistBox private slots
562 ////////////////////////////////////////////////////////////////////////////////
563 
slotPlaylistChanged()564 void PlaylistBox::slotPlaylistChanged()
565 {
566     // Don't update while the mouse is pressed down.
567 
568     if(m_doingMultiSelect)
569         return;
570 
571     const ItemList items = selectedBoxItems();
572     m_hasSelection = !items.isEmpty();
573 
574     const bool allowReload = std::any_of(items.begin(), items.end(),
575         [](const auto &item) {
576             return item->playlist() && item->playlist()->canReload();
577         });
578 
579     PlaylistList playlists;
580     for(const auto &playlistBoxItem : items) {
581         auto p = playlistBoxItem->playlist();
582         if(p) {
583             playlists.append(p);
584         }
585     }
586 
587     bool singlePlaylist = playlists.count() == 1;
588 
589     if(playlists.isEmpty() ||
590        (singlePlaylist &&
591         (playlists.front() == CollectionList::instance() ||
592          playlists.front()->readOnly())))
593     {
594         action("file_save")->setEnabled(false);
595         action("file_save_as")->setEnabled(false);
596         action("renamePlaylist")->setEnabled(false);
597         action("deleteItemPlaylist")->setEnabled(false);
598     }
599     else {
600         action("file_save")->setEnabled(true);
601         action("file_save_as")->setEnabled(true);
602         action("renamePlaylist")->setEnabled(playlists.count() == 1);
603         action("deleteItemPlaylist")->setEnabled(true);
604     }
605     action("reloadPlaylist")->setEnabled(allowReload);
606     action("duplicatePlaylist")->setEnabled(!playlists.isEmpty());
607 
608     action("editSearch")->setEnabled(singlePlaylist &&
609                                      playlists.front()->searchIsEditable());
610 
611     if(singlePlaylist) {
612         PlaylistCollection::raise(playlists.front());
613 
614         if(playlists.front() == upcomingPlaylist()) {
615             action("deleteItemPlaylist")->setText(i18n("Hid&e"));
616             action("deleteItemPlaylist")->setIcon("list-remove"_icon);
617         }
618         else {
619             action("deleteItemPlaylist")->setText(i18n("R&emove"));
620             action("deleteItemPlaylist")->setIcon("user-trash"_icon);
621         }
622     }
623     else if(!playlists.isEmpty())
624         createDynamicPlaylist(playlists);
625 }
626 
slotDoubleClicked(QTreeWidgetItem * item)627 void PlaylistBox::slotDoubleClicked(QTreeWidgetItem *item)
628 {
629     if(!item)
630         return;
631     auto *playlist = static_cast<Item *>(item)->playlist();
632 
633     playlist->slotBeginPlayback();
634 }
635 
slotShowContextMenu(const QPoint & point)636 void PlaylistBox::slotShowContextMenu(const QPoint &point)
637 {
638     m_contextMenu->popup(mapToGlobal(point));
639 }
640 
slotPlaylistItemsDropped(Playlist * p)641 void PlaylistBox::slotPlaylistItemsDropped(Playlist *p)
642 {
643     raise(p);
644 }
645 
slotSetViewMode(int index)646 void PlaylistBox::slotSetViewMode(int index)
647 {
648     if(index == m_viewModeIndex)
649         return;
650 
651     viewMode()->setShown(false);
652     m_viewModeIndex = index;
653     viewMode()->setShown(true);
654 }
655 
setupItem(Item * item)656 void PlaylistBox::setupItem(Item *item)
657 {
658     m_playlistDict.insert(item->playlist(), item);
659     viewMode()->queueRefresh();
660 }
661 
setupUpcomingPlaylist()662 void PlaylistBox::setupUpcomingPlaylist()
663 {
664     KConfigGroup config(KSharedConfig::openConfig(), "Playlists");
665     bool enable = config.readEntry("showUpcoming", false);
666 
667     setUpcomingPlaylistEnabled(enable);
668     action<KToggleAction>("showUpcoming")->setChecked(enable);
669 }
670 
671 
slotLoadCachedPlaylists()672 void PlaylistBox::slotLoadCachedPlaylists()
673 {
674     qCDebug(JUK_LOG) << "Loading cached playlists.";
675     QElapsedTimer stopwatch;
676     stopwatch.start();
677 
678     Cache::loadPlaylists(this);
679 
680     qCDebug(JUK_LOG) << "Cached playlists loaded, took" << stopwatch.elapsed() << "ms";
681 
682     // Auto-save playlists after they change.
683     m_savePlaylistTimer = new QTimer(this);
684     m_savePlaylistTimer->setInterval(3000); // 3 seconds with no change? -> commit
685     m_savePlaylistTimer->setSingleShot(true);
686     connect(m_savePlaylistTimer, &QTimer::timeout,
687             this, &PlaylistBox::slotSavePlaylists);
688 
689     clearSelection();
690     setCurrentItem(m_playlistDict[CollectionList::instance()]);
691 
692     QTimer::singleShot(0, this, [this]() {
693             CollectionList::instance()->slotCheckCache();
694             this->scanFolders();
695         });
696 }
697 
698 ////////////////////////////////////////////////////////////////////////////////
699 // PlaylistBox::Item protected methods
700 ////////////////////////////////////////////////////////////////////////////////
701 
702 PlaylistBox::Item *PlaylistBox::Item::m_collectionItem = nullptr;
703 
Item(PlaylistBox * listBox,const QString & icon,const QString & text,Playlist * l)704 PlaylistBox::Item::Item(PlaylistBox *listBox, const QString &icon, const QString &text, Playlist *l)
705     : QObject(listBox), QTreeWidgetItem(listBox, QStringList(text)),
706       m_playlist(l), m_iconName(icon), m_sortedFirst(false)
707 {
708     init();
709 }
710 
Item(Item * parent,const QString & icon,const QString & text,Playlist * l)711 PlaylistBox::Item::Item(Item *parent, const QString &icon, const QString &text, Playlist *l)
712     : QObject(parent->listView()), QTreeWidgetItem(parent, QStringList(text)),
713     m_playlist(l), m_iconName(icon), m_sortedFirst(false)
714 {
715     init();
716 }
717 
~Item()718 PlaylistBox::Item::~Item()
719 {
720 
721 }
722 
setup()723 void PlaylistBox::Item::setup()
724 {
725     listView()->viewMode()->setupItem(this);
726 }
727 
728 ////////////////////////////////////////////////////////////////////////////////
729 // PlaylistBox::Item protected slots
730 ////////////////////////////////////////////////////////////////////////////////
731 
slotSetName(const QString & name)732 void PlaylistBox::Item::slotSetName(const QString &name)
733 {
734     setText(0, name); // Display name
735     setText(1, sortTextFor(name));
736     setSelected(true);
737 
738     treeWidget()->scrollToItem(this);
739 }
740 
playlistItemDataChanged()741 void PlaylistBox::Item::playlistItemDataChanged()
742 {
743     // This avoids spuriously re-saving all playlists just because play queue
744     // changes.
745     if(m_playlist != listView()->upcomingPlaylist())
746         listView()->slotPlaylistDataChanged();
747 }
748 
749 ////////////////////////////////////////////////////////////////////////////////
750 // PlaylistBox::Item private methods
751 ////////////////////////////////////////////////////////////////////////////////
752 
init()753 void PlaylistBox::Item::init()
754 {
755     PlaylistBox *list = listView();
756 
757     list->setupItem(this);
758 
759     const QString itemText(text());
760     setIcon(0, QIcon::fromTheme(m_iconName));
761     list->addNameToDict(itemText);
762 
763     if(m_playlist) {
764         connect(m_playlist, &Playlist::signalNameChanged,
765                 this,       &Item::slotSetName);
766         connect(m_playlist, &Playlist::signalEnableDirWatch,
767                 this, [list](bool enable) {
768                     list->enableDirWatch(enable);
769                 });
770     }
771 
772     if(m_playlist == CollectionList::instance()) {
773         m_sortedFirst = true;
774         m_collectionItem = this;
775         list->viewMode()->setupDynamicPlaylists();
776     }
777 
778     if(m_playlist == list->historyPlaylist() || m_playlist == list->upcomingPlaylist())
779         m_sortedFirst = true;
780 
781     setText(1, sortTextFor(itemText));
782 
783     connect(&(m_playlist->signaller), &PlaylistInterfaceSignaller::playingItemDataChanged, this, &PlaylistBox::Item::playlistItemDataChanged);
784 }
785 
sortTextFor(const QString & name) const786 QString PlaylistBox::Item::sortTextFor(const QString &name) const
787 {
788     // Collection List goes before everything, then
789     // playlists that 'sort first', then remainder of
790     // playlists.
791     const auto prefix
792         = (playlist() == CollectionList::instance())
793             ? QStringLiteral("0")
794             : m_sortedFirst
795                 ? QStringLiteral("1")
796                 : QStringLiteral("2");
797     return prefix + name;
798 }
799 
800 // vim: set et sw=4 tw=0 sta:
801