/* Copyright 2002-2004 Mark Kretschmann, Max Howell, Christian Muehlhaeuser * Copyright 2005-2006 Seb Ruiz, Mike Diehl, Ian Monroe, Gábor Lehel, Alexandre Pereira de Oliveira * Licensed as described in the COPYING file found in the root of this distribution * Maintainer: Max Howell * NOTES * * The PlaylistWindow handles some Playlist events. Thanks! * This class has a TQOBJECT but it's private so you can only connect via PlaylistWindow::PlaylistWindow * Mostly it's sensible to implement playlist functionality in this class * TODO Obtaining information about the playlist is currently hard, we need the playlist to be globally * available and have some more useful public functions */ #define DEBUG_PREFIX "Playlist" #include #include "amarok.h" #include "amarokconfig.h" #include "app.h" #include "debug.h" #include "collectiondb.h" #include "collectionbrowser.h" #include "columnlist.h" #include "deletedialog.h" #include "enginecontroller.h" #include "expression.h" #include "k3bexporter.h" #include "metabundle.h" #include "mountpointmanager.h" #include "osd.h" #include "playerwindow.h" #include "playlistitem.h" #include "playlistbrowser.h" #include "playlistbrowseritem.h" //for stream editor dialog #include "playlistloader.h" #include "playlistselection.h" #include "queuemanager.h" #include "prettypopupmenu.h" #include "scriptmanager.h" #include "sliderwidget.h" #include "starmanager.h" #include "statusbar.h" //for status messages #include "tagdialog.h" #include "threadmanager.h" #include "xspfplaylist.h" #include //for pow() in playNextTrack() #include #include //copyToClipboard(), slotMouseButtonPressed() #include #include #include //undo system #include //eventFilter() #include //showUsageMessage() #include #include //slotGlowTimer() #include //toolTipText() #include #include #include #include //addHybridTracks() #include //playNextTrack() #include #include #include #include //setOverrideCursor() #include #include //rename() #include #include //slotShowContextMenu() #include //deleteSelectedFiles() #include //setCurrentTrack() #include #include #include #include //random Mode #include //KGlobal::dirs() #include #include //::showContextMenu() #include #include // abs extern "C" { #if KDE_VERSION < KDE_MAKE_VERSION(3,3,91) #include //ControlMask in contentsDragMoveEvent() #endif } #include "playlist.h" namespace Amarok { const DynamicMode *dynamicMode() { return Playlist::instance() ? Playlist::instance()->dynamicMode() : 0; } } typedef PlaylistIterator MyIt; ////////////////////////////////////////////////////////////////////////////////////////// /// CLASS TagWriter : Threaded tag-updating ////////////////////////////////////////////////////////////////////////////////////////// class TagWriter : public ThreadManager::Job { //TODO make this do all tags at once when you split playlist.cpp up public: TagWriter( PlaylistItem*, const TQString &oldTag, const TQString &newTag, const int, const bool updateView = true ); ~TagWriter(); bool doJob(); void completeJob(); private: PlaylistItem* const m_item; bool m_failed; TQString m_oldTagString; TQString m_newTagString; int m_tagType; bool m_updateView; }; ////////////////////////////////////////////////////////////////////////////////////////// /// Glow ////////////////////////////////////////////////////////////////////////////////////////// namespace Glow { namespace Text { static float dr, dg, db; static int r, g, b; } namespace Base { static float dr, dg, db; static int r, g, b; } static const uint STEPS = 13; static uint counter; static TQTimer timer; inline void startTimer() { counter = 0; timer.start( 40 ); } inline void reset() { counter = 0; timer.stop(); } } ////////////////////////////////////////////////////////////////////////////////////////// /// CLASS Playlist ////////////////////////////////////////////////////////////////////////////////////////// TQMutex* Playlist::s_dynamicADTMutex = new TQMutex(); Playlist *Playlist::s_instance = 0; Playlist::Playlist( TQWidget *parent ) : KListView( parent, "ThePlaylist" ) , EngineObserver( EngineController::instance() ) , m_startupTime_t( TQDateTime::currentDateTime().toTime_t() ) , m_oldestTime_t( CollectionDB::instance()->query( "SELECT MIN( createdate ) FROM statistics;" ).first().toInt() ) , m_currentTrack( 0 ) , m_marker( 0 ) , m_hoveredRating( 0 ) , m_firstColumn( 0 ) , m_totalCount( 0 ) , m_totalLength( 0 ) , m_selCount( 0 ) , m_selLength( 0 ) , m_visCount( 0 ) , m_visLength( 0 ) , m_total( 0 ) , m_itemCountDirty( false ) , m_undoButton( 0 ) , m_redoButton( 0 ) , m_clearButton( 0 ) , m_undoDir( Amarok::saveLocation( "undo/" ) ) , m_undoCounter( 0 ) , m_dynamicMode( 0 ) , m_stopAfterTrack( 0 ) , m_stopAfterMode( DoNotStop ) , m_showHelp( true ) , m_dynamicDirt( false ) , m_queueDirt( false ) , m_undoDirt( false ) , m_insertFromADT( 0 ) , m_itemToReallyCenter( 0 ) , m_renameItem( 0 ) , m_lockStack( 0 ) , m_columnFraction( PlaylistItem::NUM_COLUMNS, 0 ) , m_oldRandom( 0 ) , m_oldRepeat( 0 ) , m_playlistName( i18n( "Untitled" ) ) , m_proposeOverwriting( false ) , m_urlIndex( &PlaylistItem::url ) { s_instance = this; connect( CollectionDB::instance(), TQT_SIGNAL(fileMoved(const TQString&, const TQString&, const TQString&)), TQT_SLOT(updateEntriesUrl(const TQString&, const TQString&, const TQString&)) ); connect( CollectionDB::instance(), TQT_SIGNAL(uniqueIdChanged(const TQString&, const TQString&, const TQString&)), TQT_SLOT(updateEntriesUniqueId(const TQString&, const TQString&, const TQString&)) ); connect( CollectionDB::instance(), TQT_SIGNAL(fileDeleted(const TQString&, const TQString&)), TQT_SLOT(updateEntriesStatusDeleted(const TQString&, const TQString&)) ); connect( CollectionDB::instance(), TQT_SIGNAL(fileAdded(const TQString&, const TQString&)), TQT_SLOT(updateEntriesStatusAdded(const TQString&, const TQString&)) ); connect( CollectionDB::instance(), TQT_SIGNAL(filesAdded(const TQMap&)), TQT_SLOT(updateEntriesStatusAdded(const TQMap&)) ); initStarPixmaps(); EngineController* const ec = EngineController::instance(); connect( ec, TQT_SIGNAL(orderPrevious()), TQT_SLOT(playPrevTrack()) ); connect( ec, TQT_SIGNAL(orderNext( const bool )), TQT_SLOT(playNextTrack( const bool )) ); connect( ec, TQT_SIGNAL(orderCurrent()), TQT_SLOT(playCurrentTrack()) ); connect( this, TQT_SIGNAL( itemCountChanged( int, int, int, int, int, int ) ), ec, TQT_SLOT( playlistChanged() ) ); setShowSortIndicator( true ); setDropVisualizer( false ); //we handle the drawing for ourselves setDropVisualizerWidth( 3 ); // FIXME: This doesn't work, and steals focus when an item is clicked twice. //setItemsRenameable( true ); setAcceptDrops( true ); setSelectionMode( TQListView::Extended ); setAllColumnsShowFocus( true ); //setItemMargin( 1 ); //aesthetics setMouseTracking( true ); #if KDE_IS_VERSION( 3, 3, 91 ) setShadeSortColumn( true ); #endif for( int i = 0; i < MetaBundle::NUM_COLUMNS; ++i ) { addColumn( PlaylistItem::prettyColumnName( i ), 0 ); switch( i ) { case PlaylistItem::Title: case PlaylistItem::Artist: case PlaylistItem::Composer: case PlaylistItem::Year: case PlaylistItem::Album: case PlaylistItem::DiscNumber: case PlaylistItem::Track: case PlaylistItem::Bpm: case PlaylistItem::Genre: case PlaylistItem::Comment: case PlaylistItem::Score: case PlaylistItem::Rating: setRenameable( i, true ); continue; default: setRenameable( i, false ); } } setColumnWidth( PlaylistItem::Title, 200 ); setColumnWidth( PlaylistItem::Artist, 100 ); setColumnWidth( PlaylistItem::Album, 100 ); setColumnWidth( PlaylistItem::Length, 80 ); if( AmarokConfig::showMoodbar() ) setColumnWidth( PlaylistItem::Mood, 120 ); if( AmarokConfig::useRatings() ) setColumnWidth( PlaylistItem::Rating, PlaylistItem::ratingColumnWidth() ); setColumnAlignment( PlaylistItem::Length, TQt::AlignRight ); setColumnAlignment( PlaylistItem::Track, TQt::AlignCenter ); setColumnAlignment( PlaylistItem::DiscNumber, TQt::AlignCenter ); setColumnAlignment( PlaylistItem::Bpm, TQt::AlignRight ); setColumnAlignment( PlaylistItem::Year, TQt::AlignCenter ); setColumnAlignment( PlaylistItem::Bitrate, TQt::AlignCenter ); setColumnAlignment( PlaylistItem::SampleRate, TQt::AlignCenter ); setColumnAlignment( PlaylistItem::Filesize, TQt::AlignCenter ); setColumnAlignment( PlaylistItem::Score, TQt::AlignCenter ); setColumnAlignment( PlaylistItem::Type, TQt::AlignCenter ); setColumnAlignment( PlaylistItem::PlayCount, TQt::AlignCenter ); connect( this, TQT_SIGNAL( doubleClicked( TQListViewItem* ) ), this, TQT_SLOT( doubleClicked( TQListViewItem* ) ) ); connect( this, TQT_SIGNAL( returnPressed( TQListViewItem* ) ), this, TQT_SLOT( activate( TQListViewItem* ) ) ); connect( this, TQT_SIGNAL( mouseButtonPressed( int, TQListViewItem*, const TQPoint&, int ) ), this, TQT_SLOT( slotMouseButtonPressed( int, TQListViewItem*, const TQPoint&, int ) ) ); connect( this, TQT_SIGNAL( queueChanged( const PLItemList &, const PLItemList & ) ), this, TQT_SLOT( slotQueueChanged( const PLItemList &, const PLItemList & ) ) ); connect( this, TQT_SIGNAL( itemRenamed( TQListViewItem*, const TQString&, int ) ), this, TQT_SLOT( writeTag( TQListViewItem*, const TQString&, int ) ) ); connect( this, TQT_SIGNAL( aboutToClear() ), this, TQT_SLOT( saveUndoState() ) ); connect( CollectionDB::instance(), TQT_SIGNAL( scoreChanged( const TQString&, float ) ), this, TQT_SLOT( scoreChanged( const TQString&, float ) ) ); connect( CollectionDB::instance(), TQT_SIGNAL( ratingChanged( const TQString&, int ) ), this, TQT_SLOT( ratingChanged( const TQString&, int ) ) ); connect( CollectionDB::instance(), TQT_SIGNAL( fileMoved( const TQString&, const TQString& ) ), this, TQT_SLOT( fileMoved( const TQString&, const TQString& ) ) ); connect( header(), TQT_SIGNAL( indexChange( int, int, int ) ), this, TQT_SLOT( columnOrderChanged() ) ), connect( &Glow::timer, TQT_SIGNAL(timeout()), TQT_SLOT(slotGlowTimer()) ); KActionCollection* const ac = Amarok::actionCollection(); KAction *copy = KStdAction::copy( TQT_TQOBJECT(this), TQT_SLOT( copyToClipboard() ), ac, "playlist_copy" ); KStdAction::selectAll( TQT_TQOBJECT(this), TQT_SLOT( selectAll() ), ac, "playlist_select_all" ); m_clearButton = new KAction( i18n( "clear playlist", "&Clear" ), Amarok::icon( "playlist_clear" ), 0, TQT_TQOBJECT(this), TQT_SLOT( clear() ), ac, "playlist_clear" ); m_undoButton = KStdAction::undo( TQT_TQOBJECT(this), TQT_SLOT( undo() ), ac, "playlist_undo" ); m_redoButton = KStdAction::redo( TQT_TQOBJECT(this), TQT_SLOT( redo() ), ac, "playlist_redo" ); m_undoButton ->setIcon( Amarok::icon( "undo" ) ); m_redoButton ->setIcon( Amarok::icon( "redo" ) ); new KAction( i18n( "&Repopulate" ), Amarok::icon( "playlist_refresh" ), 0, TQT_TQOBJECT(this), TQT_SLOT( repopulate() ), ac, "repopulate" ); new KAction( i18n( "S&huffle" ), "rebuild", CTRL+Key_H, TQT_TQOBJECT(this), TQT_SLOT( shuffle() ), ac, "playlist_shuffle" ); KAction *gotoCurrent = new KAction( i18n( "&Go To Current Track" ), Amarok::icon( "music" ), CTRL+Key_J, TQT_TQOBJECT(this), TQT_SLOT( showCurrentTrack() ), ac, "playlist_show" ); new KAction( i18n( "&Remove Duplicate && Dead Entries" ), 0, TQT_TQOBJECT(this), TQT_SLOT( removeDuplicates() ), ac, "playlist_remove_duplicates" ); new KAction( i18n( "&Queue Selected Tracks" ), Amarok::icon( "queue_track" ), CTRL+Key_D, TQT_TQOBJECT(this), TQT_SLOT( queueSelected() ), ac, "queue_selected" ); KToggleAction *stopafter = new KToggleAction( i18n( "&Stop Playing After Track" ), Amarok::icon( "stop" ), CTRL+ALT+Key_V, TQT_TQOBJECT(this), TQT_SLOT( toggleStopAfterCurrentItem() ), ac, "stop_after" ); { // KAction idiocy -- shortcuts don't work until they've been plugged into a menu KPopupMenu asdf; copy->plug( &asdf ); stopafter->plug( &asdf ); gotoCurrent->plug( &asdf ); copy->unplug( &asdf ); stopafter->unplug( &asdf ); gotoCurrent->unplug( &asdf ); } //ensure we update action enabled states when repeat Playlist is toggled connect( ac->action( "repeat" ), TQT_SIGNAL(activated( int )), TQT_SLOT(updateNextPrev()) ); connect( ac->action( "repeat" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) ); connect( ac->action( "favor_tracks" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) ); connect( ac->action( "random_mode" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) ); // undostates are written in chronological order, so this is a clever way to get them back in the correct order :) TQStringList undos = m_undoDir.entryList( TQString("*.xml"), TQDir::Files, TQDir::Time ); foreach( undos ) m_undoList.append( m_undoDir.absPath() + '/' + (*it) ); m_undoCounter = m_undoList.count(); m_undoButton->setEnabled( !m_undoList.isEmpty() ); m_redoButton->setEnabled( false ); engineStateChanged( EngineController::engine()->state() ); //initialise state of UI paletteChange( palette() ); //sets up glowColors restoreLayout( KGlobal::config(), "PlaylistColumnsLayout" ); // Sorting must be disabled when current.xml is being loaded. See BUG 113042 KListView::setSorting( NO_SORT ); //use base so we don't saveUndoState() too setDynamicMode( 0 ); m_smartResizing = Amarok::config( "PlaylistWindow" )->readBoolEntry( "Smart Resizing", true ); columnOrderChanged(); //cause the column fractions to be updated, but in a safe way, ie no specific column columnResizeEvent( header()->count(), 0, 0 ); //do after you resize all the columns connect( header(), TQT_SIGNAL(sizeChange( int, int, int )), TQT_SLOT(columnResizeEvent( int, int, int )) ); connect( this, TQT_SIGNAL( contentsMoving( int, int ) ), TQT_SLOT( slotContentsMoving() ) ); connect( App::instance(), TQT_SIGNAL( useScores( bool ) ), this, TQT_SLOT( slotUseScores( bool ) ) ); connect( App::instance(), TQT_SIGNAL( useRatings( bool ) ), this, TQT_SLOT( slotUseRatings( bool ) ) ); connect( App::instance(), TQT_SIGNAL( moodbarPrefs( bool, bool, int, bool ) ), this, TQT_SLOT( slotMoodbarPrefs( bool, bool, int, bool ) ) ); Amarok::ToolTip::add( this, viewport() ); header()->installEventFilter( this ); renameLineEdit()->installEventFilter( this ); setTabOrderedRenaming( false ); m_filtertimer = new TQTimer( this ); connect( m_filtertimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(setDelayedFilter()) ); connect( MountPointManager::instance(), TQT_SIGNAL(mediumConnected( int )), TQT_SLOT(mediumChange( int )) ); connect( MountPointManager::instance(), TQT_SIGNAL(mediumRemoved( int )), TQT_SLOT(mediumChange( int )) ); m_clicktimer = new TQTimer( this ); connect( m_clicktimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotSingleClick()) ); } Playlist::~Playlist() { saveLayout( KGlobal::config(), "PlaylistColumnsLayout" ); if( AmarokConfig::savePlaylist() && m_lockStack == 0 ) saveXML( defaultPlaylistPath() ); //speed up quit a little safeClear(); //our implementation is slow Amarok::ToolTip::remove( viewport() ); blockSignals( true ); //might help s_instance = 0; } //////////////////////////////////////////////////////////////////////////////// /// Media Handling //////////////////////////////////////////////////////////////////////////////// void Playlist::mediumChange( int deviceid ) // SLOT { Q_UNUSED( deviceid ); for( TQListViewItem *it = firstChild(); it; it = it->nextSibling() ) { PlaylistItem *p = dynamic_cast( it ); if( p ) { bool exist = p->exists(); if( exist != p->checkExists() ) { p->setFilestatusEnabled( p->checkExists() ); p->update(); } } } } void Playlist::insertMedia( const KURL::List &list, int options ) { if( list.isEmpty() ) { Amarok::StatusBar::instance()->shortMessage( i18n("Attempted to insert nothing into playlist.") ); return; // don't add empty items } const bool isPlaying = EngineController::engine()->state() == Engine::Playing; if( isPlaying ) options &= ~Playlist::StartPlay; bool directPlay = options & (Playlist::DirectPlay | Playlist::StartPlay); if( options & Replace ) clear(); else options |= Playlist::Colorize; PlaylistItem *after = lastItem(); KURL::List addMe; TQPtrList alreadyHave; // Filter out duplicates foreachType( KURL::List, list ) { PlaylistItem *item = m_urlIndex.getFirst( *it ); if ( item ) alreadyHave.append( item ); else addMe.append( *it ); } if( options & Queue ) { if ( addMe.isEmpty() ) // all songs to be queued are already in the playlist { // queue all the songs foreachType( TQPtrList, alreadyHave ) queue( *it, false, false ); return; } else { // We add the track after the last track on queue, or after current if the queue is empty after = m_nextTracks.isEmpty() ? currentTrack() : m_nextTracks.getLast(); // If there's no tracks on the queue, and there's no current track, fall back to the last item if ( !after ) after = lastItem(); } } else if( options & Unique ) { int alreadyOnPlaylist = alreadyHave.count(); if ( alreadyOnPlaylist ) { if (directPlay) activate( alreadyHave.getFirst() ); Amarok::StatusBar::instance()->shortMessage( i18n("One track was already in the playlist, so it was not added.", "%n tracks were already in the playlist, so they were not added.", alreadyOnPlaylist ) ); } } if( options & Unique || options & Queue ) insertMediaInternal( addMe, after, options ); else insertMediaInternal( list, after, options ); } void Playlist::insertMediaInternal( const KURL::List &list, PlaylistItem *after, int options ) { if ( !list.isEmpty() ) { setSorting( NO_SORT ); // prevent association with something that is about to be deleted // TODO improve the playlist with a list of items that are volatile or something while( after && after->url().isEmpty() ) after = static_cast( after->itemAbove() ); ThreadManager::instance()->queueJob( new UrlLoader( list, after, options ) ); ScriptManager::instance()->notifyPlaylistChange("changed"); } } void Playlist::insertMediaSql( const TQString& sql, int options ) { const bool isPlaying = EngineController::engine()->state() == Engine::Playing; if( isPlaying ) options &= ~Playlist::StartPlay; // TODO Implement more options PlaylistItem *after = 0; if ( options & Replace ) clear(); if ( options & Append ) after = lastItem(); setSorting( NO_SORT ); ThreadManager::instance()->queueJob( new SqlLoader( sql, after, options ) ); ScriptManager::instance()->notifyPlaylistChange("changed"); } void Playlist::addDynamicModeTracks( uint songCount ) { if( songCount < 1 ) return; int currentPos = 0; for( MyIt it( this, MyIt::Visible ); *it; ++it ) { if( m_currentTrack && *it == m_currentTrack ) break; else if( !m_currentTrack && (*it)->isDynamicEnabled() ) break; ++currentPos; } currentPos++; int required = currentPos + dynamicMode()->upcomingCount(); // currentPos handles currentTrack int remainder = totalTrackCount(); if( required > remainder ) songCount = required - remainder; DynamicMode *m = modifyDynamicMode(); KURL::List tracksToInsert = m->retrieveTracks( songCount ); Playlist::instance()->finishedModifying( m ); insertMedia( tracksToInsert, Playlist::Unique ); } /** * @param songCount : Number of tracks to be shown after the current track */ void Playlist::adjustDynamicUpcoming( bool saveUndo ) { /** * If m_currentTrack exists, we iterate until we find it * Else, we iterate until we find an item which is enabled **/ MyIt it( this, MyIt::Visible ); //Notice we'll use this up to the end of the function! //Skip previously played for( ; *it; ++it ) { if( m_currentTrack && *it == m_currentTrack ) break; else if( !m_currentTrack && (*it)->isDynamicEnabled() ) break; } //Skip current if( m_currentTrack ) ++it; int x = 0; for ( ; *it && x < dynamicMode()->upcomingCount() ; ++it, ++x ); if ( x < dynamicMode()->upcomingCount() ) { addDynamicModeTracks( dynamicMode()->upcomingCount() - x ); ScriptManager::instance()->notifyPlaylistChange("changed"); } if( saveUndo ) saveUndoState(); } /** * @param songCount : Number of tracks to be shown before the current track */ void Playlist::adjustDynamicPrevious( uint songCount, bool saveUndo ) { int current = currentTrackIndex(); int x = current - songCount; TQPtrList list; int y=0; for( TQListViewItemIterator it( firstChild() ); y < x ; list.prepend( *it ), ++it, y++ ); if( list.isEmpty() ) return; if ( saveUndo ) saveUndoState(); //remove the items for( TQListViewItem *item = list.first(); item; item = list.next() ) { removeItem( static_cast( item ) ); delete item; } ScriptManager::instance()->notifyPlaylistChange("changed"); } void Playlist::setDynamicHistory( bool enable /*false*/ ) { if( !m_currentTrack ) return; for( PlaylistIterator it( this, PlaylistIterator::All ) ; *it ; ++it ) { if( *it == m_currentTrack ) break; //avoid repainting if we can. if( (*it)->isDynamicEnabled() == enable ) { (*it)->setDynamicEnabled( !enable ); (*it)->update(); } } } TQString Playlist::defaultPlaylistPath() //static { return Amarok::saveLocation() + "current.xml"; } void Playlist::restoreSession() { KURL url; if ( Amarok::config()->readBoolEntry( "First 1.4 Run", true ) ) { // On first startup of 1.4, we load a special playlist with an intro track url.setPath( locate( "data", "amarok/data/firstrun.m3u" ) ); Amarok::config()->writeEntry( "First 1.4 Run", false ); } else url.setPath( Amarok::saveLocation() + "current.xml" ); // check it exists, because on the first ever run it doesn't and // it looks bad to show "some URLs were not suitable.." on the // first ever-run if( TQFile::exists( url.path() ) ) { ThreadManager::instance()->queueJob( new UrlLoader( url, 0, 0 ) ); } } /* The following two functions (saveLayout(), restoreLayout()), taken from klistview.cpp, are largely Copyright (C) 2000 Reginald Stadlbauer Copyright (C) 2000,2003 Charles Samuels Copyright (C) 2000 Peter Putzer */ void Playlist::saveLayout(KConfig *config, const TQString &group) const { KConfigGroupSaver saver(config, group); TQStringList names, widths, order; const int colCount = columns(); TQHeader* const thisHeader = header(); for (int i = 0; i < colCount; ++i) { names << PlaylistItem::exactColumnName(i); widths << TQString::number(columnWidth(i)); order << TQString::number(thisHeader->mapToIndex(i)); } config->writeEntry("ColumnsVersion", 1); config->writeEntry("ColumnNames", names); config->writeEntry("ColumnWidths", widths); config->writeEntry("ColumnOrder", order); config->writeEntry("SortColumn", columnSorted()); config->writeEntry("SortAscending", ascendingSort()); } void Playlist::restoreLayout(KConfig *config, const TQString &group) { KConfigGroupSaver saver(config, group); int version = config->readNumEntry("ColumnsVersion", 0); TQValueList iorder; //internal ordering if( version ) { TQStringList names = config->readListEntry("ColumnNames"); for( int i = 0, n = names.count(); i < n; ++i ) { bool found = false; for( int ii = i; ii < PlaylistItem::NUM_COLUMNS; ++ii ) //most likely, it's where we left it { if( names[i] == PlaylistItem::exactColumnName(ii) ) { iorder.append(ii); found = true; break; } } if( !found ) { for( int ii = 0; ii < i; ++ii ) //but maybe it's not if( names[i] == PlaylistItem::exactColumnName(ii) ) { iorder.append(ii); found = true; break; } } if( !found ) return; //oops? -- revert to the default. } } else { int oldorder[] = { 0, 1, 2, 5, 4, 9, 8, 7, 10, 12, 13, 15, 16, 11, 17, 18, 19, 3, 6, 20 }; for( int i = 0; i != 20; ++i ) iorder.append(oldorder[i]); } TQStringList cols = config->readListEntry("ColumnWidths"); int i = 0; { // scope the iterators TQStringList::ConstIterator it = cols.constBegin(); const TQStringList::ConstIterator itEnd = cols.constEnd(); for (; it != itEnd; ++it) setColumnWidth(iorder[i++], (*it).toInt()); } // move sections in the correct sequence: from lowest to highest index position // otherwise we move a section from an index, which modifies // all index numbers to the right of the moved one cols = config->readListEntry("ColumnOrder"); const int colCount = columns(); for (i = 0; i < colCount; ++i) // final index positions from lowest to highest { TQStringList::ConstIterator it = cols.constBegin(); const TQStringList::ConstIterator itEnd = cols.constEnd(); int section = 0; for (; (it != itEnd) && (iorder[(*it).toInt()] != i); ++it, ++section) ; if ( it != itEnd ) { // found the section to move to position i header()->moveSection(iorder[section], i); } } if ( config->hasKey("SortColumn") ) { const int sort = config->readNumEntry("SortColumn"); if( sort >= 0 && uint(sort) < iorder.count() ) setSorting(iorder[config->readNumEntry("SortColumn")], config->readBoolEntry("SortAscending", true)); } if( !AmarokConfig::useScores() ) hideColumn( PlaylistItem::Score ); if( !AmarokConfig::useRatings() ) hideColumn( PlaylistItem::Rating ); if( !AmarokConfig::showMoodbar() ) hideColumn( PlaylistItem::Mood ); } void Playlist::addToUniqueMap( const TQString uniqueid, PlaylistItem* item ) { TQPtrList *list; if( m_uniqueMap.contains( uniqueid ) ) list = m_uniqueMap[uniqueid]; else list = new TQPtrList(); list->append( item ); if( !m_uniqueMap.contains( uniqueid ) ) m_uniqueMap[uniqueid] = list; } void Playlist::removeFromUniqueMap( const TQString uniqueid, PlaylistItem* item ) { if( !m_uniqueMap.contains( uniqueid ) ) return; TQPtrList *list; list = m_uniqueMap[uniqueid]; list->remove( item ); //don't care about return value if( list->isEmpty() ) { delete list; m_uniqueMap.remove( uniqueid ); } } void Playlist::updateEntriesUrl( const TQString &oldUrl, const TQString &newUrl, const TQString &uniqueid ) { // Make sure the MoodServer gets this signal first! MoodServer::instance()->slotFileMoved( oldUrl, newUrl ); TQPtrList *list; if( m_uniqueMap.contains( uniqueid ) ) { list = m_uniqueMap[uniqueid]; PlaylistItem *item; for( item = list->first(); item; item = list->next() ) { item->setUrl( KURL( newUrl ) ); item->setFilestatusEnabled( item->checkExists() ); } } } void Playlist::updateEntriesUniqueId( const TQString &/*url*/, const TQString &oldid, const TQString &newid ) { TQPtrList *list, *oldlist; if( m_uniqueMap.contains( oldid ) ) { list = m_uniqueMap[oldid]; m_uniqueMap.remove( oldid ); PlaylistItem *item; for( item = list->first(); item; item = list->next() ) { item->setUniqueId( newid ); item->readTags(); } if( !m_uniqueMap.contains( newid ) ) m_uniqueMap[newid] = list; else { oldlist = m_uniqueMap[newid]; for( item = list->first(); item; item = list->next() ) oldlist->append( item ); delete list; } } } void Playlist::updateEntriesStatusDeleted( const TQString &/*absPath*/, const TQString &uniqueid ) { TQPtrList *list; if( m_uniqueMap.contains( uniqueid ) ) { list = m_uniqueMap[uniqueid]; PlaylistItem *item; for( item = list->first(); item; item = list->next() ) item->setFilestatusEnabled( false ); } } void Playlist::updateEntriesStatusAdded( const TQString &absPath, const TQString &uniqueid ) { TQPtrList *list; if( m_uniqueMap.contains( uniqueid ) ) { list = m_uniqueMap[uniqueid]; if( !list ) return; PlaylistItem *item; for( item = list->first(); item; item = list->next() ) { if( absPath != item->url().path() ) item->setPath( absPath ); //in case the UID was the same, but the path has changed item->setFilestatusEnabled( true ); } } } void Playlist::updateEntriesStatusAdded( const TQMap &map ) { TQMap*> uniquecopy( m_uniqueMap ); TQMap*>::Iterator it; for( it = uniquecopy.begin(); it != uniquecopy.end(); ++it ) { if( map.contains( it.key() )) { updateEntriesStatusAdded( map[it.key()], it.key() ); uniquecopy.remove( it ); } } for( it = uniquecopy.begin(); it != uniquecopy.end(); ++it ) updateEntriesStatusDeleted( TQString(), it.key() ); } //////////////////////////////////////////////////////////////////////////////// /// Current Track Handling //////////////////////////////////////////////////////////////////////////////// void Playlist::playNextTrack( bool forceNext ) { PlaylistItem *item = currentTrack(); if( !m_visCount || stopAfterMode() == StopAfterCurrent ) { if( dynamicMode() && m_visCount ) { item->setDynamicEnabled( false ); advanceDynamicTrack(); m_dynamicDirt = false; } EngineController::instance()->stop(); setStopAfterMode( DoNotStop ); if( !AmarokConfig::randomMode() ) { item = MyIt::nextVisible( item ); while( item && ( !checkFileStatus( item ) || !item->exists() ) ) item = MyIt::nextVisible( item ); setCurrentTrack( item ); } return; } if( !Amarok::repeatTrack() || forceNext ) { if( !m_nextTracks.isEmpty() ) { item = m_nextTracks.first(); m_nextTracks.remove(); if ( dynamicMode() ) // move queued track to the top of the playlist, to prevent it from being played twice // this is done automatically by most queue changing functions, but not if the user manually moves the track moveItem( item, 0, m_currentTrack ); emit queueChanged( PLItemList(), PLItemList( item ) ); } else if( Amarok::entireAlbums() && m_currentTrack && m_currentTrack->nextInAlbum() ) item = m_currentTrack->nextInAlbum(); else if( Amarok::repeatAlbum() && repeatAlbumTrackCount() && ( repeatAlbumTrackCount() > 1 || !forceNext ) ) item = m_currentTrack->m_album->tracks.getFirst(); else if( AmarokConfig::randomMode() ) { TQValueVector tracks; //make a list of everything we can play if( Amarok::randomAlbums() ) // add the first visible track from every unplayed album { for( ArtistAlbumMap::const_iterator it = m_albums.constBegin(), end = m_albums.constEnd(); it != end; ++it ) for( AlbumMap::const_iterator it2 = (*it).constBegin(), end2 = (*it).constEnd(); it2 != end2; ++it2 ) if( m_prevAlbums.findRef( *it2 ) == -1 ) { if ( (*it2)->tracks.getFirst() ) tracks.append( (*it2)->tracks.getFirst() ); } } else for( MyIt it( this ); *it; ++it ) if ( !m_prevTracks.containsRef( *it ) && checkFileStatus( *it ) && (*it)->exists() ) tracks.push_back( *it ); if( tracks.isEmpty() ) { //we have played everything item = 0; if( Amarok::randomAlbums() ) { if ( m_prevAlbums.count() <= 8 ) { m_prevAlbums.first(); while( m_prevAlbums.count() ) removeFromPreviousAlbums(); if( m_currentTrack ) { // don't add it to previous albums if we only have one album in the playlist // would loop infinitely otherwise TQPtrList albums; for( PlaylistIterator it( this, PlaylistIterator::Visible ); *it && albums.count() <= 1; ++it ) if( albums.findRef( (*it)->m_album ) == -1 ) albums.append( (*it)->m_album ); if ( albums.count() > 1 ) appendToPreviousAlbums( m_currentTrack->m_album ); } } else { m_prevAlbums.first(); //set's current item to first item //keep 80 tracks in the previous list so item time user pushes play //we don't risk playing anything too recent while( m_prevAlbums.count() > 8 ) removeFromPreviousAlbums(); //removes current item } } else { if ( m_prevTracks.count() <= 80 ) { m_prevTracks.first(); while( m_prevTracks.count() ) removeFromPreviousTracks(); if( m_currentTrack ) { // don't add it to previous tracks if we only have one file in the playlist // would loop infinitely otherwise int count = 0; for( PlaylistIterator it( this, PlaylistIterator::Visible ); *it && count <= 1; ++it ) ++count; if ( count > 1 ) appendToPreviousTracks( m_currentTrack ); } } else { m_prevTracks.first(); //set's current item to first item //keep 80 tracks in the previous list so item time user pushes play //we don't risk playing anything too recent while( m_prevTracks.count() > 80 ) removeFromPreviousTracks(); //removes current item } } if( Amarok::repeatPlaylist() ) { playNextTrack(); return; } //else we stop via activate( 0 ) below } else { if( Amarok::favorNone() ) item = tracks.at( KApplication::random() % tracks.count() ); //is O(1) else { const uint currenttime_t = TQDateTime::currentDateTime().toTime_t(); TQValueVector weights( tracks.size() ); TQ_INT64 total = m_total; if( Amarok::randomAlbums() ) { for( int i = 0, n = tracks.count(); i < n; ++i ) { weights[i] = tracks.at( i )->m_album->total; if( Amarok::favorLastPlay() ) { const int inc = int( float( ( currenttime_t - m_startupTime_t ) * tracks.at( i )->m_album->tracks.count() + 0.5 ) / tracks.at( i )->m_album->tracks.count() ); weights[i] += inc; total += inc; } } } else { for( int i = 0, n = tracks.count(); i < n; ++i ) { weights[i] = tracks.at( i )->totalIncrementAmount(); if( Amarok::favorLastPlay() ) weights[i] += currenttime_t - m_startupTime_t; } if( Amarok::favorLastPlay() ) total += ( currenttime_t - m_startupTime_t ) * weights.count(); } TQ_INT64 random; if( Amarok::favorLastPlay() ) //really big huge numbers { TQ_INT64 r = TQ_INT64( ( KApplication::random() / pow( 2, sizeof( int ) * 8 ) ) * pow( 2, 64 ) ); random = r % total; } else random = KApplication::random() % total; int i = 0; for( int n = tracks.count(); i < n && random >= 0; ++i ) random -= weights.at( i ); item = tracks.at( i-1 ); } } } else if( item ) { item = MyIt::nextVisible( item ); while( item && ( !checkFileStatus( item ) || !item->exists() ) ) item = MyIt::nextVisible( item ); } else { item = *MyIt( this ); //ie. first visible item while( item && ( !checkFileStatus( item ) || !item->exists() ) ) item = item->nextSibling(); } if ( dynamicMode() && item != firstChild() ) { if( currentTrack() ) currentTrack()->setDynamicEnabled( false ); advanceDynamicTrack(); } if ( !item && Amarok::repeatPlaylist() ) item = *MyIt( this ); //ie. first visible item } if ( EngineController::engine()->loaded() ) activate( item ); else setCurrentTrack( item ); } //This is called before setCurrentItem( item ); void Playlist::advanceDynamicTrack() { int x = currentTrackIndex(); bool didDelete = false; if( dynamicMode()->cycleTracks() ) { if( x >= dynamicMode()->previousCount() ) { PlaylistItem *first = firstChild(); removeItem( first ); delete first; didDelete = true; } } const int upcomingTracks = childCount() - x - 1; // Just starting to play from stopped, don't append something needlessely // or, we have more than enough items in the queue. bool dontAppend = ( !didDelete && ( EngineController::instance()->engine()->state() == Engine::Empty ) ) || upcomingTracks > dynamicMode()->upcomingCount(); //keep upcomingTracks requirement, this seems to break StopAfterCurrent if( !dontAppend && stopAfterMode() != StopAfterCurrent ) { s_dynamicADTMutex->lock(); m_insertFromADT++; s_dynamicADTMutex->unlock(); addDynamicModeTracks( 1 ); } m_dynamicDirt = true; } void Playlist::playPrevTrack() { PlaylistItem *item = currentTrack(); if( Amarok::entireAlbums() ) { item = 0; if( m_currentTrack ) { item = m_currentTrack->prevInAlbum(); if( !item && Amarok::repeatAlbum() && m_currentTrack->m_album->tracks.count() ) item = m_currentTrack->m_album->tracks.getLast(); } if( !item ) { PlaylistAlbum* a = m_prevAlbums.last(); while( a && !a->tracks.count() ) { removeFromPreviousAlbums(); a = m_prevAlbums.last(); } if( a ) { item = a->tracks.getLast(); removeFromPreviousAlbums(); } } if( !item ) { item = *static_cast(--MyIt( item )); while( item && !checkFileStatus( item ) ) item = *static_cast(--MyIt( item )); } } else { if( dynamicMode() ) { } else if( !AmarokConfig::randomMode() || m_prevTracks.count() <= 1 ) { if( item ) { item = MyIt::prevVisible( item ); while( item && ( !checkFileStatus( item ) || !item->isEnabled() ) ) item = MyIt::prevVisible( item ); } else { item = *MyIt( this ); //ie. first visible item while( item && ( !checkFileStatus( item ) || !item->isEnabled() ) ) item = item->nextSibling(); } } else { // if enough songs in buffer, jump to the previous one m_prevTracks.last(); removeFromPreviousTracks(); //remove the track playing now item = m_prevTracks.last(); // we need to remove this item now, since it will be added in activate() again removeFromPreviousTracks(); } } if ( !item && Amarok::repeatPlaylist() ) item = *MyIt( lastItem() ); //TODO check this works! if ( EngineController::engine()->loaded() ) activate( item ); else setCurrentTrack( item ); } void Playlist::playCurrentTrack() { if ( !currentTrack() ) playNextTrack( Amarok::repeatTrack() ); //we must do this even if the above is correct //since the engine is not loaded the first time the user presses play //then calling the next() function wont play it activate( currentTrack() ); } void Playlist::setSelectedRatings( int rating ) { if( !m_selCount && currentItem() && currentItem()->isVisible() ) CollectionDB::instance()->setSongRating( currentItem()->url().path(), rating, true ); else for( MyIt it( this, MyIt::Selected ); *it; ++it ) CollectionDB::instance()->setSongRating( (*it)->url().path(), rating, true ); } void Playlist::queueSelected() { PLItemList in, out; TQPtrList dynamicList; for( MyIt it( this, MyIt::Selected ); *it; ++it ) { // Dequeuing selection with dynamic doesn't work due to the moving of the track after the last queued if( dynamicMode() ) { ( !m_nextTracks.containsRef( *it ) ? in : out ).append( *it ); dynamicList.append( *it ); } else { queue( *it, true ); ( m_nextTracks.containsRef( *it ) ? in : out ).append( *it ); } } if( dynamicMode() ) { TQListViewItem *item = dynamicList.first(); if( m_nextTracks.containsRef( static_cast(item) ) ) { for( item = dynamicList.last(); item; item = dynamicList.prev() ) queue( item, true ); } else { for( ; item; item = dynamicList.next() ) queue( item, true ); } } emit queueChanged( in, out ); } void Playlist::queue( TQListViewItem *item, bool multi, bool invertQueue ) { #define item static_cast(item) const int queueIndex = m_nextTracks.findRef( item ); const bool isQueued = queueIndex != -1; if( isQueued ) { if( invertQueue ) { //remove the item, this is better way than remove( item ) m_nextTracks.remove( queueIndex ); //sets current() to next item if( dynamicMode() ) // we move the item after the last queued item to preserve the ordered 'queue'. { PlaylistItem *after = m_nextTracks.last(); if( after ) moveItem( item, 0, after ); } } } else if( !dynamicMode() ) m_nextTracks.append( item ); else // Dynamic mode { PlaylistItem *after; m_nextTracks.isEmpty() ? after = m_currentTrack : after = m_nextTracks.last(); if( !after ) { after = firstChild(); while( after && !after->isDynamicEnabled() ) { if( after->nextSibling()->isDynamicEnabled() ) break; after = after->nextSibling(); } } if( item->isDynamicEnabled() && item != m_currentTrack ) { this->moveItem( item, 0, after ); m_nextTracks.append( item ); } else { /// we do the actual queuing through customEvent, since insertMedia is threaded m_queueDirt = true; insertMediaInternal( item->url(), after ); } } if( !multi ) { if( isQueued ) //no longer { if( invertQueue ) emit queueChanged( PLItemList(), PLItemList( item ) ); } else emit queueChanged( PLItemList( item ), PLItemList() ); } #undef item } void Playlist::sortQueuedItems() // used by dynamic mode { PlaylistItem *last = m_currentTrack; for( PlaylistItem *item = m_nextTracks.first(); item; item = m_nextTracks.next() ) { if( item->itemAbove() != last ) item->moveItem( last ); last = item; } } void Playlist::setStopAfterCurrent( bool on ) { PlaylistItem *prev_stopafter = m_stopAfterTrack; if( on ) { setStopAfterItem( m_currentTrack ); } else { setStopAfterMode( DoNotStop ); } if( m_stopAfterTrack ) m_stopAfterTrack->update(); if( prev_stopafter ) prev_stopafter->update(); } void Playlist::setStopAfterItem( PlaylistItem *item ) { if( !item ) { setStopAfterMode( DoNotStop ); return; } else if( item == m_currentTrack ) setStopAfterMode( StopAfterCurrent ); else if( item == m_nextTracks.getLast() ) setStopAfterMode( StopAfterQueue ); else setStopAfterMode( StopAfterQueue ); m_stopAfterTrack = item; } void Playlist::toggleStopAfterCurrentItem() { PlaylistItem *item = currentItem(); if( !item && m_selCount == 1 ) item = *MyIt( this, MyIt::Visible | MyIt::Selected ); if( !item ) return; PlaylistItem *prev_stopafter = m_stopAfterTrack; if( m_stopAfterTrack == item ) { m_stopAfterTrack = 0; setStopAfterMode( DoNotStop ); } else { setStopAfterItem( item ); item->setSelected( false ); item->update(); } if( prev_stopafter ) prev_stopafter->update(); } void Playlist::toggleStopAfterCurrentTrack() { PlaylistItem *item = currentTrack(); if( !item ) return; PlaylistItem *prev_stopafter = m_stopAfterTrack; if( m_stopAfterTrack == item ) { setStopAfterMode( DoNotStop ); Amarok::OSD::instance()->OSDWidget::show( i18n("Stop Playing After Track: Off") ); } else { setStopAfterItem( item ); item->setSelected( false ); item->update(); Amarok::OSD::instance()->OSDWidget::show( i18n("Stop Playing After Track: On") ); } if( prev_stopafter ) prev_stopafter->update(); } void Playlist::setStopAfterMode( int mode ) { PlaylistItem *prevStopAfter = m_stopAfterTrack; m_stopAfterMode = mode; switch( mode ) { case DoNotStop: m_stopAfterTrack = 0; break; case StopAfterCurrent: m_stopAfterTrack = m_currentTrack; break; case StopAfterQueue: m_stopAfterTrack = m_nextTracks.count() ? m_nextTracks.getLast() : m_currentTrack; break; } if( prevStopAfter ) prevStopAfter->update(); if( m_stopAfterTrack ) m_stopAfterTrack->update(); } int Playlist::stopAfterMode() { if ( m_stopAfterMode != DoNotStop && m_stopAfterTrack && m_stopAfterTrack == m_currentTrack ) { m_stopAfterMode = StopAfterCurrent; } return m_stopAfterMode; } void Playlist::generateInfo() { m_albums.clear(); if( Amarok::entireAlbums() ) for( MyIt it( this, MyIt::All ); *it; ++it ) (*it)->refAlbum(); m_total = 0; if( Amarok::entireAlbums() || AmarokConfig::favorTracks() ) for( MyIt it( this, MyIt::Visible ); *it; ++it ) (*it)->incrementTotals(); } void Playlist::doubleClicked( TQListViewItem *item ) { /* We have to check if the item exists before calling activate, otherwise clicking on an empty playlist space would stop playing (check BR #105106)*/ if( item && m_hoveredRating != item ) activate( item ); } void Playlist::slotCountChanged() { if( m_itemCountDirty ) emit itemCountChanged( totalTrackCount(), m_totalLength, m_visCount, m_visLength, m_selCount, m_selLength ); m_itemCountDirty = false; } bool Playlist::checkFileStatus( PlaylistItem * item ) { //DEBUG_BLOCK //debug() << "uniqueid of item = " << item->uniqueId() << ", url = " << item->url().path() << endl; if( !item->checkExists() ) { //debug() << "not found, finding new url" << endl; TQString path = TQString(); if( !item->uniqueId().isEmpty() ) { path = CollectionDB::instance()->urlFromUniqueId( item->uniqueId() ); //debug() << "found path = " << path << endl; } else { //debug() << "Setting uniqueid of item and trying again" << endl; item->setUniqueId(); if( !item->uniqueId().isEmpty() ) path = CollectionDB::instance()->urlFromUniqueId( item->uniqueId() ); } if( !path.isEmpty() ) { item->setUrl( KURL( path ) ); if( item->checkExists() ) item->setFilestatusEnabled( true ); else item->setFilestatusEnabled( false ); } else item->setFilestatusEnabled( false ); } else if( !item->isFilestatusEnabled() ) item->setFilestatusEnabled( true ); bool returnValue = item->isFilestatusEnabled(); return returnValue; } void Playlist::activate( TQListViewItem *item ) { ///item will be played if possible, the playback may be delayed ///so we start the glow anyway and hope //All internal requests for playback should come via //this function please! if( !item ) { //we have reached the end of the playlist EngineController::instance()->stop(); setCurrentTrack( 0 ); Amarok::OSD::instance()->OSDWidget::show( i18n("Playlist finished"), TQImage( KIconLoader().iconPath( "amarok", -KIcon::SizeHuge ) ) ); return; } #define item static_cast(item) if ( !checkFileStatus( item ) ) { Amarok::StatusBar::instance()->shortMessage( i18n("Local file does not exist.") ); return; } if( dynamicMode() && !Amarok::repeatTrack() ) { if( m_currentTrack && item->isDynamicEnabled() ) { if( item != m_currentTrack ) this->moveItem( item, 0, m_currentTrack ); } else { MyIt it( this, MyIt::Visible ); bool hasHistory = false; if ( *it && !(*it)->isDynamicEnabled() ) { hasHistory = true; for( ; *it && !(*it)->isDynamicEnabled() ; ++it ); } if( item->isDynamicEnabled() ) { hasHistory ? this->moveItem( item, 0, *it ) : this->moveItem( item, 0, 0 ); } else // !item->isDynamicEnabled() { hasHistory ? insertMediaInternal( item->url(), *it ): insertMediaInternal( item->url(), 0 ); m_dynamicDirt = true; return; } } if( !m_dynamicDirt && m_currentTrack && m_currentTrack != item ) { m_currentTrack->setDynamicEnabled( false ); advanceDynamicTrack(); } } if( Amarok::entireAlbums() ) { if( !item->nextInAlbum() ) appendToPreviousAlbums( item->m_album ); } else appendToPreviousTracks( item ); //if we are playing something from the next tracks //list, remove it from the list if( m_nextTracks.removeRef( item ) ) emit queueChanged( PLItemList(), PLItemList( item ) ); //looks bad painting selected and glowing //only do when user explicitly activates an item though item->setSelected( false ); setCurrentTrack( item ); m_dynamicDirt = false; //use PlaylistItem::MetaBundle as it also updates the audioProps EngineController::instance()->play( *item ); #undef item } TQPair Playlist::toolTipText( TQWidget*, const TQPoint &pos ) const { PlaylistItem *item = static_cast( itemAt( pos ) ); if( !item ) return TQPair( TQString(), TQRect() ); const TQPoint contentsPos = viewportToContents( pos ); const int col = header()->sectionAt( contentsPos.x() ); if( item == m_renameItem && col == m_renameColumn ) return TQPair( TQString(), TQRect() ); TQString text; if( col == PlaylistItem::Rating ) text = item->ratingDescription( item->rating() ); else text = item->text( col ); TQRect irect = itemRect( item ); const int headerPos = header()->sectionPos( col ); irect.setLeft( headerPos - 1 ); irect.setRight( headerPos + header()->sectionSize( col ) ); static TQFont f; static int minbearing = 1337 + 666; //can be 0 or negative, 2003 is less likely if( minbearing == 2003 || f != font() ) { f = font(); //getting your bearings can be expensive, so we cache them minbearing = fontMetrics().minLeftBearing() + fontMetrics().minRightBearing(); } int itemWidth = irect.width() - itemMargin() * 2 + minbearing - 2; if( item->pixmap( col ) ) itemWidth -= item->pixmap( col )->width(); if( item == m_currentTrack ) { if( col == m_firstColumn ) itemWidth -= 12; if( col == mapToLogicalColumn( numVisibleColumns() - 1 ) ) itemWidth -= 12; } if( col != PlaylistItem::Rating && fontMetrics().width( text ) <= itemWidth ) return TQPair( TQString(), TQRect() ); TQRect globalRect( viewport()->mapToGlobal( irect.topLeft() ), irect.size() ); TQSimpleRichText t( text, font() ); int dright = TQApplication::desktop()->screenGeometry( qscrollview() ).topRight().x(); t.setWidth( dright - globalRect.left() ); if( col == PlaylistItem::Rating ) globalRect.setRight( kMin( dright, kMax( globalRect.left() + t.widthUsed(), globalRect.left() + ( StarManager::instance()->getGreyStar()->width() + 1 ) * ( ( item->rating() + 1 ) / 2 ) ) ) ); else globalRect.setRight( kMin( globalRect.left() + t.widthUsed(), dright ) ); globalRect.setBottom( globalRect.top() + kMax( irect.height(), t.height() ) - 1 ); if( ( col == PlaylistItem::Rating && PlaylistItem::ratingAtPoint( contentsPos.x() ) <= item->rating() + 1 ) || ( col != PlaylistItem::Rating ) ) { text = text.replace( "&", "&" ).replace( "<", "<" ).replace( ">", ">" ); if( item->isCurrent() ) { text = TQString("%1").arg( text ); Amarok::ToolTip::s_hack = 1; //HACK for precise positioning } return TQPair( text, globalRect ); } return TQPair( TQString(), TQRect() ); } void Playlist::activateByIndex( int index ) { TQListViewItem* item = itemAtIndex( index ); if ( item ) activate(item); } void Playlist::setCurrentTrack( PlaylistItem *item ) { ///mark item as the current track and make it glow PlaylistItem *prev = m_currentTrack; //FIXME best method would be to observe usage, especially don't shift if mouse is moving nearby if( item && ( !prev || prev == currentItem() ) && !renameLineEdit()->isVisible() && m_selCount < 2 ) { if( !prev ) //if nothing is current and then playback starts, we must show the currentTrack ensureItemCentered( item ); //handles 0 gracefully else { const int prevY = itemPos( prev ); const int prevH = prev->height(); // check if the previous track is visible if( prevY <= contentsY() + visibleHeight() && prevY + prevH >= contentsY() ) { // in random mode always jump, if previous track is visible if( AmarokConfig::randomMode() ) ensureItemCentered( item ); else if( prev && prev == currentItem() ) setCurrentItem( item ); //FIXME would be better to just never be annoying // so if the user caused the track change, always show the new track // but if it is automatic be careful // if old item in view then try to keep the new one near the middle const int y = itemPos( item ); const int h = item->height(); const int vh = visibleHeight(); const int amount = h * 3; int d = y - contentsY(); if( d > 0 ) { d += h; d -= vh; if( d > 0 && d <= amount ) // scroll down setContentsPos( contentsX(), y - vh + amount ); } else if( d >= -amount ) // scroll up setContentsPos( contentsX(), y - amount ); } } } m_currentTrack = item; if ( m_currentTrack ) m_currentTrack->setIsNew(false); if ( prev ) { //reset to normal height prev->invalidateHeight(); prev->setup(); //remove pixmap in first column prev->setPixmap( m_firstColumn, TQPixmap() ); } updateNextPrev(); setCurrentTrackPixmap(); Glow::reset(); slotGlowTimer(); } int Playlist::currentTrackIndex( bool onlyCountVisible ) { int index = 0; for( MyIt it( this, onlyCountVisible ? MyIt::Visible : MyIt::All ); *it; ++it ) { if ( *it == m_currentTrack ) return index; ++index; } return -1; } int Playlist::totalTrackCount() const { return m_totalCount; } BundleList Playlist::nextTracks() const { BundleList list; for( TQPtrListIterator it( m_nextTracks ); *it; ++it ) list << (**it); return list; } uint Playlist::repeatAlbumTrackCount() const { if ( m_currentTrack && m_currentTrack->m_album ) return m_currentTrack->m_album->tracks.count(); else return 0; } const DynamicMode* Playlist::dynamicMode() const { return m_dynamicMode; } DynamicMode* Playlist::modifyDynamicMode() { DynamicMode *m = m_dynamicMode; if( !m ) return 0; m_dynamicMode = new DynamicMode( *m ); return m; } void Playlist::finishedModifying( DynamicMode *mode ) { DynamicMode *m = m_dynamicMode; setDynamicMode( mode ); delete m; } void Playlist::setCurrentTrackPixmap( int state ) { if( !m_currentTrack ) return; TQString pixmap = TQString(); if( state < 0 ) state = EngineController::engine()->state(); if( state == Engine::Paused ) pixmap = "currenttrack_pause"; else if( state == Engine::Playing ) pixmap = "currenttrack_play"; m_currentTrack->setPixmap( m_firstColumn, pixmap.isNull() ? TQPixmap() : Amarok::getPNG( pixmap ) ); PlaylistItem::setPixmapChanged(); } PlaylistItem* Playlist::restoreCurrentTrack() { ///It is always possible that the current track has been lost ///eg it was removed and then reinserted, here we check const KURL url = EngineController::instance()->playingURL(); if ( !(m_currentTrack && ( m_currentTrack->url() == url || !m_currentTrack->url().isEmpty() && url.isEmpty() ) ) ) { PlaylistItem* item; for( item = firstChild(); item && item->url() != url; item = item->nextSibling() ) {} setCurrentTrack( item ); //set even if NULL } if( m_currentTrack && EngineController::instance()->engine()->state() == Engine::Playing && !Glow::timer.isActive() ) Glow::startTimer(); return m_currentTrack; } void Playlist::countChanged() { if( !m_itemCountDirty ) { m_itemCountDirty = true; TQTimer::singleShot( 0, this, TQT_SLOT( slotCountChanged() ) ); } } bool Playlist::isTrackAfter() const { ///Is there a track after the current track? //order is carefully crafted, remember count() is O(n) //TODO randomMode will end if everything is in prevTracks return !currentTrack() && !isEmpty() || !m_nextTracks.isEmpty() || currentTrack() && currentTrack()->itemBelow() || totalTrackCount() > 1 && ( AmarokConfig::randomMode() || Amarok::repeatPlaylist() || Amarok::repeatAlbum() && repeatAlbumTrackCount() > 1 ); } bool Playlist::isTrackBefore() const { //order is carefully crafted, remember count() is O(n) return !isEmpty() && ( currentTrack() && (currentTrack()->itemAbove() || Amarok::repeatPlaylist() && totalTrackCount() > 1) || AmarokConfig::randomMode() && totalTrackCount() > 1 ); } void Playlist::updateNextPrev() { Amarok::actionCollection()->action( "play" )->setEnabled( !isEmpty() ); Amarok::actionCollection()->action( "prev" )->setEnabled( isTrackBefore() ); Amarok::actionCollection()->action( "next" )->setEnabled( isTrackAfter() ); Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !isEmpty() ); Amarok::actionCollection()->action( "playlist_show" )->setEnabled( m_currentTrack ); if( m_currentTrack ) // ensure currentTrack is shown at correct height m_currentTrack->setup(); } //////////////////////////////////////////////////////////////////////////////// /// EngineObserver Reimplementation //////////////////////////////////////////////////////////////////////////////// void Playlist::engineNewMetaData( const MetaBundle &bundle, bool trackChanged ) { if ( !bundle.podcastBundle() ) { if ( m_currentTrack && !trackChanged ) { //if the track hasn't changed then this is a meta-data update if( stopAfterMode() == StopAfterCurrent || !m_nextTracks.isEmpty() ) Playlist::instance()->playNextTrack( true ); //this is a hack, I repeat a hack! FIXME FIXME //we do it because often the stream title is from the pls file and is informative //we don't want to lose it when we get the meta data else if ( m_currentTrack->artist().isEmpty() ) { TQString comment = m_currentTrack->title(); m_currentTrack->copyFrom( bundle ); m_currentTrack->setComment( comment ); } else m_currentTrack->copyFrom( bundle ); } else //ensure the currentTrack is set correctly and highlight it restoreCurrentTrack(); } else //ensure the currentTrack is set correctly and highlight it restoreCurrentTrack(); if( m_currentTrack ) m_currentTrack->filter( m_filter ); } void Playlist::engineStateChanged( Engine::State state, Engine::State /*oldState*/ ) { switch( state ) { case Engine::Playing: Amarok::actionCollection()->action( "pause" )->setEnabled( true ); Amarok::actionCollection()->action( "stop" )->setEnabled( true ); Glow::startTimer(); break; case Engine::Paused: Amarok::actionCollection()->action( "pause" )->setEnabled( false ); Amarok::actionCollection()->action( "stop" )->setEnabled( true ); Glow::reset(); if( m_currentTrack ) slotGlowTimer(); //update glow state break; case Engine::Empty: Amarok::actionCollection()->action( "pause" )->setEnabled( false ); Amarok::actionCollection()->action( "stop" )->setEnabled( false ); //leave the glow state at full colour Glow::reset(); if ( m_currentTrack ) { //remove pixmap in all columns TQPixmap null; for( int i = 0; i < header()->count(); i++ ) m_currentTrack->setPixmap( i, null ); PlaylistItem::setPixmapChanged(); //reset glow state slotGlowTimer(); } case Engine::Idle: slotGlowTimer(); break; } //POSSIBLYAHACK //apparently you can't rely on EngineController::engine()->state() == state here, so pass it explicitly setCurrentTrackPixmap( state ); } //////////////////////////////////////////////////////////////////////////////// /// KListView Reimplementation //////////////////////////////////////////////////////////////////////////////// void Playlist::appendMedia( const TQString &path ) { appendMedia( KURL::fromPathOrURL( path ) ); } void Playlist::appendMedia( const KURL &url ) { insertMedia( KURL::List( url ) ); } void Playlist::clear() //SLOT { if( isLocked() || renameLineEdit()->isVisible() ) return; disableDynamicMode(); emit aboutToClear(); //will saveUndoState() setCurrentTrack( 0 ); m_prevTracks.clear(); m_prevAlbums.clear(); if (m_stopAfterTrack) { m_stopAfterTrack = 0; if ( stopAfterMode() != StopAfterCurrent ) { setStopAfterMode( DoNotStop ); } } const PLItemList prev = m_nextTracks; m_nextTracks.clear(); emit queueChanged( PLItemList(), prev ); // Update player button states Amarok::actionCollection()->action( "play" )->setEnabled( false ); Amarok::actionCollection()->action( "prev" )->setEnabled( false ); Amarok::actionCollection()->action( "next" )->setEnabled( false ); Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( false ); ThreadManager::instance()->abortAllJobsNamed( "TagWriter" ); // something to bear in mind, if there is any event in the loop // that depends on a PlaylistItem, we are about to crash Amarok // never unlock() the Playlist until it is safe! safeClear(); m_total = 0; m_albums.clear(); setPlaylistName( i18n( "Untitled" ) ); ScriptManager::instance()->notifyPlaylistChange("cleared"); } /** * Workaround for TQt 3.3.5 bug in TQListView::clear() * @see http://lists.kde.org/?l=kde-devel&m=113113845120155&w=2 * @see BUG 116004 */ void Playlist::safeClear() { /* 3.3.5 and 3.3.6 have bad KListView::clear() functions. 3.3.5 forgets to clear the pointer to the highlighted item. 3.3.6 forgets to clear the pointer to the last dragged item */ if ( strcmp( qVersion(), "3.3.5" ) == 0 || strcmp( qVersion(), "3.3.6" ) == 0 ) { bool block = signalsBlocked(); blockSignals( true ); clearSelection(); TQListViewItem *c = firstChild(); TQListViewItem *n; while( c ) { n = c->nextSibling(); if ( !static_cast( c )->isEmpty() ) //avoid deleting markers delete c; c = n; } blockSignals( block ); triggerUpdate(); } else KListView::clear(); } void Playlist::setSorting( int col, bool b ) { saveUndoState(); //HACK There are reasons to allow sorting in dynamic mode, but //it breaks other things that I don't have the time or patience //to figure out...at least right now if( !dynamicMode() ) KListView::setSorting( col, b ); } void Playlist::setColumnWidth( int col, int width ) { KListView::setColumnWidth( col, width ); //FIXME this is because TQt doesn't by default disable resizing width 0 columns. GRRR! //NOTE default column sizes are stored in default amarokrc so that restoreLayout() in ctor will // call this function. This is necessary because addColumn() doesn't call setColumnWidth() GRRR! header()->setResizeEnabled( width != 0, col ); } void Playlist::rename( TQListViewItem *item, int column ) //SLOT { if( !item ) return; switch( column ) { case PlaylistItem::Artist: renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->artistList() ); break; case PlaylistItem::Album: renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->albumList() ); break; case PlaylistItem::Genre: renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->genreList() ); break; case PlaylistItem::Composer: renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->composerList() ); break; default: renameLineEdit()->completionObject()->clear(); break; } renameLineEdit()->completionObject()->setCompletionMode( KGlobalSettings::CompletionPopupAuto ); renameLineEdit()->completionObject()->setIgnoreCase( true ); m_editOldTag = static_cast(item)->exactText( column ); if( m_selCount <= 1 ) { if( currentItem() ) currentItem()->setSelected( false ); item->setSelected( true ); } setCurrentItem( item ); KListView::rename( item, column ); m_renameItem = item; m_renameColumn = column; static_cast(item)->setIsBeingRenamed( true ); } void Playlist::writeTag( TQListViewItem *qitem, const TQString &, int column ) //SLOT { const bool dynamicEnabled = static_cast(qitem)->isDynamicEnabled(); if( m_itemsToChangeTagsFor.isEmpty() ) m_itemsToChangeTagsFor.append( static_cast( qitem ) ); const TQString newTag = static_cast( qitem )->exactText( column ); for( PlaylistItem *item = m_itemsToChangeTagsFor.first(); item; item = m_itemsToChangeTagsFor.next() ) { if( !checkFileStatus( item ) ) continue; const TQString oldTag = item == qitem ? m_editOldTag : item->exactText(column); if( column == PlaylistItem::Score ) CollectionDB::instance()->setSongPercentage( item->url().path(), newTag.toInt() ); else if( column == PlaylistItem::Rating ) CollectionDB::instance()->setSongRating( item->url().path(), newTag.toInt() ); else if (oldTag != newTag) ThreadManager::instance()->queueJob( new TagWriter( item, oldTag, newTag, column ) ); else if( item->deleteAfterEditing() ) { removeItem( item ); delete item; } } if( dynamicMode() ) static_cast(qitem)->setDynamicEnabled( dynamicEnabled ); m_itemsToChangeTagsFor.clear(); m_editOldTag = TQString(); } void Playlist::columnOrderChanged() //SLOT { const uint prevColumn = m_firstColumn; //determine first visible column for ( m_firstColumn = 0; m_firstColumn < header()->count(); m_firstColumn++ ) if ( header()->sectionSize( header()->mapToSection( m_firstColumn ) ) ) break; //convert to logical column m_firstColumn = header()->mapToSection( m_firstColumn ); //force redraw of currentTrack if( m_currentTrack ) { m_currentTrack->setPixmap( prevColumn, TQPixmap() ); setCurrentTrackPixmap(); } TQResizeEvent e( size(), TQSize() ); viewportResizeEvent( &e ); emit columnsChanged(); } void Playlist::paletteChange( const TQPalette &p ) { using namespace Glow; TQColor fg; TQColor bg; { using namespace Base; const uint steps = STEPS+5+5; //so we don't fade all the way to base, and all the way up to highlight either fg = colorGroup().highlight(); bg = colorGroup().base(); dr = double(bg.red() - fg.red()) / steps; dg = double(bg.green() - fg.green()) / steps; db = double(bg.blue() - fg.blue()) / steps; r = fg.red() + int(dr*5.0); //we add 5 steps so the default colour is slightly different to highlight g = fg.green() + int(dg*5.0); b = fg.blue() + int(db*5.0); } { using namespace Text; const uint steps = STEPS + 5; //so we don't fade all the way to base fg = colorGroup().highlightedText(); bg = colorGroup().text(); dr = double(bg.red() - fg.red()) / steps; dg = double(bg.green() - fg.green()) / steps; db = double(bg.blue() - fg.blue()) / steps; r = fg.red(); g = fg.green(); b = fg.blue(); } KListView::paletteChange( p ); counter = 0; // reset the counter or apparently the text lacks contrast slotGlowTimer(); // repaint currentTrack marker } void Playlist::contentsDragEnterEvent( TQDragEnterEvent *e ) { TQString data; TQCString subtype; TQTextDrag::decode( e, data, subtype ); e->accept( e->source() == viewport() || subtype == "amarok-sql" || subtype == "uri-list" || //this is to prevent DelayedUrlLists from performing their queries KURLDrag::canDecode( e ) ); } void Playlist::contentsDragMoveEvent( TQDragMoveEvent* e ) { if( !e->isAccepted() ) return; #if KDE_IS_VERSION( 3, 3, 91 ) const bool ctrlPressed = KApplication::keyboardMouseState() & TQt::ControlButton; #else const bool ctrlPressed = KApplication::keyboardModifiers() & ControlMask; #endif //Get the closest item _before_ the cursor const TQPoint p = contentsToViewport( e->pos() ); TQListViewItem *item = itemAt( p ); if( !item || ctrlPressed ) item = lastItem(); else if( p.y() - itemRect( item ).top() < (item->height()/2) ) item = item->itemAbove(); if( item != m_marker ) { //NOTE this if block prevents flicker slotEraseMarker(); m_marker = item; //NOTE this is the correct place to set m_marker viewportPaintEvent( 0 ); } } void Playlist::contentsDragLeaveEvent( TQDragLeaveEvent* ) { slotEraseMarker(); } void Playlist::contentsDropEvent( TQDropEvent *e ) { DEBUG_BLOCK //NOTE parent is always 0 currently, but we support it in case we start using trees TQListViewItem *parent = 0; TQListViewItem *after = m_marker; //make sure to disable only if in dynamic mode and you're inserting //at the beginning or in the middle of the disabled tracks //Also, that the dynamic playlist has any tracks (suggested may not) if( dynamicMode() && Playlist::instance()->firstChild() && ( !m_marker || !( static_cast(m_marker)->isDynamicEnabled() ) ) && currentTrackIndex() != -1 ) { // If marker is disabled, and there is a current track, or marker is not the last enabled track // don't allow inserting if( ( m_marker && ( m_currentTrack || ( m_marker->itemBelow() && !( static_cast(m_marker->itemBelow())->isDynamicEnabled() ) ) ) ) || ( !m_marker ) ) { slotEraseMarker(); return; } } if( !after ) findDrop( e->pos(), parent, after ); //shouldn't happen, but you never know! slotEraseMarker(); if ( e->source() == viewport() ) { setSorting( NO_SORT ); //disableSorting and saveState() movableDropEvent( parent, after ); TQPtrList items = selectedItems(); if( dynamicMode() && after ) { TQListViewItem *item; bool enabled = static_cast(after)->isDynamicEnabled(); for( item = items.first(); item; item = items.next() ) static_cast(item)->setDynamicEnabled( enabled ); } ScriptManager::instance()->notifyPlaylistChange("reordered"); } else { TQString data; TQCString subtype; TQTextDrag::decode( e, data, subtype ); debug() << "TQTextDrag::subtype(): " << subtype << endl; if( subtype == "amarok-sql" ) { setSorting( NO_SORT ); TQString query = data.section( "\n", 1 ); ThreadManager::instance()->queueJob( new SqlLoader( query, after ) ); ScriptManager::instance()->notifyPlaylistChange("changed"); } else if( subtype == "dynamic" ) { // Deserialize pointer DynamicEntry* entry = reinterpret_cast( data.toULongLong() ); loadDynamicMode( entry ); } else if( KURLDrag::canDecode( e ) ) { debug() << "KURLDrag::canDecode" << endl; KURL::List list; KURLDrag::decode( e, list ); insertMediaInternal( list, static_cast( after ) ); } else e->ignore(); } updateNextPrev(); } TQDragObject* Playlist::dragObject() { DEBUG_THREAD_FUNC_INFO KURL::List list; for( MyIt it( this, MyIt::Selected ); *it; ++it ) { const PlaylistItem *item = static_cast( *it ); const KURL url = item->url(); list += url; } KURLDrag *drag = new KURLDrag( list, viewport() ); drag->setPixmap( CollectionDB::createDragPixmap( list ), TQPoint( CollectionDB::DRAGPIXMAP_OFFSET_X, CollectionDB::DRAGPIXMAP_OFFSET_Y ) ); return drag; } #include void Playlist::viewportPaintEvent( TQPaintEvent *e ) { if( e ) KListView::viewportPaintEvent( e ); //we call with 0 in contentsDropEvent() if ( m_marker ) { TQPainter p( viewport() ); p.fillRect( drawDropVisualizer( 0, 0, m_marker ), TQBrush( colorGroup().highlight().dark(), TQBrush::Dense4Pattern ) ); } else if( m_showHelp && isEmpty() ) { TQPainter p( viewport() ); TQString minimumText(i18n( "
" "

The Playlist

" "This is the playlist. " "To create a listing, " "drag tracks from the browser-panels on the left, " "drop them here and then double-click them to start playback." "
" ) ); TQSimpleRichText *t = new TQSimpleRichText( minimumText + i18n( "
" "

The Browsers

" "The browsers are the source of all your music. " "The collection-browser holds your collection. " "The playlist-browser holds your pre-set playlistings. " "The file-browser shows a file-selector which you can use to access any music on your computer. " "
" ), TQApplication::font() ); if ( t->width()+30 >= viewport()->width() || t->height()+30 >= viewport()->height() ) { // too big for the window, so let's cut part of the text delete t; t = new TQSimpleRichText( minimumText, TQApplication::font()); if ( t->width()+30 >= viewport()->width() || t->height()+30 >= viewport()->height() ) { //still too big, giving up delete t; return; } } const uint w = t->width(); const uint h = t->height(); const uint x = (viewport()->width() - w - 30) / 2 ; const uint y = (viewport()->height() - h - 30) / 2 ; p.setBrush( colorGroup().background() ); p.drawRoundRect( x, y, w+30, h+30, (8*200)/w, (8*200)/h ); t->draw( &p, x+15, y+15, TQRect(), colorGroup() ); delete t; } } static uint negativeWidth = 0; void Playlist::viewportResizeEvent( TQResizeEvent *e ) { if ( !m_smartResizing ) { KListView::viewportResizeEvent( e ); return; } //only be clever with the sizing if there is not many items //TODO don't allow an item to be made too small (ie less than 50% of ideal width) //makes this much quicker header()->blockSignals( true ); const double W = (double)e->size().width() - negativeWidth; for( uint c = 0; c < m_columnFraction.size(); ++c ) { switch( c ) { case PlaylistItem::Track: case PlaylistItem::Bitrate: case PlaylistItem::SampleRate: case PlaylistItem::Filesize: case PlaylistItem::Score: case PlaylistItem::Rating: case PlaylistItem::Type: case PlaylistItem::PlayCount: case PlaylistItem::Length: case PlaylistItem::Year: case PlaylistItem::DiscNumber: case PlaylistItem::Bpm: break; //these columns retain their width - their items tend to have uniform size default: if( m_columnFraction[c] > 0 ) setColumnWidth( c, int(W * m_columnFraction[c]) ); } } header()->blockSignals( false ); //ensure that the listview scrollbars are updated etc. triggerUpdate(); } void Playlist::columnResizeEvent( int col, int oldw, int neww ) { if ( !m_smartResizing ) return; //prevent recursion header()->blockSignals( true ); //qlistview is stupid sometimes if ( neww < 0 ) setColumnWidth( col, 0 ); if ( neww == 0 ) { //the column in question has been hidden //we need to adjust the other columns to fit const double W = (double)width() - negativeWidth; for( uint c = 0; c < m_columnFraction.size(); ++c ) { if( c == (uint)col ) continue; switch( c ) { case PlaylistItem::Track: case PlaylistItem::Bitrate: case PlaylistItem::SampleRate: case PlaylistItem::Filesize: case PlaylistItem::Score: case PlaylistItem::Rating: case PlaylistItem::Type: case PlaylistItem::PlayCount: case PlaylistItem::Length: case PlaylistItem::Year: case PlaylistItem::DiscNumber: case PlaylistItem::Bpm: break; default: if( m_columnFraction[c] > 0 ) setColumnWidth( c, int(W * m_columnFraction[c]) ); } } } else if( oldw != 0 ) { //adjust the size of the column on the right side of this one for( int section = col, index = header()->mapToIndex( section ); index < header()->count(); ) { section = header()->mapToSection( ++index ); if ( header()->sectionSize( section ) ) { int newSize = header()->sectionSize( section ) + oldw - neww; if ( newSize > 5 ) { setColumnWidth( section, newSize ); //we only want to adjust one column! break; } } } } header()->blockSignals( false ); negativeWidth = 0; uint w = 0; //determine width excluding the columns that have static size for( uint x = 0; x < m_columnFraction.size(); ++x ) { switch( x ) { case PlaylistItem::Track: case PlaylistItem::Bitrate: case PlaylistItem::SampleRate: case PlaylistItem::Filesize: case PlaylistItem::Score: case PlaylistItem::Rating: case PlaylistItem::Type: case PlaylistItem::PlayCount: case PlaylistItem::Length: case PlaylistItem::Year: case PlaylistItem::DiscNumber: case PlaylistItem::Bpm: break; default: w += columnWidth( x ); } negativeWidth += columnWidth( x ); } //determine the revised column fractions for( uint x = 0; x < m_columnFraction.size(); ++x ) m_columnFraction[x] = (double)columnWidth( x ) / double(w); //negative width is an important property, honest! negativeWidth -= w; //we have to do this after we have established negativeWidth and set the columnFractions if( neww == 0 || oldw == 0 ) { //then this column has been inserted or removed, we need to update all the column widths TQResizeEvent e( size(), TQSize() ); viewportResizeEvent( &e ); emit columnsChanged(); } } bool Playlist::eventFilter( TQObject *o, TQEvent *e ) { #define me TQT_TQMOUSEEVENT(e) #define ke TQT_TQKEYEVENT(e) if( TQT_BASE_OBJECT(o) == TQT_BASE_OBJECT(header()) && e->type() == TQEvent::MouseButtonPress && me->button() == Qt::RightButton ) { enum { HIDE = 1000, SELECT, CUSTOM, SMARTRESIZING }; const int mouseOverColumn = header()->sectionAt( me->pos().x() ); KPopupMenu popup; if( mouseOverColumn >= 0 ) popup.insertItem( i18n("&Hide %1").arg( columnText( mouseOverColumn ) ), HIDE ); //TODO KPopupMenu sub; for( int i = 0; i < columns(); ++i ) //columns() references a property if( !columnWidth( i ) ) sub.insertItem( columnText( i ), i, i + 1 ); sub.setItemVisible( PlaylistItem::Score, AmarokConfig::useScores() ); sub.setItemVisible( PlaylistItem::Rating, AmarokConfig::useRatings() ); sub.setItemVisible( PlaylistItem::Mood, AmarokConfig::showMoodbar() ); popup.insertItem( i18n("&Show Column" ), &sub ); popup.insertItem( i18n("Select &Columns..."), SELECT ); popup.insertItem( i18n("&Fit to Width"), SMARTRESIZING ); popup.setItemChecked( SMARTRESIZING, m_smartResizing ); int col = popup.exec( TQT_TQMOUSEEVENT(e)->globalPos() ); switch( col ) { case HIDE: { hideColumn( mouseOverColumn ); TQResizeEvent e( size(), TQSize() ); viewportResizeEvent( &e ); } break; case SELECT: ColumnsDialog::display(); break; case CUSTOM: addCustomColumn(); break; case SMARTRESIZING: m_smartResizing = !m_smartResizing; Amarok::config( "PlaylistWindow" )->writeEntry( "Smart Resizing", m_smartResizing ); if ( m_smartResizing ) columnResizeEvent( 0, 0, 0 ); //force refit. FIXME: It doesn't work perfectly break; default: if( col != -1 ) { adjustColumn( col ); header()->setResizeEnabled( true, col ); } } //determine first visible column again, since it has changed columnOrderChanged(); //eat event return true; } // not in slotMouseButtonPressed because we need to disable normal usage. if( TQT_BASE_OBJECT(o) == TQT_BASE_OBJECT(viewport()) && e->type() == TQEvent::MouseButtonPress && me->state() == TQt::ControlButton && me->button() == Qt::RightButton ) { PlaylistItem *item = static_cast( itemAt( me->pos() ) ); if( !item ) return true; item->isSelected() ? queueSelected(): queue( item ); return true; //yum! } // trigger in-place tag editing else if( TQT_BASE_OBJECT(o) == TQT_BASE_OBJECT(viewport()) && e->type() == TQEvent::MouseButtonPress && me->button() == Qt::LeftButton ) { m_clicktimer->stop(); m_itemToRename = 0; int col = header()->sectionAt( viewportToContents( me->pos() ).x() ); if( col != PlaylistItem::Rating ) { PlaylistItem *item = static_cast( itemAt( me->pos() ) ); bool edit = item && item->isSelected() && selectedItems().count()==1 && (me->state() & ~Qt::LeftButton) == 0 && item->url().isLocalFile(); if( edit ) { m_clickPos = me->pos(); m_itemToRename = item; m_columnToRename = col; //return true; } } } else if( TQT_BASE_OBJECT(o) == TQT_BASE_OBJECT(viewport()) && e->type() == TQEvent::MouseButtonRelease && me->button() == Qt::LeftButton ) { int col = header()->sectionAt( viewportToContents( me->pos() ).x() ); if( col != PlaylistItem::Rating ) { PlaylistItem *item = static_cast( itemAt( me->pos() ) ); if( item == m_itemToRename && me->pos() == m_clickPos ) { m_clicktimer->start( int( TQApplication::doubleClickInterval() ), true ); return true; } else { m_itemToRename = 0; } } } // avoid in-place tag editing upon double-clicks else if( e->type() == TQEvent::MouseButtonDblClick && me->button() == Qt::LeftButton ) { m_itemToRename = 0; m_clicktimer->stop(); } // Toggle play/pause if user middle-clicks on current track else if( TQT_BASE_OBJECT(o) == TQT_BASE_OBJECT(viewport()) && e->type() == TQEvent::MouseButtonPress && me->button() == Qt::MidButton ) { PlaylistItem *item = static_cast( itemAt( me->pos() ) ); if( item && item == m_currentTrack ) { EngineController::instance()->playPause(); return true; //yum! } } else if( TQT_BASE_OBJECT(o) == TQT_BASE_OBJECT(renameLineEdit()) && e->type() == 6 /*TQEvent::KeyPress*/ && m_renameItem ) { const int visibleCols = numVisibleColumns(); int physicalColumn = visibleCols - 1; while( mapToLogicalColumn( physicalColumn ) != m_renameColumn && physicalColumn >= 0 ) physicalColumn--; if( physicalColumn < 0 ) { warning() << "the column counting code is wrong! tell illissius." << endl; return false; } int column = m_renameColumn; TQListViewItem *item = m_renameItem; if( ke->state() & TQt::AltButton ) { if( ke->key() == TQt::Key_Up && m_visCount > 1 ) if( !( item = m_renameItem->itemAbove() ) ) { item = *MyIt( this, MyIt::Visible ); while( item->itemBelow() ) item = item->itemBelow(); } if( ke->key() == TQt::Key_Down && m_visCount > 1 ) if( !( item = m_renameItem->itemBelow() ) ) item = *MyIt( this, MyIt::Visible ); if( ke->key() == TQt::Key_Left ) do { if( physicalColumn == 0 ) physicalColumn = visibleCols - 1; else physicalColumn--; column = mapToLogicalColumn( physicalColumn ); } while( !isRenameable( column ) ); if( ke->key() == TQt::Key_Right ) do { if( physicalColumn == visibleCols - 1 ) physicalColumn = 0; else physicalColumn++; column = mapToLogicalColumn( physicalColumn ); } while( !isRenameable( column ) ); } if( ke->key() == TQt::Key_Tab ) do { if( physicalColumn == visibleCols - 1 ) { if( !( item = m_renameItem->itemBelow() ) ) item = *MyIt( this, MyIt::Visible ); physicalColumn = 0; } else physicalColumn++; column = mapToLogicalColumn( physicalColumn ); } while( !isRenameable( column ) ); if( ke->key() == TQt::Key_Backtab ) do { if( physicalColumn == 0 ) { if( !( item = m_renameItem->itemAbove() ) ) { item = *MyIt( this, MyIt::Visible ); while( item->itemBelow() ) item = item->itemBelow(); } physicalColumn = visibleCols - 1; } else physicalColumn--; column = mapToLogicalColumn( physicalColumn ); } while( !isRenameable( column ) ); if( item != m_renameItem || column != m_renameColumn ) { if( !item->isSelected() ) m_itemsToChangeTagsFor.clear(); //the item that actually got changed will get added back, in writeTag() m_renameItem->setText( m_renameColumn, renameLineEdit()->text() ); doneEditing( m_renameItem, m_renameColumn ); rename( item, column ); return true; } } else if( TQT_BASE_OBJECT(o) == TQT_BASE_OBJECT(renameLineEdit()) && ( e->type() == TQEvent::Hide || e->type() == TQEvent::Close ) ) { m_renameItem = 0; } //allow the header to process this return KListView::eventFilter( o, e ); #undef me #undef ke } void Playlist::slotSingleClick() { if( m_itemToRename ) { rename( m_itemToRename, m_columnToRename ); } m_itemToRename = 0; } void Playlist::customEvent( TQCustomEvent *e ) { if( e->type() == (int)UrlLoader::JobFinishedEvent ) { refreshNextTracks( 0 ); PLItemList in, out; // Disable help if playlist is populated if ( !isEmpty() ) m_showHelp = false; if ( !m_queueList.isEmpty() ) { KURL::List::Iterator jt; for( MyIt it( this, MyIt::All ); *it; ++it ) { jt = m_queueList.find( (*it)->url() ); if ( jt != m_queueList.end() ) { queue( *it ); ( m_nextTracks.containsRef( *it ) ? in : out ).append( *it ); m_queueList.remove( jt ); } } m_queueList.clear(); } if( m_dynamicDirt ) { PlaylistItem *after = m_currentTrack; if( !after ) { after = firstChild(); while( after && !after->isDynamicEnabled() ) after = after->nextSibling(); } else after = static_cast( after->itemBelow() ); if( after ) { PlaylistItem *prev = static_cast( after->itemAbove() ); if( prev && dynamicMode() ) prev->setDynamicEnabled( false ); s_dynamicADTMutex->lock(); if( m_insertFromADT > 0 ) { if( EngineController::engine()->state() == Engine::Playing ) activate( after ); m_insertFromADT--; } else activate( after ); s_dynamicADTMutex->unlock(); if( dynamicMode() && dynamicMode()->cycleTracks() ) adjustDynamicPrevious( dynamicMode()->previousCount() ); } } if( m_queueDirt ) { PlaylistItem *after = 0; m_nextTracks.isEmpty() ? after = m_currentTrack : after = m_nextTracks.last(); if( !after ) { after = firstChild(); while( after && !after->isDynamicEnabled() ) after = after->nextSibling(); } else after = static_cast( after->itemBelow() ); if( after ) { m_nextTracks.append( after ); in.append( after ); } m_queueDirt = false; } if( !in.isEmpty() || !out.isEmpty() ) emit queueChanged( in, out ); //force redraw of currentTrack marker, play icon, etc. restoreCurrentTrack(); } updateNextPrev(); } //////////////////////////////////////////////////////////////////////////////// /// Misc Public Methods //////////////////////////////////////////////////////////////////////////////// bool Playlist::saveM3U( const TQString &path, bool relative ) const { TQValueList urls; TQValueList titles; TQValueList lengths; for( MyIt it( firstChild(), MyIt::Visible ); *it; ++it ) { urls << (*it)->url(); titles << (*it)->title(); lengths << (*it)->length(); } return PlaylistBrowser::savePlaylist( path, urls, titles, lengths, relative ); } void Playlist::saveXML( const TQString &path ) { DEBUG_BLOCK TQFile file( path ); if( !file.open( IO_WriteOnly | IO_Truncate | IO_Raw ) ) return; // Manual buffering since TQFile's is slow for whatever reason const uint kWriteSize = 256 * 1024; TQBuffer buffer; buffer.open(IO_WriteOnly); TQTextStream stream( &buffer ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); stream << "\n"; TQString dynamic; if( dynamicMode() ) { const TQString title = ( dynamicMode()->title() ).replace( "&", "&" ) .replace( "<", "<" ) .replace( ">", ">" ); dynamic = TQString(" dynamicMode=\"%1\"").arg( title ); } stream << TQString( "\n" ) .arg( "Amarok" ).arg( Amarok::xmlVersion() ).arg( dynamic ); for( MyIt it( this, MyIt::All ); *it; ++it ) { const PlaylistItem *item = *it; if( item->isEmpty() ) continue; // Skip marker items and such TQStringList attributes; const int queueIndex = m_nextTracks.findRef( item ); if ( queueIndex != -1 ) attributes << "queue_index" << TQString::number( queueIndex + 1 ); else if ( item == currentTrack() ) attributes << "queue_index" << TQString::number( 0 ); if( !item->isDynamicEnabled() ) attributes << "dynamicdisabled" << "true"; if( m_stopAfterTrack == item ) attributes << "stop_after" << "true"; item->save( stream, attributes ); if ( buffer.at() >= kWriteSize ) { file.writeBlock( buffer.buffer().data(), buffer.at() ); buffer.reset(); } } stream << "\n"; file.writeBlock(buffer.buffer().data(), buffer.at()); file.close(); } void Playlist::burnPlaylist( int projectType ) { KURL::List list; TQListViewItemIterator it( this ); for( ; it.current(); ++it ) { PlaylistItem *item = static_cast(*it); KURL url = item->url(); if( url.isLocalFile() ) list << url; } K3bExporter::instance()->exportTracks( list, projectType ); } void Playlist::burnSelectedTracks( int projectType ) { KURL::List list; TQListViewItemIterator it( this, TQListViewItemIterator::Selected ); for( ; it.current(); ++it ) { PlaylistItem *item = static_cast(*it); KURL url = item->url(); if( url.isLocalFile() ) list << url; } K3bExporter::instance()->exportTracks( list, projectType ); } void Playlist::addCustomMenuItem( const TQString &submenu, const TQString &itemTitle ) //for dcop { m_customSubmenuItem[submenu] << itemTitle; } bool Playlist::removeCustomMenuItem( const TQString &submenu, const TQString &itemTitle ) //for dcop { if( !m_customSubmenuItem.contains(submenu) ) return false; if( m_customSubmenuItem[submenu].remove( itemTitle ) != 0 ) { if( m_customSubmenuItem[submenu].count() == 0 ) m_customSubmenuItem.remove( submenu ); return true; return true; } else return false; } void Playlist::customMenuClicked(int id) //adapted from burnSelectedTracks { TQString message = m_customIdItem[id]; TQListViewItemIterator it( this, TQListViewItemIterator::Selected ); for( ; it.current(); ++it ) { PlaylistItem *item = static_cast(*it); KURL url = item->url().url(); message += ' ' + url.url(); } ScriptManager::instance()->customMenuClicked( message ); } void Playlist::setDynamicMode( DynamicMode *mode ) //SLOT { // if mode == 0, then dynamic mode was just turned off. DynamicMode* const prev = m_dynamicMode; m_dynamicMode = mode; if( mode ) AmarokConfig::setLastDynamicMode( mode->title() ); emit dynamicModeChanged( mode ); if( mode ) { m_oldRandom = AmarokConfig::randomMode(); m_oldRepeat = AmarokConfig::repeat(); } Amarok::actionCollection()->action( "random_mode" )->setEnabled( !mode ); Amarok::actionCollection()->action( "repeat" )->setEnabled( !mode ); Amarok::actionCollection()->action( "playlist_shuffle" )->setEnabled( !mode ); Amarok::actionCollection()->action( "repopulate" )->setEnabled( mode ); if( prev && mode ) { if( prev->previousCount() != mode->previousCount() ) adjustDynamicPrevious( mode->previousCount(), true ); if( prev->upcomingCount() != mode->upcomingCount() ) adjustDynamicUpcoming( true ); } else if( !prev ) { if( mode ) adjustDynamicPrevious( mode->previousCount(), true ); setDynamicHistory( true ); // disable items! } else if( !mode ) // enable items again, dynamic mode is no more setDynamicHistory( false ); } void Playlist::loadDynamicMode( DynamicMode *mode ) //SLOT { saveUndoState(); setDynamicMode( mode ); if( isEmpty() ) repopulate(); } void Playlist::editActiveDynamicMode() //SLOT { if( !m_dynamicMode ) return; DynamicMode *m = modifyDynamicMode(); ConfigDynamic::editDynamicPlaylist( PlaylistWindow::self(), m ); m->rebuildCachedItemSet(); finishedModifying( m ); } void Playlist::disableDynamicMode() //SLOT { if( !m_dynamicMode ) return; setDynamicMode( 0 ); AmarokConfig::setRandomMode( m_oldRandom ); AmarokConfig::setRepeat( m_oldRepeat ); static_cast(Amarok::actionCollection()->action( "random_mode" ))->setCurrentItem( m_oldRandom ); static_cast(Amarok::actionCollection()->action( "repeat" ))->setCurrentItem( m_oldRepeat ); } void Playlist::rebuildDynamicModeCache() //SLOT { if( !m_dynamicMode ) return; DynamicMode *m = modifyDynamicMode(); m->rebuildCachedItemSet(); finishedModifying( m ); } void Playlist::repopulate() //SLOT { if( !m_dynamicMode ) return; // Repopulate the upcoming tracks MyIt it( this, MyIt::All ); TQPtrList list; for( ; *it; ++it ) { PlaylistItem *item = static_cast(*it); int queueIndex = m_nextTracks.findRef( item ); bool isQueued = queueIndex != -1; bool isMarker = item->isEmpty(); // markers are used by playlistloader, and removing them is not good if( !item->isDynamicEnabled() || item == m_currentTrack || isQueued || isMarker ) continue; list.prepend( *it ); } saveUndoState(); //remove the items for( TQListViewItem *item = list.first(); item; item = list.next() ) { removeItem( static_cast( item ) ); delete item; } //calling advanceDynamicTrack will remove an item too, which is undesirable //block signals to avoid saveUndoState being called blockSignals( true ); addDynamicModeTracks( dynamicMode()->upcomingCount() ); blockSignals( false ); } void Playlist::shuffle() //SLOT { if( dynamicMode() ) return; TQPtrList list; setSorting( NO_SORT ); // shuffle only VISIBLE entries for( MyIt it( this ); *it; ++it ) list.append( *it ); // we do it in two steps because the iterator doesn't seem // to like it when we do takeItem and ++it in the same loop for( TQListViewItem *item = list.first(); item; item = list.next() ) takeItem( item ); //shuffle KRandomSequence( (long)KApplication::random() ).randomize( &list ); //reinsert in new order for( TQListViewItem *item = list.first(); item; item = list.next() ) insertItem( item ); updateNextPrev(); ScriptManager::instance()->notifyPlaylistChange("reordered"); } void Playlist::removeSelectedItems() //SLOT { if( isLocked() ) return; //assemble a list of what needs removing //calling removeItem() iteratively is more efficient if they are in _reverse_ order, hence the prepend() PLItemList queued, list; int dontReplaceDynamic = 0; for( PlaylistIterator it( this, MyIt::Selected ); *it; ++it ) { if( !(*it)->isDynamicEnabled() ) dontReplaceDynamic++; ( m_nextTracks.contains( *it ) ? queued : list ).prepend( *it ); } if( (int)list.count() == childCount() ) { //clear() will saveUndoState for us. clear(); // faster return; } if( list.isEmpty() && queued.isEmpty() ) return; saveUndoState(); if( dynamicMode() ) { int currentTracks = childCount(); int minTracks = dynamicMode()->upcomingCount(); if( m_currentTrack ) currentTracks -= currentTrackIndex() + 1; int difference = currentTracks - minTracks; if( difference >= 0 ) difference -= list.count(); if( difference < 0 ) { addDynamicModeTracks( -difference ); } } //remove the items if( queued.count() ) { for( TQListViewItem *item = queued.first(); item; item = queued.next() ) removeItem( static_cast( item ), true ); emit queueChanged( PLItemList(), queued ); for( TQListViewItem *item = queued.first(); item; item = queued.next() ) delete item; } for( TQListViewItem *item = list.first(); item; item = list.next() ) { removeItem( static_cast( item ) ); delete item; } updateNextPrev(); ScriptManager::instance()->notifyPlaylistChange("changed"); //NOTE no need to emit childCountChanged(), removeItem() does that for us //select next item in list setSelected( currentItem(), true ); } void Playlist::deleteSelectedFiles() //SLOT { if( isLocked() ) return; KURL::List urls; //assemble a list of what needs removing for( MyIt it( this, MyIt::Selected ); it.current(); urls << static_cast( *it )->url(), ++it ); if( DeleteDialog::showTrashDialog(this, urls) ) { CollectionDB::instance()->removeSongs( urls ); removeSelectedItems(); foreachType( KURL::List, urls ) CollectionDB::instance()->emitFileDeleted( (*it).path() ); TQTimer::singleShot( 0, CollectionView::instance(), TQT_SLOT( renderView() ) ); } } void Playlist::removeDuplicates() //SLOT { // Remove dead entries: for( TQListViewItemIterator it( this ); it.current(); ) { PlaylistItem* item = static_cast( *it ); const KURL url = item->url(); if ( url.isLocalFile() && !TQFile::exists( url.path() ) ) { removeItem( item ); ++it; delete item; } else ++it; } // Remove dupes: TQSortedList list; for( TQListViewItemIterator it( this ); it.current(); ++it ) list.prepend( static_cast( it.current() ) ); list.sort(); TQPtrListIterator it( list ); PlaylistItem *item; while( (item = it.current()) ) { const KURL &compare = item->url(); ++it; if ( *it && compare == it.current()->url() ) { removeItem( item ); delete item; } } } void Playlist::copyToClipboard( const TQListViewItem *item ) const //SLOT { if( !item ) item = currentTrack(); if( item ) { const PlaylistItem* playlistItem = static_cast( item ); TQString text = playlistItem->prettyTitle(); // For streams add the streamtitle too //TODO make prettyTitle do this if ( playlistItem->url().protocol() == "http" ) text.append( " :: " + playlistItem->url().url() ); // Copy both to clipboard and X11-selection TQApplication::clipboard()->setText( text, TQClipboard::Clipboard ); TQApplication::clipboard()->setText( text, TQClipboard::Selection ); Amarok::OSD::instance()->OSDWidget::show( i18n( "Copied: %1" ).arg( text ), TQImage(CollectionDB::instance()->albumImage(*playlistItem )) ); } } void Playlist::undo() //SLOT { if( !isLocked() ) switchState( m_undoList, m_redoList ); } void Playlist::redo() //SLOT { if( !isLocked() ) switchState( m_redoList, m_undoList ); } void Playlist::updateMetaData( const MetaBundle &mb ) //SLOT { SHOULD_BE_GUI for( MyIt it( this, MyIt::All ); *it; ++it ) if( mb.url() == (*it)->url() ) { (*it)->copyFrom( mb ); (*it)->filter( m_filter ); } } void Playlist::adjustColumn( int n ) { if( n == PlaylistItem::Rating ) setColumnWidth( n, PlaylistItem::ratingColumnWidth() ); else if( n == PlaylistItem::Mood ) setColumnWidth( n, 120 ); else KListView::adjustColumn( n ); } void Playlist::showQueueManager() { DEBUG_BLOCK // Only show the dialog once if( QueueManager::instance() ) { QueueManager::instance()->raise(); return; } QueueManager dialog; if( dialog.exec() == TQDialog::Accepted ) { changeFromQueueManager(dialog.newQueue()); } } void Playlist::changeFromQueueManager(TQPtrList list) { PLItemList oldQueue = m_nextTracks; m_nextTracks = list; PLItemList in, out; // make sure we repaint items no longer queued for( PlaylistItem* item = oldQueue.first(); item; item = oldQueue.next() ) if( !m_nextTracks.containsRef( item ) ) out << item; for( PlaylistItem* item = m_nextTracks.first(); item; item = m_nextTracks.next() ) if( !oldQueue.containsRef( item ) ) in << item; emit queueChanged( in, out ); // repaint newly queued or altered queue items if( dynamicMode() ) sortQueuedItems(); else refreshNextTracks(); } void Playlist::setFilterSlot( const TQString &query ) //SLOT { m_filtertimer->stop(); if( m_filter != query ) { m_prevfilter = m_filter; m_filter = query; } m_filtertimer->start( 50, true ); } void Playlist::setDelayedFilter() //SLOT { setFilter( m_filter ); //to me it seems sensible to do this, BUT if it seems annoying to you, remove it showCurrentTrack(); } void Playlist::setFilter( const TQString &query ) //SLOT { const bool advanced = ExpressionParser::isAdvancedExpression( query ); MyIt it( this, ( !advanced && query.lower().contains( m_prevfilter.lower() ) ) ? MyIt::Visible : MyIt::All ); if( advanced ) { ParsedExpression parsed = ExpressionParser::parse( query ); TQValueList visible = visibleColumns(); for(; *it; ++it ) (*it)->setVisible( (*it)->matchesParsedExpression( parsed, visible ) ); } else { // optimized path const TQStringList terms = TQStringList::split( ' ', query.lower() ); const MetaBundle::ColumnMask visible = getVisibleColumnMask(); for(; *it; ++it ) { (*it)->setVisible( (*it)->matchesFast(terms, visible)); } } if( m_filter != query ) { m_prevfilter = m_filter; m_filter = query; } updateNextPrev(); } void Playlist::scoreChanged( const TQString &path, float score ) { for( MyIt it( this, MyIt::All ); *it; ++it ) { PlaylistItem *item = static_cast( *it ); if ( item->url().path() == path ) { item->setScore( score ); item->setPlayCount( CollectionDB::instance()->getPlayCount( path ) ); item->setLastPlay( CollectionDB::instance()->getLastPlay( path ).toTime_t() ); item->filter( m_filter ); } } } void Playlist::ratingChanged( const TQString &path, int rating ) { for( MyIt it( this, MyIt::All ); *it; ++it ) { PlaylistItem *item = static_cast( *it ); if ( item->url().path() == path ) { item->setRating( rating ); item->filter( m_filter ); } } } void Playlist::fileMoved( const TQString &srcPath, const TQString &dstPath ) { // Make sure the MoodServer gets this signal first! MoodServer::instance()->slotFileMoved( srcPath, dstPath ); for( MyIt it( this, MyIt::All ); *it; ++it ) { PlaylistItem *item = static_cast( *it ); if ( item->url().path() == srcPath ) { item->setUrl( KURL::fromPathOrURL( dstPath ) ); item->filter( m_filter ); } } } void Playlist::appendToPreviousTracks( PlaylistItem *item ) { if( !m_prevTracks.containsRef( item ) ) { m_total -= item->totalIncrementAmount(); m_prevTracks.append( item ); } } void Playlist::appendToPreviousAlbums( PlaylistAlbum *album ) { if( !m_prevAlbums.containsRef( album ) ) { m_total -= album->total; m_prevAlbums.append( album ); } } void Playlist::removeFromPreviousTracks( PlaylistItem *item ) { if( item ) { if( m_prevTracks.removeRef( item ) ) m_total += item->totalIncrementAmount(); } else if( (item = m_prevTracks.current()) != 0 ) if( m_prevTracks.remove() ) m_total += item->totalIncrementAmount(); } void Playlist::removeFromPreviousAlbums( PlaylistAlbum *album ) { if( album ) { if( m_prevAlbums.removeRef( album ) ) m_total += album->total; } else if( (album = m_prevAlbums.current()) != 0 ) if( m_prevAlbums.remove() ) m_total += album->total; } void Playlist::showContextMenu( TQListViewItem *item, const TQPoint &p, int col ) //SLOT { //if clicked on an empty area enum { REPOPULATE, ENABLEDYNAMIC }; if( item == 0 ) { KPopupMenu popup; Amarok::actionCollection()->action("playlist_save")->plug( &popup ); Amarok::actionCollection()->action("playlist_clear")->plug( &popup ); DynamicMode *m = 0; if(dynamicMode()) popup.insertItem( SmallIconSet( Amarok::icon( "dynamic" ) ), i18n("Repopulate"), REPOPULATE); else { Amarok::actionCollection()->action("playlist_shuffle")->plug( &popup ); m = PlaylistBrowser::instance()->findDynamicModeByTitle( AmarokConfig::lastDynamicMode() ); if( m ) popup.insertItem( SmallIconSet( Amarok::icon( "dynamic" ) ), i18n("L&oad %1").arg( m->title().replace( '&', "&&" ) ), ENABLEDYNAMIC); } switch(popup.exec(p)) { case ENABLEDYNAMIC: loadDynamicMode( m ); break; case REPOPULATE: repopulate(); break; } return; } #define item static_cast(item) enum { PLAY, PLAY_NEXT, STOP_DONE, VIEW, EDIT, FILL_DOWN, COPY, CROP_PLAYLIST, SAVE_PLAYLIST, REMOVE, FILE_MENU, ORGANIZE, MOVE_TO_COLLECTION, COPY_TO_COLLECTION, DELETE, TRASH, REPEAT, LAST }; //keep LAST last const bool canRename = isRenameable( col ) && item->url().isLocalFile(); const bool isCurrent = (item == m_currentTrack); const bool isPlaying = EngineController::engine()->state() == Engine::Playing; const bool trackColumn = col == PlaylistItem::Track; const bool isLastFm = item->url().protocol() == "lastfm"; const TQString tagName = columnText( col ); const TQString tag = item->text( col ); uint itemCount = 0; for( MyIt it( this, MyIt::Selected ); *it; ++it ) itemCount++; PrettyPopupMenu popup; // if(itemCount==1) // popup.insertTitle( KStringHandler::rsqueeze( MetaBundle( item ).prettyTitle(), 50 )); // else // popup.insertTitle(i18n("1 Track", "%n Selected Tracks", itemCount)); if( isCurrent && isLastFm ) { KActionCollection *ac = Amarok::actionCollection(); if( ac->action( "skip" ) ) ac->action( "skip" )->plug( &popup ); if( ac->action( "love" ) ) ac->action( "love" )->plug( &popup ); if( ac->action( "ban" ) ) ac->action( "ban" )->plug( &popup ); popup.insertSeparator(); } if( !isCurrent || !isPlaying ) popup.insertItem( SmallIconSet( Amarok::icon( "play" ) ), isCurrent && isPlaying ? i18n( "&Restart" ) : i18n( "&Play" ), PLAY ); if( isCurrent && !isLastFm && isPlaying ) Amarok::actionCollection()->action( "pause" )->plug( &popup ); // Begin queue entry logic popup.insertItem( SmallIconSet( Amarok::icon( "queue_track" ) ), i18n("&Queue Selected Tracks"), PLAY_NEXT ); bool queueToggle = false; MyIt it( this, MyIt::Selected ); bool firstQueued = ( m_nextTracks.findRef( *it ) != -1 ); for( ++it ; *it; ++it ) { if ( ( m_nextTracks.findRef( *it ) != -1 ) != firstQueued ) { queueToggle = true; break; } } if( itemCount == 1 ) { if ( !firstQueued ) popup.changeItem( PLAY_NEXT, i18n( "&Queue Track" ) ); else popup.changeItem( PLAY_NEXT, SmallIconSet( Amarok::icon( "dequeue_track" ) ), i18n("&Dequeue Track") ); } else { if ( queueToggle ) popup.changeItem( PLAY_NEXT, i18n( "Toggle &Queue Status (1 track)", "Toggle &Queue Status (%n tracks)", itemCount ) ); else // remember, queueToggled only gets set to false if there are items queued and not queued. // so, if queueToggled is false, all items have the same queue status as the first item. if ( !firstQueued ) popup.changeItem( PLAY_NEXT, i18n( "&Queue Selected Tracks" ) ); else popup.changeItem( PLAY_NEXT, SmallIconSet( Amarok::icon( "dequeue_track" ) ), i18n("&Dequeue Selected Tracks") ); } // End queue entry logic bool afterCurrent = false; if( !m_nextTracks.isEmpty() ? m_nextTracks.getLast() : m_currentTrack ) for( MyIt it( !m_nextTracks.isEmpty() ? m_nextTracks.getLast() : m_currentTrack, MyIt::Visible ); *it; ++it ) if( *it == item ) { afterCurrent = true; break; } if( itemCount == 1 ) { Amarok::actionCollection()->action( "stop_after" )->plug( &popup ); dynamic_cast( Amarok::actionCollection()->action( "stop_after" ) )->setChecked( m_stopAfterTrack == item ); } if( isCurrent && itemCount == 1 ) { popup.insertItem( SmallIconSet( Amarok::icon( "repeat_track" ) ), i18n( "&Repeat Track" ), REPEAT ); popup.setItemChecked( REPEAT, Amarok::repeatTrack() ); } popup.insertSeparator(); if( itemCount > 1 ) { popup.insertItem( SmallIconSet( Amarok::icon( "playlist" ) ), i18n("&Set as Playlist (Crop)"), CROP_PLAYLIST ); popup.insertItem( SmallIconSet( Amarok::icon( "save" ) ), i18n("S&ave as Playlist..."), SAVE_PLAYLIST ); } popup.insertItem( SmallIconSet( Amarok::icon( "remove_from_playlist" ) ), i18n( "Re&move From Playlist" ), this, TQT_SLOT( removeSelectedItems() ), Key_Delete, REMOVE ); popup.insertSeparator(); KPopupMenu fileMenu; if( CollectionDB::instance()->isDirInCollection( item->url().directory() ) ) { fileMenu.insertItem( SmallIconSet( "filesaveas" ), i18n("&Organize File...", "&Organize %n Files...", itemCount), ORGANIZE ); } else { fileMenu.insertItem( SmallIconSet( "filesaveas" ), i18n("&Copy Track to Collection...", "&Copy %n Tracks to Collection...", itemCount), COPY_TO_COLLECTION ); fileMenu.insertItem( SmallIconSet( "filesaveas" ), i18n("&Move Track to Collection...", "&Move %n Tracks to Collection...", itemCount), MOVE_TO_COLLECTION ); } fileMenu.insertItem( SmallIconSet( Amarok::icon( "remove" ) ), i18n("&Delete File...", "&Delete %n Selected Files...", itemCount ), this, TQT_SLOT( deleteSelectedFiles() ), SHIFT+Key_Delete, DELETE ); popup.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n("Manage &Files"), &fileMenu, FILE_MENU ); if( itemCount == 1 ) popup.insertItem( SmallIconSet( Amarok::icon( "editcopy" ) ), i18n( "&Copy Tags to Clipboard" ), COPY ); if( itemCount > 1 ) popup.insertItem( trackColumn ? i18n("Iteratively Assign Track &Numbers") : i18n("&Write '%1' for Selected Tracks") .arg( KStringHandler::rsqueeze( tag, 30 ).replace( "&", "&&" ) ), FILL_DOWN ); popup.insertItem( SmallIconSet( Amarok::icon( "edit" ) ), (itemCount == 1 ? i18n( "&Edit Tag '%1'" ) : i18n( "&Edit '%1' Tag for Selected Tracks" )).arg( tagName ), EDIT ); popup.insertItem( SmallIconSet( Amarok::icon( "info" ) ) , item->url().isLocalFile() ? i18n( "Edit Track &Information...", "Edit &Information for %n Tracks...", itemCount): i18n( "Track &Information...", "&Information for %n Tracks...", itemCount) , VIEW ); popup.setItemEnabled( EDIT, canRename ); //only enable for columns that have editable tags popup.setItemEnabled( FILL_DOWN, canRename ); popup.setItemEnabled( REMOVE, !isLocked() ); // can't remove things when playlist is locked, popup.setItemEnabled( DELETE, !isLocked() && item->url().isLocalFile() ); popup.setItemEnabled( ORGANIZE, !isLocked() && item->isKioUrl() ); popup.setItemEnabled( MOVE_TO_COLLECTION, !isLocked() && item->isKioUrl() ); popup.setItemEnabled( COPY_TO_COLLECTION, !isLocked() && item->isKioUrl() ); popup.setItemEnabled( VIEW, item->url().isLocalFile() || itemCount == 1 ); // disable for CDAudio multiselection if( m_customSubmenuItem.count() > 0 ) popup.insertSeparator(); TQValueList submenuTexts = m_customSubmenuItem.keys(); for( TQValueList::Iterator keyIt =submenuTexts.begin(); keyIt != submenuTexts.end(); ++keyIt ) { KPopupMenu* menu; if( (*keyIt) == "root") menu = &popup; else { menu = new KPopupMenu(); popup.insertItem( *keyIt, menu); } foreach(m_customSubmenuItem[*keyIt]) { int id; if(m_customIdItem.isEmpty()) id=LAST; else id=m_customIdItem.keys().last()+1; menu->insertItem( (*it), id ); m_customIdItem[id]= (*keyIt) + ' ' + (*it); } } const TQPoint pos( p.x() - popup.sidePixmapWidth(), p.y() + 3 ); int menuItemId = popup.exec( pos ); PLItemList in, out; switch( menuItemId ) { case PLAY: if( itemCount == 1 ) { //Restarting track on dynamic mode if( isCurrent && isPlaying && dynamicMode() ) m_dynamicDirt = true; activate( item ); } else { MyIt it( this, MyIt::Selected ); activate( *it ); ++it; for( int i = 0; *it; ++i, ++it ) { in.append( *it ); m_nextTracks.insert( i, *it ); } emit queueChanged( in, out ); } break; case PLAY_NEXT: queueSelected(); break; case VIEW: showTagDialog( selectedItems() ); break; case EDIT: // do this because TQListView sucks, if track change occurs during // an edit event, the rename operation ends, BUT, the list is not // cleared because writeTag is never called. Q/K ListView sucks m_itemsToChangeTagsFor.clear(); if( !item->isSelected() ) m_itemsToChangeTagsFor.append( item ); else for( MyIt it( this, MyIt::Selected ); *it; ++it ) m_itemsToChangeTagsFor.append( *it ); rename( item, col ); break; case FILL_DOWN: //Spreadsheet like fill-down { TQString newTag = item->exactText( col ); MyIt it( this, MyIt::Selected ); //special handling for track column uint trackNo = (*it)->track(); //we should start at the next row if we are doing track number //and the first row has a number set if ( trackColumn && trackNo > 0 ) ++it; ThreadManager::JobList jobs; bool updateView = true; for( ; *it; ++it ) { if ( trackColumn ) //special handling for track column newTag = TQString::number( ++trackNo ); else if ( *it == item ) //skip the one we are copying continue; else if( col == PlaylistItem::Score ) { CollectionDB::instance()->setSongPercentage( (*it)->url().path(), newTag.toInt() ); continue; } else if( col == PlaylistItem::Rating ) { CollectionDB::instance()->setSongRating( (*it)->url().path(), newTag.toInt() ); continue; } if ( !(*it)->isEditing( col ) ) jobs.prepend( new TagWriter( *it, (*it)->exactText( col ), newTag, col, updateView ) ); updateView = false; } ThreadManager::instance()->queueJobs( jobs ); } break; case COPY: copyToClipboard( item ); break; case CROP_PLAYLIST: if( !isLocked() ) { //use "in" for the other just because it's there and not used otherwise for( MyIt it( this, MyIt::Unselected | MyIt::Visible ); *it; ++it ) ( m_nextTracks.containsRef( *it ) ? in : out ).append( *it ); if( !in.isEmpty() || !out.isEmpty() ) { saveUndoState(); for( PlaylistItem *it = out.first(); it; it = out.next() ) removeItem( it, true ); if( !out.isEmpty() ) emit queueChanged( PLItemList(), out ); for( PlaylistItem *it = out.first(); it; it = out.next() ) delete it; for( PlaylistItem *it = in.first(); it; it = in.next() ) { removeItem( it ); delete it; } ScriptManager::instance()->notifyPlaylistChange("cleared"); } } break; case SAVE_PLAYLIST: saveSelectedAsPlaylist(); break; case REPEAT: // FIXME HACK Accessing AmarokConfig::Enum* yields compile errors with GCC 3.3. static_cast( Amarok::actionCollection()->action( "repeat" ) ) ->setCurrentItem( Amarok::repeatTrack() ? 0 /*AmarokConfig::EnumRepeat::Off*/ : 1 /*AmarokConfig::EnumRepeat::Track*/ ); break; case ORGANIZE: case MOVE_TO_COLLECTION: case COPY_TO_COLLECTION: { KURL::List list; for( TQListViewItemIterator it( this, TQListViewItemIterator::Selected ); it.current(); ++it ) { PlaylistItem *i= static_cast(*it); KURL url = i->url(); list << url; } bool organize = CollectionDB::instance()->isDirInCollection( item->url().directory() ); bool move = menuItemId==MOVE_TO_COLLECTION; CollectionView::instance()->organizeFiles( list, organize ? i18n( "Organize Files" ) : move ? i18n( "Move Tracks to Collection" ) : i18n( "Copy Tracks to Collection"), !organize && !move ); } break; default: if(menuItemId < LAST) break; customMenuClicked(menuItemId); break; } #undef item } //////////////////////////////////////////////////////////////////////////////// /// Misc Protected Methods //////////////////////////////////////////////////////////////////////////////// void Playlist::fontChange( const TQFont &old ) { KListView::fontChange( old ); initStarPixmaps(); triggerUpdate(); } void Playlist::contentsMouseMoveEvent( TQMouseEvent *e ) { if( e ) KListView::contentsMouseMoveEvent( e ); PlaylistItem *prev = m_hoveredRating; const TQPoint pos = e ? e->pos() : viewportToContents( viewport()->mapFromGlobal( TQCursor::pos() ) ); PlaylistItem *item = static_cast( itemAt( contentsToViewport( pos ) ) ); if( item && pos.x() > header()->sectionPos( PlaylistItem::Rating ) && pos.x() < header()->sectionPos( PlaylistItem::Rating ) + header()->sectionSize( PlaylistItem::Rating ) ) { m_hoveredRating = item; m_hoveredRating->updateColumn( PlaylistItem::Rating ); } else m_hoveredRating = 0; if( prev ) { if( m_selCount > 1 && prev->isSelected() ) TQScrollView::updateContents( header()->sectionPos( PlaylistItem::Rating ) + 1, contentsY(), header()->sectionSize( PlaylistItem::Rating ) - 2, visibleHeight() ); else prev->updateColumn( PlaylistItem::Rating ); } } void Playlist::leaveEvent( TQEvent *e ) { KListView::leaveEvent( e ); PlaylistItem *prev = m_hoveredRating; m_hoveredRating = 0; if( prev ) prev->updateColumn( PlaylistItem::Rating ); } void Playlist::contentsMousePressEvent( TQMouseEvent *e ) { PlaylistItem *item = static_cast( itemAt( contentsToViewport( e->pos() ) ) ); int beginRatingSection = header()->sectionPos( PlaylistItem::Rating ); int endRatingSection = beginRatingSection + header()->sectionSize( PlaylistItem::Rating ); /// Conditions on setting the rating of an item if( item && !( e->state() & TQt::ControlButton || e->state() & TQt::ShiftButton ) && // skip if ctrl or shift held ( e->button() & Qt::LeftButton ) && // only on a left click ( e->pos().x() > beginRatingSection && e->pos().x() < endRatingSection ) ) // mouse over rating column { int rating = item->ratingAtPoint( e->pos().x() ); if( item->isSelected() ) setSelectedRatings( rating ); else // toggle half star CollectionDB::instance()->setSongRating( item->url().path(), rating, true ); } else KListView::contentsMousePressEvent( e ); } void Playlist::contentsWheelEvent( TQWheelEvent *e ) { PlaylistItem* const item = static_cast( itemAt( contentsToViewport( e->pos() ) ) ); const int column = header()->sectionAt( e->pos().x() ); const int distance = header()->sectionPos( column ) + header()->sectionSize( column ) - e->pos().x(); const int maxdistance = fontMetrics().width( TQString::number( m_nextTracks.count() ) ) + 7; if( item && column == m_firstColumn && distance <= maxdistance && item->isQueued() ) { const int n = e->delta() / 120, s = n / abs(n), pos = item->queuePosition(); PLItemList changed; for( int i = 1; i <= abs(n); ++i ) { const int dest = pos + s*i; if( kClamp( dest, 0, int( m_nextTracks.count() ) - 1 ) != dest ) break; PlaylistItem* const p = m_nextTracks.at( dest ); if( changed.findRef( p ) == -1 ) changed << p; if( changed.findRef( m_nextTracks.at( dest - s ) ) == -1 ) changed << m_nextTracks.at( dest - s ); m_nextTracks.replace( dest, m_nextTracks.at( dest - s ) ); m_nextTracks.replace( dest - s, p ); } for( int i = 0, n = changed.count(); i < n; ++i ) changed.at(i)->update(); } else KListView::contentsWheelEvent( e ); } //////////////////////////////////////////////////////////////////////////////// /// Misc Private Methods //////////////////////////////////////////////////////////////////////////////// void Playlist::lock() { if( m_lockStack == 0 ) { m_clearButton->setEnabled( false ); m_undoButton->setEnabled( false ); m_redoButton->setEnabled( false ); } m_lockStack++; } void Playlist::unlock() { Q_ASSERT( m_lockStack > 0 ); m_lockStack--; if( m_lockStack == 0 ) { m_clearButton->setEnabled( true ); m_undoButton->setEnabled( !m_undoList.isEmpty() ); m_redoButton->setEnabled( !m_redoList.isEmpty() ); } } int Playlist::numVisibleColumns() const { int r = 0, i = 1; for( const int n = columns(); i <= n; ++i) if( columnWidth( i - 1 ) ) ++r; return r; } TQValueList Playlist::visibleColumns() const { TQValueList r; for( int i = 0, n = columns(); i < n; ++i) if( columnWidth( i ) ) r.append( i ); return r; } MetaBundle::ColumnMask Playlist::getVisibleColumnMask() const { MetaBundle::ColumnMask mask = 0; for( int i = 0, n = columns(); i < n; ++i) if( columnWidth( i ) ) mask = mask | (1 << i); return mask; } int Playlist::mapToLogicalColumn( int physical ) const { int logical = header()->mapToSection( physical ); //skip hidden columns int n = 0; for( int i = 0; i <= physical; ++i ) if( !header()->sectionSize( header()->mapToSection( physical - i ) ) ) ++n; while( n ) { logical = header()->mapToSection( ++physical ); if( logical < 0 ) { logical = header()->mapToSection( physical - 1 ); break; } if( header()->sectionSize( logical ) ) --n; } return logical; } void Playlist::setColumns( TQValueList order, TQValueList visible ) { for( int i = order.count() - 1; i >= 0; --i ) header()->moveSection( order[i], i ); for( int i = 0; i < PlaylistItem::NUM_COLUMNS; ++i ) { if( visible.contains( i ) ) adjustColumn( i ); else hideColumn( i ); } columnOrderChanged(); } void Playlist::removeItem( PlaylistItem *item, bool multi ) { // NOTE we don't check isLocked() here as it is assumed that if you call this function you // really want to remove the item, there is no way the user can reach here without passing // a lock() check, (currently...) //this function ensures we don't have dangling pointers to items that are about to be removed //for some reason using TQListView::takeItem() and TQListViewItem::takeItem() was ineffective //NOTE we don't delete item for you! You must call delete item yourself :) //TODO there must be a way to do this without requiring notification from the item dtor! //NOTE orginally this was in ~PlaylistItem(), but that caused crashes due to clear() *shrug* //NOTE items already removed by takeItem() will crash if you call nextSibling() on them // taken items return 0 from listView() //FIXME if you remove a series of items including the currentTrack and all the nextTracks // then no new nextTrack will be selected and the playlist will resume from the begging // next time if( m_currentTrack == item ) { setCurrentTrack( 0 ); //ensure the playlist doesn't start at the beginning after the track that's playing ends //we don't need to do that in random mode, it's getting randomly selected anyways if( m_nextTracks.isEmpty() && !AmarokConfig::randomMode() ) { //*MyIt( item ) returns either "item" or if item is hidden, the next visible playlistitem PlaylistItem* const next = *MyIt( item ); if( next ) { m_nextTracks.append( next ); next->update(); } } } if( m_stopAfterTrack == item ) { m_stopAfterTrack = 0; //to be safe if (stopAfterMode() != StopAfterCurrent) setStopAfterMode( DoNotStop ); } //cancel rename if it is pending (Bug: #147587) if ( m_itemToRename == item ) { m_clicktimer->stop(); m_itemToRename = 0; } //keep m_nextTracks queue synchronized if( m_nextTracks.removeRef( item ) && !multi ) emit queueChanged( PLItemList(), PLItemList( item ) ); //keep recent buffer synchronized removeFromPreviousTracks( item ); //removes all pointers to item updateNextPrev(); } void Playlist::ensureItemCentered( TQListViewItem *item ) { if( !item ) return; //HACK -- apparently the various metrics aren't reliable while the UI is still updating & stuff m_itemToReallyCenter = item; TQTimer::singleShot( 0, this, TQT_SLOT( reallyEnsureItemCentered() ) ); } void Playlist::reallyEnsureItemCentered() { if( TQListViewItem *item = m_itemToReallyCenter ) { m_itemToReallyCenter = 0; if( m_selCount == 1 ) { PlaylistItem *previtem = *MyIt( this, MyIt::Selected ); if( previtem && previtem != item ) previtem->setSelected( false ); } setCurrentItem( item ); ensureVisible( contentsX(), item->itemPos() + item->height() / 2, 0, visibleHeight() / 2 ); triggerUpdate(); } } void Playlist::refreshNextTracks( int from ) { // This function scans the m_nextTracks list starting from the 'from' // position and from there on updates the progressive numbering on related // items and repaints them. In short it performs an update subsequent to // a renumbering/order changing at some point of the m_nextTracks list. //start on the 'from'-th item of the list for( PlaylistItem* item = (from == -1) ? m_nextTracks.current() : m_nextTracks.at( from ); item; item = m_nextTracks.next() ) { item->update(); } } void Playlist::saveUndoState() //SLOT { if( saveState( m_undoList ) ) { m_redoList.clear(); m_undoButton->setEnabled( true ); m_redoButton->setEnabled( false ); } } bool Playlist::saveState( TQStringList &list ) { //used by undo system, save state of playlist to undo/redo list //do not change this! It's required by the undo/redo system to work! //if you must change this, fix undo/redo first. Ask me what needs fixing if( !isEmpty() ) { TQString fileName; m_undoCounter %= AmarokConfig::undoLevels(); fileName.setNum( m_undoCounter++ ); fileName.prepend( m_undoDir.absPath() + '/' ); fileName.append( ".xml" ); if ( list.count() >= (uint)AmarokConfig::undoLevels() ) { m_undoDir.remove( list.first() ); list.pop_front(); } saveXML( fileName ); list.append( fileName ); // Reset isNew state of all items in the playlist (determines font coloring) PlaylistItem* item = static_cast( firstChild() ); while( item ) { item->setIsNew( false ); item = item->nextSibling(); } triggerUpdate(); return true; } return false; } void Playlist::switchState( TQStringList &loadFromMe, TQStringList &saveToMe ) { m_undoDirt = true; //switch to a previously saved state, remember current state KURL url; url.setPath( loadFromMe.last() ); loadFromMe.pop_back(); //save current state saveState( saveToMe ); //this is clear() minus some parts, for instance we don't want to cause a saveUndoState() here m_currentTrack = 0; disableDynamicMode(); Glow::reset(); m_prevTracks.clear(); m_prevAlbums.clear(); const PLItemList prev = m_nextTracks; m_nextTracks.clear(); emit queueChanged( PLItemList(), prev ); ThreadManager::instance()->abortAllJobsNamed( "TagWriter" ); safeClear(); m_total = 0; m_albums.clear(); insertMediaInternal( url, 0, 0 ); //because the listview is empty, undoState won't be forced m_undoButton->setEnabled( !m_undoList.isEmpty() ); m_redoButton->setEnabled( !m_redoList.isEmpty() ); if( dynamicMode() ) setDynamicHistory( true ); m_undoDirt = false; } void Playlist::saveSelectedAsPlaylist() { MyIt it( this, MyIt::Visible | MyIt::Selected ); if( !(*it) ) return; //safety const TQString album = (*it)->album(), artist = (*it)->artist(); int suggestion = !album.stripWhiteSpace().isEmpty() ? 1 : !artist.stripWhiteSpace().isEmpty() ? 2 : 3; while( *it ) { if( suggestion == 1 && (*it)->album()->lower().stripWhiteSpace() != album.lower().stripWhiteSpace() ) suggestion = 2; if( suggestion == 2 && (*it)->artist()->lower().stripWhiteSpace() != artist.lower().stripWhiteSpace() ) suggestion = 3; if( suggestion == 3 ) break; ++it; } TQString path = PlaylistDialog::getSaveFileName( suggestion == 1 ? album : suggestion == 2 ? artist : i18n( "Untitled" ) ); if( path.isEmpty() ) return; TQValueList urls; TQValueList titles; TQValueList lengths; for( it = MyIt( this, MyIt::Visible | MyIt::Selected ); *it; ++it ) { urls << (*it)->url(); titles << (*it)->title(); lengths << (*it)->length(); } if( PlaylistBrowser::savePlaylist( path, urls, titles, lengths ) ) PlaylistWindow::self()->showBrowser( "PlaylistBrowser" ); } void Playlist::initStarPixmaps() { StarManager::instance()->reinitStars( fontMetrics().height(), itemMargin() ); } void Playlist::slotMouseButtonPressed( int button, TQListViewItem *after, const TQPoint &p, int col ) //SLOT { switch( button ) { case Qt::MidButton: { const TQString path = TQApplication::clipboard()->text( TQClipboard::Selection ); const KURL url = KURL::fromPathOrURL( path ); if( url.isValid() ) insertMediaInternal( url, static_cast(after ? after : lastItem()) ); break; } case Qt::RightButton: showContextMenu( after, p, col ); break; default: ; } } void Playlist::slotContentsMoving() { Amarok::ToolTip::hideTips(); TQTimer::singleShot( 0, this, TQT_SLOT( contentsMouseMoveEvent() ) ); } void Playlist::slotQueueChanged( const PLItemList &/*in*/, const PLItemList &out) { for( TQPtrListIterator it( out ); *it; ++it ) (*it)->update(); refreshNextTracks( 0 ); updateNextPrev(); } void Playlist::slotUseScores( bool use ) { if( !use && columnWidth( MetaBundle::Score ) ) hideColumn( MetaBundle::Score ); } void Playlist::slotUseRatings( bool use ) { if( use && !columnWidth( MetaBundle::Rating ) ) adjustColumn( MetaBundle::Rating ); else if( !use && columnWidth( MetaBundle::Rating ) ) hideColumn( MetaBundle::Rating ); } // This gets called when the user presses "Ok" or "Apply" in the // config dialog. void Playlist::slotMoodbarPrefs( bool show, bool moodier, int alter, bool withMusic ) { (void) moodier; (void) alter; (void) withMusic; if( !show && columnWidth( MetaBundle::Mood ) ) hideColumn( MetaBundle::Mood ); // Reset all of our moodbars, since they may have been permanently // disabled before because the Moodbar was disabled. We need to // do this even if the column is hidden. if( show ) { // No need to call moodbar().load(), since that will happen // automatically next time it's displayed. We do have to // repaint so that they get displayed though. for( PlaylistIterator it( this, PlaylistIterator::All ) ; *it ; ++it ) { (*it)->moodbar().reset(); repaintItem(*it); } } } void Playlist::slotGlowTimer() //SLOT { if( !currentTrack() ) return; using namespace Glow; if( counter <= STEPS*2 ) { // 0 -> STEPS -> 0 const double d = (counter > STEPS) ? 2*STEPS-counter : counter; { using namespace Base; PlaylistItem::glowIntensity = d; PlaylistItem::glowBase = TQColor( r, g, b ); } { using namespace Text; PlaylistItem::glowText = TQColor( r + int(d*dr), g + int(d*dg), b + int(d*db) ); } if( currentTrack() ) currentTrack()->update(); } ++counter &= 63; //built in bounds checking with &= } void Playlist::slotRepeatTrackToggled( int /* mode */ ) { if( m_currentTrack ) m_currentTrack->update(); } void Playlist::slotEraseMarker() //SLOT { if( m_marker ) { const TQRect spot = drawDropVisualizer( 0, 0, m_marker ); m_marker = 0; viewport()->repaint( spot, false ); } } void Playlist::showTagDialog( TQPtrList items ) { /// the tag dialog was once modal, because we thought that damage would occur /// when passing playlist items into the editor and it was removed from the playlist. /// This is simply not the case, information is written to the URL, not the item. // Playlist::lock(); if( items.isEmpty() ) return; if ( items.count() == 1 ) { PlaylistItem *item = static_cast( items.first() ); bool isDaap = item->url().protocol() == "daap"; if ( !item->url().isLocalFile() && !isDaap ) { StreamEditor dialog( this, item->title(), item->url().prettyURL(), true ); if( item->url().protocol() == "cdda" ) dialog.setCaption( i18n( "CD Audio" ) ); else dialog.setCaption( i18n( "Remote Media" ) ); dialog.exec(); } else if ( isDaap ) // don't check if exists { // The tag dialog automatically disables the widgets if the file is not local, which it is not. TagDialog *dialog = new TagDialog( *item, item, instance() ); dialog->show(); } else if ( checkFileStatus( item ) ) { TagDialog *dialog = new TagDialog( *item, item, instance() ); dialog->show(); } else KMessageBox::sorry( this, i18n( "This file does not exist:" ) + ' ' + item->url().path() ); } else { //edit multiple tracks in tag dialog KURL::List urls; for( TQListViewItem *item = items.first(); item; item = items.next() ) if ( item->isVisible() ) urls << static_cast( item )->url(); TagDialog *dialog = new TagDialog( urls, instance() ); dialog->show(); } // Playlist::unlock(); } #include #include #include #include #include #include #include #include //usleep() // Moved outside the only function that uses it because // gcc 2.95 doesn't like class declarations there. class CustomColumnDialog : public KDialog { public: CustomColumnDialog( TQWidget *parent ) : KDialog( parent ) { TQLabel *textLabel1, *textLabel2, *textLabel3; TQLineEdit *lineEdit1, *lineEdit2; TQGroupBox *groupBox1; textLabel1 = new TQLabel( i18n( "

You can create a custom column that runs a shell command against each item in the playlist. " "The shell command is run as the user nobody, this is for security reasons.\n" "

You can only run the command against local files for the time being. " "The fullpath is inserted at the position %f in the string. " "If you do not specify %f it is appended." ), this ); textLabel2 = new TQLabel( i18n( "Column &name:" ), this ); textLabel3 = new TQLabel( i18n( "&Command:" ), this ); lineEdit1 = new TQLineEdit( this, "ColumnName" ); lineEdit2 = new TQLineEdit( this, "Command" ); groupBox1 = new TQGroupBox( 1, Qt::Vertical, i18n( "Examples" ), this ); groupBox1->layout()->setMargin( 11 ); new KActiveLabel( i18n( "file --brief %f\n" "ls -sh %f\n" "basename %f\n" "dirname %f" ), groupBox1 ); // buddies textLabel2->setBuddy( lineEdit1 ); textLabel3->setBuddy( lineEdit2 ); // layouts TQHBoxLayout *layout1 = new TQHBoxLayout( 0, 0, 6 ); layout1->addItem( new TQSpacerItem( 181, 20, TQSizePolicy::Expanding, TQSizePolicy::Minimum ) ); layout1->addWidget( new KPushButton( KStdGuiItem::ok(), this, "OkButton" ) ); layout1->addWidget( new KPushButton( KStdGuiItem::cancel(), this, "CancelButton" ) ); TQGridLayout *layout2 = new TQGridLayout( 0, 2, 2, 0, 6 ); layout2->TQLayout::add( textLabel2 ); layout2->TQLayout::add( lineEdit1 ); layout2->TQLayout::add( textLabel3 ); layout2->TQLayout::add( lineEdit2 ); TQVBoxLayout *Form1Layout = new TQVBoxLayout( this, 11, 6, "Form1Layout"); Form1Layout->addWidget( textLabel1 ); Form1Layout->addWidget( groupBox1 ); Form1Layout->addLayout( layout2 ); Form1Layout->addLayout( layout1 ); Form1Layout->addItem( new TQSpacerItem( 20, 231, TQSizePolicy::Minimum, TQSizePolicy::Expanding ) ); // properties setCaption( i18n("Add Custom Column") ); // connects connect( child( "OkButton" ), TQT_SIGNAL(clicked()), TQT_SLOT(accept()) ); connect( child( "CancelButton" ), TQT_SIGNAL(clicked()), TQT_SLOT(reject()) ); } TQString command() { return static_cast(TQT_TQWIDGET(child("Command")))->text(); } TQString name() { return static_cast(TQT_TQWIDGET(child("ColumnName")))->text(); } }; void Playlist::addCustomColumn() { CustomColumnDialog dialog( this ); if ( dialog.exec() == TQDialog::Accepted ) { const int index = addColumn( dialog.name(), 100 ); TQStringList args = TQStringList::split( ' ', dialog.command() ); TQStringList::Iterator pcf = args.find( "%f" ); if ( pcf == args.end() ) { //there is no %f, so add one on the end //TODO prolly this is confusing, instead ask the user if we should add one args += "%f"; --pcf; } debug() << args << endl; //TODO need to do it with a %u for url and %f for file //FIXME gets stuck it seems if you submit broken commands //FIXME issues with the column resize stuff that cause freezing in eventFilters for( MyIt it( this ); *it; ++it ) { if( (*it)->url().protocol() != "file" ) continue; *pcf = (*it)->url().path(); debug() << args << endl; TQProcess p( args ); for( p.start(); p.isRunning(); /*kapp->processEvents()*/ ) ::usleep( 5000 ); (*it)->setExactText( index, p.readStdout() ); } } } #include #include TagWriter::TagWriter( PlaylistItem *item, const TQString &oldTag, const TQString &newTag, const int col, const bool updateView ) : ThreadManager::Job( "TagWriter" ) , m_item( item ) , m_failed( true ) , m_oldTagString( oldTag ) , m_newTagString( newTag ) , m_tagType( col ) , m_updateView( updateView ) { Playlist::instance()->lock(); item->setEditing( col ); } TagWriter::~TagWriter() { Playlist::instance()->unlock(); } bool TagWriter::doJob() { MetaBundle mb( m_item->url(), true ); switch ( m_tagType ) { case PlaylistItem::Title: mb.setTitle( m_newTagString ); break; case PlaylistItem::Artist: mb.setArtist( m_newTagString ); break; case PlaylistItem::Composer: if ( !mb.hasExtendedMetaInformation() ) return true; mb.setComposer( m_newTagString ); break; case PlaylistItem::DiscNumber: if ( !mb.hasExtendedMetaInformation() ) return true; mb.setDiscNumber( m_newTagString.toInt() ); break; case PlaylistItem::Bpm: if ( !mb.hasExtendedMetaInformation() ) return true; mb.setBpm( m_newTagString.toFloat() ); break; case PlaylistItem::Album: mb.setAlbum( m_newTagString ); break; case PlaylistItem::Year: mb.setYear( m_newTagString.toInt() ); break; case PlaylistItem::Comment: //FIXME how does this work for vorbis files? //Are we likely to overwrite some other comments? //Vorbis can have multiple comment fields.. mb.setComment( m_newTagString ); break; case PlaylistItem::Genre: mb.setGenre( m_newTagString ); break; case PlaylistItem::Track: mb.setTrack( m_newTagString.toInt() ); break; default: return true; } m_failed = !mb.save(); return true; } void TagWriter::completeJob() { switch( m_failed ) { case true: // we write a space for some reason I cannot recall m_item->setExactText( m_tagType, m_oldTagString.isEmpty() ? " " : m_oldTagString ); Amarok::StatusBar::instance()->longMessage( i18n( "Sorry, the tag for %1 could not be changed." ).arg( m_item->url().fileName() ), KDE::StatusBar::Sorry ); break; case false: m_item->setExactText( m_tagType, m_newTagString.isEmpty() ? " " : m_newTagString ); CollectionDB::instance()->updateURL( m_item->url().path(), m_updateView ); } m_item->setIsBeingRenamed( false ); m_item->filter( Playlist::instance()->m_filter ); if( m_item->deleteAfterEditing() ) { Playlist::instance()->removeItem( m_item ); delete m_item; } } #include "playlist.moc"