diff --git a/collectionlist.cpp b/collectionlist.cpp index 18d9c025..c581641c 100644 --- a/collectionlist.cpp +++ b/collectionlist.cpp @@ -1,599 +1,593 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #include "collectionlist.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlistcollection.h" #include "stringshare.h" #include "cache.h" #include "actioncollection.h" #include "juktag.h" #include "viewmode.h" #include "juk_debug.h" using ActionCollection::action; //////////////////////////////////////////////////////////////////////////////// // static methods //////////////////////////////////////////////////////////////////////////////// CollectionList *CollectionList::m_list = 0; CollectionList *CollectionList::instance() { return m_list; } static QElapsedTimer stopwatch; void CollectionList::startLoadingCachedItems() { if(!m_list) return; qCDebug(JUK_LOG) << "Starting to load cached items"; stopwatch.start(); if(!Cache::instance()->prepareToLoadCachedItems()) { qCCritical(JUK_LOG) << "Unable to setup to load cache... perhaps it doesn't exist?"; completedLoadingCachedItems(); return; } qCDebug(JUK_LOG) << "Kicked off first batch"; QTimer::singleShot(0, this, SLOT(loadNextBatchCachedItems())); } void CollectionList::loadNextBatchCachedItems() { Cache *cache = Cache::instance(); bool done = false; for(int i = 0; i < 20; ++i) { FileHandle cachedItem(cache->loadNextCachedItem()); if(cachedItem.isNull()) { done = true; break; } // This may have already been created via a loaded playlist. if(!m_itemsDict.contains(cachedItem.absFilePath())) { CollectionListItem *newItem = new CollectionListItem(this, cachedItem); setupItem(newItem); } } if(!done) { QTimer::singleShot(0, this, SLOT(loadNextBatchCachedItems())); } else { completedLoadingCachedItems(); } } void CollectionList::completedLoadingCachedItems() { // The CollectionList is created with sorting disabled for speed. Re-enable // it here, and perform the sort. KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); Qt::SortOrder order = Qt::DescendingOrder; if(config.readEntry("CollectionListSortAscending", true)) order = Qt::AscendingOrder; m_list->sortByColumn(config.readEntry("CollectionListSortColumn", 1), order); qCDebug(JUK_LOG) << "Finished loading cached items, took" << stopwatch.elapsed() << "ms"; qCDebug(JUK_LOG) << m_itemsDict.size() << "items are in the CollectionList"; emit cachedItemsLoaded(); } void CollectionList::initialize(PlaylistCollection *collection) { if(m_list) return; // We have to delay initialization here because dynamic_cast or comparing to // the collection instance won't work in the PlaylistBox::Item initialization // won't work until the CollectionList is fully constructed. m_list = new CollectionList(collection); m_list->setName(i18n("Collection List")); collection->setupPlaylist(m_list, "folder-sound"); } //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// CollectionListItem *CollectionList::createItem(const FileHandle &file, QTreeWidgetItem *) { // It's probably possible to optimize the line below away, but, well, right // now it's more important to not load duplicate items. if(m_itemsDict.contains(file.absFilePath())) return nullptr; CollectionListItem *item = new CollectionListItem(this, file); if(!item->isValid()) { qCCritical(JUK_LOG) << "CollectionList::createItem() -- A valid tag was not created for \"" << file.absFilePath() << "\""; delete item; return nullptr; } setupItem(item); return item; } void CollectionList::clearItems(const PlaylistItemList &items) { foreach(PlaylistItem *item, items) { delete item; } playlistItemsChanged(); } void CollectionList::setupTreeViewEntries(ViewMode *viewMode) const { TreeViewMode *treeViewMode = dynamic_cast(viewMode); if(!treeViewMode) { qCWarning(JUK_LOG) << "Can't setup entries on a non-tree-view mode!\n"; return; } QList columnList; columnList << PlaylistItem::ArtistColumn; columnList << PlaylistItem::GenreColumn; columnList << PlaylistItem::AlbumColumn; foreach(int column, columnList) treeViewMode->addItems(m_columnTags[column]->keys(), column); } void CollectionList::slotNewItems(const KFileItemList &items) { QStringList files; for(KFileItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) files.append((*it).url().path()); addFiles(files); update(); } void CollectionList::slotRefreshItems(const QList > &items) { for(int i = 0; i < items.count(); ++i) { const KFileItem fileItem = items[i].second; CollectionListItem *item = lookup(fileItem.url().path()); if(item) { item->refreshFromDisk(); // If the item is no longer on disk, remove it from the collection. if(item->file().fileInfo().exists()) item->repaint(); else delete item; } } update(); } void CollectionList::slotDeleteItems(const KFileItemList &items) { for(const auto &item : items) { delete lookup(item.url().path()); } } void CollectionList::saveItemsToCache() const { qCDebug(JUK_LOG) << "Saving collection list to cache"; QSaveFile f(Cache::fileHandleCacheFileName()); if(!f.open(QIODevice::WriteOnly)) { qCCritical(JUK_LOG) << "Error saving cache:" << f.errorString(); return; } QByteArray data; QDataStream s(&data, QIODevice::WriteOnly); s.setVersion(QDataStream::Qt_4_3); QHash::const_iterator it; for(it = m_itemsDict.begin(); it != m_itemsDict.end(); ++it) { s << it.key(); s << (*it)->file(); } QDataStream fs(&f); qint32 checksum = qChecksum(data.data(), data.size()); fs << qint32(Cache::playlistItemsCacheVersion) << checksum << data; if(!f.commit()) qCCritical(JUK_LOG) << "Error saving cache:" << f.errorString(); } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void CollectionList::clear() { int result = KMessageBox::warningContinueCancel(this, i18n("Removing an item from the collection will also remove it from " "all of your playlists. Are you sure you want to continue?\n\n" "Note, however, that if the directory that these files are in is in " "your \"scan on startup\" list, they will be readded on startup.")); if(result == KMessageBox::Continue) { Playlist::clear(); emit signalCollectionChanged(); } } void CollectionList::slotCheckCache() { PlaylistItemList invalidItems; qCDebug(JUK_LOG) << "Starting to check cached items for consistency"; stopwatch.start(); int i = 0; foreach(CollectionListItem *item, m_itemsDict) { if(!item->checkCurrent()) invalidItems.append(item); if(++i == (m_itemsDict.size() / 2)) qCDebug(JUK_LOG) << "Checkpoint"; } clearItems(invalidItems); qCDebug(JUK_LOG) << "Finished consistency check, took" << stopwatch.elapsed() << "ms"; } void CollectionList::slotRemoveItem(const QString &file) { delete m_itemsDict[file]; } void CollectionList::slotRefreshItem(const QString &file) { if(m_itemsDict[file]) m_itemsDict[file]->refresh(); } //////////////////////////////////////////////////////////////////////////////// // protected methods //////////////////////////////////////////////////////////////////////////////// CollectionList::CollectionList(PlaylistCollection *collection) : Playlist(collection, true), m_columnTags(15, 0) { QAction *spaction = ActionCollection::actions()->addAction("showPlaying"); spaction->setText(i18n("Show Playing")); connect(spaction, SIGNAL(triggered(bool)), SLOT(slotShowPlaying())); connect(action("back")->menu(), SIGNAL(aboutToShow()), this, SLOT(slotPopulateBackMenu())); connect(action("back")->menu(), SIGNAL(triggered(QAction*)), this, SLOT(slotPlayFromBackMenu(QAction*))); setSortingEnabled(false); // Temporarily disable sorting to add items faster. m_columnTags[PlaylistItem::ArtistColumn] = new TagCountDict; m_columnTags[PlaylistItem::AlbumColumn] = new TagCountDict; m_columnTags[PlaylistItem::GenreColumn] = new TagCountDict; // Even set to true it wouldn't work with this class due to other checks setAllowDuplicates(false); } CollectionList::~CollectionList() { KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); config.writeEntry("CollectionListSortColumn", header()->sortIndicatorSection()); config.writeEntry("CollectionListSortAscending", header()->sortIndicatorOrder() == Qt::AscendingOrder); - // In some situations the dataChanged signal from clearItems will cause observers to - // subsequently try to access a deleted item. Since we're going away just remove all - // observers. - - clearObservers(); - // The CollectionListItems will try to remove themselves from the // m_columnTags member, so we must make sure they're gone before we // are. clearItems(items()); qDeleteAll(m_columnTags); m_columnTags.clear(); } void CollectionList::dropEvent(QDropEvent *e) { if(e->source() == this) return; // Don't rearrange in the CollectionList. else Playlist::dropEvent(e); } void CollectionList::dragMoveEvent(QDragMoveEvent *e) { if(e->source() != this) Playlist::dragMoveEvent(e); else e->setAccepted(false); } QString CollectionList::addStringToDict(const QString &value, int column) { if(column > m_columnTags.count() || value.trimmed().isEmpty()) return QString(); if(m_columnTags[column]->contains(value)) ++((*m_columnTags[column])[value]); else { m_columnTags[column]->insert(value, 1); emit signalNewTag(value, column); } return value; } QStringList CollectionList::uniqueSet(UniqueSetType t) const { int column; switch(t) { case Artists: column = PlaylistItem::ArtistColumn; break; case Albums: column = PlaylistItem::AlbumColumn; break; case Genres: column = PlaylistItem::GenreColumn; break; default: return QStringList(); } return m_columnTags[column]->keys(); } CollectionListItem *CollectionList::lookup(const QString &file) const { return m_itemsDict.value(file, nullptr); } void CollectionList::removeStringFromDict(const QString &value, int column) { if(column > m_columnTags.count() || value.trimmed().isEmpty()) return; if(m_columnTags[column]->contains(value) && --((*m_columnTags[column])[value])) // If the decrement goes to 0... { emit signalRemovedTag(value, column); m_columnTags[column]->remove(value); } } void CollectionList::addWatched(const QString &file) { m_dirWatch->addFile(file); } void CollectionList::removeWatched(const QString &file) { m_dirWatch->removeFile(file); } //////////////////////////////////////////////////////////////////////////////// // CollectionListItem public methods //////////////////////////////////////////////////////////////////////////////// void CollectionListItem::refresh() { int offset = CollectionList::instance()->columnOffset(); int columns = lastColumn() + offset + 1; sharedData()->metadata.resize(columns); sharedData()->cachedWidths.resize(columns); for(int i = offset; i < columns; i++) { setText(i, text(i)); int id = i - offset; if(id != TrackNumberColumn && id != LengthColumn) { // All columns other than track num and length need local-encoded data for sorting QString toLower = text(i).toLower(); // For some columns, we may be able to share some strings if((id == ArtistColumn) || (id == AlbumColumn) || (id == GenreColumn) || (id == YearColumn) || (id == CommentColumn)) { toLower = StringShare::tryShare(toLower); if(id != YearColumn && id != CommentColumn && sharedData()->metadata[id] != toLower) { CollectionList::instance()->removeStringFromDict(sharedData()->metadata[id], id); CollectionList::instance()->addStringToDict(text(i), id); } } sharedData()->metadata[id] = toLower; } #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) int newWidth = treeWidget()->fontMetrics().horizontalAdvance(text(i)); #else // .width is deprecated in Qt 5.11 or later int newWidth = treeWidget()->fontMetrics().width(text(i)); #endif if(newWidth != sharedData()->cachedWidths[i]) playlist()->slotWeightDirty(i); sharedData()->cachedWidths[i] = newWidth; } for(PlaylistItemList::Iterator it = m_children.begin(); it != m_children.end(); ++it) { (*it)->playlist()->update(); (*it)->playlist()->playlistItemsChanged(); } if(treeWidget()->isVisible()) treeWidget()->viewport()->update(); CollectionList::instance()->playlistItemsChanged(); emit CollectionList::instance()->signalCollectionChanged(); } PlaylistItem *CollectionListItem::itemForPlaylist(const Playlist *playlist) { if(playlist == CollectionList::instance()) return this; PlaylistItemList::ConstIterator it; for(it = m_children.constBegin(); it != m_children.constEnd(); ++it) if((*it)->playlist() == playlist) return *it; return 0; } void CollectionListItem::updateCollectionDict(const QString &oldPath, const QString &newPath) { CollectionList *collection = CollectionList::instance(); if(!collection) return; collection->removeFromDict(oldPath); collection->addToDict(newPath, this); } void CollectionListItem::repaint() const { // FIXME repaint /*QItemDelegate::repaint(); for(PlaylistItemList::ConstIterator it = m_children.constBegin(); it != m_children.constEnd(); ++it) (*it)->repaint();*/ } //////////////////////////////////////////////////////////////////////////////// // CollectionListItem protected methods //////////////////////////////////////////////////////////////////////////////// CollectionListItem::CollectionListItem(CollectionList *parent, const FileHandle &file) : PlaylistItem(parent), m_shuttingDown(false) { parent->addToDict(file.absFilePath(), this); sharedData()->fileHandle = file; if(file.tag()) { refresh(); parent->playlistItemsChanged(); } else { qCCritical(JUK_LOG) << "CollectionListItem::CollectionListItem() -- Tag() could not be created."; } } CollectionListItem::~CollectionListItem() { m_shuttingDown = true; foreach(PlaylistItem *item, m_children) delete item; CollectionList *l = CollectionList::instance(); if(l) { l->removeFromDict(file().absFilePath()); l->removeStringFromDict(file().tag()->album(), AlbumColumn); l->removeStringFromDict(file().tag()->artist(), ArtistColumn); l->removeStringFromDict(file().tag()->genre(), GenreColumn); } } void CollectionListItem::addChildItem(PlaylistItem *child) { m_children.append(child); } void CollectionListItem::removeChildItem(PlaylistItem *child) { if(!m_shuttingDown) m_children.removeAll(child); } bool CollectionListItem::checkCurrent() { if(!file().fileInfo().exists() || !file().fileInfo().isFile()) return false; if(!file().current()) { file().refresh(); refresh(); } return true; } // vim: set et sw=4 tw=0 sta: diff --git a/dynamicplaylist.cpp b/dynamicplaylist.cpp index 15df8cd6..a99504be 100644 --- a/dynamicplaylist.cpp +++ b/dynamicplaylist.cpp @@ -1,182 +1,171 @@ /** * Copyright (C) 2003-2004 Scott Wheeler * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #include "dynamicplaylist.h" #include "collectionlist.h" #include "playlistcollection.h" #include "tracksequencemanager.h" #include +#include -class PlaylistDirtyObserver : public PlaylistObserver +PlaylistDirtyObserver::PlaylistDirtyObserver(DynamicPlaylist* parent, Playlist* playlist) : + m_parent(parent) { -public: - PlaylistDirtyObserver(DynamicPlaylist *parent, Playlist *playlist) : - PlaylistObserver(playlist), - m_parent(parent) - { - - } - - virtual void playlistItemDataHasChanged() Q_DECL_FINAL override - { - m_parent->slotSetDirty(); - } + QObject::connect(&(playlist->signaller), &PlaylistInterfaceSignaller::playingItemDataChanged, m_parent, &DynamicPlaylist::slotSetDirty); +} -private: - DynamicPlaylist *m_parent; -}; //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// DynamicPlaylist::DynamicPlaylist(const PlaylistList &playlists, PlaylistCollection *collection, const QString &name, const QString &iconName, bool setupPlaylist, bool synchronizePlaying) : Playlist(collection, true), m_playlists(playlists), m_dirty(true), m_synchronizePlaying(synchronizePlaying) { if(setupPlaylist) collection->setupPlaylist(this, iconName); setName(name); setAllowDuplicates(false); setSortingEnabled(false); for(PlaylistList::ConstIterator it = playlists.constBegin(); it != playlists.constEnd(); ++it) m_observers.append(new PlaylistDirtyObserver(this, *it)); connect(CollectionList::instance(), SIGNAL(signalCollectionChanged()), this, SLOT(slotSetDirty())); } DynamicPlaylist::~DynamicPlaylist() { lower(); - foreach(PlaylistObserver *observer, m_observers) + foreach(PlaylistDirtyObserver *observer, m_observers) delete observer; } void DynamicPlaylist::setPlaylists(const PlaylistList &playlists) { m_playlists = playlists; updateItems(); } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void DynamicPlaylist::slotReload() { for(PlaylistList::Iterator it = m_playlists.begin(); it != m_playlists.end(); ++it) (*it)->slotReload(); checkUpdateItems(); } void DynamicPlaylist::lower(QWidget *top) { if(top == this) return; if(playing()) { PlaylistList l; l.append(this); for(PlaylistList::Iterator it = m_playlists.begin(); it != m_playlists.end(); ++it) { (*it)->synchronizePlayingItems(l, true); } } PlaylistItemList list = PlaylistItem::playingItems(); for(PlaylistItemList::Iterator it = list.begin(); it != list.end(); ++it) { if((*it)->playlist() == this) { list.erase(it); break; } } if(!list.isEmpty()) TrackSequenceManager::instance()->setCurrentPlaylist(list.front()->playlist()); } //////////////////////////////////////////////////////////////////////////////// // protected methods //////////////////////////////////////////////////////////////////////////////// PlaylistItemList DynamicPlaylist::items() { checkUpdateItems(); return Playlist::items(); } void DynamicPlaylist::showEvent(QShowEvent *e) { checkUpdateItems(); Playlist::showEvent(e); } void DynamicPlaylist::paintEvent(QPaintEvent *e) { checkUpdateItems(); Playlist::paintEvent(e); } void DynamicPlaylist::updateItems() { PlaylistItemList siblings; for(PlaylistList::ConstIterator it = m_playlists.constBegin(); it != m_playlists.constEnd(); ++it) siblings += (*it)->items(); if(m_siblings != siblings) { m_siblings = siblings; this->synchronizeItemsTo(siblings); if(m_synchronizePlaying) { synchronizePlayingItems(m_playlists, true); } } } bool DynamicPlaylist::synchronizePlaying() const { return m_synchronizePlaying; } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// void DynamicPlaylist::checkUpdateItems() { if(!m_dirty) return; updateItems(); m_dirty = false; } // vim: set et sw=4 tw=0 sta: diff --git a/dynamicplaylist.h b/dynamicplaylist.h index e89508de..4b35c847 100644 --- a/dynamicplaylist.h +++ b/dynamicplaylist.h @@ -1,112 +1,123 @@ /** * Copyright (C) 2003-2004 Scott Wheeler * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #ifndef DYNAMICPLAYLIST_H #define DYNAMICPLAYLIST_H #include "playlist.h" #include +class PlaylistDirtyObserver; + /** * A Playlist that is a union of other playlists that is created dynamically. */ class DynamicPlaylist : public Playlist { Q_OBJECT public: /** * Creates a dynamic playlist based on lists. */ DynamicPlaylist(const PlaylistList &lists, PlaylistCollection *collection, const QString &name = QString(), const QString &iconName = "audio-midi", bool setupPlaylist = true, bool synchronizePlaying = false); virtual ~DynamicPlaylist(); virtual bool canReload() const override { return false; } void setPlaylists(const PlaylistList &playlists); public slots: /** * Reimplemented so that it will reload all of the playlists that are * associated with the dynamic list. */ virtual void slotReload() override; void slotSetDirty() { m_dirty = true; } /** * This is called when lowering the widget from the widget stack so that * it can synchronize the playing item with the one that playlist it was * create from. */ void lower(QWidget *top = 0); protected: /** * Returns true if this list's items need to be updated the next time it's * shown. */ bool dirty() const { return m_dirty; } /** * Return a list of the items in this playlist. For example in a search * list this should return only the matched items. By default it returns * all of the items in the playlists associated with this dynamic list. */ virtual PlaylistItemList items() override; /** * Reimplemented from QWidget. Here it updates the list of items (when * appropriate) as the widget is shown. */ virtual void showEvent(QShowEvent *e) override; virtual void paintEvent(QPaintEvent *e) override; /** * Updates the items (unconditionally). This should be reimplemented in * subclasses to refresh the items in the dynamic list (i.e. running a * search). */ virtual void updateItems(); bool synchronizePlaying() const; private: /** * Checks to see if the current list of items is "dirty" and if so updates * this dynamic playlist's items to be in sync with the lists that it is a * wrapper around. */ void checkUpdateItems(); private: - QVector m_observers; + QVector m_observers; PlaylistItemList m_siblings; PlaylistList m_playlists; bool m_dirty; bool m_synchronizePlaying; }; +class PlaylistDirtyObserver +{ +public: + PlaylistDirtyObserver(DynamicPlaylist *parent, Playlist *playlist); + +private: + DynamicPlaylist *m_parent; +}; + #endif // vim: set et sw=4 tw=0 sta: diff --git a/nowplaying.h b/nowplaying.h index 5725a3f6..5a9519f6 100644 --- a/nowplaying.h +++ b/nowplaying.h @@ -1,143 +1,138 @@ /** * Copyright (C) 2004 Scott Wheeler * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #ifndef NOWPLAYING_H #define NOWPLAYING_H #include #include #include #include #include "filehandle.h" #include "playlistinterface.h" class QTimer; class QPoint; class NowPlayingItem; class PlaylistCollection; class Playlist; /** * This is the widget that holds all of the other items and handles updating them * when the playing item changes. */ class NowPlaying : public QWidget { Q_OBJECT public: NowPlaying(QWidget *parent, PlaylistCollection *collection); void addItem(NowPlayingItem *item); PlaylistCollection *collection() const; public slots: void slotUpdate(const FileHandle &file); void slotReloadCurrentItem(); signals: void nowPlayingHidden(); private: - struct Observer final : public PlaylistObserver + struct Observer final { - Observer(NowPlaying *parent, PlaylistInterface *playlist) : - PlaylistObserver(playlist), - m_parent(parent) {} - virtual void playlistItemDataHasChanged() override + Observer(NowPlaying *parent, PlaylistInterface *playlist) { - m_parent->slotReloadCurrentItem(); + connect(&playlist->signaller, &PlaylistInterfaceSignaller::playingItemDataChanged, parent, &NowPlaying::slotReloadCurrentItem); } - NowPlaying *m_parent; }; - friend struct Observer; Observer m_observer; Observer m_collectionListObserver; PlaylistCollection *m_collection; QList m_items; FileHandle m_file; }; /** * Abstract base for the other NowPlaying items. */ class NowPlayingItem { public: virtual ~NowPlayingItem() {} virtual void update(const FileHandle &file) = 0; NowPlaying *parentManager() const { return m_parent; } protected: NowPlayingItem(NowPlaying *parent) : m_parent(parent) { parent->addItem(this); } private: NowPlaying *m_parent; }; /** * Displays the cover of the currently playing file if available, or hides * itself if not. */ class CoverItem : public QLabel, public NowPlayingItem { public: explicit CoverItem(NowPlaying *parent); virtual void update(const FileHandle &file) override; virtual void mouseReleaseEvent(QMouseEvent *event) override; protected: virtual void dragEnterEvent(QDragEnterEvent *e) override; virtual void dropEvent(QDropEvent *e) override; virtual void mousePressEvent(QMouseEvent *e) override; virtual void mouseMoveEvent(QMouseEvent *e) override; private: FileHandle m_file; bool m_dragging; QPoint m_dragStart; }; /** * Show the text information on the current track and provides links to the * album and artist of the currently playing item. */ class TrackItem : public QWidget, public NowPlayingItem { Q_OBJECT public: explicit TrackItem(NowPlaying *parent); virtual void update(const FileHandle &file) override; private slots: void slotOpenLink(const QString &link); void slotUpdate(); void slotClearShowMore(); private: FileHandle m_file; QLabel *m_label; }; #endif // vim: set et sw=4 tw=0 sta: diff --git a/playlist.cpp b/playlist.cpp index 0bfb61c2..011e6fc9 100644 --- a/playlist.cpp +++ b/playlist.cpp @@ -1,2082 +1,2076 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2008-2018 Michael Pyne * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #include "playlist.h" #include "juk-exception.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "directoryloader.h" #include "playlistitem.h" #include "playlistcollection.h" #include "playlistsearch.h" #include "playlistsharedsettings.h" #include "mediafiles.h" #include "collectionlist.h" #include "filerenamer.h" #include "actioncollection.h" #include "tracksequencemanager.h" #include "juktag.h" #include "upcomingplaylist.h" #include "deletedialog.h" #include "webimagefetcher.h" #include "coverinfo.h" #include "coverdialog.h" #include "tagtransactionmanager.h" #include "cache.h" #include "juk_debug.h" using namespace ActionCollection; /** * Used to give every track added in the program a unique identifier. See * PlaylistItem */ quint32 g_trackID = 0; /** * Just a shortcut of sorts. */ static bool manualResize() { return action("resizeColumnsManually")->isChecked(); } //////////////////////////////////////////////////////////////////////////////// // static members //////////////////////////////////////////////////////////////////////////////// bool Playlist::m_visibleChanged = false; bool Playlist::m_shuttingDown = false; PlaylistItemList Playlist::m_history; QVector Playlist::m_backMenuItems; int Playlist::m_leftColumn = 0; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// Playlist::Playlist( bool delaySetup, const QString &name, PlaylistCollection *collection, const QString &iconName, int extraCols) : QTreeWidget(collection->playlistStack()) , m_collection(collection) , m_playlistName(name) , m_fetcher(new WebImageFetcher(this)) { // Any added columns must precede normal ones, which are normally added // in setup() for(int i = 0; i < extraCols; ++i) { addColumn(i18n("JuK")); // Placeholder text } setup(); // Some subclasses need to do even more handling but will remember to // call setupPlaylist if(!delaySetup) { collection->setupPlaylist(this, iconName); } } Playlist::Playlist(PlaylistCollection *collection, const QString &name, const QString &iconName) : Playlist(false, name, collection, iconName, 0) { } Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const QString &name, const QString &iconName) : Playlist(false, name, collection, iconName, 0) { createItems(items); } Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, const QString &iconName) : Playlist(true, QString(), collection, iconName, 0) { m_fileName = playlistFile.canonicalFilePath(); loadFile(m_fileName, playlistFile); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns) : Playlist(delaySetup, QString(), collection, QStringLiteral("audio-midi"), extraColumns) { } Playlist::~Playlist() { - // In some situations the dataChanged signal from clearItems will cause observers to - // subsequently try to access a deleted item. Since we're going away just remove all - // observers. - - clearObservers(); - // clearItem() will take care of removing the items from the history, // so call clearItems() to make sure it happens. clearItems(items()); if(!m_shuttingDown) m_collection->removePlaylist(this); } QString Playlist::name() const { if(m_playlistName.isEmpty()) return m_fileName.section(QDir::separator(), -1).section('.', 0, -2); else return m_playlistName; } FileHandle Playlist::currentFile() const { return playingItem() ? playingItem()->file() : FileHandle(); } void Playlist::playFirst() { TrackSequenceManager::instance()->setNextItem(static_cast( *QTreeWidgetItemIterator(const_cast(this), QTreeWidgetItemIterator::NotHidden))); action("forward")->trigger(); } void Playlist::playNextAlbum() { PlaylistItem *current = TrackSequenceManager::instance()->currentItem(); if(!current) return; // No next album if we're not already playing. QString currentAlbum = current->file().tag()->album(); current = TrackSequenceManager::instance()->nextItem(); while(current && current->file().tag()->album() == currentAlbum) current = TrackSequenceManager::instance()->nextItem(); TrackSequenceManager::instance()->setNextItem(current); action("forward")->trigger(); } void Playlist::playNext() { TrackSequenceManager::instance()->setCurrentPlaylist(this); setPlaying(TrackSequenceManager::instance()->nextItem()); } void Playlist::stop() { m_history.clear(); setPlaying(nullptr); } void Playlist::playPrevious() { if(!playingItem()) return; bool random = action("randomPlay") && action("randomPlay")->isChecked(); PlaylistItem *previous = nullptr; if(random && !m_history.isEmpty()) { PlaylistItemList::Iterator last = m_history.end() - 1; previous = *last; m_history.erase(last); } else { m_history.clear(); previous = TrackSequenceManager::instance()->previousItem(); } if(!previous) previous = static_cast(playingItem()->itemAbove()); setPlaying(previous, false); } void Playlist::setName(const QString &n) { m_collection->addNameToDict(n); m_collection->removeNameFromDict(m_playlistName); m_playlistName = n; emit signalNameChanged(m_playlistName); } void Playlist::save() { if(m_fileName.isEmpty()) return saveAs(); QFile file(m_fileName); if(!file.open(QIODevice::WriteOnly)) return KMessageBox::error(this, i18n("Could not save to file %1.", m_fileName)); QTextStream stream(&file); QStringList fileList = files(); foreach(const QString &file, fileList) stream << file << '\n'; file.close(); } void Playlist::saveAs() { m_collection->removeFileFromDict(m_fileName); m_fileName = MediaFiles::savePlaylistDialog(name(), this); if(!m_fileName.isEmpty()) { m_collection->addFileToDict(m_fileName); // If there's no playlist name set, use the file name. if(m_playlistName.isEmpty()) emit signalNameChanged(name()); save(); } } void Playlist::updateDeletedItem(PlaylistItem *item) { m_members.remove(item->file().absFilePath()); m_search.clearItem(item); m_history.removeAll(item); } void Playlist::clearItem(PlaylistItem *item) { // Automatically updates internal structs via updateDeletedItem delete item; playlistItemsChanged(); } void Playlist::clearItems(const PlaylistItemList &items) { foreach(PlaylistItem *item, items) delete item; playlistItemsChanged(); } PlaylistItem *Playlist::playingItem() // static { return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front(); } QStringList Playlist::files() const { QStringList list; for(QTreeWidgetItemIterator it(const_cast(this)); *it; ++it) list.append(static_cast(*it)->file().absFilePath()); return list; } PlaylistItemList Playlist::items() { return items(QTreeWidgetItemIterator::IteratorFlag(0)); } PlaylistItemList Playlist::visibleItems() { return items(QTreeWidgetItemIterator::NotHidden); } PlaylistItemList Playlist::selectedItems() { return items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); } PlaylistItem *Playlist::firstChild() const { return static_cast(topLevelItem(0)); } void Playlist::updateLeftColumn() { int newLeftColumn = leftMostVisibleColumn(); if(m_leftColumn != newLeftColumn) { updatePlaying(); m_leftColumn = newLeftColumn; } } void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static { m_visibleChanged = true; foreach(PlaylistItem *playlistItem, items) playlistItem->setHidden(!visible); } void Playlist::setSearch(const PlaylistSearch &s) { m_search = s; if(!m_searchEnabled) return; setItemsVisible(s.matchedItems(), true); setItemsVisible(s.unmatchedItems(), false); TrackSequenceManager::instance()->iterator()->playlistChanged(); } void Playlist::setSearchEnabled(bool enabled) { if(m_searchEnabled == enabled) return; m_searchEnabled = enabled; if(enabled) { setItemsVisible(m_search.matchedItems(), true); setItemsVisible(m_search.unmatchedItems(), false); } else setItemsVisible(items(), true); } // Mostly seems to be for DynamicPlaylist // TODO: See if this can't all be eliminated by making 'is-playing' a predicate // of the playlist item itself void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster) { foreach(const Playlist *p, sources) { if(p->playing()) { CollectionListItem *base = playingItem()->collectionItem(); for(QTreeWidgetItemIterator itemIt(this); *itemIt; ++itemIt) { PlaylistItem *item = static_cast(*itemIt); if(base == item->collectionItem()) { item->setPlaying(true, setMaster); PlaylistItemList playing = PlaylistItem::playingItems(); TrackSequenceManager::instance()->setCurrent(item); return; } } return; } } } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void Playlist::copy() { PlaylistItemList items = selectedItems(); QList urls; foreach(PlaylistItem *item, items) { urls << QUrl::fromLocalFile(item->file().absFilePath()); } QMimeData *mimeData = new QMimeData; mimeData->setUrls(urls); QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void Playlist::paste() { addFilesFromMimeData( QApplication::clipboard()->mimeData(), static_cast(currentItem())); } void Playlist::clear() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = items(); clearItems(l); } void Playlist::slotRefresh() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = visibleItems(); QApplication::setOverrideCursor(Qt::WaitCursor); foreach(PlaylistItem *item, l) { item->refreshFromDisk(); if(!item->file().tag() || !item->file().fileInfo().exists()) { qCDebug(JUK_LOG) << "Error while trying to refresh the tag. " << "This file has probably been removed."; delete item->collectionItem(); } processEvents(); } QApplication::restoreOverrideCursor(); } void Playlist::slotRenameFile() { FileRenamer renamer; PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; emit signalEnableDirWatch(false); m_blockDataChanged = true; renamer.rename(items); m_blockDataChanged = false; playlistItemsChanged(); emit signalEnableDirWatch(true); } void Playlist::slotViewCover() { const PlaylistItemList items = selectedItems(); if (items.isEmpty()) return; foreach(const PlaylistItem *item, items) item->file().coverInfo()->popup(); } void Playlist::slotRemoveCover() { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; int button = KMessageBox::warningContinueCancel(this, i18n("Are you sure you want to delete these covers?"), QString(), KGuiItem(i18n("&Delete Covers"))); if(button == KMessageBox::Continue) refreshAlbums(items); } void Playlist::slotShowCoverManager() { static CoverDialog *managerDialog = 0; if(!managerDialog) managerDialog = new CoverDialog(this); managerDialog->show(); } void Playlist::slotAddCover(bool retrieveLocal) { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; if(!retrieveLocal) { m_fetcher->setFile((*items.begin())->file()); m_fetcher->searchCover(); return; } QUrl file = QFileDialog::getOpenFileUrl( this, i18n("Select Cover Image File"), QUrl::fromLocalFile(QDir::home().path()), i18n("Images (*.png *.jpg)"), nullptr, {}, QStringList() << QStringLiteral("file") ); if(file.isEmpty()) return; QString artist = items.front()->file().tag()->artist(); QString album = items.front()->file().tag()->album(); coverKey newId = CoverManager::addCover(file, artist, album); if(newId != CoverManager::NoMatch) refreshAlbums(items, newId); } // Called when image fetcher has added a new cover. void Playlist::slotCoverChanged(int coverId) { qCDebug(JUK_LOG) << "Refreshing information for newly changed covers.\n"; refreshAlbums(selectedItems(), coverId); } void Playlist::slotGuessTagInfo(TagGuesser::Type type) { QApplication::setOverrideCursor(Qt::WaitCursor); const PlaylistItemList items = selectedItems(); setDynamicListsFrozen(true); m_blockDataChanged = true; foreach(PlaylistItem *item, items) { item->guessTagInfo(type); processEvents(); } // MusicBrainz queries automatically commit at this point. What would // be nice is having a signal emitted when the last query is completed. if(type == TagGuesser::FileName) TagTransactionManager::instance()->commit(); m_blockDataChanged = false; playlistItemsChanged(); setDynamicListsFrozen(false); QApplication::restoreOverrideCursor(); } void Playlist::slotReload() { QFileInfo fileInfo(m_fileName); if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable()) return; clearItems(items()); loadFile(m_fileName, fileInfo); } void Playlist::slotWeightDirty(int column) { if(column < 0) { m_weightDirty.clear(); for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) m_weightDirty.append(i); } return; } if(!m_weightDirty.contains(column)) m_weightDirty.append(column); } void Playlist::slotShowPlaying() { if(!playingItem()) return; Playlist *l = playingItem()->playlist(); l->clearSelection(); // Raise the playlist before selecting the items otherwise the tag editor // will not update when it gets the selectionChanged() notification // because it will think the user is choosing a different playlist but not // selecting a different item. m_collection->raise(l); l->setCurrentItem(playingItem()); l->scrollToItem(playingItem()); } void Playlist::slotColumnResizeModeChanged() { if(manualResize()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } if(!manualResize()) slotUpdateColumnWidths(); SharedSettings::instance()->sync(); } void Playlist::playlistItemsChanged() { if(m_blockDataChanged) return; PlaylistInterface::playlistItemsChanged(); } //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// void Playlist::removeFromDisk(const PlaylistItemList &items) { if(isVisible() && !items.isEmpty()) { QStringList files; foreach(const PlaylistItem *item, items) files.append(item->file().absFilePath()); DeleteDialog dialog(this); m_blockDataChanged = true; if(dialog.confirmDeleteList(files)) { bool shouldDelete = dialog.shouldDelete(); QStringList errorFiles; foreach(PlaylistItem *item, items) { if(playingItem() == item) action("forward")->trigger(); QString removePath = item->file().absFilePath(); QUrl removeUrl = QUrl::fromLocalFile(removePath); if((!shouldDelete && KIO::trash(removeUrl)->exec()) || (shouldDelete && QFile::remove(removePath))) { delete item->collectionItem(); } else errorFiles.append(item->file().absFilePath()); } if(!errorFiles.isEmpty()) { QString errorMsg = shouldDelete ? i18n("Could not delete these files") : i18n("Could not move these files to the Trash"); KMessageBox::errorList(this, errorMsg, errorFiles); } } m_blockDataChanged = false; playlistItemsChanged(); } } void Playlist::synchronizeItemsTo(const PlaylistItemList &itemList) { // direct call to ::items to avoid infinite loop, bug 402355 clearItems(Playlist::items()); createItems(itemList); } void Playlist::dragEnterEvent(QDragEnterEvent *e) { if(CoverDrag::isCover(e->mimeData())) { setDropIndicatorShown(false); e->accept(); return; } if(e->mimeData()->hasUrls() && !e->mimeData()->urls().isEmpty()) { setDropIndicatorShown(true); e->acceptProposedAction(); } else e->ignore(); } void Playlist::addFilesFromMimeData(const QMimeData *urls, PlaylistItem *after) { if(!urls->hasUrls()) { return; } addFiles(QUrl::toStringList(urls->urls(), QUrl::PreferLocalFile), after); } bool Playlist::eventFilter(QObject *watched, QEvent *e) { if(watched == header()) { switch(e->type()) { case QEvent::MouseMove: { if((static_cast(e)->modifiers() & Qt::LeftButton) == Qt::LeftButton && !action("resizeColumnsManually")->isChecked()) { m_columnWidthModeChanged = true; action("resizeColumnsManually")->setChecked(true); slotColumnResizeModeChanged(); } break; } case QEvent::MouseButtonPress: { if(static_cast(e)->button() == Qt::RightButton) m_headerMenu->popup(QCursor::pos()); break; } case QEvent::MouseButtonRelease: { if(m_columnWidthModeChanged) { m_columnWidthModeChanged = false; notifyUserColumnWidthModeChanged(); } if(!manualResize() && m_widthsDirty) QTimer::singleShot(0, this, SLOT(slotUpdateColumnWidths())); break; } default: break; } } return QTreeWidget::eventFilter(watched, e); } void Playlist::keyPressEvent(QKeyEvent *event) { if(event->key() == Qt::Key_Up) { const auto topItem = topLevelItem(0); if(topItem && topItem == currentItem()) { QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden); if(topItem == *visible) { emit signalMoveFocusAway(); event->accept(); } } } QTreeWidget::keyPressEvent(event); } QStringList Playlist::mimeTypes() const { return QStringList("text/uri-list"); } QMimeData* Playlist::mimeData(const QList items) const { QList urls; foreach(QTreeWidgetItem *item, items) { urls << QUrl::fromLocalFile(static_cast(item)->file().absFilePath()); } QMimeData *urlDrag = new QMimeData(); urlDrag->setUrls(urls); return urlDrag; } bool Playlist::dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action) { // TODO: Re-add DND Q_UNUSED(parent); Q_UNUSED(index); Q_UNUSED(data); Q_UNUSED(action); return false; } void Playlist::dropEvent(QDropEvent *e) { QPoint vp = e->pos(); PlaylistItem *item = static_cast(itemAt(vp)); // First see if we're dropping a cover, if so we can get it out of the // way early. if(item && CoverDrag::isCover(e->mimeData())) { coverKey id = CoverDrag::idFromData(e->mimeData()); // If the item we dropped on is selected, apply cover to all selected // items, otherwise just apply to the dropped item. if(item->isSelected()) { const PlaylistItemList selItems = selectedItems(); foreach(PlaylistItem *playlistItem, selItems) { playlistItem->file().coverInfo()->setCoverId(id); playlistItem->refresh(); } } else { item->file().coverInfo()->setCoverId(id); item->refresh(); } return; } // When dropping on the toUpper half of an item, insert before this item. // This is what the user expects, and also allows the insertion at // top of the list QRect rect = visualItemRect(item); if(!item) item = static_cast(topLevelItem(topLevelItemCount() - 1)); else if(vp.y() < rect.y() + rect.height() / 2) item = static_cast(item->itemAbove()); m_blockDataChanged = true; if(e->source() == this) { // Since we're trying to arrange things manually, turn off sorting. sortItems(columnCount() + 1, Qt::AscendingOrder); const QList items = QTreeWidget::selectedItems(); foreach(QTreeWidgetItem *listViewItem, items) { if(!item) { // Insert the item at the top of the list. This is a bit ugly, // but I don't see another way. takeItem(listViewItem); insertItem(listViewItem); } //else // listViewItem->moveItem(item); item = static_cast(listViewItem); } } else addFilesFromMimeData(e->mimeData(), item); m_blockDataChanged = false; playlistItemsChanged(); emit signalPlaylistItemsDropped(this); QTreeWidget::dropEvent(e); } void Playlist::showEvent(QShowEvent *e) { if(m_applySharedSettings) { SharedSettings::instance()->apply(this); m_applySharedSettings = false; } QTreeWidget::showEvent(e); } void Playlist::applySharedSettings() { m_applySharedSettings = true; } void Playlist::read(QDataStream &s) { s >> m_playlistName >> m_fileName; // m_fileName is probably empty. if(m_playlistName.isEmpty()) throw BICStreamException(); // Do not sort. Add the files in the order they were saved. setSortingEnabled(false); QStringList files; s >> files; QTreeWidgetItem *after = 0; m_blockDataChanged = true; foreach(const QString &file, files) { if(file.isEmpty()) throw BICStreamException(); after = createItem(FileHandle(file), after); } m_blockDataChanged = false; playlistItemsChanged(); m_collection->setupPlaylist(this, "audio-midi"); } void Playlist::paintEvent(QPaintEvent *pe) { // If there are columns that need to be updated, well, update them. if(!m_weightDirty.isEmpty() && !manualResize()) { calculateColumnWeights(); slotUpdateColumnWidths(); } QTreeWidget::paintEvent(pe); } void Playlist::resizeEvent(QResizeEvent *re) { // If the width of the view has changed, manually update the column // widths. if(re->size().width() != re->oldSize().width() && !manualResize()) slotUpdateColumnWidths(); QTreeWidget::resizeEvent(re); } // Reimplemented to show a visual indication of which of the view's playlist // items is actually playing. void Playlist::drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { PlaylistItem *item = static_cast(itemFromIndex(index)); if(Q_LIKELY(!PlaylistItem::playingItems().contains(item))) { return QTreeWidget::drawRow(p, option, index); } // Seems that the view draws the background now so we have to do this // manually p->fillRect(option.rect, QPalette{}.midlight()); QStyleOptionViewItem newOption {option}; newOption.font.setBold(true); QTreeWidget::drawRow(p, newOption, index); } void Playlist::insertItem(QTreeWidgetItem *item) { QTreeWidget::insertTopLevelItem(0, item); } void Playlist::takeItem(QTreeWidgetItem *item) { int index = indexOfTopLevelItem(item); QTreeWidget::takeTopLevelItem(index); } void Playlist::addColumn(const QString &label, int) { m_columns.append(label); setHeaderLabels(m_columns); } PlaylistItem *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after) { return createItem(file, after); } void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after) { createItems(siblings, after); } void Playlist::addFiles(const QStringList &files, PlaylistItem *after) { if(Q_UNLIKELY(files.isEmpty())) { return; } m_blockDataChanged = true; setEnabled(false); QVector> pendingFutures; for(const auto &file : files) { // some files added here will launch threads that we must wait until // they're done to cleanup auto pendingResult = addUntypedFile(file, after); if(!pendingResult.isFinished()) { pendingFutures.push_back(pendingResult); ++m_itemsLoading; } } // It's possible for no async threads to be launched, and also possible // for this function to be called while there were other threads in flight if(pendingFutures.isEmpty() && m_itemsLoading == 0) { cleanupAfterAllFileLoadsCompleted(); return; } // Build handlers for all the still-active loaders on the heap and then // return to the event loop. for(const auto &future : qAsConst(pendingFutures)) { auto loadWatcher = new QFutureWatcher(this); loadWatcher->setFuture(future); connect(loadWatcher, &QFutureWatcher::finished, this, [=]() { if(--m_itemsLoading == 0) { cleanupAfterAllFileLoadsCompleted(); } loadWatcher->deleteLater(); }); } } void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id) { QList< QPair > albums; bool setAlbumCovers = items.count() == 1; foreach(const PlaylistItem *item, items) { QString artist = item->file().tag()->artist(); QString album = item->file().tag()->album(); if(!albums.contains(qMakePair(artist, album))) albums.append(qMakePair(artist, album)); item->file().coverInfo()->setCoverId(id); if(setAlbumCovers) item->file().coverInfo()->applyCoverToWholeAlbum(true); } for(QList< QPair >::ConstIterator it = albums.constBegin(); it != albums.constEnd(); ++it) { refreshAlbum((*it).first, (*it).second); } } void Playlist::updatePlaying() const { foreach(const PlaylistItem *item, PlaylistItem::playingItems()) item->treeWidget()->viewport()->update(); } void Playlist::refreshAlbum(const QString &artist, const QString &album) { ColumnList columns; columns.append(PlaylistItem::ArtistColumn); PlaylistSearch::Component artistComponent(artist, false, columns, PlaylistSearch::Component::Exact); columns.clear(); columns.append(PlaylistItem::AlbumColumn); PlaylistSearch::Component albumComponent(album, false, columns, PlaylistSearch::Component::Exact); PlaylistSearch::ComponentList components; components.append(artist); components.append(album); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components); const PlaylistItemList matches = search.matchedItems(); foreach(PlaylistItem *item, matches) item->refresh(); } void Playlist::hideColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(false); break; } } if(isColumnHidden(c)) return; QTreeWidget::hideColumn(c); if(c == m_leftColumn) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->hideColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::showColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(true); break; } } if(!isColumnHidden(c)) return; QTreeWidget::showColumn(c); if(c == leftMostVisibleColumn()) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->showColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::sortByColumn(int column, Qt::SortOrder order) { setSortingEnabled(true); QTreeWidget::sortByColumn(column, order); } void Playlist::slotInitialize() { addColumn(i18n("Track Name")); addColumn(i18n("Artist")); addColumn(i18n("Album")); addColumn(i18n("Cover")); addColumn(i18nc("cd track number", "Track")); addColumn(i18n("Genre")); addColumn(i18n("Year")); addColumn(i18n("Length")); addColumn(i18n("Bitrate")); addColumn(i18n("Comment")); addColumn(i18n("File Name")); addColumn(i18n("File Name (full path)")); setAllColumnsShowFocus(true); setSelectionMode(QTreeWidget::ExtendedSelection); header()->setSortIndicatorShown(true); m_columnFixedWidths.resize(columnCount()); ////////////////////////////////////////////////// // setup header RMB menu ////////////////////////////////////////////////// QAction *showAction; const auto sharedSettings = SharedSettings::instance(); for(int i = 0; i < header()->count(); ++i) { if(i - columnOffset() == PlaylistItem::FileNameColumn) m_headerMenu->addSeparator(); showAction = new QAction(headerItem()->text(i), m_headerMenu); showAction->setData(i); showAction->setCheckable(true); showAction->setChecked(sharedSettings->isColumnVisible(i)); m_headerMenu->addAction(showAction); resizeColumnToContents(i); } connect(m_headerMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotToggleColumnVisible(QAction*))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowRMBMenu(QPoint))); connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotPlayCurrent())); // Disabled for now because adding new items (File->Open) causes Qt to send // an itemChanged signal for unrelated playlist items which can cause the // inline editor done slot to mistakenly overwrite tags associated to // *other* playlist items. I haven't found a way to determine whether the // itemChanged signal is really coming from the inline editor so instead // users will need to use the tag editor. :( // -- mpyne 2018-12-20 //connect(this, &QTreeWidget::itemChanged, // this, &Playlist::slotInlineEditDone); connect(action("resizeColumnsManually"), SIGNAL(triggered()), this, SLOT(slotColumnResizeModeChanged())); if(action("resizeColumnsManually")->isChecked()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragEnabled(true); m_disableColumnWidthUpdates = false; } void Playlist::setupItem(PlaylistItem *item) { item->setTrackId(g_trackID); g_trackID++; if(!m_search.isEmpty()) item->setHidden(!m_search.checkItem(item)); if(topLevelItemCount() <= 2 && !manualResize()) { slotWeightDirty(); slotUpdateColumnWidths(); viewport()->update(); } } void Playlist::setDynamicListsFrozen(bool frozen) { m_collection->setDynamicListsFrozen(frozen); } CollectionListItem *Playlist::collectionListItem(const FileHandle &file) { CollectionListItem *item = CollectionList::instance()->lookup(file.absFilePath()); if(!item) { if(!QFile::exists(file.absFilePath())) { qCCritical(JUK_LOG) << "File" << file.absFilePath() << "does not exist."; return nullptr; } item = CollectionList::instance()->createItem(file); } return item; } //////////////////////////////////////////////////////////////////////////////// // protected slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotPopulateBackMenu() const { if(!playingItem()) return; QMenu *menu = action("back")->menu(); menu->clear(); m_backMenuItems.clear(); m_backMenuItems.reserve(10); int count = 0; PlaylistItemList::ConstIterator it = m_history.constEnd(); QAction *action; while(it != m_history.constBegin() && count < 10) { ++count; --it; action = new QAction((*it)->file().tag()->title(), menu); action->setData(count - 1); menu->addAction(action); m_backMenuItems << *it; } } void Playlist::slotPlayFromBackMenu(QAction *backAction) const { int number = backAction->data().toInt(); if(number >= m_backMenuItems.size()) return; TrackSequenceManager::instance()->setNextItem(m_backMenuItems[number]); action("forward")->trigger(); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void Playlist::setup() { setAlternatingRowColors(true); setRootIsDecorated(false); setContextMenuPolicy(Qt::CustomContextMenu); setUniformRowHeights(true); setEditTriggers(QAbstractItemView::EditKeyPressed); // Don't edit on double-click connect(header(), SIGNAL(sectionMoved(int,int,int)), this, SLOT(slotColumnOrderChanged(int,int,int))); connect(m_fetcher, SIGNAL(signalCoverChanged(int)), this, SLOT(slotCoverChanged(int))); // Prevent list of selected items from changing while internet search is in // progress. connect(this, SIGNAL(itemSelectionChanged()), m_fetcher, SLOT(abortSearch())); sortByColumn(1, Qt::AscendingOrder); // Should this be itemActivated? It is quite annoying when I try it... connect(this, &QTreeWidget::itemDoubleClicked, this, &Playlist::slotPlayCurrent); // Use a timer to soak up the multiple dataChanged signals we're going to get auto updateRequestor = new QTimer(this); updateRequestor->setSingleShot(true); updateRequestor->setInterval(10); connect(model(), &QAbstractItemModel::dataChanged, updateRequestor, static_cast(&QTimer::start)); connect(updateRequestor, &QTimer::timeout, this, &Playlist::slotUpdateTime); // This apparently must be created very early in initialization for other // Playlist code requiring m_headerMenu. m_columnVisibleAction = new KActionMenu(i18n("&Show Columns"), this); ActionCollection::actions()->addAction("showColumns", m_columnVisibleAction); m_headerMenu = m_columnVisibleAction->menu(); header()->installEventFilter(this); // TODO: Determine if other stuff in setup must happen before slotInitialize(). // Explicitly call slotInitialize() so that the columns are added before // SharedSettings::apply() sets the visible and hidden ones. slotInitialize(); } void Playlist::loadFile(const QString &fileName, const QFileInfo &fileInfo) { QFile file(fileName); if(!file.open(QIODevice::ReadOnly)) return; QTextStream stream(&file); // Turn off non-explicit sorting. setSortingEnabled(false); m_disableColumnWidthUpdates = true; m_blockDataChanged = true; PlaylistItem *after = nullptr; while(!stream.atEnd()) { QString itemName = stream.readLine().trimmed(); QFileInfo item(itemName); if(item.isRelative()) item.setFile(QDir::cleanPath(fileInfo.absolutePath() + '/' + itemName)); if(item.exists() && item.isFile() && item.isReadable() && MediaFiles::isMediaFile(item.fileName())) { after = createItem(FileHandle(item), after); } } m_blockDataChanged = false; m_disableColumnWidthUpdates = false; file.close(); playlistItemsChanged(); } void Playlist::setPlaying(PlaylistItem *item, bool addToHistory) { if(playingItem() == item) return; if(playingItem()) { if(addToHistory) { if(playingItem()->playlist() == playingItem()->playlist()->m_collection->upcomingPlaylist()) m_history.append(playingItem()->collectionItem()); else m_history.append(playingItem()); } playingItem()->setPlaying(false); } TrackSequenceManager::instance()->setCurrent(item); // TODO is this replaced by MPRIS2? //kapp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data); if(!item) return; item->setPlaying(true); bool enableBack = !m_history.isEmpty(); action("back")->menu()->setEnabled(enableBack); } bool Playlist::playing() const { return playingItem() && this == playingItem()->playlist(); } int Playlist::leftMostVisibleColumn() const { int i = 0; while(i < PlaylistItem::lastColumn() && isColumnHidden(i)) i++; return i < PlaylistItem::lastColumn() ? i : 0; } PlaylistItemList Playlist::items(QTreeWidgetItemIterator::IteratorFlags flags) { PlaylistItemList list; for(QTreeWidgetItemIterator it(this, flags); *it; ++it) list.append(static_cast(*it)); return list; } void Playlist::calculateColumnWeights() { if(m_disableColumnWidthUpdates) return; const PlaylistItemList l = items(); QVector averageWidth(columnCount()); double itemCount = l.size(); QVector cachedWidth; // Here we're not using a real average, but averaging the squares of the // column widths and then using the square root of that value. This gives // a nice weighting to the longer columns without doing something arbitrary // like adding a fixed amount of padding. foreach(PlaylistItem *item, l) { cachedWidth = item->cachedWidths(); // Extra columns start at 0, but those weights aren't shared with all // items. for(int i = 0; i < columnOffset(); ++i) { averageWidth[i] += std::pow(double(columnWidth(i)), 2.0) / itemCount; } for(int column = columnOffset(); column < columnCount(); ++column) { averageWidth[column] += std::pow(double(cachedWidth[column - columnOffset()]), 2.0) / itemCount; } } if(m_columnWeights.isEmpty()) m_columnWeights.fill(-1, columnCount()); foreach(int column, m_weightDirty) { m_columnWeights[column] = int(std::sqrt(averageWidth[column]) + 0.5); } m_weightDirty.clear(); } void Playlist::addPlaylistFile(const QString &m3uFile) { if (!m_collection->containsPlaylistFile(m3uFile)) { new Playlist(m_collection, QFileInfo(m3uFile)); } } QFuture Playlist::addFilesFromDirectory(const QString &dirPath) { auto loader = new DirectoryLoader(dirPath); connect(loader, &DirectoryLoader::loadedPlaylist, this, [this](const QString &m3uFile) { addPlaylistFile(m3uFile); } ); connect(loader, &DirectoryLoader::loadedFiles, this, [this](const FileHandleList &newFiles) { for(const auto newFile : newFiles) { createItem(newFile); } } ); auto future = QtConcurrent::run(loader, &DirectoryLoader::startLoading); auto loadWatcher = new QFutureWatcher(this); connect(loadWatcher, &QFutureWatcher::finished, this, [=]() { loader->deleteLater(); loadWatcher->deleteLater(); }); return future; } // Returns a future since some codepaths will result in an async operation. QFuture Playlist::addUntypedFile(const QString &file, PlaylistItem *after) { if(hasItem(file) && !m_allowDuplicates) return {}; const QFileInfo fileInfo(file); const QString canonicalPath = fileInfo.canonicalFilePath(); if(fileInfo.isFile() && fileInfo.isReadable() && MediaFiles::isMediaFile(file)) { FileHandle f(fileInfo); f.tag(); createItem(f, after); return {}; } if(MediaFiles::isPlaylistFile(file)) { addPlaylistFile(canonicalPath); return {}; } if(fileInfo.isDir()) { foreach(const QString &directory, m_collection->excludedFolders()) { if(canonicalPath.startsWith(directory)) return {}; // Exclude it } return addFilesFromDirectory(canonicalPath); } return {}; } // Called directly or after a threaded directory load has completed, managed by // m_itemsLoading void Playlist::cleanupAfterAllFileLoadsCompleted() { m_blockDataChanged = false; setEnabled(true); // Even if doing a manual column weights we'll generally start off with // incorrect column sizes so at least figure out a reasonable column size // and let user adjust from there. if(manualResize()) { auto manualResizeAction = action("resizeColumnsManually"); manualResizeAction->toggle(); calculateColumnWeights(); slotUpdateColumnWidths(); manualResizeAction->toggle(); } playlistItemsChanged(); } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotUpdateColumnWidths() { if(m_disableColumnWidthUpdates || manualResize()) return; // Make sure that the column weights have been initialized before trying to // update the columns. QList visibleColumns; for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) visibleColumns.append(i); } // convenience handler for deprecated text metrics const auto textWidth = [](const QFontMetrics &fm, const QString &text) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) return fm.horizontalAdvance(text); #else return fm.width(text); #endif }; if(count() == 0) { foreach(int column, visibleColumns) setColumnWidth(column, textWidth(header()->fontMetrics(),headerItem()->text(column)) + 10); return; } if(m_columnWeights.isEmpty()) return; // First build a list of minimum widths based on the strings in the listview // header. We won't let the width of the column go below this width. QVector minimumWidth(columnCount(), 0); int minimumWidthTotal = 0; // Also build a list of either the minimum *or* the fixed width -- whichever is // greater. QVector minimumFixedWidth(columnCount(), 0); int minimumFixedWidthTotal = 0; foreach(int column, visibleColumns) { minimumWidth[column] = textWidth(header()->fontMetrics(), headerItem()->text(column)) + 10; minimumWidthTotal += minimumWidth[column]; minimumFixedWidth[column] = qMax(minimumWidth[column], m_columnFixedWidths[column]); minimumFixedWidthTotal += minimumFixedWidth[column]; } // Make sure that the width won't get any smaller than this. We have to // account for the scrollbar as well. Since this method is called from the // resize event this will set a pretty hard toLower bound on the size. setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width()); // If we've got enough room for the fixed widths (larger than the minimum // widths) then instead use those for our "minimum widths". if(minimumFixedWidthTotal < viewport()->width()) { minimumWidth = minimumFixedWidth; minimumWidthTotal = minimumFixedWidthTotal; } // We've got a list of columns "weights" based on some statistics gathered // about the widths of the items in that column. We need to find the total // useful weight to use as a divisor for each column's weight. double totalWeight = 0; foreach(int column, visibleColumns) totalWeight += m_columnWeights[column]; // Computed a "weighted width" for each visible column. This would be the // width if we didn't have to handle the cases of minimum and maximum widths. QVector weightedWidth(columnCount(), 0); foreach(int column, visibleColumns) weightedWidth[column] = int(double(m_columnWeights[column]) / totalWeight * viewport()->width() + 0.5); // The "extra" width for each column. This is the weighted width less the // minimum width or zero if the minimum width is greater than the weighted // width. QVector extraWidth(columnCount(), 0); // This is used as an indicator if we have any columns where the weighted // width is less than the minimum width. If this is false then we can // just use the weighted width with no problems, otherwise we have to // "readjust" the widths. bool readjust = false; // If we have columns where the weighted width is less than the minimum width // we need to steal that space from somewhere. The amount that we need to // steal is the "neededWidth". int neededWidth = 0; // While we're on the topic of stealing -- we have to have somewhere to steal // from. availableWidth is the sum of the amount of space beyond the minimum // width that each column has been allocated -- the sum of the values of // extraWidth[]. int availableWidth = 0; // Fill in the values discussed above. foreach(int column, visibleColumns) { if(weightedWidth[column] < minimumWidth[column]) { readjust = true; extraWidth[column] = 0; neededWidth += minimumWidth[column] - weightedWidth[column]; } else { extraWidth[column] = weightedWidth[column] - minimumWidth[column]; availableWidth += extraWidth[column]; } } // The adjustmentRatio is the amount of the "extraWidth[]" that columns will // actually be given. double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth); // This will be the sum of the total space that we actually use. Because of // rounding error this won't be the exact available width. int usedWidth = 0; // Now set the actual column widths. If the weighted widths are all greater // than the minimum widths, just use those, otherwise use the "readjusted // weighted width". foreach(int column, visibleColumns) { int width; if(readjust) { int adjustedExtraWidth = int(double(extraWidth[column]) * adjustmentRatio + 0.5); width = minimumWidth[column] + adjustedExtraWidth; } else width = weightedWidth[column]; setColumnWidth(column, width); usedWidth += width; } // Fill the remaining gap for a clean fit into the available space. int remainingWidth = viewport()->width() - usedWidth; setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth); m_widthsDirty = false; } void Playlist::slotAddToUpcoming() { m_collection->setUpcomingPlaylistEnabled(true); m_collection->upcomingPlaylist()->appendItems(selectedItems()); } void Playlist::slotShowRMBMenu(const QPoint &point) { QTreeWidgetItem *item = itemAt(point); int column = columnAt(point.x()); if(!item) return; // Create the RMB menu on demand. if(!m_rmbMenu) { // Probably more of these actions should be ported over to using KActions. m_rmbMenu = new QMenu(this); m_rmbMenu->addAction(QIcon::fromTheme("go-jump-today"), i18n("Add to Play Queue"), this, SLOT(slotAddToUpcoming())); m_rmbMenu->addSeparator(); if(!readOnly()) { m_rmbMenu->addAction( action("edit_cut") ); m_rmbMenu->addAction( action("edit_copy") ); m_rmbMenu->addAction( action("edit_paste") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("removeFromPlaylist") ); } else m_rmbMenu->addAction( action("edit_copy") ); m_rmbEdit = m_rmbMenu->addAction(i18n("Edit")); m_rmbMenu->addAction( action("refresh") ); m_rmbMenu->addAction( action("removeItem") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("guessTag") ); m_rmbMenu->addAction( action("renameFile") ); m_rmbMenu->addAction( action("coverManager") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( QIcon::fromTheme("folder-new"), i18n("Create Playlist From Selected Items..."), this, SLOT(slotCreateGroup())); } // Ignore any columns added by subclasses. const int adjColumn = column - columnOffset(); bool showEdit = (adjColumn == PlaylistItem::TrackColumn) || (adjColumn == PlaylistItem::ArtistColumn) || (adjColumn == PlaylistItem::AlbumColumn) || (adjColumn == PlaylistItem::TrackNumberColumn) || (adjColumn == PlaylistItem::GenreColumn) || (adjColumn == PlaylistItem::YearColumn); if(showEdit) { m_rmbEdit->setText(i18n("Edit '%1'", item->text(column))); m_rmbEdit->disconnect(this); connect(m_rmbEdit, &QAction::triggered, this, [this, item, column]() { this->editItem(item, column); }); } m_rmbEdit->setVisible(showEdit); // Disable edit menu if only one file is selected, and it's read-only FileHandle file = static_cast(item)->file(); m_rmbEdit->setEnabled(file.fileInfo().isWritable() || selectedItems().count() > 1); // View cover is based on if there is a cover to see. We should only have // the remove cover option if the cover is in our database (and not directly // embedded in the file, for instance). action("viewCover")->setEnabled(file.coverInfo()->hasCover()); action("removeCover")->setEnabled(file.coverInfo()->coverId() != CoverManager::NoMatch); m_rmbMenu->popup(mapToGlobal(point)); } bool Playlist::editTag(PlaylistItem *item, const QString &text, int column) { Tag *newTag = TagTransactionManager::duplicateTag(item->file().tag()); switch(column - columnOffset()) { case PlaylistItem::TrackColumn: newTag->setTitle(text); break; case PlaylistItem::ArtistColumn: newTag->setArtist(text); break; case PlaylistItem::AlbumColumn: newTag->setAlbum(text); break; case PlaylistItem::TrackNumberColumn: { bool ok; int value = text.toInt(&ok); if(ok) newTag->setTrack(value); break; } case PlaylistItem::GenreColumn: newTag->setGenre(text); break; case PlaylistItem::YearColumn: { bool ok; int value = text.toInt(&ok); if(ok) newTag->setYear(value); break; } } TagTransactionManager::instance()->changeTagOnItem(item, newTag); return true; } void Playlist::slotInlineEditDone(QTreeWidgetItem *item, int column) { // The column we get is as passed from QTreeWidget so it does not need // adjustment to get the right text from the QTreeWidgetItem QString text = item->text(column); const PlaylistItemList l = selectedItems(); // See if any of the files have a tag different from the input. const int adjColumn = column - columnOffset(); bool changed = std::any_of(l.cbegin(), l.cend(), [text, adjColumn] (const PlaylistItem *item) { return item->text(adjColumn) != text; } ); if(!changed || (l.count() > 1 && KMessageBox::warningContinueCancel( 0, i18n("This will edit multiple files. Are you sure?"), QString(), KGuiItem(i18n("Edit")), KStandardGuiItem::cancel(), "DontWarnMultipleTags") == KMessageBox::Cancel)) { return; } for(auto &item : l) { editTag(item, text, column); } TagTransactionManager::instance()->commit(); CollectionList::instance()->playlistItemsChanged(); playlistItemsChanged(); } void Playlist::slotColumnOrderChanged(int, int from, int to) { if(from == 0 || to == 0) { updatePlaying(); m_leftColumn = header()->sectionPosition(0); } SharedSettings::instance()->setColumnOrder(this); } void Playlist::slotToggleColumnVisible(QAction *action) { int column = action->data().toInt(); if(isColumnHidden(column)) { int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset(); int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset(); if(column == fileNameColumn && !isColumnHidden(fullPathColumn)) { hideColumn(fullPathColumn, false); SharedSettings::instance()->toggleColumnVisible(fullPathColumn); } if(column == fullPathColumn && !isColumnHidden(fileNameColumn)) { hideColumn(fileNameColumn, false); SharedSettings::instance()->toggleColumnVisible(fileNameColumn); } } if(!isColumnHidden(column)) hideColumn(column); else showColumn(column); if(column >= columnOffset()) { SharedSettings::instance()->toggleColumnVisible(column - columnOffset()); } } void Playlist::slotCreateGroup() { QString name = m_collection->playlistNameDialog(i18n("Create New Playlist")); if(!name.isEmpty()) new Playlist(m_collection, selectedItems(), name); } void Playlist::notifyUserColumnWidthModeChanged() { KMessageBox::information(this, i18n("Manual column widths have been enabled. You can " "switch back to automatic column sizes in the view " "menu."), i18n("Manual Column Widths Enabled"), "ShowManualColumnWidthInformation"); } void Playlist::columnResized(int column, int, int newSize) { m_widthsDirty = true; m_columnFixedWidths[column] = newSize; } void Playlist::slotInlineCompletionModeChanged(KCompletion::CompletionMode mode) { SharedSettings::instance()->setInlineCompletionMode(mode); } void Playlist::slotPlayCurrent() { QTreeWidgetItemIterator it(this, QTreeWidgetItemIterator::Selected); PlaylistItem *next = static_cast(*it); TrackSequenceManager::instance()->setNextItem(next); action("forward")->trigger(); } void Playlist::slotUpdateTime() { int newTime = 0; QTreeWidgetItemIterator it(this); while(*it) { const auto item = static_cast(*it); ++it; newTime += item->file().tag()->seconds(); } m_time = newTime; } //////////////////////////////////////////////////////////////////////////////// // helper functions //////////////////////////////////////////////////////////////////////////////// QDataStream &operator<<(QDataStream &s, const Playlist &p) { s << p.name(); s << p.fileName(); s << p.files(); return s; } QDataStream &operator>>(QDataStream &s, Playlist &p) { p.read(s); return s; } bool processEvents() { static QElapsedTimer time; if(time.elapsed() > 100) { time.restart(); qApp->processEvents(); return true; } return false; } // vim: set et sw=4 tw=0 sta: diff --git a/playlist.h b/playlist.h index 68a9ef3f..15ed566c 100644 --- a/playlist.h +++ b/playlist.h @@ -1,755 +1,755 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2007 Michael Pyne * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #ifndef JUK_PLAYLIST_H #define JUK_PLAYLIST_H #include #include #include #include #include #include #include "covermanager.h" #include "stringhash.h" #include "playlistsearch.h" #include "tagguesser.h" #include "playlistinterface.h" #include "filehandle.h" #include "juk_debug.h" class KActionMenu; class QFileInfo; class QMimeData; class QAction; class WebImageFetcher; class PlaylistItem; class PlaylistCollection; class CollectionListItem; typedef QVector PlaylistItemList; class Playlist : public QTreeWidget, public PlaylistInterface { Q_OBJECT public: explicit Playlist(PlaylistCollection *collection, const QString &name = QString(), const QString &iconName = "audio-midi"); Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const QString &name = QString(), const QString &iconName = "audio-midi"); Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, const QString &iconName = "audio-midi"); /** * This constructor should generally only be used either by the cache * restoration methods or by subclasses that want to handle calls to * PlaylistCollection::setupPlaylist() differently. * * @param extraColumns is used to preallocate columns for subclasses that * need them (since extra columns are assumed to start from 0). extraColumns * should be equal to columnOffset() (we can't use columnOffset until the * ctor has run). */ Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns = 0); virtual ~Playlist(); // The following group of functions implement the PlaylistInterface API. virtual QString name() const override; virtual FileHandle currentFile() const override; virtual int count() const override { return model()->rowCount(); } virtual int time() const override { return m_time; } virtual void playNext() override; virtual void playPrevious() override; virtual void stop() override; /** * Plays the top item of the playlist. */ void playFirst(); /** * Plays the next album in the playlist. Only useful when in album random * play mode. */ void playNextAlbum(); /** * Saves the file to the currently set file name. If there is no filename * currently set, the default behavior is to prompt the user for a file * name. */ virtual void save(); /** * Standard "save as". Prompts the user for a location where to save the * playlist to. */ virtual void saveAs(); /** * Removes \a item from the Playlist, but not from the disk. * * Since the GUI updates after an item is cleared, you should use clearItems() if you have * a list of items to remove, as that will remove the whole batch before updating * other components/GUI to the change. */ virtual void clearItem(PlaylistItem *item); /** * Remove \a items from the playlist and emit a signal indicating * that the number of items in the list has changed. */ virtual void clearItems(const PlaylistItemList &items); /** * Accessor function to return a pointer to the currently playing file. * * @return 0 if no file is playing, otherwise a pointer to the PlaylistItem * of the track that is currently playing. */ static PlaylistItem *playingItem(); /** * All of the (media) files in the list. */ QStringList files() const; /** * Returns a list of all of the items in the playlist. */ virtual PlaylistItemList items(); /** * Returns a list of all of the \e visible items in the playlist. */ PlaylistItemList visibleItems(); /** * Returns a list of the currently selected items. */ PlaylistItemList selectedItems(); /** * Returns properly casted first child item in list. */ PlaylistItem *firstChild() const; /** * Allow duplicate files in the playlist. */ void setAllowDuplicates(bool allow) { m_allowDuplicates = allow; } /** * This is being used as a mini-factory of sorts to make the construction * of PlaylistItems virtual. In this case it allows for the creation of * both PlaylistItems and CollectionListItems. */ virtual PlaylistItem *createItem(const FileHandle &file, QTreeWidgetItem *after = nullptr); /** * This is implemented as a template method to allow subclasses to * instantiate their PlaylistItem subclasses using the same method. */ template ItemType *createItem(const FileHandle &file, QTreeWidgetItem *after = nullptr); virtual void createItems(const PlaylistItemList &siblings, PlaylistItem *after = nullptr); /** * This handles adding files of various types -- music, playlist or directory * files. Music files that are found will be added to this playlist. New * playlist files that are found will result in new playlists being created. * * Note that this should not be used in the case of adding *only* playlist * items since it has the overhead of checking to see if the file is a playlist * or directory first. */ virtual void addFiles(const QStringList &files, PlaylistItem *after = nullptr); /** * Returns the file name associated with this playlist (an m3u file) or * an empty QString if no such file exists. */ QString fileName() const { return m_fileName; } /** * Sets the file name to be associated with this playlist; this file should * have the "m3u" extension. */ void setFileName(const QString &n) { m_fileName = n; } /** * Hides column \a c. If \a updateSearch is true then a signal that the * visible columns have changed will be emitted and things like the search * will be updated. */ void hideColumn(int c, bool updateSearch = true); /** * Shows column \a c. If \a updateSearch is true then a signal that the * visible columns have changed will be emitted and things like the search * will be updated. */ void showColumn(int c, bool updateSearch = true); void sortByColumn(int column, Qt::SortOrder order = Qt::AscendingOrder); /** * This sets a name for the playlist that is \e different from the file name. */ void setName(const QString &n); /** * Returns the KActionMenu that allows this to be embedded in menus outside * of the playlist. */ KActionMenu *columnVisibleAction() const { return m_columnVisibleAction; } /** * Set item to be the playing item. If \a item is null then this will clear * the playing indicator. */ static void setPlaying(PlaylistItem *item, bool addToHistory = true); /** * Returns true if this playlist is currently playing. */ bool playing() const override; /** * This forces an update of the left most visible column, but does not save * the settings for this. */ void updateLeftColumn(); /** * Returns the leftmost visible column of the listview. */ int leftColumn() const { return m_leftColumn; } /** * Sets the items in the list to be either visible based on the value of * visible. This is useful for search operations and such. */ static void setItemsVisible(const PlaylistItemList &items, bool visible = true); /** * Returns the search associated with this list, or an empty search if one * has not yet been set. */ PlaylistSearch search() const { return m_search; } /** * Set the search associated with this playlist. */ void setSearch(const PlaylistSearch &s); /** * If the search is disabled then all items will be shown, not just those that * match the current search. */ void setSearchEnabled(bool searchEnabled); /** * Subclasses of Playlist which add new columns will set this value to * specify how many of those columns exist. This allows the Playlist * class to do some internal calculations on the number and positions * of columns. */ virtual int columnOffset() const { return 0; } /** * Some subclasses of Playlist will be "read only" lists (i.e. the history * playlist). This is a way for those subclasses to indicate that to the * Playlist internals. */ virtual bool readOnly() const { return false; } /** * Returns true if it's possible to reload this playlist. */ virtual bool canReload() const { return !m_fileName.isEmpty(); } /** * Returns true if the playlist is a search playlist and the search should be * editable. */ virtual bool searchIsEditable() const { return false; } /** * Synchronizes the playing item in this playlist with the playing item * in \a sources. If \a setMaster is true, this list will become the source * for determining the next item. */ void synchronizePlayingItems(const PlaylistList &sources, bool setMaster); /** * Playlists have a common set of shared settings such as visible columns * that should be applied just before the playlist is shown. Calling this * method applies those. */ void applySharedSettings(); void read(QDataStream &s); static void setShuttingDown() { m_shuttingDown = true; } + void playlistItemsChanged() override; + public slots: /** * Remove the currently selected items from the playlist and disk. */ void slotRemoveSelectedItems() { removeFromDisk(selectedItems()); } /* * The edit slots are required to use the canonical names so that they are * detected by the application wide framework. */ virtual void cut() { copy(); clear(); } /** * Puts a list of URLs pointing to the files in the current selection on the * clipboard. */ virtual void copy(); /** * Checks the clipboard for local URLs to be inserted into this playlist. */ virtual void paste(); /** * Removes the selected items from the list, but not the disk. * * @see clearItem() * @see clearItems() */ virtual void clear(); /** * Refreshes the tags of the selection from disk, or all of the files in the * list if there is no selection. */ virtual void slotRefresh(); void slotGuessTagInfo(TagGuesser::Type type); /** * Renames the selected items' files based on their tags contents. * * @see PlaylistItem::renameFile() */ void slotRenameFile(); /** * Sets the cover of the selected items, pass in true if you want to load from the local system, * false if you want to load from the internet. */ void slotAddCover(bool fromLocal); /** * Shows a large image of the cover */ void slotViewCover(); /** * Removes covers from the selected items */ void slotRemoveCover(); /** * Shows the cover manager GUI dialog */ void slotShowCoverManager(); /** * Reload the playlist contents from the m3u file. */ virtual void slotReload(); /** * Tells the listview that the next time that it paints that the weighted * column widths must be recalculated. If this is called without a column * all visible columns are marked as dirty. */ void slotWeightDirty(int column = -1); void slotShowPlaying(); void slotColumnResizeModeChanged(); - virtual void playlistItemsChanged() override; - protected: /** * Remove \a items from the playlist and disk. This will ignore items that * are not actually in the list. */ void removeFromDisk(const PlaylistItemList &items); /** * Adds and removes items from this Playlist as necessary to ensure that * the same items are present in this Playlist as in @p itemList. * * No ordering guarantees are imposed, just that the playlist will have the * same items as in the given list afterwards. */ void synchronizeItemsTo(const PlaylistItemList &itemList); // the following are all reimplemented from base classes virtual bool eventFilter(QObject *watched, QEvent *e) override; virtual void keyPressEvent(QKeyEvent *e) override; QStringList mimeTypes() const override; QMimeData* mimeData(const QList items) const override; virtual bool dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action) override; virtual void dropEvent(QDropEvent *e) override; virtual void dragEnterEvent(QDragEnterEvent *e) override; virtual void showEvent(QShowEvent *e) override; virtual void paintEvent(QPaintEvent *pe) override; virtual void resizeEvent(QResizeEvent *re) override; virtual void drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override; virtual void insertItem(QTreeWidgetItem *item); virtual void takeItem(QTreeWidgetItem *item); virtual bool hasItem(const QString &file) const { return m_members.contains(file); } virtual void addColumn(const QString &label, int width = -1); /** * Do some final initialization of created items. Notably ensure that they * are shown or hidden based on the contents of the current PlaylistSearch. * * This is called by the PlaylistItem constructor. */ void setupItem(PlaylistItem *item); /** * Forwards the call to the parent to enable or disable automatic deletion * of tree view playlists. Used by CollectionListItem. */ void setDynamicListsFrozen(bool frozen); template ItemType *createItem(SiblingType *sibling, ItemType *after = nullptr); /** * As a template this allows us to use the same code to initialize the items * in subclasses. ItemType should be a PlaylistItem subclass. */ template