1 /****************************************************************************************
2  * Copyright (c) 2008 Nikolaj Hald Nielsen <nhn@kde.org>                                *
3  * Copyright (c) 2010 Bart Cerneels <bart.cerneels@kde.org>                             *
4  *                                                                                      *
5  * This program is free software; you can redistribute it and/or modify it under        *
6  * the terms of the GNU General Public License as published by the Free Software        *
7  * Foundation; either version 2 of the License, or (at your option) any later           *
8  * version.                                                                             *
9  *                                                                                      *
10  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
11  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
12  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
13  *                                                                                      *
14  * You should have received a copy of the GNU General Public License along with         *
15  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
16  ****************************************************************************************/
17 
18 #define DEBUG_PREFIX "PlaylistBrowserView"
19 
20 #include "PlaylistBrowserView.h"
21 
22 #include "MainWindow.h"
23 #include "PaletteHandler.h"
24 #include "PopupDropperFactory.h"
25 #include "SvgHandler.h"
26 #include "amarokconfig.h"
27 #include "browsers/playlistbrowser/PlaylistBrowserModel.h"
28 #include "browsers/playlistbrowser/PlaylistsByProviderProxy.h"
29 #include "browsers/playlistbrowser/PlaylistsInFoldersProxy.h"
30 #include "context/ContextView.h"
31 #include "core/support/Debug.h"
32 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
33 #include "playlist/PlaylistModel.h"
34 #include "playlistmanager/PlaylistManager.h"
35 #include "widgets/PrettyTreeRoles.h"
36 
37 #include <QCheckBox>
38 #include <QFileDialog>
39 #include <QKeyEvent>
40 #include <QLabel>
41 #include <QMenu>
42 #include <QMessageBox>
43 #include <QMouseEvent>
44 
45 #include <KConfigGroup>
46 
47 using namespace PlaylistBrowserNS;
48 
PlaylistBrowserView(QAbstractItemModel * model,QWidget * parent)49 PlaylistBrowserNS::PlaylistBrowserView::PlaylistBrowserView( QAbstractItemModel *model,
50                                                              QWidget *parent )
51     : Amarok::PrettyTreeView( parent )
52     , m_pd( 0 )
53     , m_ongoingDrag( false )
54 {
55     DEBUG_BLOCK
56     setModel( model );
57     setSelectionMode( QAbstractItemView::ExtendedSelection );
58     setSelectionBehavior( QAbstractItemView::SelectItems );
59     setDragDropMode( QAbstractItemView::DragDrop );
60     setAcceptDrops( true );
61     setEditTriggers( QAbstractItemView::EditKeyPressed );
62     setMouseTracking( true ); // needed for highlighting provider action icons
63 
64     m_createEmptyPlaylistAction = new QAction( QIcon::fromTheme( QStringLiteral("media-track-add-amarok") ),
65                                                i18n( "Create an Empty Playlist" ), this );
66     connect( m_createEmptyPlaylistAction, &QAction::triggered, this, &PlaylistBrowserView::slotCreateEmptyPlaylist );
67 
68     m_appendAction = new QAction( QIcon::fromTheme( QStringLiteral("media-track-add-amarok") ),
69             i18n( "&Add to Playlist" ), this );
70     m_appendAction->setProperty( "popupdropper_svg_id", "append" );
71     connect( m_appendAction, &QAction::triggered, this, &PlaylistBrowserView::slotAppend );
72 
73     m_loadAction = new QAction( QIcon::fromTheme( QStringLiteral("folder-open") ), i18nc( "Replace the currently "
74             "loaded tracks with these", "&Replace Playlist" ), this );
75     m_loadAction->setProperty( "popupdropper_svg_id", "load" );
76     connect( m_loadAction, &QAction::triggered, this, &PlaylistBrowserView::slotLoad );
77 
78     m_setNewAction = new QAction( QIcon::fromTheme( QStringLiteral("rating") ), i18nc( "toggle the \"new\" status "
79             " of this podcast episode", "&New" ), this );
80     m_setNewAction->setProperty( "popupdropper_svg_id", "new" );
81     m_setNewAction->setCheckable( true );
82     connect( m_setNewAction, &QAction::triggered, this, &PlaylistBrowserView::slotSetNew );
83 
84     m_renamePlaylistAction = new QAction( QIcon::fromTheme( QStringLiteral("media-track-edit-amarok") ),
85             i18n( "&Rename..." ), this );
86     m_renamePlaylistAction->setProperty( "popupdropper_svg_id", "edit" );
87     // key shortcut is only for display purposes here, actual one is determined by View in Model/View classes
88     m_renamePlaylistAction->setShortcut( Qt::Key_F2 );
89     connect( m_renamePlaylistAction, &QAction::triggered, this, &PlaylistBrowserView::slotRename );
90 
91     m_deletePlaylistAction = new QAction( QIcon::fromTheme( QStringLiteral("media-track-remove-amarok") ),
92             i18n( "&Delete..." ), this );
93     m_deletePlaylistAction->setProperty( "popupdropper_svg_id", "delete" );
94     // key shortcut is only for display purposes here, actual one is determined by View in Model/View classes
95     m_deletePlaylistAction->setShortcut( Qt::Key_Delete );
96     connect( m_deletePlaylistAction, &QAction::triggered, this, &PlaylistBrowserView::slotDelete );
97 
98     m_removeTracksAction = new QAction( QIcon::fromTheme( QStringLiteral("media-track-remove-amarok") ),
99             QStringLiteral( "<placeholder>" ), this );
100     m_removeTracksAction->setProperty( "popupdropper_svg_id", "delete" );
101     // key shortcut is only for display purposes here, actual one is determined by View in Model/View classes
102     m_removeTracksAction->setShortcut( Qt::Key_Delete );
103     connect( m_removeTracksAction, &QAction::triggered, this, &PlaylistBrowserView::slotRemoveTracks );
104 
105     m_exportAction = new QAction( QIcon::fromTheme( QStringLiteral("document-export-amarok") ),
106             i18n( "&Export As..." ), this );
107     connect( m_exportAction, &QAction::triggered, this, &PlaylistBrowserView::slotExport );
108 
109     m_separatorAction = new QAction( this );
110     m_separatorAction->setSeparator( true );
111 }
112 
113 void
setModel(QAbstractItemModel * model)114 PlaylistBrowserNS::PlaylistBrowserView::setModel( QAbstractItemModel *model )
115 {
116     if( this->model() )
117         disconnect( this->model(), 0, this, 0 );
118     Amarok::PrettyTreeView::setModel( model );
119 
120     connect( this->model(), SIGNAL(renameIndex(QModelIndex)), SLOT(edit(QModelIndex)) );
121 }
122 
123 void
mouseReleaseEvent(QMouseEvent * event)124 PlaylistBrowserNS::PlaylistBrowserView::mouseReleaseEvent( QMouseEvent *event )
125 {
126     if( m_pd )
127     {
128         connect( m_pd, &PopupDropper::fadeHideFinished, m_pd, &QObject::deleteLater );
129         m_pd->hide();
130         m_pd = 0;
131     }
132 
133     QModelIndex index = indexAt( event->pos() );
134     if( !index.isValid() )
135     {
136         PrettyTreeView::mouseReleaseEvent( event );
137         return;
138     }
139 
140     if( event->button() == Qt::MidButton )
141     {
142         insertIntoPlaylist( index, Playlist::OnMiddleClickOnSelectedItems );
143         event->accept();
144         return;
145     }
146 
147     PrettyTreeView::mouseReleaseEvent( event );
148 }
149 
startDrag(Qt::DropActions supportedActions)150 void PlaylistBrowserNS::PlaylistBrowserView::startDrag( Qt::DropActions supportedActions )
151 {
152     // Waah? when a parent item is dragged, startDrag is called a bunch of times
153     if( m_ongoingDrag )
154         return;
155     m_ongoingDrag = true;
156 
157     if( !m_pd )
158         m_pd = The::popupDropperFactory()->createPopupDropper( Context::ContextView::self() );
159 
160     if( m_pd && m_pd->isHidden() )
161     {
162         QActionList actions = actionsFor( selectedIndexes() );
163         foreach( QAction *action, actions )
164             m_pd->addItem( The::popupDropperFactory()->createItem( action ) );
165 
166         m_pd->show();
167     }
168 
169     QTreeView::startDrag( supportedActions );
170 
171     // We keep the items that the actions need to be applied to.
172     // Clear the data from all actions now that the PUD has executed.
173     resetActionTargets();
174 
175     if( m_pd )
176     {
177         connect( m_pd, &PopupDropper::fadeHideFinished, m_pd, &PopupDropper::clear );
178         m_pd->hide();
179     }
180     m_ongoingDrag = false;
181 }
182 
183 void
keyPressEvent(QKeyEvent * event)184 PlaylistBrowserNS::PlaylistBrowserView::keyPressEvent( QKeyEvent *event )
185 {
186     QModelIndexList indices = selectedIndexes();
187     // mind bug 305203
188     if( indices.isEmpty() || state() != QAbstractItemView::NoState )
189     {
190         Amarok::PrettyTreeView::keyPressEvent( event );
191         return;
192     }
193 
194     switch( event->key() )
195     {
196         //activated() only works for current index, not all selected
197         case Qt::Key_Enter:
198         case Qt::Key_Return:
199             insertIntoPlaylist( indices, Playlist::OnReturnPressedOnSelectedItems );
200             return;
201         case Qt::Key_Delete:
202         {
203             QActionList actions = actionsFor( indices ); // sets action targets
204             if( actions.contains( m_removeTracksAction ) )
205                 m_removeTracksAction->trigger();
206             else if( actions.contains( m_deletePlaylistAction ) )
207                 m_deletePlaylistAction->trigger();
208             resetActionTargets();
209             return;
210         }
211         default:
212             break;
213     }
214     Amarok::PrettyTreeView::keyPressEvent( event );
215 }
216 
217 void
mouseDoubleClickEvent(QMouseEvent * event)218 PlaylistBrowserNS::PlaylistBrowserView::mouseDoubleClickEvent( QMouseEvent *event )
219 {
220     if( event->button() == Qt::MidButton )
221     {
222         event->accept();
223         return;
224     }
225 
226     QModelIndex index = indexAt( event->pos() );
227     if( !index.isValid() )
228     {
229         event->accept();
230         return;
231     }
232 
233     // code copied in CollectionTreeView::mouseDoubleClickEvent(), keep in sync
234     // mind bug 279513
235     bool isExpandable = model()->hasChildren( index );
236     bool wouldExpand = !visualRect( index ).contains( event->pos() ) || // clicked outside item, perhaps on expander icon
237                        ( isExpandable && !style()->styleHint( QStyle::SH_ItemView_ActivateItemOnSingleClick, 0, this ) ); // we're in doubleClick
238     if( event->button() == Qt::LeftButton &&
239         event->modifiers() == Qt::NoModifier &&
240         !wouldExpand )
241     {
242         insertIntoPlaylist( index, Playlist::OnDoubleClickOnSelectedItems );
243         event->accept();
244         return;
245     }
246 
247     PrettyTreeView::mouseDoubleClickEvent( event );
248 }
249 
contextMenuEvent(QContextMenuEvent * event)250 void PlaylistBrowserNS::PlaylistBrowserView::contextMenuEvent( QContextMenuEvent *event )
251 {
252     QModelIndex clickedIdx = indexAt( event->pos() );
253 
254     QModelIndexList indices;
255     if( clickedIdx.isValid() && selectedIndexes().contains( clickedIdx ) )
256         indices << selectedIndexes();
257     else if( clickedIdx.isValid() )
258         indices << clickedIdx;
259 
260     QActionList actions = actionsFor( indices );
261     if( actions.isEmpty() )
262     {
263         resetActionTargets();
264         return;
265     }
266 
267     QMenu menu;
268     foreach( QAction *action, actions )
269         menu.addAction( action );
270     menu.exec( mapToGlobal( event->pos() ) );
271 
272     // We keep the items that the action need to be applied to.
273     // Clear the data from all actions now that the context menu has executed.
274     resetActionTargets();
275 }
276 
277 QList<QAction *>
actionsFor(const QModelIndexList & indexes)278 PlaylistBrowserNS::PlaylistBrowserView::actionsFor( const QModelIndexList &indexes )
279 {
280     resetActionTargets();
281     if( indexes.isEmpty() )
282         return QActionList();
283 
284     using namespace Playlists;
285     QSet<PlaylistProvider *> providers, writableProviders;
286     QActionList actions;
287     QModelIndexList newPodcastEpisodes, oldPodcastEpisodes;
288     foreach( const QModelIndex &idx, indexes )
289     {
290         // direct provider actions:
291         actions << idx.data( PrettyTreeRoles::DecoratorRole ).value<QActionList>();
292 
293         PlaylistProvider *provider = idx.data( PlaylistBrowserModel::ProviderRole ).value<PlaylistProvider *>();
294         if( provider )
295             providers << provider;
296         bool isWritable =  provider ? provider->isWritable() : false;
297         if( isWritable )
298             writableProviders |= provider;
299         Meta::TrackPtr track = idx.data( PlaylistBrowserModel::TrackRole ).value<Meta::TrackPtr>();
300         PlaylistPtr playlist = idx.data( PlaylistBrowserModel::PlaylistRole ).value<PlaylistPtr>();
301         if( !track && playlist ) // a playlist (must check it is not a track)
302         {
303             m_actionPlaylists << playlist;
304             if( isWritable )
305                 m_writableActionPlaylists << playlist;
306         }
307         if( track )
308         {
309             m_actionTracks.insert( playlist, idx.row() );
310             if( isWritable )
311                 m_writableActionTracks.insert( playlist, idx.row() );
312         }
313 
314         QVariant episodeIsNew = idx.data( PlaylistBrowserModel::EpisodeIsNewRole );
315         if( episodeIsNew.type() == QVariant::Bool )
316         {
317             if( episodeIsNew.toBool() )
318                 newPodcastEpisodes << idx;
319             else
320                 oldPodcastEpisodes << idx;
321         }
322     }
323     // all actions taking provider have only sense with one provider
324     if( writableProviders.count() == 1 )
325         m_writableActionProvider = writableProviders.toList().first();
326 
327     // process per-provider actions
328     foreach( PlaylistProvider *provider, providers )
329     {
330         // prepare arguments and get relevant actions
331         PlaylistList providerPlaylists;
332         foreach( const PlaylistPtr &playlist, m_actionPlaylists )
333         {
334             if( playlist->provider() == provider )
335                 providerPlaylists << playlist;
336         }
337         actions << provider->playlistActions( providerPlaylists );
338 
339         QMultiHash<PlaylistPtr, int> playlistTracks;
340         QHashIterator<PlaylistPtr, int> it( m_actionTracks );
341         while( it.hasNext() )
342         {
343             it.next();
344             if( it.key()->provider() == provider )
345                 playlistTracks.insert( it.key(), it.value() );
346         }
347         actions << provider->trackActions( playlistTracks );
348     }
349 
350     // separate model actions from standard actions we provide (at the top)
351     QActionList standardActions;
352     if( m_actionPlaylists.isEmpty() && m_actionTracks.isEmpty() && m_writableActionProvider )
353         standardActions << m_createEmptyPlaylistAction;
354     if( !m_actionPlaylists.isEmpty() || !m_actionTracks.isEmpty() )
355         standardActions << m_appendAction << m_loadAction;
356     if( !newPodcastEpisodes.isEmpty() || !oldPodcastEpisodes.isEmpty() )
357     {
358         m_setNewAction->setChecked( oldPodcastEpisodes.isEmpty() );
359         m_setNewAction->setData( QVariant::fromValue( newPodcastEpisodes + oldPodcastEpisodes ) );
360         standardActions << m_setNewAction;
361     }
362     if( m_writableActionPlaylists.count() == 1 && m_actionTracks.isEmpty() )
363         standardActions << m_renamePlaylistAction;
364     if( !m_writableActionPlaylists.isEmpty() && m_actionTracks.isEmpty() )
365         standardActions << m_deletePlaylistAction;
366     if( m_actionPlaylists.isEmpty() && !m_writableActionTracks.isEmpty() )
367     {
368         const int actionTrackCount = m_writableActionTracks.count();
369         const int playlistCount = m_writableActionTracks.uniqueKeys().count();
370         if( playlistCount > 1 )
371             m_removeTracksAction->setText( i18nc( "%1: number of tracks. %2: number of playlists",
372                 "Remove %1 From %2", i18ncp ("First part of 'Remove %1 From %2'", "a Track",
373                 "%1 Tracks", actionTrackCount), i18ncp ("Second part of 'Remove %1 From %2'", "1 Playlist",
374                 "%1 Playlists", playlistCount ) ) );
375         else
376             m_removeTracksAction->setText( i18ncp( "%2 is saved playlist name",
377                 "Remove a Track From %2", "Remove %1 Tracks From %2", actionTrackCount,
378                 m_writableActionTracks.uniqueKeys().first()->prettyName() ) );
379         standardActions << m_removeTracksAction;
380     }
381     if( m_actionPlaylists.count() == 1 && m_actionTracks.isEmpty() )
382         standardActions << m_exportAction;
383     standardActions << m_separatorAction;
384 
385     return standardActions + actions;
386 }
387 
388 void
resetActionTargets()389 PlaylistBrowserView::resetActionTargets()
390 {
391     m_writableActionProvider = 0;
392     m_actionPlaylists.clear();
393     m_writableActionPlaylists.clear();
394     m_actionTracks.clear();
395     m_writableActionTracks.clear();
396 }
397 
398 void
currentChanged(const QModelIndex & current,const QModelIndex & previous)399 PlaylistBrowserNS::PlaylistBrowserView::currentChanged( const QModelIndex &current,
400                                                         const QModelIndex &previous )
401 {
402     Q_UNUSED( previous )
403     Q_EMIT currentItemChanged( current );
404     Amarok::PrettyTreeView::currentChanged( current, previous );
405 }
406 
407 void
slotCreateEmptyPlaylist()408 PlaylistBrowserView::slotCreateEmptyPlaylist()
409 {
410     // m_actionProvider may be null, which is fine
411     The::playlistManager()->save( Meta::TrackList(), Amarok::generatePlaylistName(
412             Meta::TrackList() ), m_writableActionProvider );
413 }
414 
415 void
slotAppend()416 PlaylistBrowserView::slotAppend()
417 {
418     insertIntoPlaylist( Playlist::OnAppendToPlaylistAction );
419 }
420 
421 void
slotLoad()422 PlaylistBrowserView::slotLoad()
423 {
424     insertIntoPlaylist( Playlist::OnReplacePlaylistAction );
425 }
426 
427 void
slotSetNew(bool newState)428 PlaylistBrowserView::slotSetNew( bool newState )
429 {
430     QModelIndexList indices = m_setNewAction->data().value<QModelIndexList>();
431     foreach( const QModelIndex &idx, indices )
432         model()->setData( idx, newState, PlaylistBrowserModel::EpisodeIsNewRole );
433 }
434 
435 void
slotRename()436 PlaylistBrowserView::slotRename()
437 {
438     if( m_writableActionPlaylists.count() != 1 )
439     {
440         warning() << __PRETTY_FUNCTION__ << "m_writableActionPlaylists.count() is not 1";
441         return;
442     }
443     Playlists::PlaylistPtr playlist = m_writableActionPlaylists.at( 0 );
444 
445     // TODO: this makes a rather complicated round-trip and ends up in edit(QModelIndex)
446     // here -- simplify that
447     The::playlistManager()->rename( playlist );
448 }
449 
450 void
slotDelete()451 PlaylistBrowserView::slotDelete()
452 {
453     if( m_writableActionPlaylists.isEmpty() )
454         return;
455 
456     using namespace Playlists;
457     QHash<PlaylistProvider *, PlaylistList> providerPlaylists;
458     foreach( const PlaylistPtr &playlist, m_writableActionPlaylists )
459     {
460         if( playlist->provider() )
461             providerPlaylists[ playlist->provider() ] << playlist;
462     }
463     QStringList providerNames;
464     foreach( const PlaylistProvider *provider, providerPlaylists.keys() )
465         providerNames << provider->prettyName();
466 
467     auto button = QMessageBox::question( The::mainWindow(),
468                                          i18n( "Confirm Playlist Deletion" ),
469                                          i18nc( "%1 is playlist provider pretty name",
470                                                 "Delete playlist from %1.", providerNames.join( QStringLiteral(", ") ) ),
471                                          QMessageBox::Yes | QMessageBox::No,
472                                          QMessageBox::Yes );
473 
474     if( button == QMessageBox::Yes )
475     {
476         foreach( PlaylistProvider *provider, providerPlaylists.keys() )
477             provider->deletePlaylists( providerPlaylists.value( provider ) );
478     }
479 }
480 
481 void
slotRemoveTracks()482 PlaylistBrowserView::slotRemoveTracks()
483 {
484     foreach( Playlists::PlaylistPtr playlist, m_writableActionTracks.uniqueKeys() )
485     {
486         QList<int> trackIndices = m_writableActionTracks.values( playlist );
487         qSort( trackIndices );
488         int removed = 0;
489         foreach( int trackIndex, trackIndices )
490         {
491             playlist->removeTrack( trackIndex - removed /* account for already removed */ );
492             removed++;
493         }
494     }
495 }
496 
497 void
slotExport()498 PlaylistBrowserView::slotExport()
499 {
500     if( m_actionPlaylists.count() != 1 )
501     {
502         warning() << __PRETTY_FUNCTION__ << "m_actionPlaylists.count() is not 1";
503         return;
504     }
505     Playlists::PlaylistPtr playlist = m_actionPlaylists.at( 0 );
506 
507     // --- display save location dialog
508     // compare with MainWindow::exportPlaylist
509     // TODO: have this code only once
510     QFileDialog fileDialog;
511     fileDialog.restoreState( Amarok::config( QStringLiteral("playlist-export-dialog") ).readEntry( "state", QByteArray() ) );
512 
513     // FIXME: Make checkbox visible in dialog
514     QCheckBox *saveRelativeCheck = new QCheckBox( i18n("Use relative path for &saving"), &fileDialog );
515     saveRelativeCheck->setChecked( AmarokConfig::relativePlaylist() );
516 
517     QStringList supportedMimeTypes;
518 
519     supportedMimeTypes << QStringLiteral("video/x-ms-asf"); //ASX
520     supportedMimeTypes << QStringLiteral("audio/x-mpegurl"); //M3U
521     supportedMimeTypes << QStringLiteral("audio/x-scpls"); //PLS
522     supportedMimeTypes << QStringLiteral("application/xspf+xml"); //XSPF
523 
524     fileDialog.setMimeTypeFilters( supportedMimeTypes );
525     fileDialog.setAcceptMode( QFileDialog::AcceptSave );
526     fileDialog.setFileMode( QFileDialog::AnyFile );
527     fileDialog.setWindowTitle( i18n("Save As") );
528     fileDialog.setObjectName( QStringLiteral("PlaylistExport") );
529 
530     int result = fileDialog.exec();
531     QString playlistPath = fileDialog.selectedFiles().value( 0 );
532     if( result == QDialog::Accepted && !playlistPath.isEmpty() )
533         Playlists::exportPlaylistFile( playlist->tracks(), QUrl::fromLocalFile( playlistPath ) );
534 
535     Amarok::config( QStringLiteral("playlist-export-dialog") ).writeEntry( "state", fileDialog.saveState() );
536 }
537 
538 void
insertIntoPlaylist(const QModelIndex & index,Playlist::AddOptions options)539 PlaylistBrowserView::insertIntoPlaylist( const QModelIndex &index, Playlist::AddOptions options )
540 {
541     insertIntoPlaylist( QModelIndexList() << index, options );
542 }
543 
544 void
insertIntoPlaylist(const QModelIndexList & list,Playlist::AddOptions options)545 PlaylistBrowserView::insertIntoPlaylist( const QModelIndexList &list, Playlist::AddOptions options )
546 {
547     actionsFor( list ); // sets action targets
548     insertIntoPlaylist( options );
549     resetActionTargets();
550 }
551 
552 void
insertIntoPlaylist(Playlist::AddOptions options)553 PlaylistBrowserView::insertIntoPlaylist( Playlist::AddOptions options )
554 {
555     Meta::TrackList tracks;
556 
557     // add tracks for fully-selected playlists:
558     foreach( Playlists::PlaylistPtr playlist, m_actionPlaylists )
559     {
560         tracks << playlist->tracks();
561     }
562 
563     // filter-out tracks from playlists that are selected, add lone tracks:
564     foreach( Playlists::PlaylistPtr playlist, m_actionTracks.uniqueKeys() )
565     {
566         if( m_actionPlaylists.contains( playlist ) )
567             continue;
568 
569         Meta::TrackList playlistTracks = playlist->tracks();
570         QList<int> positions = m_actionTracks.values( playlist );
571         qSort( positions );
572         foreach( int position, positions )
573         {
574             if( position >= 0 && position < playlistTracks.count() )
575                 tracks << playlistTracks.at( position );
576         }
577     }
578 
579     if( !tracks.isEmpty() )
580         The::playlistController()->insertOptioned( tracks, options );
581 }
582