diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ab78eef..c9d047c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,198 +1,198 @@ cmake_minimum_required(VERSION 3.1 FATAL_ERROR) # KDE Application Version, managed by KDE release team automatically # https://community.kde.org/Guidelines_and_HOWTOs/Application_Versioning set (KDE_APPLICATIONS_VERSION_MAJOR "19") set (KDE_APPLICATIONS_VERSION_MINOR "03") set (KDE_APPLICATIONS_VERSION_MICRO "70") set (KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") project(juk VERSION ${KDE_APPLICATIONS_VERSION}) set(QT_MIN_VERSION "5.6.0") set(KF5_MIN_VERSION "5.35.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(CMakePushCheckState) include(CheckIncludeFileCXX) include(KDEInstallDirs) include(KDECompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings NO_POLICY_SCOPE) include(FeatureSummary) include(ECMInstallIcons) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) -find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS Gui Svg Network Test Widgets) +find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS Concurrent Gui Svg Network Test Widgets) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS CoreAddons Completion Config Crash GlobalAccel I18n IconThemes DocTools KIO JobWidgets Notifications TextWidgets XmlGui Wallet WidgetsAddons WindowSystem) find_package(Phonon4Qt5 4.6.60 REQUIRED NO_MODULE) find_package(Taglib 1.6 REQUIRED) include_directories(${CMAKE_BINARY_DIR} ${PHONON_INCLUDES}) add_subdirectory( doc ) add_subdirectory( tests ) add_subdirectory( pics ) set_package_properties(FEATURE PROPERTIES DESCRIPTION "A library for developing MusicBrainz enabled tagging applications" URL "http://www.musicbrainz.org/products/tunepimp" TYPE OPTIONAL PURPOSE "Provides MusicBrainz tagging in Juk.") # TODO: tunepimp is fully unsupported, replace this when tunepimp is replaced # with whatever is actually current this decade. set(HAVE_TUNEPIMP 0) ########### next target ############### include_directories( ${TAGLIB_INCLUDES} ) add_definitions(-DQT_STL -DQT_NO_URL_CAST_FROM_STRING) # Look for Ogg Opus support in taglib (not released yet) cmake_push_check_state() set(CMAKE_REQUIRED_INCLUDES ${CMAKE_REQUIRED_INCLUDES} ${TAGLIB_INCLUDES}) check_include_file_cxx(opusfile.h TAGLIB_HAS_OPUSFILE) cmake_pop_check_state() configure_file (config-juk.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-juk.h ) set(tunepimp_SRCS) if(TUNEPIMP_FOUND) set(tunepimp_SRCS trackpickerdialog.cpp) include_directories( ${TUNEPIMP_INCLUDE_DIR} ) endif(TUNEPIMP_FOUND) set(juk_SRCS ${tunepimp_SRCS} advancedsearchdialog.cpp slider.cpp svghandler.cpp volumepopupbutton.cpp actioncollection.cpp cache.cpp categoryreaderinterface.cpp collectionlist.cpp coverdialog.cpp covericonview.cpp coverinfo.cpp covermanager.cpp coverproxy.cpp dbuscollectionproxy.cpp deletedialog.cpp directorylist.cpp directoryloader.cpp dynamicplaylist.cpp exampleoptions.cpp folderplaylist.cpp filehandle.cpp filerenamer.cpp filerenameroptions.cpp filerenamerconfigdlg.cpp webimagefetcher.cpp historyplaylist.cpp juk.cpp keydialog.cpp lyricswidget.cpp main.cpp mediafiles.cpp mpris2/mediaplayer2.cpp mpris2/mediaplayer2player.cpp mpris2/mpris2.cpp musicbrainzquery.cpp nowplaying.cpp playermanager.cpp playlist.cpp playlistbox.cpp playlistcollection.cpp playlistinterface.cpp playlistitem.cpp playlistsearch.cpp playlistsharedsettings.cpp playlistsplitter.cpp scrobbler.cpp scrobbleconfigdlg.cpp searchplaylist.cpp searchwidget.cpp slideraction.cpp statuslabel.cpp stringshare.cpp systemtray.cpp tag.cpp tageditor.cpp tagguesser.cpp tagguesserconfigdlg.cpp tagrenameroptions.cpp tagtransactionmanager.cpp tracksequenceiterator.cpp tracksequencemanager.cpp treeviewitemplaylist.cpp upcomingplaylist.cpp ktrm.cpp viewmode.cpp ) ecm_qt_declare_logging_category(juk_SRCS HEADER juk_debug.h IDENTIFIER JUK_LOG CATEGORY_NAME org.kde.juk) qt5_add_dbus_adaptor( juk_SRCS org.kde.juk.collection.xml dbuscollectionproxy.h DBusCollectionProxy ) qt5_add_dbus_adaptor( juk_SRCS org.kde.juk.player.xml playermanager.h PlayerManager) qt5_add_dbus_adaptor( juk_SRCS org.kde.juk.search.xml searchwidget.h SearchWidget) ki18n_wrap_ui(juk_SRCS filerenamerbase.ui filerenameroptionsbase.ui directorylistbase.ui trackpickerdialogbase.ui tagguesserconfigdlgwidget.ui exampleoptionsbase.ui coverdialogbase.ui deletedialogbase.ui tageditor.ui ) file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/*-apps-juk.png") ecm_add_app_icon(juk_SRCS ICONS ${ICONS_SRCS}) add_executable(juk ${juk_SRCS}) kde_target_enable_exceptions(juk PRIVATE) target_compile_definitions(juk PRIVATE QT_USE_QSTRINGBUILDER) if(NOT MSVC AND NOT ( WIN32 AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel" ) ) set( LIBMATH m ) endif() target_link_libraries(juk ${LIBMATH} Qt5::Gui Qt5::Svg Qt5::Widgets Qt5::Network KF5::ConfigCore KF5::CoreAddons KF5::Completion KF5::Crash KF5::GlobalAccel KF5::KIOCore KF5::KIOWidgets KF5::KIOFileWidgets KF5::JobWidgets KF5::Notifications KF5::I18n KF5::IconThemes KF5::TextWidgets KF5::XmlGui KF5::WindowSystem KF5::WidgetsAddons KF5::Wallet Phonon::phonon4qt5 ${TAGLIB_LIBRARIES}) if(TUNEPIMP_FOUND) target_link_libraries(juk ${TUNEPIMP_LIBRARIES}) endif(TUNEPIMP_FOUND) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) install(TARGETS juk ${INSTALL_TARGETS_DEFAULT_ARGS} ) ########### install files ############### install( PROGRAMS org.kde.juk.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} ) install( FILES juk.notifyrc jukui.rc jukui-rtl.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/juk ) install( FILES org.kde.juk.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) install( FILES jukservicemenu.desktop DESTINATION ${SERVICES_INSTALL_DIR}/ServiceMenus ) install( FILES org.kde.juk.collection.xml org.kde.juk.player.xml org.kde.juk.search.xml DESTINATION ${DBUS_INTERFACES_INSTALL_DIR} ) ecm_install_icons(ICONS 128-apps-juk.png 16-apps-juk.png 32-apps-juk.png 48-apps-juk.png 64-apps-juk.png DESTINATION ${ICON_INSTALL_DIR} THEME hicolor ) diff --git a/playlist.cpp b/playlist.cpp index 52098ff8..42e112d8 100644 --- a/playlist.cpp +++ b/playlist.cpp @@ -1,2149 +1,2169 @@ /** * 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 "tag.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(PlaylistCollection *collection, const QString &name, const QString &iconName) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_playlistName(name), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const QString &name, const QString &iconName) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_playlistName(name), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); collection->setupPlaylist(this, iconName); createItems(items); } Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, const QString &iconName) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_fileName(playlistFile.canonicalFilePath()), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); loadFile(m_fileName, playlistFile); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { for(int i = 0; i < extraColumns; ++i) { addColumn(i18n("JuK")); // Placeholder text! } setup(); if(!delaySetup) collection->setupPlaylist(this, "audio-midi"); } 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()); /* delete m_toolTip; */ 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(); } int Playlist::time() const { // Since this method gets a lot of traffic, let's optimize for such. if(!m_addTime.isEmpty()) { foreach(const PlaylistItem *item, m_addTime) { if(item) m_time += item->file().tag()->seconds(); } m_addTime.clear(); } if(!m_subtractTime.isEmpty()) { foreach(const PlaylistItem *item, m_subtractTime) { if(item) m_time -= item->file().tag()->seconds(); } m_subtractTime.clear(); } return m_time; } 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 = 0; if(random && !m_history.isEmpty()) { PlaylistItemList::Iterator last = --m_history.end(); 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 << endl; 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); m_addTime.removeAll(item); m_subtractTime.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() { decode(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, 0, 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) { const auto &existingItems = items(); if(qAbs(itemList.count() - existingItems.count()) > qMax(itemList.count(), existingItems.count()) / 2) { // Large imbalance in list sizes, just clear all and add without // figuring out the diff also clearItems(existingItems); createItems(itemList); return; } // Determine differences between existing playlist items and patch up QHash oldItems; oldItems.reserve(qMax(existingItems.count(), itemList.count())); for(const auto &item : existingItems) { oldItems.insert(item->collectionItem(), item); } PlaylistItemList newItems; for(const auto &item : itemList) { if(oldItems.remove(item->collectionItem()) == 0) { newItems.append(item->collectionItem()); } } clearItems(PlaylistItemList(oldItems.values())); createItems(newItems); } void Playlist::dragEnterEvent(QDragEnterEvent *e) { if(CoverDrag::isCover(e->mimeData())) { //setDropHighlighter(true); setDropIndicatorShown(false); e->accept(); return; } setDropIndicatorShown(true); if(e->mimeData()->hasUrls() && !e->mimeData()->urls().isEmpty()) e->acceptProposedAction(); else e->ignore(); } bool Playlist::acceptDrag(QDropEvent *e) const { return CoverDrag::isCover(e->mimeData()) || e->mimeData()->hasUrls(); } void Playlist::decode(const QMimeData *s, PlaylistItem *item) { Q_UNUSED(s); Q_UNUSED(item); // TODO Re-add drag-drop } 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) { using ::operator|; QTreeWidgetItemIterator selected(this, QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); if(*selected) { QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden); if(*selected == *visible) QApplication::postEvent(parent(), new FocusUpEvent); } } else if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { slotPlayCurrent(); } 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 decode(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) { // Because we're called from the PlaylistItem ctor, item may not be a // PlaylistItem yet (it would be QListViewItem when being inserted. But, // it will be a PlaylistItem by the time it matters, but be careful if // you need to use the PlaylistItem from here. m_addTime.append(static_cast(item)); QTreeWidget::insertTopLevelItem(0, item); } void Playlist::takeItem(QTreeWidgetItem *item) { // See the warning in Playlist::insertItem. m_subtractTime.append(static_cast(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(!after) after = static_cast(topLevelItem(topLevelItemCount() - 1)); - QApplication::setOverrideCursor(Qt::WaitCursor); - m_blockDataChanged = true; + m_itemsLoading++; + + setEnabled(false); FileHandleList queue; for(const auto &file : files) { + // some files added here will launch threads that will do cleanup (fix + // the cursor, allow data updates etc) when the last thread is done. + // Managed by m_itemsLoading going to 0 which is why we ++ above. addUntypedFile(file, queue, true, &after); } addFileHelper(queue, &after, true); - m_blockDataChanged = false; - - slotWeightDirty(); - playlistItemsChanged(); - - QApplication::restoreOverrideCursor(); + // If no items are being loaded by now then we must have loaded all M3U + // playlists or something, so cleanup immediately since no threads will + // have been launched. + if(--m_itemsLoading == 0) { + cleanupAfterAllFileLoadsCompleted(); + } } 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; 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(!isColumnHidden(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, &QTreeWidget::itemChanged, this, &Playlist::slotInlineEditDone); connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotPlayCurrent())); 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() { 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); // 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); PlaylistItem *after = nullptr; m_disableColumnWidthUpdates = true; m_blockDataChanged = true; 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; PlaylistItemList l = items(); QList::Iterator columnIt; 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)); } } +void Playlist::addFilesFromDirectory(const QString &dirPath) +{ + ++m_itemsLoading; + DirectoryLoader *loader = new DirectoryLoader(dirPath); + + connect(loader, &DirectoryLoader::doneLoading, this, + [this, loader]() { + loader->deleteLater(); + + if(--m_itemsLoading == 0) { + cleanupAfterAllFileLoadsCompleted(); + } + } + ); + + 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); + } + } + ); + + (void) QtConcurrent::run(loader, &DirectoryLoader::startLoading); +} + /** * Super spaghetti function that adds music files, m3u playlist files, or directories * into the playlist as appropriate, but only if the playlist doesn't already contain * the file. * * @p file is the file to add (music, playlist or directory) * @p files is the current batch of FileHandles to add into this playlist * (maintained by addFileHelper) * @p after is a pointer to a PlaylistItem* which itself points to the item to * insert after (maintained by addFileHelper) */ void Playlist::addUntypedFile(const QString &file, FileHandleList &files, bool importPlaylists, PlaylistItem **after) { if(hasItem(file) && !m_allowDuplicates) return; addFileHelper(files, after); const QFileInfo fileInfo(file); const QString canonicalPath = fileInfo.canonicalFilePath(); if(fileInfo.isFile() && fileInfo.isReadable() && MediaFiles::isMediaFile(file)) { FileHandle f(fileInfo); f.tag(); files.append(f); return; } if(importPlaylists && MediaFiles::isPlaylistFile(file)) { addPlaylistFile(canonicalPath); return; } if(fileInfo.isDir()) { foreach(const QString &directory, m_collection->excludedFolders()) { if(canonicalPath.startsWith(directory)) return; // Exclude it } - DirectoryLoader *loader = new DirectoryLoader(canonicalPath); - QThread *loaderThread = new QThread; - - loader->moveToThread(loaderThread); - - connect(loaderThread, &QThread::started, loader, &DirectoryLoader::startLoading); - connect(loader, &DirectoryLoader::doneLoading, loaderThread, &QThread::quit); - connect(loader, &DirectoryLoader::doneLoading, loader, &QObject::deleteLater); - connect(loaderThread, &QThread::finished, loaderThread, &QObject::deleteLater); - - connect(loader, &DirectoryLoader::loadedPlaylist, this, - [this](const QString &m3uFile) { - addPlaylistFile(m3uFile); - } - ); - connect(loader, &DirectoryLoader::loadedFiles, this, - [this](const FileHandleList &newFiles) { - // NOTE: after and files are both invalid by this point since - // this can be called long after our own caller has returned. - - PlaylistItem *after = nullptr; - for(const auto newFile : newFiles) { - after = createItem(newFile, after); - } - } - ); - - loaderThread->start(); + addFilesFromDirectory(canonicalPath); } } void Playlist::addFileHelper(FileHandleList &files, PlaylistItem **after, bool ignoreTimer) { static QTime time = QTime::currentTime(); // Process new items every 10 seconds, when we've loaded 1000 items, or when // it's been requested in the API. if(ignoreTimer || time.elapsed() > 10000 || (files.count() >= 1000 && time.elapsed() > 1000)) { time.restart(); const bool focus = hasFocus(); const bool visible = isVisible() && files.count() > 20; if(visible) m_collection->raiseDistraction(); PlaylistItem *newAfter = *after; foreach(const FileHandle &fileHandle, files) newAfter = createItem(fileHandle, newAfter); *after = newAfter; files.clear(); if(visible) m_collection->lowerDistraction(); if(focus) setFocus(); } } +// Called directly or after a threaded directory load has completed, managed by +// m_itemsLoading +void Playlist::cleanupAfterAllFileLoadsCompleted() +{ + m_blockDataChanged = false; + setEnabled(true); + + slotWeightDirty(); + 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); } if(count() == 0) { foreach(int column, visibleColumns) setColumnWidth(column, header()->fontMetrics().width(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] = header()->fontMetrics().width(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(); } //////////////////////////////////////////////////////////////////////////////// // 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 QTime time = QTime::currentTime(); 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 1dfc433e..82d97fcc 100644 --- a/playlist.h +++ b/playlist.h @@ -1,775 +1,778 @@ /** * 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 "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 PlaylistToolTip; class CollectionListItem; typedef QList 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; virtual FileHandle currentFile() const; virtual int count() const { return model()->rowCount(); } virtual int time() const; virtual void playNext(); virtual void playPrevious(); virtual void stop(); /** * 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 = 0); /** * 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; /** * 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; } 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(); virtual void selectAll() { QTreeView::selectAll(); } /** * 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(); 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); virtual void keyPressEvent(QKeyEvent *e); virtual void decode(const QMimeData *s, PlaylistItem *item = 0); QStringList mimeTypes() const; QMimeData* mimeData(const QList items) const; virtual bool dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action); virtual void dropEvent(QDropEvent *e); virtual void dragEnterEvent(QDragEnterEvent *e); virtual void showEvent(QShowEvent *e); virtual bool acceptDrag(QDropEvent *e) const; virtual void paintEvent(QPaintEvent *pe); virtual void resizeEvent(QResizeEvent *re); virtual void drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; 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 void createItems(const QList &siblings, ItemType *after = nullptr); protected slots: void slotPopulateBackMenu() const; void slotPlayFromBackMenu(QAction *) const; signals: /** * This is connected to the PlaylistBox::Item to let it know when the * playlist's name has changed. */ void signalNameChanged(const QString &name); /** * This signal is emitted just before a playlist item is removed from the * list allowing for any cleanup that needs to happen. Typically this * is used to remove the item from the history and safeguard against * dangling pointers. */ void signalAboutToRemove(PlaylistItem *item); void signalEnableDirWatch(bool enable); void signalPlaylistItemsDropped(Playlist *p); private: void setup(); /** * This function is called to let the user know that JuK has automatically enabled * manual column width adjust mode. */ void notifyUserColumnWidthModeChanged(); /** * Load the playlist from a file. \a fileName should be the absolute path. * \a fileInfo should point to the same file as \a fileName. This is a * little awkward API-wise, but keeps us from throwing away useful * information. */ void loadFile(const QString &fileName, const QFileInfo &fileInfo); /** * Writes \a text to \a item in \a column. This is used by the inline tag * editor. Returns false if the tag update failed. */ bool editTag(PlaylistItem *item, const QString &text, int column); /** * Returns the index of the left most visible column in the playlist. * * \see isColumnHidden() */ int leftMostVisibleColumn() const; /** * This method is used internally to provide the backend to the other item * lists. * * \see items() * \see visibleItems() * \see selectedItems() */ PlaylistItemList items(QTreeWidgetItemIterator::IteratorFlags flags); /** * Build the column "weights" for the weighted width mode. */ void calculateColumnWeights(); void addPlaylistFile(const QString &m3uFile); + void addFilesFromDirectory(const QString &dirPath); void addUntypedFile(const QString &file, FileHandleList &files, bool importPlaylists, PlaylistItem **after); void addFileHelper(FileHandleList &files, PlaylistItem **after, bool ignoreTimer = false); + void cleanupAfterAllFileLoadsCompleted(); void redisplaySearch() { setSearch(m_search); } /** * Sets the cover for items to the cover identified by id. */ void refreshAlbums(const PlaylistItemList &items, coverKey id = CoverManager::NoMatch); void refreshAlbum(const QString &artist, const QString &album); void updatePlaying() const; /** * This function should be called when item is deleted to ensure that any * internal bookkeeping is performed. It is automatically called by * PlaylistItem::~PlaylistItem and by clearItem() and clearItems(). */ void updateDeletedItem(PlaylistItem *item); /** * Used as a helper to implement template<> createItem(). This grabs the * CollectionListItem for file if it exists, otherwise it creates a new one and * returns that. If nullptr is returned then some kind of error occurred, * and you should probably do nothing with the FileHandle you have. */ CollectionListItem *collectionListItem(const FileHandle &file); /** * This class is used internally to store settings that are shared by all * of the playlists, such as column order. It is implemented as a singleton. */ class SharedSettings; private slots: /** * Handle the necessary tasks needed to create and setup the playlist that * don't need to happen in the ctor, such as setting up the columns, * initializing the RMB menu, and setting up signal/slot connections. * * Used to be a subclass of K3ListView::polish() but the timing of the * call is not consistent and therefore lead to crashes. */ void slotInitialize(); void slotUpdateColumnWidths(); void slotAddToUpcoming(); /** * Show the RMB menu. Matches the signature for the signal * QListView::contextMenuRequested(). */ void slotShowRMBMenu(const QPoint &point); /** * This slot is called when the inline tag editor has completed its editing * and starts the process of renaming the values. */ void slotInlineEditDone(QTreeWidgetItem *, int column); /** * The image fetcher will update the cover asynchronously, this internal * slot is called when it happens. */ void slotCoverChanged(int coverId); /** * Moves the column \a from to the position \a to. This matches the signature * for the signal QHeader::indexChange(). */ void slotColumnOrderChanged(int, int from, int to); /** * Toggles a columns visible status. Useful for KActions. * * \see hideColumn() * \see showColumn() */ void slotToggleColumnVisible(QAction *action); /** * Prompts the user to create a new playlist with from the selected items. */ void slotCreateGroup(); /** * This slot is called when the user drags the slider in the listview header * to manually set the size of the column. */ void columnResized(int column, int oldSize, int newSize); /** * The slot is called when the completion mode for the line edit in the * inline tag editor is changed. It saves the settings and through the * magic of the SharedSettings class will apply it to the other playlists as * well. */ void slotInlineCompletionModeChanged(KCompletion::CompletionMode mode); void slotPlayCurrent(); private: friend class PlaylistItem; PlaylistCollection *m_collection; StringHash m_members; WebImageFetcher *m_fetcher; QAction *m_rmbEdit; bool m_allowDuplicates; bool m_applySharedSettings; bool m_columnWidthModeChanged; QList m_weightDirty; bool m_disableColumnWidthUpdates; mutable int m_time; mutable PlaylistItemList m_addTime; mutable PlaylistItemList m_subtractTime; /** * The average minimum widths of columns to be used in balancing calculations. */ QVector m_columnWeights; QVector m_columnFixedWidths; bool m_widthsDirty; static PlaylistItemList m_history; PlaylistSearch m_search; bool m_searchEnabled; /** * Used to store the text for inline editing before it is changed so that * we can know if something actually changed and as such if we need to save * the tag. */ QString m_editText; /** * This is only defined if the playlist name is something other than the * file name. */ QString m_playlistName; QString m_fileName; QStringList m_columns; QMenu *m_rmbMenu; QMenu *m_headerMenu; KActionMenu *m_columnVisibleAction; PlaylistToolTip *m_toolTip; + int m_itemsLoading = 0; /// Count of pending file loads outstanding + bool m_blockDataChanged; + /** * This is used to indicate if the list of visible items has changed (via a * call to setVisibleItems()) while random play is playing. */ static bool m_visibleChanged; static bool m_shuttingDown; static int m_leftColumn; static QVector m_backMenuItems; - - bool m_blockDataChanged; }; typedef QList PlaylistList; bool processEvents(); class FocusUpEvent : public QEvent { public: FocusUpEvent() : QEvent(id) {} Type type() const { return id; } static const Type id = static_cast(QEvent::User + 1); }; QDataStream &operator<<(QDataStream &s, const Playlist &p); QDataStream &operator>>(QDataStream &s, Playlist &p); // template method implementations template ItemType *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after) { CollectionListItem *item = collectionListItem(file); if(item && (!m_members.insert(file.absFilePath()) || m_allowDuplicates)) { auto i = new ItemType(item, this, after); setupItem(i); return i; } else return nullptr; } template ItemType *Playlist::createItem(SiblingType *sibling, ItemType *after) { m_disableColumnWidthUpdates = true; if(!m_members.insert(sibling->file().absFilePath()) || m_allowDuplicates) { after = new ItemType(sibling->collectionItem(), this, after); setupItem(after); } m_disableColumnWidthUpdates = false; return after; } template void Playlist::createItems(const QList &siblings, ItemType *after) { if(siblings.isEmpty()) return; foreach(SiblingType *sibling, siblings) after = createItem(sibling, after); playlistItemsChanged(); slotWeightDirty(); } #endif // vim: set et sw=4 tw=0 sta: diff --git a/playlistbox.cpp b/playlistbox.cpp index cc07fdc2..aee40e89 100644 --- a/playlistbox.cpp +++ b/playlistbox.cpp @@ -1,768 +1,762 @@ /** * 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 "playlistbox.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlist.h" #include "collectionlist.h" #include "dynamicplaylist.h" #include "upcomingplaylist.h" #include "historyplaylist.h" #include "viewmode.h" #include "searchplaylist.h" #include "treeviewitemplaylist.h" #include "actioncollection.h" #include "cache.h" #include "tracksequencemanager.h" #include "tagtransactionmanager.h" #include "playermanager.h" #include "dbuscollectionproxy.h" #include "juk_debug.h" using namespace ActionCollection; //////////////////////////////////////////////////////////////////////////////// // PlaylistBox public methods //////////////////////////////////////////////////////////////////////////////// PlaylistBox::PlaylistBox(PlayerManager *player, QWidget *parent, QStackedWidget *playlistStack) : QTreeWidget(parent), PlaylistCollection(player, playlistStack), m_viewModeIndex(0), m_hasSelection(false), m_doingMultiSelect(false), m_dropItem(0), m_showTimer(0) { readConfig(); setHeaderLabel("Playlists"); setRootIsDecorated(false); setContextMenuPolicy(Qt::CustomContextMenu); setDropIndicatorShown(true); header()->blockSignals(true); header()->hide(); header()->blockSignals(false); sortByColumn(0); viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setSelectionMode(QAbstractItemView::ExtendedSelection); m_contextMenu = new QMenu(this); m_contextMenu->addAction( action("file_new") ); m_contextMenu->addAction( action("renamePlaylist") ); m_contextMenu->addAction( action("editSearch") ); m_contextMenu->addAction( action("duplicatePlaylist") ); m_contextMenu->addAction( action("reloadPlaylist") ); m_contextMenu->addAction( action("deleteItemPlaylist") ); m_contextMenu->addAction( action("file_save") ); m_contextMenu->addAction( action("file_save_as") ); m_contextMenu->addSeparator(); // add the view modes stuff KSelectAction *viewModeAction = new KSelectAction( QIcon::fromTheme(QStringLiteral("view-choose")), i18n("View Modes"), ActionCollection::actions()); ActionCollection::actions()->addAction("viewModeMenu", viewModeAction); ViewMode* viewmode = new ViewMode(this); m_viewModes.append(viewmode); viewModeAction->addAction(QIcon::fromTheme(QStringLiteral("view-list-details")), viewmode->name()); CompactViewMode* compactviewmode = new CompactViewMode(this); m_viewModes.append(compactviewmode); viewModeAction->addAction(QIcon::fromTheme(QStringLiteral("view-list-text")), compactviewmode->name()); // TODO: Fix the broken tree view mode #if 0 TreeViewMode* treeviewmode = new TreeViewMode(this); m_viewModes.append(treeviewmode); viewModeAction->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), treeviewmode->name()); #endif CollectionList::initialize(this); viewModeAction->setCurrentItem(m_viewModeIndex); m_viewModes[m_viewModeIndex]->setShown(true); TrackSequenceManager::instance()->setCurrentPlaylist(CollectionList::instance()); raise(CollectionList::instance()); m_contextMenu->addAction( viewModeAction ); connect(viewModeAction, SIGNAL(triggered(int)), this, SLOT(slotSetViewMode(int))); connect(this, SIGNAL(itemSelectionChanged()), this, SLOT(slotPlaylistChanged())); connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotDoubleClicked(QTreeWidgetItem*))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint))); TagTransactionManager *tagManager = TagTransactionManager::instance(); connect(tagManager, SIGNAL(signalAboutToModifyTags()), SLOT(slotFreezePlaylists())); connect(tagManager, SIGNAL(signalDoneModifyingTags()), SLOT(slotUnfreezePlaylists())); setupUpcomingPlaylist(); connect(CollectionList::instance(), SIGNAL(signalNewTag(QString,uint)), this, SLOT(slotAddItem(QString,uint))); connect(CollectionList::instance(), SIGNAL(signalRemovedTag(QString,uint)), this, SLOT(slotRemoveItem(QString,uint))); connect(CollectionList::instance(), SIGNAL(cachedItemsLoaded()), this, SLOT(slotLoadCachedPlaylists())); m_savePlaylistTimer = 0; KToggleAction *historyAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-history")), i18n("Show &History"), ActionCollection::actions()); ActionCollection::actions()->addAction("showHistory", historyAction); connect(historyAction, SIGNAL(triggered(bool)), this, SLOT(slotSetHistoryPlaylistEnabled(bool))); m_showTimer = new QTimer(this); connect(m_showTimer, SIGNAL(timeout()), SLOT(slotShowDropTarget())); // hook up to the D-Bus (void) new DBusCollectionProxy(this, this); } PlaylistBox::~PlaylistBox() { PlaylistList l; CollectionList *collection = CollectionList::instance(); for(QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it) { Item *item = static_cast(*it); if(item->playlist() && item->playlist() != collection) l.append(item->playlist()); } Cache::savePlaylists(l); saveConfig(); } void PlaylistBox::raise(Playlist *playlist) { if(!playlist) return; Item *i = m_playlistDict.value(playlist, 0); if(i) { clearSelection(); setCurrentItem(i); setSingleItem(i); scrollToItem(currentItem()); } else PlaylistCollection::raise(playlist); slotPlaylistChanged(); } void PlaylistBox::duplicate() { Item *item = static_cast(currentItem()); if(!item || !item->playlist()) return; QString name = playlistNameDialog(i18nc("verb, copy the playlist", "Duplicate"), item->text(0)); if(name.isNull()) return; Playlist *p = new Playlist(this, name); p->createItems(item->playlist()->items()); } void PlaylistBox::scanFolders() { - qCDebug(JUK_LOG) << "Starting folder scan"; - QTime stopwatch; stopwatch.start(); - PlaylistCollection::scanFolders(); - - qCDebug(JUK_LOG) << "Folder scan complete, took" << stopwatch.elapsed() << "ms"; - qCDebug(JUK_LOG) << "Startup complete!"; emit startupComplete(); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox public slots //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::paste() { // TODO: Reimplement } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox protected methods //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::slotFreezePlaylists() { setDynamicListsFrozen(true); } void PlaylistBox::slotUnfreezePlaylists() { setDynamicListsFrozen(false); } void PlaylistBox::slotPlaylistDataChanged() { if(m_savePlaylistTimer) m_savePlaylistTimer->start(); // Restarts the timer if it's already running. } void PlaylistBox::slotSetHistoryPlaylistEnabled(bool enable) { setHistoryPlaylistEnabled(enable); } void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName) { setupPlaylist(playlist, iconName, 0); } void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName, Item *parentItem) { connect(playlist, SIGNAL(signalPlaylistItemsDropped(Playlist*)), SLOT(slotPlaylistItemsDropped(Playlist*))); PlaylistCollection::setupPlaylist(playlist, iconName); if(parentItem) new Item(parentItem, iconName, playlist->name(), playlist); else new Item(this, iconName, playlist->name(), playlist); } void PlaylistBox::removePlaylist(Playlist *playlist) { // Could be false if setup() wasn't run yet. if(m_playlistDict.contains(playlist)) { removeNameFromDict(m_playlistDict[playlist]->text(0)); delete m_playlistDict[playlist]; // Delete the Item* } removeFileFromDict(playlist->fileName()); m_playlistDict.remove(playlist); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox private methods //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::readConfig() { KConfigGroup config(KSharedConfig::openConfig(), "PlaylistBox"); m_viewModeIndex = config.readEntry("ViewMode", 0); // TODO Restore ability to use Tree View once fixed. if(m_viewModeIndex == 2) { m_viewModeIndex = 0; } } void PlaylistBox::saveConfig() { KConfigGroup config(KSharedConfig::openConfig(), "PlaylistBox"); config.writeEntry("ViewMode", action("viewModeMenu")->currentItem()); KSharedConfig::openConfig()->sync(); } void PlaylistBox::remove() { ItemList items = selectedBoxItems(); if(items.isEmpty()) return; QStringList files; QStringList names; foreach(Item *item, items) { if(item && item->playlist()) { if (!item->playlist()->fileName().isEmpty() && QFileInfo(item->playlist()->fileName()).exists()) { files.append(item->playlist()->fileName()); } names.append(item->playlist()->name()); } } if(!files.isEmpty()) { int remove = KMessageBox::warningYesNoCancelList( this, i18n("Do you want to delete these files from the disk as well?"), files, QString(), KStandardGuiItem::del(), KGuiItem(i18n("Keep"))); if(remove == KMessageBox::Yes) { QStringList couldNotDelete; for(QStringList::ConstIterator it = files.constBegin(); it != files.constEnd(); ++it) { if(!QFile::remove(*it)) couldNotDelete.append(*it); } if(!couldNotDelete.isEmpty()) KMessageBox::errorList(this, i18n("Could not delete these files."), couldNotDelete); } else if(remove == KMessageBox::Cancel) return; } else if(items.count() > 1 || items.front()->playlist() != upcomingPlaylist()) { if(KMessageBox::warningContinueCancelList(this, i18n("Are you sure you want to remove these " "playlists from your collection?"), names, i18n("Remove Items?"), KGuiItem(i18n("&Remove"), "user-trash")) == KMessageBox::Cancel) { return; } } PlaylistList removeQueue; for(ItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { if(*it != Item::collectionItem() && (*it)->playlist() && (!(*it)->playlist()->readOnly())) { removeQueue.append((*it)->playlist()); } } // FIXME removing items /*if(items.back()->nextSibling() && static_cast(items.back()->nextSibling())->playlist()) setSingleItem(items.back()->nextSibling()); else { Item *i = static_cast(items.front()->itemAbove()); while(i && !i->playlist()) i = static_cast(i->itemAbove()); if(!i) i = Item::collectionItem(); setSingleItem(i); }*/ for(PlaylistList::ConstIterator it = removeQueue.constBegin(); it != removeQueue.constEnd(); ++it) { if(*it != upcomingPlaylist()) delete *it; else { action("showUpcoming")->setChecked(false); setUpcomingPlaylistEnabled(false); } } } void PlaylistBox::setDynamicListsFrozen(bool frozen) { for(QList::Iterator it = m_viewModes.begin(); it != m_viewModes.end(); ++it) { (*it)->setDynamicListsFrozen(frozen); } } void PlaylistBox::slotSavePlaylists() { qCDebug(JUK_LOG) << "Auto-saving playlists.\n"; PlaylistList l; CollectionList *collection = CollectionList::instance(); for(QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it) { Item *item = static_cast(*it); if(item->playlist() && item->playlist() != collection) l.append(item->playlist()); } Cache::savePlaylists(l); } void PlaylistBox::slotShowDropTarget() { if(!m_dropItem) { qCCritical(JUK_LOG) << "Trying to show the playlist of a null item!\n"; return; } raise(m_dropItem->playlist()); } void PlaylistBox::slotAddItem(const QString &tag, unsigned column) { for(QList::Iterator it = m_viewModes.begin(); it != m_viewModes.end(); ++it) (*it)->addItems(QStringList(tag), column); } void PlaylistBox::slotRemoveItem(const QString &tag, unsigned column) { for(QList::Iterator it = m_viewModes.begin(); it != m_viewModes.end(); ++it) (*it)->removeItem(tag, column); } void PlaylistBox::mousePressEvent(QMouseEvent *e) { if(e->button() == Qt::LeftButton) m_doingMultiSelect = true; QTreeWidget::mousePressEvent(e); } void PlaylistBox::mouseReleaseEvent(QMouseEvent *e) { if(e->button() == Qt::LeftButton) { m_doingMultiSelect = false; slotPlaylistChanged(); } QTreeWidget::mouseReleaseEvent(e); } void PlaylistBox::keyPressEvent(QKeyEvent *e) { if((e->key() == Qt::Key_Up || e->key() == Qt::Key_Down) && e->modifiers() == Qt::ShiftModifier) m_doingMultiSelect = true; QTreeWidget::keyPressEvent(e); } void PlaylistBox::keyReleaseEvent(QKeyEvent *e) { if(m_doingMultiSelect && e->key() == Qt::Key_Shift) { m_doingMultiSelect = false; slotPlaylistChanged(); } QTreeWidget::keyReleaseEvent(e); } PlaylistBox::ItemList PlaylistBox::selectedBoxItems() const { ItemList l; for(QTreeWidgetItemIterator it(const_cast(this), QTreeWidgetItemIterator::Selected); *it; ++it) l.append(static_cast(*it)); return l; } void PlaylistBox::setSingleItem(QTreeWidgetItem *item) { setSelectionMode(QAbstractItemView::SingleSelection); setCurrentItem(item); setSelectionMode(QAbstractItemView::ExtendedSelection); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox private slots //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::slotPlaylistChanged() { // Don't update while the mouse is pressed down. if(m_doingMultiSelect) return; ItemList items = selectedBoxItems(); m_hasSelection = !items.isEmpty(); bool allowReload = false; PlaylistList playlists; for(ItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { Playlist *p = (*it)->playlist(); if(p) { if(p->canReload()) allowReload = true; playlists.append(p); } } bool singlePlaylist = playlists.count() == 1; if(playlists.isEmpty() || (singlePlaylist && (playlists.front() == CollectionList::instance() || playlists.front()->readOnly()))) { action("file_save")->setEnabled(false); action("file_save_as")->setEnabled(false); action("renamePlaylist")->setEnabled(false); action("deleteItemPlaylist")->setEnabled(false); } else { action("file_save")->setEnabled(true); action("file_save_as")->setEnabled(true); action("renamePlaylist")->setEnabled(playlists.count() == 1); action("deleteItemPlaylist")->setEnabled(true); } action("reloadPlaylist")->setEnabled(allowReload); action("duplicatePlaylist")->setEnabled(!playlists.isEmpty()); action("editSearch")->setEnabled(singlePlaylist && playlists.front()->searchIsEditable()); if(singlePlaylist) { PlaylistCollection::raise(playlists.front()); if(playlists.front() == upcomingPlaylist()) action("deleteItemPlaylist")->setText(i18n("Hid&e")); else action("deleteItemPlaylist")->setText(i18n("R&emove")); } else if(!playlists.isEmpty()) createDynamicPlaylist(playlists); } void PlaylistBox::slotDoubleClicked(QTreeWidgetItem *item) { if(!item) return; TrackSequenceManager *manager = TrackSequenceManager::instance(); Item *playlistItem = static_cast(item); manager->setCurrentPlaylist(playlistItem->playlist()); manager->setCurrent(0); // Reset playback PlaylistItem *next = manager->nextItem(); // Allow manager to choose if(next) { emit startFilePlayback(next->file()); playlistItem->playlist()->setPlaying(next); } else action("stop")->trigger(); } void PlaylistBox::slotShowContextMenu(const QPoint &point) { m_contextMenu->popup(mapToGlobal(point)); } void PlaylistBox::slotPlaylistItemsDropped(Playlist *p) { raise(p); } void PlaylistBox::slotSetViewMode(int index) { if(index == m_viewModeIndex) return; viewMode()->setShown(false); m_viewModeIndex = index; viewMode()->setShown(true); } void PlaylistBox::setupItem(Item *item) { m_playlistDict.insert(item->playlist(), item); viewMode()->queueRefresh(); } void PlaylistBox::setupUpcomingPlaylist() { KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); bool enable = config.readEntry("showUpcoming", false); setUpcomingPlaylistEnabled(enable); action("showUpcoming")->setChecked(enable); } void PlaylistBox::slotLoadCachedPlaylists() { qCDebug(JUK_LOG) << "Loading cached playlists."; QTime stopwatch; stopwatch.start(); Cache::loadPlaylists(this); qCDebug(JUK_LOG) << "Cached playlists loaded, took" << stopwatch.elapsed() << "ms"; // Auto-save playlists after they change. m_savePlaylistTimer = new QTimer(this); m_savePlaylistTimer->setInterval(3000); // 3 seconds with no change? -> commit m_savePlaylistTimer->setSingleShot(true); connect(m_savePlaylistTimer, SIGNAL(timeout()), SLOT(slotSavePlaylists())); clearSelection(); setCurrentItem(m_playlistDict[CollectionList::instance()]); QTimer::singleShot(0, CollectionList::instance(), SLOT(slotCheckCache())); QTimer::singleShot(0, object(), SLOT(slotScanFolders())); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox::Item protected methods //////////////////////////////////////////////////////////////////////////////// PlaylistBox::Item *PlaylistBox::Item::m_collectionItem = 0; PlaylistBox::Item::Item(PlaylistBox *listBox, const QString &icon, const QString &text, Playlist *l) : QObject(listBox), QTreeWidgetItem(listBox, QStringList(text)), PlaylistObserver(l), m_playlist(l), m_text(text), m_iconName(icon), m_sortedFirst(false) { init(); } PlaylistBox::Item::Item(Item *parent, const QString &icon, const QString &text, Playlist *l) : QObject(parent->listView()), QTreeWidgetItem(parent, QStringList(text)), PlaylistObserver(l), m_playlist(l), m_text(text), m_iconName(icon), m_sortedFirst(false) { init(); } PlaylistBox::Item::~Item() { } int PlaylistBox::Item::compare(QTreeWidgetItem *i, int col, bool) const { Item *otherItem = static_cast(i); PlaylistBox *playlistBox = static_cast(treeWidget()); if(m_playlist == playlistBox->upcomingPlaylist() && otherItem->m_playlist != CollectionList::instance()) return -1; if(otherItem->m_playlist == playlistBox->upcomingPlaylist() && m_playlist != CollectionList::instance()) return 1; if(m_sortedFirst && !otherItem->m_sortedFirst) return -1; else if(otherItem->m_sortedFirst && !m_sortedFirst) return 1; return text(col).toLower().localeAwareCompare(i->text(col).toLower()); } // FIXME paintcell /*void PlaylistBox::Item::paintCell(QPainter *painter, const QColorGroup &colorGroup, int column, int width, int align) { PlaylistBox *playlistBox = static_cast(listView()); playlistBox->viewMode()->paintCell(this, painter, colorGroup, column, width, align); }*/ void PlaylistBox::Item::setText(int column, const QString &text) { m_text = text; QTreeWidgetItem::setText(column, text); } void PlaylistBox::Item::setup() { listView()->viewMode()->setupItem(this); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox::Item protected slots //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::Item::slotSetName(const QString &name) { if(listView()) { setText(0, name); setSelected(true); treeWidget()->sortItems(0, Qt::AscendingOrder); treeWidget()->scrollToItem(treeWidget()->currentItem()); //FIXME viewmode //listView()->viewMode()->queueRefresh(); } } void PlaylistBox::Item::playingItemHasChanged() { } void PlaylistBox::Item::playlistItemDataHasChanged() { // This avoids spuriously re-saving all playlists just because play queue // changes. if(m_playlist != listView()->upcomingPlaylist()) listView()->slotPlaylistDataChanged(); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox::Item private methods //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::Item::init() { PlaylistBox *list = listView(); list->setupItem(this); setIcon(0, QIcon::fromTheme(m_iconName)); list->addNameToDict(m_text); if(m_playlist) { connect(m_playlist, SIGNAL(signalNameChanged(QString)), this, SLOT(slotSetName(QString))); connect(m_playlist, SIGNAL(signalEnableDirWatch(bool)), list->object(), SLOT(slotEnableDirWatch(bool))); } if(m_playlist == CollectionList::instance()) { m_sortedFirst = true; m_collectionItem = this; list->viewMode()->setupDynamicPlaylists(); } if(m_playlist == list->historyPlaylist() || m_playlist == list->upcomingPlaylist()) m_sortedFirst = true; } // vim: set et sw=4 tw=0 sta: