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