File indexing completed on 2024-05-19 04:49:50
0001 /**************************************************************************************** 0002 * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org> * 0003 * Copyright (c) 2008 Soren Harward <stharward@gmail.com> * 0004 * Copyright (c) 2008 Nikolaj Hald Nielsen <nhn@kde.org> * 0005 * Copyright (c) 2009 Téo Mrnjavac <teo@kde.org> * 0006 * Copyright (c) 2010 Nanno Langstraat <langstr@gmail.com> * 0007 * * 0008 * This program is free software; you can redistribute it and/or modify it under * 0009 * the terms of the GNU General Public License as published by the Free Software * 0010 * Foundation; either version 2 of the License, or (at your option) version 3 or * 0011 * any later version accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * 0013 * version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0016 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0017 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0018 * * 0019 * You should have received a copy of the GNU General Public License along with * 0020 * this program. If not, see <http://www.gnu.org/licenses/>. * 0021 ****************************************************************************************/ 0022 0023 #define DEBUG_PREFIX "Playlist::NonlinearTrackNavigator" 0024 0025 #include "NonlinearTrackNavigator.h" 0026 0027 #include "core/meta/Meta.h" 0028 #include "core/support/Debug.h" 0029 #include "playlist/PlaylistModel.h" 0030 #include "playlist/PlaylistModelStack.h" 0031 0032 0033 Playlist::NonlinearTrackNavigator::NonlinearTrackNavigator() 0034 : m_currentItem( 0 ) 0035 { 0036 // Connect to the QAbstractItemModel signals of the source model. 0037 // Ignore SIGNAL dataChanged: changes in metadata etc. don't affect the random play order. 0038 // Ignore SIGNAL layoutChanged: rows moving around doesn't affect the random play order. 0039 connect( m_model->qaim(), &QAbstractItemModel::modelReset, this, &NonlinearTrackNavigator::slotModelReset ); 0040 connect( m_model->qaim(), &QAbstractItemModel::rowsInserted, this, &NonlinearTrackNavigator::slotRowsInserted ); 0041 connect( m_model->qaim(), &QAbstractItemModel::rowsAboutToBeRemoved, this, &NonlinearTrackNavigator::slotRowsAboutToBeRemoved ); 0042 0043 // Connect to the Playlist::AbstractModel signals of the source model. 0044 connect( Playlist::ModelStack::instance()->bottom(), &Playlist::Model::activeTrackChanged, 0045 this, &NonlinearTrackNavigator::slotActiveTrackChanged ); 0046 } 0047 0048 0049 //!***** Keeping in-sync with the source model 0050 0051 void 0052 Playlist::NonlinearTrackNavigator::loadFromSourceModel() 0053 { 0054 DEBUG_BLOCK 0055 slotModelReset(); 0056 } 0057 0058 void 0059 Playlist::NonlinearTrackNavigator::slotModelReset() 0060 { 0061 DEBUG_BLOCK 0062 0063 m_insertedItems.clear(); 0064 m_removedItems += allItemsSet(); 0065 0066 int lastRowInModel = m_model->qaim()->rowCount() - 1; 0067 if ( lastRowInModel >= 0 ) 0068 slotRowsInserted( QModelIndex(), 0, lastRowInModel ); 0069 0070 doItemListsMaintenance(); 0071 0072 if ( !currentItem() ) 0073 setCurrentItem( m_model->activeId() ); 0074 } 0075 0076 // This function can get called thousands of times during a single FilterProxy change. 0077 // Be very efficient here! (e.g. no DEBUG_BLOCK) 0078 void 0079 Playlist::NonlinearTrackNavigator::slotRowsInserted( const QModelIndex& parent, int startRow, int endRow ) 0080 { 0081 Q_UNUSED( parent ); 0082 0083 for ( int row = startRow; row <= endRow; row++ ) 0084 { 0085 quint64 itemId = m_model->idAt( row ); 0086 0087 m_insertedItems.insert( itemId ); 0088 m_removedItems.remove( itemId ); 0089 } 0090 } 0091 0092 // This function can get called thousands of times during a single FilterProxy change. 0093 // Be very efficient here! (e.g. no DEBUG_BLOCK) 0094 void 0095 Playlist::NonlinearTrackNavigator::slotRowsAboutToBeRemoved( const QModelIndex& parent, int startRow, int endRow ) 0096 { 0097 Q_UNUSED( parent ); 0098 0099 for ( int row = startRow; row <= endRow; row++ ) 0100 { 0101 quint64 itemId = m_model->idAt( row ); 0102 0103 m_insertedItems.remove( itemId ); 0104 m_removedItems.insert( itemId ); 0105 } 0106 } 0107 0108 // A general note on this function: thousands of rows can be inserted/removed by a single 0109 // FilterProxy change. However, this function gets to process them in a big batch. 0110 // 0111 // So: O(n * log n) performance is good enough, but O(n^2) is not. 0112 // (that's also why we need the 'listRemove()' helper function) 0113 void 0114 Playlist::NonlinearTrackNavigator::doItemListsMaintenance() 0115 { 0116 DEBUG_BLOCK 0117 0118 // Move batch instructions to local storage immediately, because we may get called recursively. 0119 QSet<quint64> tmpInsertedItems = m_insertedItems; 0120 m_insertedItems.clear(); 0121 0122 QSet<quint64> tmpRemovedItems = m_removedItems; 0123 m_removedItems.clear(); 0124 0125 // Handle the removed items 0126 if ( !tmpRemovedItems.isEmpty() ) 0127 { 0128 QSet<quint64> knownRemovedItems = tmpRemovedItems & allItemsSet(); // Filter out items inserted+removed between calls to us. 0129 0130 Item::listRemove( m_allItemsList, tmpRemovedItems ); 0131 Item::listRemove( m_historyItems, tmpRemovedItems ); 0132 Item::listRemove( m_replayedItems, tmpRemovedItems ); 0133 Item::listRemove( m_plannedItems, tmpRemovedItems ); 0134 0135 notifyItemsRemoved( knownRemovedItems ); 0136 0137 if ( tmpRemovedItems.contains( currentItem() ) ) // After 'notifyItemsRemoved()', so that they get a chance to choose a new one. 0138 setCurrentItem( 0 ); 0139 } 0140 0141 // Handle the newly inserted items 0142 if ( !tmpInsertedItems.isEmpty() ) 0143 { 0144 QSet<quint64> unknownInsertedItems = tmpInsertedItems - allItemsSet(); // Filter out items removed+reinserted between calls to us. 0145 0146 m_allItemsList.append( unknownInsertedItems.values() ); 0147 m_plannedItems.clear(); // Could do this more subtly in each child class, but this is good enough. 0148 0149 notifyItemsInserted( unknownInsertedItems ); 0150 } 0151 0152 // Prune history size 0153 while ( m_historyItems.size() > MAX_HISTORY_SIZE ) 0154 m_historyItems.removeFirst(); 0155 } 0156 0157 0158 //!***** Current playlist item 0159 0160 quint64 0161 Playlist::NonlinearTrackNavigator::currentItem() 0162 { 0163 doItemListsMaintenance(); 0164 return m_currentItem; 0165 } 0166 0167 void 0168 Playlist::NonlinearTrackNavigator::setCurrentItem( const quint64 newItem, bool goingBackward ) 0169 { 0170 DEBUG_BLOCK 0171 0172 doItemListsMaintenance(); 0173 0174 // Remember that we've played the old item. 0175 if ( m_currentItem ) 0176 { 0177 if ( goingBackward ) 0178 m_replayedItems.prepend( m_currentItem ); 0179 else 0180 m_historyItems.append( m_currentItem ); 0181 } 0182 0183 m_currentItem = newItem; 0184 0185 // If the new current item happens to also be the next planned item, consider that 0186 // plan "done". Can happen e.g. when the user manually plays our next planned item. 0187 if ( m_currentItem ) 0188 if ( !m_plannedItems.isEmpty() && m_plannedItems.first() == m_currentItem ) 0189 m_plannedItems.removeFirst(); 0190 } 0191 0192 // In the normal case this signal slot is redundant, because 'requestNext|LastTrack()' 0193 // already called 'setCurrentItem()' long before this function gets called. 0194 // 0195 // This signal slot takes care of some special cases, like the user clicking on 0196 // an arbitrary item in the playlist. 0197 void 0198 Playlist::NonlinearTrackNavigator::slotActiveTrackChanged( const quint64 id ) 0199 { 0200 DEBUG_BLOCK 0201 0202 doItemListsMaintenance(); 0203 0204 if ( currentItem() != id ) // If the new item is not what we expected: 0205 { 0206 // Heuristic: if this new "current item" does not look like we're going back/fwd in 0207 // history, then cancel "visit history" mode. 0208 // Not important, just a small nicety. It's what the user probably wants. 0209 if ( ( m_historyItems.isEmpty() || m_historyItems.last() != id ) && 0210 ( !m_replayedItems.contains( id ) ) ) 0211 { 0212 m_historyItems.append( m_replayedItems ); 0213 m_replayedItems.clear(); 0214 } 0215 0216 // Ditch the plan. The unexpected "current item" might change what we want to do next. 0217 m_plannedItems.clear(); 0218 0219 // The main thing we need to do. 0220 setCurrentItem( id ); 0221 } 0222 } 0223 0224 0225 //!***** Choosing next playlist item 0226 0227 Playlist::ItemList* 0228 Playlist::NonlinearTrackNavigator::nextItemChooseDonorList() 0229 { 0230 DEBUG_BLOCK 0231 0232 if ( !m_queue.isEmpty() ) // User-specified queue has highest priority. 0233 return &m_queue; 0234 else if ( !m_replayedItems.isEmpty() ) // If the user pressed "previous" once or more, first re-play those items again when we go forward again. 0235 return &m_replayedItems; 0236 else 0237 { 0238 if ( m_plannedItems.isEmpty() ) 0239 planOne(); 0240 if ( !m_plannedItems.isEmpty() ) // The normal case. 0241 return &m_plannedItems; 0242 else 0243 debug() << "planOne() didn't plan a next item."; 0244 } 0245 0246 return nullptr; 0247 } 0248 0249 quint64 0250 Playlist::NonlinearTrackNavigator::likelyNextTrack() 0251 { 0252 doItemListsMaintenance(); 0253 0254 ItemList *donor = nextItemChooseDonorList(); 0255 return donor ? donor->first() : 0; 0256 } 0257 0258 // We could just call 'likelyNextTrack()' and assume that we'll get a 'slotActiveTrackChanged' 0259 // callback later. But let's follow our API strictly: update the donor list immediately. 0260 quint64 0261 Playlist::NonlinearTrackNavigator::requestNextTrack() 0262 { 0263 doItemListsMaintenance(); 0264 0265 ItemList *donor = nextItemChooseDonorList(); 0266 quint64 nextItem = donor ? donor->takeFirst() : 0; 0267 0268 setCurrentItem( nextItem ); 0269 return m_currentItem; 0270 } 0271 0272 0273 //!***** Choosing previous playlist item 0274 0275 quint64 0276 Playlist::NonlinearTrackNavigator::likelyLastTrack() 0277 { 0278 doItemListsMaintenance(); 0279 0280 return m_historyItems.isEmpty() ? 0 : m_historyItems.last(); 0281 } 0282 0283 quint64 0284 Playlist::NonlinearTrackNavigator::requestLastTrack() 0285 { 0286 doItemListsMaintenance(); 0287 0288 quint64 lastItem = 0; 0289 while (!m_historyItems.isEmpty()) 0290 { 0291 quint64 possibleLastItem = m_historyItems.takeLast(); 0292 if (m_model->trackForId(possibleLastItem)->isPlayable()) { 0293 lastItem = possibleLastItem; 0294 break; 0295 } 0296 } 0297 0298 setCurrentItem( lastItem, true ); 0299 return m_currentItem; 0300 }