File indexing completed on 2024-05-19 04:48:38

0001 /****************************************************************************************
0002  * Copyright (c) 2010 Nikolaj Hald Nielsen <nhn@kde.org>                                *
0003  * Copyright (c) 2010 Casey Link <unnamedrambler@gmail.com>                             *
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) any later           *
0008  * version.                                                                             *
0009  *                                                                                      *
0010  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0011  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0012  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0013  *                                                                                      *
0014  * You should have received a copy of the GNU General Public License along with         *
0015  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0016  ****************************************************************************************/
0017 
0018 #define DEBUG_PREFIX "FileView"
0019 
0020 #include "FileView.h"
0021 
0022 #include "EngineController.h"
0023 #include "PaletteHandler.h"
0024 #include "PopupDropperFactory.h"
0025 #include "SvgHandler.h"
0026 #include "context/ContextView.h"
0027 #include "core/playlists/PlaylistFormat.h"
0028 #include "core/support/Debug.h"
0029 #include "core-impl/collections/support/CollectionManager.h"
0030 #include "core-impl/collections/support/FileCollectionLocation.h"
0031 #include "core-impl/meta/file/File.h"
0032 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
0033 #include "core-impl/support/TrackLoader.h"
0034 #include "dialogs/TagDialog.h"
0035 
0036 #include <QAction>
0037 #include <QContextMenuEvent>
0038 #include <QFileSystemModel>
0039 #include <QIcon>
0040 #include <QItemDelegate>
0041 #include <QMenu>
0042 #include <QPainter>
0043 #include <QUrl>
0044 
0045 #include <KConfigGroup>
0046 #include <KDirModel>
0047 #include <KFileItem>
0048 #include <KIO/CopyJob>
0049 #include <KIO/DeleteJob>
0050 #include <KLocalizedString>
0051 #include <KMessageBox>
0052 
0053 #include <algorithm>
0054 
0055 FileView::FileView( QWidget *parent )
0056     : Amarok::PrettyTreeView( parent )
0057     , m_appendAction( nullptr )
0058     , m_loadAction( nullptr )
0059     , m_editAction( nullptr )
0060     , m_moveToTrashAction( nullptr )
0061     , m_deleteAction( nullptr )
0062     , m_pd( nullptr )
0063     , m_ongoingDrag( false )
0064 {
0065     setFrameStyle( QFrame::NoFrame );
0066     setItemsExpandable( false );
0067     setRootIsDecorated( false );
0068     setAlternatingRowColors( true );
0069     setUniformRowHeights( true );
0070     setEditTriggers( EditKeyPressed );
0071 
0072     The::paletteHandler()->updateItemView( this );
0073     connect( The::paletteHandler(), &PaletteHandler::newPalette,
0074              this, &FileView::newPalette );
0075 }
0076 
0077 void
0078 FileView::contextMenuEvent( QContextMenuEvent *e )
0079 {
0080     if( !model() )
0081         return;
0082 
0083     //trying to do fancy stuff while showing places only leads to tears!
0084     if( model()->objectName() == "PLACESMODEL" )
0085     {
0086         e->accept();
0087         return;
0088     }
0089 
0090     QModelIndexList indices = selectedIndexes();
0091     // Abort if nothing is selected
0092     if( indices.isEmpty() )
0093         return;
0094 
0095     QMenu menu;
0096     foreach( QAction *action, actionsForIndices( indices, PlaylistAction ) )
0097         menu.addAction( action );
0098     menu.addSeparator();
0099 
0100     // Create Copy/Move to menu items
0101     // ported from old filebrowser
0102     QList<Collections::Collection*> writableCollections;
0103     QHash<Collections::Collection*, CollectionManager::CollectionStatus> hash =
0104             CollectionManager::instance()->collections();
0105     QHash<Collections::Collection*, CollectionManager::CollectionStatus>::const_iterator it =
0106             hash.constBegin();
0107     while( it != hash.constEnd() )
0108     {
0109         Collections::Collection *coll = it.key();
0110         if( coll && coll->isWritable() )
0111             writableCollections.append( coll );
0112         ++it;
0113     }
0114     if( !writableCollections.isEmpty() )
0115     {
0116         QMenu *copyMenu = new QMenu( i18n( ("Copy to Collection") ), &menu );
0117         copyMenu->setIcon( QIcon::fromTheme( QStringLiteral("edit-copy") ) );
0118         foreach( Collections::Collection *coll, writableCollections )
0119         {
0120             CollectionAction *copyAction = new CollectionAction( coll, &menu );
0121             connect( copyAction, &QAction::triggered, this, &FileView::slotPrepareCopyTracks );
0122             copyMenu->addAction( copyAction );
0123         }
0124         menu.addMenu( copyMenu );
0125 
0126         QMenu *moveMenu = new QMenu( i18n( "Move to Collection" ), &menu );
0127         moveMenu->setIcon( QIcon::fromTheme( QStringLiteral("go-jump") ) );
0128         foreach( Collections::Collection *coll, writableCollections )
0129         {
0130             CollectionAction *moveAction = new CollectionAction( coll, &menu );
0131             connect( moveAction, &QAction::triggered, this, &FileView::slotPrepareMoveTracks );
0132             moveMenu->addAction( moveAction );
0133         }
0134         menu.addMenu( moveMenu );
0135     }
0136     foreach( QAction *action, actionsForIndices( indices, OrganizeAction ) )
0137         menu.addAction( action );
0138     menu.addSeparator();
0139 
0140     foreach( QAction *action, actionsForIndices( indices, EditAction ) )
0141         menu.addAction( action );
0142 
0143     menu.exec( e->globalPos() );
0144 }
0145 
0146 void
0147 FileView::mouseReleaseEvent( QMouseEvent *event )
0148 {
0149     QModelIndex index = indexAt( event->pos() );
0150     if( !index.isValid() )
0151     {
0152         PrettyTreeView::mouseReleaseEvent( event );
0153         return;
0154     }
0155 
0156     if( state() == QAbstractItemView::NoState && event->button() == Qt::MidButton )
0157     {
0158         addIndexToPlaylist( index, Playlist::OnMiddleClickOnSelectedItems );
0159         event->accept();
0160         return;
0161     }
0162 
0163     KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
0164     if( state() == QAbstractItemView::NoState &&
0165         event->button() == Qt::LeftButton &&
0166         event->modifiers() == Qt::NoModifier &&
0167         style()->styleHint( QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this ) &&
0168         ( file.isDir() || file.isNull() ) )
0169     {
0170         Q_EMIT navigateToDirectory( index );
0171         event->accept();
0172         return;
0173     }
0174 
0175     PrettyTreeView::mouseReleaseEvent( event );
0176 }
0177 
0178 void
0179 FileView::mouseDoubleClickEvent( QMouseEvent *event )
0180 {
0181     QModelIndex index = indexAt( event->pos() );
0182     if( !index.isValid() )
0183     {
0184         event->accept();
0185         return;
0186     }
0187 
0188     // swallow middle-button double-clicks
0189     if( event->button() == Qt::MidButton )
0190     {
0191         event->accept();
0192         return;
0193     }
0194 
0195     if( event->button() == Qt::LeftButton )
0196     {
0197         KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
0198         QUrl url = file.url();
0199         if( !file.isNull() && ( Playlists::isPlaylist( url ) || MetaFile::Track::isTrack( url ) ) )
0200             addIndexToPlaylist( index, Playlist::OnDoubleClickOnSelectedItems );
0201         else
0202             Q_EMIT navigateToDirectory( index );
0203 
0204         event->accept();
0205         return;
0206     }
0207 
0208     PrettyTreeView::mouseDoubleClickEvent( event );
0209 }
0210 
0211 void
0212 FileView::keyPressEvent( QKeyEvent *event )
0213 {
0214     QModelIndex index = currentIndex();
0215     if( !index.isValid() )
0216         return;
0217 
0218     switch( event->key() )
0219     {
0220         case Qt::Key_Enter:
0221         case Qt::Key_Return:
0222         {
0223             KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
0224             QUrl url = file.url();
0225             if( !file.isNull() && ( Playlists::isPlaylist( url ) || MetaFile::Track::isTrack( url ) ) )
0226                 // right, we test the current item, but then add the selection to playlist
0227                 addSelectionToPlaylist( Playlist::OnReturnPressedOnSelectedItems );
0228             else
0229                 Q_EMIT navigateToDirectory( index );
0230 
0231             return;
0232         }
0233         case Qt::Key_Delete:
0234             slotMoveToTrash( Qt::NoButton, event->modifiers() );
0235             break;
0236         case Qt::Key_F5:
0237             Q_EMIT refreshBrowser();
0238             break;
0239         default:
0240             break;
0241     }
0242 
0243     QTreeView::keyPressEvent( event );
0244 }
0245 
0246 void
0247 FileView::slotAppendToPlaylist()
0248 {
0249     addSelectionToPlaylist( Playlist::OnAppendToPlaylistAction );
0250 }
0251 
0252 void
0253 FileView::slotReplacePlaylist()
0254 {
0255     addSelectionToPlaylist( Playlist::OnReplacePlaylistAction );
0256 }
0257 
0258 void
0259 FileView::slotEditTracks()
0260 {
0261     Meta::TrackList tracks = tracksForEdit();
0262     if( !tracks.isEmpty() )
0263     {
0264         TagDialog *dialog = new TagDialog( tracks, this );
0265         dialog->show();
0266     }
0267 }
0268 
0269 void
0270 FileView::slotPrepareMoveTracks()
0271 {
0272     if( m_moveDestinationCollection )
0273         return;
0274 
0275     CollectionAction *action = dynamic_cast<CollectionAction*>( sender() );
0276     if( !action )
0277         return;
0278 
0279     m_moveDestinationCollection = action->collection();
0280 
0281     const KFileItemList list = selectedItems();
0282     if( list.isEmpty() )
0283         return;
0284 
0285     // prevent bug 313003, require full metadata
0286     TrackLoader* dl = new TrackLoader( TrackLoader::FullMetadataRequired ); // auto-deletes itself
0287     connect( dl, &TrackLoader::finished, this, &FileView::slotMoveTracks );
0288     dl->init( list.urlList() );
0289 }
0290 
0291 void
0292 FileView::slotPrepareCopyTracks()
0293 {
0294     if( m_copyDestinationCollection )
0295         return;
0296 
0297     CollectionAction *action = dynamic_cast<CollectionAction*>( sender() );
0298     if( !action )
0299         return;
0300 
0301     m_copyDestinationCollection = action->collection();
0302 
0303     const KFileItemList list = selectedItems();
0304     if( list.isEmpty() )
0305         return;
0306 
0307     // prevent bug 313003, require full metadata
0308     TrackLoader* dl = new TrackLoader( TrackLoader::FullMetadataRequired ); // auto-deletes itself
0309     connect( dl, &TrackLoader::finished, this, &FileView::slotCopyTracks );
0310     dl->init( list.urlList() );
0311 }
0312 
0313 void
0314 FileView::slotCopyTracks( const Meta::TrackList& tracks )
0315 {
0316     if( !m_copyDestinationCollection )
0317         return;
0318 
0319     QSet<Collections::Collection *> collections;
0320     foreach( const Meta::TrackPtr &track, tracks )
0321     {
0322         collections.insert( track->collection() );
0323     }
0324 
0325     if( collections.count() == 1 )
0326     {
0327         Collections::Collection *sourceCollection = collections.values().first();
0328         Collections::CollectionLocation *source;
0329         if( sourceCollection )
0330             source = sourceCollection->location();
0331         else
0332             source = new Collections::FileCollectionLocation();
0333 
0334         Collections::CollectionLocation *destination = m_copyDestinationCollection->location();
0335         source->prepareCopy( tracks, destination );
0336     }
0337     else
0338         warning() << "Cannot handle copying tracks from multiple collections, doing nothing to be safe";
0339 
0340     m_copyDestinationCollection.clear();
0341 }
0342 
0343 void
0344 FileView::slotMoveTracks( const Meta::TrackList& tracks )
0345 {
0346     if( !m_moveDestinationCollection )
0347         return;
0348 
0349     QSet<Collections::Collection *> collections;
0350     foreach( const Meta::TrackPtr &track, tracks )
0351     {
0352         collections.insert( track->collection() );
0353     }
0354 
0355     if( collections.count() == 1 )
0356     {
0357         Collections::Collection *sourceCollection = collections.values().first();
0358         Collections::CollectionLocation *source;
0359         if( sourceCollection )
0360             source = sourceCollection->location();
0361         else
0362             source = new Collections::FileCollectionLocation();
0363 
0364         Collections::CollectionLocation *destination = m_moveDestinationCollection->location();
0365         source->prepareMove( tracks, destination );
0366     }
0367     else
0368         warning() << "Cannot handle moving tracks from multiple collections, doing nothing to be safe";
0369 
0370     m_moveDestinationCollection.clear();
0371 }
0372 
0373 QList<QAction *>
0374 FileView::actionsForIndices( const QModelIndexList &indices, ActionType type )
0375 {
0376     QList<QAction *> actions;
0377 
0378     if( indices.isEmpty() )
0379         return actions; // get out of here!
0380 
0381     if( !m_appendAction )
0382     {
0383         m_appendAction = new QAction( QIcon::fromTheme( "media-track-add-amarok" ), i18n( "&Add to Playlist" ),
0384                                       this );
0385         m_appendAction->setProperty( "popupdropper_svg_id", "append" );
0386         connect( m_appendAction, &QAction::triggered, this, &FileView::slotAppendToPlaylist );
0387     }
0388     if( type & PlaylistAction )
0389         actions.append( m_appendAction );
0390 
0391     if( !m_loadAction )
0392     {
0393         m_loadAction = new QAction( i18nc( "Replace the currently loaded tracks with these",
0394                                            "&Replace Playlist" ), this );
0395         m_loadAction->setProperty( "popupdropper_svg_id", "load" );
0396         connect( m_loadAction, &QAction::triggered, this, &FileView::slotReplacePlaylist );
0397     }
0398     if( type & PlaylistAction )
0399         actions.append( m_loadAction );
0400 
0401     if( !m_moveToTrashAction )
0402     {
0403         m_moveToTrashAction = new QAction( QIcon::fromTheme( "user-trash" ), i18n( "&Move to Trash" ), this );
0404         m_moveToTrashAction->setProperty( "popupdropper_svg_id", "delete_file" );
0405         // key shortcut is only for display purposes here, actual one is determined by View in Model/View classes
0406         m_moveToTrashAction->setShortcut( Qt::Key_Delete );
0407         connect( m_moveToTrashAction, &QAction::triggered, this, &FileView::slotMoveToTrashWithoutModifiers );
0408     }
0409     if( type & OrganizeAction )
0410         actions.append( m_moveToTrashAction );
0411 
0412     if( !m_deleteAction )
0413     {
0414         m_deleteAction = new QAction( QIcon::fromTheme( "remove-amarok" ), i18n( "&Delete" ), this );
0415         m_deleteAction->setProperty( "popupdropper_svg_id", "delete_file" );
0416         // key shortcut is only for display purposes here, actual one is determined by View in Model/View classes
0417         m_deleteAction->setShortcut( Qt::SHIFT + Qt::Key_Delete );
0418         connect( m_deleteAction, &QAction::triggered, this, &FileView::slotDelete );
0419     }
0420     if( type & OrganizeAction )
0421         actions.append( m_deleteAction );
0422 
0423     if( !m_editAction )
0424     {
0425         m_editAction = new QAction( QIcon::fromTheme( "media-track-edit-amarok" ),
0426                                     i18n( "&Edit Track Details" ), this );
0427         m_editAction->setProperty( "popupdropper_svg_id", "edit" );
0428         connect( m_editAction, &QAction::triggered, this, &FileView::slotEditTracks );
0429     }
0430     if( type & EditAction )
0431     {
0432         actions.append( m_editAction );
0433         Meta::TrackList tracks = tracksForEdit();
0434         m_editAction->setVisible( !tracks.isEmpty() );
0435     }
0436 
0437     return actions;
0438 }
0439 
0440 void
0441 FileView::addSelectionToPlaylist( Playlist::AddOptions options )
0442 {
0443     addIndicesToPlaylist( selectedIndexes(), options );
0444 }
0445 
0446 void
0447 FileView::addIndexToPlaylist( const QModelIndex &idx, Playlist::AddOptions options )
0448 {
0449     addIndicesToPlaylist( QModelIndexList() << idx, options );
0450 }
0451 
0452 void
0453 FileView::addIndicesToPlaylist( QModelIndexList indices, Playlist::AddOptions options )
0454 {
0455     if( indices.isEmpty() )
0456         return;
0457 
0458     // let tracks & playlists appear in playlist as they are shown in the view:
0459     std::sort( indices.begin(), indices.end() );
0460 
0461     QList<QUrl> urls;
0462     foreach( const QModelIndex &index, indices )
0463     {
0464         KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
0465         QUrl url = file.url();
0466         if( file.isDir() || Playlists::isPlaylist( url ) || MetaFile::Track::isTrack( url ) )
0467         {
0468             urls << file.url();
0469         }
0470     }
0471 
0472     The::playlistController()->insertOptioned( urls, options );
0473 }
0474 
0475 void
0476 FileView::startDrag( Qt::DropActions supportedActions )
0477 {
0478     //setSelectionMode( QAbstractItemView::NoSelection );
0479     // When a parent item is dragged, startDrag() is called a bunch of times. Here we prevent that:
0480     m_dragMutex.lock();
0481     if( m_ongoingDrag )
0482     {
0483         m_dragMutex.unlock();
0484         return;
0485     }
0486     m_ongoingDrag = true;
0487     m_dragMutex.unlock();
0488 
0489     if( !m_pd )
0490         m_pd = The::popupDropperFactory()->createPopupDropper( Context::ContextView::self() );
0491 
0492     if( m_pd && m_pd->isHidden() )
0493     {
0494         QModelIndexList indices = selectedIndexes();
0495 
0496         QList<QAction *> actions = actionsForIndices( indices );
0497 
0498         QFont font;
0499         font.setPointSize( 16 );
0500         font.setBold( true );
0501 
0502         foreach( QAction *action, actions )
0503             m_pd->addItem( The::popupDropperFactory()->createItem( action ) );
0504 
0505         m_pd->show();
0506     }
0507 
0508     QTreeView::startDrag( supportedActions );
0509 
0510     if( m_pd )
0511     {
0512         connect( m_pd, &PopupDropper::fadeHideFinished, m_pd, &PopupDropper::clear );
0513         m_pd->hide();
0514     }
0515 
0516     m_dragMutex.lock();
0517     m_ongoingDrag = false;
0518     m_dragMutex.unlock();
0519 }
0520 
0521 KFileItemList
0522 FileView::selectedItems() const
0523 {
0524     KFileItemList items;
0525     QModelIndexList indices = selectedIndexes();
0526     if( indices.isEmpty() )
0527         return items;
0528 
0529     foreach( const QModelIndex& index, indices )
0530     {
0531         KFileItem item = index.data( KDirModel::FileItemRole ).value<KFileItem>();
0532         items << item;
0533     }
0534     return items;
0535 }
0536 
0537 Meta::TrackList
0538 FileView::tracksForEdit() const
0539 {
0540     Meta::TrackList tracks;
0541 
0542     QModelIndexList indices = selectedIndexes();
0543     if( indices.isEmpty() )
0544         return tracks;
0545 
0546     foreach( const QModelIndex &index, indices )
0547     {
0548         KFileItem item = index.data( KDirModel::FileItemRole ).value<KFileItem>();
0549         Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( item.url() );
0550         if( track )
0551             tracks << track;
0552     }
0553     return tracks;
0554 }
0555 
0556 void
0557 FileView::slotMoveToTrash( Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers )
0558 {
0559     Q_UNUSED( buttons )
0560     DEBUG_BLOCK
0561 
0562     QModelIndexList indices = selectedIndexes();
0563     if( indices.isEmpty() )
0564         return;
0565 
0566     const bool deleting = modifiers.testFlag( Qt::ShiftModifier );
0567     QString caption;
0568     QString labelText;
0569     if( deleting  )
0570     {
0571         caption = i18nc( "@title:window", "Confirm Delete" );
0572         labelText = i18np( "Are you sure you want to delete this item?",
0573                            "Are you sure you want to delete these %1 items?",
0574                            indices.count() );
0575     }
0576     else
0577     {
0578         caption = i18nc( "@title:window", "Confirm Move to Trash" );
0579         labelText = i18np( "Are you sure you want to move this item to trash?",
0580                            "Are you sure you want to move these %1 items to trash?",
0581                            indices.count() );
0582     }
0583 
0584     QList<QUrl> urls;
0585     QStringList filepaths;
0586     foreach( const QModelIndex& index, indices )
0587     {
0588         KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
0589         filepaths << file.localPath();
0590         urls << file.url();
0591     }
0592 
0593     KGuiItem confirmButton = deleting ? KStandardGuiItem::del() : KStandardGuiItem::remove();
0594 
0595     if( KMessageBox::warningContinueCancelList( this, labelText, filepaths, caption, confirmButton ) != KMessageBox::Continue )
0596         return;
0597 
0598     if( deleting )
0599     {
0600         KIO::del( urls, KIO::HideProgressInfo );
0601         return;
0602     }
0603 
0604     KIO::trash( urls, KIO::HideProgressInfo );
0605 }
0606 
0607 void
0608 FileView::slotDelete()
0609 {
0610     slotMoveToTrash( Qt::NoButton, Qt::ShiftModifier );
0611 }