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