1 /****************************************************************************************
2  * Copyright (c) 2007 Alexandre Pereira de Oliveira <aleprj@gmail.com>                  *
3  * Copyright (c) 2007-2009 Maximilian Kossick <maximilian.kossick@googlemail.com>       *
4  * Copyright (c) 2007 Nikolaj Hald Nielsen <nhn@kde.org>                                *
5  *                                                                                      *
6  * This program is free software; you can redistribute it and/or modify it under        *
7  * the terms of the GNU General Public License as published by the Free Software        *
8  * Foundation; either version 2 of the License, or (at your option) any later           *
9  * version.                                                                             *
10  *                                                                                      *
11  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
12  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
13  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
14  *                                                                                      *
15  * You should have received a copy of the GNU General Public License along with         *
16  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
17  ****************************************************************************************/
18 
19 #define DEBUG_PREFIX "CollectionTreeItemModelBase"
20 
21 #include "CollectionTreeItemModelBase.h"
22 
23 #include "AmarokMimeData.h"
24 #include "FileType.h"
25 #include "SvgHandler.h"
26 #include "amarokconfig.h"
27 #include "browsers/CollectionTreeItem.h"
28 #include "core/collections/Collection.h"
29 #include "core/collections/QueryMaker.h"
30 #include "core/meta/TrackEditor.h"
31 #include "core/meta/support/MetaConstants.h"
32 #include "core/support/Amarok.h"
33 #include "core/support/Debug.h"
34 #include "core-impl/collections/support/TextualQueryFilter.h"
35 #include "widgets/PrettyTreeRoles.h"
36 
37 #include <KLocalizedString>
38 #include <ThreadWeaver/Lambda>
39 #include <ThreadWeaver/Queue>
40 
41 #include <QApplication>
42 #include <QIcon>
43 #include <QPixmap>
44 #include <QPointer>
45 #include <QStandardPaths>
46 #include <QStyle>
47 #include <QTimeLine>
48 #include <QTimer>
49 
50 #include <functional>
51 
52 
53 using namespace Meta;
54 
55 
56 class TrackLoaderJob : public ThreadWeaver::Job
57 {
58 public:
TrackLoaderJob(const QModelIndex & index,const Meta::AlbumPtr & album,CollectionTreeItemModelBase * model)59     TrackLoaderJob( const QModelIndex &index, const Meta::AlbumPtr &album, CollectionTreeItemModelBase *model )
60         : m_index( index )
61         , m_album( album )
62         , m_model( model )
63         , m_abortRequested( false )
64     {
65         if( !m_model || !m_album || !m_index.isValid() )
66             requestAbort();
67     }
68 
requestAbort()69     void requestAbort() override
70     {
71         m_abortRequested = true;
72     }
73 
74 protected:
run(ThreadWeaver::JobPointer self,ThreadWeaver::Thread * thread)75     void run( ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread ) override
76     {
77         Q_UNUSED( self )
78         Q_UNUSED( thread )
79 
80         if( m_abortRequested || !m_model )
81             return;
82 
83         const auto tracks = m_album->tracks();
84 
85         if( m_model && !m_abortRequested )
86         {
87             auto slot = std::bind( &CollectionTreeItemModelBase::tracksLoaded, m_model, m_album, m_index, tracks );
88             QTimer::singleShot( 0, m_model, slot );
89         }
90     }
91 
92 private:
93     QModelIndex m_index;
94     Meta::AlbumPtr m_album;
95     QPointer<CollectionTreeItemModelBase> m_model;
96     bool m_abortRequested;
97 };
98 
qHash(const Meta::DataPtr & data)99 inline uint qHash( const Meta::DataPtr &data )
100 {
101     return qHash( data.data() );
102 }
103 
104 /**
105  * This set determines which collection browser levels should have shown Various Artists
106  * item under them. AlbumArtist is certain, (Track)Artist is questionable.
107  */
108 static const QSet<CategoryId::CatMenuId> variousArtistCategories =
109         QSet<CategoryId::CatMenuId>() << CategoryId::AlbumArtist;
110 
CollectionTreeItemModelBase()111 CollectionTreeItemModelBase::CollectionTreeItemModelBase( )
112     : QAbstractItemModel()
113     , m_rootItem( 0 )
114     , m_animFrame( 0 )
115     , m_loading1( QPixmap( QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("amarok/images/loading1.png") ) ) )
116     , m_loading2( QPixmap( QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("amarok/images/loading2.png") ) ) )
117     , m_currentAnimPixmap( m_loading1 )
118     , m_autoExpand( false )
119 {
120     m_timeLine = new QTimeLine( 10000, this );
121     m_timeLine->setFrameRange( 0, 20 );
122     m_timeLine->setLoopCount ( 0 );
123     connect( m_timeLine, &QTimeLine::frameChanged, this, &CollectionTreeItemModelBase::loadingAnimationTick );
124 }
125 
~CollectionTreeItemModelBase()126 CollectionTreeItemModelBase::~CollectionTreeItemModelBase()
127 {
128     KConfigGroup config = Amarok::config( QStringLiteral("Collection Browser") );
129     QList<int> levelNumbers;
130     foreach( CategoryId::CatMenuId category, levels() )
131         levelNumbers.append( category );
132     config.writeEntry( "TreeCategory", levelNumbers );
133 
134     if( m_rootItem )
135         m_rootItem->deleteLater();
136 }
137 
flags(const QModelIndex & index) const138 Qt::ItemFlags CollectionTreeItemModelBase::flags(const QModelIndex & index) const
139 {
140     Qt::ItemFlags flags = 0;
141     if( index.isValid() )
142     {
143         flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEditable;
144     }
145     return flags;
146 }
147 
148 bool
setData(const QModelIndex & index,const QVariant & value,int role)149 CollectionTreeItemModelBase::setData( const QModelIndex &index, const QVariant &value, int role )
150 {
151     Q_UNUSED( role )
152 
153     if( !index.isValid() )
154         return false;
155     CollectionTreeItem *item = static_cast<CollectionTreeItem*>( index.internalPointer() );
156 
157     Meta::DataPtr data = item->data();
158 
159     if( Meta::TrackPtr track = Meta::TrackPtr::dynamicCast( data ) )
160     {
161         Meta::TrackEditorPtr ec = track->editor();
162         if( ec )
163         {
164             ec->setTitle( value.toString() );
165             Q_EMIT dataChanged( index, index );
166             return true;
167         }
168     }
169     else if( Meta::AlbumPtr album = Meta::AlbumPtr::dynamicCast( data ) )
170     {
171         Meta::TrackList tracks = album->tracks();
172         if( !tracks.isEmpty() )
173         {
174             foreach( Meta::TrackPtr track, tracks )
175             {
176                 Meta::TrackEditorPtr ec = track->editor();
177                 if( ec )
178                     ec->setAlbum( value.toString() );
179             }
180             Q_EMIT dataChanged( index, index );
181             return true;
182         }
183     }
184     else if( Meta::ArtistPtr artist = Meta::ArtistPtr::dynamicCast( data ) )
185     {
186         Meta::TrackList tracks = artist->tracks();
187         if( !tracks.isEmpty() )
188         {
189             foreach( Meta::TrackPtr track, tracks )
190             {
191                 Meta::TrackEditorPtr ec = track->editor();
192                 if( ec )
193                     ec->setArtist( value.toString() );
194             }
195             Q_EMIT dataChanged( index, index );
196             return true;
197         }
198     }
199     else if( Meta::GenrePtr genre = Meta::GenrePtr::dynamicCast( data ) )
200     {
201         Meta::TrackList tracks = genre->tracks();
202         if( !tracks.isEmpty() )
203         {
204             foreach( Meta::TrackPtr track, tracks )
205             {
206                 Meta::TrackEditorPtr ec = track->editor();
207                 if( ec )
208                     ec->setGenre( value.toString() );
209             }
210             Q_EMIT dataChanged( index, index );
211             return true;
212         }
213     }
214     else if( Meta::YearPtr year = Meta::YearPtr::dynamicCast( data ) )
215     {
216         Meta::TrackList tracks = year->tracks();
217         if( !tracks.isEmpty() )
218         {
219             foreach( Meta::TrackPtr track, tracks )
220             {
221                 Meta::TrackEditorPtr ec = track->editor();
222                 if( ec )
223                     ec->setYear( value.toInt() );
224             }
225             Q_EMIT dataChanged( index, index );
226             return true;
227         }
228     }
229     else if( Meta::ComposerPtr composer = Meta::ComposerPtr::dynamicCast( data ) )
230     {
231         Meta::TrackList tracks = composer->tracks();
232         if( !tracks.isEmpty() )
233         {
234             foreach( Meta::TrackPtr track, tracks )
235             {
236                 Meta::TrackEditorPtr ec = track->editor();
237                 if( ec )
238                     ec->setComposer( value.toString() );
239             }
240             Q_EMIT dataChanged( index, index );
241             return true;
242         }
243     }
244     return false;
245 }
246 
247 QVariant
dataForItem(CollectionTreeItem * item,int role,int level) const248 CollectionTreeItemModelBase::dataForItem( CollectionTreeItem *item, int role, int level ) const
249 {
250     if( level == -1 )
251         level = item->level();
252 
253     if( item->isTrackItem() )
254     {
255         Meta::TrackPtr track = Meta::TrackPtr::dynamicCast( item->data() );
256         switch( role )
257         {
258         case Qt::DisplayRole:
259         case Qt::ToolTipRole:
260         case PrettyTreeRoles::FilterRole:
261             {
262                 QString name = track->prettyName();
263                 Meta::AlbumPtr album = track->album();
264                 Meta::ArtistPtr artist = track->artist();
265 
266                 if( album && artist && album->isCompilation() )
267                     name.prepend( QStringLiteral("%1 - ").arg(artist->prettyName()) );
268 
269                 if( AmarokConfig::showTrackNumbers() )
270                 {
271                     int trackNum = track->trackNumber();
272                     if( trackNum > 0 )
273                         name.prepend( QStringLiteral("%1 - ").arg(trackNum) );
274                 }
275 
276                 // Check empty after track logic and before album logic
277                 if( name.isEmpty() )
278                     name = i18nc( "The Name is not known", "Unknown" );
279                 return name;
280             }
281 
282         case Qt::DecorationRole:
283             return QIcon::fromTheme( QStringLiteral("media-album-track") );
284         case PrettyTreeRoles::SortRole:
285             return track->sortableName();
286         }
287     }
288     else if( item->isAlbumItem() )
289     {
290         Meta::AlbumPtr album = Meta::AlbumPtr::dynamicCast( item->data() );
291         switch( role )
292         {
293         case Qt::DisplayRole:
294         case Qt::ToolTipRole:
295             {
296                 QString name = album->prettyName();
297                 // add years for named albums (if enabled)
298                 if( AmarokConfig::showYears() )
299                 {
300                     if( m_years.contains( album.data() ) )
301                     {
302                         int year = m_years.value( album.data() );
303 
304                         if( year > 0 )
305                             name.prepend( QStringLiteral("%1 - ").arg( year ) );
306                     }
307                     else if( !album->name().isEmpty() )
308                     {
309                         if( !m_loadingAlbums.contains( album ) )
310                         {
311                             m_loadingAlbums.insert( album );
312 
313                             auto nonConstThis = const_cast<CollectionTreeItemModelBase*>( this );
314                             auto job = QSharedPointer<TrackLoaderJob>::create( itemIndex( item ), album, nonConstThis );
315                             ThreadWeaver::Queue::instance()->enqueue( job );
316                         }
317                     }
318                 }
319                 return name;
320             }
321 
322         case Qt::DecorationRole:
323             if( AmarokConfig::showAlbumArt() )
324             {
325                 QStyle *style = QApplication::style();
326                 const int largeIconSize = style->pixelMetric( QStyle::PM_LargeIconSize );
327 
328                 return The::svgHandler()->imageWithBorder( album, largeIconSize, 2 );
329             }
330             else
331                 return iconForLevel( level );
332 
333         case PrettyTreeRoles::SortRole:
334             return album->sortableName();
335 
336         case PrettyTreeRoles::HasCoverRole:
337             return AmarokConfig::showAlbumArt();
338 
339         case PrettyTreeRoles::YearRole:
340             {
341                 if( m_years.contains( album.data() ) )
342                     return m_years.value( album.data() );
343 
344                 else if( !album->name().isEmpty() )
345                 {
346                     if( !m_loadingAlbums.contains( album ) )
347                     {
348                         m_loadingAlbums.insert( album );
349 
350                         auto nonConstThis = const_cast<CollectionTreeItemModelBase*>( this );
351                         auto job = QSharedPointer<TrackLoaderJob>::create( itemIndex( item ), album, nonConstThis );
352                         ThreadWeaver::Queue::instance()->enqueue( job );
353                     }
354                 }
355                 return -1;
356             }
357         }
358     }
359     else if( item->isDataItem() )
360     {
361         switch( role )
362         {
363         case Qt::DisplayRole:
364         case Qt::ToolTipRole:
365         case PrettyTreeRoles::FilterRole:
366             {
367                 QString name = item->data()->prettyName();
368                 if( name.isEmpty() )
369                     name = i18nc( "The Name is not known", "Unknown" );
370                 return name;
371             }
372 
373         case Qt::DecorationRole:
374             {
375                 if( m_childQueries.values().contains( item ) )
376                 {
377                     if( level < m_levelType.count() )
378                         return m_currentAnimPixmap;
379                 }
380                 return iconForLevel( level );
381             }
382 
383         case PrettyTreeRoles::SortRole:
384             return item->data()->sortableName();
385         }
386     }
387     else if( item->isVariousArtistItem() )
388     {
389         switch( role )
390         {
391         case Qt::DecorationRole:
392             return QIcon::fromTheme( QStringLiteral("similarartists-amarok") );
393         case Qt::DisplayRole:
394             return i18n( "Various Artists" );
395         case PrettyTreeRoles::SortRole:
396             return QString(); // so that we can compare it against other strings
397         }
398     }
399 
400     // -- all other roles are handled by item
401     return item->data( role );
402 }
403 
404 QVariant
headerData(int section,Qt::Orientation orientation,int role) const405 CollectionTreeItemModelBase::headerData(int section, Qt::Orientation orientation, int role) const
406 {
407     if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
408     {
409         if (section == 0)
410             return m_headerText;
411     }
412     return QVariant();
413 }
414 
415 QModelIndex
index(int row,int column,const QModelIndex & parent) const416 CollectionTreeItemModelBase::index(int row, int column, const QModelIndex & parent) const
417 {
418     //ensure sanity of parameters
419     //we are a tree model, there are no columns
420     if( row < 0 || column != 0 )
421         return QModelIndex();
422 
423     CollectionTreeItem *parentItem;
424 
425     if (!parent.isValid())
426         parentItem = m_rootItem;
427     else
428         parentItem = static_cast<CollectionTreeItem*>(parent.internalPointer());
429 
430     CollectionTreeItem *childItem = parentItem->child(row);
431     if( childItem )
432         return createIndex(row, column, childItem);
433     else
434         return QModelIndex();
435 }
436 
437 QModelIndex
parent(const QModelIndex & index) const438 CollectionTreeItemModelBase::parent(const QModelIndex & index) const
439 {
440      if( !index.isValid() )
441          return QModelIndex();
442 
443      CollectionTreeItem *childItem = static_cast<CollectionTreeItem*>(index.internalPointer());
444      CollectionTreeItem *parentItem = childItem->parent();
445 
446      return itemIndex( parentItem );
447 }
448 
449 int
rowCount(const QModelIndex & parent) const450 CollectionTreeItemModelBase::rowCount(const QModelIndex & parent) const
451 {
452     CollectionTreeItem *parentItem;
453 
454     if( !parent.isValid() )
455         parentItem = m_rootItem;
456     else
457         parentItem = static_cast<CollectionTreeItem*>(parent.internalPointer());
458 
459     return parentItem->childCount();
460 }
461 
columnCount(const QModelIndex & parent) const462 int CollectionTreeItemModelBase::columnCount(const QModelIndex & parent) const
463 {
464     Q_UNUSED( parent )
465     return 1;
466 }
467 
468 QStringList
mimeTypes() const469 CollectionTreeItemModelBase::mimeTypes() const
470 {
471     QStringList types;
472     types << AmarokMimeData::TRACK_MIME;
473     return types;
474 }
475 
476 QMimeData*
mimeData(const QModelIndexList & indices) const477 CollectionTreeItemModelBase::mimeData( const QModelIndexList &indices ) const
478 {
479     if ( indices.isEmpty() )
480         return 0;
481 
482     // first, filter out duplicate entries that may arise when both parent and child are selected
483     QSet<QModelIndex> indexSet = QSet<QModelIndex>::fromList( indices );
484     QMutableSetIterator<QModelIndex> it( indexSet );
485     while( it.hasNext() )
486     {
487         it.next();
488         // we go up in parent hierarchy searching whether some parent indices are already in set
489         QModelIndex parentIndex = it.value();
490         while( parentIndex.isValid() )  // leave the root (top, invalid) index intact
491         {
492             parentIndex = parentIndex.parent();  // yes, we start from the parent of current index
493             if( indexSet.contains( parentIndex ) )
494             {
495                 it.remove(); // parent already in selected set, remove child
496                 break; // no need to continue inner loop, already deleted
497             }
498         }
499     }
500 
501     QList<CollectionTreeItem*> items;
502     foreach( const QModelIndex &index, indexSet )
503     {
504         if( index.isValid() )
505             items << static_cast<CollectionTreeItem*>( index.internalPointer() );
506     }
507 
508     return mimeData( items );
509 }
510 
511 QMimeData*
mimeData(const QList<CollectionTreeItem * > & items) const512 CollectionTreeItemModelBase::mimeData( const QList<CollectionTreeItem*> &items ) const
513 {
514     if ( items.isEmpty() )
515         return 0;
516 
517     Meta::TrackList tracks;
518     QList<Collections::QueryMaker*> queries;
519 
520     foreach( CollectionTreeItem *item, items )
521     {
522         if( item->allDescendentTracksLoaded() ) {
523             tracks << item->descendentTracks();
524         }
525         else
526         {
527             Collections::QueryMaker *qm = item->queryMaker();
528             for( CollectionTreeItem *tmp = item; tmp; tmp = tmp->parent() )
529                 tmp->addMatch( qm, levelCategory( tmp->level() - 1 ) );
530             Collections::addTextualFilter( qm, m_currentFilter );
531             queries.append( qm );
532         }
533     }
534 
535     qStableSort( tracks.begin(), tracks.end(), Meta::Track::lessThan );
536 
537     AmarokMimeData *mimeData = new AmarokMimeData();
538     mimeData->setTracks( tracks );
539     mimeData->setQueryMakers( queries );
540     mimeData->startQueries();
541     return mimeData;
542 }
543 
544 bool
hasChildren(const QModelIndex & parent) const545 CollectionTreeItemModelBase::hasChildren ( const QModelIndex & parent ) const
546 {
547      if( !parent.isValid() )
548          return true; // must be root item!
549 
550     CollectionTreeItem *item = static_cast<CollectionTreeItem*>(parent.internalPointer());
551     //we added the collection level so we have to be careful with the item level
552     return !item->isDataItem() || item->level() + levelModifier() <= m_levelType.count();
553 
554 }
555 
556 void
ensureChildrenLoaded(CollectionTreeItem * item)557 CollectionTreeItemModelBase::ensureChildrenLoaded( CollectionTreeItem *item )
558 {
559     //only start a query if necessary and we are not querying for the item's children already
560     if ( item->requiresUpdate() && !m_runningQueries.contains( item ) )
561     {
562         listForLevel( item->level() + levelModifier(), item->queryMaker(), item );
563     }
564 }
565 
566 CollectionTreeItem *
treeItem(const QModelIndex & index) const567 CollectionTreeItemModelBase::treeItem( const QModelIndex &index ) const
568 {
569     if( !index.isValid() || index.model() != this )
570         return 0;
571 
572     return static_cast<CollectionTreeItem *>( index.internalPointer() );
573 }
574 
575 QModelIndex
itemIndex(CollectionTreeItem * item) const576 CollectionTreeItemModelBase::itemIndex( CollectionTreeItem *item ) const
577 {
578     if( !item || item == m_rootItem )
579         return QModelIndex();
580 
581     return createIndex( item->row(), 0, item );
582 }
583 
584 void
listForLevel(int level,Collections::QueryMaker * qm,CollectionTreeItem * parent)585 CollectionTreeItemModelBase::listForLevel(int level, Collections::QueryMaker * qm, CollectionTreeItem * parent)
586 {
587     if( qm && parent )
588     {
589         // this check should not hurt anyone... needs to check if single... needs it
590         if( m_runningQueries.contains( parent ) )
591             return;
592 
593         // following special cases are handled extra - right when the parent is added
594         if( level > m_levelType.count() ||
595             parent->isVariousArtistItem() ||
596             parent->isNoLabelItem() )
597         {
598             qm->deleteLater();
599             return;
600         }
601 
602         // - the last level are always the tracks
603         if ( level == m_levelType.count() )
604             qm->setQueryType( Collections::QueryMaker::Track );
605 
606         // - all other levels are more complicate
607         else
608         {
609             Collections::QueryMaker::QueryType nextLevel;
610             if( level + 1 >= m_levelType.count() )
611                 nextLevel = Collections::QueryMaker::Track;
612             else
613                 nextLevel = mapCategoryToQueryType( m_levelType.value( level + 1 ) );
614 
615             qm->setQueryType( mapCategoryToQueryType( m_levelType.value( level ) ) );
616 
617             CategoryId::CatMenuId category = m_levelType.value( level );
618             if( category == CategoryId::Album )
619             {
620                 // restrict query to normal albums if the previous level
621                 // was the AlbumArtist category. In that case we handle compilations below
622                 if( levelCategory( level - 1 ) == CategoryId::AlbumArtist )
623                     qm->setAlbumQueryMode( Collections::QueryMaker::OnlyNormalAlbums );
624             }
625             else if( variousArtistCategories.contains( category ) )
626                 // we used to handleCompilations() only if nextLevel is Album, but I cannot
627                 // tell any reason why we should have done this --- strohel
628                 handleCompilations( nextLevel, parent );
629             else if( category == CategoryId::Label )
630                 handleTracksWithoutLabels( nextLevel, parent );
631         }
632 
633         for( CollectionTreeItem *tmp = parent; tmp; tmp = tmp->parent() )
634             tmp->addMatch( qm, levelCategory( tmp->level() - 1 ) );
635         Collections::addTextualFilter( qm, m_currentFilter );
636         addQueryMaker( parent, qm );
637         m_childQueries.insert( qm, parent );
638         qm->run();
639 
640         //some very quick queries may be done so fast that the loading
641         //animation creates an unnecessary flicker, therefore delay it for a bit
642         QTimer::singleShot( 150, this, &CollectionTreeItemModelBase::startAnimationTick );
643     }
644 }
645 
646 void
setLevels(const QList<CategoryId::CatMenuId> & levelType)647 CollectionTreeItemModelBase::setLevels( const QList<CategoryId::CatMenuId> &levelType )
648 {
649     if( m_levelType == levelType )
650         return;
651 
652     m_levelType = levelType;
653     updateHeaderText();
654     m_expandedItems.clear();
655     m_expandedSpecialNodes.clear();
656     m_runningQueries.clear();
657     m_childQueries.clear();
658     m_compilationQueries.clear();
659     filterChildren();
660 }
661 
662 Collections::QueryMaker::QueryType
mapCategoryToQueryType(int levelType) const663 CollectionTreeItemModelBase::mapCategoryToQueryType( int levelType ) const
664 {
665     Collections::QueryMaker::QueryType type;
666     switch( levelType )
667     {
668     case CategoryId::Album:
669         type = Collections::QueryMaker::Album;
670         break;
671     case CategoryId::Artist:
672         type = Collections::QueryMaker::Artist;
673         break;
674     case CategoryId::AlbumArtist:
675         type = Collections::QueryMaker::AlbumArtist;
676         break;
677     case CategoryId::Composer:
678         type = Collections::QueryMaker::Composer;
679         break;
680     case CategoryId::Genre:
681         type = Collections::QueryMaker::Genre;
682         break;
683     case CategoryId::Label:
684         type = Collections::QueryMaker::Label;
685         break;
686     case CategoryId::Year:
687         type = Collections::QueryMaker::Year;
688         break;
689     default:
690         type = Collections::QueryMaker::None;
691         break;
692     }
693 
694     return type;
695 }
696 
697 void
tracksLoaded(const Meta::AlbumPtr & album,const QModelIndex & index,const Meta::TrackList & tracks)698 CollectionTreeItemModelBase::tracksLoaded( const Meta::AlbumPtr &album, const QModelIndex &index, const Meta::TrackList& tracks )
699 {
700     DEBUG_BLOCK
701 
702     if( !album )
703         return;
704 
705     m_loadingAlbums.remove( album );
706 
707     if( !index.isValid() )
708         return;
709 
710     int year = 0;
711 
712     if( !tracks.isEmpty() )
713     {
714         Meta::YearPtr yearPtr = tracks.first()->year();
715         if( yearPtr )
716             year = yearPtr->year();
717 
718         debug() << "Valid album year found:" << year;
719     }
720 
721     if( !m_years.contains( album.data() ) || m_years.value( album.data() ) != year )
722     {
723         m_years[ album.data() ] = year;
724         Q_EMIT dataChanged( index, index );
725     }
726 }
727 
728 void
addQueryMaker(CollectionTreeItem * item,Collections::QueryMaker * qm) const729 CollectionTreeItemModelBase::addQueryMaker( CollectionTreeItem* item,
730                                             Collections::QueryMaker *qm ) const
731 {
732     connect( qm, &Collections::QueryMaker::newTracksReady, this, &CollectionTreeItemModelBase::newTracksReady, Qt::QueuedConnection );
733     connect( qm, &Collections::QueryMaker::newArtistsReady, this, &CollectionTreeItemModelBase::newArtistsReady, Qt::QueuedConnection );
734     connect( qm, &Collections::QueryMaker::newAlbumsReady, this, &CollectionTreeItemModelBase::newAlbumsReady, Qt::QueuedConnection );
735     connect( qm, &Collections::QueryMaker::newGenresReady, this, &CollectionTreeItemModelBase::newGenresReady, Qt::QueuedConnection );
736     connect( qm, &Collections::QueryMaker::newComposersReady, this, &CollectionTreeItemModelBase::newComposersReady, Qt::QueuedConnection );
737     connect( qm, &Collections::QueryMaker::newYearsReady, this, &CollectionTreeItemModelBase::newYearsReady, Qt::QueuedConnection );
738     connect( qm, &Collections::QueryMaker::newLabelsReady, this, &CollectionTreeItemModelBase::newLabelsReady, Qt::QueuedConnection );
739     connect( qm, &Collections::QueryMaker::newDataReady, this, &CollectionTreeItemModelBase::newDataReady, Qt::QueuedConnection );
740     connect( qm, &Collections::QueryMaker::queryDone, this, &CollectionTreeItemModelBase::queryDone, Qt::QueuedConnection );
741     m_runningQueries.insert( item, qm );
742 }
743 
744 void
queryDone()745 CollectionTreeItemModelBase::queryDone()
746 {
747     Collections::QueryMaker *qm = qobject_cast<Collections::QueryMaker*>( sender() );
748     if( !qm )
749         return;
750 
751     CollectionTreeItem* item = 0;
752     if( m_childQueries.contains( qm ) )
753         item = m_childQueries.take( qm );
754     else if( m_compilationQueries.contains( qm ) )
755         item = m_compilationQueries.take( qm );
756     else if( m_noLabelsQueries.contains( qm ) )
757         item = m_noLabelsQueries.take( qm );
758 
759     if( item )
760         m_runningQueries.remove( item, qm );
761 
762     //reset icon for this item
763     if( item && item != m_rootItem )
764     {
765         Q_EMIT dataChanged( itemIndex( item ), itemIndex( item ) );
766     }
767 
768     //stop timer if there are no more animations active
769     if( m_runningQueries.isEmpty() )
770     {
771         Q_EMIT allQueriesFinished( m_autoExpand );
772         m_autoExpand = false; // reset to default value
773         m_timeLine->stop();
774     }
775     qm->deleteLater();
776 }
777 
778 // TODO
779 
780 /** Small helper function to convert a list of e.g. tracks to a list of DataPtr */
781 template<class PointerType>
782 static Meta::DataList
convertToDataList(const QList<PointerType> & list)783 convertToDataList( const QList<PointerType>& list )
784 {
785     Meta::DataList data;
786     for( const auto &p : list )
787         data << Meta::DataPtr::staticCast( p );
788 
789     return data;
790 }
791 
792 void
newTracksReady(const Meta::TrackList & res)793 CollectionTreeItemModelBase::newTracksReady( const Meta::TrackList &res )
794 {
795     newDataReady( convertToDataList( res ) );
796 }
797 
798 void
newArtistsReady(const Meta::ArtistList & res)799 CollectionTreeItemModelBase::newArtistsReady( const Meta::ArtistList &res )
800 {
801     newDataReady( convertToDataList( res ) );
802 }
803 
804 void
newAlbumsReady(const Meta::AlbumList & res)805 CollectionTreeItemModelBase::newAlbumsReady( const Meta::AlbumList &res )
806 {
807     newDataReady( convertToDataList( res ) );
808 }
809 
810 void
newGenresReady(const Meta::GenreList & res)811 CollectionTreeItemModelBase::newGenresReady( const Meta::GenreList &res )
812 {
813     newDataReady( convertToDataList( res ) );
814 }
815 
816 void
newComposersReady(const Meta::ComposerList & res)817 CollectionTreeItemModelBase::newComposersReady( const Meta::ComposerList &res )
818 {
819     newDataReady( convertToDataList( res ) );
820 }
821 
822 void
newYearsReady(const Meta::YearList & res)823 CollectionTreeItemModelBase::newYearsReady( const Meta::YearList &res )
824 {
825     newDataReady( convertToDataList( res ) );
826 }
827 
828 void
newLabelsReady(const Meta::LabelList & res)829 CollectionTreeItemModelBase::newLabelsReady( const Meta::LabelList &res )
830 {
831     newDataReady( convertToDataList( res ) );
832 }
833 
834 void
newDataReady(const Meta::DataList & data)835 CollectionTreeItemModelBase::newDataReady( const Meta::DataList &data )
836 {
837     //if we are expanding an item, we'll find the sender in childQueries
838     //otherwise we are filtering all collections
839     Collections::QueryMaker *qm = qobject_cast<Collections::QueryMaker*>( sender() );
840     if( !qm )
841         return;
842 
843     if( m_childQueries.contains( qm ) )
844         handleNormalQueryResult( qm, data );
845 
846     else if( m_compilationQueries.contains( qm ) )
847         handleSpecialQueryResult( CollectionTreeItem::VariousArtist, qm, data );
848 
849     else if( m_noLabelsQueries.contains( qm ) )
850         handleSpecialQueryResult( CollectionTreeItem::NoLabel, qm, data );
851 }
852 
853 void
handleSpecialQueryResult(CollectionTreeItem::Type type,Collections::QueryMaker * qm,const Meta::DataList & dataList)854 CollectionTreeItemModelBase::handleSpecialQueryResult( CollectionTreeItem::Type type, Collections::QueryMaker *qm, const Meta::DataList &dataList )
855 {
856     CollectionTreeItem *parent = nullptr;
857 
858     if( type == CollectionTreeItem::VariousArtist )
859         parent = m_compilationQueries.value( qm );
860 
861     else if( type == CollectionTreeItem::NoLabel )
862         parent = m_noLabelsQueries.value( qm );
863 
864     if( parent )
865     {
866         QModelIndex parentIndex = itemIndex( parent );
867 
868         //if the special query did not return a result we have to remove the
869         //the special node itself
870         if( dataList.isEmpty() )
871         {
872             for( int i = 0; i < parent->childCount(); i++ )
873             {
874                 CollectionTreeItem *cti = parent->child( i );
875                 if( cti->type() == type )
876                 {
877                     //found the special node
878                     beginRemoveRows( parentIndex, cti->row(), cti->row() );
879                     cti = 0; //will be deleted;
880                     parent->removeChild( i );
881                     endRemoveRows();
882                     break;
883                 }
884             }
885             //we have removed the special node if it existed
886             return;
887         }
888 
889         CollectionTreeItem *specialNode = 0;
890         if( parent->childCount() == 0 )
891         {
892             //we only insert the special node
893             beginInsertRows( parentIndex, 0, 0 );
894             specialNode = new CollectionTreeItem( type, dataList, parent, this );
895             //set requiresUpdate, otherwise we will query for the children of specialNode again!
896             specialNode->setRequiresUpdate( false );
897             endInsertRows();
898         }
899         else
900         {
901             for( int i = 0; i < parent->childCount(); i++ )
902             {
903                 CollectionTreeItem *cti = parent->child( i );
904                 if( cti->type() == type )
905                 {
906                     //found the special node
907                     specialNode = cti;
908                     break;
909                 }
910             }
911             if( !specialNode )
912             {
913                 //we only insert the special node
914                 beginInsertRows( parentIndex, 0, 0 );
915                 specialNode = new CollectionTreeItem( type, dataList, parent, this );
916                 //set requiresUpdate, otherwise we will query for the children of specialNode again!
917                 specialNode->setRequiresUpdate( false );
918                 endInsertRows();
919             }
920             else
921             {
922                 //only call populateChildren for the special node if we have not
923                 //created it in this method call. The special node ctor takes care
924                 //of that itself
925                 populateChildren( dataList, specialNode, itemIndex( specialNode ) );
926             }
927             //populate children will call setRequiresUpdate on vaNode
928             //but as the special query is based on specialNode's parent,
929             //we have to call setRequiresUpdate on the parent too
930             //yes, this will mean we will call setRequiresUpdate twice
931             parent->setRequiresUpdate( false );
932 
933             for( int count = specialNode->childCount(), i = 0; i < count; ++i )
934             {
935                 CollectionTreeItem *item = specialNode->child( i );
936                 if ( m_expandedItems.contains( item->data() ) ) //item will always be a data item
937                 {
938                     listForLevel( item->level() + levelModifier(), item->queryMaker(), item );
939                 }
940             }
941         }
942 
943         //if the special node exists, check if it has to be expanded
944         if( specialNode )
945         {
946             if( m_expandedSpecialNodes.contains( parent->parentCollection() ) )
947             {
948                 Q_EMIT expandIndex( createIndex( 0, 0, specialNode ) ); //we have just inserted the vaItem at row 0
949             }
950         }
951     }
952 }
953 
954 void
handleNormalQueryResult(Collections::QueryMaker * qm,const Meta::DataList & dataList)955 CollectionTreeItemModelBase::handleNormalQueryResult( Collections::QueryMaker *qm, const Meta::DataList &dataList )
956 {
957     CollectionTreeItem *parent = m_childQueries.value( qm );
958     if( parent ) {
959         QModelIndex parentIndex = itemIndex( parent );
960         populateChildren( dataList, parent, parentIndex );
961 
962         if ( parent->isDataItem() )
963         {
964             if ( m_expandedItems.contains( parent->data() ) )
965                 Q_EMIT expandIndex( parentIndex );
966             else
967                 //simply insert the item, nothing will change if it is already in the set
968                 m_expandedItems.insert( parent->data() );
969         }
970     }
971 }
972 
973 void
populateChildren(const DataList & dataList,CollectionTreeItem * parent,const QModelIndex & parentIndex)974 CollectionTreeItemModelBase::populateChildren( const DataList &dataList, CollectionTreeItem *parent, const QModelIndex &parentIndex )
975 {
976     CategoryId::CatMenuId childCategory = levelCategory( parent->level() );
977 
978     // add new rows after existing ones here (which means all artists nodes
979     // will be inserted after the "Various Artists" node)
980     // figure out which children of parent have to be removed,
981     // which new children have to be added, and preemptively Q_EMIT dataChanged for the rest
982     // have to check how that influences performance...
983     const QSet<Meta::DataPtr> dataSet = dataList.toSet();
984     QSet<Meta::DataPtr> childrenSet;
985     foreach( CollectionTreeItem *child, parent->children() )
986     {
987         // we don't add null children, these are special-cased below
988         if( !child->data() )
989             continue;
990 
991         childrenSet.insert( child->data() );
992     }
993     const QSet<Meta::DataPtr> dataToBeAdded = dataSet - childrenSet;
994     const QSet<Meta::DataPtr> dataToBeRemoved = childrenSet - dataSet;
995 
996     // first remove all rows that have to be removed
997     // walking through the children in reverse order does not screw up the order
998     for( int i = parent->childCount() - 1; i >= 0; i-- )
999     {
1000         CollectionTreeItem *child = parent->child( i );
1001 
1002         // should this child be removed?
1003         bool toBeRemoved;
1004 
1005         if( child->isDataItem() )
1006             toBeRemoved = dataToBeRemoved.contains( child->data() );
1007         else if( child->isVariousArtistItem() )
1008             toBeRemoved = !variousArtistCategories.contains( childCategory );
1009         else if( child->isNoLabelItem() )
1010             toBeRemoved = childCategory != CategoryId::Label;
1011         else
1012         {
1013             warning() << "Unknown child type encountered in populateChildren(), removing";
1014             toBeRemoved = true;
1015         }
1016 
1017         if( toBeRemoved )
1018         {
1019             beginRemoveRows( parentIndex, i, i );
1020             parent->removeChild( i );
1021             endRemoveRows();
1022         }
1023         else
1024         {
1025             // the remaining child items may be dirty, so refresh them
1026             if( child->isDataItem() && child->data() && m_expandedItems.contains( child->data() ) )
1027                 ensureChildrenLoaded( child );
1028 
1029             // tell the view that the existing children may have changed
1030             QModelIndex idx = index( i, 0, parentIndex );
1031             Q_EMIT dataChanged( idx, idx );
1032         }
1033     }
1034 
1035     // add the new rows
1036     if( !dataToBeAdded.isEmpty() )
1037     {
1038         int lastRow = parent->childCount() - 1;
1039         //the above check ensures that Qt does not crash on beginInsertRows ( because lastRow+1 > lastRow+0)
1040         beginInsertRows( parentIndex, lastRow + 1, lastRow + dataToBeAdded.count() );
1041         foreach( Meta::DataPtr data, dataToBeAdded )
1042         {
1043             new CollectionTreeItem( data, parent, this );
1044         }
1045         endInsertRows();
1046     }
1047 
1048     parent->setRequiresUpdate( false );
1049 }
1050 
1051 void
updateHeaderText()1052 CollectionTreeItemModelBase::updateHeaderText()
1053 {
1054     m_headerText.clear();
1055     for( int i=0; i< m_levelType.count(); ++i )
1056         m_headerText += nameForLevel( i ) + " / ";
1057 
1058     m_headerText.chop( 3 );
1059 }
1060 
1061 QIcon
iconForCategory(CategoryId::CatMenuId category)1062 CollectionTreeItemModelBase::iconForCategory( CategoryId::CatMenuId category )
1063 {
1064     switch( category )
1065     {
1066         case CategoryId::Album :
1067             return QIcon::fromTheme( QStringLiteral("media-optical-amarok") );
1068         case CategoryId::Artist :
1069             return QIcon::fromTheme( QStringLiteral("view-media-artist-amarok") );
1070         case CategoryId::AlbumArtist :
1071             return QIcon::fromTheme( QStringLiteral("view-media-artist-amarok") );
1072         case CategoryId::Composer :
1073             return QIcon::fromTheme( QStringLiteral("filename-composer-amarok") );
1074         case CategoryId::Genre :
1075             return QIcon::fromTheme( QStringLiteral("favorite-genres-amarok") );
1076         case CategoryId::Year :
1077             return QIcon::fromTheme( QStringLiteral("clock") );
1078         case CategoryId::Label :
1079             return QIcon::fromTheme( QStringLiteral("label-amarok") );
1080         case CategoryId::None:
1081         default:
1082             return QIcon::fromTheme( QStringLiteral("image-missing") );
1083     }
1084 
1085 }
1086 
1087 QIcon
iconForLevel(int level) const1088 CollectionTreeItemModelBase::iconForLevel( int level ) const
1089 {
1090     return iconForCategory( m_levelType.value( level ) );
1091 }
1092 
1093 QString
nameForCategory(CategoryId::CatMenuId category,bool showYears)1094 CollectionTreeItemModelBase::nameForCategory( CategoryId::CatMenuId category, bool showYears )
1095 {
1096     switch( category )
1097     {
1098         case CategoryId::Album:
1099             return showYears ? i18n( "Year - Album" ) : i18n( "Album" );
1100         case CategoryId::Artist:
1101             return i18n( "Track Artist" );
1102         case CategoryId::AlbumArtist:
1103             return i18n( "Album Artist" );
1104         case CategoryId::Composer:
1105             return i18n( "Composer" );
1106         case CategoryId::Genre:
1107             return i18n( "Genre" );
1108         case CategoryId::Year:
1109             return i18n( "Year" );
1110         case CategoryId::Label:
1111             return i18n( "Label" );
1112         case CategoryId::None:
1113             return i18n( "None" );
1114         default:
1115             return QString();
1116     }
1117 }
1118 
1119 QString
nameForLevel(int level) const1120 CollectionTreeItemModelBase::nameForLevel( int level ) const
1121 {
1122     return nameForCategory( m_levelType.value( level ), AmarokConfig::showYears() );
1123 }
1124 
1125 void
handleCompilations(Collections::QueryMaker::QueryType queryType,CollectionTreeItem * parent) const1126 CollectionTreeItemModelBase::handleCompilations( Collections::QueryMaker::QueryType queryType, CollectionTreeItem *parent ) const
1127 {
1128     // this method will be called when we retrieve a list of artists from the database.
1129     // we have to query for all compilations, and then add a "Various Artists" node if at least
1130     // one compilation exists
1131     Collections::QueryMaker *qm = parent->queryMaker();
1132     qm->setQueryType( queryType );
1133     qm->setAlbumQueryMode( Collections::QueryMaker::OnlyCompilations );
1134     for( CollectionTreeItem *tmp = parent; tmp; tmp = tmp->parent() )
1135         tmp->addMatch( qm, levelCategory( tmp->level() - 1 ) );
1136 
1137     Collections::addTextualFilter( qm, m_currentFilter );
1138     addQueryMaker( parent, qm );
1139     m_compilationQueries.insert( qm, parent );
1140     qm->run();
1141 }
1142 
1143 void
handleTracksWithoutLabels(Collections::QueryMaker::QueryType queryType,CollectionTreeItem * parent) const1144 CollectionTreeItemModelBase::handleTracksWithoutLabels( Collections::QueryMaker::QueryType queryType, CollectionTreeItem *parent ) const
1145 {
1146     Collections::QueryMaker *qm = parent->queryMaker();
1147     qm->setQueryType( queryType );
1148     qm->setLabelQueryMode( Collections::QueryMaker::OnlyWithoutLabels );
1149     for( CollectionTreeItem *tmp = parent; tmp; tmp = tmp->parent() )
1150         tmp->addMatch( qm, levelCategory( tmp->level() - 1 ) );
1151 
1152     Collections::addTextualFilter( qm, m_currentFilter );
1153     addQueryMaker( parent, qm );
1154     m_noLabelsQueries.insert( qm, parent );
1155     qm->run();
1156 }
1157 
1158 
startAnimationTick()1159 void CollectionTreeItemModelBase::startAnimationTick()
1160 {
1161     //start animation
1162     if( ( m_timeLine->state() != QTimeLine::Running ) && !m_runningQueries.isEmpty() )
1163         m_timeLine->start();
1164 }
1165 
loadingAnimationTick()1166 void CollectionTreeItemModelBase::loadingAnimationTick()
1167 {
1168     if ( m_animFrame == 0 )
1169         m_currentAnimPixmap = m_loading2;
1170     else
1171         m_currentAnimPixmap = m_loading1;
1172 
1173     m_animFrame = 1 - m_animFrame;
1174 
1175     //trigger an update of all items being populated at the moment;
1176 
1177     QList< CollectionTreeItem * > items = m_runningQueries.uniqueKeys();
1178     foreach ( CollectionTreeItem* item, items  )
1179     {
1180         if( item == m_rootItem )
1181             continue;
1182         Q_EMIT dataChanged( itemIndex( item ), itemIndex( item ) );
1183     }
1184 }
1185 
1186 QString
currentFilter() const1187 CollectionTreeItemModelBase::currentFilter() const
1188 {
1189     return m_currentFilter;
1190 }
1191 
1192 void
setCurrentFilter(const QString & filter)1193 CollectionTreeItemModelBase::setCurrentFilter( const QString &filter )
1194 {
1195     m_currentFilter = filter;
1196     slotFilter( /* autoExpand */ true );
1197 }
1198 
1199 void
slotFilter(bool autoExpand)1200 CollectionTreeItemModelBase::slotFilter( bool autoExpand )
1201 {
1202     m_autoExpand = autoExpand;
1203     filterChildren();
1204 
1205     // following is not auto-expansion, it is restoring the state before filtering
1206     foreach( Collections::Collection *expanded, m_expandedCollections )
1207     {
1208         CollectionTreeItem *expandedItem = m_collections.value( expanded->collectionId() ).second;
1209         if( expandedItem )
1210             Q_EMIT expandIndex( itemIndex( expandedItem ) );
1211     }
1212 }
1213 
1214 void
slotCollapsed(const QModelIndex & index)1215 CollectionTreeItemModelBase::slotCollapsed( const QModelIndex &index )
1216 {
1217     if ( index.isValid() )      //probably unnecessary, but let's be safe
1218     {
1219         CollectionTreeItem *item = static_cast<CollectionTreeItem*>( index.internalPointer() );
1220 
1221         switch( item->type() )
1222         {
1223         case CollectionTreeItem::Root:
1224             break; // nothing to do
1225 
1226         case CollectionTreeItem::Collection:
1227             m_expandedCollections.remove( item->parentCollection() );
1228             break;
1229 
1230         case CollectionTreeItem::VariousArtist:
1231         case CollectionTreeItem::NoLabel:
1232             m_expandedSpecialNodes.remove( item->parentCollection() );
1233             break;
1234         case CollectionTreeItem::Data:
1235             m_expandedItems.remove( item->data() );
1236             break;
1237         }
1238     }
1239 }
1240 
1241 void
slotExpanded(const QModelIndex & index)1242 CollectionTreeItemModelBase::slotExpanded( const QModelIndex &index )
1243 {
1244     if( !index.isValid() )
1245         return;
1246 
1247     CollectionTreeItem *item = static_cast<CollectionTreeItem*>( index.internalPointer() );
1248     // we are really only interested in the special nodes here.
1249     // we have to remember whether the user expanded a various artists/no labels node or not.
1250     // otherwise we won't be able to automatically expand the special node after filtering again
1251     // there is exactly one special node per type per collection, so use the collection to store that information
1252 
1253     // we also need to store collection expansion state here as they are no longer
1254     // added to th expanded set in handleNormalQueryResult()
1255     switch( item->type() )
1256     {
1257         case CollectionTreeItem::VariousArtist:
1258         case CollectionTreeItem::NoLabel:
1259             m_expandedSpecialNodes.insert( item->parentCollection() );
1260             break;
1261         case CollectionTreeItem::Collection:
1262             m_expandedCollections.insert( item->parentCollection() );
1263         default:
1264             break;
1265     }
1266 }
1267 
markSubTreeAsDirty(CollectionTreeItem * item)1268 void CollectionTreeItemModelBase::markSubTreeAsDirty( CollectionTreeItem *item )
1269 {
1270     //tracks are the leaves so they are never dirty
1271     if( !item->isTrackItem() )
1272         item->setRequiresUpdate( true );
1273     for( int i = 0; i < item->childCount(); i++ )
1274     {
1275         markSubTreeAsDirty( item->child( i ) );
1276     }
1277 }
1278 
itemAboutToBeDeleted(CollectionTreeItem * item)1279 void CollectionTreeItemModelBase::itemAboutToBeDeleted( CollectionTreeItem *item )
1280 {
1281     // also all the children will be deleted
1282     foreach( CollectionTreeItem *child, item->children() )
1283         itemAboutToBeDeleted( child );
1284 
1285     if( !m_runningQueries.contains( item ) )
1286         return;
1287     // TODO: replace this hack with QWeakPointer now than we depend on Qt >= 4.8
1288     foreach(Collections::QueryMaker *qm, m_runningQueries.values( item ))
1289     {
1290         m_childQueries.remove( qm );
1291         m_compilationQueries.remove( qm );
1292         m_noLabelsQueries.remove( qm );
1293         m_runningQueries.remove(item, qm);
1294 
1295         //Disconnect all signals from the QueryMaker so we do not get notified about the results
1296         qm->disconnect();
1297         qm->abortQuery();
1298         //Nuke it
1299         qm->deleteLater();
1300     }
1301 }
1302 
1303 void
setDragSourceCollections(const QSet<Collections::Collection * > & collections)1304 CollectionTreeItemModelBase::setDragSourceCollections( const QSet<Collections::Collection*> &collections )
1305 {
1306     m_dragSourceCollections = collections;
1307 }
1308 
1309 bool
hasRunningQueries() const1310 CollectionTreeItemModelBase::hasRunningQueries() const
1311 {
1312     return !m_runningQueries.isEmpty();
1313 }
1314 
1315 CategoryId::CatMenuId
levelCategory(const int level) const1316 CollectionTreeItemModelBase::levelCategory( const int level ) const
1317 {
1318     const int actualLevel = level + levelModifier();
1319     if( actualLevel >= 0 && actualLevel < m_levelType.count() )
1320         return m_levelType.at( actualLevel );
1321 
1322     return CategoryId::None;
1323 }
1324 
1325