summaryrefslogtreecommitdiffstats
path: root/amarok/src/playlist.cpp
diff options
context:
space:
mode:
authortpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2010-01-09 23:52:48 +0000
committertpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2010-01-09 23:52:48 +0000
commit3ce9174229de91411a9abf5381a1f335fe0c6a98 (patch)
tree84b2736fa1b0d3fbf9c60fc04f510d2a13916b09 /amarok/src/playlist.cpp
downloadamarok-3ce9174229de91411a9abf5381a1f335fe0c6a98.tar.gz
amarok-3ce9174229de91411a9abf5381a1f335fe0c6a98.zip
Added abandoned KDE3 version of Amarok
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/amarok@1072335 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'amarok/src/playlist.cpp')
-rw-r--r--amarok/src/playlist.cpp4981
1 files changed, 4981 insertions, 0 deletions
diff --git a/amarok/src/playlist.cpp b/amarok/src/playlist.cpp
new file mode 100644
index 00000000..2e88c374
--- /dev/null
+++ b/amarok/src/playlist.cpp
@@ -0,0 +1,4981 @@
+/* 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 <max.howell@methylblue.com>
+
+ * NOTES
+ *
+ * The PlaylistWindow handles some Playlist events. Thanks!
+ * This class has a QOBJECT 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 <config.h>
+#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 <cmath> //for pow() in playNextTrack()
+
+#include <qbuffer.h>
+#include <qclipboard.h> //copyToClipboard(), slotMouseButtonPressed()
+#include <qcolor.h>
+#include <qevent.h>
+#include <qfile.h> //undo system
+#include <qheader.h> //eventFilter()
+#include <qlabel.h> //showUsageMessage()
+#include <qpainter.h>
+#include <qpen.h> //slotGlowTimer()
+#include <qsimplerichtext.h> //toolTipText()
+#include <qsortedlist.h>
+#include <qtimer.h>
+#include <qtooltip.h>
+#include <qvaluelist.h> //addHybridTracks()
+#include <qvaluevector.h> //playNextTrack()
+#include <qlayout.h>
+
+#include <kaction.h>
+#include <kapplication.h>
+#include <kcursor.h> //setOverrideCursor()
+#include <kdialogbase.h>
+#include <kglobalsettings.h> //rename()
+#include <kiconeffect.h>
+#include <kiconloader.h> //slotShowContextMenu()
+#include <kio/job.h> //deleteSelectedFiles()
+#include <klineedit.h> //setCurrentTrack()
+#include <klocale.h>
+#include <kmessagebox.h>
+#include <kpopupmenu.h>
+#include <krandomsequence.h> //random Mode
+#include <kstandarddirs.h> //KGlobal::dirs()
+#include <kstdaction.h>
+#include <kstringhandler.h> //::showContextMenu()
+#include <kurldrag.h>
+
+#include <cstdlib> // abs
+
+extern "C"
+{
+ #if KDE_VERSION < KDE_MAKE_VERSION(3,3,91)
+ #include <X11/Xlib.h> //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 QString &oldTag, const QString &newTag, const int, const bool updateView = true );
+ ~TagWriter();
+ bool doJob();
+ void completeJob();
+private:
+ PlaylistItem* const m_item;
+ bool m_failed;
+
+ QString m_oldTagString;
+ QString 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 QTimer timer;
+
+ inline void startTimer()
+ {
+ counter = 0;
+ timer.start( 40 );
+ }
+
+ inline void reset()
+ {
+ counter = 0;
+ timer.stop();
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+/// CLASS Playlist
+//////////////////////////////////////////////////////////////////////////////////////////
+
+QMutex* Playlist::s_dynamicADTMutex = new QMutex();
+Playlist *Playlist::s_instance = 0;
+
+Playlist::Playlist( QWidget *parent )
+ : KListView( parent, "ThePlaylist" )
+ , EngineObserver( EngineController::instance() )
+ , m_startupTime_t( QDateTime::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(), SIGNAL(fileMoved(const QString&,
+ const QString&, const QString&)), SLOT(updateEntriesUrl(const QString&,
+ const QString&, const QString&)) );
+ connect( CollectionDB::instance(), SIGNAL(uniqueIdChanged(const QString&,
+ const QString&, const QString&)), SLOT(updateEntriesUniqueId(const QString&,
+ const QString&, const QString&)) );
+ connect( CollectionDB::instance(), SIGNAL(fileDeleted(const QString&,
+ const QString&)), SLOT(updateEntriesStatusDeleted(const QString&, const QString&)) );
+ connect( CollectionDB::instance(), SIGNAL(fileAdded(const QString&,
+ const QString&)), SLOT(updateEntriesStatusAdded(const QString&, const QString&)) );
+ connect( CollectionDB::instance(), SIGNAL(filesAdded(const QMap<QString,QString>&)),
+ SLOT(updateEntriesStatusAdded(const QMap<QString,QString>&)) );
+
+
+ initStarPixmaps();
+
+ EngineController* const ec = EngineController::instance();
+ connect( ec, SIGNAL(orderPrevious()), SLOT(playPrevTrack()) );
+ connect( ec, SIGNAL(orderNext( const bool )), SLOT(playNextTrack( const bool )) );
+ connect( ec, SIGNAL(orderCurrent()), SLOT(playCurrentTrack()) );
+
+ connect( this, SIGNAL( itemCountChanged( int, int, int, int, int, int ) ), ec, 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( QListView::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, Qt::AlignRight );
+ setColumnAlignment( PlaylistItem::Track, Qt::AlignCenter );
+ setColumnAlignment( PlaylistItem::DiscNumber, Qt::AlignCenter );
+ setColumnAlignment( PlaylistItem::Bpm, Qt::AlignRight );
+ setColumnAlignment( PlaylistItem::Year, Qt::AlignCenter );
+ setColumnAlignment( PlaylistItem::Bitrate, Qt::AlignCenter );
+ setColumnAlignment( PlaylistItem::SampleRate, Qt::AlignCenter );
+ setColumnAlignment( PlaylistItem::Filesize, Qt::AlignCenter );
+ setColumnAlignment( PlaylistItem::Score, Qt::AlignCenter );
+ setColumnAlignment( PlaylistItem::Type, Qt::AlignCenter );
+ setColumnAlignment( PlaylistItem::PlayCount, Qt::AlignCenter );
+
+
+ connect( this, SIGNAL( doubleClicked( QListViewItem* ) ),
+ this, SLOT( doubleClicked( QListViewItem* ) ) );
+ connect( this, SIGNAL( returnPressed( QListViewItem* ) ),
+ this, SLOT( activate( QListViewItem* ) ) );
+ connect( this, SIGNAL( mouseButtonPressed( int, QListViewItem*, const QPoint&, int ) ),
+ this, SLOT( slotMouseButtonPressed( int, QListViewItem*, const QPoint&, int ) ) );
+ connect( this, SIGNAL( queueChanged( const PLItemList &, const PLItemList & ) ),
+ this, SLOT( slotQueueChanged( const PLItemList &, const PLItemList & ) ) );
+ connect( this, SIGNAL( itemRenamed( QListViewItem*, const QString&, int ) ),
+ this, SLOT( writeTag( QListViewItem*, const QString&, int ) ) );
+ connect( this, SIGNAL( aboutToClear() ),
+ this, SLOT( saveUndoState() ) );
+ connect( CollectionDB::instance(), SIGNAL( scoreChanged( const QString&, float ) ),
+ this, SLOT( scoreChanged( const QString&, float ) ) );
+ connect( CollectionDB::instance(), SIGNAL( ratingChanged( const QString&, int ) ),
+ this, SLOT( ratingChanged( const QString&, int ) ) );
+ connect( CollectionDB::instance(), SIGNAL( fileMoved( const QString&, const QString& ) ),
+ this, SLOT( fileMoved( const QString&, const QString& ) ) );
+ connect( header(), SIGNAL( indexChange( int, int, int ) ),
+ this, SLOT( columnOrderChanged() ) ),
+
+
+ connect( &Glow::timer, SIGNAL(timeout()), SLOT(slotGlowTimer()) );
+
+
+ KActionCollection* const ac = Amarok::actionCollection();
+ KAction *copy = KStdAction::copy( this, SLOT( copyToClipboard() ), ac, "playlist_copy" );
+ KStdAction::selectAll( this, SLOT( selectAll() ), ac, "playlist_select_all" );
+
+ m_clearButton = new KAction( i18n( "clear playlist", "&Clear" ), Amarok::icon( "playlist_clear" ), 0, this, SLOT( clear() ), ac, "playlist_clear" );
+ m_undoButton = KStdAction::undo( this, SLOT( undo() ), ac, "playlist_undo" );
+ m_redoButton = KStdAction::redo( this, 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, this, SLOT( repopulate() ), ac, "repopulate" );
+ new KAction( i18n( "S&huffle" ), "rebuild", CTRL+Key_H, this, SLOT( shuffle() ), ac, "playlist_shuffle" );
+ KAction *gotoCurrent = new KAction( i18n( "&Go To Current Track" ), Amarok::icon( "music" ), CTRL+Key_J, this, SLOT( showCurrentTrack() ), ac, "playlist_show" );
+ new KAction( i18n( "&Remove Duplicate && Dead Entries" ), 0, this, SLOT( removeDuplicates() ), ac, "playlist_remove_duplicates" );
+ new KAction( i18n( "&Queue Selected Tracks" ), Amarok::icon( "queue_track" ), CTRL+Key_D, this, SLOT( queueSelected() ), ac, "queue_selected" );
+ KToggleAction *stopafter = new KToggleAction( i18n( "&Stop Playing After Track" ), Amarok::icon( "stop" ), CTRL+ALT+Key_V,
+ this, 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" ), SIGNAL(activated( int )), SLOT(updateNextPrev()) );
+ connect( ac->action( "repeat" ), SIGNAL( activated( int ) ), SLOT( generateInfo() ) );
+ connect( ac->action( "favor_tracks" ), SIGNAL( activated( int ) ), SLOT( generateInfo() ) );
+ connect( ac->action( "random_mode" ), SIGNAL( activated( int ) ), SLOT( generateInfo() ) );
+
+
+ // undostates are written in chronological order, so this is a clever way to get them back in the correct order :)
+ QStringList undos = m_undoDir.entryList( QString("*.xml"), QDir::Files, QDir::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(), SIGNAL(sizeChange( int, int, int )), SLOT(columnResizeEvent( int, int, int )) );
+
+ connect( this, SIGNAL( contentsMoving( int, int ) ), SLOT( slotContentsMoving() ) );
+
+ connect( App::instance(), SIGNAL( useScores( bool ) ), this, SLOT( slotUseScores( bool ) ) );
+ connect( App::instance(), SIGNAL( useRatings( bool ) ), this, SLOT( slotUseRatings( bool ) ) );
+ connect( App::instance(), SIGNAL( moodbarPrefs( bool, bool, int, bool ) ),
+ this, SLOT( slotMoodbarPrefs( bool, bool, int, bool ) ) );
+
+ Amarok::ToolTip::add( this, viewport() );
+
+ header()->installEventFilter( this );
+ renameLineEdit()->installEventFilter( this );
+ setTabOrderedRenaming( false );
+
+ m_filtertimer = new QTimer( this );
+ connect( m_filtertimer, SIGNAL(timeout()), this, SLOT(setDelayedFilter()) );
+
+ connect( MountPointManager::instance(), SIGNAL(mediumConnected( int )),
+ SLOT(mediumChange( int )) );
+ connect( MountPointManager::instance(), SIGNAL(mediumRemoved( int )),
+ SLOT(mediumChange( int )) );
+
+ m_clicktimer = new QTimer( this );
+ connect( m_clicktimer, SIGNAL(timeout()), this, 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( QListViewItem *it = firstChild();
+ it;
+ it = it->nextSibling() )
+ {
+ PlaylistItem *p = dynamic_cast<PlaylistItem *>( 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;
+ QPtrList<PlaylistItem> 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( QPtrList<PlaylistItem>, 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<PlaylistItem*>( after->itemAbove() );
+
+ ThreadManager::instance()->queueJob( new UrlLoader( list, after, options ) );
+ ScriptManager::instance()->notifyPlaylistChange("changed");
+ }
+}
+
+void
+Playlist::insertMediaSql( const QString& 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;
+
+ QPtrList<QListViewItem> list;
+ int y=0;
+ for( QListViewItemIterator it( firstChild() ); y < x ; list.prepend( *it ), ++it, y++ );
+
+ if( list.isEmpty() ) return;
+ if ( saveUndo )
+ saveUndoState();
+
+ //remove the items
+ for( QListViewItem *item = list.first(); item; item = list.next() )
+ {
+ removeItem( static_cast<PlaylistItem*>( 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();
+ }
+ }
+}
+
+QString
+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( QFile::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 <reggie@kde.org>
+ Copyright (C) 2000,2003 Charles Samuels <charles@kde.org>
+ Copyright (C) 2000 Peter Putzer
+*/
+void Playlist::saveLayout(KConfig *config, const QString &group) const
+{
+ KConfigGroupSaver saver(config, group);
+ QStringList names, widths, order;
+
+ const int colCount = columns();
+ QHeader* const thisHeader = header();
+ for (int i = 0; i < colCount; ++i)
+ {
+ names << PlaylistItem::exactColumnName(i);
+ widths << QString::number(columnWidth(i));
+ order << QString::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 QString &group)
+{
+ KConfigGroupSaver saver(config, group);
+ int version = config->readNumEntry("ColumnsVersion", 0);
+
+ QValueList<int> iorder; //internal ordering
+ if( version )
+ {
+ QStringList 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]);
+ }
+
+
+ QStringList cols = config->readListEntry("ColumnWidths");
+ int i = 0;
+ { // scope the iterators
+ QStringList::ConstIterator it = cols.constBegin();
+ const QStringList::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
+ {
+ QStringList::ConstIterator it = cols.constBegin();
+ const QStringList::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 QString uniqueid, PlaylistItem* item )
+{
+ QPtrList<PlaylistItem> *list;
+ if( m_uniqueMap.contains( uniqueid ) )
+ list = m_uniqueMap[uniqueid];
+ else
+ list = new QPtrList<PlaylistItem>();
+ list->append( item );
+ if( !m_uniqueMap.contains( uniqueid ) )
+ m_uniqueMap[uniqueid] = list;
+}
+
+void
+Playlist::removeFromUniqueMap( const QString uniqueid, PlaylistItem* item )
+{
+ if( !m_uniqueMap.contains( uniqueid ) )
+ return;
+
+ QPtrList<PlaylistItem> *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 QString &oldUrl, const QString &newUrl, const QString &uniqueid )
+{
+ // Make sure the MoodServer gets this signal first!
+ MoodServer::instance()->slotFileMoved( oldUrl, newUrl );
+
+ QPtrList<PlaylistItem> *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 QString &/*url*/, const QString &oldid, const QString &newid )
+{
+ QPtrList<PlaylistItem> *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 QString &/*absPath*/, const QString &uniqueid )
+{
+ QPtrList<PlaylistItem> *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 QString &absPath, const QString &uniqueid )
+{
+ QPtrList<PlaylistItem> *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 QMap<QString,QString> &map )
+{
+ QMap<QString,QPtrList<PlaylistItem>*> uniquecopy( m_uniqueMap );
+
+ QMap<QString,QPtrList<PlaylistItem>*>::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( QString::null, 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() )
+ {
+ QValueVector<PlaylistItem*> 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
+ QPtrList<PlaylistAlbum> 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 = QDateTime::currentDateTime().toTime_t();
+ QValueVector<int> weights( tracks.size() );
+ Q_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();
+ }
+
+ Q_INT64 random;
+ if( Amarok::favorLastPlay() ) //really big huge numbers
+ {
+ Q_INT64 r = Q_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&>(--MyIt( item ));
+ while( item && !checkFileStatus( item ) )
+ item = *static_cast<MyIt&>(--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;
+ QPtrList<QListViewItem> 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() )
+ {
+ QListViewItem *item = dynamicList.first();
+ if( m_nextTracks.containsRef( static_cast<PlaylistItem*>(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( QListViewItem *item, bool multi, bool invertQueue )
+{
+ #define item static_cast<PlaylistItem*>(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( QListViewItem *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;
+ QString path = QString::null;
+ 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( QListViewItem *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"),
+ QImage( KIconLoader().iconPath( "amarok", -KIcon::SizeHuge ) ) );
+ return;
+ }
+
+ #define item static_cast<PlaylistItem*>(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
+}
+
+QPair<QString, QRect> Playlist::toolTipText( QWidget*, const QPoint &pos ) const
+{
+ PlaylistItem *item = static_cast<PlaylistItem*>( itemAt( pos ) );
+ if( !item )
+ return QPair<QString, QRect>( QString::null, QRect() );
+
+ const QPoint contentsPos = viewportToContents( pos );
+ const int col = header()->sectionAt( contentsPos.x() );
+
+ if( item == m_renameItem && col == m_renameColumn )
+ return QPair<QString, QRect>( QString::null, QRect() );
+
+ QString text;
+ if( col == PlaylistItem::Rating )
+ text = item->ratingDescription( item->rating() );
+ else
+ text = item->text( col );
+
+ QRect irect = itemRect( item );
+ const int headerPos = header()->sectionPos( col );
+ irect.setLeft( headerPos - 1 );
+ irect.setRight( headerPos + header()->sectionSize( col ) );
+
+ static QFont 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 QPair<QString, QRect>( QString::null, QRect() );
+
+ QRect globalRect( viewport()->mapToGlobal( irect.topLeft() ), irect.size() );
+ QSimpleRichText t( text, font() );
+ int dright = QApplication::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( "&", "&amp;" ).replace( "<", "&lt;" ).replace( ">", "&gt;" );
+ if( item->isCurrent() )
+ {
+ text = QString("<i>%1</i>").arg( text );
+ Amarok::ToolTip::s_hack = 1; //HACK for precise positioning
+ }
+ return QPair<QString, QRect>( text, globalRect );
+ }
+
+ return QPair<QString, QRect>( QString::null, QRect() );
+}
+
+void
+Playlist::activateByIndex( int index )
+{
+ QListViewItem* 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, QPixmap() );
+ }
+
+ 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( QPtrListIterator<PlaylistItem> 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;
+
+ QString pixmap = QString::null;
+
+ 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() ? QPixmap() : 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;
+ QTimer::singleShot( 0, this, 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() ) {
+ QString 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
+ QPixmap 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 QString &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 Qt 3.3.5 bug in QListView::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();
+
+ QListViewItem *c = firstChild();
+ QListViewItem *n;
+ while( c ) {
+ n = c->nextSibling();
+ if ( !static_cast<PlaylistItem *>( 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 Qt 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( QListViewItem *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<PlaylistItem *>(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<PlaylistItem*>(item)->setIsBeingRenamed( true );
+
+}
+
+void
+Playlist::writeTag( QListViewItem *qitem, const QString &, int column ) //SLOT
+{
+ const bool dynamicEnabled = static_cast<PlaylistItem*>(qitem)->isDynamicEnabled();
+
+ if( m_itemsToChangeTagsFor.isEmpty() )
+ m_itemsToChangeTagsFor.append( static_cast<PlaylistItem*>( qitem ) );
+
+ const QString newTag = static_cast<PlaylistItem*>( qitem )->exactText( column );
+
+ for( PlaylistItem *item = m_itemsToChangeTagsFor.first(); item; item = m_itemsToChangeTagsFor.next() )
+ {
+ if( !checkFileStatus( item ) )
+ continue;
+
+ const QString 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<PlaylistItem*>(qitem)->setDynamicEnabled( dynamicEnabled );
+
+ m_itemsToChangeTagsFor.clear();
+ m_editOldTag = QString::null;
+}
+
+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, QPixmap() );
+ setCurrentTrackPixmap();
+ }
+ QResizeEvent e( size(), QSize() );
+ viewportResizeEvent( &e );
+ emit columnsChanged();
+}
+
+void
+Playlist::paletteChange( const QPalette &p )
+{
+ using namespace Glow;
+
+ QColor fg;
+ QColor 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( QDragEnterEvent *e )
+{
+ QString data;
+ QCString subtype;
+ QTextDrag::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( QDragMoveEvent* e )
+{
+ if( !e->isAccepted() ) return;
+
+ #if KDE_IS_VERSION( 3, 3, 91 )
+ const bool ctrlPressed = KApplication::keyboardMouseState() & Qt::ControlButton;
+ #else
+ const bool ctrlPressed = KApplication::keyboardModifiers() & ControlMask;
+ #endif
+
+ //Get the closest item _before_ the cursor
+ const QPoint p = contentsToViewport( e->pos() );
+ QListViewItem *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( QDragLeaveEvent* )
+{
+ slotEraseMarker();
+}
+
+void
+Playlist::contentsDropEvent( QDropEvent *e )
+{
+ DEBUG_BLOCK
+
+ //NOTE parent is always 0 currently, but we support it in case we start using trees
+ QListViewItem *parent = 0;
+ QListViewItem *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<PlaylistItem *>(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<PlaylistItem *>(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 );
+ QPtrList<QListViewItem> items = selectedItems();
+ if( dynamicMode() && after )
+ {
+ QListViewItem *item;
+ bool enabled = static_cast<PlaylistItem *>(after)->isDynamicEnabled();
+ for( item = items.first(); item; item = items.next() )
+ static_cast<PlaylistItem *>(item)->setDynamicEnabled( enabled );
+ }
+ ScriptManager::instance()->notifyPlaylistChange("reordered");
+ }
+
+ else {
+ QString data;
+ QCString subtype;
+ QTextDrag::decode( e, data, subtype );
+
+ debug() << "QTextDrag::subtype(): " << subtype << endl;
+
+ if( subtype == "amarok-sql" ) {
+ setSorting( NO_SORT );
+ QString 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<DynamicEntry*>( data.toULongLong() );
+
+ loadDynamicMode( entry );
+ }
+
+ else if( KURLDrag::canDecode( e ) )
+ {
+ debug() << "KURLDrag::canDecode" << endl;
+
+ KURL::List list;
+ KURLDrag::decode( e, list );
+ insertMediaInternal( list, static_cast<PlaylistItem*>( after ) );
+ }
+ else
+ e->ignore();
+ }
+
+ updateNextPrev();
+}
+
+QDragObject*
+Playlist::dragObject()
+{
+ DEBUG_THREAD_FUNC_INFO
+
+ KURL::List list;
+
+ for( MyIt it( this, MyIt::Selected ); *it; ++it )
+ {
+ const PlaylistItem *item = static_cast<PlaylistItem*>( *it );
+ const KURL url = item->url();
+ list += url;
+ }
+
+ KURLDrag *drag = new KURLDrag( list, viewport() );
+ drag->setPixmap( CollectionDB::createDragPixmap( list ),
+ QPoint( CollectionDB::DRAGPIXMAP_OFFSET_X, CollectionDB::DRAGPIXMAP_OFFSET_Y ) );
+ return drag;
+}
+
+#include <qsimplerichtext.h>
+void
+Playlist::viewportPaintEvent( QPaintEvent *e )
+{
+ if( e ) KListView::viewportPaintEvent( e ); //we call with 0 in contentsDropEvent()
+
+ if ( m_marker ) {
+ QPainter p( viewport() );
+ p.fillRect(
+ drawDropVisualizer( 0, 0, m_marker ),
+ QBrush( colorGroup().highlight().dark(), QBrush::Dense4Pattern ) );
+ }
+ else if( m_showHelp && isEmpty() ) {
+ QPainter p( viewport() );
+ QString minimumText(i18n(
+ "<div align=center>"
+ "<h3>The Playlist</h3>"
+ "This is the playlist. "
+ "To create a listing, "
+ "<b>drag</b> tracks from the browser-panels on the left, "
+ "<b>drop</b> them here and then <b>double-click</b> them to start playback."
+ "</div>" ) );
+ QSimpleRichText *t = new QSimpleRichText( minimumText +
+ i18n( "<div align=center>"
+ "<h3>The Browsers</h3>"
+ "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. "
+ "</div>" ), QApplication::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 QSimpleRichText( minimumText, QApplication::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, QRect(), colorGroup() );
+ delete t;
+ }
+}
+
+static uint negativeWidth = 0;
+
+void
+Playlist::viewportResizeEvent( QResizeEvent *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
+ QResizeEvent e( size(), QSize() );
+ viewportResizeEvent( &e );
+ emit columnsChanged();
+ }
+}
+
+bool
+Playlist::eventFilter( QObject *o, QEvent *e )
+{
+ #define me static_cast<QMouseEvent*>(e)
+ #define ke static_cast<QKeyEvent*>(e)
+
+ if( o == header() && e->type() == QEvent::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( static_cast<QMouseEvent *>(e)->globalPos() );
+
+ switch( col ) {
+ case HIDE:
+ {
+ hideColumn( mouseOverColumn );
+ QResizeEvent e( size(), QSize() );
+ 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( o == viewport() && e->type() == QEvent::MouseButtonPress && me->state() == Qt::ControlButton && me->button() == RightButton )
+ {
+ PlaylistItem *item = static_cast<PlaylistItem*>( itemAt( me->pos() ) );
+
+ if( !item )
+ return true;
+
+ item->isSelected() ?
+ queueSelected():
+ queue( item );
+
+ return true; //yum!
+ }
+
+ // trigger in-place tag editing
+ else if( o == viewport() && e->type() == QEvent::MouseButtonPress && me->button() == LeftButton )
+ {
+ m_clicktimer->stop();
+ m_itemToRename = 0;
+ int col = header()->sectionAt( viewportToContents( me->pos() ).x() );
+ if( col != PlaylistItem::Rating )
+ {
+ PlaylistItem *item = static_cast<PlaylistItem*>( itemAt( me->pos() ) );
+ bool edit = item
+ && item->isSelected()
+ && selectedItems().count()==1
+ && (me->state() & ~LeftButton) == 0
+ && item->url().isLocalFile();
+ if( edit )
+ {
+ m_clickPos = me->pos();
+ m_itemToRename = item;
+ m_columnToRename = col;
+ //return true;
+ }
+ }
+ }
+
+ else if( o == viewport() && e->type() == QEvent::MouseButtonRelease && me->button() == LeftButton )
+ {
+ int col = header()->sectionAt( viewportToContents( me->pos() ).x() );
+ if( col != PlaylistItem::Rating )
+ {
+ PlaylistItem *item = static_cast<PlaylistItem*>( itemAt( me->pos() ) );
+ if( item == m_itemToRename && me->pos() == m_clickPos )
+ {
+ m_clicktimer->start( int( QApplication::doubleClickInterval() ), true );
+ return true;
+ }
+ else
+ {
+ m_itemToRename = 0;
+ }
+ }
+ }
+
+ // avoid in-place tag editing upon double-clicks
+ else if( e->type() == QEvent::MouseButtonDblClick && me->button() == Qt::LeftButton )
+ {
+ m_itemToRename = 0;
+ m_clicktimer->stop();
+ }
+
+ // Toggle play/pause if user middle-clicks on current track
+ else if( o == viewport() && e->type() == QEvent::MouseButtonPress && me->button() == MidButton )
+ {
+ PlaylistItem *item = static_cast<PlaylistItem*>( itemAt( me->pos() ) );
+
+ if( item && item == m_currentTrack )
+ {
+ EngineController::instance()->playPause();
+ return true; //yum!
+ }
+ }
+
+ else if( o == renameLineEdit() && e->type() == 6 /*QEvent::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;
+ QListViewItem *item = m_renameItem;
+
+ if( ke->state() & Qt::AltButton )
+ {
+ if( ke->key() == Qt::Key_Up && m_visCount > 1 )
+ if( !( item = m_renameItem->itemAbove() ) )
+ {
+ item = *MyIt( this, MyIt::Visible );
+ while( item->itemBelow() )
+ item = item->itemBelow();
+ }
+ if( ke->key() == Qt::Key_Down && m_visCount > 1 )
+ if( !( item = m_renameItem->itemBelow() ) )
+ item = *MyIt( this, MyIt::Visible );
+ if( ke->key() == Qt::Key_Left )
+ do
+ {
+ if( physicalColumn == 0 )
+ physicalColumn = visibleCols - 1;
+ else
+ physicalColumn--;
+ column = mapToLogicalColumn( physicalColumn );
+ } while( !isRenameable( column ) );
+ if( ke->key() == Qt::Key_Right )
+ do
+ {
+ if( physicalColumn == visibleCols - 1 )
+ physicalColumn = 0;
+ else
+ physicalColumn++;
+ column = mapToLogicalColumn( physicalColumn );
+ } while( !isRenameable( column ) );
+ }
+
+ if( ke->key() == Qt::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() == Qt::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( o == renameLineEdit() && ( e->type() == QEvent::Hide || e->type() == QEvent::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( QCustomEvent *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<PlaylistItem *>( after->itemBelow() );
+
+ if( after )
+ {
+ PlaylistItem *prev = static_cast<PlaylistItem *>( 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<PlaylistItem *>( 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 QString &path, bool relative ) const
+{
+ QValueList<KURL> urls;
+ QValueList<QString> titles;
+ QValueList<int> 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 QString &path )
+{
+ DEBUG_BLOCK
+
+ QFile file( path );
+ if( !file.open( IO_WriteOnly | IO_Truncate | IO_Raw ) ) return;
+
+ // Manual buffering since QFile's is slow for whatever reason
+ const uint kWriteSize = 256 * 1024;
+
+ QBuffer buffer;
+ buffer.open(IO_WriteOnly);
+
+ QTextStream stream( &buffer );
+ stream.setEncoding( QTextStream::UnicodeUTF8 );
+ stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
+
+ QString dynamic;
+ if( dynamicMode() )
+ {
+ const QString title = ( dynamicMode()->title() ).replace( "&", "&amp;" )
+ .replace( "<", "&lt;" )
+ .replace( ">", "&gt;" );
+ dynamic = QString(" dynamicMode=\"%1\"").arg( title );
+ }
+ stream << QString( "<playlist product=\"%1\" version=\"%2\"%3>\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
+
+ QStringList attributes;
+ const int queueIndex = m_nextTracks.findRef( item );
+ if ( queueIndex != -1 )
+ attributes << "queue_index" << QString::number( queueIndex + 1 );
+ else if ( item == currentTrack() )
+ attributes << "queue_index" << QString::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 << "</playlist>\n";
+ file.writeBlock(buffer.buffer().data(), buffer.at());
+ file.close();
+}
+
+void
+Playlist::burnPlaylist( int projectType )
+{
+ KURL::List list;
+
+ QListViewItemIterator it( this );
+ for( ; it.current(); ++it ) {
+ PlaylistItem *item = static_cast<PlaylistItem*>(*it);
+ KURL url = item->url();
+ if( url.isLocalFile() )
+ list << url;
+ }
+
+ K3bExporter::instance()->exportTracks( list, projectType );
+}
+
+void
+Playlist::burnSelectedTracks( int projectType )
+{
+ KURL::List list;
+
+ QListViewItemIterator it( this, QListViewItemIterator::Selected );
+ for( ; it.current(); ++it ) {
+ PlaylistItem *item = static_cast<PlaylistItem*>(*it);
+ KURL url = item->url();
+ if( url.isLocalFile() )
+ list << url;
+ }
+
+ K3bExporter::instance()->exportTracks( list, projectType );
+}
+
+void
+Playlist::addCustomMenuItem( const QString &submenu, const QString &itemTitle ) //for dcop
+{
+ m_customSubmenuItem[submenu] << itemTitle;
+}
+
+bool
+Playlist::removeCustomMenuItem( const QString &submenu, const QString &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
+{
+ QString message = m_customIdItem[id];
+ QListViewItemIterator it( this, QListViewItemIterator::Selected );
+ for( ; it.current(); ++it ) {
+ PlaylistItem *item = static_cast<PlaylistItem*>(*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<KSelectAction*>(Amarok::actionCollection()->action( "random_mode" ))->setCurrentItem( m_oldRandom );
+ static_cast<KSelectAction*>(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 );
+ QPtrList<QListViewItem> list;
+
+ for( ; *it; ++it )
+ {
+ PlaylistItem *item = static_cast<PlaylistItem *>(*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( QListViewItem *item = list.first(); item; item = list.next() )
+ {
+ removeItem( static_cast<PlaylistItem*>( 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;
+
+ QPtrList<QListViewItem> 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( QListViewItem *item = list.first(); item; item = list.next() )
+ takeItem( item );
+
+ //shuffle
+ KRandomSequence( (long)KApplication::random() ).randomize( &list );
+
+ //reinsert in new order
+ for( QListViewItem *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( QListViewItem *item = queued.first(); item; item = queued.next() )
+ removeItem( static_cast<PlaylistItem*>( item ), true );
+
+ emit queueChanged( PLItemList(), queued );
+
+ for( QListViewItem *item = queued.first(); item; item = queued.next() )
+ delete item;
+ }
+
+ for( QListViewItem *item = list.first(); item; item = list.next() )
+ {
+ removeItem( static_cast<PlaylistItem*>( 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<PlaylistItem*>( *it )->url(), ++it );
+ if( DeleteDialog::showTrashDialog(this, urls) )
+ {
+ CollectionDB::instance()->removeSongs( urls );
+ removeSelectedItems();
+ foreachType( KURL::List, urls )
+ CollectionDB::instance()->emitFileDeleted( (*it).path() );
+ QTimer::singleShot( 0, CollectionView::instance(), SLOT( renderView() ) );
+ }
+}
+
+void
+Playlist::removeDuplicates() //SLOT
+{
+ // Remove dead entries:
+
+ for( QListViewItemIterator it( this ); it.current(); ) {
+ PlaylistItem* item = static_cast<PlaylistItem*>( *it );
+ const KURL url = item->url();
+ if ( url.isLocalFile() && !QFile::exists( url.path() ) ) {
+ removeItem( item );
+ ++it;
+ delete item;
+ }
+ else ++it;
+ }
+
+ // Remove dupes:
+
+ QSortedList<PlaylistItem> list;
+ for( QListViewItemIterator it( this ); it.current(); ++it )
+ list.prepend( static_cast<PlaylistItem*>( it.current() ) );
+
+ list.sort();
+
+ QPtrListIterator<PlaylistItem> 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 QListViewItem *item ) const //SLOT
+{
+ if( !item ) item = currentTrack();
+
+ if( item )
+ {
+ const PlaylistItem* playlistItem = static_cast<const PlaylistItem*>( item );
+
+ QString 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
+ QApplication::clipboard()->setText( text, QClipboard::Clipboard );
+ QApplication::clipboard()->setText( text, QClipboard::Selection );
+
+ Amarok::OSD::instance()->OSDWidget::show( i18n( "Copied: %1" ).arg( text ),
+ QImage(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() == QDialog::Accepted )
+ {
+ changeFromQueueManager(dialog.newQueue());
+ }
+}
+
+void
+Playlist::changeFromQueueManager(QPtrList<PlaylistItem> 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 QString &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 QString &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 );
+ QValueList<int> visible = visibleColumns();
+ for(; *it; ++it )
+ (*it)->setVisible( (*it)->matchesParsedExpression( parsed, visible ) );
+ }
+ else {
+ // optimized path
+ const QStringList terms = QStringList::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 QString &path, float score )
+{
+ for( MyIt it( this, MyIt::All ); *it; ++it )
+ {
+ PlaylistItem *item = static_cast<PlaylistItem*>( *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 QString &path, int rating )
+{
+ for( MyIt it( this, MyIt::All ); *it; ++it )
+ {
+ PlaylistItem *item = static_cast<PlaylistItem*>( *it );
+ if ( item->url().path() == path )
+ {
+ item->setRating( rating );
+ item->filter( m_filter );
+ }
+ }
+}
+
+void
+Playlist::fileMoved( const QString &srcPath, const QString &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<PlaylistItem*>( *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( QListViewItem *item, const QPoint &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<PlaylistItem*>(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 QString tagName = columnText( col );
+ const QString 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<KToggleAction *>( 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, 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, 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();
+ QValueList<QString> submenuTexts = m_customSubmenuItem.keys();
+ for( QValueList<QString>::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 QPoint 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 QListView 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
+ {
+ QString 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 = QString::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<KSelectAction*>( 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( QListViewItemIterator it( this, QListViewItemIterator::Selected );
+ it.current();
+ ++it )
+ {
+ PlaylistItem *i= static_cast<PlaylistItem*>(*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 QFont &old )
+{
+ KListView::fontChange( old );
+ initStarPixmaps();
+ triggerUpdate();
+}
+
+void
+Playlist::contentsMouseMoveEvent( QMouseEvent *e )
+{
+ if( e )
+ KListView::contentsMouseMoveEvent( e );
+ PlaylistItem *prev = m_hoveredRating;
+ const QPoint pos = e ? e->pos() : viewportToContents( viewport()->mapFromGlobal( QCursor::pos() ) );
+
+ PlaylistItem *item = static_cast<PlaylistItem*>( 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() )
+ QScrollView::updateContents( header()->sectionPos( PlaylistItem::Rating ) + 1, contentsY(),
+ header()->sectionSize( PlaylistItem::Rating ) - 2, visibleHeight() );
+ else
+ prev->updateColumn( PlaylistItem::Rating );
+ }
+}
+
+void Playlist::leaveEvent( QEvent *e )
+{
+ KListView::leaveEvent( e );
+
+ PlaylistItem *prev = m_hoveredRating;
+ m_hoveredRating = 0;
+ if( prev )
+ prev->updateColumn( PlaylistItem::Rating );
+}
+
+void Playlist::contentsMousePressEvent( QMouseEvent *e )
+{
+ PlaylistItem *item = static_cast<PlaylistItem*>( 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() & Qt::ControlButton || e->state() & Qt::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( QWheelEvent *e )
+{
+ PlaylistItem* const item = static_cast<PlaylistItem*>( 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( QString::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;
+}
+
+QValueList<int> Playlist::visibleColumns() const
+{
+ QValueList<int> 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( QValueList<int> order, QValueList<int> 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 QListView::takeItem() and QListViewItem::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( QListViewItem *item )
+{
+ if( !item )
+ return;
+
+ //HACK -- apparently the various metrics aren't reliable while the UI is still updating & stuff
+ m_itemToReallyCenter = item;
+ QTimer::singleShot( 0, this, SLOT( reallyEnsureItemCentered() ) );
+}
+
+void
+Playlist::reallyEnsureItemCentered()
+{
+ if( QListViewItem *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( QStringList &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 <mxcl>
+ if( !isEmpty() )
+ {
+ QString 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<PlaylistItem*>( firstChild() );
+ while( item ) {
+ item->setIsNew( false );
+ item = item->nextSibling();
+ }
+ triggerUpdate();
+
+ return true;
+ }
+
+ return false;
+}
+
+void
+Playlist::switchState( QStringList &loadFromMe, QStringList &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 QString 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;
+ }
+ QString path = PlaylistDialog::getSaveFileName( suggestion == 1 ? album
+ : suggestion == 2 ? artist
+ : i18n( "Untitled" ) );
+
+ if( path.isEmpty() )
+ return;
+
+ QValueList<KURL> urls;
+ QValueList<QString> titles;
+ QValueList<int> 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, QListViewItem *after, const QPoint &p, int col ) //SLOT
+{
+ switch( button )
+ {
+ case Qt::MidButton:
+ {
+ const QString path = QApplication::clipboard()->text( QClipboard::Selection );
+ const KURL url = KURL::fromPathOrURL( path );
+
+ if( url.isValid() )
+ insertMediaInternal( url, static_cast<PlaylistItem*>(after ? after : lastItem()) );
+
+ break;
+ }
+
+ case Qt::RightButton:
+ showContextMenu( after, p, col );
+ break;
+
+ default:
+ ;
+ }
+}
+
+void Playlist::slotContentsMoving()
+{
+ Amarok::ToolTip::hideTips();
+ QTimer::singleShot( 0, this, SLOT( contentsMouseMoveEvent() ) );
+}
+
+void
+Playlist::slotQueueChanged( const PLItemList &/*in*/, const PLItemList &out)
+{
+ for( QPtrListIterator<PlaylistItem> 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 = QColor( r, g, b );
+ }
+
+ {
+ using namespace Text;
+ PlaylistItem::glowText = QColor( 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 QRect spot = drawDropVisualizer( 0, 0, m_marker );
+ m_marker = 0;
+ viewport()->repaint( spot, false );
+ }
+}
+
+void
+Playlist::showTagDialog( QPtrList<QListViewItem> 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<PlaylistItem*>( 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( QListViewItem *item = items.first(); item; item = items.next() )
+ if ( item->isVisible() )
+ urls << static_cast<PlaylistItem*>( item )->url();
+
+ TagDialog *dialog = new TagDialog( urls, instance() );
+ dialog->show();
+ }
+
+ // Playlist::unlock();
+}
+
+#include <kactivelabel.h>
+#include <kdialog.h>
+#include <kpushbutton.h>
+#include <qgroupbox.h>
+#include <qlabel.h>
+#include <qlayout.h>
+#include <qprocess.h>
+#include <unistd.h> //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( QWidget *parent )
+ : KDialog( parent )
+ {
+ QLabel *textLabel1, *textLabel2, *textLabel3;
+ QLineEdit *lineEdit1, *lineEdit2;
+ QGroupBox *groupBox1;
+
+ textLabel1 = new QLabel( i18n(
+ "<p>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 <b>nobody</b>, this is for security reasons.\n"
+ "<p>You can only run the command against local files for the time being. "
+ "The fullpath is inserted at the position <b>%f</b> in the string. "
+ "If you do not specify <b>%f</b> it is appended." ), this );
+ textLabel2 = new QLabel( i18n( "Column &name:" ), this );
+ textLabel3 = new QLabel( i18n( "&Command:" ), this );
+
+ lineEdit1 = new QLineEdit( this, "ColumnName" );
+ lineEdit2 = new QLineEdit( this, "Command" );
+
+ groupBox1 = new QGroupBox( 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
+ QHBoxLayout *layout1 = new QHBoxLayout( 0, 0, 6 );
+ layout1->addItem( new QSpacerItem( 181, 20, QSizePolicy::Expanding, QSizePolicy::Minimum ) );
+ layout1->addWidget( new KPushButton( KStdGuiItem::ok(), this, "OkButton" ) );
+ layout1->addWidget( new KPushButton( KStdGuiItem::cancel(), this, "CancelButton" ) );
+
+ QGridLayout *layout2 = new QGridLayout( 0, 2, 2, 0, 6 );
+ layout2->QLayout::add( textLabel2 );
+ layout2->QLayout::add( lineEdit1 );
+ layout2->QLayout::add( textLabel3 );
+ layout2->QLayout::add( lineEdit2 );
+
+ QVBoxLayout *Form1Layout = new QVBoxLayout( this, 11, 6, "Form1Layout");
+ Form1Layout->addWidget( textLabel1 );
+ Form1Layout->addWidget( groupBox1 );
+ Form1Layout->addLayout( layout2 );
+ Form1Layout->addLayout( layout1 );
+ Form1Layout->addItem( new QSpacerItem( 20, 231, QSizePolicy::Minimum, QSizePolicy::Expanding ) );
+
+ // properties
+ setCaption( i18n("Add Custom Column") );
+
+ // connects
+ connect(
+ child( "OkButton" ),
+ SIGNAL(clicked()),
+ SLOT(accept()) );
+ connect(
+ child( "CancelButton" ),
+ SIGNAL(clicked()),
+ SLOT(reject()) );
+ }
+
+ QString command() { return static_cast<KLineEdit*>(child("Command"))->text(); }
+ QString name() { return static_cast<KLineEdit*>(child("ColumnName"))->text(); }
+ };
+
+void
+Playlist::addCustomColumn()
+{
+ CustomColumnDialog dialog( this );
+
+ if ( dialog.exec() == QDialog::Accepted ) {
+ const int index = addColumn( dialog.name(), 100 );
+ QStringList args = QStringList::split( ' ', dialog.command() );
+
+ QStringList::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;
+
+ QProcess p( args );
+ for( p.start(); p.isRunning(); /*kapp->processEvents()*/ )
+ ::usleep( 5000 );
+
+ (*it)->setExactText( index, p.readStdout() );
+ }
+ }
+}
+
+#include <taglib/fileref.h>
+#include <taglib/tag.h>
+
+TagWriter::TagWriter( PlaylistItem *item, const QString &oldTag, const QString &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"