File indexing completed on 2024-05-05 04:47:29

0001 /****************************************************************************************
0002  * Copyright (c) 2007 Alexandre Pereira de Oliveira <aleprj@gmail.com>                  *
0003  * Copyright (c) 2007-2009 Maximilian Kossick <maximilian.kossick@googlemail.com>       *
0004  * Copyright (c) 2007 Nikolaj Hald Nielsen <nhn@kde.org>                                *
0005  *                                                                                      *
0006  * This program is free software; you can redistribute it and/or modify it under        *
0007  * the terms of the GNU General Public License as published by the Free Software        *
0008  * Foundation; either version 2 of the License, or (at your option) any later           *
0009  * version.                                                                             *
0010  *                                                                                      *
0011  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0012  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0013  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0014  *                                                                                      *
0015  * You should have received a copy of the GNU General Public License along with         *
0016  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0017  ****************************************************************************************/
0018 
0019 #define DEBUG_PREFIX "CollectionTreeItemModelBase"
0020 
0021 #include "CollectionTreeItemModelBase.h"
0022 
0023 #include "AmarokMimeData.h"
0024 #include "FileType.h"
0025 #include "SvgHandler.h"
0026 #include "amarokconfig.h"
0027 #include "browsers/CollectionTreeItem.h"
0028 #include "core/collections/Collection.h"
0029 #include "core/collections/QueryMaker.h"
0030 #include "core/meta/TrackEditor.h"
0031 #include "core/meta/support/MetaConstants.h"
0032 #include "core/support/Amarok.h"
0033 #include "core/support/Debug.h"
0034 #include "core-impl/collections/support/TextualQueryFilter.h"
0035 #include "widgets/PrettyTreeRoles.h"
0036 
0037 #include <KLocalizedString>
0038 #include <ThreadWeaver/Lambda>
0039 #include <ThreadWeaver/Queue>
0040 
0041 #include <QApplication>
0042 #include <QIcon>
0043 #include <QPixmap>
0044 #include <QPointer>
0045 #include <QStandardPaths>
0046 #include <QStyle>
0047 #include <QTimeLine>
0048 #include <QTimer>
0049 
0050 #include <algorithm>
0051 #include <functional>
0052 
0053 
0054 using namespace Meta;
0055 
0056 
0057 class TrackLoaderJob : public ThreadWeaver::Job
0058 {
0059 public:
0060     TrackLoaderJob( const QModelIndex &index, const Meta::AlbumPtr &album, CollectionTreeItemModelBase *model )
0061         : m_index( index )
0062         , m_album( album )
0063         , m_model( model )
0064         , m_abortRequested( false )
0065     {
0066         if( !m_model || !m_album || !m_index.isValid() )
0067             requestAbort();
0068     }
0069 
0070     void requestAbort() override
0071     {
0072         m_abortRequested = true;
0073     }
0074 
0075 protected:
0076     void run( ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread ) override
0077     {
0078         Q_UNUSED( self )
0079         Q_UNUSED( thread )
0080 
0081         if( m_abortRequested || !m_model )
0082             return;
0083 
0084         const auto tracks = m_album->tracks();
0085 
0086         if( m_model && !m_abortRequested )
0087         {
0088             auto slot = std::bind( &CollectionTreeItemModelBase::tracksLoaded, m_model, m_album, m_index, tracks );
0089             QTimer::singleShot( 0, m_model, slot );
0090         }
0091     }
0092 
0093 private:
0094     QPersistentModelIndex m_index;
0095     Meta::AlbumPtr m_album;
0096     QPointer<CollectionTreeItemModelBase> m_model;
0097     bool m_abortRequested;
0098 };
0099 
0100 inline uint qHash( const Meta::DataPtr &data )
0101 {
0102     return qHash( data.data() );
0103 }
0104 
0105 /**
0106  * This set determines which collection browser levels should have shown Various Artists
0107  * item under them. AlbumArtist is certain, (Track)Artist is questionable.
0108  */
0109 static const QSet<CategoryId::CatMenuId> variousArtistCategories =
0110         QSet<CategoryId::CatMenuId>() << CategoryId::AlbumArtist;
0111 
0112 CollectionTreeItemModelBase::CollectionTreeItemModelBase( )
0113     : QAbstractItemModel()
0114     , m_rootItem( nullptr )
0115     , m_animFrame( 0 )
0116     , m_loading1( QPixmap( QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("amarok/images/loading1.png") ) ) )
0117     , m_loading2( QPixmap( QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("amarok/images/loading2.png") ) ) )
0118     , m_currentAnimPixmap( m_loading1 )
0119     , m_autoExpand( false )
0120 {
0121     m_timeLine = new QTimeLine( 10000, this );
0122     m_timeLine->setFrameRange( 0, 20 );
0123     m_timeLine->setLoopCount ( 0 );
0124     connect( m_timeLine, &QTimeLine::frameChanged, this, &CollectionTreeItemModelBase::loadingAnimationTick );
0125 }
0126 
0127 CollectionTreeItemModelBase::~CollectionTreeItemModelBase()
0128 {
0129     KConfigGroup config = Amarok::config( QStringLiteral("Collection Browser") );
0130     QList<int> levelNumbers;
0131     foreach( CategoryId::CatMenuId category, levels() )
0132         levelNumbers.append( category );
0133     config.writeEntry( "TreeCategory", levelNumbers );
0134 
0135     if( m_rootItem )
0136         m_rootItem->deleteLater();
0137 }
0138 
0139 Qt::ItemFlags CollectionTreeItemModelBase::flags(const QModelIndex & index) const
0140 {
0141     Qt::ItemFlags flags = {};
0142     if( index.isValid() )
0143     {
0144         flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEditable;
0145     }
0146     return flags;
0147 }
0148 
0149 bool
0150 CollectionTreeItemModelBase::setData( const QModelIndex &index, const QVariant &value, int role )
0151 {
0152     Q_UNUSED( role )
0153 
0154     if( !index.isValid() )
0155         return false;
0156     CollectionTreeItem *item = static_cast<CollectionTreeItem*>( index.internalPointer() );
0157 
0158     Meta::DataPtr data = item->data();
0159 
0160     if( Meta::TrackPtr track = Meta::TrackPtr::dynamicCast( data ) )
0161     {
0162         Meta::TrackEditorPtr ec = track->editor();
0163         if( ec )
0164         {
0165             ec->setTitle( value.toString() );
0166             Q_EMIT dataChanged( index, index );
0167             return true;
0168         }
0169     }
0170     else if( Meta::AlbumPtr album = Meta::AlbumPtr::dynamicCast( data ) )
0171     {
0172         Meta::TrackList tracks = album->tracks();
0173         if( !tracks.isEmpty() )
0174         {
0175             foreach( Meta::TrackPtr track, tracks )
0176             {
0177                 Meta::TrackEditorPtr ec = track->editor();
0178                 if( ec )
0179                     ec->setAlbum( value.toString() );
0180             }
0181             Q_EMIT dataChanged( index, index );
0182             return true;
0183         }
0184     }
0185     else if( Meta::ArtistPtr artist = Meta::ArtistPtr::dynamicCast( data ) )
0186     {
0187         Meta::TrackList tracks = artist->tracks();
0188         if( !tracks.isEmpty() )
0189         {
0190             foreach( Meta::TrackPtr track, tracks )
0191             {
0192                 Meta::TrackEditorPtr ec = track->editor();
0193                 if( ec )
0194                     ec->setArtist( value.toString() );
0195             }
0196             Q_EMIT dataChanged( index, index );
0197             return true;
0198         }
0199     }
0200     else if( Meta::GenrePtr genre = Meta::GenrePtr::dynamicCast( data ) )
0201     {
0202         Meta::TrackList tracks = genre->tracks();
0203         if( !tracks.isEmpty() )
0204         {
0205             foreach( Meta::TrackPtr track, tracks )
0206             {
0207                 Meta::TrackEditorPtr ec = track->editor();
0208                 if( ec )
0209                     ec->setGenre( value.toString() );
0210             }
0211             Q_EMIT dataChanged( index, index );
0212             return true;
0213         }
0214     }
0215     else if( Meta::YearPtr year = Meta::YearPtr::dynamicCast( data ) )
0216     {
0217         Meta::TrackList tracks = year->tracks();
0218         if( !tracks.isEmpty() )
0219         {
0220             foreach( Meta::TrackPtr track, tracks )
0221             {
0222                 Meta::TrackEditorPtr ec = track->editor();
0223                 if( ec )
0224                     ec->setYear( value.toInt() );
0225             }
0226             Q_EMIT dataChanged( index, index );
0227             return true;
0228         }
0229     }
0230     else if( Meta::ComposerPtr composer = Meta::ComposerPtr::dynamicCast( data ) )
0231     {
0232         Meta::TrackList tracks = composer->tracks();
0233         if( !tracks.isEmpty() )
0234         {
0235             foreach( Meta::TrackPtr track, tracks )
0236             {
0237                 Meta::TrackEditorPtr ec = track->editor();
0238                 if( ec )
0239                     ec->setComposer( value.toString() );
0240             }
0241             Q_EMIT dataChanged( index, index );
0242             return true;
0243         }
0244     }
0245     return false;
0246 }
0247 
0248 QVariant
0249 CollectionTreeItemModelBase::dataForItem( CollectionTreeItem *item, int role, int level ) const
0250 {
0251     if( level == -1 )
0252         level = item->level();
0253 
0254     if( item->isTrackItem() )
0255     {
0256         Meta::TrackPtr track = Meta::TrackPtr::dynamicCast( item->data() );
0257         switch( role )
0258         {
0259         case Qt::DisplayRole:
0260         case Qt::ToolTipRole:
0261         case PrettyTreeRoles::FilterRole:
0262             {
0263                 QString name = track->prettyName();
0264                 Meta::AlbumPtr album = track->album();
0265                 Meta::ArtistPtr artist = track->artist();
0266 
0267                 if( album && artist && album->isCompilation() )
0268                     name.prepend( QStringLiteral("%1 - ").arg(artist->prettyName()) );
0269 
0270                 if( AmarokConfig::showTrackNumbers() )
0271                 {
0272                     int trackNum = track->trackNumber();
0273                     if( trackNum > 0 )
0274                         name.prepend( QStringLiteral("%1 - ").arg(trackNum) );
0275                 }
0276 
0277                 // Check empty after track logic and before album logic
0278                 if( name.isEmpty() )
0279                     name = i18nc( "The Name is not known", "Unknown" );
0280                 return name;
0281             }
0282 
0283         case Qt::DecorationRole:
0284             return QIcon::fromTheme( QStringLiteral("media-album-track") );
0285         case PrettyTreeRoles::SortRole:
0286             return track->sortableName();
0287         }
0288     }
0289     else if( item->isAlbumItem() )
0290     {
0291         Meta::AlbumPtr album = Meta::AlbumPtr::dynamicCast( item->data() );
0292         switch( role )
0293         {
0294         case Qt::DisplayRole:
0295         case Qt::ToolTipRole:
0296             {
0297                 QString name = album->prettyName();
0298                 // add years for named albums (if enabled)
0299                 if( AmarokConfig::showYears() )
0300                 {
0301                     if( m_years.contains( album.data() ) )
0302                     {
0303                         int year = m_years.value( album.data() );
0304 
0305                         if( year > 0 )
0306                             name.prepend( QStringLiteral("%1 - ").arg( year ) );
0307                     }
0308                     else if( !album->name().isEmpty() )
0309                     {
0310                         if( !m_loadingAlbums.contains( album ) )
0311                         {
0312                             m_loadingAlbums.insert( album );
0313 
0314                             auto nonConstThis = const_cast<CollectionTreeItemModelBase*>( this );
0315                             auto job = QSharedPointer<TrackLoaderJob>::create( itemIndex( item ), album, nonConstThis );
0316                             ThreadWeaver::Queue::instance()->enqueue( job );
0317                         }
0318                     }
0319                 }
0320                 return name;
0321             }
0322 
0323         case Qt::DecorationRole:
0324             if( AmarokConfig::showAlbumArt() )
0325             {
0326                 QStyle *style = QApplication::style();
0327                 const int largeIconSize = style->pixelMetric( QStyle::PM_LargeIconSize );
0328 
0329                 return The::svgHandler()->imageWithBorder( album, largeIconSize, 2 );
0330             }
0331             else
0332                 return iconForLevel( level );
0333 
0334         case PrettyTreeRoles::SortRole:
0335             return album->sortableName();
0336 
0337         case PrettyTreeRoles::HasCoverRole:
0338             return AmarokConfig::showAlbumArt();
0339 
0340         case PrettyTreeRoles::YearRole:
0341             {
0342                 if( m_years.contains( album.data() ) )
0343                     return m_years.value( album.data() );
0344 
0345                 else if( !album->name().isEmpty() )
0346                 {
0347                     if( !m_loadingAlbums.contains( album ) )
0348                     {
0349                         m_loadingAlbums.insert( album );
0350 
0351                         auto nonConstThis = const_cast<CollectionTreeItemModelBase*>( this );
0352                         auto job = QSharedPointer<TrackLoaderJob>::create( itemIndex( item ), album, nonConstThis );
0353                         ThreadWeaver::Queue::instance()->enqueue( job );
0354                     }
0355                 }
0356                 return -1;
0357             }
0358         }
0359     }
0360     else if( item->isDataItem() )
0361     {
0362         switch( role )
0363         {
0364         case Qt::DisplayRole:
0365         case Qt::ToolTipRole:
0366         case PrettyTreeRoles::FilterRole:
0367             {
0368                 QString name = item->data()->prettyName();
0369                 if( name.isEmpty() )
0370                     name = i18nc( "The Name is not known", "Unknown" );
0371                 return name;
0372             }
0373 
0374         case Qt::DecorationRole:
0375             {
0376                 if( m_childQueries.values().contains( item ) )
0377                 {
0378                     if( level < m_levelType.count() )
0379                         return m_currentAnimPixmap;
0380                 }
0381                 return iconForLevel( level );
0382             }
0383 
0384         case PrettyTreeRoles::SortRole:
0385             return item->data()->sortableName();
0386         }
0387     }
0388     else if( item->isVariousArtistItem() )
0389     {
0390         switch( role )
0391         {
0392         case Qt::DecorationRole:
0393             return QIcon::fromTheme( QStringLiteral("similarartists-amarok") );
0394         case Qt::DisplayRole:
0395             return i18n( "Various Artists" );
0396         case PrettyTreeRoles::SortRole:
0397             return QString(); // so that we can compare it against other strings
0398         }
0399     }
0400 
0401     // -- all other roles are handled by item
0402     return item->data( role );
0403 }
0404 
0405 QVariant
0406 CollectionTreeItemModelBase::headerData(int section, Qt::Orientation orientation, int role) const
0407 {
0408     if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
0409     {
0410         if (section == 0)
0411             return m_headerText;
0412     }
0413     return QVariant();
0414 }
0415 
0416 QModelIndex
0417 CollectionTreeItemModelBase::index(int row, int column, const QModelIndex & parent) const
0418 {
0419     //ensure sanity of parameters
0420     //we are a tree model, there are no columns
0421     if( row < 0 || column != 0 )
0422         return QModelIndex();
0423 
0424     CollectionTreeItem *parentItem;
0425 
0426     if (!parent.isValid())
0427         parentItem = m_rootItem;
0428     else
0429         parentItem = static_cast<CollectionTreeItem*>(parent.internalPointer());
0430 
0431     CollectionTreeItem *childItem = parentItem->child(row);
0432     if( childItem )
0433         return createIndex(row, column, childItem);
0434     else
0435         return QModelIndex();
0436 }
0437 
0438 QModelIndex
0439 CollectionTreeItemModelBase::parent(const QModelIndex & index) const
0440 {
0441      if( !index.isValid() )
0442          return QModelIndex();
0443 
0444      CollectionTreeItem *childItem = static_cast<CollectionTreeItem*>(index.internalPointer());
0445      CollectionTreeItem *parentItem = childItem->parent();
0446 
0447      return itemIndex( parentItem );
0448 }
0449 
0450 int
0451 CollectionTreeItemModelBase::rowCount(const QModelIndex & parent) const
0452 {
0453     CollectionTreeItem *parentItem;
0454 
0455     if( !parent.isValid() )
0456         parentItem = m_rootItem;
0457     else
0458         parentItem = static_cast<CollectionTreeItem*>(parent.internalPointer());
0459 
0460     return parentItem->childCount();
0461 }
0462 
0463 int CollectionTreeItemModelBase::columnCount(const QModelIndex & parent) const
0464 {
0465     Q_UNUSED( parent )
0466     return 1;
0467 }
0468 
0469 QStringList
0470 CollectionTreeItemModelBase::mimeTypes() const
0471 {
0472     QStringList types;
0473     types << AmarokMimeData::TRACK_MIME;
0474     return types;
0475 }
0476 
0477 QMimeData*
0478 CollectionTreeItemModelBase::mimeData( const QModelIndexList &indices ) const
0479 {
0480     if ( indices.isEmpty() )
0481         return nullptr;
0482 
0483     // first, filter out duplicate entries that may arise when both parent and child are selected
0484     QSet<QModelIndex> indexSet( indices.begin(), indices.end() );
0485     QMutableSetIterator<QModelIndex> it( indexSet );
0486     while( it.hasNext() )
0487     {
0488         it.next();
0489         // we go up in parent hierarchy searching whether some parent indices are already in set
0490         QModelIndex parentIndex = it.value();
0491         while( parentIndex.isValid() )  // leave the root (top, invalid) index intact
0492         {
0493             parentIndex = parentIndex.parent();  // yes, we start from the parent of current index
0494             if( indexSet.contains( parentIndex ) )
0495             {
0496                 it.remove(); // parent already in selected set, remove child
0497                 break; // no need to continue inner loop, already deleted
0498             }
0499         }
0500     }
0501 
0502     QList<CollectionTreeItem*> items;
0503     foreach( const QModelIndex &index, indexSet )
0504     {
0505         if( index.isValid() )
0506             items << static_cast<CollectionTreeItem*>( index.internalPointer() );
0507     }
0508 
0509     return mimeData( items );
0510 }
0511 
0512 QMimeData*
0513 CollectionTreeItemModelBase::mimeData( const QList<CollectionTreeItem*> &items ) const
0514 {
0515     if ( items.isEmpty() )
0516         return nullptr;
0517 
0518     Meta::TrackList tracks;
0519     QList<Collections::QueryMaker*> queries;
0520 
0521     foreach( CollectionTreeItem *item, items )
0522     {
0523         if( item->allDescendentTracksLoaded() ) {
0524             tracks << item->descendentTracks();
0525         }
0526         else
0527         {
0528             Collections::QueryMaker *qm = item->queryMaker();
0529             for( CollectionTreeItem *tmp = item; tmp; tmp = tmp->parent() )
0530                 tmp->addMatch( qm, levelCategory( tmp->level() - 1 ) );
0531             Collections::addTextualFilter( qm, m_currentFilter );
0532             queries.append( qm );
0533         }
0534     }
0535 
0536     std::stable_sort( tracks.begin(), tracks.end(), Meta::Track::lessThan );
0537 
0538     AmarokMimeData *mimeData = new AmarokMimeData();
0539     mimeData->setTracks( tracks );
0540     mimeData->setQueryMakers( queries );
0541     mimeData->startQueries();
0542     return mimeData;
0543 }
0544 
0545 bool
0546 CollectionTreeItemModelBase::hasChildren ( const QModelIndex & parent ) const
0547 {
0548      if( !parent.isValid() )
0549          return true; // must be root item!
0550 
0551     CollectionTreeItem *item = static_cast<CollectionTreeItem*>(parent.internalPointer());
0552     //we added the collection level so we have to be careful with the item level
0553     return !item->isDataItem() || item->level() + levelModifier() <= m_levelType.count();
0554 
0555 }
0556 
0557 void
0558 CollectionTreeItemModelBase::ensureChildrenLoaded( CollectionTreeItem *item )
0559 {
0560     //only start a query if necessary and we are not querying for the item's children already
0561     if ( item->requiresUpdate() && !m_runningQueries.contains( item ) )
0562     {
0563         listForLevel( item->level() + levelModifier(), item->queryMaker(), item );
0564     }
0565 }
0566 
0567 CollectionTreeItem *
0568 CollectionTreeItemModelBase::treeItem( const QModelIndex &index ) const
0569 {
0570     if( !index.isValid() || index.model() != this )
0571         return nullptr;
0572 
0573     return static_cast<CollectionTreeItem *>( index.internalPointer() );
0574 }
0575 
0576 QModelIndex
0577 CollectionTreeItemModelBase::itemIndex( CollectionTreeItem *item ) const
0578 {
0579     if( !item || item == m_rootItem )
0580         return QModelIndex();
0581 
0582     return createIndex( item->row(), 0, item );
0583 }
0584 
0585 void
0586 CollectionTreeItemModelBase::listForLevel(int level, Collections::QueryMaker * qm, CollectionTreeItem * parent)
0587 {
0588     if( qm && parent )
0589     {
0590         // this check should not hurt anyone... needs to check if single... needs it
0591         if( m_runningQueries.contains( parent ) )
0592             return;
0593 
0594         // following special cases are handled extra - right when the parent is added
0595         if( level > m_levelType.count() ||
0596             parent->isVariousArtistItem() ||
0597             parent->isNoLabelItem() )
0598         {
0599             qm->deleteLater();
0600             return;
0601         }
0602 
0603         // - the last level are always the tracks
0604         if ( level == m_levelType.count() )
0605             qm->setQueryType( Collections::QueryMaker::Track );
0606 
0607         // - all other levels are more complicate
0608         else
0609         {
0610             Collections::QueryMaker::QueryType nextLevel;
0611             if( level + 1 >= m_levelType.count() )
0612                 nextLevel = Collections::QueryMaker::Track;
0613             else
0614                 nextLevel = mapCategoryToQueryType( m_levelType.value( level + 1 ) );
0615 
0616             qm->setQueryType( mapCategoryToQueryType( m_levelType.value( level ) ) );
0617 
0618             CategoryId::CatMenuId category = m_levelType.value( level );
0619             if( category == CategoryId::Album )
0620             {
0621                 // restrict query to normal albums if the previous level
0622                 // was the AlbumArtist category. In that case we handle compilations below
0623                 if( levelCategory( level - 1 ) == CategoryId::AlbumArtist )
0624                     qm->setAlbumQueryMode( Collections::QueryMaker::OnlyNormalAlbums );
0625             }
0626             else if( variousArtistCategories.contains( category ) )
0627                 // we used to handleCompilations() only if nextLevel is Album, but I cannot
0628                 // tell any reason why we should have done this --- strohel
0629                 handleCompilations( nextLevel, parent );
0630             else if( category == CategoryId::Label )
0631                 handleTracksWithoutLabels( nextLevel, parent );
0632         }
0633 
0634         for( CollectionTreeItem *tmp = parent; tmp; tmp = tmp->parent() )
0635             tmp->addMatch( qm, levelCategory( tmp->level() - 1 ) );
0636         Collections::addTextualFilter( qm, m_currentFilter );
0637         addQueryMaker( parent, qm );
0638         m_childQueries.insert( qm, parent );
0639         qm->run();
0640 
0641         //some very quick queries may be done so fast that the loading
0642         //animation creates an unnecessary flicker, therefore delay it for a bit
0643         QTimer::singleShot( 150, this, &CollectionTreeItemModelBase::startAnimationTick );
0644     }
0645 }
0646 
0647 void
0648 CollectionTreeItemModelBase::setLevels( const QList<CategoryId::CatMenuId> &levelType )
0649 {
0650     if( m_levelType == levelType )
0651         return;
0652 
0653     m_levelType = levelType;
0654     updateHeaderText();
0655     m_expandedItems.clear();
0656     m_expandedSpecialNodes.clear();
0657     m_runningQueries.clear();
0658     m_childQueries.clear();
0659     m_compilationQueries.clear();
0660     filterChildren();
0661 }
0662 
0663 Collections::QueryMaker::QueryType
0664 CollectionTreeItemModelBase::mapCategoryToQueryType( int levelType ) const
0665 {
0666     Collections::QueryMaker::QueryType type;
0667     switch( levelType )
0668     {
0669     case CategoryId::Album:
0670         type = Collections::QueryMaker::Album;
0671         break;
0672     case CategoryId::Artist:
0673         type = Collections::QueryMaker::Artist;
0674         break;
0675     case CategoryId::AlbumArtist:
0676         type = Collections::QueryMaker::AlbumArtist;
0677         break;
0678     case CategoryId::Composer:
0679         type = Collections::QueryMaker::Composer;
0680         break;
0681     case CategoryId::Genre:
0682         type = Collections::QueryMaker::Genre;
0683         break;
0684     case CategoryId::Label:
0685         type = Collections::QueryMaker::Label;
0686         break;
0687     case CategoryId::Year:
0688         type = Collections::QueryMaker::Year;
0689         break;
0690     default:
0691         type = Collections::QueryMaker::None;
0692         break;
0693     }
0694 
0695     return type;
0696 }
0697 
0698 void
0699 CollectionTreeItemModelBase::tracksLoaded( const Meta::AlbumPtr &album, const QModelIndex &index, const Meta::TrackList& tracks )
0700 {
0701     DEBUG_BLOCK
0702 
0703     if( !album )
0704         return;
0705 
0706     m_loadingAlbums.remove( album );
0707 
0708     if( !index.isValid() )
0709         return;
0710 
0711     int year = 0;
0712 
0713     if( !tracks.isEmpty() )
0714     {
0715         Meta::YearPtr yearPtr = tracks.first()->year();
0716         if( yearPtr )
0717             year = yearPtr->year();
0718 
0719         debug() << "Valid album year found:" << year;
0720     }
0721 
0722     if( !m_years.contains( album.data() ) || m_years.value( album.data() ) != year )
0723     {
0724         m_years[ album.data() ] = year;
0725         Q_EMIT dataChanged( index, index );
0726     }
0727 }
0728 
0729 void
0730 CollectionTreeItemModelBase::addQueryMaker( CollectionTreeItem* item,
0731                                             Collections::QueryMaker *qm ) const
0732 {
0733     connect( qm, &Collections::QueryMaker::newTracksReady, this, &CollectionTreeItemModelBase::newTracksReady, Qt::QueuedConnection );
0734     connect( qm, &Collections::QueryMaker::newArtistsReady, this, &CollectionTreeItemModelBase::newArtistsReady, Qt::QueuedConnection );
0735     connect( qm, &Collections::QueryMaker::newAlbumsReady, this, &CollectionTreeItemModelBase::newAlbumsReady, Qt::QueuedConnection );
0736     connect( qm, &Collections::QueryMaker::newGenresReady, this, &CollectionTreeItemModelBase::newGenresReady, Qt::QueuedConnection );
0737     connect( qm, &Collections::QueryMaker::newComposersReady, this, &CollectionTreeItemModelBase::newComposersReady, Qt::QueuedConnection );
0738     connect( qm, &Collections::QueryMaker::newYearsReady, this, &CollectionTreeItemModelBase::newYearsReady, Qt::QueuedConnection );
0739     connect( qm, &Collections::QueryMaker::newLabelsReady, this, &CollectionTreeItemModelBase::newLabelsReady, Qt::QueuedConnection );
0740     connect( qm, &Collections::QueryMaker::newDataReady, this, &CollectionTreeItemModelBase::newDataReady, Qt::QueuedConnection );
0741     connect( qm, &Collections::QueryMaker::queryDone, this, &CollectionTreeItemModelBase::queryDone, Qt::QueuedConnection );
0742     m_runningQueries.insert( item, qm );
0743 }
0744 
0745 void
0746 CollectionTreeItemModelBase::queryDone()
0747 {
0748     Collections::QueryMaker *qm = qobject_cast<Collections::QueryMaker*>( sender() );
0749     if( !qm )
0750         return;
0751 
0752     CollectionTreeItem* item = nullptr;
0753     if( m_childQueries.contains( qm ) )
0754         item = m_childQueries.take( qm );
0755     else if( m_compilationQueries.contains( qm ) )
0756         item = m_compilationQueries.take( qm );
0757     else if( m_noLabelsQueries.contains( qm ) )
0758         item = m_noLabelsQueries.take( qm );
0759 
0760     if( item )
0761         m_runningQueries.remove( item, qm );
0762 
0763     //reset icon for this item
0764     if( item && item != m_rootItem )
0765     {
0766         Q_EMIT dataChanged( itemIndex( item ), itemIndex( item ) );
0767     }
0768 
0769     //stop timer if there are no more animations active
0770     if( m_runningQueries.isEmpty() )
0771     {
0772         Q_EMIT allQueriesFinished( m_autoExpand );
0773         m_autoExpand = false; // reset to default value
0774         m_timeLine->stop();
0775     }
0776     qm->deleteLater();
0777 }
0778 
0779 // TODO
0780 
0781 /** Small helper function to convert a list of e.g. tracks to a list of DataPtr */
0782 template<class PointerType>
0783 static Meta::DataList
0784 convertToDataList( const QList<PointerType>& list )
0785 {
0786     Meta::DataList data;
0787     for( const auto &p : list )
0788         data << Meta::DataPtr::staticCast( p );
0789 
0790     return data;
0791 }
0792 
0793 void
0794 CollectionTreeItemModelBase::newTracksReady( const Meta::TrackList &res )
0795 {
0796     newDataReady( convertToDataList( res ) );
0797 }
0798 
0799 void
0800 CollectionTreeItemModelBase::newArtistsReady( const Meta::ArtistList &res )
0801 {
0802     newDataReady( convertToDataList( res ) );
0803 }
0804 
0805 void
0806 CollectionTreeItemModelBase::newAlbumsReady( const Meta::AlbumList &res )
0807 {
0808     newDataReady( convertToDataList( res ) );
0809 }
0810 
0811 void
0812 CollectionTreeItemModelBase::newGenresReady( const Meta::GenreList &res )
0813 {
0814     newDataReady( convertToDataList( res ) );
0815 }
0816 
0817 void
0818 CollectionTreeItemModelBase::newComposersReady( const Meta::ComposerList &res )
0819 {
0820     newDataReady( convertToDataList( res ) );
0821 }
0822 
0823 void
0824 CollectionTreeItemModelBase::newYearsReady( const Meta::YearList &res )
0825 {
0826     newDataReady( convertToDataList( res ) );
0827 }
0828 
0829 void
0830 CollectionTreeItemModelBase::newLabelsReady( const Meta::LabelList &res )
0831 {
0832     newDataReady( convertToDataList( res ) );
0833 }
0834 
0835 void
0836 CollectionTreeItemModelBase::newDataReady( const Meta::DataList &data )
0837 {
0838     //if we are expanding an item, we'll find the sender in childQueries
0839     //otherwise we are filtering all collections
0840     Collections::QueryMaker *qm = qobject_cast<Collections::QueryMaker*>( sender() );
0841     if( !qm )
0842         return;
0843 
0844     if( m_childQueries.contains( qm ) )
0845         handleNormalQueryResult( qm, data );
0846 
0847     else if( m_compilationQueries.contains( qm ) )
0848         handleSpecialQueryResult( CollectionTreeItem::VariousArtist, qm, data );
0849 
0850     else if( m_noLabelsQueries.contains( qm ) )
0851         handleSpecialQueryResult( CollectionTreeItem::NoLabel, qm, data );
0852 }
0853 
0854 void
0855 CollectionTreeItemModelBase::handleSpecialQueryResult( CollectionTreeItem::Type type, Collections::QueryMaker *qm, const Meta::DataList &dataList )
0856 {
0857     CollectionTreeItem *parent = nullptr;
0858 
0859     if( type == CollectionTreeItem::VariousArtist )
0860         parent = m_compilationQueries.value( qm );
0861 
0862     else if( type == CollectionTreeItem::NoLabel )
0863         parent = m_noLabelsQueries.value( qm );
0864 
0865     if( parent )
0866     {
0867         QModelIndex parentIndex = itemIndex( parent );
0868 
0869         //if the special query did not return a result we have to remove the
0870         //the special node itself
0871         if( dataList.isEmpty() )
0872         {
0873             for( int i = 0; i < parent->childCount(); i++ )
0874             {
0875                 CollectionTreeItem *cti = parent->child( i );
0876                 if( cti->type() == type )
0877                 {
0878                     //found the special node
0879                     beginRemoveRows( parentIndex, cti->row(), cti->row() );
0880                     cti = nullptr; //will be deleted;
0881                     parent->removeChild( i );
0882                     endRemoveRows();
0883                     break;
0884                 }
0885             }
0886             //we have removed the special node if it existed
0887             return;
0888         }
0889 
0890         CollectionTreeItem *specialNode = nullptr;
0891         if( parent->childCount() == 0 )
0892         {
0893             //we only insert the special node
0894             beginInsertRows( parentIndex, 0, 0 );
0895             specialNode = new CollectionTreeItem( type, dataList, parent, this );
0896             //set requiresUpdate, otherwise we will query for the children of specialNode again!
0897             specialNode->setRequiresUpdate( false );
0898             endInsertRows();
0899         }
0900         else
0901         {
0902             for( int i = 0; i < parent->childCount(); i++ )
0903             {
0904                 CollectionTreeItem *cti = parent->child( i );
0905                 if( cti->type() == type )
0906                 {
0907                     //found the special node
0908                     specialNode = cti;
0909                     break;
0910                 }
0911             }
0912             if( !specialNode )
0913             {
0914                 //we only insert the special node
0915                 beginInsertRows( parentIndex, 0, 0 );
0916                 specialNode = new CollectionTreeItem( type, dataList, parent, this );
0917                 //set requiresUpdate, otherwise we will query for the children of specialNode again!
0918                 specialNode->setRequiresUpdate( false );
0919                 endInsertRows();
0920             }
0921             else
0922             {
0923                 //only call populateChildren for the special node if we have not
0924                 //created it in this method call. The special node ctor takes care
0925                 //of that itself
0926                 populateChildren( dataList, specialNode, itemIndex( specialNode ) );
0927             }
0928             //populate children will call setRequiresUpdate on vaNode
0929             //but as the special query is based on specialNode's parent,
0930             //we have to call setRequiresUpdate on the parent too
0931             //yes, this will mean we will call setRequiresUpdate twice
0932             parent->setRequiresUpdate( false );
0933 
0934             for( int count = specialNode->childCount(), i = 0; i < count; ++i )
0935             {
0936                 CollectionTreeItem *item = specialNode->child( i );
0937                 if ( m_expandedItems.contains( item->data() ) ) //item will always be a data item
0938                 {
0939                     listForLevel( item->level() + levelModifier(), item->queryMaker(), item );
0940                 }
0941             }
0942         }
0943 
0944         //if the special node exists, check if it has to be expanded
0945         if( specialNode )
0946         {
0947             if( m_expandedSpecialNodes.contains( parent->parentCollection() ) )
0948             {
0949                 Q_EMIT expandIndex( createIndex( 0, 0, specialNode ) ); //we have just inserted the vaItem at row 0
0950             }
0951         }
0952     }
0953 }
0954 
0955 void
0956 CollectionTreeItemModelBase::handleNormalQueryResult( Collections::QueryMaker *qm, const Meta::DataList &dataList )
0957 {
0958     CollectionTreeItem *parent = m_childQueries.value( qm );
0959     if( parent ) {
0960         QModelIndex parentIndex = itemIndex( parent );
0961         populateChildren( dataList, parent, parentIndex );
0962 
0963         if ( parent->isDataItem() )
0964         {
0965             if ( m_expandedItems.contains( parent->data() ) )
0966                 Q_EMIT expandIndex( parentIndex );
0967             else
0968                 //simply insert the item, nothing will change if it is already in the set
0969                 m_expandedItems.insert( parent->data() );
0970         }
0971     }
0972 }
0973 
0974 void
0975 CollectionTreeItemModelBase::populateChildren( const DataList &dataList, CollectionTreeItem *parent, const QModelIndex &parentIndex )
0976 {
0977     CategoryId::CatMenuId childCategory = levelCategory( parent->level() );
0978 
0979     // add new rows after existing ones here (which means all artists nodes
0980     // will be inserted after the "Various Artists" node)
0981     // figure out which children of parent have to be removed,
0982     // which new children have to be added, and preemptively Q_EMIT dataChanged for the rest
0983     // have to check how that influences performance...
0984     const QSet<Meta::DataPtr> dataSet(dataList.begin(), dataList.end());
0985     QSet<Meta::DataPtr> childrenSet;
0986     foreach( CollectionTreeItem *child, parent->children() )
0987     {
0988         // we don't add null children, these are special-cased below
0989         if( !child->data() )
0990             continue;
0991 
0992         childrenSet.insert( child->data() );
0993     }
0994     const QSet<Meta::DataPtr> dataToBeAdded = dataSet - childrenSet;
0995     const QSet<Meta::DataPtr> dataToBeRemoved = childrenSet - dataSet;
0996 
0997     // first remove all rows that have to be removed
0998     // walking through the children in reverse order does not screw up the order
0999     for( int i = parent->childCount() - 1; i >= 0; i-- )
1000     {
1001         CollectionTreeItem *child = parent->child( i );
1002 
1003         // should this child be removed?
1004         bool toBeRemoved;
1005 
1006         if( child->isDataItem() )
1007             toBeRemoved = dataToBeRemoved.contains( child->data() );
1008         else if( child->isVariousArtistItem() )
1009             toBeRemoved = !variousArtistCategories.contains( childCategory );
1010         else if( child->isNoLabelItem() )
1011             toBeRemoved = childCategory != CategoryId::Label;
1012         else
1013         {
1014             warning() << "Unknown child type encountered in populateChildren(), removing";
1015             toBeRemoved = true;
1016         }
1017 
1018         if( toBeRemoved )
1019         {
1020             beginRemoveRows( parentIndex, i, i );
1021             parent->removeChild( i );
1022             endRemoveRows();
1023         }
1024         else
1025         {
1026             // the remaining child items may be dirty, so refresh them
1027             if( child->isDataItem() && child->data() && m_expandedItems.contains( child->data() ) )
1028                 ensureChildrenLoaded( child );
1029 
1030             // tell the view that the existing children may have changed
1031             QModelIndex idx = index( i, 0, parentIndex );
1032             Q_EMIT dataChanged( idx, idx );
1033         }
1034     }
1035 
1036     // add the new rows
1037     if( !dataToBeAdded.isEmpty() )
1038     {
1039         int lastRow = parent->childCount() - 1;
1040         //the above check ensures that Qt does not crash on beginInsertRows ( because lastRow+1 > lastRow+0)
1041         beginInsertRows( parentIndex, lastRow + 1, lastRow + dataToBeAdded.count() );
1042         foreach( Meta::DataPtr data, dataToBeAdded )
1043         {
1044             new CollectionTreeItem( data, parent, this );
1045         }
1046         endInsertRows();
1047     }
1048 
1049     parent->setRequiresUpdate( false );
1050 }
1051 
1052 void
1053 CollectionTreeItemModelBase::updateHeaderText()
1054 {
1055     m_headerText.clear();
1056     for( int i=0; i< m_levelType.count(); ++i )
1057         m_headerText += nameForLevel( i ) + " / ";
1058 
1059     m_headerText.chop( 3 );
1060 }
1061 
1062 QIcon
1063 CollectionTreeItemModelBase::iconForCategory( CategoryId::CatMenuId category )
1064 {
1065     switch( category )
1066     {
1067         case CategoryId::Album :
1068             return QIcon::fromTheme( QStringLiteral("media-optical-amarok") );
1069         case CategoryId::Artist :
1070             return QIcon::fromTheme( QStringLiteral("view-media-artist-amarok") );
1071         case CategoryId::AlbumArtist :
1072             return QIcon::fromTheme( QStringLiteral("view-media-artist-amarok") );
1073         case CategoryId::Composer :
1074             return QIcon::fromTheme( QStringLiteral("filename-composer-amarok") );
1075         case CategoryId::Genre :
1076             return QIcon::fromTheme( QStringLiteral("favorite-genres-amarok") );
1077         case CategoryId::Year :
1078             return QIcon::fromTheme( QStringLiteral("clock") );
1079         case CategoryId::Label :
1080             return QIcon::fromTheme( QStringLiteral("label-amarok") );
1081         case CategoryId::None:
1082         default:
1083             return QIcon::fromTheme( QStringLiteral("image-missing") );
1084     }
1085 
1086 }
1087 
1088 QIcon
1089 CollectionTreeItemModelBase::iconForLevel( int level ) const
1090 {
1091     return iconForCategory( m_levelType.value( level ) );
1092 }
1093 
1094 QString
1095 CollectionTreeItemModelBase::nameForCategory( CategoryId::CatMenuId category, bool showYears )
1096 {
1097     switch( category )
1098     {
1099         case CategoryId::Album:
1100             return showYears ? i18n( "Year - Album" ) : i18n( "Album" );
1101         case CategoryId::Artist:
1102             return i18n( "Track Artist" );
1103         case CategoryId::AlbumArtist:
1104             return i18n( "Album Artist" );
1105         case CategoryId::Composer:
1106             return i18n( "Composer" );
1107         case CategoryId::Genre:
1108             return i18n( "Genre" );
1109         case CategoryId::Year:
1110             return i18n( "Year" );
1111         case CategoryId::Label:
1112             return i18n( "Label" );
1113         case CategoryId::None:
1114             return i18n( "None" );
1115         default:
1116             return QString();
1117     }
1118 }
1119 
1120 QString
1121 CollectionTreeItemModelBase::nameForLevel( int level ) const
1122 {
1123     return nameForCategory( m_levelType.value( level ), AmarokConfig::showYears() );
1124 }
1125 
1126 void
1127 CollectionTreeItemModelBase::handleCompilations( Collections::QueryMaker::QueryType queryType, CollectionTreeItem *parent ) const
1128 {
1129     // this method will be called when we retrieve a list of artists from the database.
1130     // we have to query for all compilations, and then add a "Various Artists" node if at least
1131     // one compilation exists
1132     Collections::QueryMaker *qm = parent->queryMaker();
1133     qm->setQueryType( queryType );
1134     qm->setAlbumQueryMode( Collections::QueryMaker::OnlyCompilations );
1135     for( CollectionTreeItem *tmp = parent; tmp; tmp = tmp->parent() )
1136         tmp->addMatch( qm, levelCategory( tmp->level() - 1 ) );
1137 
1138     Collections::addTextualFilter( qm, m_currentFilter );
1139     addQueryMaker( parent, qm );
1140     m_compilationQueries.insert( qm, parent );
1141     qm->run();
1142 }
1143 
1144 void
1145 CollectionTreeItemModelBase::handleTracksWithoutLabels( Collections::QueryMaker::QueryType queryType, CollectionTreeItem *parent ) const
1146 {
1147     Collections::QueryMaker *qm = parent->queryMaker();
1148     qm->setQueryType( queryType );
1149     qm->setLabelQueryMode( Collections::QueryMaker::OnlyWithoutLabels );
1150     for( CollectionTreeItem *tmp = parent; tmp; tmp = tmp->parent() )
1151         tmp->addMatch( qm, levelCategory( tmp->level() - 1 ) );
1152 
1153     Collections::addTextualFilter( qm, m_currentFilter );
1154     addQueryMaker( parent, qm );
1155     m_noLabelsQueries.insert( qm, parent );
1156     qm->run();
1157 }
1158 
1159 
1160 void CollectionTreeItemModelBase::startAnimationTick()
1161 {
1162     //start animation
1163     if( ( m_timeLine->state() != QTimeLine::Running ) && !m_runningQueries.isEmpty() )
1164         m_timeLine->start();
1165 }
1166 
1167 void CollectionTreeItemModelBase::loadingAnimationTick()
1168 {
1169     if ( m_animFrame == 0 )
1170         m_currentAnimPixmap = m_loading2;
1171     else
1172         m_currentAnimPixmap = m_loading1;
1173 
1174     m_animFrame = 1 - m_animFrame;
1175 
1176     //trigger an update of all items being populated at the moment;
1177 
1178     QList< CollectionTreeItem * > items = m_runningQueries.uniqueKeys();
1179     foreach ( CollectionTreeItem* item, items  )
1180     {
1181         if( item == m_rootItem )
1182             continue;
1183         Q_EMIT dataChanged( itemIndex( item ), itemIndex( item ) );
1184     }
1185 }
1186 
1187 QString
1188 CollectionTreeItemModelBase::currentFilter() const
1189 {
1190     return m_currentFilter;
1191 }
1192 
1193 void
1194 CollectionTreeItemModelBase::setCurrentFilter( const QString &filter )
1195 {
1196     m_currentFilter = filter;
1197     slotFilter( /* autoExpand */ true );
1198 }
1199 
1200 void
1201 CollectionTreeItemModelBase::slotFilter( bool autoExpand )
1202 {
1203     m_autoExpand = autoExpand;
1204     filterChildren();
1205 
1206     // following is not auto-expansion, it is restoring the state before filtering
1207     foreach( Collections::Collection *expanded, m_expandedCollections )
1208     {
1209         CollectionTreeItem *expandedItem = m_collections.value( expanded->collectionId() ).second;
1210         if( expandedItem )
1211             Q_EMIT expandIndex( itemIndex( expandedItem ) );
1212     }
1213 }
1214 
1215 void
1216 CollectionTreeItemModelBase::slotCollapsed( const QModelIndex &index )
1217 {
1218     if ( index.isValid() )      //probably unnecessary, but let's be safe
1219     {
1220         CollectionTreeItem *item = static_cast<CollectionTreeItem*>( index.internalPointer() );
1221 
1222         switch( item->type() )
1223         {
1224         case CollectionTreeItem::Root:
1225             break; // nothing to do
1226 
1227         case CollectionTreeItem::Collection:
1228             m_expandedCollections.remove( item->parentCollection() );
1229             break;
1230 
1231         case CollectionTreeItem::VariousArtist:
1232         case CollectionTreeItem::NoLabel:
1233             m_expandedSpecialNodes.remove( item->parentCollection() );
1234             break;
1235         case CollectionTreeItem::Data:
1236             m_expandedItems.remove( item->data() );
1237             break;
1238         }
1239     }
1240 }
1241 
1242 void
1243 CollectionTreeItemModelBase::slotExpanded( const QModelIndex &index )
1244 {
1245     if( !index.isValid() )
1246         return;
1247 
1248     CollectionTreeItem *item = static_cast<CollectionTreeItem*>( index.internalPointer() );
1249     // we are really only interested in the special nodes here.
1250     // we have to remember whether the user expanded a various artists/no labels node or not.
1251     // otherwise we won't be able to automatically expand the special node after filtering again
1252     // there is exactly one special node per type per collection, so use the collection to store that information
1253 
1254     // we also need to store collection expansion state here as they are no longer
1255     // added to th expanded set in handleNormalQueryResult()
1256     switch( item->type() )
1257     {
1258         case CollectionTreeItem::VariousArtist:
1259         case CollectionTreeItem::NoLabel:
1260             m_expandedSpecialNodes.insert( item->parentCollection() );
1261             break;
1262         case CollectionTreeItem::Collection:
1263             m_expandedCollections.insert( item->parentCollection() );
1264         default:
1265             break;
1266     }
1267 }
1268 
1269 void CollectionTreeItemModelBase::markSubTreeAsDirty( CollectionTreeItem *item )
1270 {
1271     //tracks are the leaves so they are never dirty
1272     if( !item->isTrackItem() )
1273         item->setRequiresUpdate( true );
1274     for( int i = 0; i < item->childCount(); i++ )
1275     {
1276         markSubTreeAsDirty( item->child( i ) );
1277     }
1278 }
1279 
1280 void CollectionTreeItemModelBase::itemAboutToBeDeleted( CollectionTreeItem *item )
1281 {
1282     // also all the children will be deleted
1283     foreach( CollectionTreeItem *child, item->children() )
1284         itemAboutToBeDeleted( child );
1285 
1286     if( !m_runningQueries.contains( item ) )
1287         return;
1288     // TODO: replace this hack with QWeakPointer now than we depend on Qt >= 4.8
1289     foreach(Collections::QueryMaker *qm, m_runningQueries.values( item ))
1290     {
1291         m_childQueries.remove( qm );
1292         m_compilationQueries.remove( qm );
1293         m_noLabelsQueries.remove( qm );
1294         m_runningQueries.remove(item, qm);
1295 
1296         //Disconnect all signals from the QueryMaker so we do not get notified about the results
1297         qm->disconnect();
1298         qm->abortQuery();
1299         //Nuke it
1300         qm->deleteLater();
1301     }
1302 }
1303 
1304 void
1305 CollectionTreeItemModelBase::setDragSourceCollections( const QSet<Collections::Collection*> &collections )
1306 {
1307     m_dragSourceCollections = collections;
1308 }
1309 
1310 bool
1311 CollectionTreeItemModelBase::hasRunningQueries() const
1312 {
1313     return !m_runningQueries.isEmpty();
1314 }
1315 
1316 CategoryId::CatMenuId
1317 CollectionTreeItemModelBase::levelCategory( const int level ) const
1318 {
1319     const int actualLevel = level + levelModifier();
1320     if( actualLevel >= 0 && actualLevel < m_levelType.count() )
1321         return m_levelType.at( actualLevel );
1322 
1323     return CategoryId::None;
1324 }
1325