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