File indexing completed on 2024-05-19 04:48:37
0001 /**************************************************************************************** 0002 * Copyright (c) 2007 Ian Monroe <ian@monroe.nu> * 0003 * Copyright (c) 2008-2009 Dan Meltzer <parallelgrapefruit@gmail.com> * 0004 * Copyright (c) 2011 Ralf Engels <ralf-engels@gmx.de> * 0005 * * 0006 * This program is free software; you can redistribute it and/or modify it under * 0007 * the terms of the GNU General Public License as published by the Free Software * 0008 * Foundation; either version 2 of the License, or (at your option) version 3 or * 0009 * any later version accepted by the membership of KDE e.V. (or its successor approved * 0010 * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * 0011 * version 3 of the license. * 0012 * * 0013 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0014 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0015 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0016 * * 0017 * You should have received a copy of the GNU General Public License along with * 0018 * this program. If not, see <http://www.gnu.org/licenses/>. * 0019 ****************************************************************************************/ 0020 0021 #define DEBUG_PREFIX "CollectionWidget" 0022 0023 #include "CollectionWidget.h" 0024 0025 #include "amarokconfig.h" 0026 #include "browsers/CollectionTreeItemModel.h" 0027 #include "browsers/CollectionTreeItemModelBase.h" 0028 #include "browsers/SingleCollectionTreeItemModel.h" 0029 #include "browsers/collectionbrowser/CollectionBrowserTreeView.h" 0030 #include "core/meta/support/MetaConstants.h" 0031 #include "core/support/Amarok.h" 0032 #include "core/support/Debug.h" 0033 #include "core-impl/collections/aggregate/AggregateCollection.h" 0034 #include "core-impl/collections/support/CollectionManager.h" 0035 #include "widgets/SearchWidget.h" 0036 #include "widgets/PrettyTreeDelegate.h" 0037 0038 #include <KLocalizedString> 0039 #include <KStandardGuiItem> 0040 0041 #include <QAction> 0042 #include <QActionGroup> 0043 #include <QBoxLayout> 0044 #include <QIcon> 0045 #include <QMenu> 0046 #include <QMetaEnum> 0047 #include <QMetaObject> 0048 #include <QRect> 0049 #include <QSortFilterProxyModel> 0050 #include <QStackedWidget> 0051 #include <QStandardPaths> 0052 #include <QToolBar> 0053 #include <QToolButton> 0054 0055 CollectionWidget *CollectionWidget::s_instance = nullptr; 0056 0057 #define CATEGORY_LEVEL_COUNT 3 0058 0059 Q_DECLARE_METATYPE( QList<CategoryId::CatMenuId> ) // needed to QAction payload 0060 0061 class CollectionWidget::Private 0062 { 0063 public: 0064 Private() 0065 : treeView( nullptr ) 0066 , singleTreeView( nullptr ) 0067 , viewMode( CollectionWidget::NormalCollections ) {} 0068 ~Private() {} 0069 0070 CollectionBrowserTreeView *view( CollectionWidget::ViewMode mode ); 0071 0072 CollectionBrowserTreeView *treeView; 0073 CollectionBrowserTreeView *singleTreeView; 0074 QStackedWidget *stack; 0075 SearchWidget *searchWidget; 0076 CollectionWidget::ViewMode viewMode; 0077 0078 QMenu *menuLevel[CATEGORY_LEVEL_COUNT]; 0079 QActionGroup *levelGroups[CATEGORY_LEVEL_COUNT]; 0080 }; 0081 0082 CollectionBrowserTreeView * 0083 CollectionWidget::Private::view( CollectionWidget::ViewMode mode ) 0084 { 0085 CollectionBrowserTreeView *v(nullptr); 0086 0087 switch( mode ) 0088 { 0089 case CollectionWidget::NormalCollections: 0090 if( !treeView ) 0091 { 0092 v = new CollectionBrowserTreeView( stack ); 0093 v->setAlternatingRowColors( true ); 0094 v->setFrameShape( QFrame::NoFrame ); 0095 v->setRootIsDecorated( false ); 0096 connect( v, &CollectionBrowserTreeView::leavingTree, 0097 searchWidget->comboBox(), QOverload<>::of(&QWidget::setFocus) ); 0098 PrettyTreeDelegate *delegate = new PrettyTreeDelegate( v ); 0099 v->setItemDelegate( delegate ); 0100 CollectionTreeItemModelBase *multiModel = new CollectionTreeItemModel( QList<CategoryId::CatMenuId>() ); 0101 multiModel->setParent( stack ); 0102 v->setModel( multiModel ); 0103 treeView = v; 0104 } 0105 else 0106 { 0107 v = treeView; 0108 } 0109 break; 0110 0111 case CollectionWidget::UnifiedCollection: 0112 if( !singleTreeView ) 0113 { 0114 v = new CollectionBrowserTreeView( stack ); 0115 v->setAlternatingRowColors( true ); 0116 v->setFrameShape( QFrame::NoFrame ); 0117 Collections::AggregateCollection *aggregateColl = new Collections::AggregateCollection(); 0118 connect( CollectionManager::instance(), &CollectionManager::collectionAdded, 0119 aggregateColl, &Collections::AggregateCollection::addCollection ); 0120 connect( CollectionManager::instance(), &CollectionManager::collectionRemoved, 0121 aggregateColl, &Collections::AggregateCollection::removeCollectionById ); 0122 foreach( Collections::Collection* coll, CollectionManager::instance()->viewableCollections() ) 0123 { 0124 aggregateColl->addCollection( coll, CollectionManager::CollectionViewable ); 0125 } 0126 CollectionTreeItemModelBase *singleModel = new SingleCollectionTreeItemModel( aggregateColl, QList<CategoryId::CatMenuId>() ); 0127 singleModel->setParent( stack ); 0128 v->setModel( singleModel ); 0129 singleTreeView = v; 0130 } 0131 else 0132 { 0133 v = singleTreeView; 0134 } 0135 break; 0136 } 0137 return v; 0138 } 0139 0140 CollectionWidget::CollectionWidget( const QString &name , QWidget *parent ) 0141 : BrowserCategory( name, parent ) 0142 , d( new Private ) 0143 { 0144 s_instance = this; 0145 setObjectName( name ); 0146 //TODO: we have a really nice opportunity to make these info blurbs both helpful and pretty 0147 setLongDescription( i18n( "This is where you will find your local music, as well as music from mobile audio players and CDs." ) ); 0148 setImagePath( QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("amarok/images/hover_info_collections.png") ) ); 0149 0150 // set background 0151 if( AmarokConfig::showBrowserBackgroundImage() ) 0152 setBackgroundImage( imagePath() ); 0153 0154 // --- the box for the UI elements. 0155 BoxWidget *hbox = new BoxWidget( false, this ); 0156 0157 d->stack = new QStackedWidget( this ); 0158 0159 // -- read the current view mode from the configuration 0160 const QMetaObject *mo = metaObject(); 0161 const QMetaEnum me = mo->enumerator( mo->indexOfEnumerator( "ViewMode" ) ); 0162 const QString &value = Amarok::config( QStringLiteral("Collection Browser") ).readEntry( "View Mode" ); 0163 int enumValue = me.keyToValue( value.toLocal8Bit().constData() ); 0164 enumValue == -1 ? d->viewMode = NormalCollections : d->viewMode = (ViewMode) enumValue; 0165 0166 // -- the search widget 0167 d->searchWidget = new SearchWidget( hbox ); 0168 d->searchWidget->setClickMessage( i18n( "Search collection" ) ); 0169 0170 // Filter presets. UserRole is used to store the actual syntax. 0171 QComboBox *combo = d->searchWidget->comboBox(); 0172 const QIcon icon = KStandardGuiItem::find().icon(); 0173 combo->addItem( icon, i18nc("@item:inlistbox Collection widget filter preset", "Added This Hour"), 0174 QString(Meta::shortI18nForField( Meta::valCreateDate ) + ":<1h") ); 0175 combo->addItem( icon, i18nc("@item:inlistbox Collection widget filter preset", "Added Today"), 0176 QString(Meta::shortI18nForField( Meta::valCreateDate ) + ":<1d") ); 0177 combo->addItem( icon, i18nc("@item:inlistbox Collection widget filter preset", "Added This Week"), 0178 QString(Meta::shortI18nForField( Meta::valCreateDate ) + ":<1w") ); 0179 combo->addItem( icon, i18nc("@item:inlistbox Collection widget filter preset", "Added This Month"), 0180 QString(Meta::shortI18nForField( Meta::valCreateDate ) + ":<1m") ); 0181 combo->insertSeparator( combo->count() ); 0182 0183 QMenu *filterMenu = new QMenu( nullptr ); 0184 0185 using namespace CategoryId; 0186 static const QList<QList<CatMenuId> > levelPresets = QList<QList<CatMenuId> >() 0187 << ( QList<CatMenuId>() << CategoryId::AlbumArtist << CategoryId::Album ) 0188 << ( QList<CatMenuId>() << CategoryId::Album << CategoryId::Artist ) // album artist has no sense here 0189 << ( QList<CatMenuId>() << CategoryId::Genre << CategoryId::AlbumArtist ) 0190 << ( QList<CatMenuId>() << CategoryId::Genre << CategoryId::AlbumArtist << CategoryId::Album ); 0191 foreach( const QList<CatMenuId> &levels, levelPresets ) 0192 { 0193 QStringList categoryLabels; 0194 foreach( CatMenuId category, levels ) 0195 categoryLabels << CollectionTreeItemModelBase::nameForCategory( category ); 0196 QAction *action = filterMenu->addAction( categoryLabels.join( i18nc( 0197 "separator between collection browser level categories, i.e. the ' / ' " 0198 "in 'Artist / Album'", " / " ) ) ); 0199 action->setData( QVariant::fromValue( levels ) ); 0200 } 0201 // following catches all actions in the filter menu 0202 connect( filterMenu, &QMenu::triggered, this, &CollectionWidget::sortByActionPayload ); 0203 filterMenu->addSeparator(); 0204 0205 // -- read the view level settings from the configuration 0206 QList<CategoryId::CatMenuId> levels = readLevelsFromConfig(); 0207 if ( levels.isEmpty() ) 0208 levels << levelPresets.at( 0 ); // use first preset as default 0209 0210 // -- generate the level menus 0211 d->menuLevel[0] = filterMenu->addMenu( i18n( "First Level" ) ); 0212 d->menuLevel[1] = filterMenu->addMenu( i18n( "Second Level" ) ); 0213 d->menuLevel[2] = filterMenu->addMenu( i18n( "Third Level" ) ); 0214 0215 // - fill the level menus 0216 static const QList<CatMenuId> levelChoices = QList<CatMenuId>() 0217 << CategoryId::AlbumArtist 0218 << CategoryId::Artist 0219 << CategoryId::Album 0220 << CategoryId::Genre 0221 << CategoryId::Composer 0222 << CategoryId::Label; 0223 for( int i = 0; i < CATEGORY_LEVEL_COUNT; i++ ) 0224 { 0225 QList<CatMenuId> usedLevelChoices = levelChoices; 0226 QActionGroup *actionGroup = new QActionGroup( this ); 0227 if( i > 0 ) // skip first submenu 0228 usedLevelChoices.prepend( CategoryId::None ); 0229 0230 QMenu *menuLevel = d->menuLevel[i]; 0231 foreach( CatMenuId level, usedLevelChoices ) 0232 { 0233 QAction *action = menuLevel->addAction( CollectionTreeItemModelBase::nameForCategory( level ) ); 0234 action->setData( QVariant::fromValue<CatMenuId>( level ) ); 0235 action->setCheckable( true ); 0236 action->setChecked( ( levels.count() > i ) ? ( levels[i] == level ) 0237 : ( level == CategoryId::None ) ); 0238 actionGroup->addAction( action ); 0239 } 0240 0241 d->levelGroups[i] = actionGroup; 0242 connect( menuLevel, &QMenu::triggered, this, &CollectionWidget::sortLevelSelected ); 0243 } 0244 0245 // -- create the checkboxesh 0246 filterMenu->addSeparator(); 0247 QAction *showYears = filterMenu->addAction( i18n( "Show Years" ) ); 0248 showYears->setCheckable( true ); 0249 showYears->setChecked( AmarokConfig::showYears() ); 0250 connect( showYears, &QAction::toggled, this, &CollectionWidget::slotShowYears ); 0251 0252 QAction *showTrackNumbers = filterMenu->addAction( i18nc("@action:inmenu", "Show Track Numbers") ); 0253 showTrackNumbers->setCheckable( true ); 0254 showTrackNumbers->setChecked( AmarokConfig::showTrackNumbers() ); 0255 connect( showTrackNumbers, &QAction::toggled, this, &CollectionWidget::slotShowTrackNumbers ); 0256 0257 QAction *showCovers = filterMenu->addAction( i18n( "Show Cover Art" ) ); 0258 showCovers->setCheckable( true ); 0259 showCovers->setChecked( AmarokConfig::showAlbumArt() ); 0260 connect( showCovers, &QAction::toggled, this, &CollectionWidget::slotShowCovers ); 0261 0262 d->searchWidget->toolBar()->addSeparator(); 0263 0264 QAction *toggleAction = new QAction( QIcon::fromTheme( QStringLiteral("view-list-tree") ), i18n( "Merged View" ), this ); 0265 toggleAction->setCheckable( true ); 0266 toggleAction->setChecked( d->viewMode == CollectionWidget::UnifiedCollection ); 0267 toggleView( d->viewMode == CollectionWidget::UnifiedCollection ); 0268 connect( toggleAction, &QAction::triggered, this, &CollectionWidget::toggleView ); 0269 d->searchWidget->toolBar()->addAction( toggleAction ); 0270 0271 QAction *searchMenuAction = new QAction( QIcon::fromTheme( QStringLiteral("preferences-other") ), i18n( "Sort Options" ), this ); 0272 searchMenuAction->setMenu( filterMenu ); 0273 d->searchWidget->toolBar()->addAction( searchMenuAction ); 0274 0275 QToolButton *tbutton = qobject_cast<QToolButton*>( d->searchWidget->toolBar()->widgetForAction( searchMenuAction ) ); 0276 if( tbutton ) 0277 tbutton->setPopupMode( QToolButton::InstantPopup ); 0278 0279 setLevels( levels ); 0280 } 0281 0282 CollectionWidget::~CollectionWidget() 0283 { 0284 delete d; 0285 } 0286 0287 0288 void 0289 CollectionWidget::focusInputLine() 0290 { 0291 d->searchWidget->comboBox()->setFocus(); 0292 } 0293 0294 void 0295 CollectionWidget::sortLevelSelected( QAction *action ) 0296 { 0297 Q_UNUSED( action ); 0298 0299 QList<CategoryId::CatMenuId> levels; 0300 for( int i = 0; i < CATEGORY_LEVEL_COUNT; i++ ) 0301 { 0302 const QAction *action = d->levelGroups[i]->checkedAction(); 0303 if( action ) 0304 { 0305 CategoryId::CatMenuId category = action->data().value<CategoryId::CatMenuId>(); 0306 if( category != CategoryId::None ) 0307 levels << category; 0308 } 0309 } 0310 setLevels( levels ); 0311 } 0312 0313 void 0314 CollectionWidget::sortByActionPayload( QAction *action ) 0315 { 0316 QList<CategoryId::CatMenuId> levels = action->data().value<QList<CategoryId::CatMenuId> >(); 0317 if( !levels.isEmpty() ) 0318 setLevels( levels ); 0319 } 0320 0321 void 0322 CollectionWidget::slotShowYears( bool checked ) 0323 { 0324 AmarokConfig::setShowYears( checked ); 0325 setLevels( levels() ); 0326 } 0327 0328 void 0329 CollectionWidget::slotShowTrackNumbers( bool checked ) 0330 { 0331 AmarokConfig::setShowTrackNumbers( checked ); 0332 setLevels( levels() ); 0333 } 0334 0335 void 0336 CollectionWidget::slotShowCovers(bool checked) 0337 { 0338 AmarokConfig::setShowAlbumArt( checked ); 0339 setLevels( levels() ); 0340 } 0341 0342 QString 0343 CollectionWidget::filter() const 0344 { 0345 return d->searchWidget->currentText(); 0346 } 0347 0348 void CollectionWidget::setFilter( const QString &filter ) 0349 { 0350 d->searchWidget->setSearchString( filter ); 0351 } 0352 0353 QList<CategoryId::CatMenuId> 0354 CollectionWidget::levels() const 0355 { 0356 // return const_cast<CollectionWidget*>( this )->view( d->viewMode )->levels(); 0357 return d->view( d->viewMode )->levels(); 0358 } 0359 0360 void CollectionWidget::setLevels( const QList<CategoryId::CatMenuId> &levels ) 0361 { 0362 // -- select the correct menu entries 0363 QSet<CategoryId::CatMenuId> encounteredLevels; 0364 for( int i = 0; i < CATEGORY_LEVEL_COUNT; i++ ) 0365 { 0366 CategoryId::CatMenuId category; 0367 if( levels.count() > i ) 0368 category = levels[i]; 0369 else 0370 category = CategoryId::None; 0371 0372 foreach( QAction *action, d->levelGroups[i]->actions() ) 0373 { 0374 CategoryId::CatMenuId actionCategory = action->data().value<CategoryId::CatMenuId>(); 0375 if( actionCategory == category ) 0376 action->setChecked( true ); // unchecks other actions in the same group 0377 action->setEnabled( !encounteredLevels.contains( actionCategory ) ); 0378 } 0379 0380 if( category != CategoryId::None ) 0381 encounteredLevels << category; 0382 } 0383 0384 // -- set the levels in the view 0385 d->view( d->viewMode )->setLevels( levels ); 0386 debug() << "Sort levels:" << levels; 0387 } 0388 0389 void CollectionWidget::toggleView( bool merged ) 0390 { 0391 CollectionWidget::ViewMode newMode = merged ? UnifiedCollection : NormalCollections; 0392 CollectionBrowserTreeView *oldView = d->view( d->viewMode ); 0393 0394 if( oldView ) 0395 { 0396 d->searchWidget->disconnect( oldView ); 0397 oldView->disconnect( d->searchWidget ); 0398 } 0399 0400 CollectionBrowserTreeView *newView = d->view( newMode ); 0401 connect( d->searchWidget, &SearchWidget::filterChanged, 0402 newView, &CollectionBrowserTreeView::slotSetFilter ); 0403 connect( d->searchWidget, &SearchWidget::returnPressed, 0404 newView, &CollectionBrowserTreeView::slotAddFilteredTracksToPlaylist ); 0405 // reset search string after successful adding of filtered items to playlist 0406 connect( newView, &CollectionBrowserTreeView::addingFilteredTracksDone, 0407 d->searchWidget, &SearchWidget::emptySearchString ); 0408 0409 if( d->stack->indexOf( newView ) == -1 ) 0410 d->stack->addWidget( newView ); 0411 d->stack->setCurrentWidget( newView ); 0412 const QString &filter = d->searchWidget->currentText(); 0413 if( !filter.isEmpty() ) 0414 { 0415 typedef CollectionTreeItemModelBase CTIMB; 0416 CTIMB *model = qobject_cast<CTIMB*>( newView->filterModel()->sourceModel() ); 0417 model->setCurrentFilter( filter ); 0418 } 0419 0420 d->viewMode = newMode; 0421 if( oldView ) 0422 setLevels( oldView->levels() ); 0423 0424 const QMetaObject *mo = metaObject(); 0425 const QMetaEnum me = mo->enumerator( mo->indexOfEnumerator( "ViewMode" ) ); 0426 Amarok::config( QStringLiteral("Collection Browser") ).writeEntry( "View Mode", me.valueToKey( d->viewMode ) ); 0427 } 0428 0429 QList<CategoryId::CatMenuId> 0430 CollectionWidget::readLevelsFromConfig() const 0431 { 0432 QList<int> levelNumbers = Amarok::config( QStringLiteral("Collection Browser") ).readEntry( "TreeCategory", QList<int>() ); 0433 QList<CategoryId::CatMenuId> levels; 0434 0435 // we changed "Track Artist" to "Album Artist" default before Amarok 2.8. Migrate user 0436 // config mentioning Track Artist to Album Artist where it makes sense: 0437 static const int OldArtistValue = 2; 0438 bool albumOrAlbumArtistEncountered = false; 0439 foreach( int levelNumber, levelNumbers ) 0440 { 0441 CategoryId::CatMenuId category; 0442 if( levelNumber == OldArtistValue ) 0443 { 0444 if( albumOrAlbumArtistEncountered ) 0445 category = CategoryId::Artist; 0446 else 0447 category = CategoryId::AlbumArtist; 0448 } 0449 else 0450 category = CategoryId::CatMenuId( levelNumber ); 0451 0452 levels << category; 0453 if( category == CategoryId::Album || category == CategoryId::AlbumArtist ) 0454 albumOrAlbumArtistEncountered = true; 0455 } 0456 0457 return levels; 0458 } 0459 0460 CollectionBrowserTreeView* 0461 CollectionWidget::currentView() 0462 { 0463 return d->view( d->viewMode ); 0464 } 0465 0466 CollectionWidget::ViewMode 0467 CollectionWidget::viewMode() const 0468 { 0469 return d->viewMode; 0470 } 0471 0472 SearchWidget* 0473 CollectionWidget::searchWidget() 0474 { 0475 return d->searchWidget; 0476 } 0477