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