diff --git a/playlist.cpp b/playlist.cpp index a32428a9..311999cd 100644 --- a/playlist.cpp +++ b/playlist.cpp @@ -1,2404 +1,2373 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2008 Michael Pyne * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #include "playlist.h" #include "juk-exception.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlistitem.h" #include "playlistcollection.h" #include "playlistsearch.h" #include "mediafiles.h" #include "collectionlist.h" #include "filerenamer.h" #include "actioncollection.h" #include "tracksequencemanager.h" #include "tag.h" #include "upcomingplaylist.h" #include "deletedialog.h" #include "webimagefetcher.h" #include "coverinfo.h" #include "coverdialog.h" #include "tagtransactionmanager.h" #include "cache.h" #include "juk_debug.h" using namespace ActionCollection; /** * Used to give every track added in the program a unique identifier. See * PlaylistItem */ quint32 g_trackID = 0; /** * Just a shortcut of sorts. */ static bool manualResize() { return action("resizeColumnsManually")->isChecked(); } /** * A tooltip specialized to show full filenames over the file name column. */ #ifdef __GNUC__ #warning disabling the tooltip for now #endif #if 0 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(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) { Q3MimeSourceFactory *f = Q3MimeSourceFactory::defaultFactory(); f->setImage("coverThumb", QImage(item->file().coverInfo()->pixmap(CoverInfo::Thumbnail).convertToImage())); tip(r, "
"); } else tip(r, item->text(column)); } } private: Playlist *m_playlist; }; #endif //////////////////////////////////////////////////////////////////////////////// // 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; QList m_columnOrder; QVector 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->columnCount(); ++i) m_columnOrder.append(l->header()->visualIndex(i)); // FIXME MISMATCH with apply writeConfig(); } void Playlist::SharedSettings::toggleColumnVisible(int column) { if(column >= m_columnsVisible.size()) m_columnsVisible.fill(true, column + 1); 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; //bool oldState = l->header()->blockSignals(true); foreach(int column, m_columnOrder) // FIXME this is broken l->header()->moveSection(i++ + offset, column + offset); // FIXME mismatch with setColumnOrder //l->header()->blockSignals(oldState); for(int i = 0; i < m_columnsVisible.size(); i++) { if(m_columnsVisible[i] && l->isColumnHidden(i + offset)) l->showColumn(i + offset, false); else if(!m_columnsVisible[i] && !l->isColumnHidden(i + offset)) l->hideColumn(i + offset, false); } l->updateLeftColumn(); // FIXME rename //l->renameLineEdit()->setCompletionMode(m_inlineCompletion); l->slotColumnResizeModeChanged(); } //////////////////////////////////////////////////////////////////////////////// // Playlist::ShareSettings protected members //////////////////////////////////////////////////////////////////////////////// Playlist::SharedSettings::SharedSettings() { KConfigGroup config(KSharedConfig::openConfig(), "PlaylistShared"); bool resizeColumnsManually = config.readEntry("ResizeColumnsManually", false); action("resizeColumnsManually")->setChecked(resizeColumnsManually); // Preallocate spaces so we don't need to check later. m_columnsVisible.fill(true, PlaylistItem::lastColumn() + 1); // save column order m_columnOrder = config.readEntry("ColumnOrder", QList()); QList l = config.readEntry("VisibleColumns", QList()); if(l.isEmpty()) { // Provide some default values for column visibility if none were // read from the configuration file. m_columnsVisible[PlaylistItem::BitrateColumn] = false; m_columnsVisible[PlaylistItem::CommentColumn] = false; m_columnsVisible[PlaylistItem::FileNameColumn] = false; m_columnsVisible[PlaylistItem::FullPathColumn] = false; } else { // Convert the int list into a bool list. m_columnsVisible.fill(false); for(int i = 0; i < l.size() && i < m_columnsVisible.size(); ++i) m_columnsVisible[i] = bool(l[i]); } m_inlineCompletion = KGlobalSettings::Completion( config.readEntry("InlineCompletionMode", int(KGlobalSettings::CompletionAuto))); } //////////////////////////////////////////////////////////////////////////////// // Playlist::SharedSettings private members //////////////////////////////////////////////////////////////////////////////// void Playlist::SharedSettings::writeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "PlaylistShared"); config.writeEntry("ColumnOrder", m_columnOrder); QList l; for(int i = 0; i < m_columnsVisible.size(); i++) l.append(int(m_columnsVisible[i])); config.writeEntry("VisibleColumns", l); config.writeEntry("InlineCompletionMode", int(m_inlineCompletion)); config.writeEntry("ResizeColumnsManually", manualResize()); KSharedConfig::openConfig()->sync(); } //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// PlaylistItemList Playlist::m_history; QVector Playlist::m_backMenuItems; int Playlist::m_leftColumn = 0; Playlist::Playlist(PlaylistCollection *collection, const QString &name, const QString &iconName) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), - m_selectedCount(0), m_allowDuplicates(true), 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) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), - m_selectedCount(0), m_allowDuplicates(true), 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) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), - m_selectedCount(0), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), - m_lastSelected(0), m_fileName(playlistFile.canonicalFilePath()), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); loadFile(m_fileName, playlistFile); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), - m_selectedCount(0), m_allowDuplicates(true), 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) { for(int i = 0; i < extraColumns; ++i) { addColumn(i18n("JuK")); // Placeholder text! } setup(); if(!delaySetup) collection->setupPlaylist(this, "audio-midi"); } Playlist::~Playlist() { // In some situations the dataChanged signal from clearItems will cause observers to // subsequently try to access a deleted item. Since we're going away just remove all // observers. clearObservers(); // clearItem() will take care of removing the items from the history, // so call clearItems() to make sure it happens. clearItems(items()); /* delete m_toolTip; */ if(!m_shuttingDown) m_collection->removePlaylist(this); } QString Playlist::name() const { if(m_playlistName.isEmpty()) return m_fileName.section(QDir::separator(), -1).section('.', 0, -2); else return m_playlistName; } FileHandle Playlist::currentFile() const { return playingItem() ? playingItem()->file() : FileHandle::null(); } int Playlist::time() const { // Since this method gets a lot of traffic, let's optimize for such. if(!m_addTime.isEmpty()) { foreach(const PlaylistItem *item, m_addTime) { if(item) m_time += item->file().tag()->seconds(); } m_addTime.clear(); } if(!m_subtractTime.isEmpty()) { foreach(const PlaylistItem *item, m_subtractTime) { if(item) m_time -= item->file().tag()->seconds(); } m_subtractTime.clear(); } return m_time; } void Playlist::playFirst() { TrackSequenceManager::instance()->setNextItem(static_cast( *QTreeWidgetItemIterator(const_cast(this), QTreeWidgetItemIterator::NotHidden))); action("forward")->trigger(); } void Playlist::playNextAlbum() { PlaylistItem *current = TrackSequenceManager::instance()->currentItem(); if(!current) return; // No next album if we're not already playing. QString currentAlbum = current->file().tag()->album(); current = TrackSequenceManager::instance()->nextItem(); while(current && current->file().tag()->album() == currentAlbum) current = TrackSequenceManager::instance()->nextItem(); TrackSequenceManager::instance()->setNextItem(current); action("forward")->trigger(); } void Playlist::playNext() { TrackSequenceManager::instance()->setCurrentPlaylist(this); setPlaying(TrackSequenceManager::instance()->nextItem()); } void Playlist::stop() { m_history.clear(); setPlaying(0); } void Playlist::playPrevious() { if(!playingItem()) return; bool random = action("randomPlay") && action("randomPlay")->isChecked(); PlaylistItem *previous = 0; if(random && !m_history.isEmpty()) { PlaylistItemList::Iterator last = --m_history.end(); previous = *last; m_history.erase(last); } else { m_history.clear(); previous = TrackSequenceManager::instance()->previousItem(); } if(!previous) previous = static_cast(playingItem()->itemAbove()); setPlaying(previous, false); } void Playlist::setName(const QString &n) { m_collection->addNameToDict(n); m_collection->removeNameFromDict(m_playlistName); m_playlistName = n; emit signalNameChanged(m_playlistName); } void Playlist::save() { if(m_fileName.isEmpty()) return saveAs(); QFile file(m_fileName); if(!file.open(QIODevice::WriteOnly)) return KMessageBox::error(this, i18n("Could not save to file %1.", m_fileName)); QTextStream stream(&file); QStringList fileList = files(); foreach(const QString &file, fileList) stream << file << endl; file.close(); } void Playlist::saveAs() { m_collection->removeFileFromDict(m_fileName); m_fileName = MediaFiles::savePlaylistDialog(name(), this); if(!m_fileName.isEmpty()) { m_collection->addFileToDict(m_fileName); // If there's no playlist name set, use the file name. if(m_playlistName.isEmpty()) emit signalNameChanged(name()); save(); } } void Playlist::updateDeletedItem(PlaylistItem *item) { m_members.remove(item->file().absFilePath()); m_search.clearItem(item); m_history.removeAll(item); m_addTime.removeAll(item); m_subtractTime.removeAll(item); } void Playlist::clearItem(PlaylistItem *item) { // Automatically updates internal structs via updateDeletedItem delete item; dataChanged(); } void Playlist::clearItems(const PlaylistItemList &items) { foreach(PlaylistItem *item, items) delete item; dataChanged(); } PlaylistItem *Playlist::playingItem() // static { return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front(); } QStringList Playlist::files() const { QStringList list; for(QTreeWidgetItemIterator it(const_cast(this)); *it; ++it) list.append(static_cast(*it)->file().absFilePath()); return list; } PlaylistItemList Playlist::items() { return items(QTreeWidgetItemIterator::IteratorFlag(0)); } PlaylistItemList Playlist::visibleItems() { return items(QTreeWidgetItemIterator::NotHidden); } PlaylistItemList Playlist::selectedItems() { - PlaylistItemList list; - - switch(m_selectedCount) { - case 0: - break; - // case 1: - // list.append(m_lastSelected); - // break; - default: - list = items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); - break; - } - - return list; + return items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); } PlaylistItem *Playlist::firstChild() const { return static_cast(topLevelItem(0)); } void Playlist::updateLeftColumn() { int newLeftColumn = leftMostVisibleColumn(); if(m_leftColumn != newLeftColumn) { updatePlaying(); m_leftColumn = newLeftColumn; } } void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static { m_visibleChanged = true; foreach(PlaylistItem *playlistItem, items) playlistItem->setHidden(!visible); } void Playlist::setSearch(const PlaylistSearch &s) { m_search = s; if(!m_searchEnabled) return; setItemsVisible(s.matchedItems(), true); setItemsVisible(s.unmatchedItems(), false); TrackSequenceManager::instance()->iterator()->playlistChanged(); } void Playlist::setSearchEnabled(bool enabled) { if(m_searchEnabled == enabled) return; m_searchEnabled = enabled; if(enabled) { setItemsVisible(m_search.matchedItems(), true); setItemsVisible(m_search.unmatchedItems(), false); } else setItemsVisible(items(), true); } -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) { foreach(const Playlist *p, sources) { if(p->playing()) { CollectionListItem *base = playingItem()->collectionItem(); for(QTreeWidgetItemIterator itemIt(this); *itemIt; ++itemIt) { PlaylistItem *item = static_cast(*itemIt); if(base == item->collectionItem()) { item->setPlaying(true, setMaster); PlaylistItemList playing = PlaylistItem::playingItems(); TrackSequenceManager::instance()->setCurrent(item); return; } } return; } } } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void Playlist::copy() { PlaylistItemList items = selectedItems(); KUrl::List urls; foreach(PlaylistItem *item, items) { urls << KUrl::fromPath(item->file().absFilePath()); } QMimeData *mimeData = new QMimeData; urls.populateMimeData(mimeData); QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void Playlist::paste() { decode(QApplication::clipboard()->mimeData(), static_cast(currentItem())); } void Playlist::clear() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = items(); clearItems(l); } void Playlist::slotRefresh() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = visibleItems(); QApplication::setOverrideCursor(Qt::WaitCursor); foreach(PlaylistItem *item, l) { item->refreshFromDisk(); if(!item->file().tag() || !item->file().fileInfo().exists()) { qCDebug(JUK_LOG) << "Error while trying to refresh the tag. " << "This file has probably been removed." << endl; delete item->collectionItem(); } processEvents(); } QApplication::restoreOverrideCursor(); } void Playlist::slotRenameFile() { FileRenamer renamer; PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; emit signalEnableDirWatch(false); m_blockDataChanged = true; renamer.rename(items); m_blockDataChanged = false; dataChanged(); emit signalEnableDirWatch(true); } void Playlist::slotViewCover() { const PlaylistItemList items = selectedItems(); if (items.isEmpty()) return; foreach(const PlaylistItem *item, items) item->file().coverInfo()->popup(); } void Playlist::slotRemoveCover() { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; int button = KMessageBox::warningContinueCancel(this, i18n("Are you sure you want to delete these covers?"), QString(), KGuiItem(i18n("&Delete Covers"))); if(button == KMessageBox::Continue) refreshAlbums(items); } void Playlist::slotShowCoverManager() { static CoverDialog *managerDialog = 0; if(!managerDialog) managerDialog = new CoverDialog(this); managerDialog->show(); } void Playlist::slotAddCover(bool retrieveLocal) { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; if(!retrieveLocal) { m_fetcher->setFile((*items.begin())->file()); m_fetcher->searchCover(); return; } KUrl file = KFileDialog::getImageOpenUrl( KUrl( "kfiledialog://homedir" ), this, i18n("Select Cover Image File")); if(file.isEmpty()) return; QString artist = items.front()->file().tag()->artist(); QString album = items.front()->file().tag()->album(); coverKey newId = CoverManager::addCover(file, artist, album); if(newId != CoverManager::NoMatch) refreshAlbums(items, newId); } // Called when image fetcher has added a new cover. void Playlist::slotCoverChanged(int coverId) { qCDebug(JUK_LOG) << "Refreshing information for newly changed covers.\n"; refreshAlbums(selectedItems(), coverId); } void Playlist::slotGuessTagInfo(TagGuesser::Type type) { QApplication::setOverrideCursor(Qt::WaitCursor); const PlaylistItemList items = selectedItems(); setDynamicListsFrozen(true); m_blockDataChanged = true; foreach(PlaylistItem *item, items) { item->guessTagInfo(type); processEvents(); } // MusicBrainz queries automatically commit at this point. What would // be nice is having a signal emitted when the last query is completed. if(type == TagGuesser::FileName) TagTransactionManager::instance()->commit(); m_blockDataChanged = false; dataChanged(); setDynamicListsFrozen(false); QApplication::restoreOverrideCursor(); } void Playlist::slotReload() { QFileInfo fileInfo(m_fileName); if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable()) return; clearItems(items()); loadFile(m_fileName, fileInfo); } void Playlist::slotWeightDirty(int column) { if(column < 0) { m_weightDirty.clear(); for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) m_weightDirty.append(i); } return; } if(!m_weightDirty.contains(column)) m_weightDirty.append(column); } void Playlist::slotShowPlaying() { if(!playingItem()) return; Playlist *l = playingItem()->playlist(); l->clearSelection(); // Raise the playlist before selecting the items otherwise the tag editor // will not update when it gets the selectionChanged() notification // because it will think the user is choosing a different playlist but not // selecting a different item. m_collection->raise(l); l->setCurrentItem(playingItem()); l->scrollToItem(playingItem()); } void Playlist::slotColumnResizeModeChanged() { if(manualResize()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } if(!manualResize()) slotUpdateColumnWidths(); SharedSettings::instance()->sync(); } void Playlist::dataChanged() { if(m_blockDataChanged) return; PlaylistInterface::dataChanged(); } //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// void Playlist::removeFromDisk(const PlaylistItemList &items) { if(isVisible() && !items.isEmpty()) { QStringList files; foreach(const PlaylistItem *item, items) files.append(item->file().absFilePath()); DeleteDialog dialog(this); m_blockDataChanged = true; if(dialog.confirmDeleteList(files)) { bool shouldDelete = dialog.shouldDelete(); QStringList errorFiles; foreach(PlaylistItem *item, items) { if(playingItem() == item) action("forward")->trigger(); QString removePath = item->file().absFilePath(); if((!shouldDelete && KIO::NetAccess::synchronousRun(KIO::trash(removePath), this)) || (shouldDelete && QFile::remove(removePath))) { delete item->collectionItem(); } else errorFiles.append(item->file().absFilePath()); } if(!errorFiles.isEmpty()) { QString errorMsg = shouldDelete ? i18n("Could not delete these files") : i18n("Could not move these files to the Trash"); KMessageBox::errorList(this, errorMsg, errorFiles); } } m_blockDataChanged = false; dataChanged(); } } void Playlist::dragEnterEvent(QDragEnterEvent *e) { if(CoverDrag::isCover(e->mimeData())) { //setDropHighlighter(true); setDropIndicatorShown(false); e->accept(); return; } //setDropHighlighter(false); setDropIndicatorShown(true); const KUrl::List urls = KUrl::List::fromMimeData(e->mimeData()); if (urls.isEmpty()) { e->ignore(); return; } if(e->mimeData()->hasUrls()) e->acceptProposedAction(); return; } bool Playlist::acceptDrag(QDropEvent *e) const { return CoverDrag::isCover(e->mimeData()) || KUrl::List::canDecode(e->mimeData()); } void Playlist::decode(const QMimeData *s, PlaylistItem *item) { if(!KUrl::List::canDecode(s)) return; const KUrl::List urls = KUrl::List::fromMimeData(s); if(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(QLatin1String("image/"))) { item->file().coverInfo()->setCover(QImage(file)); refreshAlbum(item->file().tag()->artist(), item->file().tag()->album()); } KIO::NetAccess::removeTempFile(file); } QStringList fileList = MediaFiles::convertURLsToLocal(urls, this); addFiles(fileList, item); } bool Playlist::eventFilter(QObject *watched, QEvent *e) { if(watched == header()) { switch(e->type()) { case QEvent::MouseMove: { if((static_cast(e)->modifiers() & Qt::LeftButton) == Qt::LeftButton && !action("resizeColumnsManually")->isChecked()) { m_columnWidthModeChanged = true; action("resizeColumnsManually")->setChecked(true); slotColumnResizeModeChanged(); } break; } case QEvent::MouseButtonPress: { if(static_cast(e)->button() == Qt::RightButton) m_headerMenu->popup(QCursor::pos()); break; } case QEvent::MouseButtonRelease: { if(m_columnWidthModeChanged) { m_columnWidthModeChanged = false; notifyUserColumnWidthModeChanged(); } if(!manualResize() && m_widthsDirty) QTimer::singleShot(0, this, SLOT(slotUpdateColumnWidths())); break; } default: break; } } return QTreeWidget::eventFilter(watched, e); } void Playlist::keyPressEvent(QKeyEvent *event) { if(event->key() == Qt::Key_Up) { using ::operator|; QTreeWidgetItemIterator selected(this, QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); if(*selected) { QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden); if(*selected == *visible) QApplication::postEvent(parent(), new FocusUpEvent); } } QTreeWidget::keyPressEvent(event); } QStringList Playlist::mimeTypes() const { return QStringList("text/uri-list"); } QMimeData* Playlist::mimeData(const QList items) const { KUrl::List urls; foreach(QTreeWidgetItem *item, items) { urls << KUrl::fromPath(static_cast(item)->file().absFilePath()); } QMimeData *urlDrag = new QMimeData(); urlDrag->setUrls(urls); return urlDrag; } bool Playlist::dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action) { qCDebug(JUK_LOG) << index; return true; } void Playlist::dropEvent(QDropEvent *e) { QPoint vp = e->pos(); PlaylistItem *item = static_cast(itemAt(vp)); // First see if we're dropping a cover, if so we can get it out of the // way early. if(item && CoverDrag::isCover(e->mimeData())) { coverKey id = CoverDrag::idFromData(e->mimeData()); // If the item we dropped on is selected, apply cover to all selected // items, otherwise just apply to the dropped item. if(item->isSelected()) { const PlaylistItemList selItems = selectedItems(); foreach(PlaylistItem *playlistItem, selItems) { playlistItem->file().coverInfo()->setCoverId(id); playlistItem->refresh(); } } else { item->file().coverInfo()->setCoverId(id); item->refresh(); } return; } // When dropping on the toUpper half of an item, insert before this item. // This is what the user expects, and also allows the insertion at // top of the list QRect rect = visualItemRect(item); if(!item) item = static_cast(topLevelItem(topLevelItemCount() - 1)); else if(vp.y() < rect.y() + rect.height() / 2) item = static_cast(item->itemAbove()); m_blockDataChanged = true; if(e->source() == this) { // Since we're trying to arrange things manually, turn off sorting. sortItems(columnCount() + 1, Qt::AscendingOrder); const QList items = QTreeWidget::selectedItems(); foreach(QTreeWidgetItem *listViewItem, items) { if(!item) { // Insert the item at the top of the list. This is a bit ugly, // but I don't see another way. takeItem(listViewItem); insertItem(listViewItem); } //else // listViewItem->moveItem(item); item = static_cast(listViewItem); } } else decode(e->mimeData(), item); m_blockDataChanged = false; dataChanged(); emit signalPlaylistItemsDropped(this); QTreeWidget::dropEvent(e); } void Playlist::showEvent(QShowEvent *e) { if(m_applySharedSettings) { SharedSettings::instance()->apply(this); m_applySharedSettings = false; } QTreeWidget::showEvent(e); } void Playlist::applySharedSettings() { m_applySharedSettings = true; } void Playlist::read(QDataStream &s) { s >> m_playlistName >> m_fileName; // m_fileName is probably empty. if(m_playlistName.isEmpty()) throw BICStreamException(); // Do not sort. Add the files in the order they were saved. setSortingEnabled(false); QStringList files; s >> files; QTreeWidgetItem *after = 0; m_blockDataChanged = true; foreach(const QString &file, files) { if(file.isEmpty()) throw BICStreamException(); after = createItem(FileHandle(file), after, false); } m_blockDataChanged = false; dataChanged(); m_collection->setupPlaylist(this, "audio-midi"); } void Playlist::paintEvent(QPaintEvent *pe) { // If there are columns that need to be updated, well, update them. if(!m_weightDirty.isEmpty() && !manualResize()) { calculateColumnWeights(); slotUpdateColumnWidths(); } QTreeWidget::paintEvent(pe); } void Playlist::resizeEvent(QResizeEvent *re) { // If the width of the view has changed, manually update the column // widths. if(re->size().width() != re->oldSize().width() && !manualResize()) slotUpdateColumnWidths(); QTreeWidget::resizeEvent(re); } void Playlist::insertItem(QTreeWidgetItem *item) { // Because we're called from the PlaylistItem ctor, item may not be a // PlaylistItem yet (it would be QListViewItem when being inserted. But, // it will be a PlaylistItem by the time it matters, but be careful if // you need to use the PlaylistItem from here. m_addTime.append(static_cast(item)); - QTreeWidget::addTopLevelItem(item); + QTreeWidget::insertTopLevelItem(0, item); } void Playlist::takeItem(QTreeWidgetItem *item) { // See the warning in Playlist::insertItem. m_subtractTime.append(static_cast(item)); int index = indexOfTopLevelItem(item); - delete takeTopLevelItem(index); + QTreeWidget::takeTopLevelItem(index); } void Playlist::addColumn(const QString &label, int) { m_columns.append(label); setHeaderLabels(m_columns); } PlaylistItem *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after, bool emitChanged) { return createItem(file, after, emitChanged); } void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after) { createItems(siblings, after); } void Playlist::addFiles(const QStringList &files, PlaylistItem *after) { if(!after) after = static_cast(topLevelItem(topLevelItemCount() - 1)); QApplication::setOverrideCursor(Qt::WaitCursor); m_blockDataChanged = true; FileHandleList queue; foreach(const QString &file, files) addFile(file, queue, true, &after); addFileHelper(queue, &after, true); m_blockDataChanged = false; slotWeightDirty(); dataChanged(); QApplication::restoreOverrideCursor(); } void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id) { QList< QPair > albums; bool setAlbumCovers = items.count() == 1; foreach(const PlaylistItem *item, items) { QString artist = item->file().tag()->artist(); QString album = item->file().tag()->album(); if(!albums.contains(qMakePair(artist, album))) albums.append(qMakePair(artist, album)); item->file().coverInfo()->setCoverId(id); if(setAlbumCovers) item->file().coverInfo()->applyCoverToWholeAlbum(true); } for(QList< QPair >::ConstIterator it = albums.constBegin(); it != albums.constEnd(); ++it) { refreshAlbum((*it).first, (*it).second); } } void Playlist::updatePlaying() const { foreach(const PlaylistItem *item, PlaylistItem::playingItems()) item->treeWidget()->viewport()->update(); } void Playlist::refreshAlbum(const QString &artist, const QString &album) { ColumnList columns; columns.append(PlaylistItem::ArtistColumn); PlaylistSearch::Component artistComponent(artist, false, columns, PlaylistSearch::Component::Exact); columns.clear(); columns.append(PlaylistItem::AlbumColumn); PlaylistSearch::Component albumComponent(album, false, columns, PlaylistSearch::Component::Exact); PlaylistSearch::ComponentList components; components.append(artist); components.append(album); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components); const PlaylistItemList matches = search.matchedItems(); foreach(PlaylistItem *item, matches) item->refresh(); } void Playlist::hideColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(false); break; } } if(isColumnHidden(c)) return; QTreeWidget::hideColumn(c); if(c == m_leftColumn) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->hideColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::showColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(true); break; } } if(!isColumnHidden(c)) return; QTreeWidget::showColumn(c); if(c == leftMostVisibleColumn()) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->showColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::sortByColumn(int column, Qt::SortOrder order) { setSortingEnabled(true); QTreeWidget::sortByColumn(column, order); } void Playlist::slotInitialize() { addColumn(i18n("Track Name")); addColumn(i18n("Artist")); addColumn(i18n("Album")); addColumn(i18n("Cover")); addColumn(i18nc("cd track number", "Track")); addColumn(i18n("Genre")); addColumn(i18n("Year")); addColumn(i18n("Length")); addColumn(i18n("Bitrate")); addColumn(i18n("Comment")); addColumn(i18n("File Name")); addColumn(i18n("File Name (full path)")); // FIXME rename /*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(QTreeWidget::ExtendedSelection); header()->setSortIndicatorShown(true); m_columnFixedWidths.resize(columnCount()); ////////////////////////////////////////////////// // setup header RMB menu ////////////////////////////////////////////////// #ifdef __GNUC__ #warning should be fixed... #endif /* m_headerMenu->insertTitle(i18n("Show")); */ QAction *showAction; for(int i = 0; i < header()->count(); ++i) { if(i - columnOffset() == PlaylistItem::FileNameColumn) m_headerMenu->addSeparator(); showAction = new QAction(headerItem()->text(i), m_headerMenu); showAction->setData(i); showAction->setCheckable(true); showAction->setChecked(true); m_headerMenu->addAction(showAction); resizeColumnToContents(i); } connect(m_headerMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotToggleColumnVisible(QAction*))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowRMBMenu(QPoint))); // FIXME rename /*connect(this, SIGNAL(itemRenamed(QTreeWidgetItem*,QString,int)), this, SLOT(slotInlineEditDone(QTreeWidgetItem*,QString,int)));*/ connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotPlayCurrent())); // FIXME returnPressed /*connect(this, SIGNAL(returnPressed(QTreeWidgetItem*)), this, SLOT(slotPlayCurrent()));*/ // FIXME rename /*connect(renameLineEdit(), SIGNAL(completionModeChanged(KGlobalSettings::Completion)), this, SLOT(slotInlineCompletionModeChanged(KGlobalSettings::Completion)));*/ connect(action("resizeColumnsManually"), SIGNAL(triggered()), this, SLOT(slotColumnResizeModeChanged())); if(action("resizeColumnsManually")->isChecked()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragEnabled(true); m_disableColumnWidthUpdates = false; // FIXME tooltips //setShowToolTips(false); /* m_toolTip = new PlaylistToolTip(viewport(), this); */ } void Playlist::setupItem(PlaylistItem *item) { item->setTrackId(g_trackID); g_trackID++; if(!m_search.isEmpty()) item->setHidden(!m_search.checkItem(item)); if(topLevelItemCount() <= 2 && !manualResize()) { slotWeightDirty(); slotUpdateColumnWidths(); viewport()->update(); } } void Playlist::setDynamicListsFrozen(bool frozen) { m_collection->setDynamicListsFrozen(frozen); } CollectionListItem *Playlist::collectionListItem(const FileHandle &file) { if(!QFile::exists(file.absFilePath())) { qCCritical(JUK_LOG) << "File" << file.absFilePath() << "does not exist."; return 0; } CollectionListItem *item = CollectionList::instance()->lookup(file.absFilePath()); if(!item) { item = CollectionList::instance()->createItem(file); } return item; } //////////////////////////////////////////////////////////////////////////////// // protected slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotPopulateBackMenu() const { if(!playingItem()) return; QMenu *menu = action("back")->menu(); menu->clear(); m_backMenuItems.clear(); m_backMenuItems.reserve(10); int count = 0; PlaylistItemList::ConstIterator it = m_history.constEnd(); QAction *action; while(it != m_history.constBegin() && count < 10) { ++count; --it; action = new QAction((*it)->file().tag()->title(), menu); action->setData(count - 1); menu->addAction(action); m_backMenuItems << *it; } } void Playlist::slotPlayFromBackMenu(QAction *backAction) const { int number = backAction->data().toInt(); if(number >= m_backMenuItems.size()) return; TrackSequenceManager::instance()->setNextItem(m_backMenuItems[number]); action("forward")->trigger(); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void Playlist::setup() { setRootIsDecorated(false); setContextMenuPolicy(Qt::CustomContextMenu); connect(header(), SIGNAL(sectionMoved(int,int,int)), this, SLOT(slotColumnOrderChanged(int,int,int))); connect(m_fetcher, SIGNAL(signalCoverChanged(int)), this, SLOT(slotCoverChanged(int))); // Prevent list of selected items from changing while internet search is in // progress. connect(this, SIGNAL(itemSelectionChanged()), m_fetcher, SLOT(abortSearch())); sortByColumn(1, Qt::AscendingOrder); // This apparently must be created very early in initialization for other // Playlist code requiring m_headerMenu. m_columnVisibleAction = new KActionMenu(i18n("&Show Columns"), this); ActionCollection::actions()->addAction("showColumns", m_columnVisibleAction); m_headerMenu = m_columnVisibleAction->menu(); header()->installEventFilter(this); // TODO: Determine if other stuff in setup must happen before slotInitialize(). // Explicitly call slotInitialize() so that the columns are added before // SharedSettings::apply() sets the visible and hidden ones. slotInitialize(); } void Playlist::loadFile(const QString &fileName, const QFileInfo &fileInfo) { QFile file(fileName); if(!file.open(QIODevice::ReadOnly)) return; QTextStream stream(&file); // Turn off non-explicit sorting. setSortingEnabled(false); PlaylistItem *after = 0; m_disableColumnWidthUpdates = true; m_blockDataChanged = true; while(!stream.atEnd()) { QString itemName = stream.readLine().trimmed(); QFileInfo item(itemName); if(item.isRelative()) item.setFile(QDir::cleanPath(fileInfo.absolutePath() + '/' + itemName)); if(item.exists() && item.isFile() && item.isReadable() && MediaFiles::isMediaFile(item.fileName())) { if(after) after = createItem(FileHandle(item, item.absoluteFilePath()), after, false); else after = createItem(FileHandle(item, item.absoluteFilePath()), 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); #ifdef __GNUC__ #warning "kde4: port it" #endif //kapp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data); if(!item) return; item->setPlaying(true); bool enableBack = !m_history.isEmpty(); action("back")->menu()->setEnabled(enableBack); } bool Playlist::playing() const { return playingItem() && this == playingItem()->playlist(); } int Playlist::leftMostVisibleColumn() const { int i = 0; while(isColumnHidden(header()->visualIndex(i)) && i < PlaylistItem::lastColumn()) i++; return header()->visualIndex(i); } PlaylistItemList Playlist::items(QTreeWidgetItemIterator::IteratorFlags flags) { PlaylistItemList list; for(QTreeWidgetItemIterator it(this, flags); *it; ++it) list.append(static_cast(*it)); return list; } void Playlist::calculateColumnWeights() { if(m_disableColumnWidthUpdates) return; PlaylistItemList l = items(); QList::Iterator columnIt; QVector averageWidth(columnCount()); double itemCount = l.size(); QVector cachedWidth; // Here we're not using a real average, but averaging the squares of the // column widths and then using the square root of that value. This gives // a nice weighting to the longer columns without doing something arbitrary // like adding a fixed amount of padding. foreach(PlaylistItem *item, l) { cachedWidth = item->cachedWidths(); // Extra columns start at 0, but those weights aren't shared with all // items. for(int i = 0; i < columnOffset(); ++i) { averageWidth[i] += std::pow(double(columnWidth(i)), 2.0) / itemCount; } for(int column = columnOffset(); column < columnCount(); ++column) { averageWidth[column] += std::pow(double(cachedWidth[column - columnOffset()]), 2.0) / itemCount; } } if(m_columnWeights.isEmpty()) m_columnWeights.fill(-1, columnCount()); foreach(int column, m_weightDirty) { m_columnWeights[column] = int(std::sqrt(averageWidth[column]) + 0.5); } m_weightDirty.clear(); } void Playlist::addFile(const QString &file, FileHandleList &files, bool importPlaylists, PlaylistItem **after) { if(hasItem(file) && !m_allowDuplicates) return; 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. const CollectionListItem *item = CollectionList::instance()->lookup(file); if(item && !item->file().isNull()) { FileHandle cached(item->file()); cached.tag(); files.append(cached); return; } const QFileInfo fileInfo(QDir::cleanPath(file)); if(!fileInfo.exists()) return; const QString canonicalPath = fileInfo.canonicalFilePath(); if(fileInfo.isFile() && fileInfo.isReadable()) { if(MediaFiles::isMediaFile(file)) { FileHandle f(fileInfo, canonicalPath); f.tag(); files.append(f); } } if(importPlaylists && MediaFiles::isPlaylistFile(file) && !m_collection->containsPlaylistFile(canonicalPath)) { new Playlist(m_collection, fileInfo); return; } if(fileInfo.isDir()) { foreach(const QString &directory, m_collection->excludedFolders()) { if(canonicalPath.startsWith(directory)) return; // Exclude it } QDirIterator dirIterator(canonicalPath, QDir::AllEntries | QDir::NoDotAndDotDot); while(dirIterator.hasNext()) { // 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 toLower // directories. addFile(dirIterator.next(), files, m_collection->importPlaylists(), after); } } } 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(); foreach(const FileHandle &fileHandle, files) *after = createItem(fileHandle, *after, false); files.clear(); if(visible) m_collection->lowerDistraction(); if(focus) setFocus(); } } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotUpdateColumnWidths() { if(m_disableColumnWidthUpdates || manualResize()) return; // Make sure that the column weights have been initialized before trying to // update the columns. QList visibleColumns; for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) visibleColumns.append(i); } if(count() == 0) { foreach(int column, visibleColumns) setColumnWidth(column, header()->fontMetrics().width(headerItem()->text(column)) + 10); return; } if(m_columnWeights.isEmpty()) return; // First build a list of minimum widths based on the strings in the listview // header. We won't let the width of the column go below this width. QVector minimumWidth(columnCount(), 0); int minimumWidthTotal = 0; // Also build a list of either the minimum *or* the fixed width -- whichever is // greater. QVector minimumFixedWidth(columnCount(), 0); int minimumFixedWidthTotal = 0; foreach(int column, visibleColumns) { minimumWidth[column] = header()->fontMetrics().width(headerItem()->text(column)) + 10; minimumWidthTotal += minimumWidth[column]; minimumFixedWidth[column] = qMax(minimumWidth[column], m_columnFixedWidths[column]); minimumFixedWidthTotal += minimumFixedWidth[column]; } // Make sure that the width won't get any smaller than this. We have to // account for the scrollbar as well. Since this method is called from the // resize event this will set a pretty hard toLower bound on the size. setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width()); // If we've got enough room for the fixed widths (larger than the minimum // widths) then instead use those for our "minimum widths". if(minimumFixedWidthTotal < viewport()->width()) { minimumWidth = minimumFixedWidth; minimumWidthTotal = minimumFixedWidthTotal; } // We've got a list of columns "weights" based on some statistics gathered // about the widths of the items in that column. We need to find the total // useful weight to use as a divisor for each column's weight. double totalWeight = 0; foreach(int column, visibleColumns) totalWeight += m_columnWeights[column]; // Computed a "weighted width" for each visible column. This would be the // width if we didn't have to handle the cases of minimum and maximum widths. QVector weightedWidth(columnCount(), 0); foreach(int column, visibleColumns) weightedWidth[column] = int(double(m_columnWeights[column]) / totalWeight * viewport()->width() + 0.5); // The "extra" width for each column. This is the weighted width less the // minimum width or zero if the minimum width is greater than the weighted // width. QVector extraWidth(columnCount(), 0); // This is used as an indicator if we have any columns where the weighted // width is less than the minimum width. If this is false then we can // just use the weighted width with no problems, otherwise we have to // "readjust" the widths. bool readjust = false; // If we have columns where the weighted width is less than the minimum width // we need to steal that space from somewhere. The amount that we need to // steal is the "neededWidth". int neededWidth = 0; // While we're on the topic of stealing -- we have to have somewhere to steal // from. availableWidth is the sum of the amount of space beyond the minimum // width that each column has been allocated -- the sum of the values of // extraWidth[]. int availableWidth = 0; // Fill in the values discussed above. foreach(int column, visibleColumns) { if(weightedWidth[column] < minimumWidth[column]) { readjust = true; extraWidth[column] = 0; neededWidth += minimumWidth[column] - weightedWidth[column]; } else { extraWidth[column] = weightedWidth[column] - minimumWidth[column]; availableWidth += extraWidth[column]; } } // The adjustmentRatio is the amount of the "extraWidth[]" that columns will // actually be given. double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth); // This will be the sum of the total space that we actually use. Because of // rounding error this won't be the exact available width. int usedWidth = 0; // Now set the actual column widths. If the weighted widths are all greater // than the minimum widths, just use those, otherwise use the "reajusted // weighted width". foreach(int column, visibleColumns) { int width; if(readjust) { int adjustedExtraWidth = int(double(extraWidth[column]) * adjustmentRatio + 0.5); width = minimumWidth[column] + adjustedExtraWidth; } else width = weightedWidth[column]; setColumnWidth(column, width); usedWidth += width; } // Fill the remaining gap for a clean fit into the available space. int remainingWidth = viewport()->width() - usedWidth; setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth); m_widthsDirty = false; } void Playlist::slotAddToUpcoming() { m_collection->setUpcomingPlaylistEnabled(true); m_collection->upcomingPlaylist()->appendItems(selectedItems()); } void Playlist::slotShowRMBMenu(const QPoint &point) { QTreeWidgetItem *item = itemAt(point); int column = columnAt(point.x()); if(!item) return; // Create the RMB menu on demand. if(!m_rmbMenu) { // Probably more of these actions should be ported over to using KActions. m_rmbMenu = new QMenu(this); m_rmbMenu->addAction(SmallIcon("go-jump-today"), i18n("Add to Play Queue"), this, SLOT(slotAddToUpcoming())); m_rmbMenu->addSeparator(); if(!readOnly()) { m_rmbMenu->addAction( action("edit_cut") ); m_rmbMenu->addAction( action("edit_copy") ); m_rmbMenu->addAction( action("edit_paste") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("removeFromPlaylist") ); } else m_rmbMenu->addAction( action("edit_copy") ); m_rmbEdit = m_rmbMenu->addAction(i18n("Edit"), this, SLOT(slotRenameTag())); m_rmbMenu->addAction( action("refresh") ); m_rmbMenu->addAction( action("removeItem") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("guessTag") ); m_rmbMenu->addAction( action("renameFile") ); m_rmbMenu->addAction( action("coverManager") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( SmallIcon("folder-new"), i18n("Create Playlist From Selected Items..."), this, SLOT(slotCreateGroup())); } // 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_rmbEdit->setText(i18n("Edit '%1'", item->text(column + columnOffset()))); m_rmbEdit->setVisible(showEdit); // Disable edit menu if only one file is selected, and it's read-only FileHandle file = static_cast(item)->file(); m_rmbEdit->setEnabled(file.fileInfo().isWritable() || selectedItems().count() > 1); // View cover is based on if there is a cover to see. We should only have // the remove cover option if the cover is in our database (and not directly // embedded in the file, for instance). action("viewCover")->setEnabled(file.coverInfo()->hasCover()); action("removeCover")->setEnabled(file.coverInfo()->coverId() != CoverManager::NoMatch); m_rmbMenu->popup(mapToGlobal(point)); m_currentColumn = column + columnOffset(); } void Playlist::slotRenameTag() { // setup completions and validators // FIXME rename /*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::Iterator 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(QTreeWidgetItem *, const QString &, int column) { // FIXME rename /*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.constBegin(); it != l.constEnd() && !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(), KGuiItem(i18n("Edit")), KStandardGuiItem::cancel(), "DontWarnMultipleTags") == KMessageBox::Cancel)) { return; } foreach(PlaylistItem *item, l) editTag(item, 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()->sectionPosition(0); } SharedSettings::instance()->setColumnOrder(this); } void Playlist::slotToggleColumnVisible(QAction *action) { int column = action->data().toInt(); if(isColumnHidden(column)) { int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset(); int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset(); if(column == fileNameColumn && !isColumnHidden(fullPathColumn)) { hideColumn(fullPathColumn, false); SharedSettings::instance()->toggleColumnVisible(fullPathColumn); } if(column == fullPathColumn && !isColumnHidden(fileNameColumn)) { hideColumn(fileNameColumn, false); SharedSettings::instance()->toggleColumnVisible(fileNameColumn); } } if(!isColumnHidden(column)) hideColumn(column); else showColumn(column); if(column >= columnOffset()) { SharedSettings::instance()->toggleColumnVisible(column - columnOffset()); } } void Playlist::slotCreateGroup() { QString name = m_collection->playlistNameDialog(i18n("Create New Playlist")); if(!name.isEmpty()) new Playlist(m_collection, selectedItems(), name); } void Playlist::notifyUserColumnWidthModeChanged() { KMessageBox::information(this, i18n("Manual column widths have been enabled. You can " "switch back to automatic column sizes in the view " "menu."), i18n("Manual Column Widths Enabled"), "ShowManualColumnWidthInformation"); } void Playlist::columnResized(int column, int, int newSize) { m_widthsDirty = true; m_columnFixedWidths[column] = newSize; } void Playlist::slotInlineCompletionModeChanged(KGlobalSettings::Completion mode) { SharedSettings::instance()->setInlineCompletionMode(mode); } void Playlist::slotPlayCurrent() { QTreeWidgetItemIterator it(this, QTreeWidgetItemIterator::Selected); PlaylistItem *next = static_cast(*it); TrackSequenceManager::instance()->setNextItem(next); action("forward")->trigger(); } //////////////////////////////////////////////////////////////////////////////// // helper functions //////////////////////////////////////////////////////////////////////////////// QDataStream &operator<<(QDataStream &s, const Playlist &p) { s << p.name(); s << p.fileName(); s << p.files(); return s; } QDataStream &operator>>(QDataStream &s, Playlist &p) { p.read(s); return s; } bool processEvents() { static QTime time = QTime::currentTime(); if(time.elapsed() > 100) { time.restart(); qApp->processEvents(); return true; } return false; } // vim: set et sw=4 tw=0 sta: diff --git a/playlist.h b/playlist.h index f45b462c..c14180ef 100644 --- a/playlist.h +++ b/playlist.h @@ -1,789 +1,781 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2007 Michael Pyne * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #ifndef PLAYLIST_H #define PLAYLIST_H #include #include #include #include #include #include "covermanager.h" #include "stringhash.h" #include "playlistsearch.h" #include "tagguesser.h" #include "playlistinterface.h" #include "filehandle.h" #include "juk_debug.h" class KMenu; class KActionMenu; class QFileInfo; class QMimeData; class QAction; class WebImageFetcher; class PlaylistItem; class PlaylistCollection; class PlaylistToolTip; class CollectionListItem; typedef QList PlaylistItemList; class Playlist : public QTreeWidget, public PlaylistInterface { Q_OBJECT public: explicit Playlist(PlaylistCollection *collection, const QString &name = QString(), const QString &iconName = "audio-midi"); Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const QString &name = QString(), const QString &iconName = "audio-midi"); Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, const QString &iconName = "audio-midi"); /** * This constructor should generally only be used either by the cache * restoration methods or by subclasses that want to handle calls to * PlaylistCollection::setupPlaylist() differently. * * @param extraColumns is used to preallocate columns for subclasses that * need them (since extra columns are assumed to start from 0). extraColumns * should be equal to columnOffset() (we can't use columnOffset until the * ctor has run). */ Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns = 0); virtual ~Playlist(); // The following group of functions implement the PlaylistInterface API. virtual QString name() const; virtual FileHandle currentFile() const; virtual int count() const { return model()->rowCount(); } virtual int time() const; virtual void playNext(); virtual void playPrevious(); virtual void stop(); /** * Plays the top item of the playlist. */ void playFirst(); /** * Plays the next album in the playlist. Only useful when in album random * play mode. */ void playNextAlbum(); /** * Saves the file to the currently set file name. If there is no filename * currently set, the default behavior is to prompt the user for a file * name. */ virtual void save(); /** * Standard "save as". Prompts the user for a location where to save the * playlist to. */ virtual void saveAs(); /** * Removes \a item from the Playlist, but not from the disk. * * Since the GUI updates after an item is cleared, you should use clearItems() if you have * a list of items to remove, as that will remove the whole batch before updating * other components/GUI to the change. */ virtual void clearItem(PlaylistItem *item); /** * Remove \a items from the playlist and emit a signal indicating * that the number of items in the list has changed. */ virtual void clearItems(const PlaylistItemList &items); /** * Accessor function to return a pointer to the currently playing file. * * @return 0 if no file is playing, otherwise a pointer to the PlaylistItem * of the track that is currently playing. */ static PlaylistItem *playingItem(); /** * All of the (media) files in the list. */ QStringList files() const; /** * Returns a list of all of the items in the playlist. */ virtual PlaylistItemList items(); /** * Returns a list of all of the \e visible items in the playlist. */ PlaylistItemList visibleItems(); /** * Returns a list of the currently selected items. */ PlaylistItemList selectedItems(); /** * Returns properly casted first child item in list. */ PlaylistItem *firstChild() const; /** * Allow duplicate files in the playlist. */ void setAllowDuplicates(bool allow) { m_allowDuplicates = allow; } /** * This is being used as a mini-factory of sorts to make the construction * of PlaylistItems virtual. In this case it allows for the creation of * both PlaylistItems and CollectionListItems. */ virtual PlaylistItem *createItem(const FileHandle &file, QTreeWidgetItem *after = 0, bool emitChanged = true); /** * This is implemented as a template method to allow subclasses to * instantiate their PlaylistItem subclasses using the same method. */ template ItemType *createItem(const FileHandle &file, QTreeWidgetItem *after = 0, bool emitChanged = true); virtual void createItems(const PlaylistItemList &siblings, PlaylistItem *after = 0); /** * This handles adding files of various types -- music, playlist or directory * files. Music files that are found will be added to this playlist. New * playlist files that are found will result in new playlists being created. * * Note that this should not be used in the case of adding *only* playlist * items since it has the overhead of checking to see if the file is a playlist * or directory first. */ virtual void addFiles(const QStringList &files, PlaylistItem *after = 0); /** * Returns the file name associated with this playlist (an m3u file) or * an empty QString if no such file exists. */ QString fileName() const { return m_fileName; } /** * Sets the file name to be associated with this playlist; this file should * have the "m3u" extension. */ void setFileName(const QString &n) { m_fileName = n; } /** * Hides column \a c. If \a updateSearch is true then a signal that the * visible columns have changed will be emitted and things like the search * will be udated. */ void hideColumn(int c, bool updateSearch = true); /** * Shows column \a c. If \a updateSearch is true then a signal that the * visible columns have changed will be emitted and things like the search * will be udated. */ void showColumn(int c, bool updateSearch = true); void sortByColumn(int column, Qt::SortOrder order = Qt::AscendingOrder); /** * This sets a name for the playlist that is \e different from the file name. */ void setName(const QString &n); /** * Returns the KActionMenu that allows this to be embedded in menus outside * of the playlist. */ KActionMenu *columnVisibleAction() const { return m_columnVisibleAction; } /** * Set item to be the playing item. If \a item is null then this will clear * the playing indicator. */ static void setPlaying(PlaylistItem *item, bool addToHistory = true); /** * Returns true if this playlist is currently playing. */ bool playing() const; /** * This forces an update of the left most visible column, but does not save * the settings for this. */ void updateLeftColumn(); /** * Returns the leftmost visible column of the listview. */ int leftColumn() const { return m_leftColumn; } /** * Sets the items in the list to be either visible based on the value of * visible. This is useful for search operations and such. */ static void setItemsVisible(const PlaylistItemList &items, bool visible = true); /** * Returns the search associated with this list, or an empty search if one * has not yet been set. */ PlaylistSearch search() const { return m_search; } /** * Set the search associtated with this playlist. */ void setSearch(const PlaylistSearch &s); /** * If the search is disabled then all items will be shown, not just those that * match the current search. */ void setSearchEnabled(bool searchEnabled); - /** - * Marks \a item as either selected or deselected based. - */ - void markItemSelected(PlaylistItem *item, bool selected); - /** * Subclasses of Playlist which add new columns will set this value to * specify how many of those columns exist. This allows the Playlist * class to do some internal calculations on the number and positions * of columns. */ virtual int columnOffset() const { return 0; } /** * Some subclasses of Playlist will be "read only" lists (i.e. the history * playlist). This is a way for those subclasses to indicate that to the * Playlist internals. */ virtual bool readOnly() const { return false; } /** * Returns true if it's possible to reload this playlist. */ virtual bool canReload() const { return !m_fileName.isEmpty(); } /** * Returns true if the playlist is a search playlist and the search should be * editable. */ virtual bool searchIsEditable() const { return false; } /** * Synchronizes the playing item in this playlist with the playing item * in \a sources. If \a setMaster is true, this list will become the source * for determining the next item. */ void synchronizePlayingItems(const PlaylistList &sources, bool setMaster); /** * Playlists have a common set of shared settings such as visible columns * that should be applied just before the playlist is shown. Calling this * method applies those. */ void applySharedSettings(); void read(QDataStream &s); static void setShuttingDown() { m_shuttingDown = true; } public slots: /** * Remove the currently selected items from the playlist and disk. */ void slotRemoveSelectedItems() { removeFromDisk(selectedItems()); } /* * The edit slots are required to use the canonical names so that they are * detected by the application wide framework. */ virtual void cut() { copy(); clear(); } /** * Puts a list of URLs pointing to the files in the current selection on the * clipboard. */ virtual void copy(); /** * Checks the clipboard for local URLs to be inserted into this playlist. */ virtual void paste(); /** * Removes the selected items from the list, but not the disk. * * @see clearItem() * @see clearItems() */ virtual void clear(); virtual void selectAll() { QTreeView::selectAll(); } /** * Refreshes the tags of the selection from disk, or all of the files in the * list if there is no selection. */ virtual void slotRefresh(); void slotGuessTagInfo(TagGuesser::Type type); /** * Renames the selected items' files based on their tags contents. * * @see PlaylistItem::renameFile() */ void slotRenameFile(); /** * Sets the cover of the selected items, pass in true if you want to load from the local system, * false if you want to load from the internet. */ void slotAddCover(bool fromLocal); /** * Shows a large image of the cover */ void slotViewCover(); /** * Removes covers from the selected items */ void slotRemoveCover(); /** * Shows the cover manager GUI dialog */ void slotShowCoverManager(); /** * Reload the playlist contents from the m3u file. */ virtual void slotReload(); /** * Tells the listview that the next time that it paints that the weighted * column widths must be recalculated. If this is called without a column * all visible columns are marked as dirty. */ void slotWeightDirty(int column = -1); void slotShowPlaying(); void slotColumnResizeModeChanged(); virtual void dataChanged(); protected: /** * Remove \a items from the playlist and disk. This will ignore items that * are not actually in the list. */ void removeFromDisk(const PlaylistItemList &items); // the following are all reimplemented from base classes virtual bool eventFilter(QObject *watched, QEvent *e); virtual void keyPressEvent(QKeyEvent *e); virtual void decode(const QMimeData *s, PlaylistItem *item = 0); QStringList mimeTypes() const; QMimeData* mimeData(const QList items) const; virtual bool dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action); virtual void dropEvent(QDropEvent *e); virtual void dragEnterEvent(QDragEnterEvent *e); virtual void showEvent(QShowEvent *e); virtual bool acceptDrag(QDropEvent *e) const; virtual void paintEvent(QPaintEvent *pe); virtual void resizeEvent(QResizeEvent *re); virtual void insertItem(QTreeWidgetItem *item); virtual void takeItem(QTreeWidgetItem *item); virtual bool hasItem(const QString &file) const { return m_members.contains(file); } virtual void addColumn(const QString &label, int width = -1); /** * Do some finial initialization of created items. Notably ensure that they * are shown or hidden based on the contents of the current PlaylistSearch. * * This is called by the PlaylistItem constructor. */ void setupItem(PlaylistItem *item); /** * Forwards the call to the parent to enable or disable automatic deletion * of tree view playlists. Used by CollectionListItem. */ void setDynamicListsFrozen(bool frozen); template ItemType *createItem(SiblingType *sibling, ItemType *after = 0); /** * As a template this allows us to use the same code to initialize the items * in subclasses. ItemType should be a PlaylistItem subclass. */ template void createItems(const QList &siblings, ItemType *after = 0); protected slots: void slotPopulateBackMenu() const; void slotPlayFromBackMenu(QAction *) const; signals: /** * This is connected to the PlaylistBox::Item to let it know when the * playlist's name has changed. */ void signalNameChanged(const QString &name); /** * This signal is emitted just before a playlist item is removed from the * list allowing for any cleanup that needs to happen. Typically this * is used to remove the item from the history and safeguard against * dangling pointers. */ void signalAboutToRemove(PlaylistItem *item); void signalEnableDirWatch(bool enable); void signalPlaylistItemsDropped(Playlist *p); private: void setup(); /** * This function is called to let the user know that JuK has automatically enabled * manual column width adjust mode. */ void notifyUserColumnWidthModeChanged(); /** * Load the playlist from a file. \a fileName should be the absolute path. * \a fileInfo should point to the same file as \a fileName. This is a * little awkward API-wise, but keeps us from throwing away useful * information. */ void loadFile(const QString &fileName, const QFileInfo &fileInfo); /** * Writes \a text to \a item in \a column. This is used by the inline tag * editor. Returns false if the tag update failed. */ bool editTag(PlaylistItem *item, const QString &text, int column); /** * Returns the index of the left most visible column in the playlist. * * \see isColumnVisible() */ int leftMostVisibleColumn() const; /** * This method is used internally to provide the backend to the other item * lists. * * \see items() * \see visibleItems() * \see selectedItems() */ PlaylistItemList items(QTreeWidgetItemIterator::IteratorFlags flags); /** * Build the column "weights" for the weighted width mode. */ void calculateColumnWeights(); void addFile(const QString &file, FileHandleList &files, bool importPlaylists, PlaylistItem **after); void addFileHelper(FileHandleList &files, PlaylistItem **after, bool ignoreTimer = false); void redisplaySearch() { setSearch(m_search); } /** * Sets the cover for items to the cover identified by id. */ void refreshAlbums(const PlaylistItemList &items, coverKey id = CoverManager::NoMatch); void refreshAlbum(const QString &artist, const QString &album); void updatePlaying() const; /** * This function should be called when item is deleted to ensure that any * internal bookkeeping is performed. It is automatically called by * PlaylistItem::~PlaylistItem and by clearItem() and clearItems(). */ void updateDeletedItem(PlaylistItem *item); /** * Used as a helper to implement template<> createItem(). This grabs the * CollectionListItem for file if it exists, otherwise it creates a new one and * returns that. If 0 is returned then some kind of error occurred, such as file not * found and probably nothing should be done with the FileHandle you have. */ CollectionListItem *collectionListItem(const FileHandle &file); /** * This class is used internally to store settings that are shared by all * of the playlists, such as column order. It is implemented as a singleton. */ class SharedSettings; private slots: /** * Handle the necessary tasks needed to create and setup the playlist that * don't need to happen in the ctor, such as setting up the columns, * initializing the RMB menu, and setting up signal/slot connections. * * Used to be a subclass of K3ListView::polish() but the timing of the * call is not consistent and therefore lead to crashes. */ void slotInitialize(); void slotUpdateColumnWidths(); void slotAddToUpcoming(); /** * Show the RMB menu. Matches the signature for the signal * QListView::contextMenuRequested(). */ void slotShowRMBMenu(const QPoint &point); /** * This slot is called when the inline tag editor has completed its editing * and starts the process of renaming the values. * * \see editTag() */ void slotInlineEditDone(QTreeWidgetItem *, const QString &, int column); /** * This starts the renaming process by displaying a line edit if the mouse is in * an appropriate position. */ void slotRenameTag(); /** * The image fetcher will update the cover asynchronously, this internal * slot is called when it happens. */ void slotCoverChanged(int coverId); /** * Moves the column \a from to the position \a to. This matches the signature * for the signal QHeader::indexChange(). */ void slotColumnOrderChanged(int, int from, int to); /** * Toggles a columns visible status. Useful for KActions. * * \see hideColumn() * \see showColumn() */ void slotToggleColumnVisible(QAction *action); /** * Prompts the user to create a new playlist with from the selected items. */ void slotCreateGroup(); /** * This slot is called when the user drags the slider in the listview header * to manually set the size of the column. */ void columnResized(int column, int oldSize, int newSize); /** * The slot is called when the completion mode for the line edit in the * inline tag editor is changed. It saves the settings and through the * magic of the SharedSettings class will apply it to the other playlists as * well. */ void slotInlineCompletionModeChanged(KGlobalSettings::Completion mode); void slotPlayCurrent(); private: friend class PlaylistItem; PlaylistCollection *m_collection; StringHash m_members; WebImageFetcher *m_fetcher; int m_currentColumn; QAction *m_rmbEdit; - int m_selectedCount; bool m_allowDuplicates; bool m_applySharedSettings; bool m_columnWidthModeChanged; QList m_weightDirty; bool m_disableColumnWidthUpdates; mutable int m_time; mutable PlaylistItemList m_addTime; mutable PlaylistItemList m_subtractTime; /** * The average minimum widths of columns to be used in balancing calculations. */ QVector m_columnWeights; QVector m_columnFixedWidths; bool m_widthsDirty; static PlaylistItemList m_history; PlaylistSearch m_search; bool m_searchEnabled; - PlaylistItem *m_lastSelected; - /** * Used to store the text for inline editing before it is changed so that * we can know if something actually changed and as such if we need to save * the tag. */ QString m_editText; /** * This is only defined if the playlist name is something other than the * file name. */ QString m_playlistName; QString m_fileName; QStringList m_columns; QMenu *m_rmbMenu; QMenu *m_headerMenu; KActionMenu *m_columnVisibleAction; PlaylistToolTip *m_toolTip; /** * This is used to indicate if the list of visible items has changed (via a * call to setVisibleItems()) while random play is playing. */ static bool m_visibleChanged; static bool m_shuttingDown; static int m_leftColumn; static QVector m_backMenuItems; bool m_blockDataChanged; }; typedef QList PlaylistList; bool processEvents(); class FocusUpEvent : public QEvent { public: FocusUpEvent() : QEvent(id) {} Type type() const { return id; } static const Type id = static_cast(QEvent::User + 1); }; QDataStream &operator<<(QDataStream &s, const Playlist &p); QDataStream &operator>>(QDataStream &s, Playlist &p); // template method implementations template ItemType *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after, bool emitChanged) { CollectionListItem *item = collectionListItem(file); if(item && (!m_members.insert(file.absFilePath()) || m_allowDuplicates)) { ItemType *i = after ? new ItemType(item, this, after) : new ItemType(item, this); setupItem(i); if(emitChanged) dataChanged(); return i; } else return 0; } template ItemType *Playlist::createItem(SiblingType *sibling, ItemType *after) { m_disableColumnWidthUpdates = true; if(!m_members.insert(sibling->file().absFilePath()) || m_allowDuplicates) { after = new ItemType(sibling->collectionItem(), this, after); setupItem(after); } m_disableColumnWidthUpdates = false; return after; } template void Playlist::createItems(const QList &siblings, ItemType *after) { if(siblings.isEmpty()) return; foreach(SiblingType *sibling, siblings) after = createItem(sibling, after); dataChanged(); slotWeightDirty(); } #endif // vim: set et sw=4 tw=0 sta: diff --git a/playlistitem.cpp b/playlistitem.cpp index c4662fb6..a5ef6b0d 100644 --- a/playlistitem.cpp +++ b/playlistitem.cpp @@ -1,489 +1,483 @@ /** * 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 "playlistitem.h" #include #include #include #include #include #include "collectionlist.h" #include "musicbrainzquery.h" #include "tag.h" #include "coverinfo.h" #include "covermanager.h" #include "tagtransactionmanager.h" #include "juk_debug.h" PlaylistItemList PlaylistItem::m_playingItems; // static static void startMusicBrainzQuery(const FileHandle &file) { #if HAVE_TUNEPIMP // This deletes itself when finished. new MusicBrainzLookup(file); #else Q_UNUSED(file) #endif } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem public methods //////////////////////////////////////////////////////////////////////////////// PlaylistItem::~PlaylistItem() { // Although this isn't the most efficient way to accomplish the task of // stopping playback when deleting the item being played, it has the // stark advantage of working reliably. I'll tell anyone who tries to // optimize this, the timing issues can be *hard*. -- mpyne m_collectionItem->removeChildItem(this); if(m_playingItems.contains(this)) { m_playingItems.removeAll(this); if(m_playingItems.isEmpty()) playlist()->setPlaying(0); } playlist()->updateDeletedItem(this); emit playlist()->signalAboutToRemove(this); if(m_watched) Pointer::clear(this); } void PlaylistItem::setFile(const FileHandle &file) { m_collectionItem->updateCollectionDict(d->fileHandle.absFilePath(), file.absFilePath()); d->fileHandle = file; refresh(); } void PlaylistItem::setFile(const QString &file) { QString oldPath = d->fileHandle.absFilePath(); d->fileHandle.setFile(file); m_collectionItem->updateCollectionDict(oldPath, d->fileHandle.absFilePath()); refresh(); } FileHandle PlaylistItem::file() const { return d->fileHandle; } Q_GLOBAL_STATIC_WITH_ARGS(QPixmap, globalGenericImage, (SmallIcon("image-x-generic"))) Q_GLOBAL_STATIC_WITH_ARGS(QPixmap, globalPlayingImage, (UserIcon("playing"))) const QPixmap *PlaylistItem::pixmap(int column) const { int offset = playlist()->columnOffset(); // Don't use hasCover here because that may dig into the track itself. // Besides, we really just want to know if the cover manager has a cover // for the track. if((column - offset) == CoverColumn && d->fileHandle.coverInfo()->coverId() != CoverManager::NoMatch) { return globalGenericImage; } if(column == playlist()->leftColumn() && m_playingItems.contains(const_cast(this))) { return globalPlayingImage; } //return QTreeWidgetItem::pixmap(column); return nullptr; } QString PlaylistItem::text(int column) const { if(!d->fileHandle.tag()) return QString(); int offset = playlist()->columnOffset(); switch(column - offset) { case TrackColumn: return d->fileHandle.tag()->title(); case ArtistColumn: return d->fileHandle.tag()->artist(); case AlbumColumn: return d->fileHandle.tag()->album(); case CoverColumn: return QString(); case TrackNumberColumn: return d->fileHandle.tag()->track() > 0 ? QString::number(d->fileHandle.tag()->track()) : QString(); case GenreColumn: return d->fileHandle.tag()->genre(); case YearColumn: return d->fileHandle.tag()->year() > 0 ? QString::number(d->fileHandle.tag()->year()) : QString(); case LengthColumn: return d->fileHandle.tag()->lengthString(); case BitrateColumn: return QString::number(d->fileHandle.tag()->bitrate()); case CommentColumn: return d->fileHandle.tag()->comment(); case FileNameColumn: return d->fileHandle.fileInfo().fileName(); case FullPathColumn: return d->fileHandle.fileInfo().absoluteFilePath(); default: return QTreeWidgetItem::text(column); } } void PlaylistItem::setText(int column, const QString &text) { QTreeWidgetItem::setText(column, text); playlist()->slotWeightDirty(column); } void PlaylistItem::setPlaying(bool playing, bool master) { m_playingItems.removeAll(this); if(playing) { if(master) m_playingItems.prepend(this); else m_playingItems.append(this); } else { // This is a tricky little recursion, but it // in fact does clear the list. if(!m_playingItems.isEmpty()) m_playingItems.front()->setPlaying(false); } treeWidget()->viewport()->update(); } -void PlaylistItem::setSelected(bool selected) -{ - playlist()->markItemSelected(this, selected); - QTreeWidgetItem::setSelected(selected); -} - void PlaylistItem::guessTagInfo(TagGuesser::Type type) { switch(type) { case TagGuesser::FileName: { TagGuesser guesser(d->fileHandle.absFilePath()); Tag *tag = TagTransactionManager::duplicateTag(d->fileHandle.tag()); if(!guesser.title().isNull()) tag->setTitle(guesser.title()); if(!guesser.artist().isNull()) tag->setArtist(guesser.artist()); if(!guesser.album().isNull()) tag->setAlbum(guesser.album()); if(!guesser.track().isNull()) tag->setTrack(guesser.track().toInt()); if(!guesser.comment().isNull()) tag->setComment(guesser.comment()); TagTransactionManager::instance()->changeTagOnItem(this, tag); break; } case TagGuesser::MusicBrainz: startMusicBrainzQuery(d->fileHandle); break; } } Playlist *PlaylistItem::playlist() const { return static_cast(treeWidget()); } QVector PlaylistItem::cachedWidths() const { return d->cachedWidths; } void PlaylistItem::refresh() { m_collectionItem->refresh(); } void PlaylistItem::refreshFromDisk() { d->fileHandle.refresh(); refresh(); } void PlaylistItem::clear() { playlist()->clearItem(this); } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem protected methods //////////////////////////////////////////////////////////////////////////////// PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent) : QTreeWidgetItem(parent), d(0), m_watched(0) { setup(item); } PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent, QTreeWidgetItem *after) : QTreeWidgetItem(parent, after), d(0), m_watched(0) { setup(item); } // This constructor should only be used by the CollectionList subclass. PlaylistItem::PlaylistItem(CollectionList *parent) : QTreeWidgetItem(parent), m_watched(0) { d = new Data; m_collectionItem = static_cast(this); setFlags(flags() | Qt::ItemIsDragEnabled); } // FIXME paintCell /*void PlaylistItem::paintCell(QPainter *p, const QColorGroup &cg, int column, int width, int align) { if(!m_playingItems.contains(this)) return QTreeWidgetItem::paintCell(p, cg, column, width, align); QPalette colorGroup = cg; QColor base = colorGroup.color( QPalette::Base ); QColor selection = colorGroup.color( QPalette::Highlight ); int r = (base.red() + selection.red()) / 2; int b = (base.blue() + selection.blue()) / 2; int g = (base.green() + selection.green()) / 2; QColor c(r, g, b); colorGroup.setColor(QPalette::Base, c); QTreeWidgetItem::paintCell(p, colorGroup, column, width, align); }*/ int PlaylistItem::compare(QTreeWidgetItem *item, int column, bool ascending) const { // reimplemented from QListViewItem int offset = playlist()->columnOffset(); if(!item) return 0; PlaylistItem *playlistItem = static_cast(item); // The following statments first check to see if you can sort based on the // specified column. If the values for the two PlaylistItems are the same // in that column it then tries to sort based on columns 1, 2, 3 and 0, // (artist, album, track number, track name) in that order. int c = compare(this, playlistItem, column, ascending); if(c != 0) return c; else { // Loop through the columns doing comparisons until something is differnt. // If all else is the same, compare the track name. int last = !playlist()->isColumnHidden(AlbumColumn + offset) ? TrackNumberColumn : ArtistColumn; for(int i = ArtistColumn; i <= last; i++) { if(!playlist()->isColumnHidden(i + offset)) { c = compare(this, playlistItem, i, ascending); if(c != 0) return c; } } return compare(this, playlistItem, TrackColumn + offset, ascending); } } int PlaylistItem::compare(const PlaylistItem *firstItem, const PlaylistItem *secondItem, int column, bool) const { int offset = playlist()->columnOffset(); if(column < 0 || column > lastColumn() + offset || !firstItem->d || !secondItem->d) return 0; if(column < offset) { QString first = firstItem->text(column).toLower(); QString second = secondItem->text(column).toLower(); return first.localeAwareCompare(second); } switch(column - offset) { case TrackNumberColumn: if(firstItem->d->fileHandle.tag()->track() > secondItem->d->fileHandle.tag()->track()) return 1; else if(firstItem->d->fileHandle.tag()->track() < secondItem->d->fileHandle.tag()->track()) return -1; else return 0; break; case LengthColumn: if(firstItem->d->fileHandle.tag()->seconds() > secondItem->d->fileHandle.tag()->seconds()) return 1; else if(firstItem->d->fileHandle.tag()->seconds() < secondItem->d->fileHandle.tag()->seconds()) return -1; else return 0; break; case BitrateColumn: if(firstItem->d->fileHandle.tag()->bitrate() > secondItem->d->fileHandle.tag()->bitrate()) return 1; else if(firstItem->d->fileHandle.tag()->bitrate() < secondItem->d->fileHandle.tag()->bitrate()) return -1; else return 0; break; case CoverColumn: if(firstItem->d->fileHandle.coverInfo()->coverId() == secondItem->d->fileHandle.coverInfo()->coverId()) return 0; else if (firstItem->d->fileHandle.coverInfo()->coverId() != CoverManager::NoMatch) return -1; else return 1; break; default: return QString::localeAwareCompare(firstItem->d->metadata[column - offset], secondItem->d->metadata[column - offset]); } } bool PlaylistItem::isValid() const { return bool(d->fileHandle.tag()); } void PlaylistItem::setTrackId(quint32 id) { m_trackId = id; } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem private methods //////////////////////////////////////////////////////////////////////////////// void PlaylistItem::setup(CollectionListItem *item) { m_collectionItem = item; d = item->d; item->addChildItem(this); setFlags(flags() | Qt::ItemIsDragEnabled); int offset = playlist()->columnOffset(); int columns = lastColumn() + offset + 1; for(int i = offset; i < columns; i++) { setText(i, text(i)); } } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem::Pointer implementation //////////////////////////////////////////////////////////////////////////////// QMap > PlaylistItem::Pointer::m_map; // static PlaylistItem::Pointer::Pointer(PlaylistItem *item) : m_item(item) { if(!m_item) return; m_item->m_watched = true; m_map[m_item].append(this); } PlaylistItem::Pointer::Pointer(const Pointer &p) : m_item(p.m_item) { m_map[m_item].append(this); } PlaylistItem::Pointer::~Pointer() { if(!m_item) return; m_map[m_item].removeAll(this); if(m_map[m_item].isEmpty()) { m_map.remove(m_item); m_item->m_watched = false; } } PlaylistItem::Pointer &PlaylistItem::Pointer::operator=(PlaylistItem *item) { if(item == m_item) return *this; if(m_item) { m_map[m_item].removeAll(this); if(m_map[m_item].isEmpty()) { m_map.remove(m_item); m_item->m_watched = false; } } if(item) { m_map[item].append(this); item->m_watched = true; } m_item = item; return *this; } void PlaylistItem::Pointer::clear(PlaylistItem *item) // static { if(!item) return; QList l = m_map[item]; foreach(Pointer *pointer, l) pointer->m_item = 0; m_map.remove(item); item->m_watched = false; } // vim: set et sw=4 tw=0 sta: diff --git a/playlistitem.h b/playlistitem.h index 161ee52b..61548887 100644 --- a/playlistitem.h +++ b/playlistitem.h @@ -1,227 +1,226 @@ /** * 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 . */ #ifndef JUK_PLAYLISTITEM_H #define JUK_PLAYLISTITEM_H #include #include #include #include #include #include "tagguesser.h" #include "filehandle.h" #include "juk_debug.h" class Playlist; class PlaylistItem; class CollectionListItem; class CollectionList; typedef QList PlaylistItemList; /** * Items for the Playlist and the baseclass for CollectionListItem. * The constructors and destructor are protected and new items should be * created via Playlist::createItem(). Items should be removed by * Playlist::clear(), Playlist::deleteFromDisk(), Playlist::clearItem() or * Playlist::clearItem(). */ class PlaylistItem : public QTreeWidgetItem { friend class Playlist; friend class SearchPlaylist; friend class UpcomingPlaylist; friend class CollectionList; friend class CollectionListItem; friend class Pointer; public: enum ColumnType { TrackColumn = 0, ArtistColumn = 1, AlbumColumn = 2, CoverColumn = 3, TrackNumberColumn = 4, GenreColumn = 5, YearColumn = 6, LengthColumn = 7, BitrateColumn = 8, CommentColumn = 9, FileNameColumn = 10, FullPathColumn = 11 }; /** * A helper class to implement guarded pointer semantics. */ class Pointer { public: Pointer() : m_item(0) {} Pointer(PlaylistItem *item); Pointer(const Pointer &p); ~Pointer(); Pointer &operator=(PlaylistItem *item); bool operator==(const Pointer &p) const { return m_item == p.m_item; } bool operator!=(const Pointer &p) const { return m_item != p.m_item; } PlaylistItem *operator->() const { return m_item; } PlaylistItem &operator*() const { return *m_item; } operator PlaylistItem*() const { return m_item; } static void clear(PlaylistItem *item); private: PlaylistItem *m_item; static QMap > m_map; }; friend class Pointer; static int lastColumn() { return FullPathColumn; } void setFile(const FileHandle &file); void setFile(const QString &file); FileHandle file() const; virtual const QPixmap *pixmap(int column) const; virtual QString text(int column) const; virtual void setText(int column, const QString &text); void setPlaying(bool playing = true, bool master = true); - virtual void setSelected(bool selected); void guessTagInfo(TagGuesser::Type type); Playlist *playlist() const; virtual CollectionListItem *collectionItem() { return m_collectionItem; } /** * This is an identifier for the playlist item which will remain unique * throughout the process lifetime. It stays constant once the PlaylistItem * is created. */ quint32 trackId() const { return m_trackId; } /** * The widths of items are cached when they're updated for us in computations * in the "weighted" listview column width mode. */ QVector cachedWidths() const; /** * This just refreshes from the in memory data. This may seem pointless at * first, but this data is shared between all of the list view items that are * based on the same file, so if another one of those items changes its data * it is important to refresh the others. */ virtual void refresh(); /** * This rereads the tag from disk. This affects all PlaylistItems based on * the same file. */ virtual void refreshFromDisk(); /** * Asks the item's playlist to remove the item (which uses deleteLater()). */ virtual void clear(); /** * Returns properly casted item below this one. */ PlaylistItem *itemBelow() { return static_cast(treeWidget()->itemBelow(this)); } /** * Returns properly casted item above this one. */ PlaylistItem *itemAbove() { return static_cast(treeWidget()->itemAbove(this)); } /** * Returns a reference to the list of the currnetly playing items, with the * first being the "master" item (i.e. the item from which the next track is * chosen). */ static const PlaylistItemList &playingItems() { return m_playingItems; } protected: /** * Items should always be created using Playlist::createItem() or through a * subclass or friend class. */ PlaylistItem(CollectionListItem *item, Playlist *parent); PlaylistItem(CollectionListItem *item, Playlist *parent, QTreeWidgetItem *after); /** * This is the constructor that shold be used by subclasses. */ PlaylistItem(CollectionList *parent); /** * See the class documentation for an explanation of construction and deletion * of PlaylistItems. */ virtual ~PlaylistItem(); //virtual void paintCell(QPainter *p, const QColorGroup &cg, int column, int width, int align); //virtual void paintFocus(QPainter *, const QColorGroup &, const QRect &) {} virtual int compare(QTreeWidgetItem *item, int column, bool ascending) const; int compare(const PlaylistItem *firstItem, const PlaylistItem *secondItem, int column, bool ascending) const; bool isValid() const; void setTrackId(quint32 id); /** * Shared data between all PlaylistItems from the same track (incl. the CollectionItem * representing said track. */ struct Data : public QSharedData { Data() {} Data(const QFileInfo &info, const QString &path) : fileHandle(info, path) {} Data(const QString &path) : fileHandle(path) {} FileHandle fileHandle; QVector metadata; ///< Artist, album, or genre tags. Other columns unfilled QVector cachedWidths; }; using DataPtr = QExplicitlySharedDataPointer; DataPtr sharedData() const { return d; } private: DataPtr d; void setup(CollectionListItem *item); CollectionListItem *m_collectionItem; quint32 m_trackId; bool m_watched; static PlaylistItemList m_playingItems; }; inline QDebug operator<<(QDebug s, const PlaylistItem &item) { s << item.text(PlaylistItem::TrackColumn); return s; } #endif // vim: set et sw=4 tw=0 sta: diff --git a/playlistsplitter.cpp b/playlistsplitter.cpp index 31f6bea0..4f90ac27 100644 --- a/playlistsplitter.cpp +++ b/playlistsplitter.cpp @@ -1,339 +1,338 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2009 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 "playlistsplitter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "searchwidget.h" #include "playlistsearch.h" #include "actioncollection.h" #include "tageditor.h" #include "collectionlist.h" #include "playermanager.h" #include "nowplaying.h" #include "playlistbox.h" #include "lyricswidget.h" #include "mpris2/mpris2.h" #include "juk_debug.h" //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// PlaylistSplitter::PlaylistSplitter(PlayerManager *player, QWidget *parent) : QSplitter(Qt::Horizontal, parent), m_newVisible(0), m_playlistBox(0), m_searchWidget(0), m_playlistStack(0), m_editor(0), m_nowPlaying(0), m_player(player), m_lyricsWidget(0), m_editorSplitter(0) { setObjectName(QLatin1String("playlistSplitter")); setupActions(); setupLayout(); readConfig(); m_editor->slotUpdateCollection(); m_editor->setupObservers(); } PlaylistSplitter::~PlaylistSplitter() { saveConfig(); // TagEditor needs to write its configuration out while it's still valid, // destroy it now. delete m_editor; delete m_lyricsWidget; // NowPlaying depends on the PlaylistCollection, so kill it now. delete m_nowPlaying; m_nowPlaying = 0; delete m_searchWidget; // Take no chances here either. // Since we want to ensure that the shutdown process for the PlaylistCollection // (a base class for PlaylistBox) has a chance to write the playlists to disk // before they are deleted we're explicitly deleting the PlaylistBox here. delete m_playlistBox; } PlaylistInterface *PlaylistSplitter::playlist() const { return m_playlistBox; } bool PlaylistSplitter::eventFilter(QObject *, QEvent *event) { if(event->type() == FocusUpEvent::id) { m_searchWidget->setFocus(); return true; } return false; } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void PlaylistSplitter::setFocus() { m_searchWidget->setFocus(); } void PlaylistSplitter::slotFocusCurrentPlaylist() { Playlist *playlist = m_playlistBox->visiblePlaylist(); if(playlist) { playlist->setFocus(); playlist->clearSelection(); // Select the top visible (and matching) item. PlaylistItem *item = static_cast(playlist->itemAt(QPoint(0, 0))); if(!item) return; // A little bit of a hack to make QListView repaint things properly. Switch // to single selection mode, set the selection and then switch back. playlist->setSelectionMode(QTreeWidget::SingleSelection); - playlist->markItemSelected(item, true); playlist->setCurrentItem(item); playlist->setSelectionMode(QTreeWidget::ExtendedSelection); } } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// Playlist *PlaylistSplitter::visiblePlaylist() const { return m_newVisible ? m_newVisible : m_playlistBox->visiblePlaylist(); } void PlaylistSplitter::setupActions() { KActionCollection* coll = ActionCollection::actions(); KToggleAction *showSearch = new KToggleAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Show &Search Bar"), this); coll->addAction("showSearch", showSearch); QAction *act = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear")), i18n("Edit Track Search"), this); coll->addAction("editTrackSearch", act); coll->setDefaultShortcut(act, Qt::Key_F6); connect(act, SIGNAL(triggered(bool)), SLOT(setFocus())); } void PlaylistSplitter::setupLayout() { setOpaqueResize(false); // Disable the GUI until startup is complete (as indicated by PlaylistBox) setEnabled(false); // Create a splitter to go between the playlists and the editor. m_editorSplitter = new QSplitter(Qt::Vertical, this); m_editorSplitter->setObjectName( QLatin1String("editorSplitter" )); // Make sure none of the optional widgets are collapsible, this causes the // widget to be essentially invisible but logically shown. this->setChildrenCollapsible(false); m_editorSplitter->setChildrenCollapsible(false); // Create the playlist and the editor. QWidget *top = new QWidget(m_editorSplitter); QVBoxLayout *topLayout = new QVBoxLayout(top); topLayout->setMargin(0); topLayout->setSpacing(0); m_playlistStack = new QStackedWidget(top); m_playlistStack->setObjectName( QLatin1String("playlistStack" )); m_playlistStack->installEventFilter(this); m_playlistStack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_playlistStack->hide(); // Will be shown after CollectionList filled. m_editor = new TagEditor(m_editorSplitter); m_editor->setObjectName( QLatin1String("TagEditor" )); // Create the lyrics widget m_lyricsWidget = new LyricsWidget(this); insertWidget(2, m_lyricsWidget); // Create the PlaylistBox m_playlistBox = new PlaylistBox(m_player, this, m_playlistStack); m_playlistBox->setObjectName( QLatin1String( "playlistBox" ) ); connect(m_playlistBox->object(), SIGNAL(signalSelectedItemsChanged()), this, SLOT(slotPlaylistSelectionChanged())); connect(m_playlistBox, SIGNAL(signalPlaylistDestroyed(Playlist*)), m_editor, SLOT(slotPlaylistDestroyed(Playlist*))); connect(m_playlistBox, SIGNAL(startupComplete()), SLOT(slotEnable())); connect(m_playlistBox, SIGNAL(startFilePlayback(FileHandle)), m_player, SLOT(play(FileHandle))); m_player->setPlaylistInterface(m_playlistBox); // Let interested parties know we're ready connect(m_playlistBox, SIGNAL(startupComplete()), SIGNAL(guiReady())); insertWidget(0, m_playlistBox); m_nowPlaying = new NowPlaying(top, m_playlistBox); connect(m_player, SIGNAL(signalItemChanged(FileHandle)), m_nowPlaying, SLOT(slotUpdate(FileHandle))); connect(m_player, SIGNAL(signalItemChanged(FileHandle)), m_lyricsWidget, SLOT(playing(FileHandle))); // Create the search widget -- this must be done after the CollectionList is created. m_searchWidget = new SearchWidget(top); // auto-shortcuts don't seem to work and aren't needed anyway. KAcceleratorManager::setNoAccel(m_searchWidget); connect(m_searchWidget, SIGNAL(signalQueryChanged()), this, SLOT(slotShowSearchResults())); connect(m_searchWidget, SIGNAL(signalDownPressed()), this, SLOT(slotFocusCurrentPlaylist())); connect(m_searchWidget, SIGNAL(signalShown(bool)), m_playlistBox->object(), SLOT(slotSetSearchEnabled(bool))); connect(m_searchWidget, SIGNAL(returnPressed()), m_playlistBox->object(), SLOT(slotPlayFirst())); connect(ActionCollection::action("showSearch"), SIGNAL(toggled(bool)), m_searchWidget, SLOT(setEnabled(bool))); topLayout->addWidget(m_nowPlaying); topLayout->addWidget(m_searchWidget); topLayout->insertStretch(-1); // Force search bar to top while playlistStack hides topLayout->addWidget(m_playlistStack, 1); // Now that GUI setup is complete, add some auto-update signals. connect(CollectionList::instance(), SIGNAL(signalCollectionChanged()), m_editor, SLOT(slotUpdateCollection())); connect(m_playlistStack, SIGNAL(currentChanged(int)), this, SLOT(slotPlaylistChanged(int))); // Show the collection on startup. m_playlistBox->setCurrentItem(m_playlistBox->topLevelItem(0)); } void PlaylistSplitter::readConfig() { KConfigGroup config(KSharedConfig::openConfig(), "Splitter"); QList splitterSizes = config.readEntry("PlaylistSplitterSizes",QList()); if(splitterSizes.isEmpty()) { splitterSizes.append(100); splitterSizes.append(640); } setSizes(splitterSizes); bool showSearch = config.readEntry("ShowSearch", true); ActionCollection::action("showSearch")->setChecked(showSearch); m_searchWidget->setHidden(!showSearch); splitterSizes = config.readEntry("EditorSplitterSizes",QList()); if(splitterSizes.isEmpty()) { // If no sizes were saved, use default sizes for the playlist and the // editor, respectively. The values are just hints for the actual size, // m_editorSplitter will distribute the space according to their // relative weight. splitterSizes.append(300); splitterSizes.append(200); } m_editorSplitter->setSizes(splitterSizes); } void PlaylistSplitter::saveConfig() { KConfigGroup config(KSharedConfig::openConfig(), "Splitter"); config.writeEntry("PlaylistSplitterSizes", sizes()); config.writeEntry("ShowSearch", ActionCollection::action("showSearch")->isChecked()); config.writeEntry("EditorSplitterSizes", m_editorSplitter->sizes()); } void PlaylistSplitter::slotShowSearchResults() { PlaylistList playlists; playlists.append(visiblePlaylist()); PlaylistSearch search = m_searchWidget->search(playlists); visiblePlaylist()->setSearch(search); } void PlaylistSplitter::slotPlaylistSelectionChanged() { m_editor->slotSetItems(visiblePlaylist()->selectedItems()); } void PlaylistSplitter::slotPlaylistChanged(int i) { Playlist *p = qobject_cast(m_playlistStack->widget(i)); if(!p) return; m_newVisible = p; m_searchWidget->setSearch(p->search()); m_newVisible = 0; } void PlaylistSplitter::slotEnable() { qCDebug(JUK_LOG) << "Enabling GUI"; QTime stopwatch; stopwatch.start(); setEnabled(true); // Ready to go. m_playlistStack->show(); qCDebug(JUK_LOG) << "Finished enabling GUI, took" << stopwatch.elapsed() << "ms"; (void) new Mpris2(this); } // vim: set et sw=4 tw=0 sta: