/*************************************************************************** begin : Sat Feb 16 2002 copyright : (C) 2002 - 2004 by Scott Wheeler email : wheeler@kde.org ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include #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 "playlist.h" #include "playlistitem.h" #include "playlistcollection.h" #include "playlistsearch.h" #include "mediafiles.h" #include "collectionlist.h" #include "filerenamer.h" #include "actioncollection.h" #include "tracksequencemanager.h" #include "juk.h" #include "tag.h" #include "k3bexporter.h" #include "upcomingplaylist.h" #include "deletedialog.h" #include "webimagefetcher.h" #include "coverinfo.h" #include "coverdialog.h" #include "tagtransactionmanager.h" #include "cache.h" using namespace ActionCollection; /** * Just a shortcut of sorts. */ static bool manualResize() { return action("resizeColumnsManually")->isChecked(); } /** * A tooltip specialized to show full filenames over the file name column. */ class PlaylistToolTip : public TQToolTip { public: PlaylistToolTip(TQWidget *parent, Playlist *playlist) : TQToolTip(parent), m_playlist(playlist) {} virtual void maybeTip(const TQPoint &p) { PlaylistItem *item = static_cast(m_playlist->itemAt(p)); if(!item) return; TQPoint 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())) { TQRect 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) { TQMimeSourceFactory *f = TQMimeSourceFactory::defaultFactory(); f->setImage("coverThumb", TQImage(item->file().coverInfo()->pixmap(CoverInfo::Thumbnail).convertToImage())); tip(r, "
"); } else tip(r, item->text(column)); } } private: Playlist *m_playlist; }; //////////////////////////////////////////////////////////////////////////////// // Playlist::SharedSettings definition //////////////////////////////////////////////////////////////////////////////// bool Playlist::m_visibleChanged = false; bool Playlist::m_shuttingDown = false; /** * Shared settings between the playlists. */ class Playlist::SharedSettings { public: static SharedSettings *instance(); /** * Sets the default column order to that of Playlist @param p. */ void setColumnOrder(const Playlist *l); void toggleColumnVisible(int column); void setInlineCompletionMode(TDEGlobalSettings::Completion mode); /** * Apply the settings. */ void apply(Playlist *l) const; void sync() { writeConfig(); } protected: SharedSettings(); ~SharedSettings() {} private: void writeConfig(); static SharedSettings *m_instance; TQValueList m_columnOrder; TQValueVector m_columnsVisible; TDEGlobalSettings::Completion m_inlineCompletion; }; Playlist::SharedSettings *Playlist::SharedSettings::m_instance = 0; //////////////////////////////////////////////////////////////////////////////// // Playlist::SharedSettings public members //////////////////////////////////////////////////////////////////////////////// Playlist::SharedSettings *Playlist::SharedSettings::instance() { static SharedSettings settings; return &settings; } void Playlist::SharedSettings::setColumnOrder(const Playlist *l) { if(!l) return; m_columnOrder.clear(); for(int i = l->columnOffset(); i < l->columns(); ++i) m_columnOrder.append(l->header()->mapToIndex(i)); writeConfig(); } void Playlist::SharedSettings::toggleColumnVisible(int column) { if(column >= int(m_columnsVisible.size())) m_columnsVisible.resize(column + 1, true); m_columnsVisible[column] = !m_columnsVisible[column]; writeConfig(); } void Playlist::SharedSettings::setInlineCompletionMode(TDEGlobalSettings::Completion mode) { m_inlineCompletion = mode; writeConfig(); } void Playlist::SharedSettings::apply(Playlist *l) const { if(!l) return; int offset = l->columnOffset(); int i = 0; for(TQValueListConstIterator it = m_columnOrder.begin(); it != m_columnOrder.end(); ++it) l->header()->moveSection(i++ + offset, *it + offset); for(uint i = 0; i < m_columnsVisible.size(); i++) { if(m_columnsVisible[i] && !l->isColumnVisible(i + offset)) l->showColumn(i + offset, false); else if(!m_columnsVisible[i] && l->isColumnVisible(i + offset)) l->hideColumn(i + offset, false); } l->updateLeftColumn(); l->renameLineEdit()->setCompletionMode(m_inlineCompletion); l->slotColumnResizeModeChanged(); } //////////////////////////////////////////////////////////////////////////////// // Playlist::ShareSettings protected members //////////////////////////////////////////////////////////////////////////////// Playlist::SharedSettings::SharedSettings() { TDEConfigGroup config(TDEGlobal::config(), "PlaylistShared"); bool resizeColumnsManually = config.readBoolEntry("ResizeColumnsManually", false); action("resizeColumnsManually")->setChecked(resizeColumnsManually); // save column order m_columnOrder = config.readIntListEntry("ColumnOrder"); TQValueList l = config.readIntListEntry("VisibleColumns"); if(l.isEmpty()) { // Provide some default values for column visibility if none were // read from the configuration file. for(int i = 0; i <= PlaylistItem::lastColumn(); i++) { switch(i) { case PlaylistItem::BitrateColumn: case PlaylistItem::CommentColumn: case PlaylistItem::FileNameColumn: case PlaylistItem::FullPathColumn: m_columnsVisible.append(false); break; default: m_columnsVisible.append(true); } } } else { // Convert the int list into a bool list. m_columnsVisible.resize(l.size(), true); uint i = 0; for(TQValueList::Iterator it = l.begin(); it != l.end(); ++it) { if(! bool(*it)) m_columnsVisible[i] = bool(*it); i++; } } m_inlineCompletion = TDEGlobalSettings::Completion( config.readNumEntry("InlineCompletionMode", TDEGlobalSettings::CompletionAuto)); } //////////////////////////////////////////////////////////////////////////////// // Playlist::SharedSettings private members //////////////////////////////////////////////////////////////////////////////// void Playlist::SharedSettings::writeConfig() { TDEConfigGroup config(TDEGlobal::config(), "PlaylistShared"); config.writeEntry("ColumnOrder", m_columnOrder); TQValueList l; for(uint i = 0; i < m_columnsVisible.size(); i++) l.append(int(m_columnsVisible[i])); config.writeEntry("VisibleColumns", l); config.writeEntry("InlineCompletionMode", m_inlineCompletion); config.writeEntry("ResizeColumnsManually", manualResize()); TDEGlobal::config()->sync(); } //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// PlaylistItemList Playlist::m_history; TQMap Playlist::m_backMenuItems; int Playlist::m_leftColumn = 0; Playlist::Playlist(PlaylistCollection *collection, const TQString &name, const TQString &iconName) : TDEListView(collection->playlistStack(), name.latin1()), m_collection(collection), m_fetcher(new WebImageFetcher(TQT_TQOBJECT(this))), m_selectedCount(0), m_allowDuplicates(false), m_polished(false), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_lastSelected(0), m_playlistName(name), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const TQString &name, const TQString &iconName) : TDEListView(collection->playlistStack(), name.latin1()), m_collection(collection), m_fetcher(new WebImageFetcher(TQT_TQOBJECT(this))), m_selectedCount(0), m_allowDuplicates(false), m_polished(false), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_lastSelected(0), m_playlistName(name), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); collection->setupPlaylist(this, iconName); createItems(items); } Playlist::Playlist(PlaylistCollection *collection, const TQFileInfo &playlistFile, const TQString &iconName) : TDEListView(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(TQT_TQOBJECT(this))), m_selectedCount(0), m_allowDuplicates(false), m_polished(false), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_lastSelected(0), m_fileName(playlistFile.absFilePath()), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); loadFile(m_fileName, playlistFile); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, bool delaySetup) : TDEListView(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(TQT_TQOBJECT(this))), m_selectedCount(0), m_allowDuplicates(false), m_polished(false), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_lastSelected(0), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); if(!delaySetup) collection->setupPlaylist(this, "audio-midi"); } Playlist::~Playlist() { // clearItem() will take care of removing the items from the history, // so call clearItems() to make sure it happens. clearItems(items()); delete m_toolTip; // Select a different playlist if we're the selected one if(isVisible() && this != CollectionList::instance()) m_collection->raise(CollectionList::instance()); if(!m_shuttingDown) m_collection->removePlaylist(this); } TQString Playlist::name() const { if(m_playlistName.isNull()) return m_fileName.section(TQDir::separator(), -1).section('.', 0, -2); else return m_playlistName; } FileHandle Playlist::currentFile() const { return playingItem() ? playingItem()->file() : FileHandle::null(); } int Playlist::time() const { // Since this method gets a lot of traffic, let's optimize for such. if(!m_addTime.isEmpty()) { for(PlaylistItemList::ConstIterator it = m_addTime.begin(); it != m_addTime.end(); ++it) { if(*it) m_time += (*it)->file().tag()->seconds(); } m_addTime.clear(); } if(!m_subtractTime.isEmpty()) { for(PlaylistItemList::ConstIterator it = m_subtractTime.begin(); it != m_subtractTime.end(); ++it) { if(*it) m_time -= (*it)->file().tag()->seconds(); } m_subtractTime.clear(); } return m_time; } void Playlist::playFirst() { TrackSequenceManager::instance()->setNextItem(static_cast( TQListViewItemIterator(const_cast(this), TQListViewItemIterator::Visible).current())); action("forward")->activate(); } void Playlist::playNextAlbum() { PlaylistItem *current = TrackSequenceManager::instance()->currentItem(); if(!current) return; // No next album if we're not already playing. TQString currentAlbum = current->file().tag()->album(); current = TrackSequenceManager::instance()->nextItem(); while(current && current->file().tag()->album() == currentAlbum) current = TrackSequenceManager::instance()->nextItem(); TrackSequenceManager::instance()->setNextItem(current); action("forward")->activate(); } void Playlist::playNext() { TrackSequenceManager::instance()->setCurrentPlaylist(this); setPlaying(TrackSequenceManager::instance()->nextItem()); } void Playlist::stop() { m_history.clear(); setPlaying(0); } void Playlist::playPrevious() { if(!playingItem()) return; bool random = action("randomPlay") && action("randomPlay")->isChecked(); PlaylistItem *previous = 0; if(random && !m_history.isEmpty()) { PlaylistItemList::Iterator last = m_history.fromLast(); previous = *last; m_history.remove(last); } else { m_history.clear(); previous = TrackSequenceManager::instance()->previousItem(); } if(!previous) previous = static_cast(playingItem()->itemAbove()); setPlaying(previous, false); } void Playlist::setName(const TQString &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(); TQFile file(m_fileName); if(!file.open(IO_WriteOnly)) return KMessageBox::error(this, i18n("Could not save to file %1.").arg(m_fileName)); TQTextStream stream(&file); TQStringList fileList = files(); for(TQStringList::Iterator it = fileList.begin(); it != fileList.end(); ++it) stream << *it << endl; file.close(); } void Playlist::saveAs() { m_collection->removeFileFromDict(m_fileName); m_fileName = MediaFiles::savePlaylistDialog(name(), this); if(!m_fileName.isEmpty()) { m_collection->addFileToDict(m_fileName); // If there's no playlist name set, use the file name. if(m_playlistName.isEmpty()) emit signalNameChanged(name()); save(); } } void Playlist::clearItem(PlaylistItem *item, bool emitChanged) { emit signalAboutToRemove(item); m_members.remove(item->file().absFilePath()); m_search.clearItem(item); m_history.remove(item); m_addTime.remove(item); m_subtractTime.remove(item); delete item; if(emitChanged) dataChanged(); } void Playlist::clearItems(const PlaylistItemList &items) { m_blockDataChanged = true; for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) clearItem(*it, false); m_blockDataChanged = false; dataChanged(); } PlaylistItem *Playlist::playingItem() // static { return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front(); } TQStringList Playlist::files() const { TQStringList list; for(TQListViewItemIterator it(const_cast(this)); it.current(); ++it) list.append(static_cast(*it)->file().absFilePath()); return list; } PlaylistItemList Playlist::items() { return items(TQListViewItemIterator::IteratorFlag(0)); } PlaylistItemList Playlist::visibleItems() { return items(TQListViewItemIterator::Visible); } PlaylistItemList Playlist::selectedItems() { PlaylistItemList list; switch(m_selectedCount) { case 0: break; // case 1: // list.append(m_lastSelected); // break; default: list = items(TQListViewItemIterator::IteratorFlag(TQListViewItemIterator::Selected | TQListViewItemIterator::Visible)); break; } return list; } PlaylistItem *Playlist::firstChild() const { return static_cast(TDEListView::firstChild()); } void Playlist::updateLeftColumn() { int newLeftColumn = leftMostVisibleColumn(); if(m_leftColumn != newLeftColumn) { updatePlaying(); m_leftColumn = newLeftColumn; } } void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static { m_visibleChanged = true; for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) (*it)->setVisible(visible); } void Playlist::setSearch(const PlaylistSearch &s) { m_search = s; if(!m_searchEnabled) return; setItemsVisible(s.matchedItems(), true); setItemsVisible(s.unmatchedItems(), false); TrackSequenceManager::instance()->iterator()->playlistChanged(); } void Playlist::setSearchEnabled(bool enabled) { if(m_searchEnabled == enabled) return; m_searchEnabled = enabled; if(enabled) { setItemsVisible(m_search.matchedItems(), true); setItemsVisible(m_search.unmatchedItems(), false); } else setItemsVisible(items(), true); } void Playlist::markItemSelected(PlaylistItem *item, bool selected) { if(selected && !item->isSelected()) { m_selectedCount++; m_lastSelected = item; } else if(!selected && item->isSelected()) m_selectedCount--; } void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster) { for(PlaylistList::ConstIterator it = sources.begin(); it != sources.end(); ++it) { if((*it)->playing()) { CollectionListItem *base = playingItem()->collectionItem(); for(TQListViewItemIterator itemIt(this); itemIt.current(); ++itemIt) { PlaylistItem *item = static_cast(itemIt.current()); if(base == item->collectionItem()) { item->setPlaying(true, setMaster); PlaylistItemList playing = PlaylistItem::playingItems(); TrackSequenceManager::instance()->setCurrent(item); return; } } return; } } } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void Playlist::copy() { kapp->clipboard()->setData(dragObject(0), TQClipboard::Clipboard); } void Playlist::paste() { decode(kapp->clipboard()->data(), 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(); TDEApplication::setOverrideCursor(TQt::waitCursor); for(PlaylistItemList::Iterator it = l.begin(); it != l.end(); ++it) { (*it)->refreshFromDisk(); if(!(*it)->file().tag() || !(*it)->file().fileInfo().exists()) { kdDebug(65432) << "Error while trying to refresh the tag. " << "This file has probably been removed." << endl; clearItem((*it)->collectionItem()); } processEvents(); } TDEApplication::restoreOverrideCursor(); } void Playlist::slotRenameFile() { FileRenamer renamer; PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; emit signalEnableDirWatch(false); m_blockDataChanged = true; renamer.rename(items); m_blockDataChanged = false; dataChanged(); emit signalEnableDirWatch(true); } void Playlist::slotViewCover() { PlaylistItemList items = selectedItems(); if (items.isEmpty()) return; for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) (*it)->file().coverInfo()->popup(); } void Playlist::slotRemoveCover() { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; int button = KMessageBox::warningContinueCancel(this, i18n("Are you sure you want to delete these covers?"), TQString(), i18n("&Delete Covers")); if(button == KMessageBox::Continue) refreshAlbums(items); } void Playlist::slotShowCoverManager() { static CoverDialog *managerDialog = 0; if(!managerDialog) managerDialog = new CoverDialog(this); managerDialog->show(); } unsigned int Playlist::eligibleCoverItems(const PlaylistItemList &items) { // This used to count the number of tracks with an artist and album, that // is not strictly required anymore. This may prove useful in the future // so I'm leaving it in for now, right now we just mark every item as // eligible. return items.count(); } void Playlist::slotAddCover(bool retrieveLocal) { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; if(eligibleCoverItems(items) == 0) { // No items in the list can be assigned a cover, inform the user and // bail. // KDE 4.0 Fix this string. KMessageBox::sorry(this, i18n("None of the items you have selected can " "be assigned a cover. A track must have both the Artist " "and Album tags set to be assigned a cover.")); return; } TQPixmap newCover; if(retrieveLocal) { KURL file = KFileDialog::getImageOpenURL( ":homedir", this, i18n("Select Cover Image File")); newCover = TQPixmap(file.directory() + "/" + file.fileName()); } else { m_fetcher->setFile((*items.begin())->file()); m_fetcher->chooseCover(); return; } if(newCover.isNull()) return; TQString artist = items.front()->file().tag()->artist(); TQString album = items.front()->file().tag()->album(); coverKey newId = CoverManager::addCover(newCover, artist, album); refreshAlbums(items, newId); } // Called when image fetcher has added a new cover. void Playlist::slotCoverChanged(int coverId) { kdDebug(65432) << "Refreshing information for newly changed covers.\n"; refreshAlbums(selectedItems(), coverId); } void Playlist::slotGuessTagInfo(TagGuesser::Type type) { TDEApplication::setOverrideCursor(TQt::waitCursor); PlaylistItemList items = selectedItems(); setDynamicListsFrozen(true); m_blockDataChanged = true; for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) { (*it)->guessTagInfo(type); processEvents(); } // MusicBrainz queries automatically commit at this point. What would // be nice is having a signal emitted when the last query is completed. if(type == TagGuesser::FileName) TagTransactionManager::instance()->commit(); m_blockDataChanged = false; dataChanged(); setDynamicListsFrozen(false); TDEApplication::restoreOverrideCursor(); } void Playlist::slotReload() { TQFileInfo fileInfo(m_fileName); if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable()) return; clearItems(items()); loadFile(m_fileName, fileInfo); } void Playlist::slotWeightDirty(int column) { if(column < 0) { m_weightDirty.clear(); for(int i = 0; i < columns(); i++) { if(isColumnVisible(i)) m_weightDirty.append(i); } return; } if(m_weightDirty.find(column) == m_weightDirty.end()) m_weightDirty.append(column); } void Playlist::slotShowPlaying() { if(!playingItem()) return; Playlist *l = playingItem()->playlist(); l->clearSelection(); // Raise the playlist before selecting the items otherwise the tag editor // will not update when it gets the selectionChanged() notification // because it will think the user is choosing a different playlist but not // selecting a different item. m_collection->raise(l); l->setSelected(playingItem(), true); l->setCurrentItem(playingItem()); l->ensureItemVisible(playingItem()); } void Playlist::slotColumnResizeModeChanged() { if(manualResize()) setHScrollBarMode(Auto); else setHScrollBarMode(AlwaysOff); if(!manualResize()) slotUpdateColumnWidths(); SharedSettings::instance()->sync(); } void Playlist::dataChanged() { if(m_blockDataChanged) return; PlaylistInterface::dataChanged(); } //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// void Playlist::removeFromDisk(const PlaylistItemList &items) { if(isVisible() && !items.isEmpty()) { TQStringList files; for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) files.append((*it)->file().absFilePath()); DeleteDialog dialog(this); m_blockDataChanged = true; if(dialog.confirmDeleteList(files)) { bool shouldDelete = dialog.shouldDelete(); TQStringList errorFiles; for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { if(playingItem() == *it) action("forward")->activate(); TQString removePath = (*it)->file().absFilePath(); if((!shouldDelete && TDEIO::NetAccess::synchronousRun(TDEIO::trash(removePath), this)) || (shouldDelete && TQFile::remove(removePath))) { CollectionList::instance()->clearItem((*it)->collectionItem()); } else errorFiles.append((*it)->file().absFilePath()); } if(!errorFiles.isEmpty()) { TQString 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(); } } TQDragObject *Playlist::dragObject(TQWidget *parent) { PlaylistItemList items = selectedItems(); KURL::List urls; for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) { KURL url; url.setPath((*it)->file().absFilePath()); urls.append(url); } KURLDrag *drag = new KURLDrag(urls, parent, "Playlist Items"); drag->setPixmap(BarIcon("audio-x-generic")); return drag; } void Playlist::contentsDragEnterEvent(TQDragEnterEvent *e) { TDEListView::contentsDragEnterEvent(e); if(CoverDrag::canDecode(e)) { setDropHighlighter(true); setDropVisualizer(false); e->accept(); return; } setDropHighlighter(false); setDropVisualizer(true); KURL::List urls; if(!KURLDrag::decode(e, urls) || urls.isEmpty()) { e->ignore(); return; } e->accept(); return; } bool Playlist::acceptDrag(TQDropEvent *e) const { return CoverDrag::canDecode(e) || KURLDrag::canDecode(e); } bool Playlist::canDecode(TQMimeSource *s) { KURL::List urls; if(CoverDrag::canDecode(s)) return true; return KURLDrag::decode(s, urls) && !urls.isEmpty(); } void Playlist::decode(TQMimeSource *s, PlaylistItem *item) { KURL::List urls; if(!KURLDrag::decode(s, urls) || urls.isEmpty()) return; // handle dropped images if(!MediaFiles::isMediaFile(urls.front().path())) { TQString file; if(urls.front().isLocalFile()) file = urls.front().path(); else TDEIO::NetAccess::download(urls.front(), file, 0); KMimeType::Ptr mimeType = KMimeType::findByPath(file); if(item && mimeType->name().startsWith("image/")) { item->file().coverInfo()->setCover(TQImage(file)); refreshAlbum(item->file().tag()->artist(), item->file().tag()->album()); } TDEIO::NetAccess::removeTempFile(file); } TQStringList fileList; for(KURL::List::Iterator it = urls.begin(); it != urls.end(); ++it) fileList += MediaFiles::convertURLsToLocal((*it).path(), this); addFiles(fileList, item); } bool Playlist::eventFilter(TQObject *watched, TQEvent *e) { if(TQT_BASE_OBJECT(watched) == TQT_BASE_OBJECT(header())) { switch(e->type()) { case TQEvent::MouseMove: { if((TQT_TQMOUSEEVENT(e)->state() & Qt::LeftButton) == Qt::LeftButton && !action("resizeColumnsManually")->isChecked()) { m_columnWidthModeChanged = true; action("resizeColumnsManually")->setChecked(true); slotColumnResizeModeChanged(); } break; } case TQEvent::MouseButtonPress: { if(TQT_TQMOUSEEVENT(e)->button() == Qt::RightButton) m_headerMenu->popup(TQCursor::pos()); break; } case TQEvent::MouseButtonRelease: { if(m_columnWidthModeChanged) { m_columnWidthModeChanged = false; notifyUserColumnWidthModeChanged(); } if(!manualResize() && m_widthsDirty) TQTimer::singleShot(0, this, TQT_SLOT(slotUpdateColumnWidths())); break; } default: break; } } return TDEListView::eventFilter(watched, e); } void Playlist::keyPressEvent(TQKeyEvent *event) { if(event->key() == Key_Up) { TQListViewItemIterator selected(this, TQListViewItemIterator::IteratorFlag( TQListViewItemIterator::Selected | TQListViewItemIterator::Visible)); if(selected.current()) { TQListViewItemIterator visible(this, TQListViewItemIterator::IteratorFlag( TQListViewItemIterator::Visible)); if(selected.current() == visible.current()) TDEApplication::postEvent(parent(), new FocusUpEvent); } } TDEListView::keyPressEvent(event); } void Playlist::contentsDropEvent(TQDropEvent *e) { TQPoint vp = contentsToViewport(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::canDecode(e)) { coverKey id; CoverDrag::decode(e, id); // If the item we dropped on is selected, apply cover to all selected // items, otherwise just apply to the dropped item. if(item->isSelected()) { PlaylistItemList selItems = selectedItems(); for(PlaylistItemList::Iterator it = selItems.begin(); it != selItems.end(); ++it) { (*it)->file().coverInfo()->setCoverId(id); (*it)->refresh(); } } else { item->file().coverInfo()->setCoverId(id); item->refresh(); } return; } // When dropping on the upper half of an item, insert before this item. // This is what the user expects, and also allows the insertion at // top of the list if(!item) item = static_cast(lastItem()); else if(vp.y() < item->itemPos() + item->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. setSorting(columns() + 1); TQPtrList items = TDEListView::selectedItems(); for(TQPtrListIterator it(items); it.current(); ++it) { if(!item) { // Insert the item at the top of the list. This is a bit ugly, // but I don't see another way. takeItem(it.current()); insertItem(it.current()); } else it.current()->moveItem(item); item = static_cast(it.current()); } } else decode(e, item); m_blockDataChanged = false; dataChanged(); emit signalPlaylistItemsDropped(this); TDEListView::contentsDropEvent(e); } void Playlist::contentsMouseDoubleClickEvent(TQMouseEvent *e) { // Filter out non left button double clicks, that way users don't have the // weird experience of switching songs from a double right-click. if(e->button() == Qt::LeftButton) TDEListView::contentsMouseDoubleClickEvent(e); } void Playlist::showEvent(TQShowEvent *e) { if(m_applySharedSettings) { SharedSettings::instance()->apply(this); m_applySharedSettings = false; } TDEListView::showEvent(e); } void Playlist::applySharedSettings() { m_applySharedSettings = true; } void Playlist::read(TQDataStream &s) { TQString buffer; s >> m_playlistName >> m_fileName; TQStringList files; s >> files; TQListViewItem *after = 0; m_blockDataChanged = true; for(TQStringList::ConstIterator it = files.begin(); it != files.end(); ++it) after = createItem(FileHandle(*it), after, false); m_blockDataChanged = false; dataChanged(); m_collection->setupPlaylist(this, "audio-midi"); } void Playlist::viewportPaintEvent(TQPaintEvent *pe) { // If there are columns that need to be updated, well, update them. if(!m_weightDirty.isEmpty() && !manualResize()) { calculateColumnWeights(); slotUpdateColumnWidths(); } TDEListView::viewportPaintEvent(pe); } void Playlist::viewportResizeEvent(TQResizeEvent *re) { // If the width of the view has changed, manually update the column // widths. if(re->size().width() != re->oldSize().width() && !manualResize()) slotUpdateColumnWidths(); TDEListView::viewportResizeEvent(re); } void Playlist::insertItem(TQListViewItem *item) { // Because we're called from the PlaylistItem ctor, item may not be a // PlaylistItem yet (it would be TQListViewItem 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)); TDEListView::insertItem(item); } void Playlist::takeItem(TQListViewItem *item) { // See the warning in Playlist::insertItem. m_subtractTime.append(static_cast(item)); TDEListView::takeItem(item); } void Playlist::addColumn(const TQString &label) { slotWeightDirty(columns()); TDEListView::addColumn(label, 30); } PlaylistItem *Playlist::createItem(const FileHandle &file, TQListViewItem *after, bool emitChanged) { return createItem(file, after, emitChanged); } void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after) { createItems(siblings, after); } void Playlist::addFiles(const TQStringList &files, PlaylistItem *after) { if(!after) after = static_cast(lastItem()); TDEApplication::setOverrideCursor(TQt::waitCursor); m_blockDataChanged = true; FileHandleList queue; const TQStringList::ConstIterator filesEnd = files.end(); for(TQStringList::ConstIterator it = files.begin(); it != filesEnd; ++it) addFile(*it, queue, true, &after); addFileHelper(queue, &after, true); m_blockDataChanged = false; slotWeightDirty(); dataChanged(); TDEApplication::restoreOverrideCursor(); } void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id) { TQValueList< TQPair > albums; bool setAlbumCovers = items.count() == 1; for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { TQString artist = (*it)->file().tag()->artist(); TQString album = (*it)->file().tag()->album(); if(albums.find(tqMakePair(artist, album)) == albums.end()) albums.append(tqMakePair(artist, album)); (*it)->file().coverInfo()->setCoverId(id); if(setAlbumCovers) (*it)->file().coverInfo()->applyCoverToWholeAlbum(true); } for(TQValueList< TQPair >::ConstIterator it = albums.begin(); it != albums.end(); ++it) { refreshAlbum((*it).first, (*it).second); } } void Playlist::updatePlaying() const { for(PlaylistItemList::ConstIterator it = PlaylistItem::playingItems().begin(); it != PlaylistItem::playingItems().end(); ++it) { (*it)->listView()->triggerUpdate(); } } void Playlist::refreshAlbum(const TQString &artist, const TQString &album) { ColumnList columns; columns.append(PlaylistItem::ArtistColumn); PlaylistSearch::Component artistComponent(artist, false, columns, PlaylistSearch::Component::Exact); columns.clear(); columns.append(PlaylistItem::AlbumColumn); PlaylistSearch::Component albumComponent(album, false, columns, PlaylistSearch::Component::Exact); PlaylistSearch::ComponentList components; components.append(artist); components.append(album); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components); PlaylistItemList matches = search.matchedItems(); for(PlaylistItemList::Iterator it = matches.begin(); it != matches.end(); ++it) (*it)->refresh(); } void Playlist::hideColumn(int c, bool updateSearch) { m_headerMenu->setItemChecked(c, false); if(!isColumnVisible(c)) return; setColumnWidthMode(c, Manual); setColumnWidth(c, 0); // Moving the column to the end seems to prevent it from randomly // popping up. header()->moveSection(c, header()->count()); header()->setResizeEnabled(false, c); if(c == m_leftColumn) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); triggerUpdate(); } if(this != CollectionList::instance()) CollectionList::instance()->hideColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::showColumn(int c, bool updateSearch) { m_headerMenu->setItemChecked(c, true); if(isColumnVisible(c)) return; // Just set the width to one to mark the column as visible -- we'll update // the real size in the next call. if(manualResize()) setColumnWidth(c, 35); // Make column at least slightly visible. else setColumnWidth(c, 1); header()->setResizeEnabled(true, c); header()->moveSection(c, c); // Approximate old position if(c == leftMostVisibleColumn()) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); triggerUpdate(); } if(this != CollectionList::instance()) CollectionList::instance()->showColumn(c, false); if(updateSearch) redisplaySearch(); } bool Playlist::isColumnVisible(int c) const { return columnWidth(c) != 0; } void Playlist::polish() { TDEListView::polish(); if(m_polished) return; m_polished = true; addColumn(i18n("Track Name")); addColumn(i18n("Artist")); addColumn(i18n("Album")); addColumn(i18n("Cover")); addColumn(i18n("Track")); addColumn(i18n("Genre")); addColumn(i18n("Year")); addColumn(i18n("Length")); addColumn(i18n("Bitrate")); addColumn(i18n("Comment")); addColumn(i18n("File Name")); addColumn(i18n("File Name (full path)")); setRenameable(PlaylistItem::TrackColumn, true); setRenameable(PlaylistItem::ArtistColumn, true); setRenameable(PlaylistItem::AlbumColumn, true); setRenameable(PlaylistItem::TrackNumberColumn, true); setRenameable(PlaylistItem::GenreColumn, true); setRenameable(PlaylistItem::YearColumn, true); setAllColumnsShowFocus(true); setSelectionMode(TQListView::Extended); setShowSortIndicator(true); setDropVisualizer(true); m_columnFixedWidths.resize(columns(), 0); ////////////////////////////////////////////////// // setup header RMB menu ////////////////////////////////////////////////// m_columnVisibleAction = new TDEActionMenu(i18n("&Show Columns"), TQT_TQOBJECT(this), "showColumns"); m_headerMenu = m_columnVisibleAction->popupMenu(); m_headerMenu->insertTitle(i18n("Show")); m_headerMenu->setCheckable(true); for(int i = 0; i < header()->count(); ++i) { if(i == PlaylistItem::FileNameColumn) m_headerMenu->insertSeparator(); m_headerMenu->insertItem(header()->label(i), i); m_headerMenu->setItemChecked(i, true); adjustColumn(i); } connect(m_headerMenu, TQT_SIGNAL(activated(int)), this, TQT_SLOT(slotToggleColumnVisible(int))); connect(this, TQT_SIGNAL(contextMenuRequested(TQListViewItem *, const TQPoint &, int)), this, TQT_SLOT(slotShowRMBMenu(TQListViewItem *, const TQPoint &, int))); connect(this, TQT_SIGNAL(itemRenamed(TQListViewItem *, const TQString &, int)), this, TQT_SLOT(slotInlineEditDone(TQListViewItem *, const TQString &, int))); connect(this, TQT_SIGNAL(doubleClicked(TQListViewItem *)), this, TQT_SLOT(slotPlayCurrent())); connect(this, TQT_SIGNAL(returnPressed(TQListViewItem *)), this, TQT_SLOT(slotPlayCurrent())); connect(header(), TQT_SIGNAL(sizeChange(int, int, int)), this, TQT_SLOT(slotColumnSizeChanged(int, int, int))); connect(renameLineEdit(), TQT_SIGNAL(completionModeChanged(TDEGlobalSettings::Completion)), this, TQT_SLOT(slotInlineCompletionModeChanged(TDEGlobalSettings::Completion))); connect(action("resizeColumnsManually"), TQT_SIGNAL(activated()), this, TQT_SLOT(slotColumnResizeModeChanged())); if(action("resizeColumnsManually")->isChecked()) setHScrollBarMode(Auto); else setHScrollBarMode(AlwaysOff); setAcceptDrops(true); setDropVisualizer(true); m_disableColumnWidthUpdates = false; setShowToolTips(false); m_toolTip = new PlaylistToolTip(viewport(), this); } void Playlist::setupItem(PlaylistItem *item) { if(!m_search.isEmpty()) item->setVisible(m_search.checkItem(item)); if(childCount() <= 2 && !manualResize()) { slotWeightDirty(); slotUpdateColumnWidths(); triggerUpdate(); } } void Playlist::setDynamicListsFrozen(bool frozen) { m_collection->setDynamicListsFrozen(frozen); } //////////////////////////////////////////////////////////////////////////////// // protected slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotPopulateBackMenu() const { if(!playingItem()) return; TDEPopupMenu *menu = action("back")->popupMenu(); menu->clear(); m_backMenuItems.clear(); int count = 0; PlaylistItemList::ConstIterator it = m_history.end(); while(it != m_history.begin() && count < 10) { ++count; --it; int index = menu->insertItem((*it)->file().tag()->title()); m_backMenuItems[index] = *it; } } void Playlist::slotPlayFromBackMenu(int number) const { if(!m_backMenuItems.contains(number)) return; TrackSequenceManager::instance()->setNextItem(m_backMenuItems[number]); action("forward")->activate(); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void Playlist::setup() { setItemMargin(3); connect(header(), TQT_SIGNAL(indexChange(int, int, int)), this, TQT_SLOT(slotColumnOrderChanged(int, int, int))); connect(m_fetcher, TQT_SIGNAL(signalCoverChanged(int)), this, TQT_SLOT(slotCoverChanged(int))); // Prevent list of selected items from changing while internet search is in // progress. connect(this, TQT_SIGNAL(selectionChanged()), m_fetcher, TQT_SLOT(abortSearch())); setSorting(1); } void Playlist::loadFile(const TQString &fileName, const TQFileInfo &fileInfo) { TQFile file(fileName); if(!file.open(IO_ReadOnly)) return; TQTextStream stream(&file); // Turn off non-explicit sorting. setSorting(PlaylistItem::lastColumn() + columnOffset() + 1); PlaylistItem *after = 0; m_disableColumnWidthUpdates = true; m_blockDataChanged = true; while(!stream.atEnd()) { TQString itemName = stream.readLine().stripWhiteSpace(); TQFileInfo item(itemName); if(item.isRelative()) item.setFile(TQDir::cleanDirPath(fileInfo.dirPath(true) + "/" + itemName)); if(item.exists() && item.isFile() && item.isReadable() && MediaFiles::isMediaFile(item.fileName())) { if(after) after = createItem(FileHandle(item, item.absFilePath()), after, false); else after = createItem(FileHandle(item, item.absFilePath()), 0, false); } } m_blockDataChanged = false; file.close(); dataChanged(); m_disableColumnWidthUpdates = false; } void Playlist::setPlaying(PlaylistItem *item, bool addToHistory) { if(playingItem() == item) return; if(playingItem()) { if(addToHistory) { if(playingItem()->playlist() == playingItem()->playlist()->m_collection->upcomingPlaylist()) m_history.append(playingItem()->collectionItem()); else m_history.append(playingItem()); } playingItem()->setPlaying(false); } TrackSequenceManager::instance()->setCurrent(item); TQByteArray data; kapp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data); if(!item) return; item->setPlaying(true); bool enableBack = !m_history.isEmpty(); action("back")->popupMenu()->setEnabled(enableBack); } bool Playlist::playing() const { return playingItem() && this == playingItem()->playlist(); } int Playlist::leftMostVisibleColumn() const { int i = 0; while(!isColumnVisible(header()->mapToSection(i)) && i < PlaylistItem::lastColumn()) i++; return header()->mapToSection(i); } PlaylistItemList Playlist::items(TQListViewItemIterator::IteratorFlag flags) { PlaylistItemList list; for(TQListViewItemIterator it(this, flags); it.current(); ++it) list.append(static_cast(it.current())); return list; } void Playlist::calculateColumnWeights() { if(m_disableColumnWidthUpdates) return; PlaylistItemList l = items(); TQValueListConstIterator columnIt; TQValueVector averageWidth(columns(), 0); double itemCount = l.size(); TQValueVector cachedWidth; // Here we're not using a real average, but averaging the squares of the // column widths and then using the square root of that value. This gives // a nice weighting to the longer columns without doing something arbitrary // like adding a fixed amount of padding. for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) { cachedWidth = (*it)->cachedWidths(); for(columnIt = m_weightDirty.begin(); columnIt != m_weightDirty.end(); ++columnIt) averageWidth[*columnIt] += pow(double(cachedWidth[*columnIt]), 2.0) / itemCount; } m_columnWeights.resize(columns(), -1); for(columnIt = m_weightDirty.begin(); columnIt != m_weightDirty.end(); ++columnIt) { m_columnWeights[*columnIt] = int(sqrt(averageWidth[*columnIt]) + 0.5); // kdDebug(65432) << k_funcinfo << "m_columnWeights[" << *columnIt << "] == " // << m_columnWeights[*columnIt] << endl; } m_weightDirty.clear(); } void Playlist::addFile(const TQString &file, FileHandleList &files, bool importPlaylists, PlaylistItem **after) { if(hasItem(file) && !m_allowDuplicates) return; processEvents(); addFileHelper(files, after); // Our biggest thing that we're fighting during startup is too many stats // of files. Make sure that we don't do one here if it's not needed. FileHandle cached = Cache::instance()->value(file); if(!cached.isNull()) { cached.tag(); files.append(cached); return; } const TQFileInfo fileInfo = TQDir::cleanDirPath(file); if(!fileInfo.exists()) return; if(fileInfo.isFile() && fileInfo.isReadable()) { if(MediaFiles::isMediaFile(file)) { FileHandle f(fileInfo, fileInfo.absFilePath()); f.tag(); files.append(f); } } if(importPlaylists && MediaFiles::isPlaylistFile(file) && !m_collection->containsPlaylistFile(fileInfo.absFilePath())) { new Playlist(m_collection, fileInfo); return; } if(fileInfo.isDir()) { // Resorting to the POSIX API because TQDir::listEntries() stats every // file and blocks while it's doing so. DIR *dir = ::opendir(TQFile::encodeName(fileInfo.filePath())); if(dir) { struct dirent *dirEntry; for(dirEntry = ::readdir(dir); dirEntry; dirEntry = ::readdir(dir)) { if(strcmp(dirEntry->d_name, ".") != 0 && strcmp(dirEntry->d_name, "..") != 0) { // We set importPlaylists to the value from the add directories // dialog as we want to load all of the ones that the user has // explicitly asked for, but not those that we find in lower // directories. addFile(fileInfo.filePath() + TQDir::separator() + TQFile::decodeName(dirEntry->d_name), files, m_collection->importPlaylists(), after); } } ::closedir(dir); } else { kdWarning(65432) << "Unable to open directory " << fileInfo.filePath() << ", make sure it is readable.\n"; } } } void Playlist::addFileHelper(FileHandleList &files, PlaylistItem **after, bool ignoreTimer) { static TQTime time = TQTime::currentTime(); // Process new items every 10 seconds, when we've loaded 1000 items, or when // it's been requested in the API. if(ignoreTimer || time.elapsed() > 10000 || (files.count() >= 1000 && time.elapsed() > 1000)) { time.restart(); const bool focus = hasFocus(); const bool visible = isVisible() && files.count() > 20; if(visible) m_collection->raiseDistraction(); const FileHandleList::ConstIterator filesEnd = files.end(); for(FileHandleList::ConstIterator it = files.begin(); it != filesEnd; ++it) *after = createItem(*it, *after, false); files.clear(); if(visible) m_collection->lowerDistraction(); if(focus) setFocus(); processEvents(); } } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotUpdateColumnWidths() { if(m_disableColumnWidthUpdates || manualResize()) return; // Make sure that the column weights have been initialized before trying to // update the columns. TQValueList visibleColumns; for(int i = 0; i < columns(); i++) { if(isColumnVisible(i)) visibleColumns.append(i); } TQValueListConstIterator it; if(count() == 0) { for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) setColumnWidth(*it, header()->fontMetrics().width(header()->label(*it)) + 10); return; } if(m_columnWeights.isEmpty()) return; // First build a list of minimum widths based on the strings in the listview // header. We won't let the width of the column go below this width. TQValueVector minimumWidth(columns(), 0); int minimumWidthTotal = 0; // Also build a list of either the minimum *or* the fixed width -- whichever is // greater. TQValueVector minimumFixedWidth(columns(), 0); int minimumFixedWidthTotal = 0; for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) { int column = *it; minimumWidth[column] = header()->fontMetrics().width(header()->label(column)) + 10; minimumWidthTotal += minimumWidth[column]; minimumFixedWidth[column] = TQMAX(minimumWidth[column], m_columnFixedWidths[column]); minimumFixedWidthTotal += minimumFixedWidth[column]; } // Make sure that the width won't get any smaller than this. We have to // account for the scrollbar as well. Since this method is called from the // resize event this will set a pretty hard lower bound on the size. setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width()); // If we've got enough room for the fixed widths (larger than the minimum // widths) then instead use those for our "minimum widths". if(minimumFixedWidthTotal < visibleWidth()) { minimumWidth = minimumFixedWidth; minimumWidthTotal = minimumFixedWidthTotal; } // We've got a list of columns "weights" based on some statistics gathered // about the widths of the items in that column. We need to find the total // useful weight to use as a divisor for each column's weight. double totalWeight = 0; for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) totalWeight += m_columnWeights[*it]; // Computed a "weighted width" for each visible column. This would be the // width if we didn't have to handle the cases of minimum and maximum widths. TQValueVector weightedWidth(columns(), 0); for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) weightedWidth[*it] = int(double(m_columnWeights[*it]) / totalWeight * visibleWidth() + 0.5); // The "extra" width for each column. This is the weighted width less the // minimum width or zero if the minimum width is greater than the weighted // width. TQValueVector extraWidth(columns(), 0); // This is used as an indicator if we have any columns where the weighted // width is less than the minimum width. If this is false then we can // just use the weighted width with no problems, otherwise we have to // "readjust" the widths. bool readjust = false; // If we have columns where the weighted width is less than the minimum width // we need to steal that space from somewhere. The amount that we need to // steal is the "neededWidth". int neededWidth = 0; // While we're on the topic of stealing -- we have to have somewhere to steal // from. availableWidth is the sum of the amount of space beyond the minimum // width that each column has been allocated -- the sum of the values of // extraWidth[]. int availableWidth = 0; // Fill in the values discussed above. for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) { if(weightedWidth[*it] < minimumWidth[*it]) { readjust = true; extraWidth[*it] = 0; neededWidth += minimumWidth[*it] - weightedWidth[*it]; } else { extraWidth[*it] = weightedWidth[*it] - minimumWidth[*it]; availableWidth += extraWidth[*it]; } } // The adjustmentRatio is the amount of the "extraWidth[]" that columns will // actually be given. double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth); // This will be the sum of the total space that we actually use. Because of // rounding error this won't be the exact available width. int usedWidth = 0; // Now set the actual column widths. If the weighted widths are all greater // than the minimum widths, just use those, otherwise use the "reajusted // weighted width". for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) { int width; if(readjust) { int adjustedExtraWidth = int(double(extraWidth[*it]) * adjustmentRatio + 0.5); width = minimumWidth[*it] + adjustedExtraWidth; } else width = weightedWidth[*it]; setColumnWidth(*it, width); usedWidth += width; } // Fill the remaining gap for a clean fit into the available space. int remainingWidth = visibleWidth() - usedWidth; setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth); m_widthsDirty = false; } void Playlist::slotAddToUpcoming() { m_collection->setUpcomingPlaylistEnabled(true); m_collection->upcomingPlaylist()->appendItems(selectedItems()); } void Playlist::slotShowRMBMenu(TQListViewItem *item, const TQPoint &point, int column) { if(!item) return; // Create the RMB menu on demand. if(!m_rmbMenu) { // A bit of a hack to get a pointer to the action collection. // Probably more of these actions should be ported over to using TDEActions. m_rmbMenu = new TDEPopupMenu(this); m_rmbUpcomingID = m_rmbMenu->insertItem(SmallIcon("today"), i18n("Add to Play Queue"), this, TQT_SLOT(slotAddToUpcoming())); m_rmbMenu->insertSeparator(); if(!readOnly()) { action("edit_cut")->plug(m_rmbMenu); action("edit_copy")->plug(m_rmbMenu); action("edit_paste")->plug(m_rmbMenu); m_rmbMenu->insertSeparator(); action("removeFromPlaylist")->plug(m_rmbMenu); } else action("edit_copy")->plug(m_rmbMenu); m_rmbEditID = m_rmbMenu->insertItem( i18n("Edit"), this, TQT_SLOT(slotRenameTag())); action("refresh")->plug(m_rmbMenu); action("removeItem")->plug(m_rmbMenu); m_rmbMenu->insertSeparator(); action("guessTag")->plug(m_rmbMenu); action("renameFile")->plug(m_rmbMenu); action("coverManager")->plug(m_rmbMenu); m_rmbMenu->insertSeparator(); m_rmbMenu->insertItem( SmallIcon("folder-new"), i18n("Create Playlist From Selected Items..."), this, TQT_SLOT(slotCreateGroup())); K3bExporter *exporter = new K3bExporter(this); TDEAction *k3bAction = exporter->action(); if(k3bAction) k3bAction->plug(m_rmbMenu); } // Ignore any columns added by subclasses. column -= columnOffset(); bool showEdit = (column == PlaylistItem::TrackColumn) || (column == PlaylistItem::ArtistColumn) || (column == PlaylistItem::AlbumColumn) || (column == PlaylistItem::TrackNumberColumn) || (column == PlaylistItem::GenreColumn) || (column == PlaylistItem::YearColumn); if(showEdit) m_rmbMenu->changeItem(m_rmbEditID, i18n("Edit '%1'").arg(columnText(column + columnOffset()))); m_rmbMenu->setItemVisible(m_rmbEditID, showEdit); // Disable edit menu if only one file is selected, and it's read-only FileHandle file = static_cast(item)->file(); m_rmbMenu->setItemEnabled(m_rmbEditID, file.fileInfo().isWritable() || selectedItems().count() > 1); action("viewCover")->setEnabled(file.coverInfo()->hasCover()); action("removeCover")->setEnabled(file.coverInfo()->hasCover()); m_rmbMenu->popup(point); m_currentColumn = column + columnOffset(); } void Playlist::slotRenameTag() { // kdDebug(65432) << "Playlist::slotRenameTag()" << endl; // setup completions and validators CollectionList *list = CollectionList::instance(); KLineEdit *edit = renameLineEdit(); switch(m_currentColumn - columnOffset()) { case PlaylistItem::ArtistColumn: edit->completionObject()->setItems(list->uniqueSet(CollectionList::Artists)); break; case PlaylistItem::AlbumColumn: edit->completionObject()->setItems(list->uniqueSet(CollectionList::Albums)); break; case PlaylistItem::GenreColumn: { TQStringList genreList; TagLib::StringList genres = TagLib::ID3v1::genreList(); for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) genreList.append(TStringToQString((*it))); edit->completionObject()->setItems(genreList); break; } default: edit->completionObject()->clear(); break; } m_editText = currentItem()->text(m_currentColumn); rename(currentItem(), m_currentColumn); } bool Playlist::editTag(PlaylistItem *item, const TQString &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(TQListViewItem *, const TQString &, int column) { TQString text = renameLineEdit()->text(); bool changed = false; PlaylistItemList l = selectedItems(); // See if any of the files have a tag different from the input. for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end() && !changed; ++it) if((*it)->text(column - columnOffset()) != text) changed = true; if(!changed || (l.count() > 1 && KMessageBox::warningContinueCancel( 0, i18n("This will edit multiple files. Are you sure?"), TQString(), i18n("Edit"), "DontWarnMultipleTags") == KMessageBox::Cancel)) { return; } for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) editTag(*it, text, column); TagTransactionManager::instance()->commit(); CollectionList::instance()->dataChanged(); dataChanged(); update(); } void Playlist::slotColumnOrderChanged(int, int from, int to) { if(from == 0 || to == 0) { updatePlaying(); m_leftColumn = header()->mapToSection(0); } SharedSettings::instance()->setColumnOrder(this); } void Playlist::slotToggleColumnVisible(int column) { if(!isColumnVisible(column)) { int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset(); int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset(); if(column == fileNameColumn && isColumnVisible(fullPathColumn)) { hideColumn(fullPathColumn, false); SharedSettings::instance()->toggleColumnVisible(fullPathColumn); } if(column == fullPathColumn && isColumnVisible(fileNameColumn)) { hideColumn(fileNameColumn, false); SharedSettings::instance()->toggleColumnVisible(fileNameColumn); } } if(isColumnVisible(column)) hideColumn(column); else showColumn(column); SharedSettings::instance()->toggleColumnVisible(column - columnOffset()); } void Playlist::slotCreateGroup() { TQString name = m_collection->playlistNameDialog(i18n("Create New Playlist")); if(!name.isEmpty()) new Playlist(m_collection, selectedItems(), name); } void Playlist::notifyUserColumnWidthModeChanged() { KMessageBox::information(this, i18n("Manual column widths have been enabled. You can " "switch back to automatic column sizes in the view " "menu."), i18n("Manual Column Widths Enabled"), "ShowManualColumnWidthInformation"); } void Playlist::slotColumnSizeChanged(int column, int, int newSize) { m_widthsDirty = true; m_columnFixedWidths[column] = newSize; } void Playlist::slotInlineCompletionModeChanged(TDEGlobalSettings::Completion mode) { SharedSettings::instance()->setInlineCompletionMode(mode); } void Playlist::slotPlayCurrent() { TQListViewItemIterator it(this, TQListViewItemIterator::Selected); PlaylistItem *next = static_cast(it.current()); TrackSequenceManager::instance()->setNextItem(next); action("forward")->activate(); } //////////////////////////////////////////////////////////////////////////////// // helper functions //////////////////////////////////////////////////////////////////////////////// TQDataStream &operator<<(TQDataStream &s, const Playlist &p) { s << p.name(); s << p.fileName(); s << p.files(); return s; } TQDataStream &operator>>(TQDataStream &s, Playlist &p) { p.read(s); return s; } bool processEvents() { static TQTime time = TQTime::currentTime(); if(time.elapsed() > 100) { time.restart(); kapp->processEvents(); return true; } return false; } #include "playlist.moc"