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 }