1 /****************************************************************************************
2  * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org>                                           *
3  * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
4  * Copyright (c) 2008 Nikolaj Hald Nielsen <nhn@kde.org>                                *
5  * Copyright (c) 2009 Téo Mrnjavac <teo@kde.org>                                        *
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::NonlinearTrackNavigator"
24 
25 #include "NonlinearTrackNavigator.h"
26 
27 #include "core/support/Debug.h"
28 #include "playlist/PlaylistModel.h"
29 #include "playlist/PlaylistModelStack.h"
30 
31 
NonlinearTrackNavigator()32 Playlist::NonlinearTrackNavigator::NonlinearTrackNavigator()
33     : m_currentItem( 0 )
34 {
35     // Connect to the QAbstractItemModel signals of the source model.
36     //   Ignore SIGNAL dataChanged: changes in metadata etc. don't affect the random play order.
37     //   Ignore SIGNAL layoutChanged: rows moving around doesn't affect the random play order.
38     connect( m_model->qaim(), &QAbstractItemModel::modelReset, this, &NonlinearTrackNavigator::slotModelReset );
39     connect( m_model->qaim(), &QAbstractItemModel::rowsInserted, this, &NonlinearTrackNavigator::slotRowsInserted );
40     connect( m_model->qaim(), &QAbstractItemModel::rowsAboutToBeRemoved, this, &NonlinearTrackNavigator::slotRowsAboutToBeRemoved );
41 
42     // Connect to the Playlist::AbstractModel signals of the source model.
43     connect( Playlist::ModelStack::instance()->bottom(), &Playlist::Model::activeTrackChanged,
44              this, &NonlinearTrackNavigator::slotActiveTrackChanged );
45 }
46 
47 
48 //!***** Keeping in-sync with the source model
49 
50 void
loadFromSourceModel()51 Playlist::NonlinearTrackNavigator::loadFromSourceModel()
52 {
53     DEBUG_BLOCK
54     slotModelReset();
55 }
56 
57 void
slotModelReset()58 Playlist::NonlinearTrackNavigator::slotModelReset()
59 {
60     DEBUG_BLOCK
61 
62     m_insertedItems.clear();
63     m_removedItems += allItemsSet();
64 
65     int lastRowInModel = m_model->qaim()->rowCount() - 1;
66     if ( lastRowInModel >= 0 )
67         slotRowsInserted( QModelIndex(), 0, lastRowInModel );
68 
69     doItemListsMaintenance();
70 
71     if ( !currentItem() )
72         setCurrentItem( m_model->activeId() );
73 }
74 
75 // This function can get called thousands of times during a single FilterProxy change.
76 // Be very efficient here! (e.g. no DEBUG_BLOCK)
77 void
slotRowsInserted(const QModelIndex & parent,int startRow,int endRow)78 Playlist::NonlinearTrackNavigator::slotRowsInserted( const QModelIndex& parent, int startRow, int endRow )
79 {
80     Q_UNUSED( parent );
81 
82     for ( int row = startRow; row <= endRow; row++ )
83     {
84         quint64 itemId = m_model->idAt( row );
85 
86         m_insertedItems.insert( itemId );
87         m_removedItems.remove( itemId );
88     }
89 }
90 
91 // This function can get called thousands of times during a single FilterProxy change.
92 // Be very efficient here! (e.g. no DEBUG_BLOCK)
93 void
slotRowsAboutToBeRemoved(const QModelIndex & parent,int startRow,int endRow)94 Playlist::NonlinearTrackNavigator::slotRowsAboutToBeRemoved( const QModelIndex& parent, int startRow, int endRow )
95 {
96     Q_UNUSED( parent );
97 
98     for ( int row = startRow; row <= endRow; row++ )
99     {
100         quint64 itemId = m_model->idAt( row );
101 
102         m_insertedItems.remove( itemId );
103         m_removedItems.insert( itemId );
104     }
105 }
106 
107 // A general note on this function: thousands of rows can be inserted/removed by a single
108 // FilterProxy change. However, this function gets to process them in a big batch.
109 //
110 // So: O(n * log n) performance is good enough, but O(n^2) is not.
111 // (that's also why we need the 'listRemove()' helper function)
112 void
doItemListsMaintenance()113 Playlist::NonlinearTrackNavigator::doItemListsMaintenance()
114 {
115     DEBUG_BLOCK
116 
117     // Move batch instructions to local storage immediately, because we may get called recursively.
118     QSet<quint64> tmpInsertedItems = m_insertedItems;
119     m_insertedItems.clear();
120 
121     QSet<quint64> tmpRemovedItems = m_removedItems;
122     m_removedItems.clear();
123 
124     // Handle the removed items
125     if ( !tmpRemovedItems.isEmpty() )
126     {
127         QSet<quint64> knownRemovedItems = tmpRemovedItems & allItemsSet();    // Filter out items inserted+removed between calls to us.
128 
129         Item::listRemove( m_allItemsList, tmpRemovedItems );
130         Item::listRemove( m_historyItems, tmpRemovedItems );
131         Item::listRemove( m_replayedItems, tmpRemovedItems );
132         Item::listRemove( m_plannedItems, tmpRemovedItems );
133 
134         notifyItemsRemoved( knownRemovedItems );
135 
136         if ( tmpRemovedItems.contains( currentItem() ) )    // After 'notifyItemsRemoved()', so that they get a chance to choose a new one.
137             setCurrentItem( 0 );
138     }
139 
140     // Handle the newly inserted items
141     if ( !tmpInsertedItems.isEmpty() )
142     {
143         QSet<quint64> unknownInsertedItems = tmpInsertedItems - allItemsSet();    // Filter out items removed+reinserted between calls to us.
144 
145         m_allItemsList.append( unknownInsertedItems.toList() );
146         m_plannedItems.clear();    // Could do this more subtly in each child class, but this is good enough.
147 
148         notifyItemsInserted( unknownInsertedItems );
149     }
150 
151     // Prune history size
152     while ( m_historyItems.size() > MAX_HISTORY_SIZE )
153         m_historyItems.removeFirst();
154 }
155 
156 
157 //!***** Current playlist item
158 
159 quint64
currentItem()160 Playlist::NonlinearTrackNavigator::currentItem()
161 {
162     doItemListsMaintenance();
163     return m_currentItem;
164 }
165 
166 void
setCurrentItem(const quint64 newItem,bool goingBackward)167 Playlist::NonlinearTrackNavigator::setCurrentItem( const quint64 newItem, bool goingBackward )
168 {
169     DEBUG_BLOCK
170 
171     doItemListsMaintenance();
172 
173     // Remember that we've played the old item.
174     if ( m_currentItem )
175     {
176         if ( goingBackward )
177             m_replayedItems.prepend( m_currentItem );
178         else
179             m_historyItems.append( m_currentItem );
180     }
181 
182     m_currentItem = newItem;
183 
184     // If the new current item happens to also be the next planned item, consider that
185     // plan "done". Can happen e.g. when the user manually plays our next planned item.
186     if ( m_currentItem )
187         if ( !m_plannedItems.isEmpty() && m_plannedItems.first() == m_currentItem )
188             m_plannedItems.removeFirst();
189 }
190 
191 // In the normal case this signal slot is redundant, because 'requestNext|LastTrack()'
192 // already called 'setCurrentItem()' long before this function gets called.
193 //
194 // This signal slot takes care of some special cases, like the user clicking on
195 // an arbitrary item in the playlist.
196 void
slotActiveTrackChanged(const quint64 id)197 Playlist::NonlinearTrackNavigator::slotActiveTrackChanged( const quint64 id )
198 {
199     DEBUG_BLOCK
200 
201     doItemListsMaintenance();
202 
203     if ( currentItem() != id )    // If the new item is not what we expected:
204     {
205         // Heuristic: if this new "current item" does not look like we're going back/fwd in
206         // history, then cancel "visit history" mode.
207         // Not important, just a small nicety. It's what the user probably wants.
208         if ( ( m_historyItems.isEmpty() || m_historyItems.last() != id ) &&
209              ( !m_replayedItems.contains( id ) ) )
210         {
211             m_historyItems.append( m_replayedItems );
212             m_replayedItems.clear();
213         }
214 
215         // Ditch the plan. The unexpected "current item" might change what we want to do next.
216         m_plannedItems.clear();
217 
218         // The main thing we need to do.
219         setCurrentItem( id );
220     }
221 }
222 
223 
224 //!***** Choosing next playlist item
225 
226 Playlist::ItemList*
nextItemChooseDonorList()227 Playlist::NonlinearTrackNavigator::nextItemChooseDonorList()
228 {
229     DEBUG_BLOCK
230 
231     if ( !m_queue.isEmpty() )    // User-specified queue has highest priority.
232         return &m_queue;
233     else if ( !m_replayedItems.isEmpty() )    // If the user pressed "previous" once or more, first re-play those items again when we go forward again.
234         return &m_replayedItems;
235     else
236     {
237         if ( m_plannedItems.isEmpty() )
238             planOne();
239         if ( !m_plannedItems.isEmpty() )    // The normal case.
240             return &m_plannedItems;
241         else
242             debug() << "planOne() didn't plan a next item.";
243     }
244 
245     return 0;
246 }
247 
248 quint64
likelyNextTrack()249 Playlist::NonlinearTrackNavigator::likelyNextTrack()
250 {
251     doItemListsMaintenance();
252 
253     ItemList *donor = nextItemChooseDonorList();
254     return donor ? donor->first() : 0;
255 }
256 
257 // We could just call 'likelyNextTrack()' and assume that we'll get a 'slotActiveTrackChanged'
258 // callback later. But let's follow our API strictly: update the donor list immediately.
259 quint64
requestNextTrack()260 Playlist::NonlinearTrackNavigator::requestNextTrack()
261 {
262     doItemListsMaintenance();
263 
264     ItemList *donor = nextItemChooseDonorList();
265     quint64 nextItem = donor ? donor->takeFirst() : 0;
266 
267     setCurrentItem( nextItem );
268     return m_currentItem;
269 }
270 
271 
272 //!***** Choosing previous playlist item
273 
274 quint64
likelyLastTrack()275 Playlist::NonlinearTrackNavigator::likelyLastTrack()
276 {
277     doItemListsMaintenance();
278 
279     return m_historyItems.isEmpty() ? 0 : m_historyItems.last();
280 }
281 
282 quint64
requestLastTrack()283 Playlist::NonlinearTrackNavigator::requestLastTrack()
284 {
285     doItemListsMaintenance();
286 
287     quint64 lastItem = m_historyItems.isEmpty() ? 0 : m_historyItems.takeLast();
288 
289     setCurrentItem( lastItem, true );
290     return m_currentItem;
291 }
292