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