File indexing completed on 2024-05-19 04:49:51
0001 /**************************************************************************************** 0002 * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu> * 0003 * Copyright (c) 2007 Nikolaj Hald Nielsen <nhn@kde.org> * 0004 * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org> * 0005 * Copyright (c) 2008 Soren Harward <stharward@gmail.com> * 0006 * Copyright (c) 2009 Téo Mrnjavac <teo@kde.org> * 0007 * Copyright (c) 2010 Nanno Langstraat <langstr@gmail.com> * 0008 * * 0009 * This program is free software; you can redistribute it and/or modify it under * 0010 * the terms of the GNU General Public License as published by the Free Software * 0011 * Foundation; either version 2 of the License, or (at your option) version 3 or * 0012 * any later version accepted by the membership of KDE e.V. (or its successor approved * 0013 * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * 0014 * version 3 of the license. * 0015 * * 0016 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0017 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0018 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License along with * 0021 * this program. If not, see <http://www.gnu.org/licenses/>. * 0022 ****************************************************************************************/ 0023 0024 #define DEBUG_PREFIX "Playlist::GroupingProxy" 0025 0026 #include "GroupingProxy.h" 0027 0028 #include "core/collections/Collection.h" 0029 #include "core/meta/Meta.h" 0030 #include "core/meta/Statistics.h" 0031 #include "core/meta/support/MetaUtility.h" 0032 #include "core/capabilities/SourceInfoCapability.h" 0033 #include "core/support/Debug.h" 0034 #include "playlist/PlaylistDefines.h" 0035 0036 #include <QVariant> 0037 #include <QFileInfo> 0038 0039 Playlist::GroupingProxy::GroupingProxy( Playlist::AbstractModel *belowModel, QObject *parent ) 0040 : ProxyBase( belowModel, parent ) 0041 { 0042 setGroupingCategory( QStringLiteral( "Album" ) ); 0043 0044 0045 // Adjust our internal state based on changes in the source model. 0046 // We connect to our own QAbstractItemModel signals, which are emitted by our 0047 // 'QSortFilterProxyModel' parent class. 0048 // 0049 // Connect to 'this' instead of 'sourceModel()' for 2 reasons: 0050 // - We happen to be a 1:1 passthrough proxy, but if we filtered/sorted rows, 0051 // we'd want to maintain state for the rows exported by the proxy. The rows 0052 // exported by the source model are of no direct interest to us. 0053 // 0054 // - Qt guarantees that our signal handlers on 'this' will be called earlier than 0055 // any other, because we're the first to call 'connect( this )' (hey, we're the 0056 // constructor!). So, we're guaranteed to be able to update our internal state 0057 // before we get any 'data()' calls from "upstream" signal handlers. 0058 // 0059 // If we connected to 'sourceModel()', there would be no such guarantee: it 0060 // would be highly likely that an "upstream" signal handler (connected to the 0061 // 'this' QSFPM signal) would get called earlier, would call our 'data()' 0062 // function, and we would return wrong answers from our stale internal state. 0063 // 0064 connect( this, &GroupingProxy::dataChanged, this, &GroupingProxy::proxyDataChanged ); 0065 connect( this, &GroupingProxy::layoutChanged, this, &GroupingProxy::proxyLayoutChanged ); 0066 connect( this, &GroupingProxy::modelReset, this, &GroupingProxy::proxyModelReset ); 0067 connect( this, &GroupingProxy::rowsInserted, this, &GroupingProxy::proxyRowsInserted ); 0068 connect( this, &GroupingProxy::rowsRemoved, this, &GroupingProxy::proxyRowsRemoved ); 0069 0070 0071 // No need to scan the pre-existing entries in sourceModel(), because we build our 0072 // internal state on-the-fly. 0073 0074 setObjectName( QStringLiteral("GroupingProxy") ); 0075 } 0076 0077 Playlist::GroupingProxy::~GroupingProxy() 0078 { 0079 } 0080 0081 0082 QString 0083 Playlist::GroupingProxy::groupingCategory() const 0084 { 0085 return m_groupingCategory; 0086 } 0087 0088 void 0089 Playlist::GroupingProxy::setGroupingCategory( const QString &groupingCategory ) 0090 { 0091 m_groupingCategory = groupingCategory; 0092 m_groupingCategoryIndex = groupableCategories().indexOf( columnForName( m_groupingCategory ) ); // May be -1 0093 0094 invalidateGrouping(); 0095 0096 // Notify our client(s) that we may now give different answers to 'data()' calls. 0097 // - Not 'layoutChanged': that is for when rows have been moved around, which they haven't. 0098 // - Not 'modelReset': that is too heavy. E.g. it also invalidates QListView item selections, etc. 0099 if ( rowCount() > 0 ) 0100 Q_EMIT dataChanged( index( 0, 0 ), index( rowCount() - 1, columnCount() - 1 ) ); 0101 } 0102 0103 0104 bool 0105 Playlist::GroupingProxy::isFirstInGroup( const QModelIndex & index ) 0106 { 0107 Grouping::GroupMode mode = groupModeForIndex( index ); 0108 return ( (mode == Grouping::Head) || (mode == Grouping::None) ); 0109 } 0110 0111 bool 0112 Playlist::GroupingProxy::isLastInGroup( const QModelIndex & index ) 0113 { 0114 Grouping::GroupMode mode = groupModeForIndex( index ); 0115 return ( (mode == Grouping::Tail) || (mode == Grouping::None) ); 0116 } 0117 0118 QModelIndex 0119 Playlist::GroupingProxy::firstIndexInSameGroup( const QModelIndex & index ) 0120 { 0121 QModelIndex currIndex = index; 0122 while ( ! isFirstInGroup( currIndex ) ) 0123 currIndex = currIndex.sibling( currIndex.row() - 1, currIndex.column() ); 0124 return currIndex; 0125 } 0126 0127 QModelIndex 0128 Playlist::GroupingProxy::lastIndexInSameGroup( const QModelIndex & index ) 0129 { 0130 QModelIndex currIndex = index; 0131 while ( ! isLastInGroup( currIndex ) ) 0132 currIndex = currIndex.sibling( currIndex.row() + 1, currIndex.column() ); 0133 return currIndex; 0134 } 0135 0136 int 0137 Playlist::GroupingProxy::groupRowCount( const QModelIndex & index ) 0138 { 0139 return ( lastIndexInSameGroup( index ).row() - firstIndexInSameGroup( index ).row() ) + 1; 0140 } 0141 0142 int 0143 Playlist::GroupingProxy::groupPlayLength( const QModelIndex & index ) 0144 { 0145 int totalLength = 0; 0146 0147 QModelIndex currIndex = firstIndexInSameGroup( index ); 0148 forever { 0149 Meta::TrackPtr track = currIndex.data( TrackRole ).value<Meta::TrackPtr>(); 0150 if ( track ) 0151 totalLength += track->length(); 0152 else 0153 warning() << "Playlist::GroupingProxy::groupPlayLength(): TrackPtr is 0! row =" << currIndex.row() << ", rowCount =" << rowCount(); 0154 0155 if ( isLastInGroup( currIndex ) ) 0156 break; 0157 currIndex = currIndex.sibling( currIndex.row() + 1, currIndex.column() ); 0158 } 0159 0160 return totalLength; 0161 } 0162 0163 0164 QVariant 0165 Playlist::GroupingProxy::data( const QModelIndex& index, int role ) const 0166 { 0167 if( !index.isValid() ) 0168 return QVariant(); 0169 0170 // Qt forces 'const' in our signature, but'groupModeForRow()' wants to do caching. 0171 GroupingProxy* nonconst_this = const_cast<GroupingProxy*>( this ); 0172 0173 switch ( role ) 0174 { 0175 case Playlist::GroupRole: 0176 return nonconst_this->groupModeForIndex( index ); 0177 0178 case Playlist::GroupedTracksRole: 0179 return nonconst_this->groupRowCount( index ); 0180 0181 case Qt::DisplayRole: 0182 case Qt::ToolTipRole: 0183 switch( index.column() ) 0184 { 0185 case GroupLength: 0186 return Meta::msToPrettyTime( nonconst_this->groupPlayLength( index ) ); 0187 case GroupTracks: 0188 return i18np ( "1 track", "%1 tracks", nonconst_this->groupRowCount( index ) ); 0189 } 0190 0191 Q_FALLTHROUGH(); 0192 0193 default: 0194 // Nothing to do with us: let our QSortFilterProxyModel parent class handle it. 0195 // (which will proxy the data() from the underlying model) 0196 return QSortFilterProxyModel::data( index, role ); 0197 } 0198 } 0199 0200 0201 // Note: being clever in this function is sometimes wasted effort, because 'dataChanged' 0202 // can cause SortProxy to nuke us with a 'layoutChanged' signal very soon anyway. 0203 void 0204 Playlist::GroupingProxy::proxyDataChanged( const QModelIndex& proxyTopLeft, const QModelIndex& proxyBottomRight ) 0205 { 0206 // The preceding and succeeding rows may get a different GroupMode too, when our 0207 // GroupMode changes. 0208 int invalidateFirstRow = proxyTopLeft.row() - 1; // May be an invalid row number 0209 int invalidateLastRow = proxyBottomRight.row() + 1; // May be an invalid row number 0210 0211 for (int row = invalidateFirstRow; row <= invalidateLastRow; row++) 0212 m_cachedGroupModeForRow.remove( row ); // Won't choke on non-existent rows. 0213 } 0214 0215 void 0216 Playlist::GroupingProxy::proxyLayoutChanged() 0217 { 0218 invalidateGrouping(); // Crude but sufficient. 0219 } 0220 0221 void 0222 Playlist::GroupingProxy::proxyModelReset() 0223 { 0224 invalidateGrouping(); // Crude but sufficient. 0225 } 0226 0227 void 0228 Playlist::GroupingProxy::proxyRowsInserted( const QModelIndex& parent, int proxyStart, int proxyEnd ) 0229 { 0230 Q_UNUSED( parent ); 0231 Q_UNUSED( proxyStart ); 0232 Q_UNUSED( proxyEnd ); 0233 0234 invalidateGrouping(); // Crude but sufficient. 0235 } 0236 0237 void 0238 Playlist::GroupingProxy::proxyRowsRemoved( const QModelIndex& parent, int proxyStart, int proxyEnd ) 0239 { 0240 Q_UNUSED( parent ); 0241 Q_UNUSED( proxyStart ); 0242 Q_UNUSED( proxyEnd ); 0243 0244 invalidateGrouping(); // Crude but sufficient. 0245 } 0246 0247 0248 Playlist::Grouping::GroupMode 0249 Playlist::GroupingProxy::groupModeForIndex( const QModelIndex & thisIndex ) 0250 { 0251 Grouping::GroupMode groupMode; 0252 0253 groupMode = m_cachedGroupModeForRow.value( thisIndex.row(), Grouping::Invalid ); // Try to get from cache 0254 0255 if ( groupMode == Grouping::Invalid ) 0256 { // Not in our cache 0257 QModelIndex prevIndex = thisIndex.sibling( thisIndex.row() - 1, thisIndex.column() ); // May be invalid, if 'thisIndex' is the first playlist item. 0258 QModelIndex nextIndex = thisIndex.sibling( thisIndex.row() + 1, thisIndex.column() ); // May be invalid, if 'thisIndex' is the last playlist item. 0259 0260 Meta::TrackPtr prevTrack = prevIndex.data( TrackRole ).value<Meta::TrackPtr>(); // Invalid index is OK: 0261 Meta::TrackPtr thisTrack = thisIndex.data( TrackRole ).value<Meta::TrackPtr>(); // will just give an 0262 Meta::TrackPtr nextTrack = nextIndex.data( TrackRole ).value<Meta::TrackPtr>(); // invalid TrackPtr. 0263 0264 bool matchBefore = shouldBeGrouped( prevTrack, thisTrack ); // Accepts invalid TrackPtrs. 0265 bool matchAfter = shouldBeGrouped( thisTrack, nextTrack ); // 0266 0267 if ( !matchBefore && matchAfter ) 0268 groupMode = Grouping::Head; 0269 else if ( matchBefore && matchAfter ) 0270 groupMode = Grouping::Body; 0271 else if ( matchBefore && !matchAfter ) 0272 groupMode = Grouping::Tail; 0273 else 0274 groupMode = Grouping::None; 0275 0276 m_cachedGroupModeForRow.insert( thisIndex.row(), groupMode ); // Cache our decision 0277 } 0278 0279 return groupMode; 0280 } 0281 0282 /** 0283 * The current implementation is a bit of a hack, but is what gives the best 0284 * user experience. 0285 * If a track has no data in the grouping category, it generally causes a non-match. 0286 */ 0287 bool 0288 Playlist::GroupingProxy::shouldBeGrouped( Meta::TrackPtr track1, Meta::TrackPtr track2 ) 0289 { 0290 // If the grouping category is empty or invalid, 'm_groupingCategoryIndex' will be -1. 0291 // That will cause us to choose "no grouping". 0292 0293 if( !track1 || !track2 ) 0294 return false; 0295 0296 // DEBUG_BLOCK 0297 // debug() << m_groupingCategoryIndex; 0298 0299 switch( m_groupingCategoryIndex ) 0300 { 0301 0302 case 0: //Album 0303 if( track1->album() && track2->album() ) 0304 { 0305 // don't group albums without name 0306 if( track1->album()->prettyName().isEmpty() || track2->album()->prettyName().isEmpty() ) 0307 return false; 0308 else 0309 return ( *track1->album().data() ) == ( *track2->album().data() ) && ( track1->discNumber() == track2->discNumber() ); 0310 } 0311 return false; 0312 case 1: //Artist 0313 if( track1->artist() && track2->artist() ) 0314 return ( *track1->artist().data() ) == ( *track2->artist().data() ); 0315 return false; 0316 case 2: //Composer 0317 if( track1->composer() && track2->composer() ) 0318 return ( *track1->composer().data() ) == ( *track2->composer().data() ); 0319 return false; 0320 case 3: //Directory 0321 return ( QFileInfo( track1->playableUrl().path() ).path() ) == 0322 ( QFileInfo( track2->playableUrl().path() ).path() ); 0323 case 4: //Genre 0324 if( track1->genre() && track2->genre() ) 0325 { 0326 debug() << "grouping by genre. Comparing " << track1->genre()->prettyName() << " with " << track2->genre()->prettyName(); 0327 debug() << track1->genre().data() << " == " << track2->genre().data() << " : " << ( *track1->genre().data() == *track2->genre().data()); 0328 return ( *track1->genre().data() ) == ( *track2->genre().data() ); 0329 } 0330 return false; 0331 case 5: //Rating 0332 if( track1->statistics()->rating() && track2->statistics()->rating() ) 0333 return ( track1->statistics()->rating() ) == ( track2->statistics()->rating() ); 0334 return false; 0335 case 6: //Source 0336 { 0337 QString source1, source2; 0338 0339 Capabilities::SourceInfoCapability *sic1 = track1->create< Capabilities::SourceInfoCapability >(); 0340 Capabilities::SourceInfoCapability *sic2 = track2->create< Capabilities::SourceInfoCapability >(); 0341 if( sic1 && sic2) 0342 { 0343 source1 = sic1->sourceName(); 0344 source2 = sic2->sourceName(); 0345 } 0346 delete sic1; 0347 delete sic2; 0348 0349 if( sic1 && sic2 ) 0350 return source1 == source2; 0351 0352 // fall back to collection 0353 return track1->collection() == track2->collection(); 0354 } 0355 case 7: //Year 0356 if( track1->year() && track2->year() ) 0357 return ( *track1->year().data() ) == ( *track2->year().data() ); 0358 return false; 0359 default: 0360 return false; 0361 } 0362 } 0363 0364 void 0365 Playlist::GroupingProxy::invalidateGrouping() 0366 { 0367 m_cachedGroupModeForRow.clear(); 0368 }