diff --git a/CMakeLists.txt b/CMakeLists.txt index 7570a7af..7618cee8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,231 +1,235 @@ 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 (RELEASE_SERVICE_VERSION_MAJOR "20") set (RELEASE_SERVICE_VERSION_MINOR "03") set (RELEASE_SERVICE_VERSION_MICRO "70") set (RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") project(juk VERSION ${RELEASE_SERVICE_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 Concurrent Gui Svg Network Test Widgets) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS CoreAddons Completion Config Crash GlobalAccel I18n IconThemes DBusAddons 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 "https://wiki.musicbrainz.org/History:libtunepimp" 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( SYSTEM ${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(juk_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 juktag.cpp keydialog.cpp lyricswidget.cpp main.cpp mediafiles.cpp mpris2/mediaplayer2.cpp mpris2/mediaplayer2player.cpp mpris2/mpris2.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 tageditor.cpp tagguesser.cpp tagguesserconfigdlg.cpp tagrenameroptions.cpp tagtransactionmanager.cpp tracksequenceiterator.cpp tracksequencemanager.cpp treeviewitemplaylist.cpp upcomingplaylist.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 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::DBusAddons 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} ) +if(Qt5Widgets_VERSION VERSION_LESS "5.13.0") + find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS ItemModels) + target_link_libraries(juk KF5::ItemModels) +endif(Qt5Widgets_VERSION VERSION_LESS "5.13.0") ########### install files ############### install( PROGRAMS org.kde.juk.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} ) install( FILES jukui.rc jukui-rtl.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/juk ) install( FILES juk.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR} ) 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/advancedsearchdialog.cpp b/advancedsearchdialog.cpp index c3624eb4..94a6abc3 100644 --- a/advancedsearchdialog.cpp +++ b/advancedsearchdialog.cpp @@ -1,186 +1,186 @@ /** * Copyright (C) 2003-2004 Scott Wheeler * Copyright (C) 2017 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 "advancedsearchdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include "collectionlist.h" #include "searchwidget.h" //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// AdvancedSearchDialog::AdvancedSearchDialog(const QString &defaultName, const PlaylistSearch &defaultSearch, QWidget *parent) : QDialog(parent) { setWindowTitle(i18n("Create Search Playlist")); setObjectName(QStringLiteral("juk_advSrchDlg")); auto mw = new QVBoxLayout(this); setLayout(mw); auto box = new QHBoxLayout; mw->addLayout(box); box->addWidget(new QLabel(i18n("Playlist name:"))); m_playlistNameLineEdit = new QLineEdit(defaultName); box->addWidget(m_playlistNameLineEdit); auto criteriaGroupBox = new QGroupBox(i18n("Search Criteria")); mw->addWidget(criteriaGroupBox, 1); m_criteriaLayout = new QVBoxLayout(criteriaGroupBox); auto group = new QGroupBox; m_matchAnyButton = new QRadioButton(i18n("Match any of the following")); m_matchAllButton = new QRadioButton(i18n("Match all of the following")); QHBoxLayout *hgroupbox = new QHBoxLayout(group); hgroupbox->addWidget(m_matchAnyButton); hgroupbox->addWidget(m_matchAllButton); m_criteriaLayout->addWidget(group); m_criteriaLayout->addStretch(1); // more()/fewer() assume this is here QWidget *buttons = new QWidget; mw->addWidget(buttons); QHBoxLayout *l = new QHBoxLayout(buttons); l->setSpacing(5); l->setContentsMargins(0, 0, 0, 0); const auto &clearGuiItem = KStandardGuiItem::clear(); QPushButton *clearButton = new QPushButton(clearGuiItem.icon(), clearGuiItem.text()); connect(clearButton, &QPushButton::clicked, this, &AdvancedSearchDialog::clearSearches); l->addWidget(clearButton); l->addStretch(1); m_moreButton = new QPushButton(i18nc("additional search options", "More")); connect(m_moreButton, &QPushButton::clicked, this, &AdvancedSearchDialog::more); l->addWidget(m_moreButton); m_fewerButton = new QPushButton(i18n("Fewer")); connect(m_fewerButton, &QPushButton::clicked, this, &AdvancedSearchDialog::fewer); l->addWidget(m_fewerButton); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); mw->addWidget(buttonBox); if(defaultSearch.isNull()) { this->more(); this->more(); // Create first 2 searches m_matchAnyButton->setChecked(true); } else { PlaylistSearch::ComponentList components = defaultSearch.components(); for(PlaylistSearch::ComponentList::ConstIterator it = components.constBegin(); it != components.constEnd(); ++it) { SearchLine *s = new SearchLine(this); s->setSearchComponent(*it); m_searchLines.append(s); m_criteriaLayout->insertWidget(m_criteriaLayout->count() - 1, s); } if(defaultSearch.searchMode() == PlaylistSearch::MatchAny) m_matchAnyButton->setChecked(true); else m_matchAllButton->setChecked(true); } m_playlistNameLineEdit->setFocus(); } //////////////////////////////////////////////////////////////////////////////// // protected slots //////////////////////////////////////////////////////////////////////////////// void AdvancedSearchDialog::accept() { - m_search.clearPlaylists(); - m_search.clearComponents(); + m_search->clearPlaylists(); + m_search->clearComponents(); - m_search.addPlaylist(CollectionList::instance()); + m_search->addPlaylist(CollectionList::instance()); for(const auto &searchLine : m_searchLines) - m_search.addComponent(searchLine->searchComponent()); + m_search->addComponent(searchLine->searchComponent()); PlaylistSearch::SearchMode m = PlaylistSearch::SearchMode(!m_matchAnyButton->isChecked()); - m_search.setSearchMode(m); + m_search->setSearchMode(m); m_playlistName = m_playlistNameLineEdit->text(); QDialog::accept(); } void AdvancedSearchDialog::clearSearches() { for(auto &searchLine : m_searchLines) searchLine->clear(); } void AdvancedSearchDialog::more() { SearchLine *searchLine = new SearchLine(this); // inserting it to keep the trailing stretch item at end m_criteriaLayout->insertWidget(m_criteriaLayout->count() - 1, searchLine); m_searchLines.append(searchLine); searchLine->show(); updateButtons(); } void AdvancedSearchDialog::fewer() { SearchLine *searchLine = m_searchLines.last(); m_searchLines.removeAll(searchLine); delete searchLine; updateButtons(); } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// void AdvancedSearchDialog::updateButtons() { m_moreButton->setEnabled(m_searchLines.count() < 16); m_fewerButton->setEnabled(m_searchLines.count() > 1); } // vim: set et sw=4 tw=0 sta: diff --git a/advancedsearchdialog.h b/advancedsearchdialog.h index 86cf40bc..0bf17af7 100644 --- a/advancedsearchdialog.h +++ b/advancedsearchdialog.h @@ -1,73 +1,73 @@ /** * Copyright (C) 2003-2004 Scott Wheeler * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #ifndef ADVANCEDSEARCHDIALOG_H #define ADVANCEDSEARCHDIALOG_H #include #include #include "playlistsearch.h" class QLineEdit; class QPushButton; class QRadioButton; class SearchLine; class QBoxLayout; class AdvancedSearchDialog : public QDialog { Q_OBJECT public: explicit AdvancedSearchDialog( const QString& defaultName, const PlaylistSearch& defaultSearch = PlaylistSearch(), QWidget* parent = nullptr); - PlaylistSearch resultSearch() const + PlaylistSearch* resultSearch() const { return m_search; } QString resultPlaylistName() const { return m_playlistName; } protected slots: void accept() Q_DECL_OVERRIDE; void clearSearches(); void more(); void fewer(); private: void updateButtons(); QBoxLayout *m_criteriaLayout; - PlaylistSearch m_search; + PlaylistSearch* m_search; QString m_playlistName; QList m_searchLines; QLineEdit *m_playlistNameLineEdit; QRadioButton *m_matchAnyButton; QRadioButton *m_matchAllButton; QPushButton *m_moreButton; QPushButton *m_fewerButton; }; #endif // vim: set et sw=4 tw=0 sta: diff --git a/coverinfo.cpp b/coverinfo.cpp index fe0dc8e4..60018b28 100644 --- a/coverinfo.cpp +++ b/coverinfo.cpp @@ -1,489 +1,491 @@ /** * Copyright (C) 2004 Nathan Toone * Copyright (C) 2005, 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 "coverinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Taglib includes #include #include #include #include #include #include #ifdef TAGLIB_WITH_MP4 #include #include #include #include #endif #include "mediafiles.h" #include "collectionlist.h" #include "playlistsearch.h" #include "playlistitem.h" #include "juktag.h" #include "juk_debug.h" struct CoverPopup : public QWidget { CoverPopup(QPixmap &image, const QPoint &p) : QWidget(0, Qt::WindowFlags(Qt::WA_DeleteOnClose | Qt::X11BypassWindowManagerHint)) { QHBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(this); layout->addWidget(label); const auto pixRatio = this->devicePixelRatioF(); QSizeF imageSize(label->width(), label->height()); if (!qFuzzyCompare(pixRatio, 1.0)) { imageSize /= pixRatio; image.setDevicePixelRatio(pixRatio); } label->setFrameStyle(QFrame::Box | QFrame::Raised); label->setLineWidth(1); label->setPixmap(image); setGeometry(QRect(p, imageSize.toSize())); show(); } virtual void leaveEvent(QEvent *) override { close(); } virtual void mouseReleaseEvent(QMouseEvent *) override { close(); } }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// CoverInfo::CoverInfo(const FileHandle &file) : m_file(file), m_hasCover(false), m_hasAttachedCover(false), m_haveCheckedForCover(false), m_coverKey(CoverManager::NoMatch) { } bool CoverInfo::hasCover() const { if(m_haveCheckedForCover) return m_hasCover || m_hasAttachedCover; m_haveCheckedForCover = true; // Check for new-style covers. First let's determine what our coverKey is // if it's not already set, as that's also tracked by the CoverManager. if(m_coverKey == CoverManager::NoMatch) m_coverKey = CoverManager::idForTrack(m_file.absFilePath()); // We were assigned a key, let's see if we already have a cover. Notice // that due to the way the CoverManager is structured, we should have a // cover if we have a cover key. If we don't then either there's a logic // error, or the user has been mucking around where they shouldn't. if(m_coverKey != CoverManager::NoMatch) m_hasCover = CoverManager::hasCover(m_coverKey); // Check if it's embedded in the file itself. m_hasAttachedCover = hasEmbeddedAlbumArt(); if(m_hasAttachedCover) return true; // Look for cover.jpg or cover.png in the directory. if(QFile::exists(m_file.fileInfo().absolutePath() + "/cover.jpg") || QFile::exists(m_file.fileInfo().absolutePath() + "/cover.png")) { m_hasCover = true; } return m_hasCover; } void CoverInfo::clearCover() { m_hasCover = false; m_hasAttachedCover = false; // Re-search for cover since we may still have a different type of cover. m_haveCheckedForCover = false; // We don't need to call removeCover because the CoverManager will // automatically unlink the cover if we were the last track to use it. CoverManager::setIdForTrack(m_file.absFilePath(), CoverManager::NoMatch); m_coverKey = CoverManager::NoMatch; } void CoverInfo::setCover(const QImage &image) { if(image.isNull()) return; m_haveCheckedForCover = true; m_hasCover = true; QPixmap cover = QPixmap::fromImage(image); // If we use replaceCover we'll change the cover for every other track // with the same coverKey, which we don't want since that case will be // handled by Playlist. Instead just replace this track's cover. m_coverKey = CoverManager::addCover(cover, m_file.tag()->artist(), m_file.tag()->album()); if(m_coverKey != CoverManager::NoMatch) CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); } void CoverInfo::setCoverId(coverKey id) { m_coverKey = id; m_haveCheckedForCover = true; m_hasCover = id != CoverManager::NoMatch; // Inform CoverManager of the change. CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); } void CoverInfo::applyCoverToWholeAlbum(bool overwriteExistingCovers) const { QString artist = m_file.tag()->artist(); QString album = m_file.tag()->album(); PlaylistSearch::ComponentList components; ColumnList columns; columns.append(PlaylistItem::ArtistColumn); components.append(PlaylistSearch::Component(artist, false, columns, PlaylistSearch::Component::Exact)); columns.clear(); columns.append(PlaylistItem::AlbumColumn); components.append(PlaylistSearch::Component(album, false, columns, PlaylistSearch::Component::Exact)); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components, PlaylistSearch::MatchAll); // Search done, iterate through results. - PlaylistItemList results = search.matchedItems(); + PlaylistItemList results; + for(QModelIndex i : search.matchedItems()) + results.append(static_cast(CollectionList::instance()->itemAt(i.row(), i.column()))); PlaylistItemList::ConstIterator it = results.constBegin(); for(; it != results.constEnd(); ++it) { // Don't worry about files that somehow already have a tag, // unless the conversion is forced. if(!overwriteExistingCovers && (*it)->file().coverInfo()->coverId() != CoverManager::NoMatch) continue; (*it)->file().coverInfo()->setCoverId(m_coverKey); } } coverKey CoverInfo::coverId() const { if(m_coverKey == CoverManager::NoMatch) m_coverKey = CoverManager::idForTrack(m_file.absFilePath()); return m_coverKey; } QPixmap CoverInfo::pixmap(CoverSize size) const { if(hasCover() && m_coverKey != CoverManager::NoMatch) { return CoverManager::coverFromId(m_coverKey, size == Thumbnail ? CoverManager::Thumbnail : CoverManager::FullSize); } QImage cover; // If m_hasCover is still true we must have a directory cover image. if(m_hasCover) { QString fileName = m_file.fileInfo().absolutePath() + "/cover.jpg"; if(!cover.load(fileName)) { fileName = m_file.fileInfo().absolutePath() + "/cover.png"; if(!cover.load(fileName)) return QPixmap(); } return QPixmap::fromImage(cover); } // If we get here, see if there is an embedded cover. cover = embeddedAlbumArt(); if(!cover.isNull() && size == Thumbnail) cover = scaleCoverToThumbnail(cover); if(cover.isNull()) { return QPixmap(); } return QPixmap::fromImage(cover); } QString CoverInfo::localPathToCover(const QString &fallbackFileName) const { if(m_coverKey != CoverManager::NoMatch) { QString path = CoverManager::coverInfo(m_coverKey).path; if(!path.isEmpty()) return path; } if(hasEmbeddedAlbumArt()) { QFile albumArtFile(fallbackFileName); if(!albumArtFile.open(QIODevice::ReadWrite)) { return QString(); } QImage albumArt = embeddedAlbumArt(); albumArt.save(&albumArtFile, "PNG"); return fallbackFileName; } QString basePath = m_file.fileInfo().absolutePath(); if(QFile::exists(basePath + "/cover.jpg")) return basePath + "/cover.jpg"; else if(QFile::exists(basePath + "/cover.png")) return basePath + "/cover.png"; return QString(); } bool CoverInfo::hasEmbeddedAlbumArt() const { QScopedPointer fileTag( MediaFiles::fileFactoryByType(m_file.absFilePath())); if (TagLib::MPEG::File *mpegFile = dynamic_cast(fileTag.data())) { TagLib::ID3v2::Tag *id3tag = mpegFile->ID3v2Tag(false); if (!id3tag) { qCCritical(JUK_LOG) << m_file.absFilePath() << "seems to have invalid ID3 tag"; return false; } // Look for attached picture frames. TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"]; return !frames.isEmpty(); } else if (TagLib::Ogg::XiphComment *oggTag = dynamic_cast(fileTag->tag())) { return !oggTag->pictureList().isEmpty(); } else if (TagLib::FLAC::File *flacFile = dynamic_cast(fileTag.data())) { // Look if images are embedded. return !flacFile->pictureList().isEmpty(); } #ifdef TAGLIB_WITH_MP4 else if(TagLib::MP4::File *mp4File = dynamic_cast(fileTag.data())) { TagLib::MP4::Tag *tag = mp4File->tag(); if (tag) { return tag->contains("covr"); } } #endif return false; } static QImage embeddedMPEGAlbumArt(TagLib::ID3v2::Tag *id3tag) { if(!id3tag) return QImage(); // Look for attached picture frames. TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"]; if(frames.isEmpty()) return QImage(); // According to the spec attached picture frames have different types. // So we should look for the corresponding picture depending on what // type of image (i.e. front cover, file info) we want. If only 1 // frame, just return that (scaled if necessary). TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0; if(frames.size() != 1) { TagLib::ID3v2::FrameList::Iterator it = frames.begin(); for(; it != frames.end(); ++it) { // This must be dynamic_cast<>, TagLib will return UnknownFrame in APIC for // encrypted frames. TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast(*it); // Both thumbnail and full size should use FrontCover, as // FileIcon may be too small even for thumbnail. if(frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover) continue; selectedFrame = frame; break; } } // If we get here we failed to pick a picture, or there was only one, // so just use the first picture. if(!selectedFrame) selectedFrame = dynamic_cast(frames.front()); if(!selectedFrame) // Could occur for encrypted picture frames. return QImage(); TagLib::ByteVector picture = selectedFrame->picture(); return QImage::fromData( reinterpret_cast(picture.data()), picture.size()); } static QImage embeddedFLACAlbumArt(const TagLib::List &flacPictures) { if(flacPictures.isEmpty()) { return QImage(); } // Always use first picture - even if multiple are embedded. TagLib::ByteVector coverData = flacPictures[0]->data(); // Will return an image or a null image on error, works either way return QImage::fromData( reinterpret_cast(coverData.data()), coverData.size()); } #ifdef TAGLIB_WITH_MP4 static QImage embeddedMP4AlbumArt(TagLib::MP4::Tag *tag) { if(!tag->contains("covr")) return QImage(); const TagLib::MP4::CoverArtList covers = tag->item("covr").toCoverArtList(); for(const auto &cover : covers) { TagLib::ByteVector coverData = cover.data(); QImage result = QImage::fromData( reinterpret_cast(coverData.data()), coverData.size()); if(!result.isNull()) return result; } // No appropriate image found return QImage(); } #endif void CoverInfo::popup() const { QPixmap image = pixmap(FullSize); QPoint mouse = QCursor::pos(); QScreen *primaryScreen = QApplication::primaryScreen(); QRect desktop = primaryScreen->availableGeometry(); int x = mouse.x(); int y = mouse.y(); int height = image.size().height() + 4; int width = image.size().width() + 4; // Detect the right direction to pop up (always towards the center of the // screen), try to pop up with the mouse pointer 10 pixels into the image in // both directions. If we're too close to the screen border for this margin, // show it at the screen edge, accounting for the four pixels (two on each // side) for the window border. if(x - desktop.x() < desktop.width() / 2) x = (x - desktop.x() < 10) ? desktop.x() : (x - 10); else x = (x - desktop.x() > desktop.width() - 10) ? desktop.width() - width +desktop.x() : (x - width + 10); if(y - desktop.y() < desktop.height() / 2) y = (y - desktop.y() < 10) ? desktop.y() : (y - 10); else y = (y - desktop.y() > desktop.height() - 10) ? desktop.height() - height + desktop.y() : (y - height + 10); new CoverPopup(image, QPoint(x, y)); } QImage CoverInfo::embeddedAlbumArt() const { QScopedPointer fileTag( MediaFiles::fileFactoryByType(m_file.absFilePath())); if (auto *mpegFile = dynamic_cast(fileTag.data())) { return embeddedMPEGAlbumArt(mpegFile->ID3v2Tag(false)); } else if (auto *oggTag = dynamic_cast(fileTag->tag())) { return embeddedFLACAlbumArt(oggTag->pictureList()); } else if (auto *flacFile = dynamic_cast(fileTag.data())) { return embeddedFLACAlbumArt(flacFile->pictureList()); } #ifdef TAGLIB_WITH_MP4 else if(auto *mp4File = dynamic_cast(fileTag.data())) { auto *tag = mp4File->tag(); if (tag) { return embeddedMP4AlbumArt(tag); } } #endif return QImage(); } QImage CoverInfo::scaleCoverToThumbnail(const QImage &image) const { return image.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation); } // vim: set et sw=4 tw=0 sta: diff --git a/playlist.cpp b/playlist.cpp index 011e6fc9..c592ac64 100644 --- a/playlist.cpp +++ b/playlist.cpp @@ -1,2076 +1,2082 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2008-2018 Michael Pyne * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #include "playlist.h" #include "juk-exception.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "directoryloader.h" #include "playlistitem.h" #include "playlistcollection.h" #include "playlistsearch.h" #include "playlistsharedsettings.h" #include "mediafiles.h" #include "collectionlist.h" #include "filerenamer.h" #include "actioncollection.h" #include "tracksequencemanager.h" #include "juktag.h" #include "upcomingplaylist.h" #include "deletedialog.h" #include "webimagefetcher.h" #include "coverinfo.h" #include "coverdialog.h" #include "tagtransactionmanager.h" #include "cache.h" #include "juk_debug.h" using namespace ActionCollection; /** * Used to give every track added in the program a unique identifier. See * PlaylistItem */ quint32 g_trackID = 0; /** * Just a shortcut of sorts. */ static bool manualResize() { return action("resizeColumnsManually")->isChecked(); } //////////////////////////////////////////////////////////////////////////////// // static members //////////////////////////////////////////////////////////////////////////////// bool Playlist::m_visibleChanged = false; bool Playlist::m_shuttingDown = false; PlaylistItemList Playlist::m_history; QVector Playlist::m_backMenuItems; int Playlist::m_leftColumn = 0; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// Playlist::Playlist( bool delaySetup, const QString &name, PlaylistCollection *collection, const QString &iconName, int extraCols) : QTreeWidget(collection->playlistStack()) , m_collection(collection) , m_playlistName(name) , m_fetcher(new WebImageFetcher(this)) { // Any added columns must precede normal ones, which are normally added // in setup() for(int i = 0; i < extraCols; ++i) { addColumn(i18n("JuK")); // Placeholder text } setup(); // Some subclasses need to do even more handling but will remember to // call setupPlaylist if(!delaySetup) { collection->setupPlaylist(this, iconName); } } Playlist::Playlist(PlaylistCollection *collection, const QString &name, const QString &iconName) : Playlist(false, name, collection, iconName, 0) { } Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const QString &name, const QString &iconName) : Playlist(false, name, collection, iconName, 0) { createItems(items); } Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, const QString &iconName) : Playlist(true, QString(), collection, iconName, 0) { m_fileName = playlistFile.canonicalFilePath(); loadFile(m_fileName, playlistFile); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns) : Playlist(delaySetup, QString(), collection, QStringLiteral("audio-midi"), extraColumns) { } Playlist::~Playlist() { // clearItem() will take care of removing the items from the history, // so call clearItems() to make sure it happens. clearItems(items()); if(!m_shuttingDown) m_collection->removePlaylist(this); } QString Playlist::name() const { if(m_playlistName.isEmpty()) return m_fileName.section(QDir::separator(), -1).section('.', 0, -2); else return m_playlistName; } FileHandle Playlist::currentFile() const { return playingItem() ? playingItem()->file() : FileHandle(); } void Playlist::playFirst() { TrackSequenceManager::instance()->setNextItem(static_cast( *QTreeWidgetItemIterator(const_cast(this), QTreeWidgetItemIterator::NotHidden))); action("forward")->trigger(); } void Playlist::playNextAlbum() { PlaylistItem *current = TrackSequenceManager::instance()->currentItem(); if(!current) return; // No next album if we're not already playing. QString currentAlbum = current->file().tag()->album(); current = TrackSequenceManager::instance()->nextItem(); while(current && current->file().tag()->album() == currentAlbum) current = TrackSequenceManager::instance()->nextItem(); TrackSequenceManager::instance()->setNextItem(current); action("forward")->trigger(); } void Playlist::playNext() { TrackSequenceManager::instance()->setCurrentPlaylist(this); setPlaying(TrackSequenceManager::instance()->nextItem()); } void Playlist::stop() { m_history.clear(); setPlaying(nullptr); } void Playlist::playPrevious() { if(!playingItem()) return; bool random = action("randomPlay") && action("randomPlay")->isChecked(); PlaylistItem *previous = nullptr; if(random && !m_history.isEmpty()) { PlaylistItemList::Iterator last = m_history.end() - 1; previous = *last; m_history.erase(last); } else { m_history.clear(); previous = TrackSequenceManager::instance()->previousItem(); } if(!previous) previous = static_cast(playingItem()->itemAbove()); setPlaying(previous, false); } void Playlist::setName(const QString &n) { m_collection->addNameToDict(n); m_collection->removeNameFromDict(m_playlistName); m_playlistName = n; emit signalNameChanged(m_playlistName); } void Playlist::save() { if(m_fileName.isEmpty()) return saveAs(); QFile file(m_fileName); if(!file.open(QIODevice::WriteOnly)) return KMessageBox::error(this, i18n("Could not save to file %1.", m_fileName)); QTextStream stream(&file); QStringList fileList = files(); foreach(const QString &file, fileList) stream << file << '\n'; file.close(); } void Playlist::saveAs() { m_collection->removeFileFromDict(m_fileName); m_fileName = MediaFiles::savePlaylistDialog(name(), this); if(!m_fileName.isEmpty()) { m_collection->addFileToDict(m_fileName); // If there's no playlist name set, use the file name. if(m_playlistName.isEmpty()) emit signalNameChanged(name()); save(); } } void Playlist::updateDeletedItem(PlaylistItem *item) { m_members.remove(item->file().absFilePath()); - m_search.clearItem(item); m_history.removeAll(item); } void Playlist::clearItem(PlaylistItem *item) { // Automatically updates internal structs via updateDeletedItem delete item; playlistItemsChanged(); } void Playlist::clearItems(const PlaylistItemList &items) { foreach(PlaylistItem *item, items) delete item; playlistItemsChanged(); } PlaylistItem *Playlist::playingItem() // static { return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front(); } QStringList Playlist::files() const { QStringList list; for(QTreeWidgetItemIterator it(const_cast(this)); *it; ++it) list.append(static_cast(*it)->file().absFilePath()); return list; } PlaylistItemList Playlist::items() { return items(QTreeWidgetItemIterator::IteratorFlag(0)); } PlaylistItemList Playlist::visibleItems() { return items(QTreeWidgetItemIterator::NotHidden); } PlaylistItemList Playlist::selectedItems() { return items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); } PlaylistItem *Playlist::firstChild() const { return static_cast(topLevelItem(0)); } void Playlist::updateLeftColumn() { int newLeftColumn = leftMostVisibleColumn(); if(m_leftColumn != newLeftColumn) { updatePlaying(); m_leftColumn = newLeftColumn; } } -void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static +void Playlist::setItemsVisible(const QModelIndexList &indexes, bool visible) // static { m_visibleChanged = true; - foreach(PlaylistItem *playlistItem, items) - playlistItem->setHidden(!visible); + for(QModelIndex index : indexes) + itemFromIndex(index)->setHidden(!visible); } -void Playlist::setSearch(const PlaylistSearch &s) +void Playlist::setSearch(PlaylistSearch* s) { m_search = s; if(!m_searchEnabled) return; - setItemsVisible(s.matchedItems(), true); - setItemsVisible(s.unmatchedItems(), false); + for(int row = 0; row < topLevelItemCount(); ++row) + topLevelItem(row)->setHidden(true); + setItemsVisible(s->matchedItems(), true); 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); + for(int row = 0; row < topLevelItemCount(); ++row) + topLevelItem(row)->setHidden(true); + setItemsVisible(m_search->matchedItems(), true); } else - setItemsVisible(items(), true); + for(PlaylistItem* item : items()) + item->setHidden(false); + } // Mostly seems to be for DynamicPlaylist // TODO: See if this can't all be eliminated by making 'is-playing' a predicate // of the playlist item itself void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster) { foreach(const Playlist *p, sources) { if(p->playing()) { CollectionListItem *base = playingItem()->collectionItem(); for(QTreeWidgetItemIterator itemIt(this); *itemIt; ++itemIt) { PlaylistItem *item = static_cast(*itemIt); if(base == item->collectionItem()) { item->setPlaying(true, setMaster); PlaylistItemList playing = PlaylistItem::playingItems(); TrackSequenceManager::instance()->setCurrent(item); return; } } return; } } } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void Playlist::copy() { PlaylistItemList items = selectedItems(); QList urls; foreach(PlaylistItem *item, items) { urls << QUrl::fromLocalFile(item->file().absFilePath()); } QMimeData *mimeData = new QMimeData; mimeData->setUrls(urls); QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void Playlist::paste() { addFilesFromMimeData( QApplication::clipboard()->mimeData(), static_cast(currentItem())); } void Playlist::clear() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = items(); clearItems(l); } void Playlist::slotRefresh() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = visibleItems(); QApplication::setOverrideCursor(Qt::WaitCursor); foreach(PlaylistItem *item, l) { item->refreshFromDisk(); if(!item->file().tag() || !item->file().fileInfo().exists()) { qCDebug(JUK_LOG) << "Error while trying to refresh the tag. " << "This file has probably been removed."; delete item->collectionItem(); } processEvents(); } QApplication::restoreOverrideCursor(); } void Playlist::slotRenameFile() { FileRenamer renamer; PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; emit signalEnableDirWatch(false); m_blockDataChanged = true; renamer.rename(items); m_blockDataChanged = false; playlistItemsChanged(); emit signalEnableDirWatch(true); } void Playlist::slotViewCover() { const PlaylistItemList items = selectedItems(); if (items.isEmpty()) return; foreach(const PlaylistItem *item, items) item->file().coverInfo()->popup(); } void Playlist::slotRemoveCover() { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; int button = KMessageBox::warningContinueCancel(this, i18n("Are you sure you want to delete these covers?"), QString(), KGuiItem(i18n("&Delete Covers"))); if(button == KMessageBox::Continue) refreshAlbums(items); } void Playlist::slotShowCoverManager() { static CoverDialog *managerDialog = 0; if(!managerDialog) managerDialog = new CoverDialog(this); managerDialog->show(); } void Playlist::slotAddCover(bool retrieveLocal) { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; if(!retrieveLocal) { m_fetcher->setFile((*items.begin())->file()); m_fetcher->searchCover(); return; } QUrl file = QFileDialog::getOpenFileUrl( this, i18n("Select Cover Image File"), QUrl::fromLocalFile(QDir::home().path()), i18n("Images (*.png *.jpg)"), nullptr, {}, QStringList() << QStringLiteral("file") ); if(file.isEmpty()) return; QString artist = items.front()->file().tag()->artist(); QString album = items.front()->file().tag()->album(); coverKey newId = CoverManager::addCover(file, artist, album); if(newId != CoverManager::NoMatch) refreshAlbums(items, newId); } // Called when image fetcher has added a new cover. void Playlist::slotCoverChanged(int coverId) { qCDebug(JUK_LOG) << "Refreshing information for newly changed covers.\n"; refreshAlbums(selectedItems(), coverId); } void Playlist::slotGuessTagInfo(TagGuesser::Type type) { QApplication::setOverrideCursor(Qt::WaitCursor); const PlaylistItemList items = selectedItems(); setDynamicListsFrozen(true); m_blockDataChanged = true; foreach(PlaylistItem *item, items) { item->guessTagInfo(type); processEvents(); } // MusicBrainz queries automatically commit at this point. What would // be nice is having a signal emitted when the last query is completed. if(type == TagGuesser::FileName) TagTransactionManager::instance()->commit(); m_blockDataChanged = false; playlistItemsChanged(); setDynamicListsFrozen(false); QApplication::restoreOverrideCursor(); } void Playlist::slotReload() { QFileInfo fileInfo(m_fileName); if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable()) return; clearItems(items()); loadFile(m_fileName, fileInfo); } void Playlist::slotWeightDirty(int column) { if(column < 0) { m_weightDirty.clear(); for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) m_weightDirty.append(i); } return; } if(!m_weightDirty.contains(column)) m_weightDirty.append(column); } void Playlist::slotShowPlaying() { if(!playingItem()) return; Playlist *l = playingItem()->playlist(); l->clearSelection(); // Raise the playlist before selecting the items otherwise the tag editor // will not update when it gets the selectionChanged() notification // because it will think the user is choosing a different playlist but not // selecting a different item. m_collection->raise(l); l->setCurrentItem(playingItem()); l->scrollToItem(playingItem()); } void Playlist::slotColumnResizeModeChanged() { if(manualResize()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } if(!manualResize()) slotUpdateColumnWidths(); SharedSettings::instance()->sync(); } void Playlist::playlistItemsChanged() { if(m_blockDataChanged) return; PlaylistInterface::playlistItemsChanged(); } //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// void Playlist::removeFromDisk(const PlaylistItemList &items) { if(isVisible() && !items.isEmpty()) { QStringList files; foreach(const PlaylistItem *item, items) files.append(item->file().absFilePath()); DeleteDialog dialog(this); m_blockDataChanged = true; if(dialog.confirmDeleteList(files)) { bool shouldDelete = dialog.shouldDelete(); QStringList errorFiles; foreach(PlaylistItem *item, items) { if(playingItem() == item) action("forward")->trigger(); QString removePath = item->file().absFilePath(); QUrl removeUrl = QUrl::fromLocalFile(removePath); if((!shouldDelete && KIO::trash(removeUrl)->exec()) || (shouldDelete && QFile::remove(removePath))) { delete item->collectionItem(); } else errorFiles.append(item->file().absFilePath()); } if(!errorFiles.isEmpty()) { QString errorMsg = shouldDelete ? i18n("Could not delete these files") : i18n("Could not move these files to the Trash"); KMessageBox::errorList(this, errorMsg, errorFiles); } } m_blockDataChanged = false; playlistItemsChanged(); } } void Playlist::synchronizeItemsTo(const PlaylistItemList &itemList) { // direct call to ::items to avoid infinite loop, bug 402355 clearItems(Playlist::items()); createItems(itemList); } void Playlist::dragEnterEvent(QDragEnterEvent *e) { if(CoverDrag::isCover(e->mimeData())) { setDropIndicatorShown(false); e->accept(); return; } if(e->mimeData()->hasUrls() && !e->mimeData()->urls().isEmpty()) { setDropIndicatorShown(true); e->acceptProposedAction(); } else e->ignore(); } void Playlist::addFilesFromMimeData(const QMimeData *urls, PlaylistItem *after) { if(!urls->hasUrls()) { return; } addFiles(QUrl::toStringList(urls->urls(), QUrl::PreferLocalFile), after); } bool Playlist::eventFilter(QObject *watched, QEvent *e) { if(watched == header()) { switch(e->type()) { case QEvent::MouseMove: { if((static_cast(e)->modifiers() & Qt::LeftButton) == Qt::LeftButton && !action("resizeColumnsManually")->isChecked()) { m_columnWidthModeChanged = true; action("resizeColumnsManually")->setChecked(true); slotColumnResizeModeChanged(); } break; } case QEvent::MouseButtonPress: { if(static_cast(e)->button() == Qt::RightButton) m_headerMenu->popup(QCursor::pos()); break; } case QEvent::MouseButtonRelease: { if(m_columnWidthModeChanged) { m_columnWidthModeChanged = false; notifyUserColumnWidthModeChanged(); } if(!manualResize() && m_widthsDirty) QTimer::singleShot(0, this, SLOT(slotUpdateColumnWidths())); break; } default: break; } } return QTreeWidget::eventFilter(watched, e); } void Playlist::keyPressEvent(QKeyEvent *event) { if(event->key() == Qt::Key_Up) { const auto topItem = topLevelItem(0); if(topItem && topItem == currentItem()) { QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden); if(topItem == *visible) { emit signalMoveFocusAway(); event->accept(); } } } QTreeWidget::keyPressEvent(event); } QStringList Playlist::mimeTypes() const { return QStringList("text/uri-list"); } QMimeData* Playlist::mimeData(const QList items) const { QList urls; foreach(QTreeWidgetItem *item, items) { urls << QUrl::fromLocalFile(static_cast(item)->file().absFilePath()); } QMimeData *urlDrag = new QMimeData(); urlDrag->setUrls(urls); return urlDrag; } bool Playlist::dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action) { // TODO: Re-add DND Q_UNUSED(parent); Q_UNUSED(index); Q_UNUSED(data); Q_UNUSED(action); return false; } void Playlist::dropEvent(QDropEvent *e) { QPoint vp = e->pos(); PlaylistItem *item = static_cast(itemAt(vp)); // First see if we're dropping a cover, if so we can get it out of the // way early. if(item && CoverDrag::isCover(e->mimeData())) { coverKey id = CoverDrag::idFromData(e->mimeData()); // If the item we dropped on is selected, apply cover to all selected // items, otherwise just apply to the dropped item. if(item->isSelected()) { const PlaylistItemList selItems = selectedItems(); foreach(PlaylistItem *playlistItem, selItems) { playlistItem->file().coverInfo()->setCoverId(id); playlistItem->refresh(); } } else { item->file().coverInfo()->setCoverId(id); item->refresh(); } return; } // When dropping on the toUpper half of an item, insert before this item. // This is what the user expects, and also allows the insertion at // top of the list QRect rect = visualItemRect(item); if(!item) item = static_cast(topLevelItem(topLevelItemCount() - 1)); else if(vp.y() < rect.y() + rect.height() / 2) item = static_cast(item->itemAbove()); m_blockDataChanged = true; if(e->source() == this) { // Since we're trying to arrange things manually, turn off sorting. sortItems(columnCount() + 1, Qt::AscendingOrder); const QList items = QTreeWidget::selectedItems(); foreach(QTreeWidgetItem *listViewItem, items) { if(!item) { // Insert the item at the top of the list. This is a bit ugly, // but I don't see another way. takeItem(listViewItem); insertItem(listViewItem); } //else // listViewItem->moveItem(item); item = static_cast(listViewItem); } } else addFilesFromMimeData(e->mimeData(), item); m_blockDataChanged = false; playlistItemsChanged(); emit signalPlaylistItemsDropped(this); QTreeWidget::dropEvent(e); } void Playlist::showEvent(QShowEvent *e) { if(m_applySharedSettings) { SharedSettings::instance()->apply(this); m_applySharedSettings = false; } QTreeWidget::showEvent(e); } void Playlist::applySharedSettings() { m_applySharedSettings = true; } void Playlist::read(QDataStream &s) { s >> m_playlistName >> m_fileName; // m_fileName is probably empty. if(m_playlistName.isEmpty()) throw BICStreamException(); // Do not sort. Add the files in the order they were saved. setSortingEnabled(false); QStringList files; s >> files; QTreeWidgetItem *after = 0; m_blockDataChanged = true; foreach(const QString &file, files) { if(file.isEmpty()) throw BICStreamException(); after = createItem(FileHandle(file), after); } m_blockDataChanged = false; playlistItemsChanged(); m_collection->setupPlaylist(this, "audio-midi"); } void Playlist::paintEvent(QPaintEvent *pe) { // If there are columns that need to be updated, well, update them. if(!m_weightDirty.isEmpty() && !manualResize()) { calculateColumnWeights(); slotUpdateColumnWidths(); } QTreeWidget::paintEvent(pe); } void Playlist::resizeEvent(QResizeEvent *re) { // If the width of the view has changed, manually update the column // widths. if(re->size().width() != re->oldSize().width() && !manualResize()) slotUpdateColumnWidths(); QTreeWidget::resizeEvent(re); } // Reimplemented to show a visual indication of which of the view's playlist // items is actually playing. void Playlist::drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { PlaylistItem *item = static_cast(itemFromIndex(index)); if(Q_LIKELY(!PlaylistItem::playingItems().contains(item))) { return QTreeWidget::drawRow(p, option, index); } // Seems that the view draws the background now so we have to do this // manually p->fillRect(option.rect, QPalette{}.midlight()); QStyleOptionViewItem newOption {option}; newOption.font.setBold(true); QTreeWidget::drawRow(p, newOption, index); } void Playlist::insertItem(QTreeWidgetItem *item) { QTreeWidget::insertTopLevelItem(0, item); } void Playlist::takeItem(QTreeWidgetItem *item) { int index = indexOfTopLevelItem(item); QTreeWidget::takeTopLevelItem(index); } void Playlist::addColumn(const QString &label, int) { m_columns.append(label); setHeaderLabels(m_columns); } PlaylistItem *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after) { return createItem(file, after); } void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after) { createItems(siblings, after); } void Playlist::addFiles(const QStringList &files, PlaylistItem *after) { if(Q_UNLIKELY(files.isEmpty())) { return; } m_blockDataChanged = true; setEnabled(false); QVector> pendingFutures; for(const auto &file : files) { // some files added here will launch threads that we must wait until // they're done to cleanup auto pendingResult = addUntypedFile(file, after); if(!pendingResult.isFinished()) { pendingFutures.push_back(pendingResult); ++m_itemsLoading; } } // It's possible for no async threads to be launched, and also possible // for this function to be called while there were other threads in flight if(pendingFutures.isEmpty() && m_itemsLoading == 0) { cleanupAfterAllFileLoadsCompleted(); return; } // Build handlers for all the still-active loaders on the heap and then // return to the event loop. for(const auto &future : qAsConst(pendingFutures)) { auto loadWatcher = new QFutureWatcher(this); loadWatcher->setFuture(future); connect(loadWatcher, &QFutureWatcher::finished, this, [=]() { if(--m_itemsLoading == 0) { cleanupAfterAllFileLoadsCompleted(); } loadWatcher->deleteLater(); }); } } void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id) { QList< QPair > albums; bool setAlbumCovers = items.count() == 1; foreach(const PlaylistItem *item, items) { QString artist = item->file().tag()->artist(); QString album = item->file().tag()->album(); if(!albums.contains(qMakePair(artist, album))) albums.append(qMakePair(artist, album)); item->file().coverInfo()->setCoverId(id); if(setAlbumCovers) item->file().coverInfo()->applyCoverToWholeAlbum(true); } for(QList< QPair >::ConstIterator it = albums.constBegin(); it != albums.constEnd(); ++it) { refreshAlbum((*it).first, (*it).second); } } void Playlist::updatePlaying() const { foreach(const PlaylistItem *item, PlaylistItem::playingItems()) item->treeWidget()->viewport()->update(); } void Playlist::refreshAlbum(const QString &artist, const QString &album) { ColumnList columns; columns.append(PlaylistItem::ArtistColumn); PlaylistSearch::Component artistComponent(artist, false, columns, PlaylistSearch::Component::Exact); columns.clear(); columns.append(PlaylistItem::AlbumColumn); PlaylistSearch::Component albumComponent(album, false, columns, PlaylistSearch::Component::Exact); PlaylistSearch::ComponentList components; components.append(artist); components.append(album); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components); - const PlaylistItemList matches = search.matchedItems(); + const QModelIndexList matches = search.matchedItems(); - foreach(PlaylistItem *item, matches) - item->refresh(); + for(QModelIndex index: matches) + static_cast(itemFromIndex(index))->refresh(); } void Playlist::hideColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(false); break; } } if(isColumnHidden(c)) return; QTreeWidget::hideColumn(c); if(c == m_leftColumn) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->hideColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::showColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(true); break; } } if(!isColumnHidden(c)) return; QTreeWidget::showColumn(c); if(c == leftMostVisibleColumn()) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->showColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::sortByColumn(int column, Qt::SortOrder order) { setSortingEnabled(true); QTreeWidget::sortByColumn(column, order); } void Playlist::slotInitialize() { addColumn(i18n("Track Name")); addColumn(i18n("Artist")); addColumn(i18n("Album")); addColumn(i18n("Cover")); addColumn(i18nc("cd track number", "Track")); addColumn(i18n("Genre")); addColumn(i18n("Year")); addColumn(i18n("Length")); addColumn(i18n("Bitrate")); addColumn(i18n("Comment")); addColumn(i18n("File Name")); addColumn(i18n("File Name (full path)")); setAllColumnsShowFocus(true); setSelectionMode(QTreeWidget::ExtendedSelection); header()->setSortIndicatorShown(true); m_columnFixedWidths.resize(columnCount()); ////////////////////////////////////////////////// // setup header RMB menu ////////////////////////////////////////////////// QAction *showAction; const auto sharedSettings = SharedSettings::instance(); for(int i = 0; i < header()->count(); ++i) { if(i - columnOffset() == PlaylistItem::FileNameColumn) m_headerMenu->addSeparator(); showAction = new QAction(headerItem()->text(i), m_headerMenu); showAction->setData(i); showAction->setCheckable(true); showAction->setChecked(sharedSettings->isColumnVisible(i)); m_headerMenu->addAction(showAction); resizeColumnToContents(i); } connect(m_headerMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotToggleColumnVisible(QAction*))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowRMBMenu(QPoint))); connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotPlayCurrent())); // Disabled for now because adding new items (File->Open) causes Qt to send // an itemChanged signal for unrelated playlist items which can cause the // inline editor done slot to mistakenly overwrite tags associated to // *other* playlist items. I haven't found a way to determine whether the // itemChanged signal is really coming from the inline editor so instead // users will need to use the tag editor. :( // -- mpyne 2018-12-20 //connect(this, &QTreeWidget::itemChanged, // this, &Playlist::slotInlineEditDone); connect(action("resizeColumnsManually"), SIGNAL(triggered()), this, SLOT(slotColumnResizeModeChanged())); if(action("resizeColumnsManually")->isChecked()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragEnabled(true); m_disableColumnWidthUpdates = false; } void Playlist::setupItem(PlaylistItem *item) { item->setTrackId(g_trackID); g_trackID++; - if(!m_search.isEmpty()) - item->setHidden(!m_search.checkItem(item)); + QModelIndex index = indexFromItem(item); + if(!m_search->isEmpty()) + item->setHidden(!m_search->checkItem(&index)); 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() { + m_search = new PlaylistSearch(this); + setAlternatingRowColors(true); setRootIsDecorated(false); setContextMenuPolicy(Qt::CustomContextMenu); setUniformRowHeights(true); setEditTriggers(QAbstractItemView::EditKeyPressed); // Don't edit on double-click connect(header(), SIGNAL(sectionMoved(int,int,int)), this, SLOT(slotColumnOrderChanged(int,int,int))); connect(m_fetcher, SIGNAL(signalCoverChanged(int)), this, SLOT(slotCoverChanged(int))); // Prevent list of selected items from changing while internet search is in // progress. connect(this, SIGNAL(itemSelectionChanged()), m_fetcher, SLOT(abortSearch())); sortByColumn(1, Qt::AscendingOrder); // Should this be itemActivated? It is quite annoying when I try it... connect(this, &QTreeWidget::itemDoubleClicked, this, &Playlist::slotPlayCurrent); // Use a timer to soak up the multiple dataChanged signals we're going to get auto updateRequestor = new QTimer(this); updateRequestor->setSingleShot(true); updateRequestor->setInterval(10); connect(model(), &QAbstractItemModel::dataChanged, updateRequestor, static_cast(&QTimer::start)); connect(updateRequestor, &QTimer::timeout, this, &Playlist::slotUpdateTime); // This apparently must be created very early in initialization for other // Playlist code requiring m_headerMenu. m_columnVisibleAction = new KActionMenu(i18n("&Show Columns"), this); ActionCollection::actions()->addAction("showColumns", m_columnVisibleAction); m_headerMenu = m_columnVisibleAction->menu(); header()->installEventFilter(this); // TODO: Determine if other stuff in setup must happen before slotInitialize(). // Explicitly call slotInitialize() so that the columns are added before // SharedSettings::apply() sets the visible and hidden ones. slotInitialize(); } void Playlist::loadFile(const QString &fileName, const QFileInfo &fileInfo) { QFile file(fileName); if(!file.open(QIODevice::ReadOnly)) return; QTextStream stream(&file); // Turn off non-explicit sorting. setSortingEnabled(false); m_disableColumnWidthUpdates = true; m_blockDataChanged = true; PlaylistItem *after = nullptr; while(!stream.atEnd()) { QString itemName = stream.readLine().trimmed(); QFileInfo item(itemName); if(item.isRelative()) item.setFile(QDir::cleanPath(fileInfo.absolutePath() + '/' + itemName)); if(item.exists() && item.isFile() && item.isReadable() && MediaFiles::isMediaFile(item.fileName())) { after = createItem(FileHandle(item), after); } } m_blockDataChanged = false; m_disableColumnWidthUpdates = false; file.close(); playlistItemsChanged(); } void Playlist::setPlaying(PlaylistItem *item, bool addToHistory) { if(playingItem() == item) return; if(playingItem()) { if(addToHistory) { if(playingItem()->playlist() == playingItem()->playlist()->m_collection->upcomingPlaylist()) m_history.append(playingItem()->collectionItem()); else m_history.append(playingItem()); } playingItem()->setPlaying(false); } TrackSequenceManager::instance()->setCurrent(item); // TODO is this replaced by MPRIS2? //kapp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data); if(!item) return; item->setPlaying(true); bool enableBack = !m_history.isEmpty(); action("back")->menu()->setEnabled(enableBack); } bool Playlist::playing() const { return playingItem() && this == playingItem()->playlist(); } int Playlist::leftMostVisibleColumn() const { int i = 0; while(i < PlaylistItem::lastColumn() && isColumnHidden(i)) i++; return i < PlaylistItem::lastColumn() ? i : 0; } PlaylistItemList Playlist::items(QTreeWidgetItemIterator::IteratorFlags flags) { PlaylistItemList list; for(QTreeWidgetItemIterator it(this, flags); *it; ++it) list.append(static_cast(*it)); return list; } void Playlist::calculateColumnWeights() { if(m_disableColumnWidthUpdates) return; const PlaylistItemList l = items(); QVector averageWidth(columnCount()); double itemCount = l.size(); QVector cachedWidth; // Here we're not using a real average, but averaging the squares of the // column widths and then using the square root of that value. This gives // a nice weighting to the longer columns without doing something arbitrary // like adding a fixed amount of padding. foreach(PlaylistItem *item, l) { cachedWidth = item->cachedWidths(); // Extra columns start at 0, but those weights aren't shared with all // items. for(int i = 0; i < columnOffset(); ++i) { averageWidth[i] += std::pow(double(columnWidth(i)), 2.0) / itemCount; } for(int column = columnOffset(); column < columnCount(); ++column) { averageWidth[column] += std::pow(double(cachedWidth[column - columnOffset()]), 2.0) / itemCount; } } if(m_columnWeights.isEmpty()) m_columnWeights.fill(-1, columnCount()); foreach(int column, m_weightDirty) { m_columnWeights[column] = int(std::sqrt(averageWidth[column]) + 0.5); } m_weightDirty.clear(); } void Playlist::addPlaylistFile(const QString &m3uFile) { if (!m_collection->containsPlaylistFile(m3uFile)) { new Playlist(m_collection, QFileInfo(m3uFile)); } } QFuture Playlist::addFilesFromDirectory(const QString &dirPath) { auto loader = new DirectoryLoader(dirPath); connect(loader, &DirectoryLoader::loadedPlaylist, this, [this](const QString &m3uFile) { addPlaylistFile(m3uFile); } ); connect(loader, &DirectoryLoader::loadedFiles, this, [this](const FileHandleList &newFiles) { for(const auto newFile : newFiles) { createItem(newFile); } } ); auto future = QtConcurrent::run(loader, &DirectoryLoader::startLoading); auto loadWatcher = new QFutureWatcher(this); connect(loadWatcher, &QFutureWatcher::finished, this, [=]() { loader->deleteLater(); loadWatcher->deleteLater(); }); return future; } // Returns a future since some codepaths will result in an async operation. QFuture Playlist::addUntypedFile(const QString &file, PlaylistItem *after) { if(hasItem(file) && !m_allowDuplicates) return {}; const QFileInfo fileInfo(file); const QString canonicalPath = fileInfo.canonicalFilePath(); if(fileInfo.isFile() && fileInfo.isReadable() && MediaFiles::isMediaFile(file)) { FileHandle f(fileInfo); f.tag(); createItem(f, after); return {}; } if(MediaFiles::isPlaylistFile(file)) { addPlaylistFile(canonicalPath); return {}; } if(fileInfo.isDir()) { foreach(const QString &directory, m_collection->excludedFolders()) { if(canonicalPath.startsWith(directory)) return {}; // Exclude it } return addFilesFromDirectory(canonicalPath); } return {}; } // Called directly or after a threaded directory load has completed, managed by // m_itemsLoading void Playlist::cleanupAfterAllFileLoadsCompleted() { m_blockDataChanged = false; setEnabled(true); // Even if doing a manual column weights we'll generally start off with // incorrect column sizes so at least figure out a reasonable column size // and let user adjust from there. if(manualResize()) { auto manualResizeAction = action("resizeColumnsManually"); manualResizeAction->toggle(); calculateColumnWeights(); slotUpdateColumnWidths(); manualResizeAction->toggle(); } playlistItemsChanged(); } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotUpdateColumnWidths() { if(m_disableColumnWidthUpdates || manualResize()) return; // Make sure that the column weights have been initialized before trying to // update the columns. QList visibleColumns; for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) visibleColumns.append(i); } // convenience handler for deprecated text metrics const auto textWidth = [](const QFontMetrics &fm, const QString &text) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) return fm.horizontalAdvance(text); #else return fm.width(text); #endif }; if(count() == 0) { foreach(int column, visibleColumns) setColumnWidth(column, textWidth(header()->fontMetrics(),headerItem()->text(column)) + 10); return; } if(m_columnWeights.isEmpty()) return; // First build a list of minimum widths based on the strings in the listview // header. We won't let the width of the column go below this width. QVector minimumWidth(columnCount(), 0); int minimumWidthTotal = 0; // Also build a list of either the minimum *or* the fixed width -- whichever is // greater. QVector minimumFixedWidth(columnCount(), 0); int minimumFixedWidthTotal = 0; foreach(int column, visibleColumns) { minimumWidth[column] = textWidth(header()->fontMetrics(), headerItem()->text(column)) + 10; minimumWidthTotal += minimumWidth[column]; minimumFixedWidth[column] = qMax(minimumWidth[column], m_columnFixedWidths[column]); minimumFixedWidthTotal += minimumFixedWidth[column]; } // Make sure that the width won't get any smaller than this. We have to // account for the scrollbar as well. Since this method is called from the // resize event this will set a pretty hard toLower bound on the size. setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width()); // If we've got enough room for the fixed widths (larger than the minimum // widths) then instead use those for our "minimum widths". if(minimumFixedWidthTotal < viewport()->width()) { minimumWidth = minimumFixedWidth; minimumWidthTotal = minimumFixedWidthTotal; } // We've got a list of columns "weights" based on some statistics gathered // about the widths of the items in that column. We need to find the total // useful weight to use as a divisor for each column's weight. double totalWeight = 0; foreach(int column, visibleColumns) totalWeight += m_columnWeights[column]; // Computed a "weighted width" for each visible column. This would be the // width if we didn't have to handle the cases of minimum and maximum widths. QVector weightedWidth(columnCount(), 0); foreach(int column, visibleColumns) weightedWidth[column] = int(double(m_columnWeights[column]) / totalWeight * viewport()->width() + 0.5); // The "extra" width for each column. This is the weighted width less the // minimum width or zero if the minimum width is greater than the weighted // width. QVector extraWidth(columnCount(), 0); // This is used as an indicator if we have any columns where the weighted // width is less than the minimum width. If this is false then we can // just use the weighted width with no problems, otherwise we have to // "readjust" the widths. bool readjust = false; // If we have columns where the weighted width is less than the minimum width // we need to steal that space from somewhere. The amount that we need to // steal is the "neededWidth". int neededWidth = 0; // While we're on the topic of stealing -- we have to have somewhere to steal // from. availableWidth is the sum of the amount of space beyond the minimum // width that each column has been allocated -- the sum of the values of // extraWidth[]. int availableWidth = 0; // Fill in the values discussed above. foreach(int column, visibleColumns) { if(weightedWidth[column] < minimumWidth[column]) { readjust = true; extraWidth[column] = 0; neededWidth += minimumWidth[column] - weightedWidth[column]; } else { extraWidth[column] = weightedWidth[column] - minimumWidth[column]; availableWidth += extraWidth[column]; } } // The adjustmentRatio is the amount of the "extraWidth[]" that columns will // actually be given. double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth); // This will be the sum of the total space that we actually use. Because of // rounding error this won't be the exact available width. int usedWidth = 0; // Now set the actual column widths. If the weighted widths are all greater // than the minimum widths, just use those, otherwise use the "readjusted // weighted width". foreach(int column, visibleColumns) { int width; if(readjust) { int adjustedExtraWidth = int(double(extraWidth[column]) * adjustmentRatio + 0.5); width = minimumWidth[column] + adjustedExtraWidth; } else width = weightedWidth[column]; setColumnWidth(column, width); usedWidth += width; } // Fill the remaining gap for a clean fit into the available space. int remainingWidth = viewport()->width() - usedWidth; setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth); m_widthsDirty = false; } void Playlist::slotAddToUpcoming() { m_collection->setUpcomingPlaylistEnabled(true); m_collection->upcomingPlaylist()->appendItems(selectedItems()); } void Playlist::slotShowRMBMenu(const QPoint &point) { QTreeWidgetItem *item = itemAt(point); int column = columnAt(point.x()); if(!item) return; // Create the RMB menu on demand. if(!m_rmbMenu) { // Probably more of these actions should be ported over to using KActions. m_rmbMenu = new QMenu(this); m_rmbMenu->addAction(QIcon::fromTheme("go-jump-today"), i18n("Add to Play Queue"), this, SLOT(slotAddToUpcoming())); m_rmbMenu->addSeparator(); if(!readOnly()) { m_rmbMenu->addAction( action("edit_cut") ); m_rmbMenu->addAction( action("edit_copy") ); m_rmbMenu->addAction( action("edit_paste") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("removeFromPlaylist") ); } else m_rmbMenu->addAction( action("edit_copy") ); m_rmbEdit = m_rmbMenu->addAction(i18n("Edit")); m_rmbMenu->addAction( action("refresh") ); m_rmbMenu->addAction( action("removeItem") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("guessTag") ); m_rmbMenu->addAction( action("renameFile") ); m_rmbMenu->addAction( action("coverManager") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( QIcon::fromTheme("folder-new"), i18n("Create Playlist From Selected Items..."), this, SLOT(slotCreateGroup())); } // Ignore any columns added by subclasses. const int adjColumn = column - columnOffset(); bool showEdit = (adjColumn == PlaylistItem::TrackColumn) || (adjColumn == PlaylistItem::ArtistColumn) || (adjColumn == PlaylistItem::AlbumColumn) || (adjColumn == PlaylistItem::TrackNumberColumn) || (adjColumn == PlaylistItem::GenreColumn) || (adjColumn == PlaylistItem::YearColumn); if(showEdit) { m_rmbEdit->setText(i18n("Edit '%1'", item->text(column))); m_rmbEdit->disconnect(this); connect(m_rmbEdit, &QAction::triggered, this, [this, item, column]() { this->editItem(item, column); }); } m_rmbEdit->setVisible(showEdit); // Disable edit menu if only one file is selected, and it's read-only FileHandle file = static_cast(item)->file(); m_rmbEdit->setEnabled(file.fileInfo().isWritable() || selectedItems().count() > 1); // View cover is based on if there is a cover to see. We should only have // the remove cover option if the cover is in our database (and not directly // embedded in the file, for instance). action("viewCover")->setEnabled(file.coverInfo()->hasCover()); action("removeCover")->setEnabled(file.coverInfo()->coverId() != CoverManager::NoMatch); m_rmbMenu->popup(mapToGlobal(point)); } bool Playlist::editTag(PlaylistItem *item, const QString &text, int column) { Tag *newTag = TagTransactionManager::duplicateTag(item->file().tag()); switch(column - columnOffset()) { case PlaylistItem::TrackColumn: newTag->setTitle(text); break; case PlaylistItem::ArtistColumn: newTag->setArtist(text); break; case PlaylistItem::AlbumColumn: newTag->setAlbum(text); break; case PlaylistItem::TrackNumberColumn: { bool ok; int value = text.toInt(&ok); if(ok) newTag->setTrack(value); break; } case PlaylistItem::GenreColumn: newTag->setGenre(text); break; case PlaylistItem::YearColumn: { bool ok; int value = text.toInt(&ok); if(ok) newTag->setYear(value); break; } } TagTransactionManager::instance()->changeTagOnItem(item, newTag); return true; } void Playlist::slotInlineEditDone(QTreeWidgetItem *item, int column) { // The column we get is as passed from QTreeWidget so it does not need // adjustment to get the right text from the QTreeWidgetItem QString text = item->text(column); const PlaylistItemList l = selectedItems(); // See if any of the files have a tag different from the input. const int adjColumn = column - columnOffset(); bool changed = std::any_of(l.cbegin(), l.cend(), [text, adjColumn] (const PlaylistItem *item) { return item->text(adjColumn) != text; } ); if(!changed || (l.count() > 1 && KMessageBox::warningContinueCancel( 0, i18n("This will edit multiple files. Are you sure?"), QString(), KGuiItem(i18n("Edit")), KStandardGuiItem::cancel(), "DontWarnMultipleTags") == KMessageBox::Cancel)) { return; } for(auto &item : l) { editTag(item, text, column); } TagTransactionManager::instance()->commit(); CollectionList::instance()->playlistItemsChanged(); playlistItemsChanged(); } void Playlist::slotColumnOrderChanged(int, int from, int to) { if(from == 0 || to == 0) { updatePlaying(); m_leftColumn = header()->sectionPosition(0); } SharedSettings::instance()->setColumnOrder(this); } void Playlist::slotToggleColumnVisible(QAction *action) { int column = action->data().toInt(); if(isColumnHidden(column)) { int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset(); int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset(); if(column == fileNameColumn && !isColumnHidden(fullPathColumn)) { hideColumn(fullPathColumn, false); SharedSettings::instance()->toggleColumnVisible(fullPathColumn); } if(column == fullPathColumn && !isColumnHidden(fileNameColumn)) { hideColumn(fileNameColumn, false); SharedSettings::instance()->toggleColumnVisible(fileNameColumn); } } if(!isColumnHidden(column)) hideColumn(column); else showColumn(column); if(column >= columnOffset()) { SharedSettings::instance()->toggleColumnVisible(column - columnOffset()); } } void Playlist::slotCreateGroup() { QString name = m_collection->playlistNameDialog(i18n("Create New Playlist")); if(!name.isEmpty()) new Playlist(m_collection, selectedItems(), name); } void Playlist::notifyUserColumnWidthModeChanged() { KMessageBox::information(this, i18n("Manual column widths have been enabled. You can " "switch back to automatic column sizes in the view " "menu."), i18n("Manual Column Widths Enabled"), "ShowManualColumnWidthInformation"); } void Playlist::columnResized(int column, int, int newSize) { m_widthsDirty = true; m_columnFixedWidths[column] = newSize; } void Playlist::slotInlineCompletionModeChanged(KCompletion::CompletionMode mode) { SharedSettings::instance()->setInlineCompletionMode(mode); } void Playlist::slotPlayCurrent() { QTreeWidgetItemIterator it(this, QTreeWidgetItemIterator::Selected); PlaylistItem *next = static_cast(*it); TrackSequenceManager::instance()->setNextItem(next); action("forward")->trigger(); } void Playlist::slotUpdateTime() { int newTime = 0; QTreeWidgetItemIterator it(this); while(*it) { const auto item = static_cast(*it); ++it; newTime += item->file().tag()->seconds(); } m_time = newTime; } //////////////////////////////////////////////////////////////////////////////// // helper functions //////////////////////////////////////////////////////////////////////////////// QDataStream &operator<<(QDataStream &s, const Playlist &p) { s << p.name(); s << p.fileName(); s << p.files(); return s; } QDataStream &operator>>(QDataStream &s, Playlist &p) { p.read(s); return s; } bool processEvents() { static QElapsedTimer time; if(time.elapsed() > 100) { time.restart(); qApp->processEvents(); return true; } return false; } // vim: set et sw=4 tw=0 sta: diff --git a/playlist.h b/playlist.h index 15ed566c..e08dfa32 100644 --- a/playlist.h +++ b/playlist.h @@ -1,755 +1,755 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2007 Michael Pyne * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #ifndef JUK_PLAYLIST_H #define JUK_PLAYLIST_H #include #include #include #include #include #include #include "covermanager.h" #include "stringhash.h" #include "playlistsearch.h" #include "tagguesser.h" #include "playlistinterface.h" #include "filehandle.h" #include "juk_debug.h" class KActionMenu; class QFileInfo; class QMimeData; class QAction; class WebImageFetcher; class PlaylistItem; class PlaylistCollection; class CollectionListItem; typedef QVector PlaylistItemList; class Playlist : public QTreeWidget, public PlaylistInterface { Q_OBJECT public: explicit Playlist(PlaylistCollection *collection, const QString &name = QString(), const QString &iconName = "audio-midi"); Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const QString &name = QString(), const QString &iconName = "audio-midi"); Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, const QString &iconName = "audio-midi"); /** * This constructor should generally only be used either by the cache * restoration methods or by subclasses that want to handle calls to * PlaylistCollection::setupPlaylist() differently. * * @param extraColumns is used to preallocate columns for subclasses that * need them (since extra columns are assumed to start from 0). extraColumns * should be equal to columnOffset() (we can't use columnOffset until the * ctor has run). */ Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns = 0); virtual ~Playlist(); // The following group of functions implement the PlaylistInterface API. virtual QString name() const override; virtual FileHandle currentFile() const override; virtual int count() const override { return model()->rowCount(); } virtual int time() const override { return m_time; } virtual void playNext() override; virtual void playPrevious() override; virtual void stop() override; /** * Plays the top item of the playlist. */ void playFirst(); /** * Plays the next album in the playlist. Only useful when in album random * play mode. */ void playNextAlbum(); /** * Saves the file to the currently set file name. If there is no filename * currently set, the default behavior is to prompt the user for a file * name. */ virtual void save(); /** * Standard "save as". Prompts the user for a location where to save the * playlist to. */ virtual void saveAs(); /** * Removes \a item from the Playlist, but not from the disk. * * Since the GUI updates after an item is cleared, you should use clearItems() if you have * a list of items to remove, as that will remove the whole batch before updating * other components/GUI to the change. */ virtual void clearItem(PlaylistItem *item); /** * Remove \a items from the playlist and emit a signal indicating * that the number of items in the list has changed. */ virtual void clearItems(const PlaylistItemList &items); /** * Accessor function to return a pointer to the currently playing file. * * @return 0 if no file is playing, otherwise a pointer to the PlaylistItem * of the track that is currently playing. */ static PlaylistItem *playingItem(); /** * All of the (media) files in the list. */ QStringList files() const; /** * Returns a list of all of the items in the playlist. */ virtual PlaylistItemList items(); /** * Returns a list of all of the \e visible items in the playlist. */ PlaylistItemList visibleItems(); /** * Returns a list of the currently selected items. */ PlaylistItemList selectedItems(); /** * Returns properly casted first child item in list. */ PlaylistItem *firstChild() const; /** * Allow duplicate files in the playlist. */ void setAllowDuplicates(bool allow) { m_allowDuplicates = allow; } /** * This is being used as a mini-factory of sorts to make the construction * of PlaylistItems virtual. In this case it allows for the creation of * both PlaylistItems and CollectionListItems. */ virtual PlaylistItem *createItem(const FileHandle &file, QTreeWidgetItem *after = nullptr); /** * This is implemented as a template method to allow subclasses to * instantiate their PlaylistItem subclasses using the same method. */ template ItemType *createItem(const FileHandle &file, QTreeWidgetItem *after = nullptr); virtual void createItems(const PlaylistItemList &siblings, PlaylistItem *after = nullptr); /** * This handles adding files of various types -- music, playlist or directory * files. Music files that are found will be added to this playlist. New * playlist files that are found will result in new playlists being created. * * Note that this should not be used in the case of adding *only* playlist * items since it has the overhead of checking to see if the file is a playlist * or directory first. */ virtual void addFiles(const QStringList &files, PlaylistItem *after = nullptr); /** * Returns the file name associated with this playlist (an m3u file) or * an empty QString if no such file exists. */ QString fileName() const { return m_fileName; } /** * Sets the file name to be associated with this playlist; this file should * have the "m3u" extension. */ void setFileName(const QString &n) { m_fileName = n; } /** * Hides column \a c. If \a updateSearch is true then a signal that the * visible columns have changed will be emitted and things like the search * will be updated. */ void hideColumn(int c, bool updateSearch = true); /** * Shows column \a c. If \a updateSearch is true then a signal that the * visible columns have changed will be emitted and things like the search * will be updated. */ void showColumn(int c, bool updateSearch = true); void sortByColumn(int column, Qt::SortOrder order = Qt::AscendingOrder); /** * This sets a name for the playlist that is \e different from the file name. */ void setName(const QString &n); /** * Returns the KActionMenu that allows this to be embedded in menus outside * of the playlist. */ KActionMenu *columnVisibleAction() const { return m_columnVisibleAction; } /** * Set item to be the playing item. If \a item is null then this will clear * the playing indicator. */ static void setPlaying(PlaylistItem *item, bool addToHistory = true); /** * Returns true if this playlist is currently playing. */ bool playing() const override; /** * This forces an update of the left most visible column, but does not save * the settings for this. */ void updateLeftColumn(); /** * Returns the leftmost visible column of the listview. */ int leftColumn() const { return m_leftColumn; } /** * Sets the items in the list to be either visible based on the value of * visible. This is useful for search operations and such. */ - static void setItemsVisible(const PlaylistItemList &items, bool visible = true); + void setItemsVisible(const QModelIndexList &indexes, 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; } + PlaylistSearch* search() const { return m_search; } /** * Set the search associated with this playlist. */ - void setSearch(const PlaylistSearch &s); + void setSearch(PlaylistSearch* s); /** * If the search is disabled then all items will be shown, not just those that * match the current search. */ void setSearchEnabled(bool searchEnabled); /** * Subclasses of Playlist which add new columns will set this value to * specify how many of those columns exist. This allows the Playlist * class to do some internal calculations on the number and positions * of columns. */ virtual int columnOffset() const { return 0; } /** * Some subclasses of Playlist will be "read only" lists (i.e. the history * playlist). This is a way for those subclasses to indicate that to the * Playlist internals. */ virtual bool readOnly() const { return false; } /** * Returns true if it's possible to reload this playlist. */ virtual bool canReload() const { return !m_fileName.isEmpty(); } /** * Returns true if the playlist is a search playlist and the search should be * editable. */ virtual bool searchIsEditable() const { return false; } /** * Synchronizes the playing item in this playlist with the playing item * in \a sources. If \a setMaster is true, this list will become the source * for determining the next item. */ void synchronizePlayingItems(const PlaylistList &sources, bool setMaster); /** * Playlists have a common set of shared settings such as visible columns * that should be applied just before the playlist is shown. Calling this * method applies those. */ void applySharedSettings(); void read(QDataStream &s); static void setShuttingDown() { m_shuttingDown = true; } void playlistItemsChanged() override; public slots: /** * Remove the currently selected items from the playlist and disk. */ void slotRemoveSelectedItems() { removeFromDisk(selectedItems()); } /* * The edit slots are required to use the canonical names so that they are * detected by the application wide framework. */ virtual void cut() { copy(); clear(); } /** * Puts a list of URLs pointing to the files in the current selection on the * clipboard. */ virtual void copy(); /** * Checks the clipboard for local URLs to be inserted into this playlist. */ virtual void paste(); /** * Removes the selected items from the list, but not the disk. * * @see clearItem() * @see clearItems() */ virtual void clear(); /** * Refreshes the tags of the selection from disk, or all of the files in the * list if there is no selection. */ virtual void slotRefresh(); void slotGuessTagInfo(TagGuesser::Type type); /** * Renames the selected items' files based on their tags contents. * * @see PlaylistItem::renameFile() */ void slotRenameFile(); /** * Sets the cover of the selected items, pass in true if you want to load from the local system, * false if you want to load from the internet. */ void slotAddCover(bool fromLocal); /** * Shows a large image of the cover */ void slotViewCover(); /** * Removes covers from the selected items */ void slotRemoveCover(); /** * Shows the cover manager GUI dialog */ void slotShowCoverManager(); /** * Reload the playlist contents from the m3u file. */ virtual void slotReload(); /** * Tells the listview that the next time that it paints that the weighted * column widths must be recalculated. If this is called without a column * all visible columns are marked as dirty. */ void slotWeightDirty(int column = -1); void slotShowPlaying(); void slotColumnResizeModeChanged(); protected: /** * Remove \a items from the playlist and disk. This will ignore items that * are not actually in the list. */ void removeFromDisk(const PlaylistItemList &items); /** * Adds and removes items from this Playlist as necessary to ensure that * the same items are present in this Playlist as in @p itemList. * * No ordering guarantees are imposed, just that the playlist will have the * same items as in the given list afterwards. */ void synchronizeItemsTo(const PlaylistItemList &itemList); // the following are all reimplemented from base classes virtual bool eventFilter(QObject *watched, QEvent *e) override; virtual void keyPressEvent(QKeyEvent *e) override; QStringList mimeTypes() const override; QMimeData* mimeData(const QList items) const override; virtual bool dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action) override; virtual void dropEvent(QDropEvent *e) override; virtual void dragEnterEvent(QDragEnterEvent *e) override; virtual void showEvent(QShowEvent *e) override; virtual void paintEvent(QPaintEvent *pe) override; virtual void resizeEvent(QResizeEvent *re) override; virtual void drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override; virtual void insertItem(QTreeWidgetItem *item); virtual void takeItem(QTreeWidgetItem *item); virtual bool hasItem(const QString &file) const { return m_members.contains(file); } virtual void addColumn(const QString &label, int width = -1); /** * Do some final initialization of created items. Notably ensure that they * are shown or hidden based on the contents of the current PlaylistSearch. * * This is called by the PlaylistItem constructor. */ void setupItem(PlaylistItem *item); /** * Forwards the call to the parent to enable or disable automatic deletion * of tree view playlists. Used by CollectionListItem. */ void setDynamicListsFrozen(bool frozen); template ItemType *createItem(SiblingType *sibling, ItemType *after = nullptr); /** * As a template this allows us to use the same code to initialize the items * in subclasses. ItemType should be a PlaylistItem subclass. */ template