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 }