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