1 /****************************************************************************************
2  * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
3  * Copyright (c) 2009 Téo Mrnjavac <teo@kde.org>                                        *
4  * Copyright (c) 2009 Nikolaj Hald Nielsen <nhn@kde.org>                                *
5  * Copyright (c) 2009 John Atkinson <john@fauxnetic.co.uk>                              *
6  * Copyright (c) 2009-2010 Oleksandr Khayrullin <saniokh@gmail.com>                     *
7  * Copyright (c) 2010 Nanno Langstraat <langstr@gmail.com>                              *
8  *                                                                                      *
9  * This program is free software; you can redistribute it and/or modify it under        *
10  * the terms of the GNU General Public License as published by the Free Software        *
11  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
12  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
13  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
14  * version 3 of the license.                                                            *
15  *                                                                                      *
16  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
17  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
18  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
19  *                                                                                      *
20  * You should have received a copy of the GNU General Public License along with         *
21  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
22  ****************************************************************************************/
23 
24 #define DEBUG_PREFIX "Playlist::PrettyListView"
25 
26 #include "PrettyListView.h"
27 
28 #include "amarokconfig.h"
29 #include "AmarokMimeData.h"
30 #include "context/ContextView.h"
31 #include "context/popupdropper/libpud/PopupDropper.h"
32 #include "core/support/Debug.h"
33 #include "EngineController.h"
34 #include "dialogs/TagDialog.h"
35 #include "GlobalCurrentTrackActions.h"
36 #include "core/capabilities/ActionsCapability.h"
37 #include "core/capabilities/FindInSourceCapability.h"
38 #include "core/capabilities/MultiSourceCapability.h"
39 #include "core/meta/Meta.h"
40 #include "PaletteHandler.h"
41 #include "playlist/layouts/LayoutManager.h"
42 #include "playlist/proxymodels/GroupingProxy.h"
43 #include "playlist/PlaylistActions.h"
44 #include "playlist/PlaylistModelStack.h"
45 #include "playlist/PlaylistController.h"
46 #include "playlist/view/PlaylistViewCommon.h"
47 #include "playlist/PlaylistDefines.h"
48 #include "PopupDropperFactory.h"
49 #include "SvgHandler.h"
50 #include "SourceSelectionPopup.h"
51 
52 #include <QApplication>
53 #include <QClipboard>
54 #include <QContextMenuEvent>
55 #include <QDropEvent>
56 #include <QItemSelection>
57 #include <QKeyEvent>
58 #include <QListView>
59 #include <QMenu>
60 #include <QModelIndex>
61 #include <QMouseEvent>
62 #include <QPainter>
63 #include <QPalette>
64 #include <QPersistentModelIndex>
65 #include <QScrollBar>
66 #include <QSvgRenderer>
67 #include <QTimer>
68 #include <QUrl>
69 
70 #include <KLocalizedString>
71 
72 
PrettyListView(QWidget * parent)73 Playlist::PrettyListView::PrettyListView( QWidget* parent )
74         : QListView( parent )
75         , ViewCommon()
76         , m_headerPressIndex( QModelIndex() )
77         , m_mousePressInHeader( false )
78         , m_skipAutoScroll( false )
79         , m_firstScrollToActiveTrack( true )
80         , m_rowsInsertedScrollItem( 0 )
81         , m_showOnlyMatches( false )
82         , m_pd( 0 )
83 {
84     // QAbstractItemView basics
85     setModel( The::playlist()->qaim() );
86 
87     m_prettyDelegate = new PrettyItemDelegate( this );
88     connect( m_prettyDelegate, &PrettyItemDelegate::redrawRequested, this, &Playlist::PrettyListView::redrawActive );
89     setItemDelegate( m_prettyDelegate );
90 
91     setSelectionMode( ExtendedSelection );
92     setDragDropMode( DragDrop );
93     setDropIndicatorShown( false ); // we draw our own drop indicator
94     setEditTriggers ( SelectedClicked | EditKeyPressed );
95     setAutoScroll( true );
96 
97     setVerticalScrollMode( ScrollPerPixel );
98 
99     setMouseTracking( true );
100 
101     // Rendering adjustments
102     setFrameShape( QFrame::NoFrame );
103     setAlternatingRowColors( true) ;
104     The::paletteHandler()->updateItemView( this );
105     connect( The::paletteHandler(), &PaletteHandler::newPalette, this, &PrettyListView::newPalette );
106 
107     setAutoFillBackground( false );
108 
109 
110     // Signal connections
111     connect( this, &Playlist::PrettyListView::doubleClicked,
112              this, &Playlist::PrettyListView::trackActivated );
113     connect( selectionModel(), &QItemSelectionModel::selectionChanged,
114              this, &Playlist::PrettyListView::slotSelectionChanged );
115 
116     connect( LayoutManager::instance(), &LayoutManager::activeLayoutChanged, this, &PrettyListView::playlistLayoutChanged );
117 
118     if (auto m = static_cast<Playlist::Model*>(model()))
119     {
120         connect( m, &Playlist::Model::activeTrackChanged, this, &Playlist::PrettyListView::slotPlaylistActiveTrackChanged );
121         connect( m, &Playlist::Model::queueChanged, viewport(), QOverload<>::of(&QWidget::update) );
122     }
123     else
124         warning() << "Model is not a Playlist::Model";
125 
126     //   Warning, this one doesn't connect to the normal 'model()' (i.e. '->top()'), but to '->bottom()'.
127     connect( Playlist::ModelStack::instance()->bottom(), &Playlist::Model::rowsInserted, this, &Playlist::PrettyListView::bottomModelRowsInserted );
128 
129     // Timers
130     m_proxyUpdateTimer = new QTimer( this );
131     m_proxyUpdateTimer->setSingleShot( true );
132     connect( m_proxyUpdateTimer, &QTimer::timeout, this, &Playlist::PrettyListView::updateProxyTimeout );
133 
134     m_animationTimer = new QTimer(this);
135     connect( m_animationTimer, &QTimer::timeout, this, &Playlist::PrettyListView::redrawActive );
136     m_animationTimer->setInterval( 250 );
137 
138     playlistLayoutChanged();
139 
140     // We do the following call here to be formally correct, but note:
141     //   - It happens to be redundant, because 'playlistLayoutChanged()' already schedules
142     //     another one, via a QTimer( 0 ).
143     //   - Both that one and this one don't work right (they scroll like 'PositionAtTop',
144     //     not 'PositionAtCenter'). This is probably because MainWindow changes its
145     //     geometry in a QTimer( 0 )? As a fix, MainWindow does a 'slotShowActiveTrack()'
146     //     at the end of its QTimer slot, which will finally scroll to the right spot.
147     slotPlaylistActiveTrackChanged();
148 }
149 
~PrettyListView()150 Playlist::PrettyListView::~PrettyListView()
151 {}
152 
153 int
verticalOffset() const154 Playlist::PrettyListView::verticalOffset() const
155 {
156     int ret = QListView::verticalOffset();
157     if ( verticalScrollBar() && verticalScrollBar()->maximum() )
158         ret += verticalScrollBar()->value() * 10 / verticalScrollBar()->maximum();
159     return ret;
160 }
161 
162 void
editTrackInformation()163 Playlist::PrettyListView::editTrackInformation()
164 {
165     Meta::TrackList tl;
166     foreach( const QModelIndex &index, selectedIndexes() )
167     {
168         tl.append( index.data( TrackRole ).value<Meta::TrackPtr>() );
169     }
170 
171     if( !tl.isEmpty() )
172     {
173         TagDialog *dialog = new TagDialog( tl, this );
174         dialog->show();
175     }
176 }
177 
178 void
playFirstSelected()179 Playlist::PrettyListView::playFirstSelected()
180 {
181     QModelIndexList selected = selectedIndexes();
182     if( !selected.isEmpty() )
183         trackActivated( selected.first() );
184 }
185 
186 void
removeSelection()187 Playlist::PrettyListView::removeSelection()
188 {
189     QList<int> sr = selectedRows();
190     if( !sr.isEmpty() )
191     {
192         // Now that we have the list of selected rows in the topmost proxy, we can perform the
193         // removal.
194         The::playlistController()->removeRows( sr );
195 
196         // Next, we look for the first row.
197         int firstRow = sr.first();
198         foreach( int i, sr )
199         {
200             if( i < firstRow )
201                 firstRow = i;
202         }
203 
204         //Select the track occupied by the first deleted track. Also move the current item to here as
205         //button presses up or down wil otherwise not behave as expected.
206         firstRow = qBound( 0, firstRow, model()->rowCount() - 1 );
207         QModelIndex newSelectionIndex = model()->index( firstRow, 0 );
208         setCurrentIndex( newSelectionIndex );
209         selectionModel()->select( newSelectionIndex, QItemSelectionModel::Select );
210     }
211 }
212 
213 void
queueSelection()214 Playlist::PrettyListView::queueSelection()
215 {
216     Actions::instance()->queue( selectedRows() );
217 }
218 
219 void
dequeueSelection()220 Playlist::PrettyListView::dequeueSelection()
221 {
222     Actions::instance()->dequeue( selectedRows() );
223 }
224 
225 void
switchQueueState()226 Playlist::PrettyListView::switchQueueState() // slot
227 {
228     DEBUG_BLOCK
229     const bool isQueued = currentIndex().data( Playlist::QueuePositionRole ).toInt() != 0;
230     isQueued ? dequeueSelection() : queueSelection();
231 }
232 
selectSource()233 void Playlist::PrettyListView::selectSource()
234 {
235     DEBUG_BLOCK
236 
237     QList<int> rows = selectedRows();
238 
239     //for now, bail out of more than 1 row...
240     if ( rows.count() != 1 )
241         return;
242 
243     //get the track...
244     QModelIndex index = model()->index( rows.at( 0 ), 0 );
245     Meta::TrackPtr track = index.data( Playlist::TrackRole ).value< Meta::TrackPtr >();
246 
247     //get multiSource capability:
248 
249     Capabilities::MultiSourceCapability *msc = track->create<Capabilities::MultiSourceCapability>();
250     if ( msc )
251     {
252         debug() << "sources: " << msc->sources();
253         SourceSelectionPopup * sourceSelector = new SourceSelectionPopup( this, msc );
254         sourceSelector->show();
255         //dialog deletes msc when done with it.
256     }
257 }
258 
259 void
scrollToActiveTrack()260 Playlist::PrettyListView::scrollToActiveTrack()
261 {
262     DEBUG_BLOCK
263 
264     if( m_skipAutoScroll )
265     {
266         m_skipAutoScroll = false;
267         return;
268     }
269 
270     QModelIndex activeIndex = model()->index( The::playlist()->activeRow(), 0, QModelIndex() );
271     if ( activeIndex.isValid() )
272     {
273         scrollTo( activeIndex, QAbstractItemView::PositionAtCenter );
274         m_firstScrollToActiveTrack = false;
275         m_rowsInsertedScrollItem = 0;    // This "new active track" scroll supersedes a pending "rows inserted" scroll.
276     }
277 }
278 
279 void
downOneTrack()280 Playlist::PrettyListView::downOneTrack()
281 {
282     DEBUG_BLOCK
283 
284     moveTrackSelection( 1 );
285 }
286 
287 void
upOneTrack()288 Playlist::PrettyListView::upOneTrack()
289 {
290     DEBUG_BLOCK
291 
292     moveTrackSelection( -1 );
293 }
294 
295 void
moveTrackSelection(int offset)296 Playlist::PrettyListView::moveTrackSelection( int offset )
297 {
298     if ( offset == 0 )
299         return;
300 
301     int finalRow = model()->rowCount() - 1;
302     int target = 0;
303 
304     if ( offset < 0 )
305         target = finalRow;
306 
307     QList<int> rows = selectedRows();
308     if ( rows.count() > 0 )
309         target = rows.at( 0 ) + offset;
310 
311     target = qBound(0, target, finalRow);
312     QModelIndex index = model()->index( target, 0 );
313     setCurrentIndex( index );
314 }
315 
316 void
slotPlaylistActiveTrackChanged()317 Playlist::PrettyListView::slotPlaylistActiveTrackChanged()
318 {
319     DEBUG_BLOCK
320 
321     // A playlist 'activeTrackChanged' signal happens:
322     //   - During startup, on "saved playlist" load. (Might happen before this view exists)
323     //   - When Amarok starts playing a new item in the playlist.
324     //     In that case, don't auto-scroll if the user doesn't like us to.
325 
326     if( AmarokConfig::autoScrollPlaylist() || m_firstScrollToActiveTrack )
327         scrollToActiveTrack();
328 }
329 
330 void
slotSelectionChanged()331 Playlist::PrettyListView::slotSelectionChanged()
332 {
333     m_lastTimeSelectionChanged = QDateTime::currentDateTime();
334 }
335 
336 void
trackActivated(const QModelIndex & idx)337 Playlist::PrettyListView::trackActivated( const QModelIndex& idx )
338 {
339     DEBUG_BLOCK
340     m_skipAutoScroll = true; // we don't want to do crazy view changes when selecting an item in the view
341     Actions::instance()->play( idx );
342 
343     //make sure that the track we just activated is also set as the current index or
344     //the selected index will get moved to the first row, making keyboard navigation difficult (BUG 225791)
345     selectionModel_setCurrentIndex( idx, QItemSelectionModel::ClearAndSelect );
346 
347     setFocus();
348 }
349 
350 
351 // The following 2 functions are a workaround for crash BUG 222961 and BUG 229240:
352 //   There appears to be a bad interaction between Qt 'setCurrentIndex()' and
353 //   Qt 'selectedIndexes()' / 'selectionModel()->select()' / 'scrollTo()'.
354 //
355 //   'setCurrentIndex()' appears to do something bad with its QModelIndex parameter,
356 //   leading to a crash deep within Qt.
357 //
358 //   It might be our fault, but we suspect a bug in Qt.  (Qt 4.6 at least)
359 //
360 //   The problem goes away if we use a fresh QModelIndex, which we also don't re-use
361 //   afterwards.
362 void
setCurrentIndex(const QModelIndex & index)363 Playlist::PrettyListView::setCurrentIndex( const QModelIndex &index )
364 {
365     QModelIndex indexCopy = model()->index( index.row(), index.column() );
366     QListView::setCurrentIndex( indexCopy );
367 }
368 
369 void
selectionModel_setCurrentIndex(const QModelIndex & index,QItemSelectionModel::SelectionFlags command)370 Playlist::PrettyListView::selectionModel_setCurrentIndex( const QModelIndex &index, QItemSelectionModel::SelectionFlags command )
371 {
372     QModelIndex indexCopy = model()->index( index.row(), index.column() );
373     selectionModel()->setCurrentIndex( indexCopy, command );
374 }
375 
376 void
showEvent(QShowEvent * event)377 Playlist::PrettyListView::showEvent( QShowEvent* event )
378 {
379     QTimer::singleShot( 0, this, &Playlist::PrettyListView::fixInvisible );
380 
381     QListView::showEvent( event );
382 }
383 
384 // This method is a workaround for BUG 184714.
385 //
386 // It prevents the playlist from becoming invisible (clear) after changing the model, while Amarok is hidden in the tray.
387 // Without this workaround the playlist stays invisible when the application is restored from the tray.
388 // This is especially a problem with the Dynamic Playlist mode, which modifies the model without user interaction.
389 //
390 // The bug only seems to happen with Qt 4.5.x, so it might actually be a bug in Qt.
391 void
fixInvisible()392 Playlist::PrettyListView::fixInvisible() //SLOT
393 {
394     // DEBUG_BLOCK
395 
396     // Part 1: Palette change
397     newPalette( palette() );
398 
399     // Part 2: Change item selection
400     const QItemSelection oldSelection( selectionModel()->selection() );
401     selectionModel()->clear();
402     selectionModel()->select( oldSelection, QItemSelectionModel::SelectCurrent );
403 
404     // NOTE: A simple update() call is not sufficient, but in fact the above two steps are required.
405 }
406 
407 void
contextMenuEvent(QContextMenuEvent * event)408 Playlist::PrettyListView::contextMenuEvent( QContextMenuEvent* event )
409 {
410     DEBUG_BLOCK
411     QModelIndex index = indexAt( event->pos() );
412 
413     if ( !index.isValid() )
414         return;
415 
416     //Ctrl + Right Click is used for queuing
417     if( event->modifiers() & Qt::ControlModifier )
418         return;
419 
420     trackMenu( this, &index, event->globalPos() );
421     event->accept();
422 }
423 
424 void
dragLeaveEvent(QDragLeaveEvent * event)425 Playlist::PrettyListView::dragLeaveEvent( QDragLeaveEvent* event )
426 {
427     m_mousePressInHeader = false;
428     m_dropIndicator = QRect( 0, 0, 0, 0 );
429     QListView::dragLeaveEvent( event );
430 }
431 
432 void
stopAfterTrack()433 Playlist::PrettyListView::stopAfterTrack()
434 {
435     const quint64 id = currentIndex().data( UniqueIdRole ).value<quint64>();
436     if( Actions::instance()->willStopAfterTrack( id ) )
437         Actions::instance()->stopAfterPlayingTrack( 0 ); // disable stopping
438     else
439         Actions::instance()->stopAfterPlayingTrack( id );
440 }
441 
442 void
findInSource()443 Playlist::PrettyListView::findInSource()
444 {
445     DEBUG_BLOCK
446 
447     Meta::TrackPtr track = currentIndex().data( TrackRole ).value<Meta::TrackPtr>();
448     if ( track )
449     {
450         if( track->has<Capabilities::FindInSourceCapability>() )
451         {
452             Capabilities::FindInSourceCapability *fis = track->create<Capabilities::FindInSourceCapability>();
453             if ( fis )
454             {
455                 fis->findInSource();
456             }
457             delete fis;
458         }
459     }
460 }
461 
462 void
dragEnterEvent(QDragEnterEvent * event)463 Playlist::PrettyListView::dragEnterEvent( QDragEnterEvent *event )
464 {
465     const QMimeData *mime = event->mimeData();
466     if( mime->hasUrls() ||
467         mime->hasFormat( AmarokMimeData::TRACK_MIME ) ||
468         mime->hasFormat( AmarokMimeData::PLAYLIST_MIME ) ||
469         mime->hasFormat( AmarokMimeData::PODCASTEPISODE_MIME ) ||
470         mime->hasFormat( AmarokMimeData::PODCASTCHANNEL_MIME ) )
471     {
472         event->acceptProposedAction();
473     }
474 }
475 
476 void
dragMoveEvent(QDragMoveEvent * event)477 Playlist::PrettyListView::dragMoveEvent( QDragMoveEvent* event )
478 {
479     QModelIndex index = indexAt( event->pos() );
480     if ( index.isValid() )
481     {
482         m_dropIndicator = visualRect( index );
483     }
484     else
485     {
486         // draw it on the bottom of the last item
487         index = model()->index( model()->rowCount() - 1, 0, QModelIndex() );
488         m_dropIndicator = visualRect( index );
489         m_dropIndicator = m_dropIndicator.translated( 0, m_dropIndicator.height() );
490     }
491     QListView::dragMoveEvent( event );
492 }
493 
494 void
dropEvent(QDropEvent * event)495 Playlist::PrettyListView::dropEvent( QDropEvent* event )
496 {
497     DEBUG_BLOCK
498     QRect oldDrop = m_dropIndicator;
499     m_dropIndicator = QRect( 0, 0, 0, 0 );
500     if ( qobject_cast<PrettyListView*>( event->source() ) == this )
501     {
502         QAbstractItemModel* plModel = model();
503         int targetRow = indexAt( event->pos() ).row();
504         targetRow = ( targetRow < 0 ) ? plModel->rowCount() : targetRow; // target of < 0 means we dropped on the end of the playlist
505         QList<int> sr = selectedRows();
506         int realtarget = The::playlistController()->moveRows( sr, targetRow );
507         QItemSelection selItems;
508         foreach( int row, sr )
509         {
510             Q_UNUSED( row )
511             selItems.select( plModel->index( realtarget, 0 ), plModel->index( realtarget, 0 ) );
512             realtarget++;
513         }
514         selectionModel()->select( selItems, QItemSelectionModel::ClearAndSelect );
515         event->accept();
516     }
517     else
518     {
519         QListView::dropEvent( event );
520     }
521     // add some padding around the old drop area which to repaint, as we add offsets when painting. See paintEvent().
522     oldDrop.adjust( -6, -6, 6, 6 );
523     repaint( oldDrop );
524 }
525 
526 void
keyPressEvent(QKeyEvent * event)527 Playlist::PrettyListView::keyPressEvent( QKeyEvent *event )
528 {
529     if( event->matches( QKeySequence::Delete ) )
530     {
531         removeSelection();
532         event->accept();
533     }
534     else if( event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return )
535     {
536         trackActivated( currentIndex() );
537         event->accept();
538     }
539     else if( event->matches( QKeySequence::SelectAll ) )
540     {
541         QModelIndex topIndex = model()->index( 0, 0 );
542         QModelIndex bottomIndex = model()->index( model()->rowCount() - 1, 0 );
543         QItemSelection selItems( topIndex, bottomIndex );
544         selectionModel()->select( selItems, QItemSelectionModel::ClearAndSelect );
545         event->accept();
546     }
547     else
548         QListView::keyPressEvent( event );
549 }
550 
551 void
mousePressEvent(QMouseEvent * event)552 Playlist::PrettyListView::mousePressEvent( QMouseEvent* event )
553 {
554     //get the item that was clicked
555     QModelIndex index = indexAt( event->pos() );
556 
557     //first of all, if a left click, check if the delegate wants to do something about this click
558     if( event->button() == Qt::LeftButton )
559     {
560         //we need to translate the position of the click into something relative to the item that was clicked.
561         QRect itemRect = visualRect( index );
562         QPoint relPos =  event->pos() - itemRect.topLeft();
563 
564         if( m_prettyDelegate->clicked( relPos, itemRect, index ) )
565         {
566             event->accept();
567             return;  //click already handled...
568         }
569     }
570 
571     if ( mouseEventInHeader( event ) && ( event->button() == Qt::LeftButton ) )
572     {
573         m_mousePressInHeader = true;
574         m_headerPressIndex = QPersistentModelIndex( index );
575         int rows = index.data( GroupedTracksRole ).toInt();
576         QModelIndex bottomIndex = model()->index( index.row() + rows - 1, 0 );
577 
578         //offset by 1 as the actual header item is selected in QListView::mousePressEvent( event ); and is otherwise deselected again
579         QItemSelection selItems( model()->index( index.row() + 1, 0 ), bottomIndex );
580         QItemSelectionModel::SelectionFlags command = headerPressSelectionCommand( index, event );
581         selectionModel()->select( selItems, command );
582         // TODO: if you're doing shift-select on rows above the header, then the rows following the header will be lost from the selection
583         selectionModel_setCurrentIndex( index, QItemSelectionModel::NoUpdate );
584     }
585     else
586     {
587         m_mousePressInHeader = false;
588     }
589 
590     if ( event->button() == Qt::MidButton )
591     {
592         QUrl url( QApplication::clipboard()->text() );
593         if ( url.isValid() )
594         {
595             QList<QUrl> urls = QList<QUrl>() << url;
596             if( index.isValid() )
597                 The::playlistController()->insertUrls( index.row() + 1, urls );
598             else
599                 The::playlistController()->insertOptioned( urls, Playlist::OnAppendToPlaylistAction );
600         }
601     }
602 
603     // This should always be forwarded, as it is used to determine the offset
604     // relative to the mouse of the selection we are dragging!
605     QListView::mousePressEvent( event );
606 
607     // This must go after the call to the super class as the current index is not yet selected otherwise
608     // Queueing support for Ctrl Right click
609     if( event->button() == Qt::RightButton && event->modifiers() & Qt::ControlModifier )
610     {
611         QList<int> list;
612         if (selectedRows().contains( index.row()) )
613         {
614             // select all selected rows if mouse is over selection area
615             list = selectedRows();
616         }
617         else
618         {
619             // select only current mouse-over-index if mouse is out of selection area
620             list.append( index.row() );
621         }
622 
623         if( index.data( Playlist::QueuePositionRole ).toInt() != 0 )
624             Actions::instance()->dequeue( list );
625         else
626             Actions::instance()->queue( list );
627     }
628 }
629 
630 void
mouseReleaseEvent(QMouseEvent * event)631 Playlist::PrettyListView::mouseReleaseEvent( QMouseEvent* event )
632 {
633     if ( mouseEventInHeader( event ) && ( event->button() == Qt::LeftButton ) && m_mousePressInHeader && m_headerPressIndex.isValid() )
634     {
635         QModelIndex index = indexAt( event->pos() );
636         if ( index == m_headerPressIndex )
637         {
638             int rows = index.data( GroupedTracksRole ).toInt();
639             QModelIndex bottomIndex = model()->index( index.row() + rows - 1, 0 );
640             QItemSelection selItems( index, bottomIndex );
641             QItemSelectionModel::SelectionFlags command = headerReleaseSelectionCommand( index, event );
642             selectionModel()->select( selItems, command );
643         }
644         event->accept();
645     }
646     else
647     {
648         QListView::mouseReleaseEvent( event );
649     }
650     m_mousePressInHeader = false;
651 }
652 
653 bool
mouseEventInHeader(const QMouseEvent * event) const654 Playlist::PrettyListView::mouseEventInHeader( const QMouseEvent* event ) const
655 {
656     QModelIndex index = indexAt( event->pos() );
657     if ( index.data( GroupRole ).toInt() == Grouping::Head )
658     {
659         QPoint mousePressPos = event->pos();
660         mousePressPos.rx() += horizontalOffset();
661         mousePressPos.ry() += verticalOffset();
662         return m_prettyDelegate->insideItemHeader( mousePressPos, rectForIndex( index ) );
663     }
664     return false;
665 }
666 
667 void
paintEvent(QPaintEvent * event)668 Playlist::PrettyListView::paintEvent( QPaintEvent *event )
669 {
670     if( m_dropIndicator.isValid() ||
671         model()->rowCount( rootIndex() ) == 0 )
672     {
673         QPainter painter( viewport() );
674 
675         if( m_dropIndicator.isValid() )
676         {
677             const QPoint offset( 6, 0 );
678             QColor c = QApplication::palette().color( QPalette::Highlight );
679             painter.setPen( QPen( c, 6, Qt::SolidLine, Qt::RoundCap ) );
680             painter.drawLine( m_dropIndicator.topLeft() + offset,
681                               m_dropIndicator.topRight() - offset );
682         }
683 
684         if( model()->rowCount( rootIndex() ) == 0 )
685         {
686             // here we assume that an empty list is caused by the filter if it's active
687             QString emptyText;
688             if( m_showOnlyMatches && Playlist::ModelStack::instance()->bottom()->rowCount() > 0 )
689                 emptyText = i18n( "Tracks have been hidden due to the active search." );
690             else
691                 emptyText = i18n( "Add some songs here by dragging them from all around." );
692 
693             QColor c = QApplication::palette().color( foregroundRole() );
694             c.setAlpha( c.alpha() / 2 );
695             painter.setPen( c );
696             painter.drawText( rect(),
697                               Qt::AlignCenter | Qt::TextWordWrap,
698                               emptyText );
699         }
700     }
701 
702     QListView::paintEvent( event );
703 }
704 
705 void
startDrag(Qt::DropActions supportedActions)706 Playlist::PrettyListView::startDrag( Qt::DropActions supportedActions )
707 {
708     DEBUG_BLOCK
709 
710     QModelIndexList indices = selectedIndexes();
711     if( indices.isEmpty() )
712         return; // no items selected in the view, abort. See bug 226167
713 
714     //Waah? when a parent item is dragged, startDrag is called a bunch of times
715     static bool ongoingDrags = false;
716     if( ongoingDrags )
717         return;
718     ongoingDrags = true;
719 
720     if( !m_pd )
721         m_pd = The::popupDropperFactory()->createPopupDropper( Context::ContextView::self() );
722 
723     if( m_pd && m_pd->isHidden() )
724     {
725         m_pd->setSvgRenderer( The::svgHandler()->getRenderer( QStringLiteral("amarok/images/pud_items.svg") ) );
726         qDebug() << "svgHandler SVG renderer is " << (QObject*)(The::svgHandler()->getRenderer( QStringLiteral("amarok/images/pud_items.svg") ));
727         qDebug() << "m_pd SVG renderer is " << (QObject*)(m_pd->svgRenderer());
728         qDebug() << "does play exist in renderer? " << ( The::svgHandler()->getRenderer( QStringLiteral("amarok/images/pud_items.svg") )->elementExists( QStringLiteral("load") ) );
729 
730         QList<QAction*> actions =  actionsFor( this, &indices.first() );
731         foreach( QAction * action, actions )
732             m_pd->addItem( The::popupDropperFactory()->createItem( action ), true );
733 
734         m_pd->show();
735     }
736 
737     QListView::startDrag( supportedActions );
738     debug() << "After the drag!";
739 
740     if( m_pd )
741     {
742         debug() << "clearing PUD";
743         connect( m_pd, &PopupDropper::fadeHideFinished, m_pd, &PopupDropper::clear );
744         m_pd->hide();
745     }
746     ongoingDrags = false;
747 }
748 
749 bool
edit(const QModelIndex & index,EditTrigger trigger,QEvent * event)750 Playlist::PrettyListView::edit( const QModelIndex &index, EditTrigger trigger, QEvent *event )
751 {
752     // we want to prevent a click to change the selection and open the editor (BR 220818)
753     if( m_lastTimeSelectionChanged.msecsTo( QDateTime::currentDateTime() ) < qApp->doubleClickInterval() + 50 )
754         return false;
755     return QListView::edit( index, trigger, event );
756 }
757 
758 QItemSelectionModel::SelectionFlags
headerPressSelectionCommand(const QModelIndex & index,const QMouseEvent * event) const759 Playlist::PrettyListView::headerPressSelectionCommand( const QModelIndex& index, const QMouseEvent* event ) const
760 {
761     if ( !index.isValid() )
762         return QItemSelectionModel::NoUpdate;
763 
764     const bool shiftKeyPressed = event->modifiers() & Qt::ShiftModifier;
765     //const bool controlKeyPressed = event->modifiers() & Qt::ControlModifier;
766     const bool indexIsSelected = selectionModel()->isSelected( index );
767     const bool controlKeyPressed = event->modifiers() & Qt::ControlModifier;
768 
769     if ( shiftKeyPressed )
770         return QItemSelectionModel::SelectCurrent;
771 
772     if ( indexIsSelected && controlKeyPressed ) //make this consistent with how single items work. This also makes it possible to drag the header
773         return QItemSelectionModel::Deselect;
774 
775     return QItemSelectionModel::Select;
776 }
777 
778 QItemSelectionModel::SelectionFlags
headerReleaseSelectionCommand(const QModelIndex & index,const QMouseEvent * event) const779 Playlist::PrettyListView::headerReleaseSelectionCommand( const QModelIndex& index, const QMouseEvent* event ) const
780 {
781     if ( !index.isValid() )
782         return QItemSelectionModel::NoUpdate;
783 
784     const bool shiftKeyPressed = event->modifiers() & Qt::ShiftModifier;
785     const bool controlKeyPressed = event->modifiers() & Qt::ControlModifier;
786 
787     if ( !controlKeyPressed && !shiftKeyPressed )
788         return QItemSelectionModel::ClearAndSelect;
789     return QItemSelectionModel::NoUpdate;
790 }
791 
792 QList<int>
selectedRows() const793 Playlist::PrettyListView::selectedRows() const
794 {
795     QList<int> rows;
796     foreach( const QModelIndex &idx, selectedIndexes() )
797         rows.append( idx.row() );
798     return rows;
799 }
800 
newPalette(const QPalette & palette)801 void Playlist::PrettyListView::newPalette( const QPalette & palette )
802 {
803     Q_UNUSED( palette )
804     The::paletteHandler()->updateItemView( this );
805     reset();
806 }
807 
find(const QString & searchTerm,int fields,bool filter)808 void Playlist::PrettyListView::find( const QString &searchTerm, int fields, bool filter )
809 {
810     bool updateProxy = false;
811     if ( ( The::playlist()->currentSearchFields() != fields ) || ( The::playlist()->currentSearchTerm() != searchTerm ) )
812         updateProxy = true;
813 
814     m_searchTerm = searchTerm;
815     m_fields = fields;
816     m_filter = filter;
817 
818     int row = The::playlist()->find( m_searchTerm, m_fields );
819     if( row != -1 )
820     {
821         //select this track
822         QModelIndex index = model()->index( row, 0 );
823         QItemSelection selItems( index, index );
824         selectionModel()->select( selItems, QItemSelectionModel::SelectCurrent );
825     }
826 
827     //instead of kicking the proxy right away, start a small timeout.
828     //this stops us from updating it for each letter of a long search term,
829     //and since it does not affect any views, this is fine. Worst case is that
830     //a navigator skips to a track form the old search if the track change happens
831     //before this  timeout. Only start count if values have actually changed!
832     if ( updateProxy )
833         startProxyUpdateTimeout();
834 }
835 
findNext(const QString & searchTerm,int fields)836 void Playlist::PrettyListView::findNext( const QString & searchTerm, int fields )
837 {
838     DEBUG_BLOCK
839     QList<int> selected = selectedRows();
840 
841     bool updateProxy = false;
842     if ( ( The::playlist()->currentSearchFields() != fields ) || ( The::playlist()->currentSearchTerm() != searchTerm ) )
843         updateProxy = true;
844 
845     int currentRow = -1;
846     if( selected.size() > 0 )
847         currentRow = selected.last();
848 
849     int row = The::playlist()->findNext( searchTerm, currentRow, fields );
850     if( row != -1 )
851     {
852         //select this track
853 
854         QModelIndex index = model()->index( row, 0 );
855         QItemSelection selItems( index, index );
856         selectionModel()->select( selItems, QItemSelectionModel::SelectCurrent );
857 
858         QModelIndex foundIndex = model()->index( row, 0, QModelIndex() );
859         setCurrentIndex( foundIndex );
860         if ( foundIndex.isValid() )
861             scrollTo( foundIndex, QAbstractItemView::PositionAtCenter );
862 
863         Q_EMIT( found() );
864     }
865     else
866         Q_EMIT( notFound() );
867 
868     if ( updateProxy )
869         The::playlist()->filterUpdated();
870 }
871 
findPrevious(const QString & searchTerm,int fields)872 void Playlist::PrettyListView::findPrevious( const QString & searchTerm, int fields )
873 {
874     DEBUG_BLOCK
875     QList<int> selected = selectedRows();
876 
877     bool updateProxy = false;
878     if ( ( The::playlist()->currentSearchFields() != fields ) || ( The::playlist()->currentSearchTerm() != searchTerm ) )
879         updateProxy = true;
880 
881     int currentRow = model()->rowCount();
882     if( selected.size() > 0 )
883         currentRow = selected.first();
884 
885     int row = The::playlist()->findPrevious( searchTerm, currentRow, fields );
886     if( row != -1 )
887     {
888         //select this track
889 
890         QModelIndex index = model()->index( row, 0 );
891         QItemSelection selItems( index, index );
892         selectionModel()->select( selItems, QItemSelectionModel::SelectCurrent );
893 
894         QModelIndex foundIndex = model()->index( row, 0, QModelIndex() );
895         setCurrentIndex( foundIndex );
896         if ( foundIndex.isValid() )
897             scrollTo( foundIndex, QAbstractItemView::PositionAtCenter );
898 
899         Q_EMIT( found() );
900     }
901     else
902         Q_EMIT( notFound() );
903 
904     if ( updateProxy )
905         The::playlist()->filterUpdated();
906 }
907 
clearSearchTerm()908 void Playlist::PrettyListView::clearSearchTerm()
909 {
910     DEBUG_BLOCK
911 
912     // Choose a focus item, to scroll to later.
913     QModelIndex focusIndex;
914     QModelIndexList selected = selectedIndexes();
915     if( !selected.isEmpty() )
916         focusIndex = selected.first();
917     else
918         focusIndex = indexAt( QPoint( 0, 0 ) );
919 
920     // Remember the focus item id, because the row numbers change when we reset the filter.
921     quint64 focusItemId = The::playlist()->idAt( focusIndex.row() );
922 
923     The::playlist()->clearSearchTerm();
924     The::playlist()->filterUpdated();
925 
926     // Now scroll to the focus item.
927     QModelIndex newIndex = model()->index( The::playlist()->rowForId( focusItemId ), 0, QModelIndex() );
928     if ( newIndex.isValid() )
929         scrollTo( newIndex, QAbstractItemView::PositionAtCenter );
930 }
931 
startProxyUpdateTimeout()932 void Playlist::PrettyListView::startProxyUpdateTimeout()
933 {
934     DEBUG_BLOCK
935     if ( m_proxyUpdateTimer->isActive() )
936         m_proxyUpdateTimer->stop();
937 
938     m_proxyUpdateTimer->setInterval( 200 );
939     m_proxyUpdateTimer->start();
940 }
941 
updateProxyTimeout()942 void Playlist::PrettyListView::updateProxyTimeout()
943 {
944     DEBUG_BLOCK
945     The::playlist()->filterUpdated();
946 
947     int row = The::playlist()->find( m_searchTerm, m_fields );
948     if( row != -1 )
949     {
950         QModelIndex foundIndex = model()->index( row, 0, QModelIndex() );
951         setCurrentIndex( foundIndex );
952 
953         if ( !m_filter )
954         {
955             if ( foundIndex.isValid() )
956                 scrollTo( foundIndex, QAbstractItemView::PositionAtCenter );
957         }
958 
959         Q_EMIT( found() );
960     }
961     else
962         Q_EMIT( notFound() );
963 }
964 
showOnlyMatches(bool onlyMatches)965 void Playlist::PrettyListView::showOnlyMatches( bool onlyMatches )
966 {
967     m_showOnlyMatches = onlyMatches;
968 
969     The::playlist()->showOnlyMatches( onlyMatches );
970 }
971 
972 // Handle scrolling to newly inserted playlist items.
973 // Warning, this slot is connected to the 'rowsInserted' signal of the *bottom* model,
974 // not the normal top model.
975 // The reason: FilterProxy can Q_EMIT *A LOT* (thousands) of 'rowsInserted' signals when its
976 // search string changes. For that case we don't want to do any scrollTo() at all.
977 void
bottomModelRowsInserted(const QModelIndex & parent,int start,int end)978 Playlist::PrettyListView::bottomModelRowsInserted( const QModelIndex& parent, int start, int end )
979 {
980     Q_UNUSED( parent )
981     Q_UNUSED( end )
982 
983     // skip scrolling if tracks were added while playlist is in dynamicMode
984     if( m_rowsInsertedScrollItem == 0 && !AmarokConfig::dynamicMode() )
985     {
986         m_rowsInsertedScrollItem = Playlist::ModelStack::instance()->bottom()->idAt( start );
987         QTimer::singleShot( 0, this, &Playlist::PrettyListView::bottomModelRowsInsertedScroll );
988     }
989 }
990 
bottomModelRowsInsertedScroll()991 void Playlist::PrettyListView::bottomModelRowsInsertedScroll()
992 {
993     DEBUG_BLOCK
994 
995     if( m_rowsInsertedScrollItem )
996     {   // Note: we don't bother handling the case "first inserted item in bottom model
997         // does not have a row in the top 'model()' due to FilterProxy" nicely.
998         int firstRowInserted = The::playlist()->rowForId( m_rowsInsertedScrollItem );    // In the *top* model.
999         QModelIndex index = model()->index( firstRowInserted, 0 );
1000 
1001         if( index.isValid() )
1002             scrollTo( index, QAbstractItemView::PositionAtCenter );
1003 
1004         m_rowsInsertedScrollItem = 0;
1005     }
1006 }
1007 
redrawActive()1008 void Playlist::PrettyListView::redrawActive()
1009 {
1010     int activeRow = The::playlist()->activeRow();
1011     QModelIndex index = model()->index( activeRow, 0, QModelIndex() );
1012     update( index );
1013 }
1014 
playlistLayoutChanged()1015 void Playlist::PrettyListView::playlistLayoutChanged()
1016 {
1017     if ( LayoutManager::instance()->activeLayout().inlineControls() )
1018         m_animationTimer->start();
1019     else
1020         m_animationTimer->stop();
1021 
1022     // -- update the tooltip columns in the playlist model
1023     bool tooltipColumns[Playlist::NUM_COLUMNS];
1024     for( int i=0; i<Playlist::NUM_COLUMNS; ++i )
1025         tooltipColumns[i] = true;
1026 
1027     // bool excludeCover = false;
1028 
1029     for( int part = 0; part < PlaylistLayout::NumParts; part++ )
1030     {
1031         // bool single = ( part == PlaylistLayout::Single );
1032         Playlist::PlaylistLayout layout = Playlist::LayoutManager::instance()->activeLayout();
1033         Playlist::LayoutItemConfig item = layout.layoutForPart( (PlaylistLayout::Part)part );
1034 
1035         for (int activeRow = 0; activeRow < item.rows(); activeRow++)
1036         {
1037             for (int activeElement = 0; activeElement < item.row(activeRow).count();activeElement++)
1038             {
1039                 Playlist::Column column = (Playlist::Column)item.row(activeRow).element(activeElement).value();
1040                 tooltipColumns[column] = false;
1041             }
1042         }
1043         // excludeCover |= item.showCover();
1044     }
1045     Playlist::Model::setTooltipColumns( tooltipColumns );
1046     Playlist::Model::enableToolTip( Playlist::LayoutManager::instance()->activeLayout().tooltips() );
1047 
1048     update();
1049 
1050     // Schedule a re-scroll to the active playlist row. Assumption: Qt will run this *after* the repaint.
1051     QTimer::singleShot( 0, this, &Playlist::PrettyListView::slotPlaylistActiveTrackChanged );
1052 }
1053 
1054