summaryrefslogtreecommitdiffstats
path: root/juk/playlist.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'juk/playlist.cpp')
-rw-r--r--juk/playlist.cpp2361
1 files changed, 2361 insertions, 0 deletions
diff --git a/juk/playlist.cpp b/juk/playlist.cpp
new file mode 100644
index 00000000..b090dca2
--- /dev/null
+++ b/juk/playlist.cpp
@@ -0,0 +1,2361 @@
+/***************************************************************************
+ begin : Sat Feb 16 2002
+ copyright : (C) 2002 - 2004 by Scott Wheeler
+ email : wheeler@kde.org
+***************************************************************************/
+
+/***************************************************************************
+ * *
+ * 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. *
+ * *
+ ***************************************************************************/
+
+#include <kconfig.h>
+#include <kmessagebox.h>
+#include <kurldrag.h>
+#include <kiconloader.h>
+#include <klineedit.h>
+#include <kaction.h>
+#include <kpopupmenu.h>
+#include <klocale.h>
+#include <kdebug.h>
+#include <kinputdialog.h>
+#include <kfiledialog.h>
+#include <kglobalsettings.h>
+#include <kurl.h>
+#include <kio/netaccess.h>
+#include <kio/job.h>
+#include <dcopclient.h>
+
+#include <qheader.h>
+#include <qcursor.h>
+#include <qdir.h>
+#include <qeventloop.h>
+#include <qtooltip.h>
+#include <qwidgetstack.h>
+#include <qfile.h>
+#include <qhbox.h>
+
+#include <id3v1genres.h>
+
+#include <time.h>
+#include <math.h>
+#include <dirent.h>
+
+#include "playlist.h"
+#include "playlistitem.h"
+#include "playlistcollection.h"
+#include "playlistsearch.h"
+#include "mediafiles.h"
+#include "collectionlist.h"
+#include "filerenamer.h"
+#include "actioncollection.h"
+#include "tracksequencemanager.h"
+#include "juk.h"
+#include "tag.h"
+#include "k3bexporter.h"
+#include "upcomingplaylist.h"
+#include "deletedialog.h"
+#include "webimagefetcher.h"
+#include "coverinfo.h"
+#include "coverdialog.h"
+#include "tagtransactionmanager.h"
+#include "cache.h"
+
+using namespace ActionCollection;
+
+/**
+ * Just a shortcut of sorts.
+ */
+
+static bool manualResize()
+{
+ return action<KToggleAction>("resizeColumnsManually")->isChecked();
+}
+
+/**
+ * A tooltip specialized to show full filenames over the file name column.
+ */
+
+class PlaylistToolTip : public QToolTip
+{
+public:
+ PlaylistToolTip(QWidget *parent, Playlist *playlist) :
+ QToolTip(parent), m_playlist(playlist) {}
+
+ virtual void maybeTip(const QPoint &p)
+ {
+ PlaylistItem *item = static_cast<PlaylistItem *>(m_playlist->itemAt(p));
+
+ if(!item)
+ return;
+
+ QPoint contentsPosition = m_playlist->viewportToContents(p);
+
+ int column = m_playlist->header()->sectionAt(contentsPosition.x());
+
+ if(column == m_playlist->columnOffset() + PlaylistItem::FileNameColumn ||
+ item->cachedWidths()[column] > m_playlist->columnWidth(column) ||
+ (column == m_playlist->columnOffset() + PlaylistItem::CoverColumn &&
+ item->file().coverInfo()->hasCover()))
+ {
+ QRect r = m_playlist->itemRect(item);
+ int headerPosition = m_playlist->header()->sectionPos(column);
+ r.setLeft(headerPosition);
+ r.setRight(headerPosition + m_playlist->header()->sectionSize(column));
+
+ if(column == m_playlist->columnOffset() + PlaylistItem::FileNameColumn)
+ tip(r, item->file().absFilePath());
+ else if(column == m_playlist->columnOffset() + PlaylistItem::CoverColumn) {
+ QMimeSourceFactory *f = QMimeSourceFactory::defaultFactory();
+ f->setImage("coverThumb",
+ QImage(item->file().coverInfo()->pixmap(CoverInfo::Thumbnail).convertToImage()));
+ tip(r, "<center><img source=\"coverThumb\"/></center>");
+ }
+ else
+ tip(r, item->text(column));
+ }
+ }
+
+private:
+ Playlist *m_playlist;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Playlist::SharedSettings definition
+////////////////////////////////////////////////////////////////////////////////
+
+bool Playlist::m_visibleChanged = false;
+bool Playlist::m_shuttingDown = false;
+
+/**
+ * Shared settings between the playlists.
+ */
+
+class Playlist::SharedSettings
+{
+public:
+ static SharedSettings *instance();
+ /**
+ * Sets the default column order to that of Playlist @param p.
+ */
+ void setColumnOrder(const Playlist *l);
+ void toggleColumnVisible(int column);
+ void setInlineCompletionMode(KGlobalSettings::Completion mode);
+
+ /**
+ * Apply the settings.
+ */
+ void apply(Playlist *l) const;
+ void sync() { writeConfig(); }
+
+protected:
+ SharedSettings();
+ ~SharedSettings() {}
+
+private:
+ void writeConfig();
+
+ static SharedSettings *m_instance;
+ QValueList<int> m_columnOrder;
+ QValueVector<bool> m_columnsVisible;
+ KGlobalSettings::Completion m_inlineCompletion;
+};
+
+Playlist::SharedSettings *Playlist::SharedSettings::m_instance = 0;
+
+////////////////////////////////////////////////////////////////////////////////
+// Playlist::SharedSettings public members
+////////////////////////////////////////////////////////////////////////////////
+
+Playlist::SharedSettings *Playlist::SharedSettings::instance()
+{
+ static SharedSettings settings;
+ return &settings;
+}
+
+void Playlist::SharedSettings::setColumnOrder(const Playlist *l)
+{
+ if(!l)
+ return;
+
+ m_columnOrder.clear();
+
+ for(int i = l->columnOffset(); i < l->columns(); ++i)
+ m_columnOrder.append(l->header()->mapToIndex(i));
+
+ writeConfig();
+}
+
+void Playlist::SharedSettings::toggleColumnVisible(int column)
+{
+ if(column >= int(m_columnsVisible.size()))
+ m_columnsVisible.resize(column + 1, true);
+
+ m_columnsVisible[column] = !m_columnsVisible[column];
+
+ writeConfig();
+}
+
+void Playlist::SharedSettings::setInlineCompletionMode(KGlobalSettings::Completion mode)
+{
+ m_inlineCompletion = mode;
+ writeConfig();
+}
+
+
+void Playlist::SharedSettings::apply(Playlist *l) const
+{
+ if(!l)
+ return;
+
+ int offset = l->columnOffset();
+ int i = 0;
+ for(QValueListConstIterator<int> it = m_columnOrder.begin(); it != m_columnOrder.end(); ++it)
+ l->header()->moveSection(i++ + offset, *it + offset);
+
+ for(uint i = 0; i < m_columnsVisible.size(); i++) {
+ if(m_columnsVisible[i] && !l->isColumnVisible(i + offset))
+ l->showColumn(i + offset, false);
+ else if(!m_columnsVisible[i] && l->isColumnVisible(i + offset))
+ l->hideColumn(i + offset, false);
+ }
+
+ l->updateLeftColumn();
+ l->renameLineEdit()->setCompletionMode(m_inlineCompletion);
+ l->slotColumnResizeModeChanged();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Playlist::ShareSettings protected members
+////////////////////////////////////////////////////////////////////////////////
+
+Playlist::SharedSettings::SharedSettings()
+{
+ KConfigGroup config(KGlobal::config(), "PlaylistShared");
+
+ bool resizeColumnsManually = config.readBoolEntry("ResizeColumnsManually", false);
+ action<KToggleAction>("resizeColumnsManually")->setChecked(resizeColumnsManually);
+
+ // save column order
+ m_columnOrder = config.readIntListEntry("ColumnOrder");
+
+ QValueList<int> l = config.readIntListEntry("VisibleColumns");
+
+ if(l.isEmpty()) {
+
+ // Provide some default values for column visibility if none were
+ // read from the configuration file.
+
+ for(int i = 0; i <= PlaylistItem::lastColumn(); i++) {
+ switch(i) {
+ case PlaylistItem::BitrateColumn:
+ case PlaylistItem::CommentColumn:
+ case PlaylistItem::FileNameColumn:
+ case PlaylistItem::FullPathColumn:
+ m_columnsVisible.append(false);
+ break;
+ default:
+ m_columnsVisible.append(true);
+ }
+ }
+ }
+ else {
+ // Convert the int list into a bool list.
+
+ m_columnsVisible.resize(l.size(), true);
+ uint i = 0;
+ for(QValueList<int>::Iterator it = l.begin(); it != l.end(); ++it) {
+ if(! bool(*it))
+ m_columnsVisible[i] = bool(*it);
+ i++;
+ }
+ }
+
+ m_inlineCompletion = KGlobalSettings::Completion(
+ config.readNumEntry("InlineCompletionMode", KGlobalSettings::CompletionAuto));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Playlist::SharedSettings private members
+////////////////////////////////////////////////////////////////////////////////
+
+void Playlist::SharedSettings::writeConfig()
+{
+ KConfigGroup config(KGlobal::config(), "PlaylistShared");
+ config.writeEntry("ColumnOrder", m_columnOrder);
+
+ QValueList<int> l;
+ for(uint i = 0; i < m_columnsVisible.size(); i++)
+ l.append(int(m_columnsVisible[i]));
+
+ config.writeEntry("VisibleColumns", l);
+ config.writeEntry("InlineCompletionMode", m_inlineCompletion);
+
+ config.writeEntry("ResizeColumnsManually", manualResize());
+
+ KGlobal::config()->sync();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// public members
+////////////////////////////////////////////////////////////////////////////////
+
+PlaylistItemList Playlist::m_history;
+QMap<int, PlaylistItem *> Playlist::m_backMenuItems;
+int Playlist::m_leftColumn = 0;
+
+Playlist::Playlist(PlaylistCollection *collection, const QString &name,
+ const QString &iconName) :
+ KListView(collection->playlistStack(), name.latin1()),
+ m_collection(collection),
+ m_fetcher(new WebImageFetcher(this)),
+ m_selectedCount(0),
+ m_allowDuplicates(false),
+ m_polished(false),
+ m_applySharedSettings(true),
+ m_columnWidthModeChanged(false),
+ m_disableColumnWidthUpdates(true),
+ m_time(0),
+ m_widthsDirty(true),
+ m_searchEnabled(true),
+ m_lastSelected(0),
+ m_playlistName(name),
+ m_rmbMenu(0),
+ m_toolTip(0),
+ m_blockDataChanged(false)
+{
+ setup();
+ collection->setupPlaylist(this, iconName);
+}
+
+Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items,
+ const QString &name, const QString &iconName) :
+ KListView(collection->playlistStack(), name.latin1()),
+ m_collection(collection),
+ m_fetcher(new WebImageFetcher(this)),
+ m_selectedCount(0),
+ m_allowDuplicates(false),
+ m_polished(false),
+ m_applySharedSettings(true),
+ m_columnWidthModeChanged(false),
+ m_disableColumnWidthUpdates(true),
+ m_time(0),
+ m_widthsDirty(true),
+ m_searchEnabled(true),
+ m_lastSelected(0),
+ m_playlistName(name),
+ m_rmbMenu(0),
+ m_toolTip(0),
+ m_blockDataChanged(false)
+{
+ setup();
+ collection->setupPlaylist(this, iconName);
+ createItems(items);
+}
+
+Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile,
+ const QString &iconName) :
+ KListView(collection->playlistStack()),
+ m_collection(collection),
+ m_fetcher(new WebImageFetcher(this)),
+ m_selectedCount(0),
+ m_allowDuplicates(false),
+ m_polished(false),
+ m_applySharedSettings(true),
+ m_columnWidthModeChanged(false),
+ m_disableColumnWidthUpdates(true),
+ m_time(0),
+ m_widthsDirty(true),
+ m_searchEnabled(true),
+ m_lastSelected(0),
+ m_fileName(playlistFile.absFilePath()),
+ m_rmbMenu(0),
+ m_toolTip(0),
+ m_blockDataChanged(false)
+{
+ setup();
+ loadFile(m_fileName, playlistFile);
+ collection->setupPlaylist(this, iconName);
+}
+
+Playlist::Playlist(PlaylistCollection *collection, bool delaySetup) :
+ KListView(collection->playlistStack()),
+ m_collection(collection),
+ m_fetcher(new WebImageFetcher(this)),
+ m_selectedCount(0),
+ m_allowDuplicates(false),
+ m_polished(false),
+ m_applySharedSettings(true),
+ m_columnWidthModeChanged(false),
+ m_disableColumnWidthUpdates(true),
+ m_time(0),
+ m_widthsDirty(true),
+ m_searchEnabled(true),
+ m_lastSelected(0),
+ m_rmbMenu(0),
+ m_toolTip(0),
+ m_blockDataChanged(false)
+{
+ setup();
+
+ if(!delaySetup)
+ collection->setupPlaylist(this, "midi");
+}
+
+Playlist::~Playlist()
+{
+ // clearItem() will take care of removing the items from the history,
+ // so call clearItems() to make sure it happens.
+
+ clearItems(items());
+
+ delete m_toolTip;
+
+ // Select a different playlist if we're the selected one
+
+ if(isVisible() && this != CollectionList::instance())
+ m_collection->raise(CollectionList::instance());
+
+ if(!m_shuttingDown)
+ m_collection->removePlaylist(this);
+}
+
+QString Playlist::name() const
+{
+ if(m_playlistName.isNull())
+ return m_fileName.section(QDir::separator(), -1).section('.', 0, -2);
+ else
+ return m_playlistName;
+}
+
+FileHandle Playlist::currentFile() const
+{
+ return playingItem() ? playingItem()->file() : FileHandle::null();
+}
+
+int Playlist::time() const
+{
+ // Since this method gets a lot of traffic, let's optimize for such.
+
+ if(!m_addTime.isEmpty()) {
+ for(PlaylistItemList::ConstIterator it = m_addTime.begin();
+ it != m_addTime.end(); ++it)
+ {
+ if(*it)
+ m_time += (*it)->file().tag()->seconds();
+ }
+
+ m_addTime.clear();
+ }
+
+ if(!m_subtractTime.isEmpty()) {
+ for(PlaylistItemList::ConstIterator it = m_subtractTime.begin();
+ it != m_subtractTime.end(); ++it)
+ {
+ if(*it)
+ m_time -= (*it)->file().tag()->seconds();
+ }
+
+ m_subtractTime.clear();
+ }
+
+ return m_time;
+}
+
+void Playlist::playFirst()
+{
+ TrackSequenceManager::instance()->setNextItem(static_cast<PlaylistItem *>(
+ QListViewItemIterator(const_cast<Playlist *>(this), QListViewItemIterator::Visible).current()));
+ action("forward")->activate();
+}
+
+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")->activate();
+}
+
+void Playlist::playNext()
+{
+ TrackSequenceManager::instance()->setCurrentPlaylist(this);
+ setPlaying(TrackSequenceManager::instance()->nextItem());
+}
+
+void Playlist::stop()
+{
+ m_history.clear();
+ setPlaying(0);
+}
+
+void Playlist::playPrevious()
+{
+ if(!playingItem())
+ return;
+
+ bool random = action("randomPlay") && action<KToggleAction>("randomPlay")->isChecked();
+
+ PlaylistItem *previous = 0;
+
+ if(random && !m_history.isEmpty()) {
+ PlaylistItemList::Iterator last = m_history.fromLast();
+ previous = *last;
+ m_history.remove(last);
+ }
+ else {
+ m_history.clear();
+ previous = TrackSequenceManager::instance()->previousItem();
+ }
+
+ if(!previous)
+ previous = static_cast<PlaylistItem *>(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(IO_WriteOnly))
+ return KMessageBox::error(this, i18n("Could not save to file %1.").arg(m_fileName));
+
+ QTextStream stream(&file);
+
+ QStringList fileList = files();
+
+ for(QStringList::Iterator it = fileList.begin(); it != fileList.end(); ++it)
+ stream << *it << endl;
+
+ file.close();
+}
+
+void Playlist::saveAs()
+{
+ m_collection->removeFileFromDict(m_fileName);
+
+ m_fileName = MediaFiles::savePlaylistDialog(name(), this);
+
+ if(!m_fileName.isEmpty()) {
+ m_collection->addFileToDict(m_fileName);
+
+ // If there's no playlist name set, use the file name.
+ if(m_playlistName.isEmpty())
+ emit signalNameChanged(name());
+ save();
+ }
+}
+
+void Playlist::clearItem(PlaylistItem *item, bool emitChanged)
+{
+ emit signalAboutToRemove(item);
+ m_members.remove(item->file().absFilePath());
+ m_search.clearItem(item);
+
+ m_history.remove(item);
+ m_addTime.remove(item);
+ m_subtractTime.remove(item);
+
+ delete item;
+ if(emitChanged)
+ dataChanged();
+}
+
+void Playlist::clearItems(const PlaylistItemList &items)
+{
+ m_blockDataChanged = true;
+
+ for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it)
+ clearItem(*it, false);
+
+ m_blockDataChanged = false;
+
+ dataChanged();
+}
+
+PlaylistItem *Playlist::playingItem() // static
+{
+ return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front();
+}
+
+QStringList Playlist::files() const
+{
+ QStringList list;
+
+ for(QListViewItemIterator it(const_cast<Playlist *>(this)); it.current(); ++it)
+ list.append(static_cast<PlaylistItem *>(*it)->file().absFilePath());
+
+ return list;
+}
+
+PlaylistItemList Playlist::items()
+{
+ return items(QListViewItemIterator::IteratorFlag(0));
+}
+
+PlaylistItemList Playlist::visibleItems()
+{
+ return items(QListViewItemIterator::Visible);
+}
+
+PlaylistItemList Playlist::selectedItems()
+{
+ PlaylistItemList list;
+
+ switch(m_selectedCount) {
+ case 0:
+ break;
+ // case 1:
+ // list.append(m_lastSelected);
+ // break;
+ default:
+ list = items(QListViewItemIterator::IteratorFlag(QListViewItemIterator::Selected |
+ QListViewItemIterator::Visible));
+ break;
+ }
+
+ return list;
+}
+
+PlaylistItem *Playlist::firstChild() const
+{
+ return static_cast<PlaylistItem *>(KListView::firstChild());
+}
+
+void Playlist::updateLeftColumn()
+{
+ int newLeftColumn = leftMostVisibleColumn();
+
+ if(m_leftColumn != newLeftColumn) {
+ updatePlaying();
+ m_leftColumn = newLeftColumn;
+ }
+}
+
+void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static
+{
+ m_visibleChanged = true;
+ for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it)
+ (*it)->setVisible(visible);
+}
+
+void Playlist::setSearch(const PlaylistSearch &s)
+{
+ m_search = s;
+
+ if(!m_searchEnabled)
+ return;
+
+ setItemsVisible(s.matchedItems(), true);
+ setItemsVisible(s.unmatchedItems(), false);
+
+ TrackSequenceManager::instance()->iterator()->playlistChanged();
+}
+
+void Playlist::setSearchEnabled(bool enabled)
+{
+ if(m_searchEnabled == enabled)
+ return;
+
+ m_searchEnabled = enabled;
+
+ if(enabled) {
+ setItemsVisible(m_search.matchedItems(), true);
+ setItemsVisible(m_search.unmatchedItems(), false);
+ }
+ else
+ setItemsVisible(items(), true);
+}
+
+void Playlist::markItemSelected(PlaylistItem *item, bool selected)
+{
+ if(selected && !item->isSelected()) {
+ m_selectedCount++;
+ m_lastSelected = item;
+ }
+ else if(!selected && item->isSelected())
+ m_selectedCount--;
+}
+
+void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster)
+{
+ for(PlaylistList::ConstIterator it = sources.begin(); it != sources.end(); ++it) {
+ if((*it)->playing()) {
+ CollectionListItem *base = playingItem()->collectionItem();
+ for(QListViewItemIterator itemIt(this); itemIt.current(); ++itemIt) {
+ PlaylistItem *item = static_cast<PlaylistItem *>(itemIt.current());
+ if(base == item->collectionItem()) {
+ item->setPlaying(true, setMaster);
+ PlaylistItemList playing = PlaylistItem::playingItems();
+ TrackSequenceManager::instance()->setCurrent(item);
+ return;
+ }
+ }
+ return;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// public slots
+////////////////////////////////////////////////////////////////////////////////
+
+void Playlist::copy()
+{
+ kapp->clipboard()->setData(dragObject(0), QClipboard::Clipboard);
+}
+
+void Playlist::paste()
+{
+ decode(kapp->clipboard()->data(), static_cast<PlaylistItem *>(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();
+
+ KApplication::setOverrideCursor(Qt::waitCursor);
+ for(PlaylistItemList::Iterator it = l.begin(); it != l.end(); ++it) {
+ (*it)->refreshFromDisk();
+
+ if(!(*it)->file().tag() || !(*it)->file().fileInfo().exists()) {
+ kdDebug(65432) << "Error while trying to refresh the tag. "
+ << "This file has probably been removed."
+ << endl;
+ clearItem((*it)->collectionItem());
+ }
+
+ processEvents();
+ }
+ KApplication::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;
+ dataChanged();
+
+ emit signalEnableDirWatch(true);
+}
+
+void Playlist::slotViewCover()
+{
+ PlaylistItemList items = selectedItems();
+ if (items.isEmpty())
+ return;
+ for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it)
+ (*it)->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::null,
+ i18n("&Delete Covers"));
+ if(button == KMessageBox::Continue)
+ refreshAlbums(items);
+}
+
+void Playlist::slotShowCoverManager()
+{
+ static CoverDialog *managerDialog = 0;
+
+ if(!managerDialog)
+ managerDialog = new CoverDialog(this);
+
+ managerDialog->show();
+}
+
+unsigned int Playlist::eligibleCoverItems(const PlaylistItemList &items)
+{
+ // This used to count the number of tracks with an artist and album, that
+ // is not strictly required anymore. This may prove useful in the future
+ // so I'm leaving it in for now, right now we just mark every item as
+ // eligible.
+
+ return items.count();
+}
+
+void Playlist::slotAddCover(bool retrieveLocal)
+{
+ PlaylistItemList items = selectedItems();
+
+ if(items.isEmpty())
+ return;
+
+ if(eligibleCoverItems(items) == 0) {
+ // No items in the list can be assigned a cover, inform the user and
+ // bail.
+
+ // KDE 4.0 Fix this string.
+ KMessageBox::sorry(this, i18n("None of the items you have selected can "
+ "be assigned a cover. A track must have both the Artist "
+ "and Album tags set to be assigned a cover."));
+
+ return;
+ }
+
+ QPixmap newCover;
+
+ if(retrieveLocal) {
+ KURL file = KFileDialog::getImageOpenURL(
+ ":homedir", this, i18n("Select Cover Image File"));
+ newCover = QPixmap(file.directory() + "/" + file.fileName());
+ }
+ else {
+ m_fetcher->setFile((*items.begin())->file());
+ m_fetcher->chooseCover();
+ return;
+ }
+
+ if(newCover.isNull())
+ return;
+
+ QString artist = items.front()->file().tag()->artist();
+ QString album = items.front()->file().tag()->album();
+
+ coverKey newId = CoverManager::addCover(newCover, artist, album);
+ refreshAlbums(items, newId);
+}
+
+// Called when image fetcher has added a new cover.
+void Playlist::slotCoverChanged(int coverId)
+{
+ kdDebug(65432) << "Refreshing information for newly changed covers.\n";
+ refreshAlbums(selectedItems(), coverId);
+}
+
+void Playlist::slotGuessTagInfo(TagGuesser::Type type)
+{
+ KApplication::setOverrideCursor(Qt::waitCursor);
+ PlaylistItemList items = selectedItems();
+ setDynamicListsFrozen(true);
+
+ m_blockDataChanged = true;
+
+ for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) {
+ (*it)->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;
+
+ dataChanged();
+ setDynamicListsFrozen(false);
+ KApplication::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 < columns(); i++) {
+ if(isColumnVisible(i))
+ m_weightDirty.append(i);
+ }
+ return;
+ }
+
+ if(m_weightDirty.find(column) == m_weightDirty.end())
+ 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->setSelected(playingItem(), true);
+ l->setCurrentItem(playingItem());
+ l->ensureItemVisible(playingItem());
+}
+
+void Playlist::slotColumnResizeModeChanged()
+{
+ if(manualResize())
+ setHScrollBarMode(Auto);
+ else
+ setHScrollBarMode(AlwaysOff);
+
+ if(!manualResize())
+ slotUpdateColumnWidths();
+
+ SharedSettings::instance()->sync();
+}
+
+void Playlist::dataChanged()
+{
+ if(m_blockDataChanged)
+ return;
+ PlaylistInterface::dataChanged();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// protected members
+////////////////////////////////////////////////////////////////////////////////
+
+void Playlist::removeFromDisk(const PlaylistItemList &items)
+{
+ if(isVisible() && !items.isEmpty()) {
+
+ QStringList files;
+ for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it)
+ files.append((*it)->file().absFilePath());
+
+ DeleteDialog dialog(this);
+
+ m_blockDataChanged = true;
+
+ if(dialog.confirmDeleteList(files)) {
+ bool shouldDelete = dialog.shouldDelete();
+ QStringList errorFiles;
+
+ for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) {
+ if(playingItem() == *it)
+ action("forward")->activate();
+
+ QString removePath = (*it)->file().absFilePath();
+ if((!shouldDelete && KIO::NetAccess::synchronousRun(KIO::trash(removePath), this)) ||
+ (shouldDelete && QFile::remove(removePath)))
+ {
+ CollectionList::instance()->clearItem((*it)->collectionItem());
+ }
+ else
+ errorFiles.append((*it)->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;
+
+ dataChanged();
+ }
+}
+
+QDragObject *Playlist::dragObject(QWidget *parent)
+{
+ PlaylistItemList items = selectedItems();
+ KURL::List urls;
+ for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) {
+ KURL url;
+ url.setPath((*it)->file().absFilePath());
+ urls.append(url);
+ }
+
+ KURLDrag *drag = new KURLDrag(urls, parent, "Playlist Items");
+ drag->setPixmap(BarIcon("sound"));
+
+ return drag;
+}
+
+void Playlist::contentsDragEnterEvent(QDragEnterEvent *e)
+{
+ KListView::contentsDragEnterEvent(e);
+
+ if(CoverDrag::canDecode(e)) {
+ setDropHighlighter(true);
+ setDropVisualizer(false);
+
+ e->accept();
+ return;
+ }
+
+ setDropHighlighter(false);
+ setDropVisualizer(true);
+
+ KURL::List urls;
+ if(!KURLDrag::decode(e, urls) || urls.isEmpty()) {
+ e->ignore();
+ return;
+ }
+
+ e->accept();
+ return;
+}
+
+bool Playlist::acceptDrag(QDropEvent *e) const
+{
+ return CoverDrag::canDecode(e) || KURLDrag::canDecode(e);
+}
+
+bool Playlist::canDecode(QMimeSource *s)
+{
+ KURL::List urls;
+
+ if(CoverDrag::canDecode(s))
+ return true;
+
+ return KURLDrag::decode(s, urls) && !urls.isEmpty();
+}
+
+void Playlist::decode(QMimeSource *s, PlaylistItem *item)
+{
+ KURL::List urls;
+
+ if(!KURLDrag::decode(s, urls) || urls.isEmpty())
+ return;
+
+ // handle dropped images
+
+ if(!MediaFiles::isMediaFile(urls.front().path())) {
+
+ QString file;
+
+ if(urls.front().isLocalFile())
+ file = urls.front().path();
+ else
+ KIO::NetAccess::download(urls.front(), file, 0);
+
+ KMimeType::Ptr mimeType = KMimeType::findByPath(file);
+
+ if(item && mimeType->name().startsWith("image/")) {
+ item->file().coverInfo()->setCover(QImage(file));
+ refreshAlbum(item->file().tag()->artist(),
+ item->file().tag()->album());
+ }
+
+ KIO::NetAccess::removeTempFile(file);
+ }
+
+ QStringList fileList;
+
+ for(KURL::List::Iterator it = urls.begin(); it != urls.end(); ++it)
+ fileList += MediaFiles::convertURLsToLocal((*it).path(), this);
+
+ addFiles(fileList, item);
+}
+
+bool Playlist::eventFilter(QObject *watched, QEvent *e)
+{
+ if(watched == header()) {
+ switch(e->type()) {
+ case QEvent::MouseMove:
+ {
+ if((static_cast<QMouseEvent *>(e)->state() & LeftButton) == LeftButton &&
+ !action<KToggleAction>("resizeColumnsManually")->isChecked())
+ {
+ m_columnWidthModeChanged = true;
+
+ action<KToggleAction>("resizeColumnsManually")->setChecked(true);
+ slotColumnResizeModeChanged();
+ }
+
+ break;
+ }
+ case QEvent::MouseButtonPress:
+ {
+ if(static_cast<QMouseEvent *>(e)->button() == 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 KListView::eventFilter(watched, e);
+}
+
+void Playlist::keyPressEvent(QKeyEvent *event)
+{
+ if(event->key() == Key_Up) {
+ QListViewItemIterator selected(this, QListViewItemIterator::IteratorFlag(
+ QListViewItemIterator::Selected |
+ QListViewItemIterator::Visible));
+ if(selected.current()) {
+ QListViewItemIterator visible(this, QListViewItemIterator::IteratorFlag(
+ QListViewItemIterator::Visible));
+ if(selected.current() == visible.current())
+ KApplication::postEvent(parent(), new FocusUpEvent);
+ }
+
+ }
+
+ KListView::keyPressEvent(event);
+}
+
+void Playlist::contentsDropEvent(QDropEvent *e)
+{
+ QPoint vp = contentsToViewport(e->pos());
+ PlaylistItem *item = static_cast<PlaylistItem *>(itemAt(vp));
+
+ // First see if we're dropping a cover, if so we can get it out of the
+ // way early.
+ if(item && CoverDrag::canDecode(e)) {
+ coverKey id;
+ CoverDrag::decode(e, id);
+
+ // If the item we dropped on is selected, apply cover to all selected
+ // items, otherwise just apply to the dropped item.
+
+ if(item->isSelected()) {
+ PlaylistItemList selItems = selectedItems();
+ for(PlaylistItemList::Iterator it = selItems.begin(); it != selItems.end(); ++it) {
+ (*it)->file().coverInfo()->setCoverId(id);
+ (*it)->refresh();
+ }
+ }
+ else {
+ item->file().coverInfo()->setCoverId(id);
+ item->refresh();
+ }
+
+ return;
+ }
+
+ // When dropping on the upper half of an item, insert before this item.
+ // This is what the user expects, and also allows the insertion at
+ // top of the list
+
+ if(!item)
+ item = static_cast<PlaylistItem *>(lastItem());
+ else if(vp.y() < item->itemPos() + item->height() / 2)
+ item = static_cast<PlaylistItem *>(item->itemAbove());
+
+ m_blockDataChanged = true;
+
+ if(e->source() == this) {
+
+ // Since we're trying to arrange things manually, turn off sorting.
+
+ setSorting(columns() + 1);
+
+ QPtrList<QListViewItem> items = KListView::selectedItems();
+
+ for(QPtrListIterator<QListViewItem> it(items); it.current(); ++it) {
+ if(!item) {
+
+ // Insert the item at the top of the list. This is a bit ugly,
+ // but I don't see another way.
+
+ takeItem(it.current());
+ insertItem(it.current());
+ }
+ else
+ it.current()->moveItem(item);
+
+ item = static_cast<PlaylistItem *>(it.current());
+ }
+ }
+ else
+ decode(e, item);
+
+ m_blockDataChanged = false;
+
+ dataChanged();
+ emit signalPlaylistItemsDropped(this);
+ KListView::contentsDropEvent(e);
+}
+
+void Playlist::contentsMouseDoubleClickEvent(QMouseEvent *e)
+{
+ // Filter out non left button double clicks, that way users don't have the
+ // weird experience of switching songs from a double right-click.
+
+ if(e->button() == LeftButton)
+ KListView::contentsMouseDoubleClickEvent(e);
+}
+
+void Playlist::showEvent(QShowEvent *e)
+{
+ if(m_applySharedSettings) {
+ SharedSettings::instance()->apply(this);
+ m_applySharedSettings = false;
+ }
+ KListView::showEvent(e);
+}
+
+void Playlist::applySharedSettings()
+{
+ m_applySharedSettings = true;
+}
+
+void Playlist::read(QDataStream &s)
+{
+ QString buffer;
+
+ s >> m_playlistName
+ >> m_fileName;
+
+ QStringList files;
+ s >> files;
+
+ QListViewItem *after = 0;
+
+ m_blockDataChanged = true;
+
+ for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it)
+ after = createItem(FileHandle(*it), after, false);
+
+ m_blockDataChanged = false;
+
+ dataChanged();
+ m_collection->setupPlaylist(this, "midi");
+}
+
+void Playlist::viewportPaintEvent(QPaintEvent *pe)
+{
+ // If there are columns that need to be updated, well, update them.
+
+ if(!m_weightDirty.isEmpty() && !manualResize())
+ {
+ calculateColumnWeights();
+ slotUpdateColumnWidths();
+ }
+
+ KListView::viewportPaintEvent(pe);
+}
+
+void Playlist::viewportResizeEvent(QResizeEvent *re)
+{
+ // If the width of the view has changed, manually update the column
+ // widths.
+
+ if(re->size().width() != re->oldSize().width() && !manualResize())
+ slotUpdateColumnWidths();
+
+ KListView::viewportResizeEvent(re);
+}
+
+void Playlist::insertItem(QListViewItem *item)
+{
+ // Because we're called from the PlaylistItem ctor, item may not be a
+ // PlaylistItem yet (it would be QListViewItem when being inserted. But,
+ // it will be a PlaylistItem by the time it matters, but be careful if
+ // you need to use the PlaylistItem from here.
+
+ m_addTime.append(static_cast<PlaylistItem *>(item));
+ KListView::insertItem(item);
+}
+
+void Playlist::takeItem(QListViewItem *item)
+{
+ // See the warning in Playlist::insertItem.
+
+ m_subtractTime.append(static_cast<PlaylistItem *>(item));
+ KListView::takeItem(item);
+}
+
+void Playlist::addColumn(const QString &label)
+{
+ slotWeightDirty(columns());
+ KListView::addColumn(label, 30);
+}
+
+PlaylistItem *Playlist::createItem(const FileHandle &file,
+ QListViewItem *after, bool emitChanged)
+{
+ return createItem<PlaylistItem, CollectionListItem, CollectionList>(file, after, emitChanged);
+}
+
+void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after)
+{
+ createItems<CollectionListItem, PlaylistItem, PlaylistItem>(siblings, after);
+}
+
+void Playlist::addFiles(const QStringList &files, PlaylistItem *after)
+{
+ if(!after)
+ after = static_cast<PlaylistItem *>(lastItem());
+
+ KApplication::setOverrideCursor(Qt::waitCursor);
+
+ m_blockDataChanged = true;
+
+ FileHandleList queue;
+
+ const QStringList::ConstIterator filesEnd = files.end();
+ for(QStringList::ConstIterator it = files.begin(); it != filesEnd; ++it)
+ addFile(*it, queue, true, &after);
+
+ addFileHelper(queue, &after, true);
+
+ m_blockDataChanged = false;
+
+ slotWeightDirty();
+ dataChanged();
+
+ KApplication::restoreOverrideCursor();
+}
+
+void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id)
+{
+ QValueList< QPair<QString, QString> > albums;
+ bool setAlbumCovers = items.count() == 1;
+
+ for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) {
+ QString artist = (*it)->file().tag()->artist();
+ QString album = (*it)->file().tag()->album();
+
+ if(albums.find(qMakePair(artist, album)) == albums.end())
+ albums.append(qMakePair(artist, album));
+
+ (*it)->file().coverInfo()->setCoverId(id);
+ if(setAlbumCovers)
+ (*it)->file().coverInfo()->applyCoverToWholeAlbum(true);
+ }
+
+ for(QValueList< QPair<QString, QString> >::ConstIterator it = albums.begin();
+ it != albums.end(); ++it)
+ {
+ refreshAlbum((*it).first, (*it).second);
+ }
+}
+
+void Playlist::updatePlaying() const
+{
+ for(PlaylistItemList::ConstIterator it = PlaylistItem::playingItems().begin();
+ it != PlaylistItem::playingItems().end(); ++it)
+ {
+ (*it)->listView()->triggerUpdate();
+ }
+}
+
+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);
+ PlaylistItemList matches = search.matchedItems();
+
+ for(PlaylistItemList::Iterator it = matches.begin(); it != matches.end(); ++it)
+ (*it)->refresh();
+}
+
+void Playlist::hideColumn(int c, bool updateSearch)
+{
+ m_headerMenu->setItemChecked(c, false);
+
+ if(!isColumnVisible(c))
+ return;
+
+ setColumnWidthMode(c, Manual);
+ setColumnWidth(c, 0);
+
+ // Moving the column to the end seems to prevent it from randomly
+ // popping up.
+
+ header()->moveSection(c, header()->count());
+ header()->setResizeEnabled(false, c);
+
+ if(c == m_leftColumn) {
+ updatePlaying();
+ m_leftColumn = leftMostVisibleColumn();
+ }
+
+ if(!manualResize()) {
+ slotUpdateColumnWidths();
+ triggerUpdate();
+ }
+
+ if(this != CollectionList::instance())
+ CollectionList::instance()->hideColumn(c, false);
+
+ if(updateSearch)
+ redisplaySearch();
+}
+
+void Playlist::showColumn(int c, bool updateSearch)
+{
+ m_headerMenu->setItemChecked(c, true);
+
+ if(isColumnVisible(c))
+ return;
+
+ // Just set the width to one to mark the column as visible -- we'll update
+ // the real size in the next call.
+
+ if(manualResize())
+ setColumnWidth(c, 35); // Make column at least slightly visible.
+ else
+ setColumnWidth(c, 1);
+
+ header()->setResizeEnabled(true, c);
+ header()->moveSection(c, c); // Approximate old position
+
+ if(c == leftMostVisibleColumn()) {
+ updatePlaying();
+ m_leftColumn = leftMostVisibleColumn();
+ }
+
+ if(!manualResize()) {
+ slotUpdateColumnWidths();
+ triggerUpdate();
+ }
+
+ if(this != CollectionList::instance())
+ CollectionList::instance()->showColumn(c, false);
+
+ if(updateSearch)
+ redisplaySearch();
+}
+
+bool Playlist::isColumnVisible(int c) const
+{
+ return columnWidth(c) != 0;
+}
+
+void Playlist::polish()
+{
+ KListView::polish();
+
+ if(m_polished)
+ return;
+
+ m_polished = true;
+
+ addColumn(i18n("Track Name"));
+ addColumn(i18n("Artist"));
+ addColumn(i18n("Album"));
+ addColumn(i18n("Cover"));
+ addColumn(i18n("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)"));
+
+ setRenameable(PlaylistItem::TrackColumn, true);
+ setRenameable(PlaylistItem::ArtistColumn, true);
+ setRenameable(PlaylistItem::AlbumColumn, true);
+ setRenameable(PlaylistItem::TrackNumberColumn, true);
+ setRenameable(PlaylistItem::GenreColumn, true);
+ setRenameable(PlaylistItem::YearColumn, true);
+
+ setAllColumnsShowFocus(true);
+ setSelectionMode(QListView::Extended);
+ setShowSortIndicator(true);
+ setDropVisualizer(true);
+
+ m_columnFixedWidths.resize(columns(), 0);
+
+ //////////////////////////////////////////////////
+ // setup header RMB menu
+ //////////////////////////////////////////////////
+
+ m_columnVisibleAction = new KActionMenu(i18n("&Show Columns"), this, "showColumns");
+
+ m_headerMenu = m_columnVisibleAction->popupMenu();
+ m_headerMenu->insertTitle(i18n("Show"));
+ m_headerMenu->setCheckable(true);
+
+ for(int i = 0; i < header()->count(); ++i) {
+ if(i == PlaylistItem::FileNameColumn)
+ m_headerMenu->insertSeparator();
+ m_headerMenu->insertItem(header()->label(i), i);
+ m_headerMenu->setItemChecked(i, true);
+ adjustColumn(i);
+ }
+
+ connect(m_headerMenu, SIGNAL(activated(int)), this, SLOT(slotToggleColumnVisible(int)));
+
+ connect(this, SIGNAL(contextMenuRequested(QListViewItem *, const QPoint &, int)),
+ this, SLOT(slotShowRMBMenu(QListViewItem *, const QPoint &, int)));
+ connect(this, SIGNAL(itemRenamed(QListViewItem *, const QString &, int)),
+ this, SLOT(slotInlineEditDone(QListViewItem *, const QString &, int)));
+ connect(this, SIGNAL(doubleClicked(QListViewItem *)),
+ this, SLOT(slotPlayCurrent()));
+ connect(this, SIGNAL(returnPressed(QListViewItem *)),
+ this, SLOT(slotPlayCurrent()));
+
+ connect(header(), SIGNAL(sizeChange(int, int, int)),
+ this, SLOT(slotColumnSizeChanged(int, int, int)));
+
+ connect(renameLineEdit(), SIGNAL(completionModeChanged(KGlobalSettings::Completion)),
+ this, SLOT(slotInlineCompletionModeChanged(KGlobalSettings::Completion)));
+
+ connect(action("resizeColumnsManually"), SIGNAL(activated()),
+ this, SLOT(slotColumnResizeModeChanged()));
+
+ if(action<KToggleAction>("resizeColumnsManually")->isChecked())
+ setHScrollBarMode(Auto);
+ else
+ setHScrollBarMode(AlwaysOff);
+
+ setAcceptDrops(true);
+ setDropVisualizer(true);
+
+ m_disableColumnWidthUpdates = false;
+
+ setShowToolTips(false);
+ m_toolTip = new PlaylistToolTip(viewport(), this);
+}
+
+void Playlist::setupItem(PlaylistItem *item)
+{
+ if(!m_search.isEmpty())
+ item->setVisible(m_search.checkItem(item));
+
+ if(childCount() <= 2 && !manualResize()) {
+ slotWeightDirty();
+ slotUpdateColumnWidths();
+ triggerUpdate();
+ }
+}
+
+void Playlist::setDynamicListsFrozen(bool frozen)
+{
+ m_collection->setDynamicListsFrozen(frozen);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// protected slots
+////////////////////////////////////////////////////////////////////////////////
+
+void Playlist::slotPopulateBackMenu() const
+{
+ if(!playingItem())
+ return;
+
+ KPopupMenu *menu = action<KToolBarPopupAction>("back")->popupMenu();
+ menu->clear();
+ m_backMenuItems.clear();
+
+ int count = 0;
+ PlaylistItemList::ConstIterator it = m_history.end();
+
+ while(it != m_history.begin() && count < 10) {
+ ++count;
+ --it;
+ int index = menu->insertItem((*it)->file().tag()->title());
+ m_backMenuItems[index] = *it;
+ }
+}
+
+void Playlist::slotPlayFromBackMenu(int number) const
+{
+ if(!m_backMenuItems.contains(number))
+ return;
+
+ TrackSequenceManager::instance()->setNextItem(m_backMenuItems[number]);
+ action("forward")->activate();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// private members
+////////////////////////////////////////////////////////////////////////////////
+
+void Playlist::setup()
+{
+ setItemMargin(3);
+
+ connect(header(), SIGNAL(indexChange(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(selectionChanged()), m_fetcher, SLOT(abortSearch()));
+
+ setSorting(1);
+}
+
+void Playlist::loadFile(const QString &fileName, const QFileInfo &fileInfo)
+{
+ QFile file(fileName);
+ if(!file.open(IO_ReadOnly))
+ return;
+
+ QTextStream stream(&file);
+
+ // Turn off non-explicit sorting.
+
+ setSorting(PlaylistItem::lastColumn() + columnOffset() + 1);
+
+ PlaylistItem *after = 0;
+
+ m_disableColumnWidthUpdates = true;
+
+ m_blockDataChanged = true;
+
+ while(!stream.atEnd()) {
+ QString itemName = stream.readLine().stripWhiteSpace();
+
+ QFileInfo item(itemName);
+
+ if(item.isRelative())
+ item.setFile(QDir::cleanDirPath(fileInfo.dirPath(true) + "/" + itemName));
+
+ if(item.exists() && item.isFile() && item.isReadable() &&
+ MediaFiles::isMediaFile(item.fileName()))
+ {
+ if(after)
+ after = createItem(FileHandle(item, item.absFilePath()), after, false);
+ else
+ after = createItem(FileHandle(item, item.absFilePath()), 0, false);
+ }
+ }
+
+ m_blockDataChanged = false;
+
+ file.close();
+
+ dataChanged();
+
+ m_disableColumnWidthUpdates = false;
+}
+
+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);
+ QByteArray data;
+ kapp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data);
+
+ if(!item)
+ return;
+
+ item->setPlaying(true);
+
+ bool enableBack = !m_history.isEmpty();
+ action<KToolBarPopupAction>("back")->popupMenu()->setEnabled(enableBack);
+}
+
+bool Playlist::playing() const
+{
+ return playingItem() && this == playingItem()->playlist();
+}
+
+int Playlist::leftMostVisibleColumn() const
+{
+ int i = 0;
+ while(!isColumnVisible(header()->mapToSection(i)) && i < PlaylistItem::lastColumn())
+ i++;
+
+ return header()->mapToSection(i);
+}
+
+PlaylistItemList Playlist::items(QListViewItemIterator::IteratorFlag flags)
+{
+ PlaylistItemList list;
+
+ for(QListViewItemIterator it(this, flags); it.current(); ++it)
+ list.append(static_cast<PlaylistItem *>(it.current()));
+
+ return list;
+}
+
+void Playlist::calculateColumnWeights()
+{
+ if(m_disableColumnWidthUpdates)
+ return;
+
+ PlaylistItemList l = items();
+ QValueListConstIterator<int> columnIt;
+
+ QValueVector<double> averageWidth(columns(), 0);
+ double itemCount = l.size();
+
+ QValueVector<int> 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.
+
+ for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) {
+ cachedWidth = (*it)->cachedWidths();
+ for(columnIt = m_weightDirty.begin(); columnIt != m_weightDirty.end(); ++columnIt)
+ averageWidth[*columnIt] += pow(double(cachedWidth[*columnIt]), 2.0) / itemCount;
+ }
+
+ m_columnWeights.resize(columns(), -1);
+
+ for(columnIt = m_weightDirty.begin(); columnIt != m_weightDirty.end(); ++columnIt) {
+ m_columnWeights[*columnIt] = int(sqrt(averageWidth[*columnIt]) + 0.5);
+
+ // kdDebug(65432) << k_funcinfo << "m_columnWeights[" << *columnIt << "] == "
+ // << m_columnWeights[*columnIt] << endl;
+ }
+
+ m_weightDirty.clear();
+}
+
+void Playlist::addFile(const QString &file, FileHandleList &files, bool importPlaylists,
+ PlaylistItem **after)
+{
+ if(hasItem(file) && !m_allowDuplicates)
+ return;
+
+ processEvents();
+ addFileHelper(files, after);
+
+ // Our biggest thing that we're fighting during startup is too many stats
+ // of files. Make sure that we don't do one here if it's not needed.
+
+ FileHandle cached = Cache::instance()->value(file);
+
+ if(!cached.isNull()) {
+ cached.tag();
+ files.append(cached);
+ return;
+ }
+
+
+ const QFileInfo fileInfo = QDir::cleanDirPath(file);
+ if(!fileInfo.exists())
+ return;
+
+ if(fileInfo.isFile() && fileInfo.isReadable()) {
+ if(MediaFiles::isMediaFile(file)) {
+ FileHandle f(fileInfo, fileInfo.absFilePath());
+ f.tag();
+ files.append(f);
+ }
+ }
+
+ if(importPlaylists && MediaFiles::isPlaylistFile(file) &&
+ !m_collection->containsPlaylistFile(fileInfo.absFilePath()))
+ {
+ new Playlist(m_collection, fileInfo);
+ return;
+ }
+
+ if(fileInfo.isDir()) {
+
+ // Resorting to the POSIX API because QDir::listEntries() stats every
+ // file and blocks while it's doing so.
+
+ DIR *dir = ::opendir(QFile::encodeName(fileInfo.filePath()));
+
+ if(dir) {
+ struct dirent *dirEntry;
+
+ for(dirEntry = ::readdir(dir); dirEntry; dirEntry = ::readdir(dir)) {
+ if(strcmp(dirEntry->d_name, ".") != 0 && strcmp(dirEntry->d_name, "..") != 0) {
+
+ // We set importPlaylists to the value from the add directories
+ // dialog as we want to load all of the ones that the user has
+ // explicitly asked for, but not those that we find in lower
+ // directories.
+
+ addFile(fileInfo.filePath() + QDir::separator() + QFile::decodeName(dirEntry->d_name),
+ files, m_collection->importPlaylists(), after);
+ }
+ }
+ ::closedir(dir);
+ }
+ else {
+ kdWarning(65432) << "Unable to open directory "
+ << fileInfo.filePath()
+ << ", make sure it is readable.\n";
+ }
+ }
+}
+
+void Playlist::addFileHelper(FileHandleList &files, PlaylistItem **after, bool ignoreTimer)
+{
+ static QTime time = QTime::currentTime();
+
+ // Process new items every 10 seconds, when we've loaded 1000 items, or when
+ // it's been requested in the API.
+
+ if(ignoreTimer || time.elapsed() > 10000 ||
+ (files.count() >= 1000 && time.elapsed() > 1000))
+ {
+ time.restart();
+
+ const bool focus = hasFocus();
+ const bool visible = isVisible() && files.count() > 20;
+
+ if(visible)
+ m_collection->raiseDistraction();
+ const FileHandleList::ConstIterator filesEnd = files.end();
+ for(FileHandleList::ConstIterator it = files.begin(); it != filesEnd; ++it)
+ *after = createItem(*it, *after, false);
+ files.clear();
+
+ if(visible)
+ m_collection->lowerDistraction();
+
+ if(focus)
+ setFocus();
+
+ processEvents();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 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.
+
+ QValueList<int> visibleColumns;
+ for(int i = 0; i < columns(); i++) {
+ if(isColumnVisible(i))
+ visibleColumns.append(i);
+ }
+
+ QValueListConstIterator<int> it;
+
+ if(count() == 0) {
+ for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it)
+ setColumnWidth(*it, header()->fontMetrics().width(header()->label(*it)) + 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.
+
+ QValueVector<int> minimumWidth(columns(), 0);
+ int minimumWidthTotal = 0;
+
+ // Also build a list of either the minimum *or* the fixed width -- whichever is
+ // greater.
+
+ QValueVector<int> minimumFixedWidth(columns(), 0);
+ int minimumFixedWidthTotal = 0;
+
+ for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) {
+ int column = *it;
+ minimumWidth[column] = header()->fontMetrics().width(header()->label(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 lower 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 < visibleWidth()) {
+ 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;
+ for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it)
+ totalWeight += m_columnWeights[*it];
+
+ // 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.
+
+ QValueVector<int> weightedWidth(columns(), 0);
+ for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it)
+ weightedWidth[*it] = int(double(m_columnWeights[*it]) / totalWeight * visibleWidth() + 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.
+
+ QValueVector<int> extraWidth(columns(), 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.
+
+ for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) {
+ if(weightedWidth[*it] < minimumWidth[*it]) {
+ readjust = true;
+ extraWidth[*it] = 0;
+ neededWidth += minimumWidth[*it] - weightedWidth[*it];
+ }
+ else {
+ extraWidth[*it] = weightedWidth[*it] - minimumWidth[*it];
+ availableWidth += extraWidth[*it];
+ }
+ }
+
+ // 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 "reajusted
+ // weighted width".
+
+ for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) {
+ int width;
+ if(readjust) {
+ int adjustedExtraWidth = int(double(extraWidth[*it]) * adjustmentRatio + 0.5);
+ width = minimumWidth[*it] + adjustedExtraWidth;
+ }
+ else
+ width = weightedWidth[*it];
+
+ setColumnWidth(*it, width);
+ usedWidth += width;
+ }
+
+ // Fill the remaining gap for a clean fit into the available space.
+
+ int remainingWidth = visibleWidth() - 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(QListViewItem *item, const QPoint &point, int column)
+{
+ if(!item)
+ return;
+
+ // Create the RMB menu on demand.
+
+ if(!m_rmbMenu) {
+
+ // A bit of a hack to get a pointer to the action collection.
+ // Probably more of these actions should be ported over to using KActions.
+
+ m_rmbMenu = new KPopupMenu(this);
+
+ m_rmbUpcomingID = m_rmbMenu->insertItem(SmallIcon("today"),
+ i18n("Add to Play Queue"), this, SLOT(slotAddToUpcoming()));
+ m_rmbMenu->insertSeparator();
+
+ if(!readOnly()) {
+ action("edit_cut")->plug(m_rmbMenu);
+ action("edit_copy")->plug(m_rmbMenu);
+ action("edit_paste")->plug(m_rmbMenu);
+ m_rmbMenu->insertSeparator();
+ action("removeFromPlaylist")->plug(m_rmbMenu);
+ }
+ else
+ action("edit_copy")->plug(m_rmbMenu);
+
+ m_rmbEditID = m_rmbMenu->insertItem(
+ i18n("Edit"), this, SLOT(slotRenameTag()));
+
+ action("refresh")->plug(m_rmbMenu);
+ action("removeItem")->plug(m_rmbMenu);
+
+ m_rmbMenu->insertSeparator();
+
+ action("guessTag")->plug(m_rmbMenu);
+ action("renameFile")->plug(m_rmbMenu);
+
+ action("coverManager")->plug(m_rmbMenu);
+
+ m_rmbMenu->insertSeparator();
+
+ m_rmbMenu->insertItem(
+ SmallIcon("folder_new"), i18n("Create Playlist From Selected Items..."), this, SLOT(slotCreateGroup()));
+
+ K3bExporter *exporter = new K3bExporter(this);
+ KAction *k3bAction = exporter->action();
+ if(k3bAction)
+ k3bAction->plug(m_rmbMenu);
+ }
+
+ // Ignore any columns added by subclasses.
+
+ column -= columnOffset();
+
+ bool showEdit =
+ (column == PlaylistItem::TrackColumn) ||
+ (column == PlaylistItem::ArtistColumn) ||
+ (column == PlaylistItem::AlbumColumn) ||
+ (column == PlaylistItem::TrackNumberColumn) ||
+ (column == PlaylistItem::GenreColumn) ||
+ (column == PlaylistItem::YearColumn);
+
+ if(showEdit)
+ m_rmbMenu->changeItem(m_rmbEditID,
+ i18n("Edit '%1'").arg(columnText(column + columnOffset())));
+
+ m_rmbMenu->setItemVisible(m_rmbEditID, showEdit);
+
+ // Disable edit menu if only one file is selected, and it's read-only
+
+ FileHandle file = static_cast<PlaylistItem*>(item)->file();
+
+ m_rmbMenu->setItemEnabled(m_rmbEditID, file.fileInfo().isWritable() ||
+ selectedItems().count() > 1);
+
+ action("viewCover")->setEnabled(file.coverInfo()->hasCover());
+ action("removeCover")->setEnabled(file.coverInfo()->hasCover());
+
+ m_rmbMenu->popup(point);
+ m_currentColumn = column + columnOffset();
+}
+
+void Playlist::slotRenameTag()
+{
+ // kdDebug(65432) << "Playlist::slotRenameTag()" << endl;
+
+ // setup completions and validators
+
+ CollectionList *list = CollectionList::instance();
+
+ KLineEdit *edit = renameLineEdit();
+
+ switch(m_currentColumn - columnOffset())
+ {
+ case PlaylistItem::ArtistColumn:
+ edit->completionObject()->setItems(list->uniqueSet(CollectionList::Artists));
+ break;
+ case PlaylistItem::AlbumColumn:
+ edit->completionObject()->setItems(list->uniqueSet(CollectionList::Albums));
+ break;
+ case PlaylistItem::GenreColumn:
+ {
+ QStringList genreList;
+ TagLib::StringList genres = TagLib::ID3v1::genreList();
+ for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it)
+ genreList.append(TStringToQString((*it)));
+ edit->completionObject()->setItems(genreList);
+ break;
+ }
+ default:
+ edit->completionObject()->clear();
+ break;
+ }
+
+ m_editText = currentItem()->text(m_currentColumn);
+
+ rename(currentItem(), m_currentColumn);
+}
+
+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(QListViewItem *, const QString &, int column)
+{
+ QString text = renameLineEdit()->text();
+ bool changed = false;
+
+ PlaylistItemList l = selectedItems();
+
+ // See if any of the files have a tag different from the input.
+
+ for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end() && !changed; ++it)
+ if((*it)->text(column - columnOffset()) != text)
+ changed = true;
+
+ if(!changed ||
+ (l.count() > 1 && KMessageBox::warningContinueCancel(
+ 0,
+ i18n("This will edit multiple files. Are you sure?"),
+ QString::null,
+ i18n("Edit"),
+ "DontWarnMultipleTags") == KMessageBox::Cancel))
+ {
+ return;
+ }
+
+ for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it)
+ editTag(*it, text, column);
+
+ TagTransactionManager::instance()->commit();
+
+ CollectionList::instance()->dataChanged();
+ dataChanged();
+ update();
+}
+
+void Playlist::slotColumnOrderChanged(int, int from, int to)
+{
+ if(from == 0 || to == 0) {
+ updatePlaying();
+ m_leftColumn = header()->mapToSection(0);
+ }
+
+ SharedSettings::instance()->setColumnOrder(this);
+}
+
+void Playlist::slotToggleColumnVisible(int column)
+{
+ if(!isColumnVisible(column)) {
+ int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset();
+ int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset();
+
+ if(column == fileNameColumn && isColumnVisible(fullPathColumn)) {
+ hideColumn(fullPathColumn, false);
+ SharedSettings::instance()->toggleColumnVisible(fullPathColumn);
+ }
+ if(column == fullPathColumn && isColumnVisible(fileNameColumn)) {
+ hideColumn(fileNameColumn, false);
+ SharedSettings::instance()->toggleColumnVisible(fileNameColumn);
+ }
+ }
+
+ if(isColumnVisible(column))
+ hideColumn(column);
+ else
+ showColumn(column);
+
+ 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::slotColumnSizeChanged(int column, int, int newSize)
+{
+ m_widthsDirty = true;
+ m_columnFixedWidths[column] = newSize;
+}
+
+void Playlist::slotInlineCompletionModeChanged(KGlobalSettings::Completion mode)
+{
+ SharedSettings::instance()->setInlineCompletionMode(mode);
+}
+
+void Playlist::slotPlayCurrent()
+{
+ QListViewItemIterator it(this, QListViewItemIterator::Selected);
+ PlaylistItem *next = static_cast<PlaylistItem *>(it.current());
+ TrackSequenceManager::instance()->setNextItem(next);
+ action("forward")->activate();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// helper functions
+////////////////////////////////////////////////////////////////////////////////
+
+QDataStream &operator<<(QDataStream &s, const Playlist &p)
+{
+ s << p.name();
+ s << p.fileName();
+ s << p.files();
+
+ return s;
+}
+
+QDataStream &operator>>(QDataStream &s, Playlist &p)
+{
+ p.read(s);
+ return s;
+}
+
+bool processEvents()
+{
+ static QTime time = QTime::currentTime();
+
+ if(time.elapsed() > 100) {
+ time.restart();
+ kapp->processEvents();
+ return true;
+ }
+ return false;
+}
+
+#include "playlist.moc"