You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4982 lines
156 KiB
4982 lines
156 KiB
/* 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 TQOBJECT but it's private so you can only connect via PlaylistWindow::PlaylistWindow
|
|
* Mostly it's sensible to implement playlist functionality in this class
|
|
* TODO Obtaining information about the playlist is currently hard, we need the playlist to be globally
|
|
* available and have some more useful public functions
|
|
*/
|
|
|
|
#define DEBUG_PREFIX "Playlist"
|
|
|
|
#include <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 <tqbuffer.h>
|
|
#include <tqclipboard.h> //copyToClipboard(), slotMouseButtonPressed()
|
|
#include <tqcolor.h>
|
|
#include <tqevent.h>
|
|
#include <tqfile.h> //undo system
|
|
#include <tqheader.h> //eventFilter()
|
|
#include <tqlabel.h> //showUsageMessage()
|
|
#include <tqpainter.h>
|
|
#include <tqpen.h> //slotGlowTimer()
|
|
#include <tqsimplerichtext.h> //toolTipText()
|
|
#include <tqtimer.h>
|
|
#include <tqtooltip.h>
|
|
#include <tqvaluelist.h> //addHybridTracks()
|
|
#include <tqvaluevector.h> //playNextTrack()
|
|
#include <tqlayout.h>
|
|
|
|
#include <tdeaction.h>
|
|
#include <tdeapplication.h>
|
|
#include <kcursor.h> //setOverrideCursor()
|
|
#include <kdialogbase.h>
|
|
#include <tdeglobalsettings.h> //rename()
|
|
#include <kiconeffect.h>
|
|
#include <kiconloader.h> //slotShowContextMenu()
|
|
#include <tdeio/job.h> //deleteSelectedFiles()
|
|
#include <klineedit.h> //setCurrentTrack()
|
|
#include <tdelocale.h>
|
|
#include <tdemessagebox.h>
|
|
#include <tdepopupmenu.h>
|
|
#include <krandomsequence.h> //random Mode
|
|
#include <kstandarddirs.h> //TDEGlobal::dirs()
|
|
#include <kstdaction.h>
|
|
#include <kstringhandler.h> //::showContextMenu()
|
|
#include <kurldrag.h>
|
|
|
|
#include <cstdlib> // abs
|
|
|
|
extern "C"
|
|
{
|
|
#if TDE_VERSION < TDE_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 TQString &oldTag, const TQString &newTag, const int, const bool updateView = true );
|
|
~TagWriter();
|
|
bool doJob();
|
|
void completeJob();
|
|
private:
|
|
PlaylistItem* const m_item;
|
|
bool m_failed;
|
|
|
|
TQString m_oldTagString;
|
|
TQString m_newTagString;
|
|
int m_tagType;
|
|
bool m_updateView;
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
/// Glow
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
namespace Glow
|
|
{
|
|
namespace Text
|
|
{
|
|
static float dr, dg, db;
|
|
static int r, g, b;
|
|
}
|
|
namespace Base
|
|
{
|
|
static float dr, dg, db;
|
|
static int r, g, b;
|
|
}
|
|
|
|
static const uint STEPS = 13;
|
|
static uint counter;
|
|
static TQTimer timer;
|
|
|
|
inline void startTimer()
|
|
{
|
|
counter = 0;
|
|
timer.start( 40 );
|
|
}
|
|
|
|
inline void reset()
|
|
{
|
|
counter = 0;
|
|
timer.stop();
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
/// CLASS Playlist
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
TQMutex* Playlist::s_dynamicADTMutex = new TQMutex();
|
|
Playlist *Playlist::s_instance = 0;
|
|
|
|
Playlist::Playlist( TQWidget *parent )
|
|
: TDEListView( parent, "ThePlaylist" )
|
|
, EngineObserver( EngineController::instance() )
|
|
, m_startupTime_t( TQDateTime::currentDateTime().toTime_t() )
|
|
, m_oldestTime_t( CollectionDB::instance()->query( "SELECT MIN( createdate ) FROM statistics;" ).first().toInt() )
|
|
, m_currentTrack( 0 )
|
|
, m_marker( 0 )
|
|
, m_hoveredRating( 0 )
|
|
, m_firstColumn( 0 )
|
|
, m_totalCount( 0 )
|
|
, m_totalLength( 0 )
|
|
, m_selCount( 0 )
|
|
, m_selLength( 0 )
|
|
, m_visCount( 0 )
|
|
, m_visLength( 0 )
|
|
, m_total( 0 )
|
|
, m_itemCountDirty( false )
|
|
, m_undoButton( 0 )
|
|
, m_redoButton( 0 )
|
|
, m_clearButton( 0 )
|
|
, m_undoDir( Amarok::saveLocation( "undo/" ) )
|
|
, m_undoCounter( 0 )
|
|
, m_dynamicMode( 0 )
|
|
, m_stopAfterTrack( 0 )
|
|
, m_stopAfterMode( DoNotStop )
|
|
, m_showHelp( true )
|
|
, m_dynamicDirt( false )
|
|
, m_queueDirt( false )
|
|
, m_undoDirt( false )
|
|
, m_insertFromADT( 0 )
|
|
, m_itemToReallyCenter( 0 )
|
|
, m_renameItem( 0 )
|
|
, m_lockStack( 0 )
|
|
, m_columnFraction( PlaylistItem::NUM_COLUMNS, 0 )
|
|
, m_oldRandom( 0 )
|
|
, m_oldRepeat( 0 )
|
|
, m_playlistName( i18n( "Untitled" ) )
|
|
, m_proposeOverwriting( false )
|
|
, m_urlIndex( &PlaylistItem::url )
|
|
|
|
{
|
|
s_instance = this;
|
|
|
|
connect( CollectionDB::instance(), TQT_SIGNAL(fileMoved(const TQString&,
|
|
const TQString&, const TQString&)), TQT_SLOT(updateEntriesUrl(const TQString&,
|
|
const TQString&, const TQString&)) );
|
|
connect( CollectionDB::instance(), TQT_SIGNAL(uniqueIdChanged(const TQString&,
|
|
const TQString&, const TQString&)), TQT_SLOT(updateEntriesUniqueId(const TQString&,
|
|
const TQString&, const TQString&)) );
|
|
connect( CollectionDB::instance(), TQT_SIGNAL(fileDeleted(const TQString&,
|
|
const TQString&)), TQT_SLOT(updateEntriesStatusDeleted(const TQString&, const TQString&)) );
|
|
connect( CollectionDB::instance(), TQT_SIGNAL(fileAdded(const TQString&,
|
|
const TQString&)), TQT_SLOT(updateEntriesStatusAdded(const TQString&, const TQString&)) );
|
|
connect( CollectionDB::instance(), TQT_SIGNAL(filesAdded(const TQMap<TQString,TQString>&)),
|
|
TQT_SLOT(updateEntriesStatusAdded(const TQMap<TQString,TQString>&)) );
|
|
|
|
|
|
initStarPixmaps();
|
|
|
|
EngineController* const ec = EngineController::instance();
|
|
connect( ec, TQT_SIGNAL(orderPrevious()), TQT_SLOT(playPrevTrack()) );
|
|
connect( ec, TQT_SIGNAL(orderNext( const bool )), TQT_SLOT(playNextTrack( const bool )) );
|
|
connect( ec, TQT_SIGNAL(orderCurrent()), TQT_SLOT(playCurrentTrack()) );
|
|
|
|
connect( this, TQT_SIGNAL( itemCountChanged( int, int, int, int, int, int ) ), ec, TQT_SLOT( playlistChanged() ) );
|
|
|
|
|
|
setShowSortIndicator( true );
|
|
setDropVisualizer( false ); //we handle the drawing for ourselves
|
|
setDropVisualizerWidth( 3 );
|
|
|
|
// FIXME: This doesn't work, and steals focus when an item is clicked twice.
|
|
//setItemsRenameable( true );
|
|
|
|
setAcceptDrops( true );
|
|
setSelectionMode( TQListView::Extended );
|
|
setAllColumnsShowFocus( true );
|
|
//setItemMargin( 1 ); //aesthetics
|
|
|
|
setMouseTracking( true );
|
|
|
|
#if KDE_IS_VERSION( 3, 3, 91 )
|
|
setShadeSortColumn( true );
|
|
#endif
|
|
|
|
for( int i = 0; i < MetaBundle::NUM_COLUMNS; ++i )
|
|
{
|
|
addColumn( PlaylistItem::prettyColumnName( i ), 0 );
|
|
switch( i )
|
|
{
|
|
case PlaylistItem::Title:
|
|
case PlaylistItem::Artist:
|
|
case PlaylistItem::Composer:
|
|
case PlaylistItem::Year:
|
|
case PlaylistItem::Album:
|
|
case PlaylistItem::DiscNumber:
|
|
case PlaylistItem::Track:
|
|
case PlaylistItem::Bpm:
|
|
case PlaylistItem::Genre:
|
|
case PlaylistItem::Comment:
|
|
case PlaylistItem::Score:
|
|
case PlaylistItem::Rating:
|
|
setRenameable( i, true );
|
|
continue;
|
|
default:
|
|
setRenameable( i, false );
|
|
}
|
|
}
|
|
|
|
setColumnWidth( PlaylistItem::Title, 200 );
|
|
setColumnWidth( PlaylistItem::Artist, 100 );
|
|
setColumnWidth( PlaylistItem::Album, 100 );
|
|
setColumnWidth( PlaylistItem::Length, 80 );
|
|
if( AmarokConfig::showMoodbar() )
|
|
setColumnWidth( PlaylistItem::Mood, 120 );
|
|
if( AmarokConfig::useRatings() )
|
|
setColumnWidth( PlaylistItem::Rating, PlaylistItem::ratingColumnWidth() );
|
|
|
|
setColumnAlignment( PlaylistItem::Length, TQt::AlignRight );
|
|
setColumnAlignment( PlaylistItem::Track, TQt::AlignCenter );
|
|
setColumnAlignment( PlaylistItem::DiscNumber, TQt::AlignCenter );
|
|
setColumnAlignment( PlaylistItem::Bpm, TQt::AlignRight );
|
|
setColumnAlignment( PlaylistItem::Year, TQt::AlignCenter );
|
|
setColumnAlignment( PlaylistItem::Bitrate, TQt::AlignCenter );
|
|
setColumnAlignment( PlaylistItem::SampleRate, TQt::AlignCenter );
|
|
setColumnAlignment( PlaylistItem::Filesize, TQt::AlignCenter );
|
|
setColumnAlignment( PlaylistItem::Score, TQt::AlignCenter );
|
|
setColumnAlignment( PlaylistItem::Type, TQt::AlignCenter );
|
|
setColumnAlignment( PlaylistItem::PlayCount, TQt::AlignCenter );
|
|
|
|
|
|
connect( this, TQT_SIGNAL( doubleClicked( TQListViewItem* ) ),
|
|
this, TQT_SLOT( doubleClicked( TQListViewItem* ) ) );
|
|
connect( this, TQT_SIGNAL( returnPressed( TQListViewItem* ) ),
|
|
this, TQT_SLOT( activate( TQListViewItem* ) ) );
|
|
connect( this, TQT_SIGNAL( mouseButtonPressed( int, TQListViewItem*, const TQPoint&, int ) ),
|
|
this, TQT_SLOT( slotMouseButtonPressed( int, TQListViewItem*, const TQPoint&, int ) ) );
|
|
connect( this, TQT_SIGNAL( queueChanged( const PLItemList &, const PLItemList & ) ),
|
|
this, TQT_SLOT( slotQueueChanged( const PLItemList &, const PLItemList & ) ) );
|
|
connect( this, TQT_SIGNAL( itemRenamed( TQListViewItem*, const TQString&, int ) ),
|
|
this, TQT_SLOT( writeTag( TQListViewItem*, const TQString&, int ) ) );
|
|
connect( this, TQT_SIGNAL( aboutToClear() ),
|
|
this, TQT_SLOT( saveUndoState() ) );
|
|
connect( CollectionDB::instance(), TQT_SIGNAL( scoreChanged( const TQString&, float ) ),
|
|
this, TQT_SLOT( scoreChanged( const TQString&, float ) ) );
|
|
connect( CollectionDB::instance(), TQT_SIGNAL( ratingChanged( const TQString&, int ) ),
|
|
this, TQT_SLOT( ratingChanged( const TQString&, int ) ) );
|
|
connect( CollectionDB::instance(), TQT_SIGNAL( fileMoved( const TQString&, const TQString& ) ),
|
|
this, TQT_SLOT( fileMoved( const TQString&, const TQString& ) ) );
|
|
connect( header(), TQT_SIGNAL( indexChange( int, int, int ) ),
|
|
this, TQT_SLOT( columnOrderChanged() ) ),
|
|
|
|
|
|
connect( &Glow::timer, TQT_SIGNAL(timeout()), TQT_SLOT(slotGlowTimer()) );
|
|
|
|
|
|
TDEActionCollection* const ac = Amarok::actionCollection();
|
|
TDEAction *copy = KStdAction::copy( TQT_TQOBJECT(this), TQT_SLOT( copyToClipboard() ), ac, "playlist_copy" );
|
|
KStdAction::selectAll( TQT_TQOBJECT(this), TQT_SLOT( selectAll() ), ac, "playlist_select_all" );
|
|
|
|
m_clearButton = new TDEAction( i18n( "clear playlist", "&Clear" ), Amarok::icon( "playlist_clear" ), 0, TQT_TQOBJECT(this), TQT_SLOT( clear() ), ac, "playlist_clear" );
|
|
m_undoButton = KStdAction::undo( TQT_TQOBJECT(this), TQT_SLOT( undo() ), ac, "playlist_undo" );
|
|
m_redoButton = KStdAction::redo( TQT_TQOBJECT(this), TQT_SLOT( redo() ), ac, "playlist_redo" );
|
|
m_undoButton ->setIcon( Amarok::icon( "undo" ) );
|
|
m_redoButton ->setIcon( Amarok::icon( "redo" ) );
|
|
|
|
new TDEAction( i18n( "&Repopulate" ), Amarok::icon( "playlist_refresh" ), 0, TQT_TQOBJECT(this), TQT_SLOT( repopulate() ), ac, "repopulate" );
|
|
new TDEAction( i18n( "S&huffle" ), "rebuild", CTRL+Key_H, TQT_TQOBJECT(this), TQT_SLOT( shuffle() ), ac, "playlist_shuffle" );
|
|
TDEAction *gotoCurrent = new TDEAction( i18n( "&Go To Current Track" ), Amarok::icon( "music" ), CTRL+Key_J, TQT_TQOBJECT(this), TQT_SLOT( showCurrentTrack() ), ac, "playlist_show" );
|
|
new TDEAction( i18n( "&Remove Duplicate && Dead Entries" ), 0, TQT_TQOBJECT(this), TQT_SLOT( removeDuplicates() ), ac, "playlist_remove_duplicates" );
|
|
new TDEAction( i18n( "&Queue Selected Tracks" ), Amarok::icon( "queue_track" ), CTRL+Key_D, TQT_TQOBJECT(this), TQT_SLOT( queueSelected() ), ac, "queue_selected" );
|
|
TDEToggleAction *stopafter = new TDEToggleAction( i18n( "&Stop Playing After Track" ), Amarok::icon( "stop" ), CTRL+ALT+Key_V,
|
|
TQT_TQOBJECT(this), TQT_SLOT( toggleStopAfterCurrentItem() ), ac, "stop_after" );
|
|
|
|
{ // TDEAction idiocy -- shortcuts don't work until they've been plugged into a menu
|
|
TDEPopupMenu asdf;
|
|
|
|
copy->plug( &asdf );
|
|
stopafter->plug( &asdf );
|
|
gotoCurrent->plug( &asdf );
|
|
|
|
copy->unplug( &asdf );
|
|
stopafter->unplug( &asdf );
|
|
gotoCurrent->unplug( &asdf );
|
|
}
|
|
|
|
//ensure we update action enabled states when repeat Playlist is toggled
|
|
connect( ac->action( "repeat" ), TQT_SIGNAL(activated( int )), TQT_SLOT(updateNextPrev()) );
|
|
connect( ac->action( "repeat" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) );
|
|
connect( ac->action( "favor_tracks" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) );
|
|
connect( ac->action( "random_mode" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) );
|
|
|
|
|
|
// undostates are written in chronological order, so this is a clever way to get them back in the correct order :)
|
|
TQStringList undos = m_undoDir.entryList( TQString("*.xml"), TQDir::Files, TQDir::Time );
|
|
|
|
foreach( undos )
|
|
m_undoList.append( m_undoDir.absPath() + '/' + (*it) );
|
|
|
|
m_undoCounter = m_undoList.count();
|
|
|
|
m_undoButton->setEnabled( !m_undoList.isEmpty() );
|
|
m_redoButton->setEnabled( false );
|
|
|
|
engineStateChanged( EngineController::engine()->state() ); //initialise state of UI
|
|
paletteChange( palette() ); //sets up glowColors
|
|
restoreLayout( TDEGlobal::config(), "PlaylistColumnsLayout" );
|
|
|
|
// Sorting must be disabled when current.xml is being loaded. See BUG 113042
|
|
TDEListView::setSorting( NO_SORT ); //use base so we don't saveUndoState() too
|
|
|
|
setDynamicMode( 0 );
|
|
|
|
m_smartResizing = Amarok::config( "PlaylistWindow" )->readBoolEntry( "Smart Resizing", true );
|
|
|
|
columnOrderChanged();
|
|
//cause the column fractions to be updated, but in a safe way, ie no specific column
|
|
columnResizeEvent( header()->count(), 0, 0 );
|
|
|
|
//do after you resize all the columns
|
|
connect( header(), TQT_SIGNAL(sizeChange( int, int, int )), TQT_SLOT(columnResizeEvent( int, int, int )) );
|
|
|
|
connect( this, TQT_SIGNAL( contentsMoving( int, int ) ), TQT_SLOT( slotContentsMoving() ) );
|
|
|
|
connect( App::instance(), TQT_SIGNAL( useScores( bool ) ), this, TQT_SLOT( slotUseScores( bool ) ) );
|
|
connect( App::instance(), TQT_SIGNAL( useRatings( bool ) ), this, TQT_SLOT( slotUseRatings( bool ) ) );
|
|
connect( App::instance(), TQT_SIGNAL( moodbarPrefs( bool, bool, int, bool ) ),
|
|
this, TQT_SLOT( slotMoodbarPrefs( bool, bool, int, bool ) ) );
|
|
|
|
Amarok::ToolTip::add( this, viewport() );
|
|
|
|
header()->installEventFilter( this );
|
|
renameLineEdit()->installEventFilter( this );
|
|
setTabOrderedRenaming( false );
|
|
|
|
m_filtertimer = new TQTimer( this );
|
|
connect( m_filtertimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(setDelayedFilter()) );
|
|
|
|
connect( MountPointManager::instance(), TQT_SIGNAL(mediumConnected( int )),
|
|
TQT_SLOT(mediumChange( int )) );
|
|
connect( MountPointManager::instance(), TQT_SIGNAL(mediumRemoved( int )),
|
|
TQT_SLOT(mediumChange( int )) );
|
|
|
|
m_clicktimer = new TQTimer( this );
|
|
connect( m_clicktimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotSingleClick()) );
|
|
}
|
|
|
|
Playlist::~Playlist()
|
|
{
|
|
saveLayout( TDEGlobal::config(), "PlaylistColumnsLayout" );
|
|
|
|
if( AmarokConfig::savePlaylist() && m_lockStack == 0 ) saveXML( defaultPlaylistPath() );
|
|
|
|
//speed up quit a little
|
|
safeClear(); //our implementation is slow
|
|
Amarok::ToolTip::remove( viewport() );
|
|
blockSignals( true ); //might help
|
|
s_instance = 0;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// Media Handling
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
Playlist::mediumChange( int deviceid ) // SLOT
|
|
{
|
|
Q_UNUSED( deviceid );
|
|
|
|
for( TQListViewItem *it = firstChild();
|
|
it;
|
|
it = it->nextSibling() )
|
|
{
|
|
PlaylistItem *p = dynamic_cast<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;
|
|
TQPtrList<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( TQPtrList<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 TQString& sql, int options )
|
|
{
|
|
const bool isPlaying = EngineController::engine()->state() == Engine::Playing;
|
|
if( isPlaying )
|
|
options &= ~Playlist::StartPlay;
|
|
|
|
// TODO Implement more options
|
|
PlaylistItem *after = 0;
|
|
|
|
if ( options & Replace )
|
|
clear();
|
|
if ( options & Append )
|
|
after = lastItem();
|
|
|
|
setSorting( NO_SORT );
|
|
ThreadManager::instance()->queueJob( new SqlLoader( sql, after, options ) );
|
|
ScriptManager::instance()->notifyPlaylistChange("changed");
|
|
}
|
|
|
|
void
|
|
Playlist::addDynamicModeTracks( uint songCount )
|
|
{
|
|
if( songCount < 1 ) return;
|
|
|
|
int currentPos = 0;
|
|
for( MyIt it( this, MyIt::Visible ); *it; ++it )
|
|
{
|
|
if( m_currentTrack && *it == m_currentTrack )
|
|
break;
|
|
else if( !m_currentTrack && (*it)->isDynamicEnabled() )
|
|
break;
|
|
|
|
++currentPos;
|
|
}
|
|
currentPos++;
|
|
|
|
int required = currentPos + dynamicMode()->upcomingCount(); // currentPos handles currentTrack
|
|
int remainder = totalTrackCount();
|
|
|
|
if( required > remainder )
|
|
songCount = required - remainder;
|
|
|
|
DynamicMode *m = modifyDynamicMode();
|
|
KURL::List tracksToInsert = m->retrieveTracks( songCount );
|
|
Playlist::instance()->finishedModifying( m );
|
|
|
|
insertMedia( tracksToInsert, Playlist::Unique );
|
|
}
|
|
|
|
|
|
/**
|
|
* @param songCount : Number of tracks to be shown after the current track
|
|
*/
|
|
|
|
void
|
|
Playlist::adjustDynamicUpcoming( bool saveUndo )
|
|
{
|
|
/**
|
|
* If m_currentTrack exists, we iterate until we find it
|
|
* Else, we iterate until we find an item which is enabled
|
|
**/
|
|
MyIt it( this, MyIt::Visible ); //Notice we'll use this up to the end of the function!
|
|
//Skip previously played
|
|
for( ; *it; ++it )
|
|
{
|
|
if( m_currentTrack && *it == m_currentTrack )
|
|
break;
|
|
else if( !m_currentTrack && (*it)->isDynamicEnabled() )
|
|
break;
|
|
}
|
|
//Skip current
|
|
if( m_currentTrack )
|
|
++it;
|
|
|
|
int x = 0;
|
|
for ( ; *it && x < dynamicMode()->upcomingCount() ; ++it, ++x );
|
|
|
|
if ( x < dynamicMode()->upcomingCount() )
|
|
{
|
|
addDynamicModeTracks( dynamicMode()->upcomingCount() - x );
|
|
ScriptManager::instance()->notifyPlaylistChange("changed");
|
|
}
|
|
|
|
if( saveUndo )
|
|
saveUndoState();
|
|
}
|
|
|
|
/**
|
|
* @param songCount : Number of tracks to be shown before the current track
|
|
*/
|
|
|
|
void
|
|
Playlist::adjustDynamicPrevious( uint songCount, bool saveUndo )
|
|
{
|
|
int current = currentTrackIndex();
|
|
int x = current - songCount;
|
|
|
|
TQPtrList<TQListViewItem> list;
|
|
int y=0;
|
|
for( TQListViewItemIterator it( firstChild() ); y < x ; list.prepend( *it ), ++it, y++ );
|
|
|
|
if( list.isEmpty() ) return;
|
|
if ( saveUndo )
|
|
saveUndoState();
|
|
|
|
//remove the items
|
|
for( TQListViewItem *item = list.first(); item; item = list.next() )
|
|
{
|
|
removeItem( static_cast<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();
|
|
}
|
|
}
|
|
}
|
|
|
|
TQString
|
|
Playlist::defaultPlaylistPath() //static
|
|
{
|
|
return Amarok::saveLocation() + "current.xml";
|
|
}
|
|
|
|
void
|
|
Playlist::restoreSession()
|
|
{
|
|
KURL url;
|
|
|
|
if ( Amarok::config()->readBoolEntry( "First 1.4 Run", true ) ) {
|
|
// On first startup of 1.4, we load a special playlist with an intro track
|
|
url.setPath( locate( "data", "amarok/data/firstrun.m3u" ) );
|
|
Amarok::config()->writeEntry( "First 1.4 Run", false );
|
|
}
|
|
else
|
|
url.setPath( Amarok::saveLocation() + "current.xml" );
|
|
|
|
// check it exists, because on the first ever run it doesn't and
|
|
// it looks bad to show "some URLs were not suitable.." on the
|
|
// first ever-run
|
|
if( TQFile::exists( url.path() ) )
|
|
{
|
|
ThreadManager::instance()->queueJob( new UrlLoader( url, 0, 0 ) );
|
|
}
|
|
}
|
|
|
|
/*
|
|
The following two functions (saveLayout(), restoreLayout()), taken from tdelistview.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(TDEConfig *config, const TQString &group) const
|
|
{
|
|
TDEConfigGroupSaver saver(config, group);
|
|
TQStringList names, widths, order;
|
|
|
|
const int colCount = columns();
|
|
TQHeader* const thisHeader = header();
|
|
for (int i = 0; i < colCount; ++i)
|
|
{
|
|
names << PlaylistItem::exactColumnName(i);
|
|
widths << TQString::number(columnWidth(i));
|
|
order << TQString::number(thisHeader->mapToIndex(i));
|
|
}
|
|
config->writeEntry("ColumnsVersion", 1);
|
|
config->writeEntry("ColumnNames", names);
|
|
config->writeEntry("ColumnWidths", widths);
|
|
config->writeEntry("ColumnOrder", order);
|
|
config->writeEntry("SortColumn", columnSorted());
|
|
config->writeEntry("SortAscending", ascendingSort());
|
|
}
|
|
|
|
void Playlist::restoreLayout(TDEConfig *config, const TQString &group)
|
|
{
|
|
TDEConfigGroupSaver saver(config, group);
|
|
int version = config->readNumEntry("ColumnsVersion", 0);
|
|
|
|
TQValueList<int> iorder; //internal ordering
|
|
if( version )
|
|
{
|
|
TQStringList names = config->readListEntry("ColumnNames");
|
|
for( int i = 0, n = names.count(); i < n; ++i )
|
|
{
|
|
bool found = false;
|
|
for( int ii = i; ii < PlaylistItem::NUM_COLUMNS; ++ii ) //most likely, it's where we left it
|
|
{
|
|
if( names[i] == PlaylistItem::exactColumnName(ii) )
|
|
{
|
|
iorder.append(ii);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if( !found )
|
|
{
|
|
for( int ii = 0; ii < i; ++ii ) //but maybe it's not
|
|
if( names[i] == PlaylistItem::exactColumnName(ii) )
|
|
{
|
|
iorder.append(ii);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if( !found )
|
|
return; //oops? -- revert to the default.
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int oldorder[] = { 0, 1, 2, 5, 4, 9, 8, 7, 10, 12, 13, 15, 16, 11, 17, 18, 19, 3, 6, 20 };
|
|
for( int i = 0; i != 20; ++i )
|
|
iorder.append(oldorder[i]);
|
|
}
|
|
|
|
|
|
TQStringList cols = config->readListEntry("ColumnWidths");
|
|
int i = 0;
|
|
{ // scope the iterators
|
|
TQStringList::ConstIterator it = cols.constBegin();
|
|
const TQStringList::ConstIterator itEnd = cols.constEnd();
|
|
for (; it != itEnd; ++it)
|
|
setColumnWidth(iorder[i++], (*it).toInt());
|
|
}
|
|
|
|
|
|
// move sections in the correct sequence: from lowest to highest index position
|
|
// otherwise we move a section from an index, which modifies
|
|
// all index numbers to the right of the moved one
|
|
cols = config->readListEntry("ColumnOrder");
|
|
const int colCount = columns();
|
|
for (i = 0; i < colCount; ++i) // final index positions from lowest to highest
|
|
{
|
|
TQStringList::ConstIterator it = cols.constBegin();
|
|
const TQStringList::ConstIterator itEnd = cols.constEnd();
|
|
|
|
int section = 0;
|
|
for (; (it != itEnd) && (iorder[(*it).toInt()] != i); ++it, ++section) ;
|
|
|
|
if ( it != itEnd ) {
|
|
// found the section to move to position i
|
|
header()->moveSection(iorder[section], i);
|
|
}
|
|
}
|
|
|
|
if ( config->hasKey("SortColumn") )
|
|
{
|
|
const int sort = config->readNumEntry("SortColumn");
|
|
if( sort >= 0 && uint(sort) < iorder.count() )
|
|
setSorting(iorder[config->readNumEntry("SortColumn")], config->readBoolEntry("SortAscending", true));
|
|
}
|
|
|
|
if( !AmarokConfig::useScores() )
|
|
hideColumn( PlaylistItem::Score );
|
|
if( !AmarokConfig::useRatings() )
|
|
hideColumn( PlaylistItem::Rating );
|
|
if( !AmarokConfig::showMoodbar() )
|
|
hideColumn( PlaylistItem::Mood );
|
|
}
|
|
|
|
void
|
|
Playlist::addToUniqueMap( const TQString uniqueid, PlaylistItem* item )
|
|
{
|
|
TQPtrList<PlaylistItem> *list;
|
|
if( m_uniqueMap.contains( uniqueid ) )
|
|
list = m_uniqueMap[uniqueid];
|
|
else
|
|
list = new TQPtrList<PlaylistItem>();
|
|
list->append( item );
|
|
if( !m_uniqueMap.contains( uniqueid ) )
|
|
m_uniqueMap[uniqueid] = list;
|
|
}
|
|
|
|
void
|
|
Playlist::removeFromUniqueMap( const TQString uniqueid, PlaylistItem* item )
|
|
{
|
|
if( !m_uniqueMap.contains( uniqueid ) )
|
|
return;
|
|
|
|
TQPtrList<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 TQString &oldUrl, const TQString &newUrl, const TQString &uniqueid )
|
|
{
|
|
// Make sure the MoodServer gets this signal first!
|
|
MoodServer::instance()->slotFileMoved( oldUrl, newUrl );
|
|
|
|
TQPtrList<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 TQString &/*url*/, const TQString &oldid, const TQString &newid )
|
|
{
|
|
TQPtrList<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 TQString &/*absPath*/, const TQString &uniqueid )
|
|
{
|
|
TQPtrList<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 TQString &absPath, const TQString &uniqueid )
|
|
{
|
|
TQPtrList<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 TQMap<TQString,TQString> &map )
|
|
{
|
|
TQMap<TQString,TQPtrList<PlaylistItem>*> uniquecopy( m_uniqueMap );
|
|
|
|
TQMap<TQString,TQPtrList<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( TQString(), it.key() );
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// Current Track Handling
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
Playlist::playNextTrack( bool forceNext )
|
|
{
|
|
PlaylistItem *item = currentTrack();
|
|
|
|
if( !m_visCount || stopAfterMode() == StopAfterCurrent )
|
|
{
|
|
if( dynamicMode() && m_visCount )
|
|
{
|
|
item->setDynamicEnabled( false );
|
|
advanceDynamicTrack();
|
|
m_dynamicDirt = false;
|
|
}
|
|
|
|
EngineController::instance()->stop();
|
|
setStopAfterMode( DoNotStop );
|
|
|
|
if( !AmarokConfig::randomMode() ) {
|
|
item = MyIt::nextVisible( item );
|
|
while( item && ( !checkFileStatus( item ) || !item->exists() ) )
|
|
item = MyIt::nextVisible( item );
|
|
setCurrentTrack( item );
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if( !Amarok::repeatTrack() || forceNext )
|
|
{
|
|
if( !m_nextTracks.isEmpty() )
|
|
{
|
|
item = m_nextTracks.first();
|
|
m_nextTracks.remove();
|
|
if ( dynamicMode() )
|
|
// move queued track to the top of the playlist, to prevent it from being played twice
|
|
// this is done automatically by most queue changing functions, but not if the user manually moves the track
|
|
moveItem( item, 0, m_currentTrack );
|
|
emit queueChanged( PLItemList(), PLItemList( item ) );
|
|
}
|
|
|
|
else if( Amarok::entireAlbums() && m_currentTrack && m_currentTrack->nextInAlbum() )
|
|
item = m_currentTrack->nextInAlbum();
|
|
|
|
else if( Amarok::repeatAlbum() &&
|
|
repeatAlbumTrackCount() && ( repeatAlbumTrackCount() > 1 || !forceNext ) )
|
|
item = m_currentTrack->m_album->tracks.getFirst();
|
|
|
|
else if( AmarokConfig::randomMode() )
|
|
{
|
|
TQValueVector<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
|
|
TQPtrList<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( TDEApplication::random() % tracks.count() ); //is O(1)
|
|
else
|
|
{
|
|
const uint currenttime_t = TQDateTime::currentDateTime().toTime_t();
|
|
TQValueVector<int> weights( tracks.size() );
|
|
TQ_INT64 total = m_total;
|
|
if( Amarok::randomAlbums() )
|
|
{
|
|
for( int i = 0, n = tracks.count(); i < n; ++i )
|
|
{
|
|
weights[i] = tracks.at( i )->m_album->total;
|
|
if( Amarok::favorLastPlay() )
|
|
{
|
|
const int inc = int( float( ( currenttime_t - m_startupTime_t )
|
|
* tracks.at( i )->m_album->tracks.count() + 0.5 )
|
|
/ tracks.at( i )->m_album->tracks.count() );
|
|
weights[i] += inc;
|
|
total += inc;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for( int i = 0, n = tracks.count(); i < n; ++i )
|
|
{
|
|
weights[i] = tracks.at( i )->totalIncrementAmount();
|
|
if( Amarok::favorLastPlay() )
|
|
weights[i] += currenttime_t - m_startupTime_t;
|
|
}
|
|
if( Amarok::favorLastPlay() )
|
|
total += ( currenttime_t - m_startupTime_t ) * weights.count();
|
|
}
|
|
|
|
TQ_INT64 random;
|
|
if( Amarok::favorLastPlay() ) //really big huge numbers
|
|
{
|
|
TQ_INT64 r = TQ_INT64( ( TDEApplication::random() / pow( 2, sizeof( int ) * 8 ) )
|
|
* pow( 2, 64 ) );
|
|
random = r % total;
|
|
}
|
|
else
|
|
random = TDEApplication::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;
|
|
TQPtrList<TQListViewItem> dynamicList;
|
|
|
|
for( MyIt it( this, MyIt::Selected ); *it; ++it )
|
|
{
|
|
// Dequeuing selection with dynamic doesn't work due to the moving of the track after the last queued
|
|
if( dynamicMode() )
|
|
{
|
|
( !m_nextTracks.containsRef( *it ) ? in : out ).append( *it );
|
|
dynamicList.append( *it );
|
|
}
|
|
else
|
|
{
|
|
queue( *it, true );
|
|
( m_nextTracks.containsRef( *it ) ? in : out ).append( *it );
|
|
}
|
|
|
|
}
|
|
|
|
if( dynamicMode() )
|
|
{
|
|
TQListViewItem *item = dynamicList.first();
|
|
if( m_nextTracks.containsRef( static_cast<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( TQListViewItem *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( TQListViewItem *item )
|
|
{
|
|
/* We have to check if the item exists before calling activate, otherwise clicking on an empty
|
|
playlist space would stop playing (check BR #105106)*/
|
|
if( item && m_hoveredRating != item )
|
|
activate( item );
|
|
}
|
|
|
|
void
|
|
Playlist::slotCountChanged()
|
|
{
|
|
if( m_itemCountDirty )
|
|
emit itemCountChanged( totalTrackCount(), m_totalLength,
|
|
m_visCount, m_visLength,
|
|
m_selCount, m_selLength );
|
|
|
|
m_itemCountDirty = false;
|
|
}
|
|
|
|
bool
|
|
Playlist::checkFileStatus( PlaylistItem * item )
|
|
{
|
|
//DEBUG_BLOCK
|
|
//debug() << "uniqueid of item = " << item->uniqueId() << ", url = " << item->url().path() << endl;
|
|
if( !item->checkExists() )
|
|
{
|
|
//debug() << "not found, finding new url" << endl;
|
|
TQString path = TQString();
|
|
if( !item->uniqueId().isEmpty() )
|
|
{
|
|
path = CollectionDB::instance()->urlFromUniqueId( item->uniqueId() );
|
|
//debug() << "found path = " << path << endl;
|
|
}
|
|
else
|
|
{
|
|
//debug() << "Setting uniqueid of item and trying again" << endl;
|
|
item->setUniqueId();
|
|
if( !item->uniqueId().isEmpty() )
|
|
path = CollectionDB::instance()->urlFromUniqueId( item->uniqueId() );
|
|
}
|
|
if( !path.isEmpty() )
|
|
{
|
|
item->setUrl( KURL( path ) );
|
|
if( item->checkExists() )
|
|
item->setFilestatusEnabled( true );
|
|
else
|
|
item->setFilestatusEnabled( false );
|
|
}
|
|
else
|
|
item->setFilestatusEnabled( false );
|
|
}
|
|
else if( !item->isFilestatusEnabled() )
|
|
item->setFilestatusEnabled( true );
|
|
|
|
bool returnValue = item->isFilestatusEnabled();
|
|
|
|
return returnValue;
|
|
}
|
|
|
|
void
|
|
Playlist::activate( TQListViewItem *item )
|
|
{
|
|
///item will be played if possible, the playback may be delayed
|
|
///so we start the glow anyway and hope
|
|
|
|
//All internal requests for playback should come via
|
|
//this function please!
|
|
|
|
if( !item )
|
|
{
|
|
//we have reached the end of the playlist
|
|
EngineController::instance()->stop();
|
|
setCurrentTrack( 0 );
|
|
Amarok::OSD::instance()->OSDWidget::show( i18n("Playlist finished"),
|
|
TQImage( TDEIconLoader().iconPath( "amarok", -TDEIcon::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
|
|
}
|
|
|
|
TQPair<TQString, TQRect> Playlist::toolTipText( TQWidget*, const TQPoint &pos ) const
|
|
{
|
|
PlaylistItem *item = static_cast<PlaylistItem*>( itemAt( pos ) );
|
|
if( !item )
|
|
return TQPair<TQString, TQRect>( TQString(), TQRect() );
|
|
|
|
const TQPoint contentsPos = viewportToContents( pos );
|
|
const int col = header()->sectionAt( contentsPos.x() );
|
|
|
|
if( item == m_renameItem && col == m_renameColumn )
|
|
return TQPair<TQString, TQRect>( TQString(), TQRect() );
|
|
|
|
TQString text;
|
|
if( col == PlaylistItem::Rating )
|
|
text = item->ratingDescription( item->rating() );
|
|
else
|
|
text = item->text( col );
|
|
|
|
TQRect irect = itemRect( item );
|
|
const int headerPos = header()->sectionPos( col );
|
|
irect.setLeft( headerPos - 1 );
|
|
irect.setRight( headerPos + header()->sectionSize( col ) );
|
|
|
|
static TQFont f;
|
|
static int minbearing = 1337 + 666; //can be 0 or negative, 2003 is less likely
|
|
if( minbearing == 2003 || f != font() )
|
|
{
|
|
f = font(); //getting your bearings can be expensive, so we cache them
|
|
minbearing = fontMetrics().minLeftBearing() + fontMetrics().minRightBearing();
|
|
}
|
|
|
|
int itemWidth = irect.width() - itemMargin() * 2 + minbearing - 2;
|
|
if( item->pixmap( col ) )
|
|
itemWidth -= item->pixmap( col )->width();
|
|
if( item == m_currentTrack )
|
|
{
|
|
if( col == m_firstColumn )
|
|
itemWidth -= 12;
|
|
if( col == mapToLogicalColumn( numVisibleColumns() - 1 ) )
|
|
itemWidth -= 12;
|
|
}
|
|
|
|
if( col != PlaylistItem::Rating && fontMetrics().width( text ) <= itemWidth )
|
|
return TQPair<TQString, TQRect>( TQString(), TQRect() );
|
|
|
|
TQRect globalRect( viewport()->mapToGlobal( irect.topLeft() ), irect.size() );
|
|
TQSimpleRichText t( text, font() );
|
|
int dright = TQApplication::desktop()->screenGeometry( qscrollview() ).topRight().x();
|
|
t.setWidth( dright - globalRect.left() );
|
|
if( col == PlaylistItem::Rating )
|
|
globalRect.setRight( kMin( dright, kMax( globalRect.left() + t.widthUsed(), globalRect.left() + ( StarManager::instance()->getGreyStar()->width() + 1 ) * ( ( item->rating() + 1 ) / 2 ) ) ) );
|
|
else
|
|
globalRect.setRight( kMin( globalRect.left() + t.widthUsed(), dright ) );
|
|
globalRect.setBottom( globalRect.top() + kMax( irect.height(), t.height() ) - 1 );
|
|
|
|
if( ( col == PlaylistItem::Rating && PlaylistItem::ratingAtPoint( contentsPos.x() ) <= item->rating() + 1 ) ||
|
|
( col != PlaylistItem::Rating ) )
|
|
{
|
|
text = text.replace( "&", "&" ).replace( "<", "<" ).replace( ">", ">" );
|
|
if( item->isCurrent() )
|
|
{
|
|
text = TQString("<i>%1</i>").arg( text );
|
|
Amarok::ToolTip::s_hack = 1; //HACK for precise positioning
|
|
}
|
|
return TQPair<TQString, TQRect>( text, globalRect );
|
|
}
|
|
|
|
return TQPair<TQString, TQRect>( TQString(), TQRect() );
|
|
}
|
|
|
|
void
|
|
Playlist::activateByIndex( int index )
|
|
{
|
|
TQListViewItem* item = itemAtIndex( index );
|
|
|
|
if ( item )
|
|
activate(item);
|
|
}
|
|
|
|
void
|
|
Playlist::setCurrentTrack( PlaylistItem *item )
|
|
{
|
|
///mark item as the current track and make it glow
|
|
|
|
PlaylistItem *prev = m_currentTrack;
|
|
|
|
//FIXME best method would be to observe usage, especially don't shift if mouse is moving nearby
|
|
if( item && ( !prev || prev == currentItem() ) && !renameLineEdit()->isVisible() && m_selCount < 2 )
|
|
{
|
|
if( !prev )
|
|
//if nothing is current and then playback starts, we must show the currentTrack
|
|
ensureItemCentered( item ); //handles 0 gracefully
|
|
|
|
else {
|
|
const int prevY = itemPos( prev );
|
|
const int prevH = prev->height();
|
|
|
|
// check if the previous track is visible
|
|
if( prevY <= contentsY() + visibleHeight() && prevY + prevH >= contentsY() )
|
|
{
|
|
// in random mode always jump, if previous track is visible
|
|
if( AmarokConfig::randomMode() )
|
|
ensureItemCentered( item );
|
|
else if( prev && prev == currentItem() )
|
|
setCurrentItem( item );
|
|
|
|
//FIXME would be better to just never be annoying
|
|
// so if the user caused the track change, always show the new track
|
|
// but if it is automatic be careful
|
|
|
|
// if old item in view then try to keep the new one near the middle
|
|
const int y = itemPos( item );
|
|
const int h = item->height();
|
|
const int vh = visibleHeight();
|
|
const int amount = h * 3;
|
|
|
|
int d = y - contentsY();
|
|
|
|
if( d > 0 ) {
|
|
d += h;
|
|
d -= vh;
|
|
|
|
if( d > 0 && d <= amount )
|
|
// scroll down
|
|
setContentsPos( contentsX(), y - vh + amount );
|
|
}
|
|
else if( d >= -amount )
|
|
// scroll up
|
|
setContentsPos( contentsX(), y - amount );
|
|
}
|
|
}
|
|
}
|
|
|
|
m_currentTrack = item;
|
|
if ( m_currentTrack )
|
|
m_currentTrack->setIsNew(false);
|
|
|
|
if ( prev ) {
|
|
//reset to normal height
|
|
prev->invalidateHeight();
|
|
prev->setup();
|
|
//remove pixmap in first column
|
|
prev->setPixmap( m_firstColumn, TQPixmap() );
|
|
}
|
|
|
|
updateNextPrev();
|
|
|
|
setCurrentTrackPixmap();
|
|
|
|
Glow::reset();
|
|
slotGlowTimer();
|
|
}
|
|
|
|
int
|
|
Playlist::currentTrackIndex( bool onlyCountVisible )
|
|
{
|
|
int index = 0;
|
|
for( MyIt it( this, onlyCountVisible ? MyIt::Visible : MyIt::All ); *it; ++it )
|
|
{
|
|
if ( *it == m_currentTrack )
|
|
return index;
|
|
++index;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
int
|
|
Playlist::totalTrackCount() const
|
|
{
|
|
return m_totalCount;
|
|
}
|
|
|
|
BundleList
|
|
Playlist::nextTracks() const
|
|
{
|
|
BundleList list;
|
|
for( TQPtrListIterator<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;
|
|
|
|
TQString pixmap = TQString();
|
|
|
|
if( state < 0 )
|
|
state = EngineController::engine()->state();
|
|
|
|
if( state == Engine::Paused )
|
|
pixmap = "currenttrack_pause";
|
|
else if( state == Engine::Playing )
|
|
pixmap = "currenttrack_play";
|
|
|
|
m_currentTrack->setPixmap( m_firstColumn, pixmap.isNull() ? TQPixmap() : Amarok::getPNG( pixmap ) );
|
|
PlaylistItem::setPixmapChanged();
|
|
}
|
|
|
|
PlaylistItem*
|
|
Playlist::restoreCurrentTrack()
|
|
{
|
|
///It is always possible that the current track has been lost
|
|
///eg it was removed and then reinserted, here we check
|
|
|
|
const KURL url = EngineController::instance()->playingURL();
|
|
|
|
if ( !(m_currentTrack && ( m_currentTrack->url() == url || !m_currentTrack->url().isEmpty() && url.isEmpty() ) ) )
|
|
{
|
|
PlaylistItem* item;
|
|
|
|
for( item = firstChild();
|
|
item && item->url() != url;
|
|
item = item->nextSibling() )
|
|
{}
|
|
|
|
setCurrentTrack( item ); //set even if NULL
|
|
}
|
|
|
|
if( m_currentTrack && EngineController::instance()->engine()->state() == Engine::Playing && !Glow::timer.isActive() )
|
|
Glow::startTimer();
|
|
|
|
return m_currentTrack;
|
|
}
|
|
|
|
void
|
|
Playlist::countChanged()
|
|
{
|
|
if( !m_itemCountDirty )
|
|
{
|
|
m_itemCountDirty = true;
|
|
TQTimer::singleShot( 0, this, TQT_SLOT( slotCountChanged() ) );
|
|
}
|
|
}
|
|
|
|
bool
|
|
Playlist::isTrackAfter() const
|
|
{
|
|
///Is there a track after the current track?
|
|
//order is carefully crafted, remember count() is O(n)
|
|
//TODO randomMode will end if everything is in prevTracks
|
|
|
|
return !currentTrack() && !isEmpty() ||
|
|
!m_nextTracks.isEmpty() ||
|
|
currentTrack() && currentTrack()->itemBelow() ||
|
|
totalTrackCount() > 1 && ( AmarokConfig::randomMode() || Amarok::repeatPlaylist()
|
|
|| Amarok::repeatAlbum() && repeatAlbumTrackCount() > 1 );
|
|
}
|
|
|
|
bool
|
|
Playlist::isTrackBefore() const
|
|
{
|
|
//order is carefully crafted, remember count() is O(n)
|
|
|
|
return !isEmpty() &&
|
|
(
|
|
currentTrack() && (currentTrack()->itemAbove() || Amarok::repeatPlaylist() && totalTrackCount() > 1)
|
|
||
|
|
AmarokConfig::randomMode() && totalTrackCount() > 1
|
|
);
|
|
}
|
|
|
|
void
|
|
Playlist::updateNextPrev()
|
|
{
|
|
Amarok::actionCollection()->action( "play" )->setEnabled( !isEmpty() );
|
|
Amarok::actionCollection()->action( "prev" )->setEnabled( isTrackBefore() );
|
|
Amarok::actionCollection()->action( "next" )->setEnabled( isTrackAfter() );
|
|
Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !isEmpty() );
|
|
Amarok::actionCollection()->action( "playlist_show" )->setEnabled( m_currentTrack );
|
|
|
|
if( m_currentTrack )
|
|
// ensure currentTrack is shown at correct height
|
|
m_currentTrack->setup();
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// EngineObserver Reimplementation
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
Playlist::engineNewMetaData( const MetaBundle &bundle, bool trackChanged )
|
|
{
|
|
if ( !bundle.podcastBundle() )
|
|
{
|
|
if ( m_currentTrack && !trackChanged ) {
|
|
//if the track hasn't changed then this is a meta-data update
|
|
if( stopAfterMode() == StopAfterCurrent || !m_nextTracks.isEmpty() )
|
|
Playlist::instance()->playNextTrack( true );
|
|
//this is a hack, I repeat a hack! FIXME FIXME
|
|
//we do it because often the stream title is from the pls file and is informative
|
|
//we don't want to lose it when we get the meta data
|
|
else if ( m_currentTrack->artist().isEmpty() ) {
|
|
TQString comment = m_currentTrack->title();
|
|
m_currentTrack->copyFrom( bundle );
|
|
m_currentTrack->setComment( comment );
|
|
}
|
|
else
|
|
m_currentTrack->copyFrom( bundle );
|
|
}
|
|
else
|
|
//ensure the currentTrack is set correctly and highlight it
|
|
restoreCurrentTrack();
|
|
}
|
|
else
|
|
//ensure the currentTrack is set correctly and highlight it
|
|
restoreCurrentTrack();
|
|
|
|
if( m_currentTrack )
|
|
m_currentTrack->filter( m_filter );
|
|
}
|
|
|
|
void
|
|
Playlist::engineStateChanged( Engine::State state, Engine::State /*oldState*/ )
|
|
{
|
|
switch( state )
|
|
{
|
|
case Engine::Playing:
|
|
Amarok::actionCollection()->action( "pause" )->setEnabled( true );
|
|
Amarok::actionCollection()->action( "stop" )->setEnabled( true );
|
|
|
|
Glow::startTimer();
|
|
|
|
break;
|
|
|
|
case Engine::Paused:
|
|
Amarok::actionCollection()->action( "pause" )->setEnabled( false );
|
|
Amarok::actionCollection()->action( "stop" )->setEnabled( true );
|
|
|
|
Glow::reset();
|
|
|
|
if( m_currentTrack )
|
|
slotGlowTimer(); //update glow state
|
|
|
|
break;
|
|
|
|
case Engine::Empty:
|
|
Amarok::actionCollection()->action( "pause" )->setEnabled( false );
|
|
Amarok::actionCollection()->action( "stop" )->setEnabled( false );
|
|
|
|
//leave the glow state at full colour
|
|
Glow::reset();
|
|
|
|
if ( m_currentTrack )
|
|
{
|
|
//remove pixmap in all columns
|
|
TQPixmap null;
|
|
for( int i = 0; i < header()->count(); i++ )
|
|
m_currentTrack->setPixmap( i, null );
|
|
|
|
PlaylistItem::setPixmapChanged();
|
|
|
|
//reset glow state
|
|
slotGlowTimer();
|
|
}
|
|
|
|
case Engine::Idle:
|
|
slotGlowTimer();
|
|
|
|
break;
|
|
}
|
|
|
|
//POSSIBLYAHACK
|
|
//apparently you can't rely on EngineController::engine()->state() == state here, so pass it explicitly
|
|
setCurrentTrackPixmap( state );
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// TDEListView Reimplementation
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
Playlist::appendMedia( const TQString &path )
|
|
{
|
|
appendMedia( KURL::fromPathOrURL( path ) );
|
|
}
|
|
|
|
void
|
|
Playlist::appendMedia( const KURL &url )
|
|
{
|
|
insertMedia( KURL::List( url ) );
|
|
}
|
|
|
|
void
|
|
Playlist::clear() //SLOT
|
|
{
|
|
if( isLocked() || renameLineEdit()->isVisible() ) return;
|
|
|
|
disableDynamicMode();
|
|
|
|
emit aboutToClear(); //will saveUndoState()
|
|
|
|
setCurrentTrack( 0 );
|
|
m_prevTracks.clear();
|
|
m_prevAlbums.clear();
|
|
|
|
if (m_stopAfterTrack) {
|
|
m_stopAfterTrack = 0;
|
|
if ( stopAfterMode() != StopAfterCurrent ) {
|
|
setStopAfterMode( DoNotStop );
|
|
}
|
|
}
|
|
const PLItemList prev = m_nextTracks;
|
|
m_nextTracks.clear();
|
|
emit queueChanged( PLItemList(), prev );
|
|
|
|
// Update player button states
|
|
Amarok::actionCollection()->action( "play" )->setEnabled( false );
|
|
Amarok::actionCollection()->action( "prev" )->setEnabled( false );
|
|
Amarok::actionCollection()->action( "next" )->setEnabled( false );
|
|
Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( false );
|
|
|
|
ThreadManager::instance()->abortAllJobsNamed( "TagWriter" );
|
|
|
|
// something to bear in mind, if there is any event in the loop
|
|
// that depends on a PlaylistItem, we are about to crash Amarok
|
|
// never unlock() the Playlist until it is safe!
|
|
safeClear();
|
|
m_total = 0;
|
|
m_albums.clear();
|
|
|
|
setPlaylistName( i18n( "Untitled" ) );
|
|
ScriptManager::instance()->notifyPlaylistChange("cleared");
|
|
}
|
|
|
|
/**
|
|
* Workaround for TQt 3.3.5 bug in TQListView::clear()
|
|
* @see http://lists.kde.org/?l=kde-devel&m=113113845120155&w=2
|
|
* @see BUG 116004
|
|
*/
|
|
void
|
|
Playlist::safeClear()
|
|
{
|
|
/* 3.3.5 and 3.3.6 have bad TDEListView::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( tqVersion(), "3.3.5" ) == 0
|
|
|| strcmp( tqVersion(), "3.3.6" ) == 0 )
|
|
{
|
|
bool block = signalsBlocked();
|
|
blockSignals( true );
|
|
clearSelection();
|
|
|
|
TQListViewItem *c = firstChild();
|
|
TQListViewItem *n;
|
|
while( c ) {
|
|
n = c->nextSibling();
|
|
if ( !static_cast<PlaylistItem *>( c )->isEmpty() ) //avoid deleting markers
|
|
delete c;
|
|
c = n;
|
|
}
|
|
blockSignals( block );
|
|
triggerUpdate();
|
|
}
|
|
else
|
|
TDEListView::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() )
|
|
TDEListView::setSorting( col, b );
|
|
}
|
|
|
|
void
|
|
Playlist::setColumnWidth( int col, int width )
|
|
{
|
|
|
|
TDEListView::setColumnWidth( col, width );
|
|
|
|
//FIXME this is because TQt doesn't by default disable resizing width 0 columns. GRRR!
|
|
//NOTE default column sizes are stored in default amarokrc so that restoreLayout() in ctor will
|
|
// call this function. This is necessary because addColumn() doesn't call setColumnWidth() GRRR!
|
|
header()->setResizeEnabled( width != 0, col );
|
|
}
|
|
|
|
void
|
|
Playlist::rename( TQListViewItem *item, int column ) //SLOT
|
|
{
|
|
if( !item )
|
|
return;
|
|
|
|
switch( column )
|
|
{
|
|
case PlaylistItem::Artist:
|
|
renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->artistList() );
|
|
break;
|
|
|
|
case PlaylistItem::Album:
|
|
renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->albumList() );
|
|
break;
|
|
|
|
case PlaylistItem::Genre:
|
|
renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->genreList() );
|
|
break;
|
|
|
|
case PlaylistItem::Composer:
|
|
renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->composerList() );
|
|
break;
|
|
|
|
default:
|
|
renameLineEdit()->completionObject()->clear();
|
|
break;
|
|
}
|
|
|
|
renameLineEdit()->completionObject()->setCompletionMode( TDEGlobalSettings::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 );
|
|
TDEListView::rename( item, column );
|
|
|
|
m_renameItem = item;
|
|
m_renameColumn = column;
|
|
|
|
static_cast<PlaylistItem*>(item)->setIsBeingRenamed( true );
|
|
|
|
}
|
|
|
|
void
|
|
Playlist::writeTag( TQListViewItem *qitem, const TQString &, int column ) //SLOT
|
|
{
|
|
const bool dynamicEnabled = static_cast<PlaylistItem*>(qitem)->isDynamicEnabled();
|
|
|
|
if( m_itemsToChangeTagsFor.isEmpty() )
|
|
m_itemsToChangeTagsFor.append( static_cast<PlaylistItem*>( qitem ) );
|
|
|
|
const TQString newTag = static_cast<PlaylistItem*>( qitem )->exactText( column );
|
|
|
|
for( PlaylistItem *item = m_itemsToChangeTagsFor.first(); item; item = m_itemsToChangeTagsFor.next() )
|
|
{
|
|
if( !checkFileStatus( item ) )
|
|
continue;
|
|
|
|
const TQString oldTag = item == qitem ? m_editOldTag : item->exactText(column);
|
|
|
|
if( column == PlaylistItem::Score )
|
|
CollectionDB::instance()->setSongPercentage( item->url().path(), newTag.toInt() );
|
|
else if( column == PlaylistItem::Rating )
|
|
CollectionDB::instance()->setSongRating( item->url().path(), newTag.toInt() );
|
|
else
|
|
if (oldTag != newTag)
|
|
ThreadManager::instance()->queueJob( new TagWriter( item, oldTag, newTag, column ) );
|
|
else if( item->deleteAfterEditing() )
|
|
{
|
|
removeItem( item );
|
|
delete item;
|
|
}
|
|
}
|
|
|
|
if( dynamicMode() )
|
|
static_cast<PlaylistItem*>(qitem)->setDynamicEnabled( dynamicEnabled );
|
|
|
|
m_itemsToChangeTagsFor.clear();
|
|
m_editOldTag = TQString();
|
|
}
|
|
|
|
void
|
|
Playlist::columnOrderChanged() //SLOT
|
|
{
|
|
const uint prevColumn = m_firstColumn;
|
|
|
|
//determine first visible column
|
|
for ( m_firstColumn = 0; m_firstColumn < header()->count(); m_firstColumn++ )
|
|
if ( header()->sectionSize( header()->mapToSection( m_firstColumn ) ) )
|
|
break;
|
|
|
|
//convert to logical column
|
|
m_firstColumn = header()->mapToSection( m_firstColumn );
|
|
|
|
//force redraw of currentTrack
|
|
if( m_currentTrack )
|
|
{
|
|
m_currentTrack->setPixmap( prevColumn, TQPixmap() );
|
|
setCurrentTrackPixmap();
|
|
}
|
|
TQResizeEvent e( size(), TQSize() );
|
|
viewportResizeEvent( &e );
|
|
emit columnsChanged();
|
|
}
|
|
|
|
void
|
|
Playlist::paletteChange( const TQPalette &p )
|
|
{
|
|
using namespace Glow;
|
|
|
|
TQColor fg;
|
|
TQColor bg;
|
|
|
|
{
|
|
using namespace Base;
|
|
|
|
const uint steps = STEPS+5+5; //so we don't fade all the way to base, and all the way up to highlight either
|
|
|
|
fg = colorGroup().highlight();
|
|
bg = colorGroup().base();
|
|
|
|
dr = double(bg.red() - fg.red()) / steps;
|
|
dg = double(bg.green() - fg.green()) / steps;
|
|
db = double(bg.blue() - fg.blue()) / steps;
|
|
|
|
r = fg.red() + int(dr*5.0); //we add 5 steps so the default colour is slightly different to highlight
|
|
g = fg.green() + int(dg*5.0);
|
|
b = fg.blue() + int(db*5.0);
|
|
}
|
|
|
|
{
|
|
using namespace Text;
|
|
|
|
const uint steps = STEPS + 5; //so we don't fade all the way to base
|
|
|
|
fg = colorGroup().highlightedText();
|
|
bg = colorGroup().text();
|
|
|
|
dr = double(bg.red() - fg.red()) / steps;
|
|
dg = double(bg.green() - fg.green()) / steps;
|
|
db = double(bg.blue() - fg.blue()) / steps;
|
|
|
|
r = fg.red();
|
|
g = fg.green();
|
|
b = fg.blue();
|
|
}
|
|
|
|
TDEListView::paletteChange( p );
|
|
|
|
counter = 0; // reset the counter or apparently the text lacks contrast
|
|
slotGlowTimer(); // repaint currentTrack marker
|
|
}
|
|
|
|
void
|
|
Playlist::contentsDragEnterEvent( TQDragEnterEvent *e )
|
|
{
|
|
TQString data;
|
|
TQCString subtype;
|
|
TQTextDrag::decode( e, data, subtype );
|
|
|
|
e->accept(
|
|
e->source() == viewport() ||
|
|
subtype == "amarok-sql" ||
|
|
subtype == "uri-list" || //this is to prevent DelayedUrlLists from performing their queries
|
|
KURLDrag::canDecode( e ) );
|
|
}
|
|
|
|
void
|
|
Playlist::contentsDragMoveEvent( TQDragMoveEvent* e )
|
|
{
|
|
if( !e->isAccepted() ) return;
|
|
|
|
#if KDE_IS_VERSION( 3, 3, 91 )
|
|
const bool ctrlPressed = TDEApplication::keyboardMouseState() & TQt::ControlButton;
|
|
#else
|
|
const bool ctrlPressed = TDEApplication::keyboardModifiers() & ControlMask;
|
|
#endif
|
|
|
|
//Get the closest item _before_ the cursor
|
|
const TQPoint p = contentsToViewport( e->pos() );
|
|
TQListViewItem *item = itemAt( p );
|
|
if( !item || ctrlPressed ) item = lastItem();
|
|
else if( p.y() - itemRect( item ).top() < (item->height()/2) ) item = item->itemAbove();
|
|
|
|
if( item != m_marker ) {
|
|
//NOTE this if block prevents flicker
|
|
slotEraseMarker();
|
|
m_marker = item; //NOTE this is the correct place to set m_marker
|
|
viewportPaintEvent( 0 );
|
|
}
|
|
}
|
|
|
|
void
|
|
Playlist::contentsDragLeaveEvent( TQDragLeaveEvent* )
|
|
{
|
|
slotEraseMarker();
|
|
}
|
|
|
|
void
|
|
Playlist::contentsDropEvent( TQDropEvent *e )
|
|
{
|
|
DEBUG_BLOCK
|
|
|
|
//NOTE parent is always 0 currently, but we support it in case we start using trees
|
|
TQListViewItem *parent = 0;
|
|
TQListViewItem *after = m_marker;
|
|
|
|
//make sure to disable only if in dynamic mode and you're inserting
|
|
//at the beginning or in the middle of the disabled tracks
|
|
//Also, that the dynamic playlist has any tracks (suggested may not)
|
|
if( dynamicMode() && Playlist::instance()->firstChild() &&
|
|
( !m_marker || !( static_cast<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 );
|
|
TQPtrList<TQListViewItem> items = selectedItems();
|
|
if( dynamicMode() && after )
|
|
{
|
|
TQListViewItem *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 {
|
|
TQString data;
|
|
TQCString subtype;
|
|
TQTextDrag::decode( e, data, subtype );
|
|
|
|
debug() << "TQTextDrag::subtype(): " << subtype << endl;
|
|
|
|
if( subtype == "amarok-sql" ) {
|
|
setSorting( NO_SORT );
|
|
TQString query = data.section( "\n", 1 );
|
|
ThreadManager::instance()->queueJob( new SqlLoader( query, after ) );
|
|
ScriptManager::instance()->notifyPlaylistChange("changed");
|
|
}
|
|
|
|
else if( subtype == "dynamic" ) {
|
|
// Deserialize pointer
|
|
DynamicEntry* entry = reinterpret_cast<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();
|
|
}
|
|
|
|
TQDragObject*
|
|
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 ),
|
|
TQPoint( CollectionDB::DRAGPIXMAP_OFFSET_X, CollectionDB::DRAGPIXMAP_OFFSET_Y ) );
|
|
return drag;
|
|
}
|
|
|
|
#include <tqsimplerichtext.h>
|
|
void
|
|
Playlist::viewportPaintEvent( TQPaintEvent *e )
|
|
{
|
|
if( e ) TDEListView::viewportPaintEvent( e ); //we call with 0 in contentsDropEvent()
|
|
|
|
if ( m_marker ) {
|
|
TQPainter p( viewport() );
|
|
p.fillRect(
|
|
drawDropVisualizer( 0, 0, m_marker ),
|
|
TQBrush( colorGroup().highlight().dark(), TQBrush::Dense4Pattern ) );
|
|
}
|
|
else if( m_showHelp && isEmpty() ) {
|
|
TQPainter p( viewport() );
|
|
TQString minimumText(i18n(
|
|
"<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>" ) );
|
|
TQSimpleRichText *t = new TQSimpleRichText( 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>" ), TQApplication::font() );
|
|
|
|
if ( t->width()+30 >= viewport()->width() || t->height()+30 >= viewport()->height() ) {
|
|
// too big for the window, so let's cut part of the text
|
|
delete t;
|
|
t = new TQSimpleRichText( minimumText, TQApplication::font());
|
|
if ( t->width()+30 >= viewport()->width() || t->height()+30 >= viewport()->height() ) {
|
|
//still too big, giving up
|
|
delete t;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const uint w = t->width();
|
|
const uint h = t->height();
|
|
const uint x = (viewport()->width() - w - 30) / 2 ;
|
|
const uint y = (viewport()->height() - h - 30) / 2 ;
|
|
|
|
p.setBrush( colorGroup().background() );
|
|
p.drawRoundRect( x, y, w+30, h+30, (8*200)/w, (8*200)/h );
|
|
t->draw( &p, x+15, y+15, TQRect(), colorGroup() );
|
|
delete t;
|
|
}
|
|
}
|
|
|
|
static uint negativeWidth = 0;
|
|
|
|
void
|
|
Playlist::viewportResizeEvent( TQResizeEvent *e )
|
|
{
|
|
if ( !m_smartResizing ) {
|
|
TDEListView::viewportResizeEvent( e );
|
|
return;
|
|
}
|
|
//only be clever with the sizing if there is not many items
|
|
//TODO don't allow an item to be made too small (ie less than 50% of ideal width)
|
|
|
|
//makes this much quicker
|
|
header()->blockSignals( true );
|
|
|
|
const double W = (double)e->size().width() - negativeWidth;
|
|
|
|
for( uint c = 0; c < m_columnFraction.size(); ++c ) {
|
|
switch( c ) {
|
|
case PlaylistItem::Track:
|
|
case PlaylistItem::Bitrate:
|
|
case PlaylistItem::SampleRate:
|
|
case PlaylistItem::Filesize:
|
|
case PlaylistItem::Score:
|
|
case PlaylistItem::Rating:
|
|
case PlaylistItem::Type:
|
|
case PlaylistItem::PlayCount:
|
|
case PlaylistItem::Length:
|
|
case PlaylistItem::Year:
|
|
case PlaylistItem::DiscNumber:
|
|
case PlaylistItem::Bpm:
|
|
break; //these columns retain their width - their items tend to have uniform size
|
|
default:
|
|
if( m_columnFraction[c] > 0 )
|
|
setColumnWidth( c, int(W * m_columnFraction[c]) );
|
|
}
|
|
}
|
|
|
|
header()->blockSignals( false );
|
|
|
|
//ensure that the listview scrollbars are updated etc.
|
|
triggerUpdate();
|
|
}
|
|
|
|
void
|
|
Playlist::columnResizeEvent( int col, int oldw, int neww )
|
|
{
|
|
if ( !m_smartResizing )
|
|
return;
|
|
//prevent recursion
|
|
header()->blockSignals( true );
|
|
|
|
//qlistview is stupid sometimes
|
|
if ( neww < 0 )
|
|
setColumnWidth( col, 0 );
|
|
|
|
if ( neww == 0 ) {
|
|
//the column in question has been hidden
|
|
//we need to adjust the other columns to fit
|
|
|
|
const double W = (double)width() - negativeWidth;
|
|
|
|
for( uint c = 0; c < m_columnFraction.size(); ++c ) {
|
|
if( c == (uint)col )
|
|
continue;
|
|
switch( c ) {
|
|
case PlaylistItem::Track:
|
|
case PlaylistItem::Bitrate:
|
|
case PlaylistItem::SampleRate:
|
|
case PlaylistItem::Filesize:
|
|
case PlaylistItem::Score:
|
|
case PlaylistItem::Rating:
|
|
case PlaylistItem::Type:
|
|
case PlaylistItem::PlayCount:
|
|
case PlaylistItem::Length:
|
|
case PlaylistItem::Year:
|
|
case PlaylistItem::DiscNumber:
|
|
case PlaylistItem::Bpm:
|
|
break;
|
|
default:
|
|
if( m_columnFraction[c] > 0 )
|
|
setColumnWidth( c, int(W * m_columnFraction[c]) );
|
|
}
|
|
}
|
|
}
|
|
|
|
else if( oldw != 0 ) {
|
|
//adjust the size of the column on the right side of this one
|
|
|
|
for( int section = col, index = header()->mapToIndex( section ); index < header()->count(); ) {
|
|
section = header()->mapToSection( ++index );
|
|
|
|
if ( header()->sectionSize( section ) ) {
|
|
int newSize = header()->sectionSize( section ) + oldw - neww;
|
|
if ( newSize > 5 ) {
|
|
setColumnWidth( section, newSize );
|
|
//we only want to adjust one column!
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
header()->blockSignals( false );
|
|
|
|
negativeWidth = 0;
|
|
uint w = 0;
|
|
|
|
//determine width excluding the columns that have static size
|
|
for( uint x = 0; x < m_columnFraction.size(); ++x ) {
|
|
switch( x ) {
|
|
case PlaylistItem::Track:
|
|
case PlaylistItem::Bitrate:
|
|
case PlaylistItem::SampleRate:
|
|
case PlaylistItem::Filesize:
|
|
case PlaylistItem::Score:
|
|
case PlaylistItem::Rating:
|
|
case PlaylistItem::Type:
|
|
case PlaylistItem::PlayCount:
|
|
case PlaylistItem::Length:
|
|
case PlaylistItem::Year:
|
|
case PlaylistItem::DiscNumber:
|
|
case PlaylistItem::Bpm:
|
|
break;
|
|
default:
|
|
w += columnWidth( x );
|
|
}
|
|
|
|
negativeWidth += columnWidth( x );
|
|
}
|
|
|
|
//determine the revised column fractions
|
|
for( uint x = 0; x < m_columnFraction.size(); ++x )
|
|
m_columnFraction[x] = (double)columnWidth( x ) / double(w);
|
|
|
|
//negative width is an important property, honest!
|
|
negativeWidth -= w;
|
|
|
|
//we have to do this after we have established negativeWidth and set the columnFractions
|
|
if( neww == 0 || oldw == 0 ) {
|
|
//then this column has been inserted or removed, we need to update all the column widths
|
|
TQResizeEvent e( size(), TQSize() );
|
|
viewportResizeEvent( &e );
|
|
emit columnsChanged();
|
|
}
|
|
}
|
|
|
|
bool
|
|
Playlist::eventFilter( TQObject *o, TQEvent *e )
|
|
{
|
|
#define me TQT_TQMOUSEEVENT(e)
|
|
#define ke TQT_TQKEYEVENT(e)
|
|
|
|
if( o == header() && e->type() == TQEvent::MouseButtonPress && me->button() == Qt::RightButton )
|
|
{
|
|
enum { HIDE = 1000, SELECT, CUSTOM, SMARTRESIZING };
|
|
|
|
const int mouseOverColumn = header()->sectionAt( me->pos().x() );
|
|
|
|
TDEPopupMenu popup;
|
|
if( mouseOverColumn >= 0 )
|
|
popup.insertItem( i18n("&Hide %1").arg( columnText( mouseOverColumn ) ), HIDE ); //TODO
|
|
|
|
TDEPopupMenu sub;
|
|
for( int i = 0; i < columns(); ++i ) //columns() references a property
|
|
if( !columnWidth( i ) )
|
|
sub.insertItem( columnText( i ), i, i + 1 );
|
|
sub.setItemVisible( PlaylistItem::Score, AmarokConfig::useScores() );
|
|
sub.setItemVisible( PlaylistItem::Rating, AmarokConfig::useRatings() );
|
|
sub.setItemVisible( PlaylistItem::Mood, AmarokConfig::showMoodbar() );
|
|
|
|
popup.insertItem( i18n("&Show Column" ), &sub );
|
|
|
|
popup.insertItem( i18n("Select &Columns..."), SELECT );
|
|
|
|
popup.insertItem( i18n("&Fit to Width"), SMARTRESIZING );
|
|
popup.setItemChecked( SMARTRESIZING, m_smartResizing );
|
|
|
|
int col = popup.exec( TQT_TQMOUSEEVENT(e)->globalPos() );
|
|
|
|
switch( col ) {
|
|
case HIDE:
|
|
{
|
|
hideColumn( mouseOverColumn );
|
|
TQResizeEvent e( size(), TQSize() );
|
|
viewportResizeEvent( &e );
|
|
}
|
|
break;
|
|
|
|
case SELECT:
|
|
ColumnsDialog::display();
|
|
break;
|
|
|
|
case CUSTOM:
|
|
addCustomColumn();
|
|
break;
|
|
|
|
case SMARTRESIZING:
|
|
m_smartResizing = !m_smartResizing;
|
|
Amarok::config( "PlaylistWindow" )->writeEntry( "Smart Resizing", m_smartResizing );
|
|
if ( m_smartResizing )
|
|
columnResizeEvent( 0, 0, 0 ); //force refit. FIXME: It doesn't work perfectly
|
|
break;
|
|
|
|
default:
|
|
if( col != -1 )
|
|
{
|
|
adjustColumn( col );
|
|
header()->setResizeEnabled( true, col );
|
|
}
|
|
}
|
|
|
|
//determine first visible column again, since it has changed
|
|
columnOrderChanged();
|
|
//eat event
|
|
return true;
|
|
}
|
|
|
|
// not in slotMouseButtonPressed because we need to disable normal usage.
|
|
if( o == viewport() && e->type() == TQEvent::MouseButtonPress && me->state() == TQt::ControlButton && me->button() == Qt::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() == TQEvent::MouseButtonPress && me->button() == Qt::LeftButton )
|
|
{
|
|
m_clicktimer->stop();
|
|
m_itemToRename = 0;
|
|
int col = header()->sectionAt( viewportToContents( me->pos() ).x() );
|
|
if( col != PlaylistItem::Rating )
|
|
{
|
|
PlaylistItem *item = static_cast<PlaylistItem*>( itemAt( me->pos() ) );
|
|
bool edit = item
|
|
&& item->isSelected()
|
|
&& selectedItems().count()==1
|
|
&& (me->state() & ~Qt::LeftButton) == 0
|
|
&& item->url().isLocalFile();
|
|
if( edit )
|
|
{
|
|
m_clickPos = me->pos();
|
|
m_itemToRename = item;
|
|
m_columnToRename = col;
|
|
//return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
else if( o == viewport() && e->type() == TQEvent::MouseButtonRelease && me->button() == Qt::LeftButton )
|
|
{
|
|
int col = header()->sectionAt( viewportToContents( me->pos() ).x() );
|
|
if( col != PlaylistItem::Rating )
|
|
{
|
|
PlaylistItem *item = static_cast<PlaylistItem*>( itemAt( me->pos() ) );
|
|
if( item == m_itemToRename && me->pos() == m_clickPos )
|
|
{
|
|
m_clicktimer->start( int( TQApplication::doubleClickInterval() ), true );
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
m_itemToRename = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// avoid in-place tag editing upon double-clicks
|
|
else if( e->type() == TQEvent::MouseButtonDblClick && me->button() == Qt::LeftButton )
|
|
{
|
|
m_itemToRename = 0;
|
|
m_clicktimer->stop();
|
|
}
|
|
|
|
// Toggle play/pause if user middle-clicks on current track
|
|
else if( o == viewport() && e->type() == TQEvent::MouseButtonPress && me->button() == Qt::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 /*TQEvent::KeyPress*/ && m_renameItem )
|
|
{
|
|
const int visibleCols = numVisibleColumns();
|
|
int physicalColumn = visibleCols - 1;
|
|
|
|
while( mapToLogicalColumn( physicalColumn ) != m_renameColumn && physicalColumn >= 0 )
|
|
physicalColumn--;
|
|
if( physicalColumn < 0 )
|
|
{
|
|
warning() << "the column counting code is wrong! tell illissius." << endl;
|
|
return false;
|
|
}
|
|
|
|
int column = m_renameColumn;
|
|
TQListViewItem *item = m_renameItem;
|
|
|
|
if( ke->state() & TQt::AltButton )
|
|
{
|
|
if( ke->key() == TQt::Key_Up && m_visCount > 1 )
|
|
if( !( item = m_renameItem->itemAbove() ) )
|
|
{
|
|
item = *MyIt( this, MyIt::Visible );
|
|
while( item->itemBelow() )
|
|
item = item->itemBelow();
|
|
}
|
|
if( ke->key() == TQt::Key_Down && m_visCount > 1 )
|
|
if( !( item = m_renameItem->itemBelow() ) )
|
|
item = *MyIt( this, MyIt::Visible );
|
|
if( ke->key() == TQt::Key_Left )
|
|
do
|
|
{
|
|
if( physicalColumn == 0 )
|
|
physicalColumn = visibleCols - 1;
|
|
else
|
|
physicalColumn--;
|
|
column = mapToLogicalColumn( physicalColumn );
|
|
} while( !isRenameable( column ) );
|
|
if( ke->key() == TQt::Key_Right )
|
|
do
|
|
{
|
|
if( physicalColumn == visibleCols - 1 )
|
|
physicalColumn = 0;
|
|
else
|
|
physicalColumn++;
|
|
column = mapToLogicalColumn( physicalColumn );
|
|
} while( !isRenameable( column ) );
|
|
}
|
|
|
|
if( ke->key() == TQt::Key_Tab )
|
|
do
|
|
{
|
|
if( physicalColumn == visibleCols - 1 )
|
|
{
|
|
if( !( item = m_renameItem->itemBelow() ) )
|
|
item = *MyIt( this, MyIt::Visible );
|
|
physicalColumn = 0;
|
|
}
|
|
else
|
|
physicalColumn++;
|
|
column = mapToLogicalColumn( physicalColumn );
|
|
} while( !isRenameable( column ) );
|
|
if( ke->key() == TQt::Key_Backtab )
|
|
do
|
|
{
|
|
if( physicalColumn == 0 )
|
|
{
|
|
if( !( item = m_renameItem->itemAbove() ) )
|
|
{
|
|
item = *MyIt( this, MyIt::Visible );
|
|
while( item->itemBelow() )
|
|
item = item->itemBelow();
|
|
}
|
|
physicalColumn = visibleCols - 1;
|
|
}
|
|
else
|
|
physicalColumn--;
|
|
column = mapToLogicalColumn( physicalColumn );
|
|
} while( !isRenameable( column ) );
|
|
|
|
if( item != m_renameItem || column != m_renameColumn )
|
|
{
|
|
if( !item->isSelected() )
|
|
m_itemsToChangeTagsFor.clear();
|
|
//the item that actually got changed will get added back, in writeTag()
|
|
m_renameItem->setText( m_renameColumn, renameLineEdit()->text() );
|
|
doneEditing( m_renameItem, m_renameColumn );
|
|
rename( item, column );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
else if( o == renameLineEdit() && ( e->type() == TQEvent::Hide || e->type() == TQEvent::Close ) )
|
|
{
|
|
m_renameItem = 0;
|
|
}
|
|
|
|
//allow the header to process this
|
|
return TDEListView::eventFilter( o, e );
|
|
|
|
#undef me
|
|
#undef ke
|
|
}
|
|
|
|
void
|
|
Playlist::slotSingleClick()
|
|
{
|
|
if( m_itemToRename )
|
|
{
|
|
rename( m_itemToRename, m_columnToRename );
|
|
}
|
|
|
|
m_itemToRename = 0;
|
|
}
|
|
|
|
void
|
|
Playlist::customEvent( TQCustomEvent *e )
|
|
{
|
|
if( e->type() == (int)UrlLoader::JobFinishedEvent ) {
|
|
refreshNextTracks( 0 );
|
|
PLItemList in, out;
|
|
|
|
// Disable help if playlist is populated
|
|
if ( !isEmpty() )
|
|
m_showHelp = false;
|
|
|
|
if ( !m_queueList.isEmpty() ) {
|
|
KURL::List::Iterator jt;
|
|
for( MyIt it( this, MyIt::All ); *it; ++it ) {
|
|
jt = m_queueList.find( (*it)->url() );
|
|
|
|
if ( jt != m_queueList.end() ) {
|
|
queue( *it );
|
|
( m_nextTracks.containsRef( *it ) ? in : out ).append( *it );
|
|
m_queueList.remove( jt );
|
|
}
|
|
}
|
|
m_queueList.clear();
|
|
}
|
|
|
|
if( m_dynamicDirt )
|
|
{
|
|
PlaylistItem *after = m_currentTrack;
|
|
if( !after )
|
|
{
|
|
after = firstChild();
|
|
while( after && !after->isDynamicEnabled() )
|
|
after = after->nextSibling();
|
|
}
|
|
else
|
|
after = static_cast<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 TQString &path, bool relative ) const
|
|
{
|
|
TQValueList<KURL> urls;
|
|
TQValueList<TQString> titles;
|
|
TQValueList<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 TQString &path )
|
|
{
|
|
DEBUG_BLOCK
|
|
|
|
TQFile file( path );
|
|
if( !file.open( IO_WriteOnly | IO_Truncate | IO_Raw ) ) return;
|
|
|
|
// Manual buffering since TQFile's is slow for whatever reason
|
|
const uint kWriteSize = 256 * 1024;
|
|
|
|
TQBuffer buffer;
|
|
buffer.open(IO_WriteOnly);
|
|
|
|
TQTextStream stream( &buffer );
|
|
stream.setEncoding( TQTextStream::UnicodeUTF8 );
|
|
stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
|
|
|
TQString dynamic;
|
|
if( dynamicMode() )
|
|
{
|
|
const TQString title = ( dynamicMode()->title() ).replace( "&", "&" )
|
|
.replace( "<", "<" )
|
|
.replace( ">", ">" );
|
|
dynamic = TQString(" dynamicMode=\"%1\"").arg( title );
|
|
}
|
|
stream << TQString( "<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
|
|
|
|
TQStringList attributes;
|
|
const int queueIndex = m_nextTracks.findRef( item );
|
|
if ( queueIndex != -1 )
|
|
attributes << "queue_index" << TQString::number( queueIndex + 1 );
|
|
else if ( item == currentTrack() )
|
|
attributes << "queue_index" << TQString::number( 0 );
|
|
|
|
if( !item->isDynamicEnabled() )
|
|
attributes << "dynamicdisabled" << "true";
|
|
|
|
if( m_stopAfterTrack == item )
|
|
attributes << "stop_after" << "true";
|
|
|
|
item->save( stream, attributes );
|
|
|
|
if ( buffer.at() >= kWriteSize )
|
|
{
|
|
file.writeBlock( buffer.buffer().data(), buffer.at() );
|
|
buffer.reset();
|
|
}
|
|
}
|
|
|
|
stream << "</playlist>\n";
|
|
file.writeBlock(buffer.buffer().data(), buffer.at());
|
|
file.close();
|
|
}
|
|
|
|
void
|
|
Playlist::burnPlaylist( int projectType )
|
|
{
|
|
KURL::List list;
|
|
|
|
TQListViewItemIterator it( this );
|
|
for( ; it.current(); ++it ) {
|
|
PlaylistItem *item = static_cast<PlaylistItem*>(*it);
|
|
KURL url = item->url();
|
|
if( url.isLocalFile() )
|
|
list << url;
|
|
}
|
|
|
|
K3bExporter::instance()->exportTracks( list, projectType );
|
|
}
|
|
|
|
void
|
|
Playlist::burnSelectedTracks( int projectType )
|
|
{
|
|
KURL::List list;
|
|
|
|
TQListViewItemIterator it( this, TQListViewItemIterator::Selected );
|
|
for( ; it.current(); ++it ) {
|
|
PlaylistItem *item = static_cast<PlaylistItem*>(*it);
|
|
KURL url = item->url();
|
|
if( url.isLocalFile() )
|
|
list << url;
|
|
}
|
|
|
|
K3bExporter::instance()->exportTracks( list, projectType );
|
|
}
|
|
|
|
void
|
|
Playlist::addCustomMenuItem( const TQString &submenu, const TQString &itemTitle ) //for dcop
|
|
{
|
|
m_customSubmenuItem[submenu] << itemTitle;
|
|
}
|
|
|
|
bool
|
|
Playlist::removeCustomMenuItem( const TQString &submenu, const TQString &itemTitle ) //for dcop
|
|
{
|
|
if( !m_customSubmenuItem.contains(submenu) )
|
|
return false;
|
|
if( m_customSubmenuItem[submenu].remove( itemTitle ) != 0 )
|
|
{
|
|
if( m_customSubmenuItem[submenu].count() == 0 )
|
|
m_customSubmenuItem.remove( submenu );
|
|
return true;
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
void
|
|
Playlist::customMenuClicked(int id) //adapted from burnSelectedTracks
|
|
{
|
|
TQString message = m_customIdItem[id];
|
|
TQListViewItemIterator it( this, TQListViewItemIterator::Selected );
|
|
for( ; it.current(); ++it ) {
|
|
PlaylistItem *item = static_cast<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<TDESelectAction*>(Amarok::actionCollection()->action( "random_mode" ))->setCurrentItem( m_oldRandom );
|
|
static_cast<TDESelectAction*>(Amarok::actionCollection()->action( "repeat" ))->setCurrentItem( m_oldRepeat );
|
|
}
|
|
|
|
void
|
|
Playlist::rebuildDynamicModeCache() //SLOT
|
|
{
|
|
if( !m_dynamicMode )
|
|
return;
|
|
|
|
DynamicMode *m = modifyDynamicMode();
|
|
m->rebuildCachedItemSet();
|
|
finishedModifying( m );
|
|
}
|
|
|
|
void
|
|
Playlist::repopulate() //SLOT
|
|
{
|
|
if( !m_dynamicMode )
|
|
return;
|
|
|
|
// Repopulate the upcoming tracks
|
|
MyIt it( this, MyIt::All );
|
|
TQPtrList<TQListViewItem> 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( TQListViewItem *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;
|
|
|
|
TQPtrList<TQListViewItem> list;
|
|
|
|
setSorting( NO_SORT );
|
|
|
|
// shuffle only VISIBLE entries
|
|
for( MyIt it( this ); *it; ++it )
|
|
list.append( *it );
|
|
|
|
// we do it in two steps because the iterator doesn't seem
|
|
// to like it when we do takeItem and ++it in the same loop
|
|
for( TQListViewItem *item = list.first(); item; item = list.next() )
|
|
takeItem( item );
|
|
|
|
//shuffle
|
|
KRandomSequence( (long)TDEApplication::random() ).randomize( &list );
|
|
|
|
//reinsert in new order
|
|
for( TQListViewItem *item = list.first(); item; item = list.next() )
|
|
insertItem( item );
|
|
|
|
updateNextPrev();
|
|
ScriptManager::instance()->notifyPlaylistChange("reordered");
|
|
}
|
|
|
|
void
|
|
Playlist::removeSelectedItems() //SLOT
|
|
{
|
|
if( isLocked() ) return;
|
|
|
|
//assemble a list of what needs removing
|
|
//calling removeItem() iteratively is more efficient if they are in _reverse_ order, hence the prepend()
|
|
PLItemList queued, list;
|
|
int dontReplaceDynamic = 0;
|
|
|
|
for( PlaylistIterator it( this, MyIt::Selected ); *it; ++it )
|
|
{
|
|
if( !(*it)->isDynamicEnabled() )
|
|
dontReplaceDynamic++;
|
|
( m_nextTracks.contains( *it ) ? queued : list ).prepend( *it );
|
|
}
|
|
|
|
if( (int)list.count() == childCount() )
|
|
{
|
|
//clear() will saveUndoState for us.
|
|
clear(); // faster
|
|
return;
|
|
}
|
|
|
|
if( list.isEmpty() && queued.isEmpty() ) return;
|
|
saveUndoState();
|
|
|
|
if( dynamicMode() )
|
|
{
|
|
int currentTracks = childCount();
|
|
int minTracks = dynamicMode()->upcomingCount();
|
|
|
|
if( m_currentTrack )
|
|
currentTracks -= currentTrackIndex() + 1;
|
|
|
|
int difference = currentTracks - minTracks;
|
|
|
|
if( difference >= 0 )
|
|
difference -= list.count();
|
|
|
|
if( difference < 0 )
|
|
{
|
|
addDynamicModeTracks( -difference );
|
|
}
|
|
}
|
|
|
|
//remove the items
|
|
if( queued.count() )
|
|
{
|
|
for( TQListViewItem *item = queued.first(); item; item = queued.next() )
|
|
removeItem( static_cast<PlaylistItem*>( item ), true );
|
|
|
|
emit queueChanged( PLItemList(), queued );
|
|
|
|
for( TQListViewItem *item = queued.first(); item; item = queued.next() )
|
|
delete item;
|
|
}
|
|
|
|
for( TQListViewItem *item = list.first(); item; item = list.next() )
|
|
{
|
|
removeItem( static_cast<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() );
|
|
TQTimer::singleShot( 0, CollectionView::instance(), TQT_SLOT( renderView() ) );
|
|
}
|
|
}
|
|
|
|
void
|
|
Playlist::removeDuplicates() //SLOT
|
|
{
|
|
// Remove dead entries
|
|
for (TQListViewItemIterator it(this); it.current(); )
|
|
{
|
|
PlaylistItem *item = static_cast<PlaylistItem*>(*it);
|
|
const KURL url = item->url();
|
|
if (url.isLocalFile() && !TQFile::exists(url.path()))
|
|
{
|
|
removeItem(item);
|
|
++it;
|
|
delete item;
|
|
}
|
|
else ++it;
|
|
}
|
|
|
|
// Remove duplicates
|
|
TQMap<KURL, bool> existingItems;
|
|
for (TQListViewItemIterator it(this); it.current(); )
|
|
{
|
|
PlaylistItem *item = static_cast<PlaylistItem*>(*it);
|
|
const KURL &itemUrl = item->url();
|
|
if (!existingItems.contains(itemUrl))
|
|
{
|
|
existingItems[itemUrl] = true;
|
|
++it;
|
|
}
|
|
else
|
|
{
|
|
removeItem(item);
|
|
++it;
|
|
delete item;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
Playlist::copyToClipboard( const TQListViewItem *item ) const //SLOT
|
|
{
|
|
if( !item ) item = currentTrack();
|
|
|
|
if( item )
|
|
{
|
|
const PlaylistItem* playlistItem = static_cast<const PlaylistItem*>( item );
|
|
|
|
TQString text = playlistItem->prettyTitle();
|
|
// For streams add the streamtitle too
|
|
//TODO make prettyTitle do this
|
|
if ( playlistItem->url().protocol() == "http" )
|
|
text.append( " :: " + playlistItem->url().url() );
|
|
|
|
// Copy both to clipboard and X11-selection
|
|
TQApplication::clipboard()->setText( text, TQClipboard::Clipboard );
|
|
TQApplication::clipboard()->setText( text, TQClipboard::Selection );
|
|
|
|
Amarok::OSD::instance()->OSDWidget::show( i18n( "Copied: %1" ).arg( text ),
|
|
TQImage(CollectionDB::instance()->albumImage(*playlistItem )) );
|
|
}
|
|
}
|
|
|
|
void Playlist::undo() //SLOT
|
|
{
|
|
if( !isLocked() )
|
|
switchState( m_undoList, m_redoList );
|
|
}
|
|
|
|
void Playlist::redo() //SLOT
|
|
{
|
|
if( !isLocked() )
|
|
switchState( m_redoList, m_undoList );
|
|
}
|
|
|
|
void
|
|
Playlist::updateMetaData( const MetaBundle &mb ) //SLOT
|
|
{
|
|
SHOULD_BE_GUI
|
|
for( MyIt it( this, MyIt::All ); *it; ++it )
|
|
if( mb.url() == (*it)->url() )
|
|
{
|
|
(*it)->copyFrom( mb );
|
|
(*it)->filter( m_filter );
|
|
}
|
|
}
|
|
|
|
void
|
|
Playlist::adjustColumn( int n )
|
|
{
|
|
if( n == PlaylistItem::Rating )
|
|
setColumnWidth( n, PlaylistItem::ratingColumnWidth() );
|
|
else if( n == PlaylistItem::Mood )
|
|
setColumnWidth( n, 120 );
|
|
else
|
|
TDEListView::adjustColumn( n );
|
|
}
|
|
|
|
void
|
|
Playlist::showQueueManager()
|
|
{
|
|
DEBUG_BLOCK
|
|
|
|
// Only show the dialog once
|
|
if( QueueManager::instance() ) {
|
|
QueueManager::instance()->raise();
|
|
return;
|
|
}
|
|
|
|
QueueManager dialog;
|
|
if( dialog.exec() == TQDialog::Accepted )
|
|
{
|
|
changeFromQueueManager(dialog.newQueue());
|
|
}
|
|
}
|
|
|
|
void
|
|
Playlist::changeFromQueueManager(TQPtrList<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 TQString &query ) //SLOT
|
|
{
|
|
m_filtertimer->stop();
|
|
if( m_filter != query )
|
|
{
|
|
m_prevfilter = m_filter;
|
|
m_filter = query;
|
|
}
|
|
m_filtertimer->start( 50, true );
|
|
}
|
|
|
|
void
|
|
Playlist::setDelayedFilter() //SLOT
|
|
{
|
|
setFilter( m_filter );
|
|
|
|
//to me it seems sensible to do this, BUT if it seems annoying to you, remove it
|
|
showCurrentTrack();
|
|
}
|
|
|
|
void
|
|
Playlist::setFilter( const TQString &query ) //SLOT
|
|
{
|
|
const bool advanced = ExpressionParser::isAdvancedExpression( query );
|
|
MyIt it( this, ( !advanced && query.lower().contains( m_prevfilter.lower() ) )
|
|
? MyIt::Visible
|
|
: MyIt::All );
|
|
|
|
|
|
if( advanced )
|
|
{
|
|
ParsedExpression parsed = ExpressionParser::parse( query );
|
|
TQValueList<int> visible = visibleColumns();
|
|
for(; *it; ++it )
|
|
(*it)->setVisible( (*it)->matchesParsedExpression( parsed, visible ) );
|
|
}
|
|
else {
|
|
// optimized path
|
|
const TQStringList terms = TQStringList::split( ' ', query.lower() );
|
|
const MetaBundle::ColumnMask visible = getVisibleColumnMask();
|
|
for(; *it; ++it ) {
|
|
(*it)->setVisible( (*it)->matchesFast(terms, visible));
|
|
}
|
|
}
|
|
|
|
if( m_filter != query )
|
|
{
|
|
m_prevfilter = m_filter;
|
|
m_filter = query;
|
|
}
|
|
updateNextPrev();
|
|
}
|
|
|
|
void
|
|
Playlist::scoreChanged( const TQString &path, float score )
|
|
{
|
|
for( MyIt it( this, MyIt::All ); *it; ++it )
|
|
{
|
|
PlaylistItem *item = static_cast<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 TQString &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 TQString &srcPath, const TQString &dstPath )
|
|
{
|
|
// Make sure the MoodServer gets this signal first!
|
|
MoodServer::instance()->slotFileMoved( srcPath, dstPath );
|
|
|
|
for( MyIt it( this, MyIt::All ); *it; ++it )
|
|
{
|
|
PlaylistItem *item = static_cast<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( TQListViewItem *item, const TQPoint &p, int col ) //SLOT
|
|
{
|
|
//if clicked on an empty area
|
|
enum { REPOPULATE, ENABLEDYNAMIC };
|
|
if( item == 0 )
|
|
{
|
|
TDEPopupMenu 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 TQString tagName = columnText( col );
|
|
const TQString tag = item->text( col );
|
|
|
|
uint itemCount = 0;
|
|
for( MyIt it( this, MyIt::Selected ); *it; ++it )
|
|
itemCount++;
|
|
|
|
PrettyPopupMenu popup;
|
|
|
|
// if(itemCount==1)
|
|
// popup.insertTitle( KStringHandler::rsqueeze( MetaBundle( item ).prettyTitle(), 50 ));
|
|
// else
|
|
// popup.insertTitle(i18n("1 Track", "%n Selected Tracks", itemCount));
|
|
|
|
if( isCurrent && isLastFm )
|
|
{
|
|
TDEActionCollection *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<TDEToggleAction *>( Amarok::actionCollection()->action( "stop_after" ) )->setChecked( m_stopAfterTrack == item );
|
|
}
|
|
|
|
if( isCurrent && itemCount == 1 )
|
|
{
|
|
popup.insertItem( SmallIconSet( Amarok::icon( "repeat_track" ) ), i18n( "&Repeat Track" ), REPEAT );
|
|
popup.setItemChecked( REPEAT, Amarok::repeatTrack() );
|
|
}
|
|
|
|
popup.insertSeparator();
|
|
|
|
if( itemCount > 1 )
|
|
{
|
|
popup.insertItem( SmallIconSet( Amarok::icon( "playlist" ) ), i18n("&Set as Playlist (Crop)"), CROP_PLAYLIST );
|
|
popup.insertItem( SmallIconSet( Amarok::icon( "save" ) ), i18n("S&ave as Playlist..."), SAVE_PLAYLIST );
|
|
}
|
|
|
|
popup.insertItem( SmallIconSet( Amarok::icon( "remove_from_playlist" ) ), i18n( "Re&move From Playlist" ), this, TQT_SLOT( removeSelectedItems() ), Key_Delete, REMOVE );
|
|
|
|
popup.insertSeparator();
|
|
|
|
TDEPopupMenu fileMenu;
|
|
if( CollectionDB::instance()->isDirInCollection( item->url().directory() ) )
|
|
{
|
|
fileMenu.insertItem( SmallIconSet( "document-save-as" ), i18n("&Organize File...", "&Organize %n Files...", itemCount), ORGANIZE );
|
|
}
|
|
else
|
|
{
|
|
fileMenu.insertItem( SmallIconSet( "document-save-as" ), i18n("&Copy Track to Collection...", "&Copy %n Tracks to Collection...", itemCount), COPY_TO_COLLECTION );
|
|
fileMenu.insertItem( SmallIconSet( "document-save-as" ), i18n("&Move Track to Collection...", "&Move %n Tracks to Collection...", itemCount), MOVE_TO_COLLECTION );
|
|
}
|
|
fileMenu.insertItem( SmallIconSet( Amarok::icon( "remove" ) ), i18n("&Delete File...", "&Delete %n Selected Files...", itemCount ), this, TQT_SLOT( deleteSelectedFiles() ), SHIFT+Key_Delete, DELETE );
|
|
popup.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n("Manage &Files"), &fileMenu, FILE_MENU );
|
|
|
|
if( itemCount == 1 )
|
|
popup.insertItem( SmallIconSet( Amarok::icon( "edit-copy" ) ), i18n( "&Copy Tags to Clipboard" ), COPY );
|
|
|
|
if( itemCount > 1 )
|
|
popup.insertItem( trackColumn
|
|
? i18n("Iteratively Assign Track &Numbers")
|
|
: i18n("&Write '%1' for Selected Tracks")
|
|
.arg( KStringHandler::rsqueeze( tag, 30 ).replace( "&", "&&" ) ), FILL_DOWN );
|
|
|
|
popup.insertItem( SmallIconSet( Amarok::icon( "edit" ) ), (itemCount == 1
|
|
? i18n( "&Edit Tag '%1'" )
|
|
: i18n( "&Edit '%1' Tag for Selected Tracks" )).arg( tagName ), EDIT );
|
|
|
|
popup.insertItem( SmallIconSet( Amarok::icon( "info" ) )
|
|
, item->url().isLocalFile() ?
|
|
i18n( "Edit Track &Information...", "Edit &Information for %n Tracks...", itemCount):
|
|
i18n( "Track &Information...", "&Information for %n Tracks...", itemCount)
|
|
, VIEW );
|
|
|
|
popup.setItemEnabled( EDIT, canRename ); //only enable for columns that have editable tags
|
|
popup.setItemEnabled( FILL_DOWN, canRename );
|
|
popup.setItemEnabled( REMOVE, !isLocked() ); // can't remove things when playlist is locked,
|
|
popup.setItemEnabled( DELETE, !isLocked() && item->url().isLocalFile() );
|
|
popup.setItemEnabled( ORGANIZE, !isLocked() && item->isKioUrl() );
|
|
popup.setItemEnabled( MOVE_TO_COLLECTION, !isLocked() && item->isKioUrl() );
|
|
popup.setItemEnabled( COPY_TO_COLLECTION, !isLocked() && item->isKioUrl() );
|
|
popup.setItemEnabled( VIEW, item->url().isLocalFile() || itemCount == 1 ); // disable for CDAudio multiselection
|
|
|
|
if( m_customSubmenuItem.count() > 0 )
|
|
popup.insertSeparator();
|
|
TQValueList<TQString> submenuTexts = m_customSubmenuItem.keys();
|
|
for( TQValueList<TQString>::Iterator keyIt =submenuTexts.begin(); keyIt != submenuTexts.end(); ++keyIt )
|
|
{
|
|
TDEPopupMenu* menu;
|
|
if( (*keyIt) == "root")
|
|
menu = &popup;
|
|
else
|
|
{
|
|
menu = new TDEPopupMenu();
|
|
popup.insertItem( *keyIt, menu);
|
|
}
|
|
foreach(m_customSubmenuItem[*keyIt])
|
|
{
|
|
int id;
|
|
if(m_customIdItem.isEmpty())
|
|
id=LAST;
|
|
else
|
|
id=m_customIdItem.keys().last()+1;
|
|
menu->insertItem( (*it), id );
|
|
m_customIdItem[id]= (*keyIt) + ' ' + (*it);
|
|
}
|
|
}
|
|
|
|
const TQPoint pos( p.x() - popup.sidePixmapWidth(), p.y() + 3 );
|
|
int menuItemId = popup.exec( pos );
|
|
PLItemList in, out;
|
|
|
|
switch( menuItemId )
|
|
{
|
|
case PLAY:
|
|
if( itemCount == 1 )
|
|
{
|
|
//Restarting track on dynamic mode
|
|
if( isCurrent && isPlaying && dynamicMode() )
|
|
m_dynamicDirt = true;
|
|
activate( item );
|
|
}
|
|
else
|
|
{
|
|
MyIt it( this, MyIt::Selected );
|
|
activate( *it );
|
|
++it;
|
|
for( int i = 0; *it; ++i, ++it )
|
|
{
|
|
in.append( *it );
|
|
m_nextTracks.insert( i, *it );
|
|
}
|
|
emit queueChanged( in, out );
|
|
}
|
|
break;
|
|
|
|
case PLAY_NEXT:
|
|
queueSelected();
|
|
break;
|
|
|
|
case VIEW:
|
|
showTagDialog( selectedItems() );
|
|
break;
|
|
|
|
case EDIT:
|
|
// do this because TQListView sucks, if track change occurs during
|
|
// an edit event, the rename operation ends, BUT, the list is not
|
|
// cleared because writeTag is never called. Q/K ListView sucks
|
|
m_itemsToChangeTagsFor.clear();
|
|
|
|
if( !item->isSelected() )
|
|
m_itemsToChangeTagsFor.append( item );
|
|
else
|
|
for( MyIt it( this, MyIt::Selected ); *it; ++it )
|
|
m_itemsToChangeTagsFor.append( *it );
|
|
|
|
rename( item, col );
|
|
break;
|
|
|
|
case FILL_DOWN:
|
|
//Spreadsheet like fill-down
|
|
{
|
|
TQString newTag = item->exactText( col );
|
|
MyIt it( this, MyIt::Selected );
|
|
|
|
//special handling for track column
|
|
uint trackNo = (*it)->track();
|
|
|
|
//we should start at the next row if we are doing track number
|
|
//and the first row has a number set
|
|
if ( trackColumn && trackNo > 0 )
|
|
++it;
|
|
|
|
ThreadManager::JobList jobs;
|
|
bool updateView = true;
|
|
for( ; *it; ++it ) {
|
|
if ( trackColumn )
|
|
//special handling for track column
|
|
newTag = TQString::number( ++trackNo );
|
|
|
|
else if ( *it == item )
|
|
//skip the one we are copying
|
|
continue;
|
|
|
|
else if( col == PlaylistItem::Score )
|
|
{
|
|
CollectionDB::instance()->setSongPercentage( (*it)->url().path(), newTag.toInt() );
|
|
continue;
|
|
}
|
|
else if( col == PlaylistItem::Rating )
|
|
{
|
|
CollectionDB::instance()->setSongRating( (*it)->url().path(), newTag.toInt() );
|
|
continue;
|
|
}
|
|
|
|
if ( !(*it)->isEditing( col ) )
|
|
jobs.prepend( new TagWriter( *it, (*it)->exactText( col ), newTag, col, updateView ) );
|
|
|
|
updateView = false;
|
|
}
|
|
|
|
ThreadManager::instance()->queueJobs( jobs );
|
|
}
|
|
break;
|
|
|
|
case COPY:
|
|
copyToClipboard( item );
|
|
break;
|
|
|
|
case CROP_PLAYLIST:
|
|
if( !isLocked() )
|
|
{
|
|
//use "in" for the other just because it's there and not used otherwise
|
|
for( MyIt it( this, MyIt::Unselected | MyIt::Visible ); *it; ++it )
|
|
( m_nextTracks.containsRef( *it ) ? in : out ).append( *it );
|
|
|
|
if( !in.isEmpty() || !out.isEmpty() )
|
|
{
|
|
saveUndoState();
|
|
|
|
for( PlaylistItem *it = out.first(); it; it = out.next() )
|
|
removeItem( it, true );
|
|
if( !out.isEmpty() )
|
|
emit queueChanged( PLItemList(), out );
|
|
for( PlaylistItem *it = out.first(); it; it = out.next() )
|
|
delete it;
|
|
|
|
for( PlaylistItem *it = in.first(); it; it = in.next() )
|
|
{
|
|
removeItem( it );
|
|
delete it;
|
|
}
|
|
ScriptManager::instance()->notifyPlaylistChange("cleared");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SAVE_PLAYLIST:
|
|
saveSelectedAsPlaylist();
|
|
break;
|
|
|
|
case REPEAT:
|
|
// FIXME HACK Accessing AmarokConfig::Enum* yields compile errors with GCC 3.3.
|
|
static_cast<TDESelectAction*>( Amarok::actionCollection()->action( "repeat" ) )
|
|
->setCurrentItem( Amarok::repeatTrack()
|
|
? 0 /*AmarokConfig::EnumRepeat::Off*/
|
|
: 1 /*AmarokConfig::EnumRepeat::Track*/ );
|
|
break;
|
|
|
|
case ORGANIZE:
|
|
case MOVE_TO_COLLECTION:
|
|
case COPY_TO_COLLECTION:
|
|
{
|
|
KURL::List list;
|
|
|
|
for( TQListViewItemIterator it( this, TQListViewItemIterator::Selected );
|
|
it.current();
|
|
++it )
|
|
{
|
|
PlaylistItem *i= static_cast<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 TQFont &old )
|
|
{
|
|
TDEListView::fontChange( old );
|
|
initStarPixmaps();
|
|
triggerUpdate();
|
|
}
|
|
|
|
void
|
|
Playlist::contentsMouseMoveEvent( TQMouseEvent *e )
|
|
{
|
|
if( e )
|
|
TDEListView::contentsMouseMoveEvent( e );
|
|
PlaylistItem *prev = m_hoveredRating;
|
|
const TQPoint pos = e ? e->pos() : viewportToContents( viewport()->mapFromGlobal( TQCursor::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() )
|
|
TQScrollView::updateContents( header()->sectionPos( PlaylistItem::Rating ) + 1, contentsY(),
|
|
header()->sectionSize( PlaylistItem::Rating ) - 2, visibleHeight() );
|
|
else
|
|
prev->updateColumn( PlaylistItem::Rating );
|
|
}
|
|
}
|
|
|
|
void Playlist::leaveEvent( TQEvent *e )
|
|
{
|
|
TDEListView::leaveEvent( e );
|
|
|
|
PlaylistItem *prev = m_hoveredRating;
|
|
m_hoveredRating = 0;
|
|
if( prev )
|
|
prev->updateColumn( PlaylistItem::Rating );
|
|
}
|
|
|
|
void Playlist::contentsMousePressEvent( TQMouseEvent *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() & TQt::ControlButton || e->state() & TQt::ShiftButton ) && // skip if ctrl or shift held
|
|
( e->button() & Qt::LeftButton ) && // only on a left click
|
|
( e->pos().x() > beginRatingSection && e->pos().x() < endRatingSection ) ) // mouse over rating column
|
|
{
|
|
int rating = item->ratingAtPoint( e->pos().x() );
|
|
|
|
if( item->isSelected() )
|
|
setSelectedRatings( rating );
|
|
else // toggle half star
|
|
CollectionDB::instance()->setSongRating( item->url().path(), rating, true );
|
|
}
|
|
else
|
|
TDEListView::contentsMousePressEvent( e );
|
|
}
|
|
|
|
void Playlist::contentsWheelEvent( TQWheelEvent *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( TQString::number( m_nextTracks.count() ) ) + 7;
|
|
if( item && column == m_firstColumn && distance <= maxdistance && item->isQueued() )
|
|
{
|
|
const int n = e->delta() / 120,
|
|
s = n / abs(n),
|
|
pos = item->queuePosition();
|
|
PLItemList changed;
|
|
for( int i = 1; i <= abs(n); ++i )
|
|
{
|
|
const int dest = pos + s*i;
|
|
if( kClamp( dest, 0, int( m_nextTracks.count() ) - 1 ) != dest )
|
|
break;
|
|
PlaylistItem* const p = m_nextTracks.at( dest );
|
|
if( changed.findRef( p ) == -1 )
|
|
changed << p;
|
|
if( changed.findRef( m_nextTracks.at( dest - s ) ) == -1 )
|
|
changed << m_nextTracks.at( dest - s );
|
|
m_nextTracks.replace( dest, m_nextTracks.at( dest - s ) );
|
|
m_nextTracks.replace( dest - s, p );
|
|
}
|
|
|
|
for( int i = 0, n = changed.count(); i < n; ++i )
|
|
changed.at(i)->update();
|
|
}
|
|
else
|
|
TDEListView::contentsWheelEvent( e );
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// Misc Private Methods
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void
|
|
Playlist::lock()
|
|
{
|
|
if( m_lockStack == 0 ) {
|
|
m_clearButton->setEnabled( false );
|
|
m_undoButton->setEnabled( false );
|
|
m_redoButton->setEnabled( false );
|
|
}
|
|
|
|
m_lockStack++;
|
|
}
|
|
|
|
void
|
|
Playlist::unlock()
|
|
{
|
|
Q_ASSERT( m_lockStack > 0 );
|
|
|
|
m_lockStack--;
|
|
|
|
if( m_lockStack == 0 ) {
|
|
m_clearButton->setEnabled( true );
|
|
m_undoButton->setEnabled( !m_undoList.isEmpty() );
|
|
m_redoButton->setEnabled( !m_redoList.isEmpty() );
|
|
}
|
|
}
|
|
|
|
int
|
|
Playlist::numVisibleColumns() const
|
|
{
|
|
int r = 0, i = 1;
|
|
for( const int n = columns(); i <= n; ++i)
|
|
if( columnWidth( i - 1 ) )
|
|
++r;
|
|
return r;
|
|
}
|
|
|
|
TQValueList<int> Playlist::visibleColumns() const
|
|
{
|
|
TQValueList<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( TQValueList<int> order, TQValueList<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 TQListView::takeItem() and TQListViewItem::takeItem() was ineffective
|
|
//NOTE we don't delete item for you! You must call delete item yourself :)
|
|
|
|
//TODO there must be a way to do this without requiring notification from the item dtor!
|
|
//NOTE orginally this was in ~PlaylistItem(), but that caused crashes due to clear() *shrug*
|
|
//NOTE items already removed by takeItem() will crash if you call nextSibling() on them
|
|
// taken items return 0 from listView()
|
|
//FIXME if you remove a series of items including the currentTrack and all the nextTracks
|
|
// then no new nextTrack will be selected and the playlist will resume from the begging
|
|
// next time
|
|
|
|
if( m_currentTrack == item )
|
|
{
|
|
setCurrentTrack( 0 );
|
|
|
|
//ensure the playlist doesn't start at the beginning after the track that's playing ends
|
|
//we don't need to do that in random mode, it's getting randomly selected anyways
|
|
if( m_nextTracks.isEmpty() && !AmarokConfig::randomMode() )
|
|
{
|
|
//*MyIt( item ) returns either "item" or if item is hidden, the next visible playlistitem
|
|
PlaylistItem* const next = *MyIt( item );
|
|
if( next )
|
|
{
|
|
m_nextTracks.append( next );
|
|
next->update();
|
|
}
|
|
}
|
|
}
|
|
|
|
if( m_stopAfterTrack == item ) {
|
|
m_stopAfterTrack = 0; //to be safe
|
|
if (stopAfterMode() != StopAfterCurrent)
|
|
setStopAfterMode( DoNotStop );
|
|
}
|
|
|
|
//cancel rename if it is pending (Bug: #147587)
|
|
if ( m_itemToRename == item ) {
|
|
m_clicktimer->stop();
|
|
m_itemToRename = 0;
|
|
}
|
|
|
|
//keep m_nextTracks queue synchronized
|
|
if( m_nextTracks.removeRef( item ) && !multi )
|
|
emit queueChanged( PLItemList(), PLItemList( item ) );
|
|
|
|
//keep recent buffer synchronized
|
|
removeFromPreviousTracks( item ); //removes all pointers to item
|
|
|
|
updateNextPrev();
|
|
}
|
|
|
|
void Playlist::ensureItemCentered( TQListViewItem *item )
|
|
{
|
|
if( !item )
|
|
return;
|
|
|
|
//HACK -- apparently the various metrics aren't reliable while the UI is still updating & stuff
|
|
m_itemToReallyCenter = item;
|
|
TQTimer::singleShot( 0, this, TQT_SLOT( reallyEnsureItemCentered() ) );
|
|
}
|
|
|
|
void
|
|
Playlist::reallyEnsureItemCentered()
|
|
{
|
|
if( TQListViewItem *item = m_itemToReallyCenter )
|
|
{
|
|
m_itemToReallyCenter = 0;
|
|
if( m_selCount == 1 )
|
|
{
|
|
PlaylistItem *previtem = *MyIt( this, MyIt::Selected );
|
|
if( previtem && previtem != item )
|
|
previtem->setSelected( false );
|
|
}
|
|
setCurrentItem( item );
|
|
ensureVisible( contentsX(), item->itemPos() + item->height() / 2, 0, visibleHeight() / 2 );
|
|
triggerUpdate();
|
|
}
|
|
}
|
|
|
|
void
|
|
Playlist::refreshNextTracks( int from )
|
|
{
|
|
// This function scans the m_nextTracks list starting from the 'from'
|
|
// position and from there on updates the progressive numbering on related
|
|
// items and repaints them. In short it performs an update subsequent to
|
|
// a renumbering/order changing at some point of the m_nextTracks list.
|
|
|
|
//start on the 'from'-th item of the list
|
|
|
|
for( PlaylistItem* item = (from == -1) ? m_nextTracks.current() : m_nextTracks.at( from );
|
|
item;
|
|
item = m_nextTracks.next() )
|
|
{
|
|
item->update();
|
|
}
|
|
}
|
|
|
|
void
|
|
Playlist::saveUndoState() //SLOT
|
|
{
|
|
if( saveState( m_undoList ) )
|
|
{
|
|
m_redoList.clear();
|
|
|
|
m_undoButton->setEnabled( true );
|
|
m_redoButton->setEnabled( false );
|
|
}
|
|
}
|
|
|
|
bool
|
|
Playlist::saveState( TQStringList &list )
|
|
{
|
|
//used by undo system, save state of playlist to undo/redo list
|
|
|
|
//do not change this! It's required by the undo/redo system to work!
|
|
//if you must change this, fix undo/redo first. Ask me what needs fixing <mxcl>
|
|
if( !isEmpty() )
|
|
{
|
|
TQString fileName;
|
|
m_undoCounter %= AmarokConfig::undoLevels();
|
|
fileName.setNum( m_undoCounter++ );
|
|
fileName.prepend( m_undoDir.absPath() + '/' );
|
|
fileName.append( ".xml" );
|
|
|
|
if ( list.count() >= (uint)AmarokConfig::undoLevels() )
|
|
{
|
|
m_undoDir.remove( list.first() );
|
|
list.pop_front();
|
|
}
|
|
|
|
saveXML( fileName );
|
|
list.append( fileName );
|
|
|
|
// Reset isNew state of all items in the playlist (determines font coloring)
|
|
PlaylistItem* item = static_cast<PlaylistItem*>( firstChild() );
|
|
while( item ) {
|
|
item->setIsNew( false );
|
|
item = item->nextSibling();
|
|
}
|
|
triggerUpdate();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
Playlist::switchState( TQStringList &loadFromMe, TQStringList &saveToMe )
|
|
{
|
|
m_undoDirt = true;
|
|
//switch to a previously saved state, remember current state
|
|
KURL url; url.setPath( loadFromMe.last() );
|
|
loadFromMe.pop_back();
|
|
|
|
//save current state
|
|
saveState( saveToMe );
|
|
|
|
//this is clear() minus some parts, for instance we don't want to cause a saveUndoState() here
|
|
m_currentTrack = 0;
|
|
disableDynamicMode();
|
|
Glow::reset();
|
|
m_prevTracks.clear();
|
|
m_prevAlbums.clear();
|
|
const PLItemList prev = m_nextTracks;
|
|
m_nextTracks.clear();
|
|
emit queueChanged( PLItemList(), prev );
|
|
ThreadManager::instance()->abortAllJobsNamed( "TagWriter" );
|
|
safeClear();
|
|
m_total = 0;
|
|
m_albums.clear();
|
|
|
|
insertMediaInternal( url, 0, 0 ); //because the listview is empty, undoState won't be forced
|
|
|
|
m_undoButton->setEnabled( !m_undoList.isEmpty() );
|
|
m_redoButton->setEnabled( !m_redoList.isEmpty() );
|
|
|
|
if( dynamicMode() ) setDynamicHistory( true );
|
|
m_undoDirt = false;
|
|
}
|
|
|
|
void
|
|
Playlist::saveSelectedAsPlaylist()
|
|
{
|
|
MyIt it( this, MyIt::Visible | MyIt::Selected );
|
|
if( !(*it) )
|
|
return; //safety
|
|
const TQString album = (*it)->album(),
|
|
artist = (*it)->artist();
|
|
int suggestion = !album.stripWhiteSpace().isEmpty() ? 1 : !artist.stripWhiteSpace().isEmpty() ? 2 : 3;
|
|
while( *it )
|
|
{
|
|
if( suggestion == 1 && (*it)->album()->lower().stripWhiteSpace() != album.lower().stripWhiteSpace() )
|
|
suggestion = 2;
|
|
if( suggestion == 2 && (*it)->artist()->lower().stripWhiteSpace() != artist.lower().stripWhiteSpace() )
|
|
suggestion = 3;
|
|
if( suggestion == 3 )
|
|
break;
|
|
++it;
|
|
}
|
|
TQString path = PlaylistDialog::getSaveFileName( suggestion == 1 ? album
|
|
: suggestion == 2 ? artist
|
|
: i18n( "Untitled" ) );
|
|
|
|
if( path.isEmpty() )
|
|
return;
|
|
|
|
TQValueList<KURL> urls;
|
|
TQValueList<TQString> titles;
|
|
TQValueList<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, TQListViewItem *after, const TQPoint &p, int col ) //SLOT
|
|
{
|
|
switch( button )
|
|
{
|
|
case Qt::MidButton:
|
|
{
|
|
const TQString path = TQApplication::clipboard()->text( TQClipboard::Selection );
|
|
const KURL url = KURL::fromPathOrURL( path );
|
|
|
|
if( url.isValid() )
|
|
insertMediaInternal( url, static_cast<PlaylistItem*>(after ? after : lastItem()) );
|
|
|
|
break;
|
|
}
|
|
|
|
case Qt::RightButton:
|
|
showContextMenu( after, p, col );
|
|
break;
|
|
|
|
default:
|
|
;
|
|
}
|
|
}
|
|
|
|
void Playlist::slotContentsMoving()
|
|
{
|
|
Amarok::ToolTip::hideTips();
|
|
TQTimer::singleShot( 0, this, TQT_SLOT( contentsMouseMoveEvent() ) );
|
|
}
|
|
|
|
void
|
|
Playlist::slotQueueChanged( const PLItemList &/*in*/, const PLItemList &out)
|
|
{
|
|
for( TQPtrListIterator<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 = TQColor( r, g, b );
|
|
}
|
|
|
|
{
|
|
using namespace Text;
|
|
PlaylistItem::glowText = TQColor( r + int(d*dr), g + int(d*dg), b + int(d*db) );
|
|
}
|
|
|
|
if( currentTrack() )
|
|
currentTrack()->update();
|
|
}
|
|
|
|
++counter &= 63; //built in bounds checking with &=
|
|
}
|
|
|
|
void
|
|
Playlist::slotRepeatTrackToggled( int /* mode */ )
|
|
{
|
|
if( m_currentTrack )
|
|
m_currentTrack->update();
|
|
}
|
|
|
|
void
|
|
Playlist::slotEraseMarker() //SLOT
|
|
{
|
|
if( m_marker )
|
|
{
|
|
const TQRect spot = drawDropVisualizer( 0, 0, m_marker );
|
|
m_marker = 0;
|
|
viewport()->repaint( spot, false );
|
|
}
|
|
}
|
|
|
|
void
|
|
Playlist::showTagDialog( TQPtrList<TQListViewItem> 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( TQListViewItem *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 <tqgroupbox.h>
|
|
#include <tqlabel.h>
|
|
#include <tqlayout.h>
|
|
#include <tqprocess.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( TQWidget *parent )
|
|
: KDialog( parent )
|
|
{
|
|
TQLabel *textLabel1, *textLabel2, *textLabel3;
|
|
TQLineEdit *lineEdit1, *lineEdit2;
|
|
TQGroupBox *groupBox1;
|
|
|
|
textLabel1 = new TQLabel( 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 TQLabel( i18n( "Column &name:" ), this );
|
|
textLabel3 = new TQLabel( i18n( "&Command:" ), this );
|
|
|
|
lineEdit1 = new TQLineEdit( this, "ColumnName" );
|
|
lineEdit2 = new TQLineEdit( this, "Command" );
|
|
|
|
groupBox1 = new TQGroupBox( 1, Qt::Vertical, i18n( "Examples" ), this );
|
|
groupBox1->layout()->setMargin( 11 );
|
|
new KActiveLabel( i18n( "file --brief %f\n" "ls -sh %f\n" "basename %f\n" "dirname %f" ), groupBox1 );
|
|
|
|
// buddies
|
|
textLabel2->setBuddy( lineEdit1 );
|
|
textLabel3->setBuddy( lineEdit2 );
|
|
|
|
// layouts
|
|
TQHBoxLayout *layout1 = new TQHBoxLayout( 0, 0, 6 );
|
|
layout1->addItem( new TQSpacerItem( 181, 20, TQSizePolicy::Expanding, TQSizePolicy::Minimum ) );
|
|
layout1->addWidget( new KPushButton( KStdGuiItem::ok(), this, "OkButton" ) );
|
|
layout1->addWidget( new KPushButton( KStdGuiItem::cancel(), this, "CancelButton" ) );
|
|
|
|
TQGridLayout *layout2 = new TQGridLayout( 0, 2, 2, 0, 6 );
|
|
layout2->TQLayout::add( textLabel2 );
|
|
layout2->TQLayout::add( lineEdit1 );
|
|
layout2->TQLayout::add( textLabel3 );
|
|
layout2->TQLayout::add( lineEdit2 );
|
|
|
|
TQVBoxLayout *Form1Layout = new TQVBoxLayout( this, 11, 6, "Form1Layout");
|
|
Form1Layout->addWidget( textLabel1 );
|
|
Form1Layout->addWidget( groupBox1 );
|
|
Form1Layout->addLayout( layout2 );
|
|
Form1Layout->addLayout( layout1 );
|
|
Form1Layout->addItem( new TQSpacerItem( 20, 231, TQSizePolicy::Minimum, TQSizePolicy::Expanding ) );
|
|
|
|
// properties
|
|
setCaption( i18n("Add Custom Column") );
|
|
|
|
// connects
|
|
connect(
|
|
child( "OkButton" ),
|
|
TQT_SIGNAL(clicked()),
|
|
TQT_SLOT(accept()) );
|
|
connect(
|
|
child( "CancelButton" ),
|
|
TQT_SIGNAL(clicked()),
|
|
TQT_SLOT(reject()) );
|
|
}
|
|
|
|
TQString command() { return static_cast<KLineEdit*>(TQT_TQWIDGET(child("Command")))->text(); }
|
|
TQString name() { return static_cast<KLineEdit*>(TQT_TQWIDGET(child("ColumnName")))->text(); }
|
|
};
|
|
|
|
void
|
|
Playlist::addCustomColumn()
|
|
{
|
|
CustomColumnDialog dialog( this );
|
|
|
|
if ( dialog.exec() == TQDialog::Accepted ) {
|
|
const int index = addColumn( dialog.name(), 100 );
|
|
TQStringList args = TQStringList::split( ' ', dialog.command() );
|
|
|
|
TQStringList::Iterator pcf = args.find( "%f" );
|
|
if ( pcf == args.end() ) {
|
|
//there is no %f, so add one on the end
|
|
//TODO prolly this is confusing, instead ask the user if we should add one
|
|
args += "%f";
|
|
--pcf;
|
|
}
|
|
|
|
debug() << args << endl;
|
|
|
|
//TODO need to do it with a %u for url and %f for file
|
|
//FIXME gets stuck it seems if you submit broken commands
|
|
|
|
//FIXME issues with the column resize stuff that cause freezing in eventFilters
|
|
|
|
for( MyIt it( this ); *it; ++it ) {
|
|
if( (*it)->url().protocol() != "file" )
|
|
continue;
|
|
|
|
*pcf = (*it)->url().path();
|
|
|
|
debug() << args << endl;
|
|
|
|
TQProcess p( args );
|
|
for( p.start(); p.isRunning(); /*kapp->processEvents()*/ )
|
|
::usleep( 5000 );
|
|
|
|
(*it)->setExactText( index, p.readStdout() );
|
|
}
|
|
}
|
|
}
|
|
|
|
#include <taglib/fileref.h>
|
|
#include <taglib/tag.h>
|
|
|
|
TagWriter::TagWriter( PlaylistItem *item, const TQString &oldTag, const TQString &newTag, const int col, const bool updateView )
|
|
: ThreadManager::Job( "TagWriter" )
|
|
, m_item( item )
|
|
, m_failed( true )
|
|
, m_oldTagString( oldTag )
|
|
, m_newTagString( newTag )
|
|
, m_tagType( col )
|
|
, m_updateView( updateView )
|
|
{
|
|
Playlist::instance()->lock();
|
|
|
|
item->setEditing( col );
|
|
}
|
|
|
|
TagWriter::~TagWriter()
|
|
{
|
|
Playlist::instance()->unlock();
|
|
}
|
|
|
|
bool
|
|
TagWriter::doJob()
|
|
{
|
|
MetaBundle mb( m_item->url(), true );
|
|
|
|
switch ( m_tagType )
|
|
{
|
|
case PlaylistItem::Title:
|
|
mb.setTitle( m_newTagString );
|
|
break;
|
|
case PlaylistItem::Artist:
|
|
mb.setArtist( m_newTagString );
|
|
break;
|
|
case PlaylistItem::Composer:
|
|
if ( !mb.hasExtendedMetaInformation() )
|
|
return true;
|
|
mb.setComposer( m_newTagString );
|
|
break;
|
|
case PlaylistItem::DiscNumber:
|
|
if ( !mb.hasExtendedMetaInformation() )
|
|
return true;
|
|
mb.setDiscNumber( m_newTagString.toInt() );
|
|
break;
|
|
case PlaylistItem::Bpm:
|
|
if ( !mb.hasExtendedMetaInformation() )
|
|
return true;
|
|
mb.setBpm( m_newTagString.toFloat() );
|
|
break;
|
|
case PlaylistItem::Album:
|
|
mb.setAlbum( m_newTagString );
|
|
break;
|
|
case PlaylistItem::Year:
|
|
mb.setYear( m_newTagString.toInt() );
|
|
break;
|
|
case PlaylistItem::Comment:
|
|
//FIXME how does this work for vorbis files?
|
|
//Are we likely to overwrite some other comments?
|
|
//Vorbis can have multiple comment fields..
|
|
mb.setComment( m_newTagString );
|
|
break;
|
|
case PlaylistItem::Genre:
|
|
mb.setGenre( m_newTagString );
|
|
break;
|
|
case PlaylistItem::Track:
|
|
mb.setTrack( m_newTagString.toInt() );
|
|
break;
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
|
|
m_failed = !mb.save();
|
|
return true;
|
|
}
|
|
|
|
void
|
|
TagWriter::completeJob()
|
|
{
|
|
switch( m_failed ) {
|
|
case true:
|
|
// we write a space for some reason I cannot recall
|
|
m_item->setExactText( m_tagType, m_oldTagString.isEmpty() ? " " : m_oldTagString );
|
|
Amarok::StatusBar::instance()->longMessage( i18n(
|
|
"Sorry, the tag for %1 could not be changed." ).arg( m_item->url().fileName() ), KDE::StatusBar::Sorry );
|
|
break;
|
|
|
|
case false:
|
|
m_item->setExactText( m_tagType, m_newTagString.isEmpty() ? " " : m_newTagString );
|
|
CollectionDB::instance()->updateURL( m_item->url().path(), m_updateView );
|
|
|
|
}
|
|
m_item->setIsBeingRenamed( false );
|
|
m_item->filter( Playlist::instance()->m_filter );
|
|
if( m_item->deleteAfterEditing() )
|
|
{
|
|
Playlist::instance()->removeItem( m_item );
|
|
delete m_item;
|
|
}
|
|
}
|
|
|
|
|
|
#include "playlist.moc"
|