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 }