1 /****************************************************************************************
2  * Copyright (c) 2007 Ian Monroe <ian@monroe.nu>                                        *
3  * Copyright (c) 2008-2009 Dan Meltzer <parallelgrapefruit@gmail.com>                   *
4  * Copyright (c) 2011 Ralf Engels <ralf-engels@gmx.de>                                  *
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) version 3 or        *
9  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
10  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
11  * version 3 of the license.                                                            *
12  *                                                                                      *
13  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
14  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
15  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
16  *                                                                                      *
17  * You should have received a copy of the GNU General Public License along with         *
18  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
19  ****************************************************************************************/
20 
21 #define DEBUG_PREFIX "CollectionWidget"
22 
23 #include "CollectionWidget.h"
24 
25 #include "amarokconfig.h"
26 #include "browsers/CollectionTreeItemModel.h"
27 #include "browsers/CollectionTreeItemModelBase.h"
28 #include "browsers/SingleCollectionTreeItemModel.h"
29 #include "browsers/collectionbrowser/CollectionBrowserTreeView.h"
30 #include "core/meta/support/MetaConstants.h"
31 #include "core/support/Amarok.h"
32 #include "core/support/Debug.h"
33 #include "core-impl/collections/aggregate/AggregateCollection.h"
34 #include "core-impl/collections/support/CollectionManager.h"
35 #include "widgets/SearchWidget.h"
36 #include "widgets/PrettyTreeDelegate.h"
37 
38 #include <KLocalizedString>
39 #include <KStandardGuiItem>
40 
41 #include <QAction>
42 #include <QActionGroup>
43 #include <QBoxLayout>
44 #include <QIcon>
45 #include <QMenu>
46 #include <QMetaEnum>
47 #include <QMetaObject>
48 #include <QRect>
49 #include <QSortFilterProxyModel>
50 #include <QStackedWidget>
51 #include <QStandardPaths>
52 #include <QToolBar>
53 #include <QToolButton>
54 
55 CollectionWidget *CollectionWidget::s_instance = nullptr;
56 
57 #define CATEGORY_LEVEL_COUNT 3
58 
59 Q_DECLARE_METATYPE( QList<CategoryId::CatMenuId> ) // needed to QAction payload
60 
61 class CollectionWidget::Private
62 {
63 public:
Private()64     Private()
65         : treeView( 0 )
66         , singleTreeView( 0 )
67         , viewMode( CollectionWidget::NormalCollections ) {}
~Private()68     ~Private() {}
69 
70     CollectionBrowserTreeView *view( CollectionWidget::ViewMode mode );
71 
72     CollectionBrowserTreeView *treeView;
73     CollectionBrowserTreeView *singleTreeView;
74     QStackedWidget *stack;
75     SearchWidget *searchWidget;
76     CollectionWidget::ViewMode viewMode;
77 
78     QMenu *menuLevel[CATEGORY_LEVEL_COUNT];
79     QActionGroup *levelGroups[CATEGORY_LEVEL_COUNT];
80 };
81 
82 CollectionBrowserTreeView *
view(CollectionWidget::ViewMode mode)83 CollectionWidget::Private::view( CollectionWidget::ViewMode mode )
84 {
85     CollectionBrowserTreeView *v(nullptr);
86 
87     switch( mode )
88     {
89     case CollectionWidget::NormalCollections:
90         if( !treeView )
91         {
92             v = new CollectionBrowserTreeView( stack );
93             v->setAlternatingRowColors( true );
94             v->setFrameShape( QFrame::NoFrame );
95             v->setRootIsDecorated( false );
96             connect( v, &CollectionBrowserTreeView::leavingTree,
97                      searchWidget->comboBox(), QOverload<>::of(&QWidget::setFocus) );
98             PrettyTreeDelegate *delegate = new PrettyTreeDelegate( v );
99             v->setItemDelegate( delegate );
100             CollectionTreeItemModelBase *multiModel = new CollectionTreeItemModel( QList<CategoryId::CatMenuId>() );
101             multiModel->setParent( stack );
102             v->setModel( multiModel );
103             treeView = v;
104         }
105         else
106         {
107             v = treeView;
108         }
109         break;
110 
111     case CollectionWidget::UnifiedCollection:
112         if( !singleTreeView )
113         {
114             v = new CollectionBrowserTreeView( stack );
115             v->setAlternatingRowColors( true );
116             v->setFrameShape( QFrame::NoFrame );
117             Collections::AggregateCollection *aggregateColl = new Collections::AggregateCollection();
118             connect( CollectionManager::instance(), &CollectionManager::collectionAdded,
119                      aggregateColl, &Collections::AggregateCollection::addCollection );
120             connect( CollectionManager::instance(), &CollectionManager::collectionRemoved,
121                      aggregateColl, &Collections::AggregateCollection::removeCollectionById );
122             foreach( Collections::Collection* coll, CollectionManager::instance()->viewableCollections() )
123             {
124                 aggregateColl->addCollection( coll, CollectionManager::CollectionViewable );
125             }
126             CollectionTreeItemModelBase *singleModel = new SingleCollectionTreeItemModel( aggregateColl, QList<CategoryId::CatMenuId>() );
127             singleModel->setParent( stack );
128             v->setModel( singleModel );
129             singleTreeView = v;
130         }
131         else
132         {
133             v = singleTreeView;
134         }
135         break;
136     }
137     return v;
138 }
139 
CollectionWidget(const QString & name,QWidget * parent)140 CollectionWidget::CollectionWidget( const QString &name , QWidget *parent )
141     : BrowserCategory( name, parent )
142     , d( new Private )
143 {
144     s_instance = this;
145     setObjectName( name );
146     //TODO: we have a really nice opportunity to make these info blurbs both helpful and pretty
147     setLongDescription( i18n( "This is where you will find your local music, as well as music from mobile audio players and CDs." ) );
148     setImagePath( QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("amarok/images/hover_info_collections.png") ) );
149 
150     // set background
151     if( AmarokConfig::showBrowserBackgroundImage() )
152         setBackgroundImage( imagePath() );
153 
154     // --- the box for the UI elements.
155     BoxWidget *hbox = new BoxWidget( false, this );
156 
157     d->stack = new QStackedWidget( this );
158 
159     // -- read the current view mode from the configuration
160     const QMetaObject *mo = metaObject();
161     const QMetaEnum me = mo->enumerator( mo->indexOfEnumerator( "ViewMode" ) );
162     const QString &value = Amarok::config( QStringLiteral("Collection Browser") ).readEntry( "View Mode" );
163     int enumValue = me.keyToValue( value.toLocal8Bit().constData() );
164     enumValue == -1 ? d->viewMode = NormalCollections : d->viewMode = (ViewMode) enumValue;
165 
166     // -- the search widget
167     d->searchWidget = new SearchWidget( hbox );
168     d->searchWidget->setClickMessage( i18n( "Search collection" ) );
169 
170     // Filter presets. UserRole is used to store the actual syntax.
171     QComboBox *combo = d->searchWidget->comboBox();
172     const QIcon icon = KStandardGuiItem::find().icon();
173     combo->addItem( icon, i18nc("@item:inlistbox Collection widget filter preset", "Added This Hour"),
174                     QString(Meta::shortI18nForField( Meta::valCreateDate ) + ":<1h") );
175     combo->addItem( icon, i18nc("@item:inlistbox Collection widget filter preset", "Added Today"),
176                     QString(Meta::shortI18nForField( Meta::valCreateDate ) + ":<1d") );
177     combo->addItem( icon, i18nc("@item:inlistbox Collection widget filter preset", "Added This Week"),
178                     QString(Meta::shortI18nForField( Meta::valCreateDate ) + ":<1w") );
179     combo->addItem( icon, i18nc("@item:inlistbox Collection widget filter preset", "Added This Month"),
180                     QString(Meta::shortI18nForField( Meta::valCreateDate ) + ":<1m") );
181     combo->insertSeparator( combo->count() );
182 
183     QMenu *filterMenu = new QMenu( nullptr );
184 
185     using namespace CategoryId;
186     static const QList<QList<CatMenuId> > levelPresets = QList<QList<CatMenuId> >()
187         << ( QList<CatMenuId>() << CategoryId::AlbumArtist << CategoryId::Album )
188         << ( QList<CatMenuId>() << CategoryId::Album << CategoryId::Artist ) // album artist has no sense here
189         << ( QList<CatMenuId>() << CategoryId::Genre << CategoryId::AlbumArtist )
190         << ( QList<CatMenuId>() << CategoryId::Genre << CategoryId::AlbumArtist << CategoryId::Album );
191     foreach( const QList<CatMenuId> &levels, levelPresets )
192     {
193         QStringList categoryLabels;
194         foreach( CatMenuId category, levels )
195             categoryLabels << CollectionTreeItemModelBase::nameForCategory( category );
196         QAction *action = filterMenu->addAction( categoryLabels.join( i18nc(
197                 "separator between collection browser level categories, i.e. the ' / ' "
198                 "in 'Artist / Album'", " / " ) ) );
199         action->setData( QVariant::fromValue( levels ) );
200     }
201     // following catches all actions in the filter menu
202     connect( filterMenu, &QMenu::triggered, this, &CollectionWidget::sortByActionPayload );
203     filterMenu->addSeparator();
204 
205     // -- read the view level settings from the configuration
206     QList<CategoryId::CatMenuId> levels = readLevelsFromConfig();
207     if ( levels.isEmpty() )
208         levels << levelPresets.at( 0 ); // use first preset as default
209 
210     // -- generate the level menus
211     d->menuLevel[0] = filterMenu->addMenu( i18n( "First Level" ) );
212     d->menuLevel[1] = filterMenu->addMenu( i18n( "Second Level" ) );
213     d->menuLevel[2] = filterMenu->addMenu( i18n( "Third Level" ) );
214 
215     // - fill the level menus
216     static const QList<CatMenuId> levelChoices = QList<CatMenuId>()
217             << CategoryId::AlbumArtist
218             << CategoryId::Artist
219             << CategoryId::Album
220             << CategoryId::Genre
221             << CategoryId::Composer
222             << CategoryId::Label;
223     for( int i = 0; i < CATEGORY_LEVEL_COUNT; i++ )
224     {
225         QList<CatMenuId> usedLevelChoices = levelChoices;
226         QActionGroup *actionGroup = new QActionGroup( this );
227         if( i > 0 ) // skip first submenu
228             usedLevelChoices.prepend( CategoryId::None );
229 
230         QMenu *menuLevel = d->menuLevel[i];
231         foreach( CatMenuId level, usedLevelChoices )
232         {
233             QAction *action = menuLevel->addAction( CollectionTreeItemModelBase::nameForCategory( level ) );
234             action->setData( QVariant::fromValue<CatMenuId>( level ) );
235             action->setCheckable( true );
236             action->setChecked( ( levels.count() > i ) ? ( levels[i] == level )
237                     : ( level == CategoryId::None ) );
238             actionGroup->addAction( action );
239         }
240 
241         d->levelGroups[i] = actionGroup;
242         connect( menuLevel, &QMenu::triggered, this, &CollectionWidget::sortLevelSelected );
243     }
244 
245     // -- create the checkboxesh
246     filterMenu->addSeparator();
247     QAction *showYears = filterMenu->addAction( i18n( "Show Years" ) );
248     showYears->setCheckable( true );
249     showYears->setChecked( AmarokConfig::showYears() );
250     connect( showYears, &QAction::toggled, this, &CollectionWidget::slotShowYears );
251 
252     QAction *showTrackNumbers = filterMenu->addAction( i18nc("@action:inmenu", "Show Track Numbers") );
253     showTrackNumbers->setCheckable( true );
254     showTrackNumbers->setChecked( AmarokConfig::showTrackNumbers() );
255     connect( showTrackNumbers, &QAction::toggled, this, &CollectionWidget::slotShowTrackNumbers );
256 
257     QAction *showCovers = filterMenu->addAction( i18n( "Show Cover Art" ) );
258     showCovers->setCheckable( true );
259     showCovers->setChecked( AmarokConfig::showAlbumArt() );
260     connect( showCovers, &QAction::toggled, this, &CollectionWidget::slotShowCovers );
261 
262     d->searchWidget->toolBar()->addSeparator();
263 
264     QAction *toggleAction = new QAction( QIcon::fromTheme( QStringLiteral("view-list-tree") ), i18n( "Merged View" ), this );
265     toggleAction->setCheckable( true );
266     toggleAction->setChecked( d->viewMode == CollectionWidget::UnifiedCollection );
267     toggleView( d->viewMode == CollectionWidget::UnifiedCollection );
268     connect( toggleAction, &QAction::triggered, this, &CollectionWidget::toggleView );
269     d->searchWidget->toolBar()->addAction( toggleAction );
270 
271     QAction *searchMenuAction = new QAction( QIcon::fromTheme( QStringLiteral("preferences-other") ), i18n( "Sort Options" ), this );
272     searchMenuAction->setMenu( filterMenu );
273     d->searchWidget->toolBar()->addAction( searchMenuAction );
274 
275     QToolButton *tbutton = qobject_cast<QToolButton*>( d->searchWidget->toolBar()->widgetForAction( searchMenuAction ) );
276     if( tbutton )
277         tbutton->setPopupMode( QToolButton::InstantPopup );
278 
279     setLevels( levels );
280 }
281 
~CollectionWidget()282 CollectionWidget::~CollectionWidget()
283 {
284     delete d;
285 }
286 
287 
288 void
focusInputLine()289 CollectionWidget::focusInputLine()
290 {
291     d->searchWidget->comboBox()->setFocus();
292 }
293 
294 void
sortLevelSelected(QAction * action)295 CollectionWidget::sortLevelSelected( QAction *action )
296 {
297     Q_UNUSED( action );
298 
299     QList<CategoryId::CatMenuId> levels;
300     for( int i = 0; i < CATEGORY_LEVEL_COUNT; i++ )
301     {
302         const QAction *action = d->levelGroups[i]->checkedAction();
303         if( action )
304         {
305             CategoryId::CatMenuId category = action->data().value<CategoryId::CatMenuId>();
306             if( category != CategoryId::None )
307                 levels << category;
308         }
309     }
310     setLevels( levels );
311 }
312 
313 void
sortByActionPayload(QAction * action)314 CollectionWidget::sortByActionPayload( QAction *action )
315 {
316     QList<CategoryId::CatMenuId> levels = action->data().value<QList<CategoryId::CatMenuId> >();
317     if( !levels.isEmpty() )
318         setLevels( levels );
319 }
320 
321 void
slotShowYears(bool checked)322 CollectionWidget::slotShowYears( bool checked )
323 {
324     AmarokConfig::setShowYears( checked );
325     setLevels( levels() );
326 }
327 
328 void
slotShowTrackNumbers(bool checked)329 CollectionWidget::slotShowTrackNumbers( bool checked )
330 {
331     AmarokConfig::setShowTrackNumbers( checked );
332     setLevels( levels() );
333 }
334 
335 void
slotShowCovers(bool checked)336 CollectionWidget::slotShowCovers(bool checked)
337 {
338     AmarokConfig::setShowAlbumArt( checked );
339     setLevels( levels() );
340 }
341 
342 QString
filter() const343 CollectionWidget::filter() const
344 {
345     return d->searchWidget->currentText();
346 }
347 
setFilter(const QString & filter)348 void CollectionWidget::setFilter( const QString &filter )
349 {
350     d->searchWidget->setSearchString( filter );
351 }
352 
353 QList<CategoryId::CatMenuId>
levels() const354 CollectionWidget::levels() const
355 {
356     // return const_cast<CollectionWidget*>( this )->view( d->viewMode )->levels();
357     return d->view( d->viewMode )->levels();
358 }
359 
setLevels(const QList<CategoryId::CatMenuId> & levels)360 void CollectionWidget::setLevels( const QList<CategoryId::CatMenuId> &levels )
361 {
362     // -- select the correct menu entries
363     QSet<CategoryId::CatMenuId> encounteredLevels;
364     for( int i = 0; i < CATEGORY_LEVEL_COUNT; i++ )
365     {
366         CategoryId::CatMenuId category;
367         if( levels.count() > i )
368             category = levels[i];
369         else
370             category = CategoryId::None;
371 
372         foreach( QAction *action, d->levelGroups[i]->actions() )
373         {
374             CategoryId::CatMenuId actionCategory = action->data().value<CategoryId::CatMenuId>();
375             if( actionCategory == category )
376                 action->setChecked( true ); // unchecks other actions in the same group
377             action->setEnabled( !encounteredLevels.contains( actionCategory ) );
378         }
379 
380         if( category != CategoryId::None )
381             encounteredLevels << category;
382     }
383 
384     // -- set the levels in the view
385     d->view( d->viewMode )->setLevels( levels );
386     debug() << "Sort levels:" << levels;
387 }
388 
toggleView(bool merged)389 void CollectionWidget::toggleView( bool merged )
390 {
391     CollectionWidget::ViewMode newMode = merged ? UnifiedCollection : NormalCollections;
392     CollectionBrowserTreeView *oldView = d->view( d->viewMode );
393 
394     if( oldView )
395     {
396         d->searchWidget->disconnect( oldView );
397         oldView->disconnect( d->searchWidget );
398     }
399 
400     CollectionBrowserTreeView *newView = d->view( newMode );
401     connect( d->searchWidget, &SearchWidget::filterChanged,
402              newView, &CollectionBrowserTreeView::slotSetFilter );
403     connect( d->searchWidget, &SearchWidget::returnPressed,
404              newView, &CollectionBrowserTreeView::slotAddFilteredTracksToPlaylist );
405     // reset search string after successful adding of filtered items to playlist
406     connect( newView, &CollectionBrowserTreeView::addingFilteredTracksDone,
407              d->searchWidget, &SearchWidget::emptySearchString );
408 
409     if( d->stack->indexOf( newView ) == -1 )
410         d->stack->addWidget( newView );
411     d->stack->setCurrentWidget( newView );
412     const QString &filter = d->searchWidget->currentText();
413     if( !filter.isEmpty() )
414     {
415         typedef CollectionTreeItemModelBase CTIMB;
416         CTIMB *model = qobject_cast<CTIMB*>( newView->filterModel()->sourceModel() );
417         model->setCurrentFilter( filter );
418     }
419 
420     d->viewMode = newMode;
421     if( oldView )
422         setLevels( oldView->levels() );
423 
424     const QMetaObject *mo = metaObject();
425     const QMetaEnum me = mo->enumerator( mo->indexOfEnumerator( "ViewMode" ) );
426     Amarok::config( QStringLiteral("Collection Browser") ).writeEntry( "View Mode", me.valueToKey( d->viewMode ) );
427 }
428 
429 QList<CategoryId::CatMenuId>
readLevelsFromConfig() const430 CollectionWidget::readLevelsFromConfig() const
431 {
432     QList<int> levelNumbers = Amarok::config( QStringLiteral("Collection Browser") ).readEntry( "TreeCategory", QList<int>() );
433     QList<CategoryId::CatMenuId> levels;
434 
435     // we changed "Track Artist" to "Album Artist" default before Amarok 2.8. Migrate user
436     // config mentioning Track Artist to Album Artist where it makes sense:
437     static const int OldArtistValue = 2;
438     bool albumOrAlbumArtistEncountered = false;
439     foreach( int levelNumber, levelNumbers )
440     {
441         CategoryId::CatMenuId category;
442         if( levelNumber == OldArtistValue )
443         {
444             if( albumOrAlbumArtistEncountered )
445                 category = CategoryId::Artist;
446             else
447                 category = CategoryId::AlbumArtist;
448         }
449         else
450             category = CategoryId::CatMenuId( levelNumber );
451 
452         levels << category;
453         if( category == CategoryId::Album || category == CategoryId::AlbumArtist )
454             albumOrAlbumArtistEncountered = true;
455     }
456 
457     return levels;
458 }
459 
460 CollectionBrowserTreeView*
currentView()461 CollectionWidget::currentView()
462 {
463     return d->view( d->viewMode );
464 }
465 
466 CollectionWidget::ViewMode
viewMode() const467 CollectionWidget::viewMode() const
468 {
469     return d->viewMode;
470 }
471 
472 SearchWidget*
searchWidget()473 CollectionWidget::searchWidget()
474 {
475     return d->searchWidget;
476 }
477 
478