Playlist: fix small breakage introduced by commit 1fafbc4bd3a27a40028f6a68614822d673a...
[amarok:amarok.git] / src / playlist / PlaylistModel.cpp
1 /****************************************************************************************
2  * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu>                                   *
3  * Copyright (c) 2007-2009 Nikolaj Hald Nielsen <nhn@kde.org>                           *
4  * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org>                                           *
5  * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
6  * Copyright (c) 2010 Nanno Langstraat <langstr@gmail.com>                              *
7  *                                                                                      *
8  * This program is free software; you can redistribute it and/or modify it under        *
9  * the terms of the GNU General Public License as published by the Free Software        *
10  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
11  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
12  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
13  * version 3 of the license.                                                            *
14  *                                                                                      *
15  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
16  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
17  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
18  *                                                                                      *
19  * You should have received a copy of the GNU General Public License along with         *
20  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
21  ****************************************************************************************/
22
23 #define DEBUG_PREFIX "Playlist::Model"
24
25 #include "PlaylistModel.h"
26
27 #include "core/support/Amarok.h"
28 #include "SvgHandler.h"
29 #include "amarokconfig.h"
30 #include "AmarokMimeData.h"
31 #include "core/capabilities/ReadLabelCapability.h"
32 #include "core/support/Debug.h"
33 #include "DirectoryLoader.h"
34 #include "EngineController.h"
35 #include "core/capabilities/SourceInfoCapability.h"
36 #include "core/collections/Collection.h"
37 #include "core/meta/support/MetaUtility.h"
38 #include "PlaylistActions.h"
39 #include "PlaylistModelStack.h"
40 #include "PlaylistItem.h"
41 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
42 #include "UndoCommands.h"
43
44 #include <KGlobal>
45 #include <KUrl>
46
47 #include <QAction>
48 #include <QDate>
49 #include <QStringList>
50 #include <QTextDocument>
51
52 #include <typeinfo>
53
54
55 Playlist::Model::Model( QObject *parent )
56         : QAbstractListModel( parent )
57         , m_activeRow( -1 )
58         , m_totalLength( 0 )
59         , m_totalSize( 0 )
60         , m_setStateOfItem_batchMinRow( -1 )
61 {
62     DEBUG_BLOCK
63 }
64
65 Playlist::Model::~Model()
66 {
67     DEBUG_BLOCK
68
69     // Save current playlist
70     exportPlaylist( defaultPlaylistPath() );
71 }
72
73 QVariant
74 Playlist::Model::headerData( int section, Qt::Orientation orientation, int role ) const
75 {
76     Q_UNUSED( orientation );
77
78     if ( role != Qt::DisplayRole )
79         return QVariant();
80
81     return columnNames[section];
82 }
83
84 QVariant
85 Playlist::Model::data( const QModelIndex& index, int role ) const
86 {
87     int row = index.row();
88
89     if ( !index.isValid() || !rowExists( row ) )
90         return QVariant();
91
92     if ( role == UniqueIdRole )
93         return QVariant( idAt( row ) );
94
95     else if ( role == ActiveTrackRole )
96         return ( row == m_activeRow );
97
98     else if ( role == TrackRole && m_items.at( row )->track() )
99         return QVariant::fromValue( m_items.at( row )->track() );
100
101     else if ( role == StateRole )
102         return m_items.at( row )->state();
103
104     else if ( role == QueuePositionRole )
105         return Actions::instance()->queuePosition( idAt( row ) ) + 1;
106
107     else if ( role == InCollectionRole )
108         return  m_items.at( row )->track()->inCollection();
109
110     else if ( role == MultiSourceRole )
111         return  m_items.at( row )->track()->hasCapabilityInterface( Capabilities::Capability::MultiSource );
112
113     else if ( role == StopAfterTrackRole )
114         return Actions::instance()->willStopAfterTrack( idAt( row ) );
115
116     else if ( role == Qt::DisplayRole || role == Qt::ToolTipRole )
117     {
118         switch ( index.column() )
119         {
120             case PlaceHolder:
121                 return QString();
122             case Album:
123             {
124                 if ( m_items.at( row )->track()->album() )
125                     return m_items.at( row )->track()->album()->name();
126                 return QString();
127             }
128             case AlbumArtist:
129             {
130                 if ( m_items.at( row )->track()->album() )
131                     if (  m_items.at( row )->track()->album()->albumArtist() )
132                         return m_items.at( row )->track()->album()->albumArtist()->name();
133                 return QString();
134             }
135             case Artist:
136             {
137                 if ( m_items.at( row )->track()->artist() )
138                     return m_items.at( row )->track()->artist()->name();
139                 return QString();
140             }
141             case Bitrate:
142             {
143                 return Meta::prettyBitrate( m_items.at( row )->track()->bitrate() );
144             }
145             case Bpm:
146             {
147                 if ( m_items.at( row )->track()->bpm() > 0.0 )
148                     return QString::number( m_items.at( row )->track()->bpm() );
149                 return QString();
150             }
151             case Comment:
152             {
153                 return m_items.at( row )->track()->comment();
154             }
155             case Composer:
156             {
157                 if ( m_items.at( row )->track()->composer() )
158                     return m_items.at( row )->track()->composer()->name();
159                 return QString();
160             }
161             case CoverImage:
162             {
163                 if ( m_items.at( row )->track()->album() )
164                     return The::svgHandler()->imageWithBorder( m_items.at( row )->track()->album(), 100 ); //FIXME:size?
165                 return QImage();
166             }
167             case Directory:
168             {
169                 if ( m_items.at( row )->track()->playableUrl().isLocalFile() )
170                     return m_items.at( row )->track()->playableUrl().directory();
171                 else
172                     return QString();
173             }
174             case DiscNumber:
175             {
176                 return m_items.at( row )->track()->discNumber();
177             }
178             case Filename:
179             {
180
181                 if ( m_items.at( row )->track()->playableUrl().isLocalFile() )
182                     return m_items.at( row )->track()->playableUrl().fileName();
183                 else
184                     return QString();
185             }
186             case Filesize:
187             {
188                 return Meta::prettyFilesize( m_items.at( row )->track()->filesize() );
189             }
190             case Genre:
191             {
192                 if ( m_items.at( row )->track()->genre() )
193                     return m_items.at( row )->track()->genre()->name();
194                 return QString();
195             }
196             case GroupLength:
197             {
198                 return Meta::secToPrettyTime( 0 );
199             }
200             case GroupTracks:
201             {
202                 return QString();
203             }
204             case Labels:
205             {
206                 Meta::TrackPtr track = m_items.at( row )->track();
207                 if( track )
208                 {
209                     QStringList labelNames;
210                     foreach( const Meta::LabelPtr &label, track->labels() )
211                     {
212                         labelNames << label->prettyName();
213                     }
214                     return labelNames.join( ", " );
215                 }
216                 return QString();
217             }
218             case LastPlayed:
219             {
220                 if ( m_items.at( row )->track()->lastPlayed() == 0 )
221                     return i18n( "Never" );
222                 else {
223                     QDateTime date = QDateTime::fromTime_t( m_items.at( row )->track()->lastPlayed() );
224                     return Amarok::verboseTimeSince( date );
225                 }
226             }
227             case Length:
228             {
229                 return Meta::msToPrettyTime( m_items.at( row )->track()->length() );
230             }
231             case LengthInSeconds:
232             {
233                 return m_items.at( row )->track()->length() / 1000;
234             }
235             case Mood:
236             {
237                 return QString(); //FIXME
238             }
239             case PlayCount:
240             {
241                 return m_items.at( row )->track()->playCount();
242             }
243             case Rating:
244             {
245                 return m_items.at( row )->track()->rating();
246             }
247             case SampleRate:
248             {
249                 return m_items.at( row )->track()->sampleRate();
250             }
251             case Score:
252             {
253                 return (int)m_items.at( row )->track()->score(); // Cast to int, as we don't need to show the decimals in the view..
254             }
255             case Source:
256             {
257                 QString sourceName;
258                 Capabilities::SourceInfoCapability *sic = m_items.at( row )->track()->create<Capabilities::SourceInfoCapability>();
259                 if ( sic )
260                 {
261                     sourceName = sic->sourceName();
262                     delete sic;
263                 }
264                 else
265                 {
266                     sourceName = m_items.at( row )->track()->collection() ? m_items.at( row )->track()->collection()->prettyName() : QString();
267                 }
268                 return sourceName;
269             }
270             case SourceEmblem:
271             {
272                 QPixmap emblem;
273                 Capabilities::SourceInfoCapability *sic = m_items.at( row )->track()->create<Capabilities::SourceInfoCapability>();
274                 if ( sic )
275                 {
276                     QString source = sic->sourceName();
277                     if ( !source.isEmpty() )
278                         emblem = sic->emblem();
279                     delete sic;
280                 }
281                 return emblem;
282             }
283             case Title:
284             {
285                 return m_items.at( row )->track()->prettyName();
286             }
287             case TitleWithTrackNum:
288             {
289                 QString trackString;
290                 QString trackName = m_items.at( row )->track()->prettyName();
291                 if ( m_items.at( row )->track()->trackNumber() > 0 )
292                 {
293                     QString trackNumber = QString::number( m_items.at( row )->track()->trackNumber() );
294                     trackString =  QString( trackNumber + " - " + trackName );
295                 } else
296                     trackString = trackName;
297
298                 return trackString;
299             }
300             case TrackNumber:
301             {
302                 return m_items.at( row )->track()->trackNumber();
303             }
304             case Type:
305             {
306                 return m_items.at( row )->track()->type();
307             }
308             case Year:
309             {
310                 if ( m_items.at( row )->track()->year() )
311                     return m_items.at( row )->track()->year()->name();
312                 return QString();
313             }
314             default:
315                 return QString();
316
317         }
318     }
319     // else
320     return QVariant();
321 }
322
323 Qt::DropActions
324 Playlist::Model::supportedDropActions() const
325 {
326     return Qt::MoveAction | QAbstractListModel::supportedDropActions();
327 }
328
329 Qt::ItemFlags
330 Playlist::Model::flags( const QModelIndex &index ) const
331 {
332     if ( index.isValid() )
333         return ( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsEditable );
334     return Qt::ItemIsDropEnabled;
335 }
336
337 QStringList
338 Playlist::Model::mimeTypes() const
339 {
340     QStringList ret = QAbstractListModel::mimeTypes();
341     ret << AmarokMimeData::TRACK_MIME;
342     ret << "text/uri-list"; //we do accept urls
343     return ret;
344 }
345
346 QMimeData*
347 Playlist::Model::mimeData( const QModelIndexList &indexes ) const
348 {
349     AmarokMimeData* mime = new AmarokMimeData();
350     Meta::TrackList selectedTracks;
351
352     foreach( const QModelIndex &it, indexes )
353     selectedTracks << m_items.at( it.row() )->track();
354
355     mime->setTracks( selectedTracks );
356     return mime;
357 }
358
359 bool
360 Playlist::Model::dropMimeData( const QMimeData* data, Qt::DropAction action, int row, int, const QModelIndex& parent )
361 {
362     DEBUG_BLOCK
363     if ( action == Qt::IgnoreAction )
364         return true;
365
366     int beginRow;
367     if ( row != -1 )
368         beginRow = row;
369     else if ( parent.isValid() )
370         beginRow = parent.row();
371     else
372         beginRow = m_items.size();
373
374     if( data->hasFormat( AmarokMimeData::TRACK_MIME ) )
375     {
376         debug() << "this is a track";
377         const AmarokMimeData* trackListDrag = dynamic_cast<const AmarokMimeData*>( data );
378         if( trackListDrag )
379         {
380
381             Meta::TrackList tracks = trackListDrag->tracks();
382             qStableSort( tracks.begin(), tracks.end(), Meta::Track::lessThan );
383
384             The::playlistController()->insertTracks( beginRow, tracks );
385         }
386         return true;
387     }
388     else if( data->hasFormat( AmarokMimeData::PLAYLIST_MIME ) )
389     {
390         debug() << "this is a playlist";
391         const AmarokMimeData* dragList = dynamic_cast<const AmarokMimeData*>( data );
392         if( dragList )
393             The::playlistController()->insertPlaylists( beginRow, dragList->playlists() );
394         return true;
395     }
396     else if( data->hasFormat( AmarokMimeData::PODCASTEPISODE_MIME ) )
397     {
398         debug() << "this is a podcast episode";
399         const AmarokMimeData* dragList = dynamic_cast<const AmarokMimeData*>( data );
400         if( dragList )
401         {
402             Meta::TrackList tracks;
403             foreach( Podcasts::PodcastEpisodePtr episode, dragList->podcastEpisodes() )
404                 tracks << Meta::TrackPtr::staticCast( episode );
405             The::playlistController()->insertTracks( beginRow, tracks );
406         }
407         return true;
408     }
409     else if( data->hasFormat( AmarokMimeData::PODCASTCHANNEL_MIME ) )
410     {
411         debug() << "this is a podcast channel";
412         const AmarokMimeData* dragList = dynamic_cast<const AmarokMimeData*>( data );
413         if( dragList )
414         {
415             Meta::TrackList tracks;
416             foreach( Podcasts::PodcastChannelPtr channel, dragList->podcastChannels() )
417                 foreach( Podcasts::PodcastEpisodePtr episode, channel->episodes() )
418                     tracks << Meta::TrackPtr::staticCast( episode );
419             The::playlistController()->insertTracks( beginRow, tracks );
420         }
421         return true;
422     }
423     else if( data->hasUrls() )
424     {
425         debug() << "this is _something_ with a url....";
426         DirectoryLoader* dl = new DirectoryLoader(); //this deletes itself
427         dl->insertAtRow( beginRow );
428         dl->init( data->urls() );
429         return true;
430     }
431
432     debug() << "I have no idea what the hell this is...";
433     return false;
434 }
435
436 void
437 Playlist::Model::setActiveRow( int row )
438 {
439     if ( rowExists( row ) )
440     {
441         setStateOfRow( row, Item::Played );
442         m_activeRow = row;
443         emit activeTrackChanged( m_items.at( row )->id() );
444     }
445     else
446     {
447         m_activeRow = -1;
448         emit activeTrackChanged( 0 );
449     }
450 }
451
452 void
453 Playlist::Model::setRowQueued( int row )
454 {
455     if( rowExists( row ) )
456     {
457         Item::State state = stateOfRow(row);
458         if( state == Item::Invalid )
459             state = Item::Queued;
460         else
461             state = (Item::State) ( state | Item::Queued );
462         setStateOfRow( row, state );
463         emit queueChanged();
464     }
465 }
466
467 void
468 Playlist::Model::setRowDequeued( int row )
469 {
470     if( rowExists( row ) )
471     {
472         Item::State state = stateOfRow(row);
473         if( state == Item::Queued )
474             state = Item::Invalid;
475         else
476             state = (Item::State) ( stateOfRow(row) & ~Item::Queued );
477         setStateOfRow( row, state );
478         emit queueChanged();
479     }
480 }
481
482 Playlist::Item::State
483 Playlist::Model::stateOfRow( int row ) const
484 {
485     if ( rowExists( row ) )
486         return m_items.at( row )->state();
487     else
488         return Item::Invalid;
489 }
490
491 bool
492 Playlist::Model::containsTrack( const Meta::TrackPtr track ) const
493 {
494     foreach( Item* i, m_items )
495     {
496         if ( i->track() == track )
497             return true;
498     }
499     return false;
500 }
501
502 int
503 Playlist::Model::firstRowForTrack( const Meta::TrackPtr track ) const
504 {
505     int row = 0;
506     foreach( Item* i, m_items )
507     {
508         if ( i->track() == track )
509             return row;
510         row++;
511     }
512     return -1;
513 }
514
515 QSet<int>
516 Playlist::Model::allRowsForTrack( const Meta::TrackPtr track ) const
517 {
518     QSet<int> trackRows;
519
520     int row = 0;
521     foreach( Item* i, m_items )
522     {
523         if ( i->track() == track )
524             trackRows.insert( row );
525         row++;
526     }
527     return trackRows;
528 }
529
530 Meta::TrackPtr
531 Playlist::Model::trackAt( int row ) const
532 {
533     if ( rowExists( row ) )
534         return m_items.at( row )->track();
535     else
536         return Meta::TrackPtr();
537 }
538
539 Meta::TrackPtr
540 Playlist::Model::activeTrack() const
541 {
542     if ( rowExists( m_activeRow ) )
543         return m_items.at( m_activeRow )->track();
544     else
545         return Meta::TrackPtr();
546 }
547
548 int
549 Playlist::Model::rowForId( const quint64 id ) const
550 {
551     return m_items.indexOf( m_itemIds.value( id ) );    // Returns -1 on miss, same as our API.
552 }
553
554 Meta::TrackPtr
555 Playlist::Model::trackForId( const quint64 id ) const
556 {
557     Item* item = m_itemIds.value( id, 0 );
558     if ( item )
559         return item->track();
560     else
561         return Meta::TrackPtr();
562 }
563
564 quint64
565 Playlist::Model::idAt( const int row ) const
566 {
567     if ( rowExists( row ) )
568         return m_items.at( row )->id();
569     else
570         return 0;
571 }
572
573 quint64
574 Playlist::Model::activeId() const
575 {
576     if ( rowExists( m_activeRow ) )
577         return m_items.at( m_activeRow )->id();
578     else
579         return 0;
580 }
581
582 Playlist::Item::State
583 Playlist::Model::stateOfId( quint64 id ) const
584 {
585     Item* item = m_itemIds.value( id, 0 );
586     if ( item )
587         return item->state();
588     else
589         return Item::Invalid;
590 }
591
592 void
593 Playlist::Model::metadataChanged( Meta::TrackPtr track )
594 {
595     DEBUG_BLOCK
596
597     const int size = m_items.size();
598     for ( int i = 0; i < size; i++ )
599     {
600         if ( m_items.at( i )->track() == track )
601         {
602             emit dataChanged( index( i, 0 ), index( i, columnCount() - 1 ) );
603             debug()<<"Metadata updated for track"<<track->prettyName();
604             break;
605         }
606     }
607 }
608
609 void
610 Playlist::Model::metadataChanged( Meta::AlbumPtr album )
611 {
612     DEBUG_BLOCK
613
614     Meta::TrackList tracks = album->tracks();
615     foreach( Meta::TrackPtr track, tracks )
616         metadataChanged( track );
617     debug()<<"Album metadata changed";
618 }
619
620 bool
621 Playlist::Model::exportPlaylist( const QString &path ) const
622 {
623     // check queue state
624     QQueue<quint64> queueIds = The::playlistActions()->queue();
625     QList<int> queued;
626     foreach( quint64 id, queueIds ) {
627       queued << rowForId( id );
628     }
629     return Playlists::exportPlaylistFile( tracks(), path, queued );
630 }
631
632 Meta::TrackList
633 Playlist::Model::tracks() const
634 {
635     Meta::TrackList tl;
636     foreach( Item* item, m_items )
637         tl << item->track();
638     return tl;
639 }
640
641 QString
642 Playlist::Model::prettyColumnName( Column index ) //static
643 {
644     switch ( index )
645     {
646     case Filename:   return i18nc( "The name of the file this track is stored in", "Filename" );
647     case Title:      return i18n( "Title" );
648     case Artist:     return i18n( "Artist" );
649     case AlbumArtist: return i18n( "Album Artist" );
650     case Composer:   return i18n( "Composer" );
651     case Year:       return i18n( "Year" );
652     case Album:      return i18n( "Album" );
653     case DiscNumber: return i18n( "Disc Number" );
654     case TrackNumber: return i18nc( "The Track number for this item", "Track" );
655     case Bpm:        return i18n( "BPM" );
656     case Genre:      return i18n( "Genre" );
657     case Comment:    return i18n( "Comment" );
658     case Directory:  return i18nc( "The location on disc of this track", "Directory" );
659     case Type:       return i18n( "Type" );
660     case Length:     return i18n( "Length" );
661     case Bitrate:    return i18n( "Bitrate" );
662     case SampleRate: return i18n( "Sample Rate" );
663     case Score:      return i18n( "Score" );
664     case Rating:     return i18n( "Rating" );
665     case PlayCount:  return i18n( "Play Count" );
666     case LastPlayed: return i18nc( "Column name", "Last Played" );
667     case Mood:       return i18n( "Mood" );
668     case Filesize:   return i18n( "File Size" );
669     default:         return "This is a bug.";
670     }
671
672 }
673
674
675 ////////////
676 //Private Methods
677 ////////////
678
679 void
680 Playlist::Model::insertTracksCommand( const InsertCmdList& cmds )
681 {
682     if ( cmds.size() < 1 )
683         return;
684
685     setAllNewlyAddedToUnplayed();
686
687     int activeShift = 0;
688     int min = m_items.size() + cmds.size();
689     int max = 0;
690     int begin = cmds.at( 0 ).second;
691     foreach( const InsertCmd &ic, cmds )
692     {
693         min = qMin( min, ic.second );
694         max = qMax( max, ic.second );
695         activeShift += ( begin <= m_activeRow ) ? 1 : 0;
696     }
697
698     // actually do the insertion
699     beginInsertRows( QModelIndex(), min, max );
700     foreach( const InsertCmd &ic, cmds )
701     {
702         Meta::TrackPtr track = ic.first;
703         m_totalLength += track->length();
704         m_totalSize += track->filesize();
705         subscribeTo( track );
706
707         if ( track->album() )
708             subscribeTo( track->album() );
709
710         Item* newitem = new Item( track );
711         m_items.insert( ic.second, newitem );
712         m_itemIds.insert( newitem->id(), newitem );
713     }
714     endInsertRows();
715
716     if( m_activeRow >= 0 )
717         m_activeRow += activeShift;
718     else
719     {
720          // If one of the inserted tracks is currently playing, choose it as the active track.
721         const Meta::TrackPtr engineTrack = The::engineController()->currentTrack();
722         if( engineTrack )
723         {
724             int engineRow = firstRowForTrack( engineTrack );
725             if( engineRow > -1 )
726                 setActiveRow( engineRow );
727         }
728     }
729 }
730
731
732 void
733 Playlist::Model::removeTracksCommand( const RemoveCmdList& cmds )
734 {
735     DEBUG_BLOCK
736     if ( cmds.size() < 1 )
737         return;
738
739     if ( cmds.size() == m_items.size() )
740     {
741         clearCommand();
742         return;
743     }
744
745     int activeShift = 0;
746     bool activeDeleted = false;
747     foreach( const RemoveCmd &rc, cmds )
748     {
749         activeShift += ( rc.second < m_activeRow ) ? 1 : 0;
750         if ( rc.second == m_activeRow )
751             activeDeleted = true;
752     }
753
754     /* This next bit is probably more complicated that you expected it to be.
755      * The reason for the complexity comes from the following:
756      *
757      * 1. Qt's Model/View architecture can handle removal of only consecutive rows
758      * 2. The "remove rows" command from the Controller must handle
759      *    non-consecutive rows, and the removal command probably isn't sorted
760      *
761      * So each item has to be removed individually, and you can't just iterate
762      * over the commands, calling "m_items.removeAt(index)" as you go, because
763      * the indices of m_items will change with every removeAt().  Thus the
764      * following strategy of copying m_item, looking up the index in the copy and
765      * removing the item from the original list. (The strategy was changed from
766      * only replacing m_items at the end because that meant lying to the view -- Max)
767      *
768      * As a safety measure, the items themselves are not deleted until after m_items
769      * has been replaced.  If you delete as you go, then m_items will be holding
770      * dangling pointers, and the program will probably crash if the model is
771      * accessed in this state.   -- stharward */
772
773     QList<Item*> originalList(m_items); // copy the current item list
774     QList<Item*> delitems;
775     foreach( const RemoveCmd &rc, cmds )
776     {
777         Meta::TrackPtr track = rc.first;
778         m_totalLength -= track->length();
779         m_totalSize -= track->filesize();
780         unsubscribeFrom( track );
781         if ( track->album() )
782             unsubscribeFrom( track->album() );
783
784         Item* item = originalList.at(rc.second);
785         int idx = rowForItem( item );
786         if (idx != -1) {
787             beginRemoveRows(QModelIndex(), idx, idx);
788             delitems.append(item);
789             m_items.removeAll( item );
790             m_itemIds.remove( item->id() );
791             endRemoveRows();
792         } else {
793             error() << "tried to delete a non-existent item:" << rc.first->prettyName() << rc.second;
794         }
795     }
796
797     qDeleteAll(delitems);
798     delitems.clear();
799
800     //update the active row
801     if ( !activeDeleted && ( m_activeRow >= 0 ) )
802     {
803         m_activeRow = ( m_activeRow > 0 ) ? m_activeRow - activeShift : 0;
804     }
805     else
806     {
807         m_activeRow = -1;
808     }
809
810     //make sure that there are enough tracks if we just removed from a dynamic playlist.
811     Playlist::Actions::instance()->normalizeDynamicPlaylist();
812 }
813
814
815 void Playlist::Model::clearCommand()
816 {
817     setActiveRow( -1 );
818
819     beginRemoveRows( QModelIndex(), 0, rowCount() - 1 );
820
821     m_totalLength = 0;
822     m_totalSize = 0;
823
824     qDeleteAll( m_items );
825     m_items.clear();
826     m_itemIds.clear();
827
828     endRemoveRows();
829 }
830
831
832 // Note: this function depends on 'MoveCmdList' to be a complete "cycle", in the sense
833 // that if row A is moved to row B, another row MUST be moved to row A.
834 // Very strange API design IMHO, because it forces our caller to e.g. move ALL ROWS in
835 // the playlist to move row 0 to the last row. This function should just have been
836 // equivalent to a 'removeTracks()' followed by an 'insertTracks()' IMHO.  --Nanno
837
838 void
839 Playlist::Model::moveTracksCommand( const MoveCmdList& cmds, bool reverse )
840 {
841     DEBUG_BLOCK
842     debug()<<"moveTracksCommand:"<<cmds.size()<<reverse;
843
844     if ( cmds.size() < 1 )
845         return;
846
847     int min = m_items.size() + cmds.size();
848     int max = 0;
849     foreach( const MoveCmd &rc, cmds )
850     {
851         min = qMin( min, rc.first );
852         min = qMin( min, rc.second );
853         max = qMax( max, rc.first );
854         max = qMax( max, rc.second );
855     }
856
857     int newActiveRow = m_activeRow;
858     QList<Item*> oldItems( m_items );
859     if ( reverse )
860     {
861         foreach( const MoveCmd &mc, cmds )
862         {
863             m_items[mc.first] = oldItems.at( mc.second );
864             if ( m_activeRow == mc.second )
865                 newActiveRow = mc.first;
866         }
867     }
868     else
869     {
870         foreach( const MoveCmd &mc, cmds )
871         {
872             m_items[mc.second] = oldItems.at( mc.first );
873             if ( m_activeRow == mc.first )
874                 newActiveRow = mc.second;
875         }
876     }
877
878     // We have 3 choices:
879     //   - Qt 4.6 'beginMoveRows()' / 'endMoveRows()'. Drawback: we'd need to do N of them, all causing resorts etc.
880     //   - Emit 'layoutAboutToChange' / 'layoutChanged'. Drawback: unspecific, 'changePersistentIndex()' complications.
881     //   - Emit 'dataChanged'. Drawback: a bit inappropriate. But not wrong.
882     emit dataChanged( index( min, 0 ), index( max, columnCount() - 1 ) );
883
884     //update the active row
885     m_activeRow = newActiveRow;
886 }
887
888
889 // When doing a 'setStateOfItem_batch', we emit 1 crude 'dataChanged' signal. If we're
890 // unlucky, that signal may span many innocent rows that haven't changed at all.
891 // Although that "worst case" will cause unnecessary work in our listeners "upstream", it
892 // still has much better performance than the worst case of emitting very many tiny
893 // 'dataChanged' signals.
894 //
895 // Being more clever (coalesce multiple contiguous ranges, etc.) is not worth the effort.
896 void
897 Playlist::Model::setStateOfItem_batchStart()
898 {
899     m_setStateOfItem_batchMinRow = rowCount() + 1;
900     m_setStateOfItem_batchMaxRow = 0;
901 }
902
903 void
904 Playlist::Model::setStateOfItem_batchEnd()
905 {
906     if ( ( m_setStateOfItem_batchMaxRow - m_setStateOfItem_batchMinRow ) >= 0 )
907         emit dataChanged( index( m_setStateOfItem_batchMinRow, 0 ), index( m_setStateOfItem_batchMaxRow, columnCount() - 1 ) );
908
909     m_setStateOfItem_batchMinRow = -1;
910 }
911
912 void
913 Playlist::Model::setStateOfItem( Item *item, int row, Item::State state )
914 {
915     item->setState( state );
916
917     if ( m_setStateOfItem_batchMinRow == -1 )    // If not in batch mode
918         emit dataChanged( index( row, 0 ), index( row, columnCount() - 1 ) );
919     else
920     {
921         m_setStateOfItem_batchMinRow = qMin( m_setStateOfItem_batchMinRow, row );
922         m_setStateOfItem_batchMaxRow = qMax( m_setStateOfItem_batchMaxRow, row );
923     }
924 }
925
926
927 // Unimportant TODO: the performance of this function is O(n) in playlist size.
928 // Not a big problem, because it's called infrequently.
929 // Can be fixed by maintaining a new member variable 'QMultiHash<Item::State, Item*>'.
930 void
931 Playlist::Model::setAllNewlyAddedToUnplayed()
932 {
933     DEBUG_BLOCK
934
935     setStateOfItem_batchStart();
936
937     for ( int row = 0; row < rowCount(); row++ )
938     {
939         Item* item = m_items.at( row );
940         if ( item->state() == Item::NewlyAdded )
941             setStateOfItem( item, row, Item::Unplayed );
942     }
943
944     setStateOfItem_batchEnd();
945 }
946
947 void Playlist::Model::setAllUnplayed()
948 {
949     DEBUG_BLOCK
950
951     setStateOfItem_batchStart();
952
953     for ( int row = 0; row < rowCount(); row++ )
954     {
955         Item* item = m_items.at( row );
956         setStateOfItem( item, row, Item::Unplayed );
957     }
958
959     setStateOfItem_batchEnd();
960 }