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.
amarok/amarok/src/playlist.cpp

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 <tqsortedlist.h>
#include <tqtimer.h>
#include <tqtooltip.h>
#include <tqvaluelist.h> //addHybridTracks()
#include <tqvaluevector.h> //playNextTrack()
#include <tqlayout.h>
#include <kaction.h>
#include <kapplication.h>
#include <kcursor.h> //setOverrideCursor()
#include <kdialogbase.h>
#include <kglobalsettings.h> //rename()
#include <kiconeffect.h>
#include <kiconloader.h> //slotShowContextMenu()
#include <kio/job.h> //deleteSelectedFiles()
#include <klineedit.h> //setCurrentTrack()
#include <klocale.h>
#include <kmessagebox.h>
#include <kpopupmenu.h>
#include <krandomsequence.h> //random Mode
#include <kstandarddirs.h> //KGlobal::dirs()
#include <kstdaction.h>
#include <kstringhandler.h> //::showContextMenu()
#include <kurldrag.h>
#include <cstdlib> // abs
extern "C"
{
#if KDE_VERSION < KDE_MAKE_VERSION(3,3,91)
#include <X11/Xlib.h> //ControlMask in contentsDragMoveEvent()
#endif
}
#include "playlist.h"
namespace Amarok
{
const DynamicMode *dynamicMode() { return Playlist::instance() ? Playlist::instance()->dynamicMode() : 0; }
}
typedef PlaylistIterator MyIt;
//////////////////////////////////////////////////////////////////////////////////////////
/// CLASS TagWriter : Threaded tag-updating
//////////////////////////////////////////////////////////////////////////////////////////
class TagWriter : public ThreadManager::Job
{ //TODO make this do all tags at once when you split playlist.cpp up
public:
TagWriter( PlaylistItem*, const TQString &oldTag, const TQString &newTag, const int, const bool updateView = true );
~TagWriter();
bool doJob();
void completeJob();
private:
PlaylistItem* const m_item;
bool m_failed;
TQString m_oldTagString;
TQString m_newTagString;
int m_tagType;
bool m_updateView;
};
//////////////////////////////////////////////////////////////////////////////////////////
/// Glow
//////////////////////////////////////////////////////////////////////////////////////////
namespace Glow
{
namespace Text
{
static float dr, dg, db;
static int r, g, b;
}
namespace Base
{
static float dr, dg, db;
static int r, g, b;
}
static const uint STEPS = 13;
static uint counter;
static TQTimer timer;
inline void startTimer()
{
counter = 0;
timer.start( 40 );
}
inline void reset()
{
counter = 0;
timer.stop();
}
}
//////////////////////////////////////////////////////////////////////////////////////////
/// CLASS Playlist
//////////////////////////////////////////////////////////////////////////////////////////
TQMutex* Playlist::s_dynamicADTMutex = new TQMutex();
Playlist *Playlist::s_instance = 0;
Playlist::Playlist( TQWidget *parent )
: KListView( parent, "ThePlaylist" )
, EngineObserver( EngineController::instance() )
, m_startupTime_t( TQDateTime::currentDateTime().toTime_t() )
, m_oldestTime_t( CollectionDB::instance()->query( "SELECT MIN( createdate ) FROM statistics;" ).first().toInt() )
, m_currentTrack( 0 )
, m_marker( 0 )
, m_hoveredRating( 0 )
, m_firstColumn( 0 )
, m_totalCount( 0 )
, m_totalLength( 0 )
, m_selCount( 0 )
, m_selLength( 0 )
, m_visCount( 0 )
, m_visLength( 0 )
, m_total( 0 )
, m_itemCountDirty( false )
, m_undoButton( 0 )
, m_redoButton( 0 )
, m_clearButton( 0 )
, m_undoDir( Amarok::saveLocation( "undo/" ) )
, m_undoCounter( 0 )
, m_dynamicMode( 0 )
, m_stopAfterTrack( 0 )
, m_stopAfterMode( DoNotStop )
, m_showHelp( true )
, m_dynamicDirt( false )
, m_queueDirt( false )
, m_undoDirt( false )
, m_insertFromADT( 0 )
, m_itemToReallyCenter( 0 )
, m_renameItem( 0 )
, m_lockStack( 0 )
, m_columnFraction( PlaylistItem::NUM_COLUMNS, 0 )
, m_oldRandom( 0 )
, m_oldRepeat( 0 )
, m_playlistName( i18n( "Untitled" ) )
, m_proposeOverwriting( false )
, m_urlIndex( &PlaylistItem::url )
{
s_instance = this;
connect( CollectionDB::instance(), TQT_SIGNAL(fileMoved(const TQString&,
const TQString&, const TQString&)), TQT_SLOT(updateEntriesUrl(const TQString&,
const TQString&, const TQString&)) );
connect( CollectionDB::instance(), TQT_SIGNAL(uniqueIdChanged(const TQString&,
const TQString&, const TQString&)), TQT_SLOT(updateEntriesUniqueId(const TQString&,
const TQString&, const TQString&)) );
connect( CollectionDB::instance(), TQT_SIGNAL(fileDeleted(const TQString&,
const TQString&)), TQT_SLOT(updateEntriesStatusDeleted(const TQString&, const TQString&)) );
connect( CollectionDB::instance(), TQT_SIGNAL(fileAdded(const TQString&,
const TQString&)), TQT_SLOT(updateEntriesStatusAdded(const TQString&, const TQString&)) );
connect( CollectionDB::instance(), TQT_SIGNAL(filesAdded(const TQMap<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()) );
KActionCollection* const ac = Amarok::actionCollection();
KAction *copy = KStdAction::copy( TQT_TQOBJECT(this), TQT_SLOT( copyToClipboard() ), ac, "playlist_copy" );
KStdAction::selectAll( TQT_TQOBJECT(this), TQT_SLOT( selectAll() ), ac, "playlist_select_all" );
m_clearButton = new KAction( i18n( "clear playlist", "&Clear" ), Amarok::icon( "playlist_clear" ), 0, TQT_TQOBJECT(this), TQT_SLOT( clear() ), ac, "playlist_clear" );
m_undoButton = KStdAction::undo( TQT_TQOBJECT(this), TQT_SLOT( undo() ), ac, "playlist_undo" );
m_redoButton = KStdAction::redo( TQT_TQOBJECT(this), TQT_SLOT( redo() ), ac, "playlist_redo" );
m_undoButton ->setIcon( Amarok::icon( "undo" ) );
m_redoButton ->setIcon( Amarok::icon( "redo" ) );
new KAction( i18n( "&Repopulate" ), Amarok::icon( "playlist_refresh" ), 0, TQT_TQOBJECT(this), TQT_SLOT( repopulate() ), ac, "repopulate" );
new KAction( i18n( "S&huffle" ), "rebuild", CTRL+Key_H, TQT_TQOBJECT(this), TQT_SLOT( shuffle() ), ac, "playlist_shuffle" );
KAction *gotoCurrent = new KAction( i18n( "&Go To Current Track" ), Amarok::icon( "music" ), CTRL+Key_J, TQT_TQOBJECT(this), TQT_SLOT( showCurrentTrack() ), ac, "playlist_show" );
new KAction( i18n( "&Remove Duplicate && Dead Entries" ), 0, TQT_TQOBJECT(this), TQT_SLOT( removeDuplicates() ), ac, "playlist_remove_duplicates" );
new KAction( i18n( "&Queue Selected Tracks" ), Amarok::icon( "queue_track" ), CTRL+Key_D, TQT_TQOBJECT(this), TQT_SLOT( queueSelected() ), ac, "queue_selected" );
KToggleAction *stopafter = new KToggleAction( i18n( "&Stop Playing After Track" ), Amarok::icon( "stop" ), CTRL+ALT+Key_V,
TQT_TQOBJECT(this), TQT_SLOT( toggleStopAfterCurrentItem() ), ac, "stop_after" );
{ // KAction idiocy -- shortcuts don't work until they've been plugged into a menu
KPopupMenu asdf;
copy->plug( &asdf );
stopafter->plug( &asdf );
gotoCurrent->plug( &asdf );
copy->unplug( &asdf );
stopafter->unplug( &asdf );
gotoCurrent->unplug( &asdf );
}
//ensure we update action enabled states when repeat Playlist is toggled
connect( ac->action( "repeat" ), TQT_SIGNAL(activated( int )), TQT_SLOT(updateNextPrev()) );
connect( ac->action( "repeat" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) );
connect( ac->action( "favor_tracks" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) );
connect( ac->action( "random_mode" ), TQT_SIGNAL( activated( int ) ), TQT_SLOT( generateInfo() ) );
// undostates are written in chronological order, so this is a clever way to get them back in the correct order :)
TQStringList undos = m_undoDir.entryList( TQString("*.xml"), TQDir::Files, TQDir::Time );
foreach( undos )
m_undoList.append( m_undoDir.absPath() + '/' + (*it) );
m_undoCounter = m_undoList.count();
m_undoButton->setEnabled( !m_undoList.isEmpty() );
m_redoButton->setEnabled( false );
engineStateChanged( EngineController::engine()->state() ); //initialise state of UI
paletteChange( palette() ); //sets up glowColors
restoreLayout( KGlobal::config(), "PlaylistColumnsLayout" );
// Sorting must be disabled when current.xml is being loaded. See BUG 113042
KListView::setSorting( NO_SORT ); //use base so we don't saveUndoState() too
setDynamicMode( 0 );
m_smartResizing = Amarok::config( "PlaylistWindow" )->readBoolEntry( "Smart Resizing", true );
columnOrderChanged();
//cause the column fractions to be updated, but in a safe way, ie no specific column
columnResizeEvent( header()->count(), 0, 0 );
//do after you resize all the columns
connect( header(), TQT_SIGNAL(sizeChange( int, int, int )), TQT_SLOT(columnResizeEvent( int, int, int )) );
connect( this, TQT_SIGNAL( contentsMoving( int, int ) ), TQT_SLOT( slotContentsMoving() ) );
connect( App::instance(), TQT_SIGNAL( useScores( bool ) ), this, TQT_SLOT( slotUseScores( bool ) ) );
connect( App::instance(), TQT_SIGNAL( useRatings( bool ) ), this, TQT_SLOT( slotUseRatings( bool ) ) );
connect( App::instance(), TQT_SIGNAL( moodbarPrefs( bool, bool, int, bool ) ),
this, TQT_SLOT( slotMoodbarPrefs( bool, bool, int, bool ) ) );
Amarok::ToolTip::add( this, viewport() );
header()->installEventFilter( this );
renameLineEdit()->installEventFilter( this );
setTabOrderedRenaming( false );
m_filtertimer = new TQTimer( this );
connect( m_filtertimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(setDelayedFilter()) );
connect( MountPointManager::instance(), TQT_SIGNAL(mediumConnected( int )),
TQT_SLOT(mediumChange( int )) );
connect( MountPointManager::instance(), TQT_SIGNAL(mediumRemoved( int )),
TQT_SLOT(mediumChange( int )) );
m_clicktimer = new TQTimer( this );
connect( m_clicktimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotSingleClick()) );
}
Playlist::~Playlist()
{
saveLayout( KGlobal::config(), "PlaylistColumnsLayout" );
if( AmarokConfig::savePlaylist() && m_lockStack == 0 ) saveXML( defaultPlaylistPath() );
//speed up quit a little
safeClear(); //our implementation is slow
Amarok::ToolTip::remove( viewport() );
blockSignals( true ); //might help
s_instance = 0;
}
////////////////////////////////////////////////////////////////////////////////
/// Media Handling
////////////////////////////////////////////////////////////////////////////////
void
Playlist::mediumChange( int deviceid ) // SLOT
{
Q_UNUSED( deviceid );
for( TQListViewItem *it = firstChild();
it;
it = it->nextSibling() )
{
PlaylistItem *p = dynamic_cast<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 klistview.cpp, are largely
Copyright (C) 2000 Reginald Stadlbauer <reggie@kde.org>
Copyright (C) 2000,2003 Charles Samuels <charles@kde.org>
Copyright (C) 2000 Peter Putzer
*/
void Playlist::saveLayout(KConfig *config, const TQString &group) const
{
KConfigGroupSaver saver(config, group);
TQStringList names, widths, order;
const int colCount = columns();
TQHeader* const thisHeader = header();
for (int i = 0; i < colCount; ++i)
{
names << PlaylistItem::exactColumnName(i);
widths << TQString::number(columnWidth(i));
order << TQString::number(thisHeader->mapToIndex(i));
}
config->writeEntry("ColumnsVersion", 1);
config->writeEntry("ColumnNames", names);
config->writeEntry("ColumnWidths", widths);
config->writeEntry("ColumnOrder", order);
config->writeEntry("SortColumn", columnSorted());
config->writeEntry("SortAscending", ascendingSort());
}
void Playlist::restoreLayout(KConfig *config, const TQString &group)
{
KConfigGroupSaver saver(config, group);
int version = config->readNumEntry("ColumnsVersion", 0);
TQValueList<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( KApplication::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( ( KApplication::random() / pow( 2, sizeof( int ) * 8 ) )
* pow( 2, 64 ) );
random = r % total;
}
else
random = KApplication::random() % total;
int i = 0;
for( int n = tracks.count(); i < n && random >= 0; ++i )
random -= weights.at( i );
item = tracks.at( i-1 );
}
}
}
else if( item )
{
item = MyIt::nextVisible( item );
while( item && ( !checkFileStatus( item ) || !item->exists() ) )
item = MyIt::nextVisible( item );
}
else
{
item = *MyIt( this ); //ie. first visible item
while( item && ( !checkFileStatus( item ) || !item->exists() ) )
item = item->nextSibling();
}
if ( dynamicMode() && item != firstChild() )
{
if( currentTrack() )
currentTrack()->setDynamicEnabled( false );
advanceDynamicTrack();
}
if ( !item && Amarok::repeatPlaylist() )
item = *MyIt( this ); //ie. first visible item
}
if ( EngineController::engine()->loaded() )
activate( item );
else
setCurrentTrack( item );
}
//This is called before setCurrentItem( item );
void
Playlist::advanceDynamicTrack()
{
int x = currentTrackIndex();
bool didDelete = false;
if( dynamicMode()->cycleTracks() )
{
if( x >= dynamicMode()->previousCount() )
{
PlaylistItem *first = firstChild();
removeItem( first );
delete first;
didDelete = true;
}
}
const int upcomingTracks = childCount() - x - 1;
// Just starting to play from stopped, don't append something needlessely
// or, we have more than enough items in the queue.
bool dontAppend = ( !didDelete &&
( EngineController::instance()->engine()->state() == Engine::Empty ) ) ||
upcomingTracks > dynamicMode()->upcomingCount();
//keep upcomingTracks requirement, this seems to break StopAfterCurrent
if( !dontAppend && stopAfterMode() != StopAfterCurrent )
{
s_dynamicADTMutex->lock();
m_insertFromADT++;
s_dynamicADTMutex->unlock();
addDynamicModeTracks( 1 );
}
m_dynamicDirt = true;
}
void
Playlist::playPrevTrack()
{
PlaylistItem *item = currentTrack();
if( Amarok::entireAlbums() )
{
item = 0;
if( m_currentTrack )
{
item = m_currentTrack->prevInAlbum();
if( !item && Amarok::repeatAlbum() && m_currentTrack->m_album->tracks.count() )
item = m_currentTrack->m_album->tracks.getLast();
}
if( !item )
{
PlaylistAlbum* a = m_prevAlbums.last();
while( a && !a->tracks.count() )
{
removeFromPreviousAlbums();
a = m_prevAlbums.last();
}
if( a )
{
item = a->tracks.getLast();
removeFromPreviousAlbums();
}
}
if( !item )
{
item = *static_cast<MyIt&>(--MyIt( item ));
while( item && !checkFileStatus( item ) )
item = *static_cast<MyIt&>(--MyIt( item ));
}
}
else
{
if( dynamicMode() )
{
}
else if( !AmarokConfig::randomMode() || m_prevTracks.count() <= 1 )
{
if( item )
{
item = MyIt::prevVisible( item );
while( item && ( !checkFileStatus( item ) || !item->isEnabled() ) )
item = MyIt::prevVisible( item );
}
else
{
item = *MyIt( this ); //ie. first visible item
while( item && ( !checkFileStatus( item ) || !item->isEnabled() ) )
item = item->nextSibling();
}
}
else {
// if enough songs in buffer, jump to the previous one
m_prevTracks.last();
removeFromPreviousTracks(); //remove the track playing now
item = m_prevTracks.last();
// we need to remove this item now, since it will be added in activate() again
removeFromPreviousTracks();
}
}
if ( !item && Amarok::repeatPlaylist() )
item = *MyIt( lastItem() ); //TODO check this works!
if ( EngineController::engine()->loaded() )
activate( item );
else
setCurrentTrack( item );
}
void
Playlist::playCurrentTrack()
{
if ( !currentTrack() )
playNextTrack( Amarok::repeatTrack() );
//we must do this even if the above is correct
//since the engine is not loaded the first time the user presses play
//then calling the next() function wont play it
activate( currentTrack() );
}
void
Playlist::setSelectedRatings( int rating )
{
if( !m_selCount && currentItem() && currentItem()->isVisible() )
CollectionDB::instance()->setSongRating( currentItem()->url().path(), rating, true );
else
for( MyIt it( this, MyIt::Selected ); *it; ++it )
CollectionDB::instance()->setSongRating( (*it)->url().path(), rating, true );
}
void
Playlist::queueSelected()
{
PLItemList in, out;
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( KIconLoader().iconPath( "amarok", -KIcon::SizeHuge ) ) );
return;
}
#define item static_cast<PlaylistItem*>(item)
if ( !checkFileStatus( item ) )
{
Amarok::StatusBar::instance()->shortMessage( i18n("Local file does not exist.") );
return;
}
if( dynamicMode() && !Amarok::repeatTrack() )
{
if( m_currentTrack && item->isDynamicEnabled() )
{
if( item != m_currentTrack )
this->moveItem( item, 0, m_currentTrack );
}
else
{
MyIt it( this, MyIt::Visible );
bool hasHistory = false;
if ( *it && !(*it)->isDynamicEnabled() )
{
hasHistory = true;
for( ; *it && !(*it)->isDynamicEnabled() ; ++it );
}
if( item->isDynamicEnabled() )
{
hasHistory ?
this->moveItem( item, 0, *it ) :
this->moveItem( item, 0, 0 );
}
else // !item->isDynamicEnabled()
{
hasHistory ?
insertMediaInternal( item->url(), *it ):
insertMediaInternal( item->url(), 0 );
m_dynamicDirt = true;
return;
}
}
if( !m_dynamicDirt && m_currentTrack && m_currentTrack != item )
{
m_currentTrack->setDynamicEnabled( false );
advanceDynamicTrack();
}
}
if( Amarok::entireAlbums() )
{
if( !item->nextInAlbum() )
appendToPreviousAlbums( item->m_album );
}
else
appendToPreviousTracks( item );
//if we are playing something from the next tracks
//list, remove it from the list
if( m_nextTracks.removeRef( item ) )
emit queueChanged( PLItemList(), PLItemList( item ) );
//looks bad painting selected and glowing
//only do when user explicitly activates an item though
item->setSelected( false );
setCurrentTrack( item );
m_dynamicDirt = false;
//use PlaylistItem::MetaBundle as it also updates the audioProps
EngineController::instance()->play( *item );
#undef item
}
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( "&", "&amp;" ).replace( "<", "&lt;" ).replace( ">", "&gt;" );
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 );
}
////////////////////////////////////////////////////////////////////////////////
/// KListView Reimplementation
////////////////////////////////////////////////////////////////////////////////
void
Playlist::appendMedia( const TQString &path )
{
appendMedia( KURL::fromPathOrURL( path ) );
}
void
Playlist::appendMedia( const KURL &url )
{
insertMedia( KURL::List( url ) );
}
void
Playlist::clear() //SLOT
{
if( isLocked() || renameLineEdit()->isVisible() ) return;
disableDynamicMode();
emit aboutToClear(); //will saveUndoState()
setCurrentTrack( 0 );
m_prevTracks.clear();
m_prevAlbums.clear();
if (m_stopAfterTrack) {
m_stopAfterTrack = 0;
if ( stopAfterMode() != StopAfterCurrent ) {
setStopAfterMode( DoNotStop );
}
}
const PLItemList prev = m_nextTracks;
m_nextTracks.clear();
emit queueChanged( PLItemList(), prev );
// Update player button states
Amarok::actionCollection()->action( "play" )->setEnabled( false );
Amarok::actionCollection()->action( "prev" )->setEnabled( false );
Amarok::actionCollection()->action( "next" )->setEnabled( false );
Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( false );
ThreadManager::instance()->abortAllJobsNamed( "TagWriter" );
// something to bear in mind, if there is any event in the loop
// that depends on a PlaylistItem, we are about to crash Amarok
// never unlock() the Playlist until it is safe!
safeClear();
m_total = 0;
m_albums.clear();
setPlaylistName( i18n( "Untitled" ) );
ScriptManager::instance()->notifyPlaylistChange("cleared");
}
/**
* Workaround for TQt 3.3.5 bug in TQListView::clear()
* @see http://lists.kde.org/?l=kde-devel&m=113113845120155&w=2
* @see BUG 116004
*/
void
Playlist::safeClear()
{
/* 3.3.5 and 3.3.6 have bad KListView::clear() functions.
3.3.5 forgets to clear the pointer to the highlighted item.
3.3.6 forgets to clear the pointer to the last dragged item */
if ( strcmp( qVersion(), "3.3.5" ) == 0
|| strcmp( qVersion(), "3.3.6" ) == 0 )
{
bool block = signalsBlocked();
blockSignals( true );
clearSelection();
TQListViewItem *c = firstChild();
TQListViewItem *n;
while( c ) {
n = c->nextSibling();
if ( !static_cast<PlaylistItem *>( c )->isEmpty() ) //avoid deleting markers
delete c;
c = n;
}
blockSignals( block );
triggerUpdate();
}
else
KListView::clear();
}
void
Playlist::setSorting( int col, bool b )
{
saveUndoState();
//HACK There are reasons to allow sorting in dynamic mode, but
//it breaks other things that I don't have the time or patience
//to figure out...at least right now
if( !dynamicMode() )
KListView::setSorting( col, b );
}
void
Playlist::setColumnWidth( int col, int width )
{
KListView::setColumnWidth( col, width );
//FIXME this is because TQt doesn't by default disable resizing width 0 columns. GRRR!
//NOTE default column sizes are stored in default amarokrc so that restoreLayout() in ctor will
// call this function. This is necessary because addColumn() doesn't call setColumnWidth() GRRR!
header()->setResizeEnabled( width != 0, col );
}
void
Playlist::rename( TQListViewItem *item, int column ) //SLOT
{
if( !item )
return;
switch( column )
{
case PlaylistItem::Artist:
renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->artistList() );
break;
case PlaylistItem::Album:
renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->albumList() );
break;
case PlaylistItem::Genre:
renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->genreList() );
break;
case PlaylistItem::Composer:
renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->composerList() );
break;
default:
renameLineEdit()->completionObject()->clear();
break;
}
renameLineEdit()->completionObject()->setCompletionMode( KGlobalSettings::CompletionPopupAuto );
renameLineEdit()->completionObject()->setIgnoreCase( true );
m_editOldTag = static_cast<PlaylistItem *>(item)->exactText( column );
if( m_selCount <= 1 )
{
if( currentItem() )
currentItem()->setSelected( false );
item->setSelected( true );
}
setCurrentItem( item );
KListView::rename( item, column );
m_renameItem = item;
m_renameColumn = column;
static_cast<PlaylistItem*>(item)->setIsBeingRenamed( true );
}
void
Playlist::writeTag( 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();
}
KListView::paletteChange( p );
counter = 0; // reset the counter or apparently the text lacks contrast
slotGlowTimer(); // repaint currentTrack marker
}
void
Playlist::contentsDragEnterEvent( TQDragEnterEvent *e )
{
TQString data;
TQCString subtype;
TQTextDrag::decode( e, data, subtype );
e->accept(
e->source() == viewport() ||
subtype == "amarok-sql" ||
subtype == "uri-list" || //this is to prevent DelayedUrlLists from performing their queries
KURLDrag::canDecode( e ) );
}
void
Playlist::contentsDragMoveEvent( TQDragMoveEvent* e )
{
if( !e->isAccepted() ) return;
#if KDE_IS_VERSION( 3, 3, 91 )
const bool ctrlPressed = KApplication::keyboardMouseState() & TQt::ControlButton;
#else
const bool ctrlPressed = KApplication::keyboardModifiers() & ControlMask;
#endif
//Get the closest item _before_ the cursor
const TQPoint p = contentsToViewport( e->pos() );
TQListViewItem *item = itemAt( p );
if( !item || ctrlPressed ) item = lastItem();
else if( p.y() - itemRect( item ).top() < (item->height()/2) ) item = item->itemAbove();
if( item != m_marker ) {
//NOTE this if block prevents flicker
slotEraseMarker();
m_marker = item; //NOTE this is the correct place to set m_marker
viewportPaintEvent( 0 );
}
}
void
Playlist::contentsDragLeaveEvent( TQDragLeaveEvent* )
{
slotEraseMarker();
}
void
Playlist::contentsDropEvent( TQDropEvent *e )
{
DEBUG_BLOCK
//NOTE parent is always 0 currently, but we support it in case we start using trees
TQListViewItem *parent = 0;
TQListViewItem *after = m_marker;
//make sure to disable only if in dynamic mode and you're inserting
//at the beginning or in the middle of the disabled tracks
//Also, that the dynamic playlist has any tracks (suggested may not)
if( dynamicMode() && Playlist::instance()->firstChild() &&
( !m_marker || !( static_cast<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 ) KListView::viewportPaintEvent( e ); //we call with 0 in contentsDropEvent()
if ( m_marker ) {
TQPainter p( viewport() );
p.fillRect(
drawDropVisualizer( 0, 0, m_marker ),
TQBrush( colorGroup().highlight().dark(), TQBrush::Dense4Pattern ) );
}
else if( m_showHelp && isEmpty() ) {
TQPainter p( viewport() );
TQString minimumText(i18n(
"<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() )</