File indexing completed on 2024-05-05 04:47:30

0001 /****************************************************************************************
0002  * Copyright (c) 2007 Alexandre Pereira de Oliveira <aleprj@gmail.com>                  *
0003  * Copyright (c) 2007 Ian Monroe <ian@monroe.nu>                                        *
0004  *                                                                                      *
0005  * This program is free software; you can redistribute it and/or modify it under        *
0006  * the terms of the GNU General Public License as published by the Free Software        *
0007  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
0008  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
0009  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
0010  * version 3 of the license.                                                            *
0011  *                                                                                      *
0012  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0013  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0014  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0015  *                                                                                      *
0016  * You should have received a copy of the GNU General Public License along with         *
0017  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0018  ****************************************************************************************/
0019 
0020 #define DEBUG_PREFIX "CollectionTreeView"
0021 
0022 #include "CollectionTreeView.h"
0023 
0024 #include "AmarokMimeData.h"
0025 #include "GlobalCollectionActions.h"
0026 #include "PopupDropperFactory.h"
0027 #include "SvgHandler.h"
0028 #include "browsers/CollectionSortFilterProxyModel.h"
0029 #include "browsers/CollectionTreeItemModel.h"
0030 #include "context/ContextView.h"
0031 #include "core/capabilities/ActionsCapability.h"
0032 #include "core/capabilities/BookmarkThisCapability.h"
0033 #include "core/collections/CollectionLocation.h"
0034 #include "core/collections/MetaQueryMaker.h"
0035 #include "core/collections/QueryMaker.h"
0036 #include "core/meta/Meta.h"
0037 #include "core/support/Amarok.h"
0038 #include "core/support/Debug.h"
0039 #include "core-impl/collections/support/CollectionManager.h"
0040 #include "core-impl/collections/support/TextualQueryFilter.h"
0041 #include "core-impl/collections/support/TrashCollectionLocation.h"
0042 #include "dialogs/TagDialog.h"
0043 #include "playlist/PlaylistModelStack.h"
0044 #include "scripting/scriptengine/AmarokCollectionViewScript.h"
0045 
0046 #include <QAction>
0047 #include <QIcon>
0048 #include <QComboBox>
0049 #include <QMenu>
0050 
0051 #include <QContextMenuEvent>
0052 #include <QHash>
0053 #include <QMouseEvent>
0054 #include <QQueue>
0055 #include <QSortFilterProxyModel>
0056 #include <QScrollBar>
0057 
0058 #include <algorithm>
0059 
0060 using namespace Collections;
0061 
0062 /**
0063  * RAII class to perform restoring of the scroll position once all queries are
0064  * finished. DelayedScroller auto-deletes itself once its job is over (ot if it finds
0065  * it is useless).
0066  */
0067 class DelayedScroller : public QObject
0068 {
0069     Q_OBJECT
0070 
0071     public:
0072         DelayedScroller( CollectionTreeView *treeView,
0073                          CollectionTreeItemModelBase *treeModel,
0074                          const QModelIndex &treeModelScrollToIndex, int topOffset )
0075             : QObject( treeView )
0076             , m_treeView( treeView )
0077             , m_treeModel( treeModel )
0078             , m_topOffset( topOffset )
0079         {
0080             connect( treeModel, &CollectionTreeItemModelBase::destroyed,
0081                      this, &DelayedScroller::deleteLater );
0082             connect( treeModel, &CollectionTreeItemModelBase::allQueriesFinished,
0083                      this, &DelayedScroller::slotScroll );
0084 
0085             m_scrollToItem = m_treeModel->treeItem( treeModelScrollToIndex );
0086             if( m_scrollToItem )
0087                 connect( m_scrollToItem, &CollectionTreeItem::destroyed, this, &DelayedScroller::deleteLater );
0088             else
0089                 deleteLater(); // nothing to do
0090         }
0091 
0092     private Q_SLOTS:
0093         void slotScroll()
0094         {
0095             deleteLater();
0096             QModelIndex idx = m_treeModel->itemIndex( m_scrollToItem );
0097             QSortFilterProxyModel *filterModel = m_treeView->filterModel();
0098             idx = filterModel ? filterModel->mapFromSource( idx ) : QModelIndex();
0099             QScrollBar *scrollBar = m_treeView->verticalScrollBar();
0100             if( !idx.isValid() || !scrollBar )
0101                 return;
0102 
0103             int newTopOffset = m_treeView->visualRect( idx ).top();
0104             scrollBar->setValue( scrollBar->value() + (newTopOffset - m_topOffset) );
0105         }
0106 
0107     private:
0108         CollectionTreeView *m_treeView;
0109         CollectionTreeItemModelBase *m_treeModel;
0110         CollectionTreeItem *m_scrollToItem;
0111         int m_topOffset;
0112 };
0113 
0114 /**
0115  * RAII class to auto-expand collection tree entries after filtering.
0116  * AutoExpander auto-deletes itself once its job is over (or if it finds
0117  * it is useless).
0118  */
0119 class AutoExpander : public QObject
0120 {
0121     Q_OBJECT
0122 
0123     public:
0124         AutoExpander( CollectionTreeView *treeView,
0125                       CollectionTreeItemModelBase *treeModel,
0126                       QAbstractItemModel *filterModel)
0127             : QObject( treeView )
0128             , m_treeView( treeView )
0129             , m_filterModel( filterModel )
0130         {
0131             connect( filterModel, &QObject::destroyed, this, &QObject::deleteLater );
0132             connect( treeModel, &CollectionTreeItemModelBase::allQueriesFinished, this, &AutoExpander::slotExpandMore );
0133 
0134             // start with the root index
0135             m_indicesToCheck.enqueue( QModelIndex() );
0136             slotExpandMore();
0137         }
0138 
0139     private Q_SLOTS:
0140         void slotExpandMore()
0141         {
0142             const int maxChildrenToExpand = 3;
0143 
0144             QQueue<QModelIndex> pendingIndices;
0145             while( !m_indicesToCheck.isEmpty() )
0146             {
0147                 if( !m_filterModel )
0148                     return;
0149 
0150                 QModelIndex current = m_indicesToCheck.dequeue();
0151 
0152                 if( m_filterModel->canFetchMore( current ) )
0153                 {
0154                     m_filterModel->fetchMore( current );
0155                     pendingIndices.enqueue( current );
0156                     continue;
0157                 }
0158 
0159                 if( m_filterModel->rowCount( current ) <= maxChildrenToExpand )
0160                 {
0161                     m_treeView->expand( current );
0162                     for( int i = 0; i < m_filterModel->rowCount( current ); i++ )
0163                         m_indicesToCheck.enqueue( m_filterModel->index( i, 0, current ) );
0164                 }
0165             }
0166 
0167             if( pendingIndices.isEmpty() )
0168                 // nothing left to do
0169                 deleteLater();
0170             else
0171                 // process pending indices when queries finish
0172                 m_indicesToCheck.swap( pendingIndices );
0173         }
0174 
0175     private:
0176         Q_DISABLE_COPY(AutoExpander)
0177 
0178         CollectionTreeView *m_treeView;
0179         QPointer<QAbstractItemModel> m_filterModel;
0180         QQueue<QModelIndex> m_indicesToCheck;
0181 };
0182 
0183 CollectionTreeView::CollectionTreeView( QWidget *parent)
0184     : Amarok::PrettyTreeView( parent )
0185     , m_filterModel( nullptr )
0186     , m_treeModel( nullptr )
0187     , m_pd( nullptr )
0188     , m_appendAction( nullptr )
0189     , m_loadAction( nullptr )
0190     , m_editAction( nullptr )
0191     , m_organizeAction( nullptr )
0192     , m_ongoingDrag( false )
0193 {
0194     setSortingEnabled( true );
0195     setFocusPolicy( Qt::StrongFocus );
0196     sortByColumn( 0, Qt::AscendingOrder );
0197     setSelectionMode( QAbstractItemView::ExtendedSelection );
0198     setSelectionBehavior( QAbstractItemView::SelectRows );
0199     setEditTriggers( EditKeyPressed );
0200 
0201     setDragDropMode( QAbstractItemView::DragDrop );
0202 
0203     connect( this, &CollectionTreeView::collapsed,
0204              this, &CollectionTreeView::slotCollapsed );
0205     connect( this, &CollectionTreeView::expanded,
0206              this, &CollectionTreeView::slotExpanded );
0207 }
0208 
0209 void
0210 CollectionTreeView::setModel( QAbstractItemModel *model )
0211 {
0212     if( m_treeModel )
0213         disconnect( m_treeModel, nullptr, this, nullptr);
0214 
0215     m_treeModel = qobject_cast<CollectionTreeItemModelBase *>( model );
0216     if( !m_treeModel )
0217         return;
0218 
0219     connect( m_treeModel, &CollectionTreeItemModelBase::allQueriesFinished,
0220              this, &CollectionTreeView::slotCheckAutoExpand );
0221     connect( m_treeModel, &CollectionTreeItemModelBase::expandIndex,
0222              this, &CollectionTreeView::slotExpandIndex );
0223 
0224     if( m_filterModel )
0225         m_filterModel->deleteLater();
0226     m_filterModel = new CollectionSortFilterProxyModel( this );
0227     m_filterModel->setSourceModel( model );
0228 
0229     QTreeView::setModel( m_filterModel );
0230 
0231     QTimer::singleShot( 0, this, &CollectionTreeView::slotCheckAutoExpandReally );
0232 }
0233 
0234 CollectionTreeView::~CollectionTreeView()
0235 {
0236     // we don't own m_treeModel pointer
0237     // m_filterModel will get deleted by QObject parentship
0238 }
0239 
0240 void
0241 CollectionTreeView::setLevels( const QList<CategoryId::CatMenuId> &levels )
0242 {
0243     if( m_treeModel )
0244         m_treeModel->setLevels( levels );
0245 }
0246 
0247 QList<CategoryId::CatMenuId>
0248 CollectionTreeView::levels() const
0249 {
0250     if( m_treeModel )
0251         return m_treeModel->levels();
0252     return QList<CategoryId::CatMenuId>();
0253 }
0254 
0255 void
0256 CollectionTreeView::setLevel( int level, CategoryId::CatMenuId type )
0257 {
0258     if( !m_treeModel )
0259         return;
0260     QList<CategoryId::CatMenuId> levels = m_treeModel->levels();
0261     if( type == CategoryId::None )
0262     {
0263         while( levels.count() >= level )
0264             levels.removeLast();
0265     }
0266     else
0267     {
0268         levels.removeAll( type );
0269         levels[level] = type;
0270     }
0271     setLevels( levels );
0272 }
0273 
0274 QSortFilterProxyModel *
0275 CollectionTreeView::filterModel() const
0276 {
0277     return m_filterModel;
0278 }
0279 
0280 void
0281 CollectionTreeView::contextMenuEvent( QContextMenuEvent *event )
0282 {
0283     if( !m_treeModel )
0284         return;
0285 
0286     QModelIndex index = indexAt( event->pos() );
0287     if( !index.isValid() )
0288     {
0289         Amarok::PrettyTreeView::contextMenuEvent( event );
0290         return;
0291     }
0292 
0293     QModelIndexList indices = selectedIndexes();
0294 
0295     // if previously selected indices do not contain the index of the item
0296     // currently under the mouse when context menu is invoked.
0297     if( !indices.contains( index ) )
0298     {
0299         indices.clear();
0300         indices << index;
0301         setCurrentIndex( index );
0302     }
0303 
0304     //TODO: get rid of this, it's a hack.
0305     // Put remove actions in model so we don't need access to the internal pointer in view
0306     if( m_filterModel )
0307     {
0308         QModelIndexList tmp;
0309         foreach( const QModelIndex &idx, indices )
0310         {
0311             tmp.append( m_filterModel->mapToSource( idx ) );
0312         }
0313         indices = tmp;
0314     }
0315 
0316     // Abort if nothing is selected
0317     if( indices.isEmpty() )
0318         return;
0319 
0320     m_currentItems.clear();
0321     foreach( const QModelIndex &index, indices )
0322     {
0323         if( index.isValid() && index.internalPointer() )
0324             m_currentItems.insert(
0325                             static_cast<CollectionTreeItem *>( index.internalPointer() )
0326                         );
0327     }
0328 
0329     QMenu menu( this );
0330 
0331     // Destroy the menu when the model is reset (collection update), so that we don't
0332     // operate on invalid data. see BUG 190056
0333     connect( m_treeModel, &CollectionTreeItemModelBase::modelReset, &menu, &QMenu::deleteLater );
0334 
0335     // create basic actions
0336     QActionList actions = createBasicActions( indices );
0337     foreach( QAction *action, actions ) {
0338         menu.addAction( action );
0339     }
0340     menu.addSeparator();
0341     actions.clear();
0342 
0343     QActionList albumActions = createCustomActions( indices );
0344     QMenu menuAlbum( i18n( "Album" )  );
0345     foreach( QAction *action, albumActions )
0346     {
0347         if( !action->parent() )
0348             action->setParent( &menuAlbum );
0349     }
0350 
0351     if( albumActions.count() > 1 )
0352     {
0353         menuAlbum.addActions( albumActions );
0354         menuAlbum.setIcon( QIcon::fromTheme( QStringLiteral("filename-album-amarok") ) );
0355         menu.addMenu( &menuAlbum );
0356         menu.addSeparator();
0357     }
0358     else if( albumActions.count() == 1 )
0359     {
0360         menu.addActions( albumActions );
0361     }
0362 
0363     QActionList collectionActions = createCollectionActions( indices );
0364     QMenu menuCollection( i18n( "Collection" ) );
0365     foreach( QAction *action, collectionActions )
0366     {
0367         if( !action->parent() )
0368             action->setParent( &menuCollection );
0369     }
0370 
0371     if( collectionActions.count() > 1 )
0372     {
0373         menuCollection.setIcon( QIcon::fromTheme( QStringLiteral("drive-harddisk") ) );
0374         menuCollection.addActions( collectionActions );
0375         menu.addMenu( &menuCollection );
0376         menu.addSeparator();
0377     }
0378     else if( collectionActions.count() == 1 )
0379     {
0380         menu.addActions( collectionActions );
0381     }
0382 
0383     m_currentCopyDestination = getCopyActions( indices );
0384     m_currentMoveDestination = getMoveActions( indices );
0385 
0386     if( !m_currentCopyDestination.empty() )
0387     {
0388         QMenu *copyMenu = new QMenu( i18n( "Copy to Collection" ), &menu );
0389         copyMenu->setIcon( QIcon::fromTheme( QStringLiteral("edit-copy") ) );
0390         copyMenu->addActions( m_currentCopyDestination.keys() );
0391         menu.addMenu( copyMenu );
0392     }
0393 
0394     //Move = copy + delete from source
0395     if( !m_currentMoveDestination.empty() )
0396     {
0397         QMenu *moveMenu = new QMenu( i18n( "Move to Collection" ), &menu );
0398         moveMenu->setIcon( QIcon::fromTheme( QStringLiteral("go-jump") ) );
0399         moveMenu->addActions( m_currentMoveDestination.keys() );
0400         menu.addMenu( moveMenu );
0401     }
0402 
0403     // create trash and delete actions
0404     if( onlyOneCollection( indices ) )
0405     {
0406         Collection *collection = getCollection( indices.first() );
0407         if( collection && collection->isWritable() )
0408         {
0409             //TODO: don't recreate action
0410             QAction *trashAction = new QAction( QIcon::fromTheme( QStringLiteral("user-trash") ),
0411                                                 i18n( "Move Tracks to Trash" ),
0412                                                 &menu );
0413             trashAction->setProperty( "popupdropper_svg_id", "delete" );
0414             // key shortcut is only for display purposes here, actual one is
0415             // determined by View in Model/View classes
0416             trashAction->setShortcut( Qt::Key_Delete );
0417             connect( trashAction, &QAction::triggered,
0418                      this, &CollectionTreeView::slotTrashTracks );
0419             menu.addAction( trashAction );
0420 
0421             QAction *deleteAction = new QAction( QIcon::fromTheme( QStringLiteral("remove-amarok") ),
0422                                                  i18n( "Delete Tracks" ),
0423                                                  &menu );
0424             deleteAction->setProperty( "popupdropper_svg_id", "delete" );
0425             // key shortcut is only for display purposes here, actual one is
0426             // determined by View in Model/View classes
0427             deleteAction->setShortcut( Qt::SHIFT + Qt::Key_Delete );
0428             connect( deleteAction, &QAction::triggered, this, &CollectionTreeView::slotDeleteTracks );
0429             menu.addAction( deleteAction );
0430         }
0431     }
0432 
0433     // add extended actions
0434     menu.addSeparator();
0435     actions += createExtendedActions( indices );
0436     foreach( QAction *action, actions ) {
0437         menu.addAction( action );
0438     }
0439     AmarokScript::AmarokCollectionViewScript::createScriptedActions( menu, indices );
0440 
0441     menu.exec( event->globalPos() );
0442 }
0443 
0444 void
0445 CollectionTreeView::mouseDoubleClickEvent( QMouseEvent *event )
0446 {
0447     if( event->button() == Qt::MidButton )
0448     {
0449         event->accept();
0450         return;
0451     }
0452 
0453     QModelIndex index = indexAt( event->pos() );
0454     if( !index.isValid() )
0455     {
0456         event->accept();
0457         return;
0458     }
0459 
0460     // code copied in PlaylistBrowserView::mouseDoubleClickEvent(), keep in sync
0461     // mind bug 279513
0462     bool isExpandable = model()->hasChildren( index );
0463     bool wouldExpand = !visualRect( index ).contains( event->pos() ) || // clicked outside item, perhaps on expander icon
0464                        ( isExpandable && !style()->styleHint( QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this ) ); // we're in doubleClick
0465     if( event->button() == Qt::LeftButton &&
0466         event->modifiers() == Qt::NoModifier &&
0467         !wouldExpand )
0468     {
0469         CollectionTreeItem *item = getItemFromIndex( index );
0470         playChildTracks( item, Playlist::OnDoubleClickOnSelectedItems );
0471         event->accept();
0472         return;
0473     }
0474 
0475     PrettyTreeView::mouseDoubleClickEvent( event );
0476 }
0477 
0478 void
0479 CollectionTreeView::mouseReleaseEvent( QMouseEvent *event )
0480 {
0481     if( m_pd )
0482     {
0483         connect( m_pd, &PopupDropper::fadeHideFinished, m_pd, &PopupDropper::deleteLater );
0484         m_pd->hide();
0485         m_pd = nullptr;
0486     }
0487 
0488     QModelIndex index = indexAt( event->pos() );
0489     if( !index.isValid() )
0490     {
0491         PrettyTreeView::mouseReleaseEvent( event );
0492         return;
0493     }
0494 
0495     if( event->button() == Qt::MidButton )
0496     {
0497         CollectionTreeItem *item = getItemFromIndex( index );
0498         playChildTracks( item, Playlist::OnMiddleClickOnSelectedItems );
0499         event->accept();
0500         return;
0501     }
0502 
0503     PrettyTreeView::mouseReleaseEvent( event );
0504 }
0505 
0506 CollectionTreeItem *
0507 CollectionTreeView::getItemFromIndex( QModelIndex &index )
0508 {
0509     QModelIndex filteredIndex;
0510     if( m_filterModel )
0511         filteredIndex = m_filterModel->mapToSource( index );
0512     else
0513         filteredIndex = index;
0514 
0515     if( !filteredIndex.isValid() )
0516     {
0517         return nullptr;
0518     }
0519 
0520     return static_cast<CollectionTreeItem *>( filteredIndex.internalPointer() );
0521 }
0522 
0523 void
0524 CollectionTreeView::keyPressEvent( QKeyEvent *event )
0525 {
0526     QModelIndexList indices = selectedIndexes();
0527     if( indices.isEmpty() )
0528     {
0529         Amarok::PrettyTreeView::keyPressEvent( event );
0530         return;
0531     }
0532 
0533     if( m_filterModel )
0534     {
0535         QModelIndexList tmp;
0536         foreach( const QModelIndex &idx, indices )
0537             tmp.append( m_filterModel->mapToSource( idx ) );
0538         indices = tmp;
0539     }
0540 
0541     m_currentItems.clear();
0542     foreach( const QModelIndex &index, indices )
0543     {
0544         if( index.isValid() && index.internalPointer() )
0545         {
0546             m_currentItems.insert(
0547                         static_cast<CollectionTreeItem *>( index.internalPointer() ) );
0548         }
0549     }
0550 
0551     QModelIndex current = currentIndex();
0552     switch( event->key() )
0553     {
0554         case Qt::Key_Enter:
0555         case Qt::Key_Return:
0556             playChildTracks( m_currentItems, Playlist::OnReturnPressedOnSelectedItems );
0557             return;
0558         case Qt::Key_Delete:
0559             if( !onlyOneCollection( indices ) )
0560                 break;
0561             removeTracks( m_currentItems, !( event->modifiers() & Qt::ShiftModifier ) );
0562             return;
0563         case Qt::Key_Up:
0564             if( current.parent() == QModelIndex() && current.row() == 0 )
0565             {
0566                 Q_EMIT leavingTree();
0567                 return;
0568             }
0569             break;
0570         case Qt::Key_Down:
0571             break;
0572         // L and R should magically work when we get a patched version of qt
0573         case Qt::Key_Right:
0574         case Qt::Key_Direction_R:
0575             expand( current );
0576             return;
0577         case Qt::Key_Left:
0578         case Qt::Key_Direction_L:
0579             collapse( current );
0580             return;
0581         default:
0582             break;
0583     }
0584     Amarok::PrettyTreeView::keyPressEvent( event );
0585 }
0586 
0587 void
0588 CollectionTreeView::dragEnterEvent( QDragEnterEvent *event )
0589 {
0590     // We want to indicate to the user that dropping to the same collection is not possible.
0591     // CollectionTreeItemModel therefore needs to know what collection the drag originated
0592     // so that is can play with Qt::ItemIsDropEnabled in flags()
0593     const AmarokMimeData *mimeData =
0594             qobject_cast<const AmarokMimeData *>( event->mimeData() );
0595     if( mimeData ) // drag from within Amarok
0596     {
0597         QSet<Collection *> srcCollections;
0598         foreach( Meta::TrackPtr track, mimeData->tracks() )
0599         {
0600             srcCollections.insert( track->collection() );
0601         }
0602         m_treeModel->setDragSourceCollections( srcCollections );
0603     }
0604     QAbstractItemView::dragEnterEvent( event );
0605 }
0606 
0607 void
0608 CollectionTreeView::dragMoveEvent( QDragMoveEvent *event )
0609 {
0610     // this mangling is not needed for Copy/Move distinction to work, it is only needed
0611     // for mouse cursor changing to work
0612     if( (event->keyboardModifiers() & Qt::ShiftModifier)
0613         && (event->possibleActions() & Qt::MoveAction) )
0614     {
0615         event->setDropAction( Qt::MoveAction );
0616     }
0617     else if( event->possibleActions() & Qt::CopyAction )
0618     {
0619         event->setDropAction( Qt::CopyAction );
0620     }
0621 
0622     QTreeView::dragMoveEvent( event );
0623 }
0624 
0625 void
0626 CollectionTreeView::startDrag(Qt::DropActions supportedActions)
0627 {
0628     DEBUG_BLOCK
0629 
0630     // Make sure that the left mouse button is actually pressed. Otherwise we're prone to
0631     // mis-detecting clicks as dragging
0632     if( !( QApplication::mouseButtons() & Qt::LeftButton ) )
0633         return;
0634 
0635     QModelIndexList indices = selectedIndexes();
0636     if( indices.isEmpty() )
0637         return;
0638 
0639     // When a parent item is dragged, startDrag() is called a bunch of times. Here we
0640     // prevent that:
0641     if( m_ongoingDrag )
0642         return;
0643     m_ongoingDrag = true;
0644 
0645     if( !m_pd )
0646         m_pd = The::popupDropperFactory()->createPopupDropper( Context::ContextView::self() );
0647 
0648     if( m_pd && m_pd->isHidden() )
0649     {
0650         if( m_filterModel )
0651         {
0652             QModelIndexList tmp;
0653             foreach( const QModelIndex &idx, indices )
0654             {
0655                 tmp.append( m_filterModel->mapToSource( idx ) );
0656             }
0657             indices = tmp;
0658         }
0659 
0660         QActionList actions = createBasicActions( indices );
0661 
0662         QFont font;
0663         font.setPointSize( 16 );
0664         font.setBold( true );
0665 
0666         foreach( QAction * action, actions )
0667             m_pd->addItem( The::popupDropperFactory()->createItem( action ) );
0668 
0669         m_currentCopyDestination = getCopyActions( indices );
0670         m_currentMoveDestination = getMoveActions( indices );
0671 
0672         m_currentItems.clear();
0673         foreach( const QModelIndex &index, indices )
0674         {
0675             if( index.isValid() && index.internalPointer() )
0676             {
0677                 m_currentItems.insert(
0678                         static_cast<CollectionTreeItem *>( index.internalPointer() ) );
0679             }
0680         }
0681 
0682         PopupDropperItem *subItem;
0683 
0684         actions = createExtendedActions( indices );
0685 
0686         PopupDropper *morePud = nullptr;
0687         if( actions.count() > 1 )
0688         {
0689             morePud = The::popupDropperFactory()->createPopupDropper( nullptr, true );
0690 
0691             foreach( QAction *action, actions )
0692                 morePud->addItem( The::popupDropperFactory()->createItem( action ) );
0693         }
0694         else
0695             m_pd->addItem( The::popupDropperFactory()->createItem( actions[0] ) );
0696 
0697         //TODO: Keep bugging i18n team about problems with 3 dots
0698         if ( actions.count() > 1 )
0699         {
0700             subItem = m_pd->addSubmenu( &morePud, i18n( "More..." )  );
0701             The::popupDropperFactory()->adjustItem( subItem );
0702         }
0703 
0704         m_pd->show();
0705     }
0706 
0707     QTreeView::startDrag( supportedActions );
0708     debug() << "After the drag!";
0709 
0710     if( m_pd )
0711     {
0712         debug() << "clearing PUD";
0713         connect( m_pd, &PopupDropper::fadeHideFinished, m_pd, &PopupDropper::clear );
0714         m_pd->hide();
0715     }
0716 
0717     m_ongoingDrag = false;
0718 }
0719 
0720 void
0721 CollectionTreeView::selectionChanged( const QItemSelection &selected,
0722                                       const QItemSelection &deselected )
0723 {
0724     QModelIndexList indexes = selected.indexes();
0725 
0726     QModelIndexList changedIndexes = indexes;
0727     changedIndexes << deselected.indexes();
0728     foreach( const QModelIndex &index, changedIndexes )
0729         update( index );
0730 
0731     if( indexes.count() < 1 )
0732         return;
0733 
0734     QModelIndex index;
0735     if( m_filterModel )
0736         index = m_filterModel->mapToSource( indexes[0] );
0737     else
0738         index = indexes[0];
0739 
0740     CollectionTreeItem *item =
0741             static_cast<CollectionTreeItem *>( index.internalPointer() );
0742     Q_EMIT( itemSelected ( item ) );
0743 }
0744 
0745 void
0746 CollectionTreeView::slotCollapsed( const QModelIndex &index )
0747 {
0748     if( !m_treeModel )
0749         return;
0750     if( m_filterModel )
0751         m_treeModel->slotCollapsed( m_filterModel->mapToSource( index ) );
0752     else
0753         m_treeModel->slotCollapsed( index );
0754 }
0755 
0756 void
0757 CollectionTreeView::slotExpanded( const QModelIndex &index )
0758 {
0759     if( !m_treeModel )
0760         return;
0761     if( m_filterModel )
0762         m_treeModel->slotExpanded( m_filterModel->mapToSource( index ));
0763     else
0764         m_treeModel->slotExpanded( index );
0765 }
0766 
0767 void
0768 CollectionTreeView::slotExpandIndex( const QModelIndex &index )
0769 {
0770     if( !m_treeModel )
0771         return;
0772     if( m_filterModel )
0773         expand( m_filterModel->mapFromSource( index ) );
0774 }
0775 
0776 void
0777 CollectionTreeView::slotCheckAutoExpand( bool reallyExpand )
0778 {
0779     if( !m_filterModel || !reallyExpand )
0780         return;
0781 
0782     // auto-deletes itself:
0783     new AutoExpander( this, m_treeModel, m_filterModel );
0784 }
0785 
0786 void
0787 CollectionTreeView::playChildTracks( CollectionTreeItem *item, Playlist::AddOptions insertMode )
0788 {
0789     QSet<CollectionTreeItem*> items;
0790     items.insert( item );
0791 
0792     playChildTracks( items, insertMode );
0793 }
0794 
0795 void
0796 CollectionTreeView::playChildTracks( const QSet<CollectionTreeItem *> &items,
0797                                      Playlist::AddOptions insertMode )
0798 {
0799     if( !m_treeModel )
0800         return;
0801     //Ensure that if a parent and child are both selected we ignore the child
0802     QSet<CollectionTreeItem *> parents( cleanItemSet( items ) );
0803 
0804     //Store the type of playlist insert to be done and cause a slot to be invoked when the tracklist has been generated.
0805     AmarokMimeData *mime = dynamic_cast<AmarokMimeData*>(
0806                 m_treeModel->mimeData( QList<CollectionTreeItem *>( parents.begin(), parents.end() ) ) );
0807     m_playChildTracksMode.insert( mime, insertMode );
0808     connect( mime, &AmarokMimeData::trackListSignal,
0809              this, &CollectionTreeView::playChildTracksSlot );
0810     mime->getTrackListSignal();
0811 }
0812 
0813 void
0814 CollectionTreeView::playChildTracksSlot( Meta::TrackList list ) //slot
0815 {
0816     AmarokMimeData *mime = dynamic_cast<AmarokMimeData *>( sender() );
0817 
0818     Playlist::AddOptions insertMode = m_playChildTracksMode.take( mime );
0819 
0820     std::stable_sort( list.begin(), list.end(), Meta::Track::lessThan );
0821     The::playlistController()->insertOptioned( list, insertMode );
0822 
0823     mime->deleteLater();
0824 }
0825 
0826 void
0827 CollectionTreeView::organizeTracks( const QSet<CollectionTreeItem *> &items ) const
0828 {
0829     DEBUG_BLOCK
0830     if( !items.count() )
0831         return;
0832 
0833     //Create query based upon items, ensuring that if a parent and child are both
0834     //selected we ignore the child
0835     Collections::QueryMaker *qm = createMetaQueryFromItems( items, true );
0836     if( !qm )
0837         return;
0838 
0839     CollectionTreeItem *item = items.values().first();
0840     while( item->isDataItem() )
0841         item = item->parent();
0842 
0843     Collection *coll = item->parentCollection();
0844     CollectionLocation *location = coll->location();
0845     if( !location->isOrganizable() )
0846     {
0847         debug() << "Collection not organizable";
0848         //how did we get here??
0849         delete location;
0850         delete qm;
0851         return;
0852     }
0853     location->prepareMove( qm, coll->location() );
0854 }
0855 
0856 void
0857 CollectionTreeView::copySelectedToLocalCollection()
0858 {
0859     DEBUG_BLOCK
0860 
0861     // Get the local collection
0862     Collections::Collection *collection = nullptr;
0863     const QList<Collections::Collection*> collections = CollectionManager::instance()->collections().keys();
0864 
0865     foreach( collection, collections )
0866     {
0867         if ( collection->collectionId() == QLatin1String("localCollection") )
0868             break;
0869     }
0870 
0871     if( !collection )
0872         return;
0873 
0874     // Get selected items
0875     QModelIndexList indexes = selectedIndexes();
0876     if( m_filterModel )
0877     {
0878         QModelIndexList tmp;
0879         foreach( const QModelIndex &idx, indexes )
0880             tmp.append( m_filterModel->mapToSource( idx ) );
0881         indexes = tmp;
0882     }
0883 
0884     m_currentItems.clear();
0885     foreach( const QModelIndex &index, indexes )
0886     {
0887         if( index.isValid() && index.internalPointer() )
0888             m_currentItems.insert( static_cast<CollectionTreeItem *>( index.internalPointer() ) );
0889     }
0890 
0891     copyTracks( m_currentItems, collection, false );
0892 }
0893 
0894 void
0895 CollectionTreeView::copyTracks( const QSet<CollectionTreeItem *> &items,
0896                                 Collection *destination, bool removeSources ) const
0897 {
0898     DEBUG_BLOCK
0899 
0900     if( !destination )
0901     {
0902         warning() << "collection is not writable (0-pointer)! Aborting";
0903         return;
0904     }
0905     if( !destination->isWritable() )
0906     {
0907         warning() << "collection " << destination->prettyName() << " is not writable! Aborting";
0908         return;
0909     }
0910     //copied from organizeTracks. create a method for this somewhere
0911     if( !items.count() )
0912     {
0913         warning() << "No items to copy! Aborting";
0914         return;
0915     }
0916 
0917     //Create query based upon items, ensuring that if a parent and child are both selected we ignore the child
0918     Collections::QueryMaker *qm = createMetaQueryFromItems( items, true );
0919     if( !qm )
0920     {
0921         warning() << "could not get qm!";
0922         return;
0923     }
0924 
0925     CollectionTreeItem *item = items.values().first();
0926     while( item->isDataItem() )
0927     {
0928         item = item->parent();
0929     }
0930     Collection *coll = item->parentCollection();
0931     CollectionLocation *source = coll->location();
0932     CollectionLocation *dest = destination->location();
0933     if( removeSources )
0934     {
0935         if( !source->isWritable() ) //error
0936         {
0937             warning() << "We can not write to ze source!!! OMGooses!";
0938             delete dest;
0939             delete source;
0940             delete qm;
0941             return;
0942         }
0943 
0944         debug() << "starting source->prepareMove";
0945         source->prepareMove( qm, dest );
0946     }
0947     else
0948     {
0949         debug() << "starting source->prepareCopy";
0950         source->prepareCopy( qm, dest );
0951     }
0952 }
0953 
0954 void
0955 CollectionTreeView::removeTracks( const QSet<CollectionTreeItem *> &items,
0956                                   bool useTrash ) const
0957 {
0958     DEBUG_BLOCK
0959 
0960     //copied from organizeTracks. create a method for this somewhere
0961     if( !items.count() )
0962         return;
0963 
0964     //Create query based upon items, ensuring that if a parent and child are both selected we ignore the child
0965     Collections::QueryMaker *qm = createMetaQueryFromItems( items, true );
0966     if( !qm )
0967         return;
0968 
0969     CollectionTreeItem *item = items.values().first();
0970     while( item->isDataItem() )
0971         item = item->parent();
0972     Collection *coll = item->parentCollection();
0973 
0974     CollectionLocation *source = coll->location();
0975     if( !source->isWritable() ) //error
0976     {
0977         warning() << "We can not write to ze source!!! OMGooses!";
0978         delete source;
0979         delete qm;
0980         return;
0981     }
0982 
0983     if( useTrash )
0984     {
0985         TrashCollectionLocation *trash = new TrashCollectionLocation();
0986         source->prepareMove( qm, trash );
0987     }
0988     else
0989         source->prepareRemove( qm );
0990 }
0991 
0992 void
0993 CollectionTreeView::editTracks( const QSet<CollectionTreeItem *> &items ) const
0994 {
0995     //Create query based upon items, ensuring that if a parent and child are both
0996     //selected we ignore the child
0997     Collections::QueryMaker *qm = createMetaQueryFromItems( items, true );
0998     if( !qm )
0999         return;
1000 
1001     (void)new TagDialog( qm ); //the dialog will show itself automatically as soon as it is ready
1002 }
1003 
1004 void
1005 CollectionTreeView::slotSetFilter( const QString &filter )
1006 {
1007     QString currentFilter = m_treeModel ? m_treeModel->currentFilter() : QString();
1008     if( !m_filterModel || !m_treeModel || filter == currentFilter )
1009         return;
1010 
1011     // special case: transitioning from non-empty to empty buffer
1012     // -> trigger later restoring of the scroll position
1013     if( filter.isEmpty() ) // currentFilter must not be empty then (see earlier check)
1014     {
1015         // take first item, descending to leaf ones if expanded. There may be better
1016         // ways to determine what item should stay "fixed".
1017         QModelIndex scrollToIndex = m_filterModel->index( 0, 0 );
1018         while( isExpanded( scrollToIndex ) && m_filterModel->rowCount( scrollToIndex ) > 0 )
1019             scrollToIndex = m_filterModel->index( 0, 0, scrollToIndex);
1020         int topOffset = visualRect( scrollToIndex ).top();
1021 
1022         QModelIndex bottomIndex = m_filterModel->mapToSource( scrollToIndex );
1023         // if we have somewhere to scroll to after filter is cleared...
1024         if( bottomIndex.isValid() )
1025             // auto-destroys itself
1026             new DelayedScroller( this, m_treeModel, bottomIndex, topOffset );
1027     }
1028     m_treeModel->setCurrentFilter( filter );
1029 }
1030 
1031 void
1032 CollectionTreeView::slotAddFilteredTracksToPlaylist()
1033 {
1034     if( !m_treeModel )
1035         return;
1036 
1037     // disconnect any possible earlier connection we've done
1038     disconnect( m_treeModel, &CollectionTreeItemModelBase::allQueriesFinished,
1039                 this, &CollectionTreeView::slotAddFilteredTracksToPlaylist );
1040 
1041     if( m_treeModel->hasRunningQueries() )
1042         // wait for the queries to finish
1043         connect( m_treeModel, &CollectionTreeItemModelBase::allQueriesFinished,
1044                  this, &CollectionTreeView::slotAddFilteredTracksToPlaylist );
1045     else
1046     {
1047         // yay, we can add the tracks now
1048         QSet<CollectionTreeItem *> items;
1049         for( int row = 0; row < m_treeModel->rowCount(); row++ )
1050         {
1051             QModelIndex idx = m_treeModel->index( row, 0 );
1052             CollectionTreeItem *item = idx.isValid()
1053                     ? static_cast<CollectionTreeItem *>( idx.internalPointer() ) : nullptr;
1054             if( item )
1055                 items.insert( item );
1056         }
1057         if( !items.isEmpty() )
1058             playChildTracks( items, Playlist::OnAppendToPlaylistAction );
1059         Q_EMIT addingFilteredTracksDone();
1060     }
1061 }
1062 
1063 QActionList
1064 CollectionTreeView::createBasicActions( const QModelIndexList &indices )
1065 {
1066     QActionList actions;
1067 
1068     if( !indices.isEmpty() )
1069     {
1070         if( m_appendAction == nullptr )
1071         {
1072             m_appendAction = new QAction( QIcon::fromTheme( QStringLiteral("media-track-add-amarok") ),
1073                                           i18n( "&Add to Playlist" ), this );
1074             m_appendAction->setProperty( "popupdropper_svg_id", "append" );
1075             connect( m_appendAction, &QAction::triggered, this, &CollectionTreeView::slotAppendChildTracks );
1076         }
1077 
1078         actions.append( m_appendAction );
1079 
1080         if( m_loadAction == nullptr )
1081         {
1082             m_loadAction = new QAction(
1083                         i18nc( "Replace the currently loaded tracks with these",
1084                                "&Replace Playlist" ), this );
1085             m_loadAction->setProperty( "popupdropper_svg_id", "load" );
1086             connect( m_loadAction, &QAction::triggered,
1087                      this, &CollectionTreeView::slotReplacePlaylistWithChildTracks );
1088         }
1089 
1090         actions.append( m_loadAction );
1091     }
1092 
1093     return actions;
1094 }
1095 
1096 QActionList
1097 CollectionTreeView::createExtendedActions( const QModelIndexList &indices )
1098 {
1099     QActionList actions;
1100 
1101     if( !indices.isEmpty() )
1102     {
1103         {   //keep the scope of item minimal
1104             CollectionTreeItem *item =
1105                     static_cast<CollectionTreeItem *>( indices.first().internalPointer() );
1106             while( item->isDataItem() )
1107                 item = item->parent();
1108 
1109             Collection *collection = item->parentCollection();
1110             CollectionLocation* location = collection->location();
1111 
1112             if( location->isOrganizable() )
1113             {
1114                 bool onlyOneCollection = true;
1115                 foreach( const QModelIndex &index, indices )
1116                 {
1117                     Q_UNUSED( index )
1118                     CollectionTreeItem *item = static_cast<CollectionTreeItem *>(
1119                                 indices.first().internalPointer() );
1120                     while( item->isDataItem() )
1121                         item = item->parent();
1122 
1123                     onlyOneCollection = item->parentCollection() == collection;
1124                     if( !onlyOneCollection )
1125                         break;
1126                 }
1127 
1128                 if( onlyOneCollection )
1129                 {
1130                     if( m_organizeAction == nullptr )
1131                     {
1132                         m_organizeAction = new QAction( QIcon::fromTheme(QStringLiteral("folder-open") ),
1133                                     i18nc( "Organize Files", "Organize Files" ), this );
1134                         m_organizeAction->setProperty( "popupdropper_svg_id", "organize" );
1135                         connect( m_organizeAction, &QAction::triggered,
1136                                  this, &CollectionTreeView::slotOrganize );
1137                     }
1138                     actions.append( m_organizeAction );
1139                 }
1140             }
1141             delete location;
1142         }
1143 
1144         //hmmm... figure out what kind of item we are dealing with....
1145 
1146         if( indices.size() == 1 )
1147         {
1148             debug() << "checking for global actions";
1149             CollectionTreeItem *item = static_cast<CollectionTreeItem *>(
1150                         indices.first().internalPointer() );
1151 
1152             QActionList gActions = The::globalCollectionActions()->actionsFor( item->data() );
1153             foreach( QAction *action, gActions )
1154             {
1155                 if( action ) // Can become 0-pointer, see https://bugs.kde.org/show_bug.cgi?id=183250
1156                 {
1157                     actions.append( action );
1158                     debug() << "Got global action: " << action->text();
1159                 }
1160             }
1161         }
1162 
1163         if( m_editAction == nullptr )
1164         {
1165             m_editAction = new QAction( QIcon::fromTheme( QStringLiteral("media-track-edit-amarok") ),
1166                                         i18n( "&Edit Track Details" ), this );
1167             setProperty( "popupdropper_svg_id", "edit" );
1168             connect( m_editAction, &QAction::triggered, this, &CollectionTreeView::slotEditTracks );
1169         }
1170         actions.append( m_editAction );
1171     }
1172     else
1173         debug() << "invalid index or null internalPointer";
1174 
1175     return actions;
1176 }
1177 
1178 QActionList
1179 CollectionTreeView::createCustomActions( const QModelIndexList &indices )
1180 {
1181     QActionList actions;
1182     if( indices.count() == 1 )
1183     {
1184         if( indices.first().isValid() && indices.first().internalPointer() )
1185         {
1186             Meta::DataPtr data = static_cast<CollectionTreeItem *>(
1187                         indices.first().internalPointer() )->data();
1188             if( data )
1189             {
1190                 QScopedPointer<Capabilities::ActionsCapability> ac(
1191                             data->create<Capabilities::ActionsCapability>() );
1192                 if( ac )
1193                 {
1194                     QActionList cActions = ac->actions();
1195 
1196                     foreach( QAction *action, cActions )
1197                     {
1198                         Q_ASSERT( action );
1199                         actions.append( action );
1200                         debug() << "Got custom action: " << action->text();
1201                     }
1202                 }
1203 
1204                 //check if this item can be bookmarked...
1205                 QScopedPointer<Capabilities::BookmarkThisCapability> btc(
1206                             data->create<Capabilities::BookmarkThisCapability>() );
1207                 if( btc && btc->isBookmarkable() && btc->bookmarkAction() )
1208                     actions.append( btc->bookmarkAction() );
1209             }
1210         }
1211     }
1212     return actions;
1213 }
1214 
1215 QActionList
1216 CollectionTreeView::createCollectionActions( const QModelIndexList &indices )
1217 {
1218     QActionList actions;
1219     // Extract collection whose constituent was selected
1220 
1221     CollectionTreeItem *item =
1222             static_cast<CollectionTreeItem *>( indices.first().internalPointer() );
1223 
1224     // Don't return any collection actions for non collection items
1225     if( item->isDataItem() )
1226         return actions;
1227 
1228     Collection *collection = item->parentCollection();
1229 
1230     // Generate CollectionCapability, test for existence
1231 
1232     QScopedPointer<Capabilities::ActionsCapability> cc(
1233                 collection->create<Capabilities::ActionsCapability>() );
1234 
1235     if( cc )
1236         actions = cc->actions();
1237 
1238     return actions;
1239 }
1240 
1241 
1242 QHash<QAction *, Collection *>
1243 CollectionTreeView::getCopyActions( const QModelIndexList &indices )
1244 {
1245     QHash<QAction *, Collection *> currentCopyDestination;
1246 
1247     if( onlyOneCollection( indices ) )
1248     {
1249         Collection *collection = getCollection( indices.first() );
1250         QList<Collection *> writableCollections;
1251         QHash<Collection *, CollectionManager::CollectionStatus> hash =
1252                 CollectionManager::instance()->collections();
1253         QHash<Collection *, CollectionManager::CollectionStatus>::const_iterator it =
1254                 hash.constBegin();
1255         while( it != hash.constEnd() )
1256         {
1257             Collection *coll = it.key();
1258             if( coll && coll->isWritable() && coll != collection )
1259                 writableCollections.append( coll );
1260             ++it;
1261         }
1262         if( !writableCollections.isEmpty() )
1263         {
1264             foreach( Collection *coll, writableCollections )
1265             {
1266                 QAction *action = new QAction( coll->icon(), coll->prettyName(), nullptr );
1267                 action->setProperty( "popupdropper_svg_id", "collection" );
1268                 connect( action, &QAction::triggered, this, &CollectionTreeView::slotCopyTracks );
1269 
1270                 currentCopyDestination.insert( action, coll );
1271             }
1272         }
1273     }
1274     return currentCopyDestination;
1275 }
1276 
1277 QHash<QAction *, Collection *>
1278 CollectionTreeView::getMoveActions( const QModelIndexList &indices )
1279 {
1280     QHash<QAction *, Collection *> currentMoveDestination;
1281 
1282     if( onlyOneCollection( indices ) )
1283     {
1284         Collection *collection = getCollection( indices.first() );
1285         QList<Collection *> writableCollections;
1286         QHash<Collection *, CollectionManager::CollectionStatus> hash =
1287                 CollectionManager::instance()->collections();
1288         QHash<Collection *, CollectionManager::CollectionStatus>::const_iterator it =
1289                 hash.constBegin();
1290         while( it != hash.constEnd() )
1291         {
1292             Collection *coll = it.key();
1293             if( coll && coll->isWritable() && coll != collection )
1294                 writableCollections.append( coll );
1295             ++it;
1296         }
1297         if( !writableCollections.isEmpty() )
1298         {
1299             if( collection->isWritable() )
1300             {
1301                 foreach( Collection *coll, writableCollections )
1302                 {
1303                     QAction *action = new QAction( coll->icon(), coll->prettyName(), nullptr );
1304                     action->setProperty( "popupdropper_svg_id", "collection" );
1305                     connect( action, &QAction::triggered, this, &CollectionTreeView::slotMoveTracks );
1306                     currentMoveDestination.insert( action, coll );
1307                 }
1308             }
1309         }
1310     }
1311     return currentMoveDestination;
1312 }
1313 
1314 bool CollectionTreeView::onlyOneCollection( const QModelIndexList &indices )
1315 {
1316     if( !indices.isEmpty() )
1317     {
1318         Collection *collection = getCollection( indices.first() );
1319         foreach( const QModelIndex &index, indices )
1320         {
1321             Collection *currentCollection = getCollection( index );
1322             if( collection != currentCollection )
1323                 return false;
1324         }
1325     }
1326 
1327     return true;
1328 }
1329 
1330 Collection *
1331 CollectionTreeView::getCollection( const QModelIndex &index )
1332 {
1333     Collection *collection = nullptr;
1334     if( index.isValid() )
1335     {
1336         CollectionTreeItem *item =
1337                 static_cast<CollectionTreeItem *>( index.internalPointer() );
1338         while( item->isDataItem() )
1339             item = item->parent();
1340         collection = item->parentCollection();
1341     }
1342 
1343     return collection;
1344 }
1345 
1346 void
1347 CollectionTreeView::slotReplacePlaylistWithChildTracks()
1348 {
1349     playChildTracks( m_currentItems, Playlist::OnReplacePlaylistAction );
1350 }
1351 
1352 void
1353 CollectionTreeView::slotAppendChildTracks()
1354 {
1355     playChildTracks( m_currentItems, Playlist::OnAppendToPlaylistAction );
1356 }
1357 
1358 void
1359 CollectionTreeView::slotQueueChildTracks()
1360 {
1361     playChildTracks( m_currentItems, Playlist::OnQueueToPlaylistAction );
1362 }
1363 
1364 void
1365 CollectionTreeView::slotEditTracks()
1366 {
1367     editTracks( m_currentItems );
1368 }
1369 
1370 void
1371 CollectionTreeView::slotCopyTracks()
1372 {
1373     if( !sender() )
1374         return;
1375     if( QAction *action = dynamic_cast<QAction *>( sender() ) )
1376         copyTracks( m_currentItems, m_currentCopyDestination[ action ], false );
1377 }
1378 
1379 void
1380 CollectionTreeView::slotMoveTracks()
1381 {
1382     if( !sender() )
1383         return;
1384     if ( QAction *action = dynamic_cast<QAction *>( sender() ) )
1385         copyTracks( m_currentItems, m_currentMoveDestination[ action ], true );
1386 }
1387 
1388 void
1389 CollectionTreeView::slotTrashTracks()
1390 {
1391     removeTracks( m_currentItems, true );
1392 }
1393 
1394 void
1395 CollectionTreeView::slotDeleteTracks()
1396 {
1397     removeTracks( m_currentItems, false /* do not use trash */ );
1398 }
1399 
1400 void
1401 CollectionTreeView::slotOrganize()
1402 {
1403     if( sender() )
1404     {
1405         if( QAction *action = dynamic_cast<QAction *>( sender() ) )
1406         {
1407             Q_UNUSED( action )
1408             organizeTracks( m_currentItems );
1409         }
1410     }
1411 }
1412 
1413 QSet<CollectionTreeItem *>
1414 CollectionTreeView::cleanItemSet( const QSet<CollectionTreeItem *> &items )
1415 {
1416     QSet<CollectionTreeItem *> parents;
1417     foreach( CollectionTreeItem *item, items )
1418     {
1419         CollectionTreeItem *tmpItem = item;
1420         while( tmpItem )
1421         {
1422             if( items.contains( tmpItem->parent() ) )
1423                 tmpItem = tmpItem->parent();
1424             else
1425             {
1426                 parents.insert( tmpItem );
1427                 break;
1428             }
1429         }
1430     }
1431     return parents;
1432 }
1433 
1434 Collections::QueryMaker *
1435 CollectionTreeView::createMetaQueryFromItems( const QSet<CollectionTreeItem *> &items,
1436                                               bool cleanItems ) const
1437 {
1438     if( !m_treeModel )
1439         return nullptr;
1440 
1441     QSet<CollectionTreeItem*> parents = cleanItems ? cleanItemSet( items ) : items;
1442 
1443     QList<Collections::QueryMaker *> queryMakers;
1444     foreach( CollectionTreeItem *item, parents )
1445     {
1446         Collections::QueryMaker *qm = item->queryMaker();
1447         for( CollectionTreeItem *tmp = item; tmp; tmp = tmp->parent() )
1448             tmp->addMatch( qm, m_treeModel->levelCategory( tmp->level() - 1 ) );
1449         Collections::addTextualFilter( qm, m_treeModel->currentFilter() );
1450         queryMakers.append( qm );
1451     }
1452     return new Collections::MetaQueryMaker( queryMakers );
1453 }
1454 
1455 #include "CollectionTreeView.moc"  // Q_OBJECTs defined in CollectionTreeView.cpp
1456 #include "moc_CollectionTreeView.cpp"  // Q_OBJECTs defined in CollectionTreeView.h