diff --git a/juk.cpp b/juk.cpp index f8a9213e..01492828 100644 --- a/juk.cpp +++ b/juk.cpp @@ -1,656 +1,658 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2008, 2009, 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 "juk.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 "slideraction.h" #include "statuslabel.h" #include "systemtray.h" #include "keydialog.h" #include "tagguesserconfigdlg.h" #include "filerenamerconfigdlg.h" #include "scrobbler.h" #include "scrobbleconfigdlg.h" #include "actioncollection.h" #include "cache.h" #include "playlistsplitter.h" #include "collectionlist.h" #include "covermanager.h" #include "tagtransactionmanager.h" #include "juk_debug.h" using namespace ActionCollection; JuK* JuK::m_instance; template void deleteAndClear(T *&ptr) { delete ptr; ptr = 0; } //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// JuK::JuK(const QStringList &filesToOpen, QWidget *parent) : KXmlGuiWindow(parent, Qt::WindowFlags(Qt::WA_DeleteOnClose)), m_splitter(nullptr), m_statusLabel(nullptr), m_systemTray(nullptr), m_player(new PlayerManager), m_scrobbler(nullptr), m_filesToOpen(filesToOpen), m_shuttingDown(false), m_pmToken(0) { // Expect segfaults if you change this order. // Allow to be passed across threads qRegisterMetaType(); qRegisterMetaType(); m_instance = this; readSettings(); Cache::ensureAppDataStorageExists(); setupActions(); setupLayout(); bool firstRun = !KSharedConfig::openConfig()->hasGroup("MainWindow"); if(firstRun) { KConfigGroup mainWindowConfig(KSharedConfig::openConfig(), "MainWindow"); KConfigGroup playToolBarConfig(&mainWindowConfig, "Toolbar playToolBar"); playToolBarConfig.writeEntry("ToolButtonStyle", "IconOnly"); } QSize defaultSize(800, 480); if(QApplication::isRightToLeft()) setupGUI(defaultSize, ToolBar | Save | Create, "jukui-rtl.rc"); else setupGUI(defaultSize, ToolBar | Save | Create); // Center the GUI if this is our first run ever. if(firstRun) { QRect r = rect(); - r.moveCenter(QApplication::desktop()->screenGeometry().center()); + const QRect screenCenter = QApplication::primaryScreen()->availableGeometry(); + r.moveCenter(screenCenter.center()); move(r.topLeft()); } connect(m_splitter, SIGNAL(guiReady()), SLOT(slotSetupSystemTray())); readConfig(); setupGlobalAccels(); activateScrobblerIfEnabled(); QDBusInterface *pmInterface = new QDBusInterface(QStringLiteral("org.kde.Solid.PowerManagement"), QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), QDBusConnection::sessionBus()); connect(m_player, &PlayerManager::signalPlay, [=] () { QDBusReply reply; if (pmInterface->isValid() && (m_pmToken == 0)) { reply = pmInterface->call(QStringLiteral("Inhibit"), KAboutData::applicationData().componentName(), QStringLiteral("playing audio")); if (reply.isValid()) { m_pmToken = reply.value(); } } }); auto uninhibitPowerManagement = [=] () { QDBusMessage reply; if (pmInterface->isValid() && (m_pmToken != 0)) { reply = pmInterface->call(QStringLiteral("UnInhibit"), m_pmToken); if (reply.errorName().isEmpty()) { m_pmToken = 0; } } }; connect(m_player, &PlayerManager::signalPause, uninhibitPowerManagement); connect(m_player, &PlayerManager::signalStop, uninhibitPowerManagement); // The system tray quit command will go straight to qApp->quit without calling // our quit action, so make sure we save config changes no matter how quit is // called. connect(qApp, &QCoreApplication::aboutToQuit, this, [this]() { saveConfig(); }); // slotCheckCache loads the cached entries first to populate the collection list QTimer::singleShot(0, this, SLOT(slotClearOldCovers())); QTimer::singleShot(0, CollectionList::instance(), SLOT(startLoadingCachedItems())); QTimer::singleShot(0, this, SLOT(slotProcessArgs())); } JuK::~JuK() { if(!m_shuttingDown) { // Sometimes KMainWindow doesn't actually call QCoreApplication::quit // after queryClose, even if not in a session shutdown, so make sure to // do so ourselves when closing the main window. slotQuit(); } // Some items need to be deleted before others, though I haven't looked // at this in some time so refinement is probably possible. delete m_systemTray; delete m_splitter; delete m_player; delete m_statusLabel; } JuK* JuK::JuKInstance() { return m_instance; } PlayerManager *JuK::playerManager() const { return m_player; } void JuK::coverDownloaded(const QPixmap &cover) { QString event(cover.isNull() ? "coverFailed" : "coverDownloaded"); KNotification *notification = new KNotification(event, this); notification->setPixmap(cover); notification->setFlags(KNotification::CloseOnTimeout); if(cover.isNull()) notification->setText(i18n("Your album art failed to download.")); else notification->setText(i18n("Your album art has finished downloading.")); notification->sendEvent(); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void JuK::setupLayout() { m_splitter = new PlaylistSplitter(m_player, this); setCentralWidget(m_splitter); m_statusLabel = new StatusLabel(*m_splitter->playlist(), statusBar()); statusBar()->addWidget(m_statusLabel, 1); connect(m_player, &PlayerManager::tick, m_statusLabel, &StatusLabel::setItemCurrentTime); connect(m_player, &PlayerManager::totalTimeChanged, m_statusLabel, &StatusLabel::setItemTotalTime); connect(m_splitter, &PlaylistSplitter::currentPlaylistChanged, m_statusLabel, &StatusLabel::slotCurrentPlaylistHasChanged); m_splitter->setFocus(); } void JuK::setupActions() { KActionCollection *collection = ActionCollection::actions(); // Setup KDE standard actions that JuK uses. KStandardAction::quit(this, SLOT(slotQuit()), collection); KStandardAction::undo(this, SLOT(slotUndo()), collection); KStandardAction::cut(collection); KStandardAction::copy(collection); KStandardAction::paste(collection); QAction *clear = KStandardAction::clear(collection); KStandardAction::selectAll(collection); KStandardAction::keyBindings(this, SLOT(slotEditKeys()), collection); KStandardAction::showMenubar(menuBar(), SLOT(setVisible(bool)), collection); // Setup the menu which handles the random play options. KActionMenu *actionMenu = collection->add("actionMenu"); actionMenu->setText(i18n("&Random Play")); actionMenu->setIcon(QIcon::fromTheme( QLatin1String( "media-playlist-shuffle" ))); actionMenu->setDelayed(false); QActionGroup* randomPlayGroup = new QActionGroup(this); QAction *act = collection->add("disableRandomPlay"); act->setText(i18n("&Disable Random Play")); act->setIcon(QIcon::fromTheme( QLatin1String( "go-down" ))); act->setActionGroup(randomPlayGroup); actionMenu->addAction(act); m_randomPlayAction = collection->add("randomPlay"); m_randomPlayAction->setText(i18n("Use &Random Play")); m_randomPlayAction->setIcon(QIcon::fromTheme( QLatin1String( "media-playlist-shuffle" ))); m_randomPlayAction->setActionGroup(randomPlayGroup); actionMenu->addAction(m_randomPlayAction); act = collection->add("albumRandomPlay"); act->setEnabled(false); act->setText(i18n("Use &Album Random Play")); act->setIcon(QIcon::fromTheme( QLatin1String( "media-playlist-shuffle" ))); act->setActionGroup(randomPlayGroup); connect(act, SIGNAL(triggered(bool)), SLOT(slotCheckAlbumNextAction(bool))); actionMenu->addAction(act); act = collection->addAction("removeFromPlaylist", clear, SLOT(clear())); act->setText(i18n("Remove From Playlist")); act->setIcon(QIcon::fromTheme( QLatin1String( "list-remove" ))); act = collection->addAction("play", m_player, SLOT(play())); act->setText(i18n("&Play")); act->setIcon(QIcon::fromTheme( QLatin1String( "media-playback-start" ))); act = collection->addAction("pause", m_player, SLOT(pause())); act->setEnabled(false); act->setText(i18n("P&ause")); act->setIcon(QIcon::fromTheme( QLatin1String( "media-playback-pause" ))); act = collection->addAction("stop", m_player, SLOT(stop())); act->setEnabled(false); act->setText(i18n("&Stop")); act->setIcon(QIcon::fromTheme( QLatin1String( "media-playback-stop" ))); act = new KToolBarPopupAction(QIcon::fromTheme( QLatin1String( "media-skip-backward") ), i18nc("previous track", "Previous" ), collection); act->setEnabled(false); collection->addAction("back", act); connect(act, SIGNAL(triggered(bool)), m_player, SLOT(back())); act = collection->addAction("forward", m_player, SLOT(forward())); act->setEnabled(false); act->setText(i18nc("next track", "&Next")); act->setIcon(QIcon::fromTheme( QLatin1String( "media-skip-forward" ))); act = collection->addAction("loopPlaylist"); act->setText(i18n("&Loop Playlist")); act->setCheckable(true); act = collection->add("resizeColumnsManually"); act->setText(i18n("&Resize Playlist Columns Manually")); // the following are not visible by default act = collection->addAction("mute", m_player, SLOT(mute())); act->setText(i18nc("silence playback", "Mute")); act->setIcon(QIcon::fromTheme( QLatin1String( "audio-volume-muted" ))); act = collection->addAction("volumeUp", m_player, SLOT(volumeUp())); act->setText(i18n("Volume Up")); act->setIcon(QIcon::fromTheme( QLatin1String( "audio-volume-high" ))); act = collection->addAction("volumeDown", m_player, SLOT(volumeDown())); act->setText(i18n("Volume Down")); act->setIcon(QIcon::fromTheme( QLatin1String( "audio-volume-low" ))); act = collection->addAction("playPause", m_player, SLOT(playPause())); act->setText(i18n("Play / Pause")); act->setIcon(QIcon::fromTheme( QLatin1String( "media-playback-start" ))); act = collection->addAction("seekForward", m_player, SLOT(seekForward())); act->setText(i18n("Seek Forward")); act->setIcon(QIcon::fromTheme( QLatin1String( "media-seek-forward" ))); act = collection->addAction("seekBack", m_player, SLOT(seekBack())); act->setText(i18n("Seek Back")); act->setIcon(QIcon::fromTheme( QLatin1String( "media-seek-backward" ))); act = collection->addAction("showHide", this, SLOT(slotShowHide())); act->setText(i18n("Show / Hide")); ////////////////////////////////////////////////// // settings menu ////////////////////////////////////////////////// m_toggleSystemTrayAction = collection->add("toggleSystemTray"); m_toggleSystemTrayAction->setText(i18n("&Dock in System Tray")); connect(m_toggleSystemTrayAction, SIGNAL(triggered(bool)), SLOT(slotToggleSystemTray(bool))); m_toggleDockOnCloseAction = collection->add("dockOnClose"); m_toggleDockOnCloseAction->setText(i18n("&Stay in System Tray on Close")); m_togglePopupsAction = collection->add("togglePopups"); m_togglePopupsAction->setText(i18n("Popup &Track Announcement")); act = collection->add("saveUpcomingTracks"); act->setText(i18n("Save &Play Queue on Exit")); act = collection->addAction("tagGuesserConfig", this, SLOT(slotConfigureTagGuesser())); act->setText(i18n("&Tag Guesser...")); act = collection->addAction("fileRenamerConfig", this, SLOT(slotConfigureFileRenamer())); act->setText(i18n("&File Renamer...")); act = collection->addAction("scrobblerConfig", this, SLOT(slotConfigureScrobbling())); act->setText(i18n("&Configure scrobbling...")); ////////////////////////////////////////////////// // just in the toolbar ////////////////////////////////////////////////// collection->addAction("trackPositionAction", new TrackPositionAction(i18n("Track Position"), this)); collection->addAction("volumeAction", new VolumeAction(i18n("Volume"), this)); ActionCollection::actions()->addAssociatedWidget(this); foreach (QAction* action, ActionCollection::actions()->actions()) action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } void JuK::slotSetupSystemTray() { if(m_toggleSystemTrayAction && m_toggleSystemTrayAction->isChecked()) { m_systemTray = new SystemTray(m_player, this); m_systemTray->setObjectName(QStringLiteral("systemTray")); m_toggleDockOnCloseAction->setEnabled(true); m_togglePopupsAction->setEnabled(true); // If this flag gets set then JuK will quit if you click the cover on // the track announcement popup when JuK is only in the system tray // (the systray has no widget). qGuiApp->setQuitOnLastWindowClosed(false); } else { m_systemTray = nullptr; m_toggleDockOnCloseAction->setEnabled(false); m_togglePopupsAction->setEnabled(false); } } void JuK::setupGlobalAccels() { KeyDialog::setupActionShortcut("play"); KeyDialog::setupActionShortcut("playPause"); KeyDialog::setupActionShortcut("stop"); KeyDialog::setupActionShortcut("back"); KeyDialog::setupActionShortcut("forward"); KeyDialog::setupActionShortcut("seekBack"); KeyDialog::setupActionShortcut("seekForward"); KeyDialog::setupActionShortcut("volumeUp"); KeyDialog::setupActionShortcut("volumeDown"); KeyDialog::setupActionShortcut("mute"); KeyDialog::setupActionShortcut("showHide"); KeyDialog::setupActionShortcut("forwardAlbum"); } void JuK::slotProcessArgs() { CollectionList::instance()->addFiles(m_filesToOpen); } void JuK::slotClearOldCovers() { // Find all saved covers from the previous run of JuK and clear them out, in case // we find our tracks in a different order this run, which would cause old saved // covers to be wrong. // See mpris2/mediaplayer2player.cpp QString tmpDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QStringList nameFilters; nameFilters << QStringLiteral("juk-cover-*.png"); QDirIterator jukCoverIter(tmpDir, nameFilters); while (jukCoverIter.hasNext()) { QFile::remove(jukCoverIter.next()); } } void JuK::keyPressEvent(QKeyEvent *e) { if (e->key() >= Qt::Key_Back && e->key() <= Qt::Key_MediaLast) e->accept(); KXmlGuiWindow::keyPressEvent(e); } /** * These are settings that need to be know before setting up the GUI. */ void JuK::readSettings() { KConfigGroup config(KSharedConfig::openConfig(), "Settings"); m_startDocked = config.readEntry("StartDocked", false); } void JuK::readConfig() { // player settings KConfigGroup playerConfig(KSharedConfig::openConfig(), "Player"); if(m_player) { const int maxVolume = 100; const int volume = playerConfig.readEntry("Volume", maxVolume); m_player->setVolume(volume * 0.01); //bool enableCrossfade = playerConfig.readEntry("CrossfadeTracks", true); //m_player->setCrossfadeEnabled(enableCrossfade); //ActionCollection::action("crossfadeTracks")->setChecked(enableCrossfade); } // Default to no random play ActionCollection::action("disableRandomPlay")->setChecked(true); QString randomPlayMode = playerConfig.readEntry("RandomPlay", "Disabled"); if(randomPlayMode == "true" || randomPlayMode == "Normal") m_randomPlayAction->setChecked(true); else if(randomPlayMode == "AlbumRandomPlay") ActionCollection::action("albumRandomPlay")->setChecked(true); bool loopPlaylist = playerConfig.readEntry("LoopPlaylist", false); ActionCollection::action("loopPlaylist")->setChecked(loopPlaylist); // general settings KConfigGroup settingsConfig(KSharedConfig::openConfig(), "Settings"); bool dockInSystemTray = settingsConfig.readEntry("DockInSystemTray", true); m_toggleSystemTrayAction->setChecked(dockInSystemTray); bool dockOnClose = settingsConfig.readEntry("DockOnClose", true); m_toggleDockOnCloseAction->setChecked(dockOnClose); bool showPopups = settingsConfig.readEntry("TrackPopup", false); m_togglePopupsAction->setChecked(showPopups); } void JuK::saveConfig() { // player settings KConfigGroup playerConfig(KSharedConfig::openConfig(), "Player"); if (m_player) { playerConfig.writeEntry("Volume", static_cast(100.0 * m_player->volume())); } playerConfig.writeEntry("RandomPlay", m_randomPlayAction->isChecked()); QAction *a = ActionCollection::action("loopPlaylist"); playerConfig.writeEntry("LoopPlaylist", a->isChecked()); playerConfig.writeEntry("CrossfadeTracks", false); // TODO bring back a = ActionCollection::action("albumRandomPlay"); if(a->isChecked()) playerConfig.writeEntry("RandomPlay", "AlbumRandomPlay"); else if(m_randomPlayAction->isChecked()) playerConfig.writeEntry("RandomPlay", "Normal"); else playerConfig.writeEntry("RandomPlay", "Disabled"); // general settings KConfigGroup settingsConfig(KSharedConfig::openConfig(), "Settings"); settingsConfig.writeEntry("StartDocked", m_startDocked); settingsConfig.writeEntry("DockInSystemTray", m_toggleSystemTrayAction->isChecked()); settingsConfig.writeEntry("DockOnClose", m_toggleDockOnCloseAction->isChecked()); settingsConfig.writeEntry("TrackPopup", m_togglePopupsAction->isChecked()); KSharedConfig::openConfig()->sync(); } bool JuK::queryClose() { if(!m_shuttingDown && !qApp->isSavingSession() && m_systemTray && m_toggleDockOnCloseAction->isChecked()) { KMessageBox::information(this, i18n("Closing the main window will keep JuK running in the system tray. " "Use Quit from the File menu to quit the application."), i18n("Docking in System Tray"), "hideOnCloseInfo"); hide(); return false; } return true; } //////////////////////////////////////////////////////////////////////////////// // private slot definitions //////////////////////////////////////////////////////////////////////////////// void JuK::slotShowHide() { setHidden(!isHidden()); } void JuK::slotQuit() { m_shuttingDown = true; // Some phonon backends will crash on shutdown unless we've stopped // playback. if(m_player->playing()) { m_player->stop(); } m_startDocked = !isVisible(); saveConfig(); QTimer::singleShot(0, qApp, &QCoreApplication::quit); } //////////////////////////////////////////////////////////////////////////////// // settings menu //////////////////////////////////////////////////////////////////////////////// void JuK::slotToggleSystemTray(bool enabled) { if(enabled && !m_systemTray) slotSetupSystemTray(); else if(!enabled && m_systemTray) { delete m_systemTray; m_systemTray = nullptr; m_toggleDockOnCloseAction->setEnabled(false); m_togglePopupsAction->setEnabled(false); qGuiApp->setQuitOnLastWindowClosed(true); } } void JuK::slotEditKeys() { KeyDialog(ActionCollection::actions(), this).configure(); } void JuK::slotConfigureTagGuesser() { TagGuesserConfigDlg(this).exec(); } void JuK::slotConfigureFileRenamer() { FileRenamerConfigDlg(this).exec(); } void JuK::slotConfigureScrobbling() { ScrobbleConfigDlg(this).exec(); activateScrobblerIfEnabled(); } void JuK::activateScrobblerIfEnabled() { bool isScrobbling = Scrobbler::isScrobblingEnabled(); if (!m_scrobbler && isScrobbling) { m_scrobbler = new Scrobbler(this); connect (m_player, SIGNAL(signalItemChanged(FileHandle)), m_scrobbler, SLOT(nowPlaying(FileHandle))); } else if (m_scrobbler && !isScrobbling) { delete m_scrobbler; m_scrobbler = 0; } } void JuK::slotUndo() { TagTransactionManager::instance()->undo(); } void JuK::slotCheckAlbumNextAction(bool albumRandomEnabled) { // If album random play is enabled, then enable the Play Next Album action // unless we're not playing right now. if(albumRandomEnabled && !m_player->playing()) albumRandomEnabled = false; action("forwardAlbum")->setEnabled(albumRandomEnabled); } // vim: set et sw=4 tw=0 sta: diff --git a/tageditor.cpp b/tageditor.cpp index cfdffb5f..daec5575 100644 --- a/tageditor.cpp +++ b/tageditor.cpp @@ -1,655 +1,658 @@ /** * 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 "tageditor.h" #include "collectionlist.h" #include "playlistitem.h" #include "tag.h" #include "actioncollection.h" #include "tagtransactionmanager.h" #include "juk_debug.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 #undef KeyRelease class FileNameValidator final : public QValidator { public: FileNameValidator(QObject *parent, const char *name = 0) : QValidator(parent) { setObjectName( QLatin1String( name ) ); } virtual void fixup(QString &s) const override { s.remove('/'); } virtual State validate(QString &s, int &) const override { if(s.contains('/')) return Invalid; return Acceptable; } }; class FixedHLayout final : public QHBoxLayout { public: FixedHLayout(QWidget *parent, int margin = 0, int spacing = -1, const char *name = 0) : QHBoxLayout(parent), m_width(-1) { setMargin(margin); setSpacing(spacing); setObjectName(QLatin1String(name)); } FixedHLayout(QLayout *parentLayout, int spacing = -1, const char *name = 0) : QHBoxLayout(), m_width(-1) { parentLayout->addItem(this); setSpacing(spacing); setObjectName(QLatin1String(name)); } void setWidth(int w = -1) { m_width = w == -1 ? QHBoxLayout::minimumSize().width() : w; } virtual QSize minimumSize() const override { QSize s = QHBoxLayout::minimumSize(); s.setWidth(m_width); return s; } private: int m_width; }; class CollectionObserver final : public PlaylistObserver { public: CollectionObserver(TagEditor *parent) : PlaylistObserver(CollectionList::instance()), m_parent(parent) { } virtual void playlistItemDataHasChanged() override { if(m_parent && m_parent->m_currentPlaylist && m_parent->isVisible()) m_parent->slotSetItems(m_parent->m_currentPlaylist->selectedItems()); } private: TagEditor *m_parent; }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// TagEditor::TagEditor(QWidget *parent) : QWidget(parent), m_currentPlaylist(0), m_observer(0), m_performingSave(false) { setupActions(); setupLayout(); readConfig(); m_dataChanged = false; m_collectionChanged = false; setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); } TagEditor::~TagEditor() { delete m_observer; saveConfig(); } void TagEditor::setupObservers() { m_observer = new CollectionObserver(this); } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void TagEditor::slotSetItems(const PlaylistItemList &list) { if(m_performingSave) return; // Store the playlist that we're setting because saveChangesPrompt // can delete the PlaylistItems in list. Playlist *itemPlaylist = 0; if(!list.isEmpty()) itemPlaylist = list.first()->playlist(); bool hadPlaylist = m_currentPlaylist != 0; saveChangesPrompt(); if(m_currentPlaylist) { disconnect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem*)), this, SLOT(slotItemRemoved(PlaylistItem*))); } if((hadPlaylist && !m_currentPlaylist) || !itemPlaylist) { m_currentPlaylist = 0; m_items.clear(); } else { m_currentPlaylist = itemPlaylist; // We can't use list here, it may not be valid m_items = itemPlaylist->selectedItems(); } if(m_currentPlaylist) { connect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem*)), this, SLOT(slotItemRemoved(PlaylistItem*))); connect(m_currentPlaylist, SIGNAL(destroyed()), this, SLOT(slotPlaylistRemoved())); } if(isVisible()) slotRefresh(); else m_collectionChanged = true; } void TagEditor::slotRefresh() { // This method takes the list of currently selected m_items and tries to // figure out how to show that in the tag editor. The current strategy -- // the most common case -- is to just process the first item. Then we // check after that to see if there are other m_items and adjust accordingly. if(m_items.isEmpty() || !m_items.first()->file().tag()) { slotClear(); setEnabled(false); return; } setEnabled(true); PlaylistItem *item = m_items.first(); Q_ASSERT(item); Tag *tag = item->file().tag(); QFileInfo fi(item->file().absFilePath()); if(!fi.isWritable() && m_items.count() == 1) setEnabled(false); artistNameBox->setEditText(tag->artist()); trackNameBox->setText(tag->title()); albumNameBox->setEditText(tag->album()); fileNameBox->setText(item->file().fileInfo().fileName()); fileNameBox->setToolTip(item->file().absFilePath()); bitrateBox->setText(QString::number(tag->bitrate())); lengthBox->setText(tag->lengthString()); if(m_genreList.indexOf(tag->genre()) >= 0) genreBox->setCurrentIndex(m_genreList.indexOf(tag->genre()) + 1); else { genreBox->setCurrentIndex(0); genreBox->setEditText(tag->genre()); } trackSpin->setValue(tag->track()); yearSpin->setValue(tag->year()); commentBox->setPlainText(tag->comment()); // Start at the second item, since we've already processed the first. PlaylistItemList::Iterator it = m_items.begin(); ++it; // If there is more than one item in the m_items that we're dealing with... QList disabledForMulti; disabledForMulti << fileNameLabel << fileNameBox << lengthLabel << lengthBox << bitrateLabel << bitrateBox; foreach(QWidget *w, disabledForMulti) { w->setDisabled(m_items.size() > 1); if(m_items.size() > 1 && !w->inherits("QLabel")) QMetaObject::invokeMethod(w, "clear"); } if(it != m_items.end()) { foreach(QCheckBox *box, m_enableBoxes) { box->setChecked(true); box->show(); } // Yep, this is ugly. Loop through all of the files checking to see // if their fields are the same. If so, by default, enable their // checkbox. // Also, if there are more than 50 m_items, don't scan all of them. if(m_items.count() > 50) { m_enableBoxes[artistNameBox]->setChecked(false); m_enableBoxes[trackNameBox]->setChecked(false); m_enableBoxes[albumNameBox]->setChecked(false); m_enableBoxes[genreBox]->setChecked(false); m_enableBoxes[trackSpin]->setChecked(false); m_enableBoxes[yearSpin]->setChecked(false); m_enableBoxes[commentBox]->setChecked(false); } else { for(; it != m_items.end(); ++it) { tag = (*it)->file().tag(); if(tag) { if(artistNameBox->currentText() != tag->artist() && m_enableBoxes.contains(artistNameBox)) { artistNameBox->lineEdit()->clear(); m_enableBoxes[artistNameBox]->setChecked(false); } if(trackNameBox->text() != tag->title() && m_enableBoxes.contains(trackNameBox)) { trackNameBox->clear(); m_enableBoxes[trackNameBox]->setChecked(false); } if(albumNameBox->currentText() != tag->album() && m_enableBoxes.contains(albumNameBox)) { albumNameBox->lineEdit()->clear(); m_enableBoxes[albumNameBox]->setChecked(false); } if(genreBox->currentText() != tag->genre() && m_enableBoxes.contains(genreBox)) { genreBox->lineEdit()->clear(); m_enableBoxes[genreBox]->setChecked(false); } if(trackSpin->value() != tag->track() && m_enableBoxes.contains(trackSpin)) { trackSpin->setValue(0); m_enableBoxes[trackSpin]->setChecked(false); } if(yearSpin->value() != tag->year() && m_enableBoxes.contains(yearSpin)) { yearSpin->setValue(0); m_enableBoxes[yearSpin]->setChecked(false); } if(commentBox->toPlainText() != tag->comment() && m_enableBoxes.contains(commentBox)) { commentBox->clear(); m_enableBoxes[commentBox]->setChecked(false); } } } } } else { foreach(QCheckBox *box, m_enableBoxes) { box->setChecked(true); box->hide(); } } m_dataChanged = false; } void TagEditor::slotClear() { artistNameBox->lineEdit()->clear(); trackNameBox->clear(); albumNameBox->lineEdit()->clear(); genreBox->setCurrentIndex(0); fileNameBox->clear(); fileNameBox->setToolTip(QString()); trackSpin->setValue(0); yearSpin->setValue(0); lengthBox->clear(); bitrateBox->clear(); commentBox->clear(); } void TagEditor::slotUpdateCollection() { if(isVisible()) updateCollection(); else m_collectionChanged = true; } void TagEditor::updateCollection() { m_collectionChanged = false; CollectionList *list = CollectionList::instance(); if(!list) return; QStringList artistList = list->uniqueSet(CollectionList::Artists); artistList.sort(); artistNameBox->clear(); artistNameBox->addItems(artistList); artistNameBox->completionObject()->setItems(artistList); QStringList albumList = list->uniqueSet(CollectionList::Albums); albumList.sort(); albumNameBox->clear(); albumNameBox->addItems(albumList); albumNameBox->completionObject()->setItems(albumList); // Merge the list of genres found in tags with the standard ID3v1 set. StringHash genreHash; m_genreList = list->uniqueSet(CollectionList::Genres); foreach(const QString &genre, m_genreList) genreHash.insert(genre); TagLib::StringList genres = TagLib::ID3v1::genreList(); for(TagLib::StringList::Iterator it = genres.begin(); it != genres.end(); ++it) genreHash.insert(TStringToQString((*it))); m_genreList = genreHash.values(); m_genreList.sort(); genreBox->clear(); genreBox->addItem(QString()); genreBox->addItems(m_genreList); genreBox->completionObject()->setItems(m_genreList); // We've cleared out the original entries of these list boxes, re-read // the current item if one is selected. slotRefresh(); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void TagEditor::readConfig() { // combo box completion modes KConfigGroup config(KSharedConfig::openConfig(), "TagEditor"); if(artistNameBox && albumNameBox) { readCompletionMode(config, artistNameBox, "ArtistNameBoxMode"); readCompletionMode(config, albumNameBox, "AlbumNameBoxMode"); readCompletionMode(config, genreBox, "GenreBoxMode"); } bool show = config.readEntry("Show", false); ActionCollection::action("showEditor")->setChecked(show); setVisible(show); TagLib::StringList genres = TagLib::ID3v1::genreList(); for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) m_genreList.append(TStringToQString((*it))); m_genreList.sort(); genreBox->clear(); genreBox->addItem(QString()); genreBox->addItems(m_genreList); genreBox->completionObject()->setItems(m_genreList); } void TagEditor::readCompletionMode(const KConfigGroup &config, KComboBox *box, const QString &key) { KCompletion::CompletionMode mode = KCompletion::CompletionMode(config.readEntry(key, (int)KCompletion::CompletionAuto)); box->setCompletionMode(mode); } void TagEditor::saveConfig() { // combo box completion modes KConfigGroup config(KSharedConfig::openConfig(), "TagEditor"); if(artistNameBox && albumNameBox) { config.writeEntry("ArtistNameBoxMode", (int)artistNameBox->completionMode()); config.writeEntry("AlbumNameBoxMode", (int)albumNameBox->completionMode()); config.writeEntry("GenreBoxMode", (int)genreBox->completionMode()); } config.writeEntry("Show", ActionCollection::action("showEditor")->isChecked()); } void TagEditor::setupActions() { KToggleAction *show = new KToggleAction(QIcon::fromTheme(QLatin1String("document-properties")), i18n("Show &Tag Editor"), this); ActionCollection::actions()->addAction("showEditor", show); connect(show, &QAction::toggled, this, &TagEditor::setVisible); QAction *act = new QAction(QIcon::fromTheme(QLatin1String( "document-save")), i18n("&Save"), this); ActionCollection::actions()->addAction("saveItem", act); ActionCollection::actions()->setDefaultShortcut(act, QKeySequence(Qt::CTRL + Qt::Key_T)); connect(act, &QAction::triggered, this, &TagEditor::slotSave); } void TagEditor::setupLayout() { setupUi(this); // Do some meta-programming to find the matching enable boxes - for(auto enable : findChildren(QRegExp("Enable$"))) { + const auto enableCheckBoxes = findChildren(QRegularExpression("Enable$")); + for(auto enable : enableCheckBoxes) { enable->hide(); // These are shown only when multiple items are being edited // Each enable checkbox is identified by having its objectName end in "Enable". // The corresponding widget to be adjusted is identified by assigning a custom // property in Qt Designer "associatedObjectName", the value of which is the name // for the widget to be enabled (or not). auto associatedVariantValue = enable->property("associatedObjectName"); Q_ASSERT(associatedVariantValue.isValid()); QWidget *associatedWidget = findChild(associatedVariantValue.toString()); Q_ASSERT(associatedWidget != nullptr); m_enableBoxes[associatedWidget] = enable; } // Make sure that the labels are as tall as the enable boxes so that the // layout doesn't jump around as the enable boxes are shown/hidden. - for(auto label : findChildren()) { + const auto editorLabels = findChildren(); + for(auto label : editorLabels) { if(m_enableBoxes.contains(label->buddy())) label->setMinimumHeight(m_enableBoxes[label->buddy()]->height()); } tagEditorLayout->setColumnMinimumWidth(1, 200); } void TagEditor::save(const PlaylistItemList &list) { if(!list.isEmpty() && m_dataChanged) { QApplication::setOverrideCursor(Qt::WaitCursor); m_dataChanged = false; m_performingSave = true; // The list variable can become corrupted if the playlist holding its // items dies, which is possible as we edit tags. So we need to copy // the end marker. PlaylistItemList::ConstIterator end = list.end(); for(PlaylistItemList::ConstIterator it = list.begin(); it != end; /* Deliberately missing */ ) { // Process items before we being modifying tags, as the dynamic // playlists will try to modify the file we edit if the tag changes // due to our alterations here. qApp->processEvents(QEventLoop::ExcludeUserInputEvents); PlaylistItem *item = *it; // The playlist can be deleted from under us if this is the last // item and we edit it so that it doesn't match the search, which // means we can't increment the iterator, so let's do it now. ++it; QString fileName = item->file().fileInfo().path() + QDir::separator() + fileNameBox->text(); if(list.count() > 1) fileName = item->file().fileInfo().absoluteFilePath(); Tag *tag = TagTransactionManager::duplicateTag(item->file().tag(), fileName); // A bit more ugliness. If there are multiple files that are // being modified, they each have a "enabled" checkbox that // says if that field is to be respected for the multiple // files. We have to check to see if that is enabled before // each field that we write. if(m_enableBoxes[artistNameBox]->isChecked()) tag->setArtist(artistNameBox->currentText()); if(m_enableBoxes[trackNameBox]->isChecked()) tag->setTitle(trackNameBox->text()); if(m_enableBoxes[albumNameBox]->isChecked()) tag->setAlbum(albumNameBox->currentText()); if(m_enableBoxes[trackSpin]->isChecked()) { if(trackSpin->text().isEmpty()) trackSpin->setValue(0); tag->setTrack(trackSpin->value()); } if(m_enableBoxes[yearSpin]->isChecked()) { if(yearSpin->text().isEmpty()) yearSpin->setValue(0); tag->setYear(yearSpin->value()); } if(m_enableBoxes[commentBox]->isChecked()) tag->setComment(commentBox->toPlainText()); if(m_enableBoxes[genreBox]->isChecked()) tag->setGenre(genreBox->currentText()); TagTransactionManager::instance()->changeTagOnItem(item, tag); } TagTransactionManager::instance()->commit(); CollectionList::instance()->playlistItemsChanged(); m_performingSave = false; QApplication::restoreOverrideCursor(); } } void TagEditor::saveChangesPrompt() { if(!isVisible() || !m_dataChanged || m_items.isEmpty()) return; QStringList files; foreach(const PlaylistItem *item, m_items) files.append(item->file().absFilePath()); if(KMessageBox::questionYesNoList(this, i18n("Do you want to save your changes to:\n"), files, i18n("Save Changes"), KStandardGuiItem::save(), KStandardGuiItem::discard(), "tagEditor_showSaveChangesBox") == KMessageBox::Yes) { save(m_items); } } void TagEditor::showEvent(QShowEvent *e) { if(m_collectionChanged) { updateCollection(); } QWidget::showEvent(e); } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void TagEditor::slotDataChanged() { m_dataChanged = true; } void TagEditor::slotItemRemoved(PlaylistItem *item) { m_items.removeAll(item); if(m_items.isEmpty()) slotRefresh(); } void TagEditor::slotPlaylistDestroyed(Playlist *p) { if(m_currentPlaylist == p) { m_currentPlaylist = 0; slotSetItems(PlaylistItemList()); } } // vim: set et sw=4 tw=0 sta: