1 /****************************************************************************************
2 * Copyright (c) 2010 Nikolaj Hald Nielsen <nhn@kde.org> *
3 * Copyright (c) 2010 Casey Link <unnamedrambler@gmail.com> *
4 * *
5 * This program is free software; you can redistribute it and/or modify it under *
6 * the terms of the GNU General Public License as published by the Free Software *
7 * Foundation; either version 2 of the License, or (at your option) any later *
8 * version. *
9 * *
10 * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
11 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
12 * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License along with *
15 * this program. If not, see <http://www.gnu.org/licenses/>. *
16 ****************************************************************************************/
17
18 #define DEBUG_PREFIX "FileView"
19
20 #include "FileView.h"
21
22 #include "EngineController.h"
23 #include "PaletteHandler.h"
24 #include "PopupDropperFactory.h"
25 #include "SvgHandler.h"
26 #include "context/ContextView.h"
27 #include "core/playlists/PlaylistFormat.h"
28 #include "core/support/Debug.h"
29 #include "core-impl/collections/support/CollectionManager.h"
30 #include "core-impl/collections/support/FileCollectionLocation.h"
31 #include "core-impl/meta/file/File.h"
32 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
33 #include "core-impl/support/TrackLoader.h"
34 #include "dialogs/TagDialog.h"
35
36 #include <QAction>
37 #include <QContextMenuEvent>
38 #include <QFileSystemModel>
39 #include <QIcon>
40 #include <QItemDelegate>
41 #include <QMenu>
42 #include <QPainter>
43 #include <QUrl>
44
45 #include <KConfigGroup>
46 #include <KDirModel>
47 #include <KFileItem>
48 #include <KIO/CopyJob>
49 #include <KIO/DeleteJob>
50 #include <KLocalizedString>
51 #include <KMessageBox>
52
FileView(QWidget * parent)53 FileView::FileView( QWidget *parent )
54 : Amarok::PrettyTreeView( parent )
55 , m_appendAction( 0 )
56 , m_loadAction( 0 )
57 , m_editAction( 0 )
58 , m_moveToTrashAction( 0 )
59 , m_deleteAction( 0 )
60 , m_pd( 0 )
61 , m_ongoingDrag( false )
62 {
63 setFrameStyle( QFrame::NoFrame );
64 setItemsExpandable( false );
65 setRootIsDecorated( false );
66 setAlternatingRowColors( true );
67 setUniformRowHeights( true );
68 setEditTriggers( EditKeyPressed );
69
70 The::paletteHandler()->updateItemView( this );
71 connect( The::paletteHandler(), &PaletteHandler::newPalette,
72 this, &FileView::newPalette );
73 }
74
75 void
contextMenuEvent(QContextMenuEvent * e)76 FileView::contextMenuEvent( QContextMenuEvent *e )
77 {
78 if( !model() )
79 return;
80
81 //trying to do fancy stuff while showing places only leads to tears!
82 if( model()->objectName() == "PLACESMODEL" )
83 {
84 e->accept();
85 return;
86 }
87
88 QModelIndexList indices = selectedIndexes();
89 // Abort if nothing is selected
90 if( indices.isEmpty() )
91 return;
92
93 QMenu menu;
94 foreach( QAction *action, actionsForIndices( indices, PlaylistAction ) )
95 menu.addAction( action );
96 menu.addSeparator();
97
98 // Create Copy/Move to menu items
99 // ported from old filebrowser
100 QList<Collections::Collection*> writableCollections;
101 QHash<Collections::Collection*, CollectionManager::CollectionStatus> hash =
102 CollectionManager::instance()->collections();
103 QHash<Collections::Collection*, CollectionManager::CollectionStatus>::const_iterator it =
104 hash.constBegin();
105 while( it != hash.constEnd() )
106 {
107 Collections::Collection *coll = it.key();
108 if( coll && coll->isWritable() )
109 writableCollections.append( coll );
110 ++it;
111 }
112 if( !writableCollections.isEmpty() )
113 {
114 QMenu *copyMenu = new QMenu( i18n( ("Copy to Collection") ), &menu );
115 copyMenu->setIcon( QIcon::fromTheme( QStringLiteral("edit-copy") ) );
116 foreach( Collections::Collection *coll, writableCollections )
117 {
118 CollectionAction *copyAction = new CollectionAction( coll, &menu );
119 connect( copyAction, &QAction::triggered, this, &FileView::slotPrepareCopyTracks );
120 copyMenu->addAction( copyAction );
121 }
122 menu.addMenu( copyMenu );
123
124 QMenu *moveMenu = new QMenu( i18n( "Move to Collection" ), &menu );
125 moveMenu->setIcon( QIcon::fromTheme( QStringLiteral("go-jump") ) );
126 foreach( Collections::Collection *coll, writableCollections )
127 {
128 CollectionAction *moveAction = new CollectionAction( coll, &menu );
129 connect( moveAction, &QAction::triggered, this, &FileView::slotPrepareMoveTracks );
130 moveMenu->addAction( moveAction );
131 }
132 menu.addMenu( moveMenu );
133 }
134 foreach( QAction *action, actionsForIndices( indices, OrganizeAction ) )
135 menu.addAction( action );
136 menu.addSeparator();
137
138 foreach( QAction *action, actionsForIndices( indices, EditAction ) )
139 menu.addAction( action );
140
141 menu.exec( e->globalPos() );
142 }
143
144 void
mouseReleaseEvent(QMouseEvent * event)145 FileView::mouseReleaseEvent( QMouseEvent *event )
146 {
147 QModelIndex index = indexAt( event->pos() );
148 if( !index.isValid() )
149 {
150 PrettyTreeView::mouseReleaseEvent( event );
151 return;
152 }
153
154 if( state() == QAbstractItemView::NoState && event->button() == Qt::MidButton )
155 {
156 addIndexToPlaylist( index, Playlist::OnMiddleClickOnSelectedItems );
157 event->accept();
158 return;
159 }
160
161 KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
162 if( state() == QAbstractItemView::NoState &&
163 event->button() == Qt::LeftButton &&
164 event->modifiers() == Qt::NoModifier &&
165 style()->styleHint( QStyle::SH_ItemView_ActivateItemOnSingleClick, 0, this ) &&
166 ( file.isDir() || file.isNull() ) )
167 {
168 Q_EMIT navigateToDirectory( index );
169 event->accept();
170 return;
171 }
172
173 PrettyTreeView::mouseReleaseEvent( event );
174 }
175
176 void
mouseDoubleClickEvent(QMouseEvent * event)177 FileView::mouseDoubleClickEvent( QMouseEvent *event )
178 {
179 QModelIndex index = indexAt( event->pos() );
180 if( !index.isValid() )
181 {
182 event->accept();
183 return;
184 }
185
186 // swallow middle-button double-clicks
187 if( event->button() == Qt::MidButton )
188 {
189 event->accept();
190 return;
191 }
192
193 if( event->button() == Qt::LeftButton )
194 {
195 KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
196 QUrl url = file.url();
197 if( !file.isNull() && ( Playlists::isPlaylist( url ) || MetaFile::Track::isTrack( url ) ) )
198 addIndexToPlaylist( index, Playlist::OnDoubleClickOnSelectedItems );
199 else
200 Q_EMIT navigateToDirectory( index );
201
202 event->accept();
203 return;
204 }
205
206 PrettyTreeView::mouseDoubleClickEvent( event );
207 }
208
209 void
keyPressEvent(QKeyEvent * event)210 FileView::keyPressEvent( QKeyEvent *event )
211 {
212 QModelIndex index = currentIndex();
213 if( !index.isValid() )
214 return;
215
216 switch( event->key() )
217 {
218 case Qt::Key_Enter:
219 case Qt::Key_Return:
220 {
221 KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
222 QUrl url = file.url();
223 if( !file.isNull() && ( Playlists::isPlaylist( url ) || MetaFile::Track::isTrack( url ) ) )
224 // right, we test the current item, but then add the selection to playlist
225 addSelectionToPlaylist( Playlist::OnReturnPressedOnSelectedItems );
226 else
227 Q_EMIT navigateToDirectory( index );
228
229 return;
230 }
231 case Qt::Key_Delete:
232 slotMoveToTrash( Qt::NoButton, event->modifiers() );
233 break;
234 case Qt::Key_F5:
235 Q_EMIT refreshBrowser();
236 break;
237 default:
238 break;
239 }
240
241 QTreeView::keyPressEvent( event );
242 }
243
244 void
slotAppendToPlaylist()245 FileView::slotAppendToPlaylist()
246 {
247 addSelectionToPlaylist( Playlist::OnAppendToPlaylistAction );
248 }
249
250 void
slotReplacePlaylist()251 FileView::slotReplacePlaylist()
252 {
253 addSelectionToPlaylist( Playlist::OnReplacePlaylistAction );
254 }
255
256 void
slotEditTracks()257 FileView::slotEditTracks()
258 {
259 Meta::TrackList tracks = tracksForEdit();
260 if( !tracks.isEmpty() )
261 {
262 TagDialog *dialog = new TagDialog( tracks, this );
263 dialog->show();
264 }
265 }
266
267 void
slotPrepareMoveTracks()268 FileView::slotPrepareMoveTracks()
269 {
270 if( m_moveDestinationCollection )
271 return;
272
273 CollectionAction *action = dynamic_cast<CollectionAction*>( sender() );
274 if( !action )
275 return;
276
277 m_moveDestinationCollection = action->collection();
278
279 const KFileItemList list = selectedItems();
280 if( list.isEmpty() )
281 return;
282
283 // prevent bug 313003, require full metadata
284 TrackLoader* dl = new TrackLoader( TrackLoader::FullMetadataRequired ); // auto-deletes itself
285 connect( dl, &TrackLoader::finished, this, &FileView::slotMoveTracks );
286 dl->init( list.urlList() );
287 }
288
289 void
slotPrepareCopyTracks()290 FileView::slotPrepareCopyTracks()
291 {
292 if( m_copyDestinationCollection )
293 return;
294
295 CollectionAction *action = dynamic_cast<CollectionAction*>( sender() );
296 if( !action )
297 return;
298
299 m_copyDestinationCollection = action->collection();
300
301 const KFileItemList list = selectedItems();
302 if( list.isEmpty() )
303 return;
304
305 // prevent bug 313003, require full metadata
306 TrackLoader* dl = new TrackLoader( TrackLoader::FullMetadataRequired ); // auto-deletes itself
307 connect( dl, &TrackLoader::finished, this, &FileView::slotCopyTracks );
308 dl->init( list.urlList() );
309 }
310
311 void
slotCopyTracks(const Meta::TrackList & tracks)312 FileView::slotCopyTracks( const Meta::TrackList& tracks )
313 {
314 if( !m_copyDestinationCollection )
315 return;
316
317 QSet<Collections::Collection *> collections;
318 foreach( const Meta::TrackPtr &track, tracks )
319 {
320 collections.insert( track->collection() );
321 }
322
323 if( collections.count() == 1 )
324 {
325 Collections::Collection *sourceCollection = collections.values().first();
326 Collections::CollectionLocation *source;
327 if( sourceCollection )
328 source = sourceCollection->location();
329 else
330 source = new Collections::FileCollectionLocation();
331
332 Collections::CollectionLocation *destination = m_copyDestinationCollection->location();
333 source->prepareCopy( tracks, destination );
334 }
335 else
336 warning() << "Cannot handle copying tracks from multiple collections, doing nothing to be safe";
337
338 m_copyDestinationCollection.clear();
339 }
340
341 void
slotMoveTracks(const Meta::TrackList & tracks)342 FileView::slotMoveTracks( const Meta::TrackList& tracks )
343 {
344 if( !m_moveDestinationCollection )
345 return;
346
347 QSet<Collections::Collection *> collections;
348 foreach( const Meta::TrackPtr &track, tracks )
349 {
350 collections.insert( track->collection() );
351 }
352
353 if( collections.count() == 1 )
354 {
355 Collections::Collection *sourceCollection = collections.values().first();
356 Collections::CollectionLocation *source;
357 if( sourceCollection )
358 source = sourceCollection->location();
359 else
360 source = new Collections::FileCollectionLocation();
361
362 Collections::CollectionLocation *destination = m_moveDestinationCollection->location();
363 source->prepareMove( tracks, destination );
364 }
365 else
366 warning() << "Cannot handle moving tracks from multiple collections, doing nothing to be safe";
367
368 m_moveDestinationCollection.clear();
369 }
370
371 QList<QAction *>
actionsForIndices(const QModelIndexList & indices,ActionType type)372 FileView::actionsForIndices( const QModelIndexList &indices, ActionType type )
373 {
374 QList<QAction *> actions;
375
376 if( indices.isEmpty() )
377 return actions; // get out of here!
378
379 if( !m_appendAction )
380 {
381 m_appendAction = new QAction( QIcon::fromTheme( "media-track-add-amarok" ), i18n( "&Add to Playlist" ),
382 this );
383 m_appendAction->setProperty( "popupdropper_svg_id", "append" );
384 connect( m_appendAction, &QAction::triggered, this, &FileView::slotAppendToPlaylist );
385 }
386 if( type & PlaylistAction )
387 actions.append( m_appendAction );
388
389 if( !m_loadAction )
390 {
391 m_loadAction = new QAction( i18nc( "Replace the currently loaded tracks with these",
392 "&Replace Playlist" ), this );
393 m_loadAction->setProperty( "popupdropper_svg_id", "load" );
394 connect( m_loadAction, &QAction::triggered, this, &FileView::slotReplacePlaylist );
395 }
396 if( type & PlaylistAction )
397 actions.append( m_loadAction );
398
399 if( !m_moveToTrashAction )
400 {
401 m_moveToTrashAction = new QAction( QIcon::fromTheme( "user-trash" ), i18n( "&Move to Trash" ), this );
402 m_moveToTrashAction->setProperty( "popupdropper_svg_id", "delete_file" );
403 // key shortcut is only for display purposes here, actual one is determined by View in Model/View classes
404 m_moveToTrashAction->setShortcut( Qt::Key_Delete );
405 connect( m_moveToTrashAction, &QAction::triggered, this, &FileView::slotMoveToTrashWithoutModifiers );
406 }
407 if( type & OrganizeAction )
408 actions.append( m_moveToTrashAction );
409
410 if( !m_deleteAction )
411 {
412 m_deleteAction = new QAction( QIcon::fromTheme( "remove-amarok" ), i18n( "&Delete" ), this );
413 m_deleteAction->setProperty( "popupdropper_svg_id", "delete_file" );
414 // key shortcut is only for display purposes here, actual one is determined by View in Model/View classes
415 m_deleteAction->setShortcut( Qt::SHIFT + Qt::Key_Delete );
416 connect( m_deleteAction, &QAction::triggered, this, &FileView::slotDelete );
417 }
418 if( type & OrganizeAction )
419 actions.append( m_deleteAction );
420
421 if( !m_editAction )
422 {
423 m_editAction = new QAction( QIcon::fromTheme( "media-track-edit-amarok" ),
424 i18n( "&Edit Track Details" ), this );
425 m_editAction->setProperty( "popupdropper_svg_id", "edit" );
426 connect( m_editAction, &QAction::triggered, this, &FileView::slotEditTracks );
427 }
428 if( type & EditAction )
429 {
430 actions.append( m_editAction );
431 Meta::TrackList tracks = tracksForEdit();
432 m_editAction->setVisible( !tracks.isEmpty() );
433 }
434
435 return actions;
436 }
437
438 void
addSelectionToPlaylist(Playlist::AddOptions options)439 FileView::addSelectionToPlaylist( Playlist::AddOptions options )
440 {
441 addIndicesToPlaylist( selectedIndexes(), options );
442 }
443
444 void
addIndexToPlaylist(const QModelIndex & idx,Playlist::AddOptions options)445 FileView::addIndexToPlaylist( const QModelIndex &idx, Playlist::AddOptions options )
446 {
447 addIndicesToPlaylist( QModelIndexList() << idx, options );
448 }
449
450 void
addIndicesToPlaylist(QModelIndexList indices,Playlist::AddOptions options)451 FileView::addIndicesToPlaylist( QModelIndexList indices, Playlist::AddOptions options )
452 {
453 if( indices.isEmpty() )
454 return;
455
456 // let tracks & playlists appear in playlist as they are shown in the view:
457 qSort( indices );
458
459 QList<QUrl> urls;
460 foreach( const QModelIndex &index, indices )
461 {
462 KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
463 QUrl url = file.url();
464 if( file.isDir() || Playlists::isPlaylist( url ) || MetaFile::Track::isTrack( url ) )
465 {
466 urls << file.url();
467 }
468 }
469
470 The::playlistController()->insertOptioned( urls, options );
471 }
472
473 void
startDrag(Qt::DropActions supportedActions)474 FileView::startDrag( Qt::DropActions supportedActions )
475 {
476 //setSelectionMode( QAbstractItemView::NoSelection );
477 // When a parent item is dragged, startDrag() is called a bunch of times. Here we prevent that:
478 m_dragMutex.lock();
479 if( m_ongoingDrag )
480 {
481 m_dragMutex.unlock();
482 return;
483 }
484 m_ongoingDrag = true;
485 m_dragMutex.unlock();
486
487 if( !m_pd )
488 m_pd = The::popupDropperFactory()->createPopupDropper( Context::ContextView::self() );
489
490 if( m_pd && m_pd->isHidden() )
491 {
492 QModelIndexList indices = selectedIndexes();
493
494 QList<QAction *> actions = actionsForIndices( indices );
495
496 QFont font;
497 font.setPointSize( 16 );
498 font.setBold( true );
499
500 foreach( QAction *action, actions )
501 m_pd->addItem( The::popupDropperFactory()->createItem( action ) );
502
503 m_pd->show();
504 }
505
506 QTreeView::startDrag( supportedActions );
507
508 if( m_pd )
509 {
510 connect( m_pd, &PopupDropper::fadeHideFinished, m_pd, &PopupDropper::clear );
511 m_pd->hide();
512 }
513
514 m_dragMutex.lock();
515 m_ongoingDrag = false;
516 m_dragMutex.unlock();
517 }
518
519 KFileItemList
selectedItems() const520 FileView::selectedItems() const
521 {
522 KFileItemList items;
523 QModelIndexList indices = selectedIndexes();
524 if( indices.isEmpty() )
525 return items;
526
527 foreach( const QModelIndex& index, indices )
528 {
529 KFileItem item = index.data( KDirModel::FileItemRole ).value<KFileItem>();
530 items << item;
531 }
532 return items;
533 }
534
535 Meta::TrackList
tracksForEdit() const536 FileView::tracksForEdit() const
537 {
538 Meta::TrackList tracks;
539
540 QModelIndexList indices = selectedIndexes();
541 if( indices.isEmpty() )
542 return tracks;
543
544 foreach( const QModelIndex &index, indices )
545 {
546 KFileItem item = index.data( KDirModel::FileItemRole ).value<KFileItem>();
547 Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( item.url() );
548 if( track )
549 tracks << track;
550 }
551 return tracks;
552 }
553
554 void
slotMoveToTrash(Qt::MouseButtons buttons,Qt::KeyboardModifiers modifiers)555 FileView::slotMoveToTrash( Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers )
556 {
557 Q_UNUSED( buttons )
558 DEBUG_BLOCK
559
560 QModelIndexList indices = selectedIndexes();
561 if( indices.isEmpty() )
562 return;
563
564 const bool deleting = modifiers.testFlag( Qt::ShiftModifier );
565 QString caption;
566 QString labelText;
567 if( deleting )
568 {
569 caption = i18nc( "@title:window", "Confirm Delete" );
570 labelText = i18np( "Are you sure you want to delete this item?",
571 "Are you sure you want to delete these %1 items?",
572 indices.count() );
573 }
574 else
575 {
576 caption = i18nc( "@title:window", "Confirm Move to Trash" );
577 labelText = i18np( "Are you sure you want to move this item to trash?",
578 "Are you sure you want to move these %1 items to trash?",
579 indices.count() );
580 }
581
582 QList<QUrl> urls;
583 QStringList filepaths;
584 foreach( const QModelIndex& index, indices )
585 {
586 KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
587 filepaths << file.localPath();
588 urls << file.url();
589 }
590
591 KGuiItem confirmButton = deleting ? KStandardGuiItem::del() : KStandardGuiItem::remove();
592
593 if( KMessageBox::warningContinueCancelList( this, labelText, filepaths, caption, confirmButton ) != KMessageBox::Continue )
594 return;
595
596 if( deleting )
597 {
598 KIO::del( urls, KIO::HideProgressInfo );
599 return;
600 }
601
602 KIO::trash( urls, KIO::HideProgressInfo );
603 }
604
605 void
slotDelete()606 FileView::slotDelete()
607 {
608 slotMoveToTrash( Qt::NoButton, Qt::ShiftModifier );
609 }
610