File indexing completed on 2025-01-05 04:26:38

0001 /****************************************************************************************
0002  * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
0003  * Copyright (c) 2009 Téo Mrnjavac <teo@kde.org>                                        *
0004  * Copyright (c) 2009 Nikolaj Hald Nielsen <nhn@kde.org>                                *
0005  * Copyright (c) 2009 John Atkinson <john@fauxnetic.co.uk>                              *
0006  * Copyright (c) 2009-2010 Oleksandr Khayrullin <saniokh@gmail.com>                     *
0007  * Copyright (c) 2010 Nanno Langstraat <langstr@gmail.com>                              *
0008  *                                                                                      *
0009  * This program is free software; you can redistribute it and/or modify it under        *
0010  * the terms of the GNU General Public License as published by the Free Software        *
0011  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
0012  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
0013  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
0014  * version 3 of the license.                                                            *
0015  *                                                                                      *
0016  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0017  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0018  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0019  *                                                                                      *
0020  * You should have received a copy of the GNU General Public License along with         *
0021  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0022  ****************************************************************************************/
0023 
0024 #define DEBUG_PREFIX "Playlist::PrettyListView"
0025 
0026 #include "PrettyListView.h"
0027 
0028 #include "amarokconfig.h"
0029 #include "AmarokMimeData.h"
0030 #include "context/ContextView.h"
0031 #include "context/popupdropper/libpud/PopupDropper.h"
0032 #include "core/support/Debug.h"
0033 #include "EngineController.h"
0034 #include "dialogs/TagDialog.h"
0035 #include "GlobalCurrentTrackActions.h"
0036 #include "core/capabilities/ActionsCapability.h"
0037 #include "core/capabilities/FindInSourceCapability.h"
0038 #include "core/capabilities/MultiSourceCapability.h"
0039 #include "core/meta/Meta.h"
0040 #include "PaletteHandler.h"
0041 #include "playlist/layouts/LayoutManager.h"
0042 #include "playlist/proxymodels/GroupingProxy.h"
0043 #include "playlist/PlaylistActions.h"
0044 #include "playlist/PlaylistModelStack.h"
0045 #include "playlist/PlaylistController.h"
0046 #include "playlist/view/PlaylistViewCommon.h"
0047 #include "playlist/PlaylistDefines.h"
0048 #include "PopupDropperFactory.h"
0049 #include "SvgHandler.h"
0050 #include "SourceSelectionPopup.h"
0051 
0052 #include <QApplication>
0053 #include <QClipboard>
0054 #include <QContextMenuEvent>
0055 #include <QDropEvent>
0056 #include <QItemSelection>
0057 #include <QKeyEvent>
0058 #include <QListView>
0059 #include <QMenu>
0060 #include <QModelIndex>
0061 #include <QMouseEvent>
0062 #include <QPainter>
0063 #include <QPalette>
0064 #include <QPersistentModelIndex>
0065 #include <QScrollBar>
0066 #include <QSvgRenderer>
0067 #include <QTimer>
0068 #include <QUrl>
0069 
0070 #include <KLocalizedString>
0071 
0072 
0073 Playlist::PrettyListView::PrettyListView( QWidget* parent )
0074         : QListView( parent )
0075         , ViewCommon()
0076         , m_headerPressIndex( QModelIndex() )
0077         , m_mousePressInHeader( false )
0078         , m_skipAutoScroll( false )
0079         , m_firstScrollToActiveTrack( true )
0080         , m_rowsInsertedScrollItem( 0 )
0081         , m_showOnlyMatches( false )
0082         , m_pd( nullptr )
0083 {
0084     // QAbstractItemView basics
0085     setModel( The::playlist()->qaim() );
0086 
0087     m_prettyDelegate = new PrettyItemDelegate( this );
0088     connect( m_prettyDelegate, &PrettyItemDelegate::redrawRequested, this, &Playlist::PrettyListView::redrawActive );
0089     setItemDelegate( m_prettyDelegate );
0090 
0091     setSelectionMode( ExtendedSelection );
0092     setDragDropMode( DragDrop );
0093     setDropIndicatorShown( false ); // we draw our own drop indicator
0094     setEditTriggers ( SelectedClicked | EditKeyPressed );
0095     setAutoScroll( true );
0096 
0097     setVerticalScrollMode( ScrollPerPixel );
0098 
0099     setMouseTracking( true );
0100 
0101     // Rendering adjustments
0102     setFrameShape( QFrame::NoFrame );
0103     setAlternatingRowColors( true) ;
0104     The::paletteHandler()->updateItemView( this );
0105     connect( The::paletteHandler(), &PaletteHandler::newPalette, this, &PrettyListView::newPalette );
0106 
0107     setAutoFillBackground( false );
0108 
0109 
0110     // Signal connections
0111     connect( this, &Playlist::PrettyListView::doubleClicked,
0112              this, &Playlist::PrettyListView::trackActivated );
0113     connect( selectionModel(), &QItemSelectionModel::selectionChanged,
0114              this, &Playlist::PrettyListView::slotSelectionChanged );
0115 
0116     connect( LayoutManager::instance(), &LayoutManager::activeLayoutChanged, this, &PrettyListView::playlistLayoutChanged );
0117 
0118     if (auto m = static_cast<Playlist::Model*>(model()))
0119     {
0120         connect( m, &Playlist::Model::activeTrackChanged, this, &Playlist::PrettyListView::slotPlaylistActiveTrackChanged );
0121         connect( m, &Playlist::Model::queueChanged, viewport(), QOverload<>::of(&QWidget::update) );
0122     }
0123     else
0124         warning() << "Model is not a Playlist::Model";
0125 
0126     //   Warning, this one doesn't connect to the normal 'model()' (i.e. '->top()'), but to '->bottom()'.
0127     connect( Playlist::ModelStack::instance()->bottom(), &Playlist::Model::rowsInserted, this, &Playlist::PrettyListView::bottomModelRowsInserted );
0128 
0129     // Timers
0130     m_proxyUpdateTimer = new QTimer( this );
0131     m_proxyUpdateTimer->setSingleShot( true );
0132     connect( m_proxyUpdateTimer, &QTimer::timeout, this, &Playlist::PrettyListView::updateProxyTimeout );
0133 
0134     m_animationTimer = new QTimer(this);
0135     connect( m_animationTimer, &QTimer::timeout, this, &Playlist::PrettyListView::redrawActive );
0136     m_animationTimer->setInterval( 250 );
0137 
0138     playlistLayoutChanged();
0139 
0140     // We do the following call here to be formally correct, but note:
0141     //   - It happens to be redundant, because 'playlistLayoutChanged()' already schedules
0142     //     another one, via a QTimer( 0 ).
0143     //   - Both that one and this one don't work right (they scroll like 'PositionAtTop',
0144     //     not 'PositionAtCenter'). This is probably because MainWindow changes its
0145     //     geometry in a QTimer( 0 )? As a fix, MainWindow does a 'slotShowActiveTrack()'
0146     //     at the end of its QTimer slot, which will finally scroll to the right spot.
0147     slotPlaylistActiveTrackChanged();
0148 }
0149 
0150 Playlist::PrettyListView::~PrettyListView()
0151 {}
0152 
0153 int
0154 Playlist::PrettyListView::verticalOffset() const
0155 {
0156     int ret = QListView::verticalOffset();
0157     if ( verticalScrollBar() && verticalScrollBar()->maximum() )
0158         ret += verticalScrollBar()->value() * 10 / verticalScrollBar()->maximum();
0159     return ret;
0160 }
0161 
0162 void
0163 Playlist::PrettyListView::editTrackInformation()
0164 {
0165     Meta::TrackList tl;
0166     foreach( const QModelIndex &index, selectedIndexes() )
0167     {
0168         tl.append( index.data( TrackRole ).value<Meta::TrackPtr>() );
0169     }
0170 
0171     if( !tl.isEmpty() )
0172     {
0173         TagDialog *dialog = new TagDialog( tl, this );
0174         dialog->show();
0175     }
0176 }
0177 
0178 void
0179 Playlist::PrettyListView::playFirstSelected()
0180 {
0181     QModelIndexList selected = selectedIndexes();
0182     if( !selected.isEmpty() )
0183         trackActivated( selected.first() );
0184 }
0185 
0186 void
0187 Playlist::PrettyListView::removeSelection()
0188 {
0189     QList<int> sr = selectedRows();
0190     if( !sr.isEmpty() )
0191     {
0192         // Now that we have the list of selected rows in the topmost proxy, we can perform the
0193         // removal.
0194         The::playlistController()->removeRows( sr );
0195 
0196         // Next, we look for the first row.
0197         int firstRow = sr.first();
0198         foreach( int i, sr )
0199         {
0200             if( i < firstRow )
0201                 firstRow = i;
0202         }
0203 
0204         //Select the track occupied by the first deleted track. Also move the current item to here as
0205         //button presses up or down wil otherwise not behave as expected.
0206         firstRow = qBound( 0, firstRow, model()->rowCount() - 1 );
0207         QModelIndex newSelectionIndex = model()->index( firstRow, 0 );
0208         setCurrentIndex( newSelectionIndex );
0209         selectionModel()->select( newSelectionIndex, QItemSelectionModel::Select );
0210     }
0211 }
0212 
0213 void
0214 Playlist::PrettyListView::queueSelection()
0215 {
0216     Actions::instance()->queue( selectedRows() );
0217 }
0218 
0219 void
0220 Playlist::PrettyListView::dequeueSelection()
0221 {
0222     Actions::instance()->dequeue( selectedRows() );
0223 }
0224 
0225 void
0226 Playlist::PrettyListView::switchQueueState() // slot
0227 {
0228     DEBUG_BLOCK
0229     const bool isQueued = currentIndex().data( Playlist::QueuePositionRole ).toInt() != 0;
0230     isQueued ? dequeueSelection() : queueSelection();
0231 }
0232 
0233 void Playlist::PrettyListView::selectSource()
0234 {
0235     DEBUG_BLOCK
0236 
0237     QList<int> rows = selectedRows();
0238 
0239     //for now, bail out of more than 1 row...
0240     if ( rows.count() != 1 )
0241         return;
0242 
0243     //get the track...
0244     QModelIndex index = model()->index( rows.at( 0 ), 0 );
0245     Meta::TrackPtr track = index.data( Playlist::TrackRole ).value< Meta::TrackPtr >();
0246 
0247     //get multiSource capability:
0248 
0249     Capabilities::MultiSourceCapability *msc = track->create<Capabilities::MultiSourceCapability>();
0250     if ( msc )
0251     {
0252         debug() << "sources: " << msc->sources();
0253         SourceSelectionPopup * sourceSelector = new SourceSelectionPopup( this, msc );
0254         sourceSelector->show();
0255         //dialog deletes msc when done with it.
0256     }
0257 }
0258 
0259 void
0260 Playlist::PrettyListView::scrollToActiveTrack()
0261 {
0262     DEBUG_BLOCK
0263 
0264     if( m_skipAutoScroll )
0265     {
0266         m_skipAutoScroll = false;
0267         return;
0268     }
0269 
0270     QModelIndex activeIndex = model()->index( The::playlist()->activeRow(), 0, QModelIndex() );
0271     if ( activeIndex.isValid() )
0272     {
0273         scrollTo( activeIndex, QAbstractItemView::PositionAtCenter );
0274         m_firstScrollToActiveTrack = false;
0275         m_rowsInsertedScrollItem = 0;    // This "new active track" scroll supersedes a pending "rows inserted" scroll.
0276     }
0277 }
0278 
0279 void
0280 Playlist::PrettyListView::downOneTrack()
0281 {
0282     DEBUG_BLOCK
0283 
0284     moveTrackSelection( 1 );
0285 }
0286 
0287 void
0288 Playlist::PrettyListView::upOneTrack()
0289 {
0290     DEBUG_BLOCK
0291 
0292     moveTrackSelection( -1 );
0293 }
0294 
0295 void
0296 Playlist::PrettyListView::moveTrackSelection( int offset )
0297 {
0298     if ( offset == 0 )
0299         return;
0300 
0301     int finalRow = model()->rowCount() - 1;
0302     int target = 0;
0303 
0304     if ( offset < 0 )
0305         target = finalRow;
0306 
0307     QList<int> rows = selectedRows();
0308     if ( rows.count() > 0 )
0309         target = rows.at( 0 ) + offset;
0310 
0311     target = qBound(0, target, finalRow);
0312     QModelIndex index = model()->index( target, 0 );
0313     setCurrentIndex( index );
0314 }
0315 
0316 void
0317 Playlist::PrettyListView::slotPlaylistActiveTrackChanged()
0318 {
0319     DEBUG_BLOCK
0320 
0321     // A playlist 'activeTrackChanged' signal happens:
0322     //   - During startup, on "saved playlist" load. (Might happen before this view exists)
0323     //   - When Amarok starts playing a new item in the playlist.
0324     //     In that case, don't auto-scroll if the user doesn't like us to.
0325 
0326     if( AmarokConfig::autoScrollPlaylist() || m_firstScrollToActiveTrack )
0327         scrollToActiveTrack();
0328 }
0329 
0330 void
0331 Playlist::PrettyListView::slotSelectionChanged()
0332 {
0333     m_lastTimeSelectionChanged = QDateTime::currentDateTime();
0334 }
0335 
0336 void
0337 Playlist::PrettyListView::trackActivated( const QModelIndex& idx )
0338 {
0339     DEBUG_BLOCK
0340     m_skipAutoScroll = true; // we don't want to do crazy view changes when selecting an item in the view
0341     Actions::instance()->play( idx );
0342 
0343     //make sure that the track we just activated is also set as the current index or
0344     //the selected index will get moved to the first row, making keyboard navigation difficult (BUG 225791)
0345     selectionModel_setCurrentIndex( idx, QItemSelectionModel::ClearAndSelect );
0346 
0347     setFocus();
0348 }
0349 
0350 
0351 // The following 2 functions are a workaround for crash BUG 222961 and BUG 229240:
0352 //   There appears to be a bad interaction between Qt 'setCurrentIndex()' and
0353 //   Qt 'selectedIndexes()' / 'selectionModel()->select()' / 'scrollTo()'.
0354 //
0355 //   'setCurrentIndex()' appears to do something bad with its QModelIndex parameter,
0356 //   leading to a crash deep within Qt.
0357 //
0358 //   It might be our fault, but we suspect a bug in Qt.  (Qt 4.6 at least)
0359 //
0360 //   The problem goes away if we use a fresh QModelIndex, which we also don't re-use
0361 //   afterwards.
0362 void
0363 Playlist::PrettyListView::setCurrentIndex( const QModelIndex &index )
0364 {
0365     QModelIndex indexCopy = model()->index( index.row(), index.column() );
0366     QListView::setCurrentIndex( indexCopy );
0367 }
0368 
0369 void
0370 Playlist::PrettyListView::selectionModel_setCurrentIndex( const QModelIndex &index, QItemSelectionModel::SelectionFlags command )
0371 {
0372     QModelIndex indexCopy = model()->index( index.row(), index.column() );
0373     selectionModel()->setCurrentIndex( indexCopy, command );
0374 }
0375 
0376 void
0377 Playlist::PrettyListView::showEvent( QShowEvent* event )
0378 {
0379     QTimer::singleShot( 0, this, &Playlist::PrettyListView::fixInvisible );
0380 
0381     QListView::showEvent( event );
0382 }
0383 
0384 // This method is a workaround for BUG 184714.
0385 //
0386 // It prevents the playlist from becoming invisible (clear) after changing the model, while Amarok is hidden in the tray.
0387 // Without this workaround the playlist stays invisible when the application is restored from the tray.
0388 // This is especially a problem with the Dynamic Playlist mode, which modifies the model without user interaction.
0389 //
0390 // The bug only seems to happen with Qt 4.5.x, so it might actually be a bug in Qt.
0391 void
0392 Playlist::PrettyListView::fixInvisible() //SLOT
0393 {
0394     // DEBUG_BLOCK
0395 
0396     // Part 1: Palette change
0397     newPalette( palette() );
0398 
0399     // Part 2: Change item selection
0400     const QItemSelection oldSelection( selectionModel()->selection() );
0401     selectionModel()->clear();
0402     selectionModel()->select( oldSelection, QItemSelectionModel::SelectCurrent );
0403 
0404     // NOTE: A simple update() call is not sufficient, but in fact the above two steps are required.
0405 }
0406 
0407 void
0408 Playlist::PrettyListView::contextMenuEvent( QContextMenuEvent* event )
0409 {
0410     DEBUG_BLOCK
0411     QModelIndex index = indexAt( event->pos() );
0412 
0413     if ( !index.isValid() )
0414         return;
0415 
0416     //Ctrl + Right Click is used for queuing
0417     if( event->modifiers() & Qt::ControlModifier )
0418         return;
0419 
0420     trackMenu( this, &index, event->globalPos() );
0421     event->accept();
0422 }
0423 
0424 void
0425 Playlist::PrettyListView::dragLeaveEvent( QDragLeaveEvent* event )
0426 {
0427     m_mousePressInHeader = false;
0428     m_dropIndicator = QRect( 0, 0, 0, 0 );
0429     QListView::dragLeaveEvent( event );
0430 }
0431 
0432 void
0433 Playlist::PrettyListView::stopAfterTrack()
0434 {
0435     const quint64 id = currentIndex().data( UniqueIdRole ).value<quint64>();
0436     if( Actions::instance()->willStopAfterTrack( id ) )
0437         Actions::instance()->stopAfterPlayingTrack( 0 ); // disable stopping
0438     else
0439         Actions::instance()->stopAfterPlayingTrack( id );
0440 }
0441 
0442 void
0443 Playlist::PrettyListView::findInSource()
0444 {
0445     DEBUG_BLOCK
0446 
0447     Meta::TrackPtr track = currentIndex().data( TrackRole ).value<Meta::TrackPtr>();
0448     if ( track )
0449     {
0450         if( track->has<Capabilities::FindInSourceCapability>() )
0451         {
0452             Capabilities::FindInSourceCapability *fis = track->create<Capabilities::FindInSourceCapability>();
0453             if ( fis )
0454             {
0455                 fis->findInSource();
0456             }
0457             delete fis;
0458         }
0459     }
0460 }
0461 
0462 void
0463 Playlist::PrettyListView::dragEnterEvent( QDragEnterEvent *event )
0464 {
0465     const QMimeData *mime = event->mimeData();
0466     if( mime->hasUrls() ||
0467         mime->hasFormat( AmarokMimeData::TRACK_MIME ) ||
0468         mime->hasFormat( AmarokMimeData::PLAYLIST_MIME ) ||
0469         mime->hasFormat( AmarokMimeData::PODCASTEPISODE_MIME ) ||
0470         mime->hasFormat( AmarokMimeData::PODCASTCHANNEL_MIME ) )
0471     {
0472         event->acceptProposedAction();
0473     }
0474 }
0475 
0476 void
0477 Playlist::PrettyListView::dragMoveEvent( QDragMoveEvent* event )
0478 {
0479     QModelIndex index = indexAt( event->pos() );
0480     if ( index.isValid() )
0481     {
0482         m_dropIndicator = visualRect( index );
0483     }
0484     else
0485     {
0486         // draw it on the bottom of the last item
0487         index = model()->index( model()->rowCount() - 1, 0, QModelIndex() );
0488         m_dropIndicator = visualRect( index );
0489         m_dropIndicator = m_dropIndicator.translated( 0, m_dropIndicator.height() );
0490     }
0491     QListView::dragMoveEvent( event );
0492 }
0493 
0494 void
0495 Playlist::PrettyListView::dropEvent( QDropEvent* event )
0496 {
0497     DEBUG_BLOCK
0498     QRect oldDrop = m_dropIndicator;
0499     m_dropIndicator = QRect( 0, 0, 0, 0 );
0500     if ( qobject_cast<PrettyListView*>( event->source() ) == this )
0501     {
0502         QAbstractItemModel* plModel = model();
0503         int targetRow = indexAt( event->pos() ).row();
0504         targetRow = ( targetRow < 0 ) ? plModel->rowCount() : targetRow; // target of < 0 means we dropped on the end of the playlist
0505         QList<int> sr = selectedRows();
0506         int realtarget = The::playlistController()->moveRows( sr, targetRow );
0507         QItemSelection selItems;
0508         foreach( int row, sr )
0509         {
0510             Q_UNUSED( row )
0511             selItems.select( plModel->index( realtarget, 0 ), plModel->index( realtarget, 0 ) );
0512             realtarget++;
0513         }
0514         selectionModel()->select( selItems, QItemSelectionModel::ClearAndSelect );
0515         event->accept();
0516     }
0517     else
0518     {
0519         QListView::dropEvent( event );
0520     }
0521     // add some padding around the old drop area which to repaint, as we add offsets when painting. See paintEvent().
0522     oldDrop.adjust( -6, -6, 6, 6 );
0523     repaint( oldDrop );
0524 }
0525 
0526 void
0527 Playlist::PrettyListView::keyPressEvent( QKeyEvent *event )
0528 {
0529     if( event->matches( QKeySequence::Delete ) )
0530     {
0531         removeSelection();
0532         event->accept();
0533     }
0534     else if( event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return )
0535     {
0536         trackActivated( currentIndex() );
0537         event->accept();
0538     }
0539     else if( event->matches( QKeySequence::SelectAll ) )
0540     {
0541         QModelIndex topIndex = model()->index( 0, 0 );
0542         QModelIndex bottomIndex = model()->index( model()->rowCount() - 1, 0 );
0543         QItemSelection selItems( topIndex, bottomIndex );
0544         selectionModel()->select( selItems, QItemSelectionModel::ClearAndSelect );
0545         event->accept();
0546     }
0547     else
0548         QListView::keyPressEvent( event );
0549 }
0550 
0551 void
0552 Playlist::PrettyListView::mousePressEvent( QMouseEvent* event )
0553 {
0554     //get the item that was clicked
0555     QModelIndex index = indexAt( event->pos() );
0556 
0557     //first of all, if a left click, check if the delegate wants to do something about this click
0558     if( event->button() == Qt::LeftButton )
0559     {
0560         //we need to translate the position of the click into something relative to the item that was clicked.
0561         QRect itemRect = visualRect( index );
0562         QPoint relPos =  event->pos() - itemRect.topLeft();
0563 
0564         if( m_prettyDelegate->clicked( relPos, itemRect, index ) )
0565         {
0566             event->accept();
0567             return;  //click already handled...
0568         }
0569     }
0570 
0571     if ( mouseEventInHeader( event ) && ( event->button() == Qt::LeftButton ) )
0572     {
0573         m_mousePressInHeader = true;
0574         m_headerPressIndex = QPersistentModelIndex( index );
0575         int rows = index.data( GroupedTracksRole ).toInt();
0576         QModelIndex bottomIndex = model()->index( index.row() + rows - 1, 0 );
0577 
0578         //offset by 1 as the actual header item is selected in QListView::mousePressEvent( event ); and is otherwise deselected again
0579         QItemSelection selItems( model()->index( index.row() + 1, 0 ), bottomIndex );
0580         QItemSelectionModel::SelectionFlags command = headerPressSelectionCommand( index, event );
0581         selectionModel()->select( selItems, command );
0582         // TODO: if you're doing shift-select on rows above the header, then the rows following the header will be lost from the selection
0583         selectionModel_setCurrentIndex( index, QItemSelectionModel::NoUpdate );
0584     }
0585     else
0586     {
0587         m_mousePressInHeader = false;
0588     }
0589 
0590     if ( event->button() == Qt::MidButton )
0591     {
0592         QUrl url( QApplication::clipboard()->text() );
0593         if ( url.isValid() )
0594         {
0595             QList<QUrl> urls = QList<QUrl>() << url;
0596             if( index.isValid() )
0597                 The::playlistController()->insertUrls( index.row() + 1, urls );
0598             else
0599                 The::playlistController()->insertOptioned( urls, Playlist::OnAppendToPlaylistAction );
0600         }
0601     }
0602 
0603     // This should always be forwarded, as it is used to determine the offset
0604     // relative to the mouse of the selection we are dragging!
0605     QListView::mousePressEvent( event );
0606 
0607     // This must go after the call to the super class as the current index is not yet selected otherwise
0608     // Queueing support for Ctrl Right click
0609     if( event->button() == Qt::RightButton && event->modifiers() & Qt::ControlModifier )
0610     {
0611         QList<int> list;
0612         if (selectedRows().contains( index.row()) )
0613         {
0614             // select all selected rows if mouse is over selection area
0615             list = selectedRows();
0616         }
0617         else
0618         {
0619             // select only current mouse-over-index if mouse is out of selection area
0620             list.append( index.row() );
0621         }
0622 
0623         if( index.data( Playlist::QueuePositionRole ).toInt() != 0 )
0624             Actions::instance()->dequeue( list );
0625         else
0626             Actions::instance()->queue( list );
0627     }
0628 }
0629 
0630 void
0631 Playlist::PrettyListView::mouseReleaseEvent( QMouseEvent* event )
0632 {
0633     if ( mouseEventInHeader( event ) && ( event->button() == Qt::LeftButton ) && m_mousePressInHeader && m_headerPressIndex.isValid() )
0634     {
0635         QModelIndex index = indexAt( event->pos() );
0636         if ( index == m_headerPressIndex )
0637         {
0638             int rows = index.data( GroupedTracksRole ).toInt();
0639             QModelIndex bottomIndex = model()->index( index.row() + rows - 1, 0 );
0640             QItemSelection selItems( index, bottomIndex );
0641             QItemSelectionModel::SelectionFlags command = headerReleaseSelectionCommand( index, event );
0642             selectionModel()->select( selItems, command );
0643         }
0644         event->accept();
0645     }
0646     else
0647     {
0648         QListView::mouseReleaseEvent( event );
0649     }
0650     m_mousePressInHeader = false;
0651 }
0652 
0653 bool
0654 Playlist::PrettyListView::mouseEventInHeader( const QMouseEvent* event ) const
0655 {
0656     QModelIndex index = indexAt( event->pos() );
0657     if ( index.data( GroupRole ).toInt() == Grouping::Head )
0658     {
0659         QPoint mousePressPos = event->pos();
0660         mousePressPos.rx() += horizontalOffset();
0661         mousePressPos.ry() += verticalOffset();
0662         return m_prettyDelegate->insideItemHeader( mousePressPos, rectForIndex( index ) );
0663     }
0664     return false;
0665 }
0666 
0667 void
0668 Playlist::PrettyListView::paintEvent( QPaintEvent *event )
0669 {
0670     if( m_dropIndicator.isValid() ||
0671         model()->rowCount( rootIndex() ) == 0 )
0672     {
0673         QPainter painter( viewport() );
0674 
0675         if( m_dropIndicator.isValid() )
0676         {
0677             const QPoint offset( 6, 0 );
0678             QColor c = QApplication::palette().color( QPalette::Highlight );
0679             painter.setPen( QPen( c, 6, Qt::SolidLine, Qt::RoundCap ) );
0680             painter.drawLine( m_dropIndicator.topLeft() + offset,
0681                               m_dropIndicator.topRight() - offset );
0682         }
0683 
0684         if( model()->rowCount( rootIndex() ) == 0 )
0685         {
0686             // here we assume that an empty list is caused by the filter if it's active
0687             QString emptyText;
0688             if( m_showOnlyMatches && Playlist::ModelStack::instance()->bottom()->rowCount() > 0 )
0689                 emptyText = i18n( "Tracks have been hidden due to the active search." );
0690             else
0691                 emptyText = i18n( "Add some songs here by dragging them from all around." );
0692 
0693             QColor c = QApplication::palette().color( foregroundRole() );
0694             c.setAlpha( c.alpha() / 2 );
0695             painter.setPen( c );
0696             painter.drawText( rect(),
0697                               Qt::AlignCenter | Qt::TextWordWrap,
0698                               emptyText );
0699         }
0700     }
0701 
0702     QListView::paintEvent( event );
0703 }
0704 
0705 void
0706 Playlist::PrettyListView::startDrag( Qt::DropActions supportedActions )
0707 {
0708     DEBUG_BLOCK
0709 
0710     QModelIndexList indices = selectedIndexes();
0711     if( indices.isEmpty() )
0712         return; // no items selected in the view, abort. See bug 226167
0713 
0714     //Waah? when a parent item is dragged, startDrag is called a bunch of times
0715     static bool ongoingDrags = false;
0716     if( ongoingDrags )
0717         return;
0718     ongoingDrags = true;
0719 
0720     if( !m_pd )
0721         m_pd = The::popupDropperFactory()->createPopupDropper( Context::ContextView::self() );
0722 
0723     if( m_pd && m_pd->isHidden() )
0724     {
0725         m_pd->setSvgRenderer( The::svgHandler()->getRenderer( QStringLiteral("amarok/images/pud_items.svg") ) );
0726         qDebug() << "svgHandler SVG renderer is " << (QObject*)(The::svgHandler()->getRenderer( QStringLiteral("amarok/images/pud_items.svg") ));
0727         qDebug() << "m_pd SVG renderer is " << (QObject*)(m_pd->svgRenderer());
0728         qDebug() << "does play exist in renderer? " << ( The::svgHandler()->getRenderer( QStringLiteral("amarok/images/pud_items.svg") )->elementExists( QStringLiteral("load") ) );
0729 
0730         QList<QAction*> actions =  actionsFor( this, &indices.first() );
0731         foreach( QAction * action, actions )
0732             m_pd->addItem( The::popupDropperFactory()->createItem( action ), true );
0733 
0734         m_pd->show();
0735     }
0736 
0737     QListView::startDrag( supportedActions );
0738     debug() << "After the drag!";
0739 
0740     if( m_pd )
0741     {
0742         debug() << "clearing PUD";
0743         connect( m_pd, &PopupDropper::fadeHideFinished, m_pd, &PopupDropper::clear );
0744         m_pd->hide();
0745     }
0746     ongoingDrags = false;
0747 }
0748 
0749 bool
0750 Playlist::PrettyListView::edit( const QModelIndex &index, EditTrigger trigger, QEvent *event )
0751 {
0752     // we want to prevent a click to change the selection and open the editor (BR 220818)
0753     if( m_lastTimeSelectionChanged.msecsTo( QDateTime::currentDateTime() ) < qApp->doubleClickInterval() + 50 )
0754         return false;
0755     return QListView::edit( index, trigger, event );
0756 }
0757 
0758 QItemSelectionModel::SelectionFlags
0759 Playlist::PrettyListView::headerPressSelectionCommand( const QModelIndex& index, const QMouseEvent* event ) const
0760 {
0761     if ( !index.isValid() )
0762         return QItemSelectionModel::NoUpdate;
0763 
0764     const bool shiftKeyPressed = event->modifiers() & Qt::ShiftModifier;
0765     //const bool controlKeyPressed = event->modifiers() & Qt::ControlModifier;
0766     const bool indexIsSelected = selectionModel()->isSelected( index );
0767     const bool controlKeyPressed = event->modifiers() & Qt::ControlModifier;
0768 
0769     if ( shiftKeyPressed )
0770         return QItemSelectionModel::SelectCurrent;
0771 
0772     if ( indexIsSelected && controlKeyPressed ) //make this consistent with how single items work. This also makes it possible to drag the header
0773         return QItemSelectionModel::Deselect;
0774 
0775     return QItemSelectionModel::Select;
0776 }
0777 
0778 QItemSelectionModel::SelectionFlags
0779 Playlist::PrettyListView::headerReleaseSelectionCommand( const QModelIndex& index, const QMouseEvent* event ) const
0780 {
0781     if ( !index.isValid() )
0782         return QItemSelectionModel::NoUpdate;
0783 
0784     const bool shiftKeyPressed = event->modifiers() & Qt::ShiftModifier;
0785     const bool controlKeyPressed = event->modifiers() & Qt::ControlModifier;
0786 
0787     if ( !controlKeyPressed && !shiftKeyPressed )
0788         return QItemSelectionModel::ClearAndSelect;
0789     return QItemSelectionModel::NoUpdate;
0790 }
0791 
0792 QList<int>
0793 Playlist::PrettyListView::selectedRows() const
0794 {
0795     QList<int> rows;
0796     foreach( const QModelIndex &idx, selectedIndexes() )
0797         rows.append( idx.row() );
0798     return rows;
0799 }
0800 
0801 void Playlist::PrettyListView::newPalette( const QPalette & palette )
0802 {
0803     Q_UNUSED( palette )
0804     The::paletteHandler()->updateItemView( this );
0805     reset();
0806 }
0807 
0808 void Playlist::PrettyListView::find( const QString &searchTerm, int fields, bool filter )
0809 {
0810     bool updateProxy = false;
0811     if ( ( The::playlist()->currentSearchFields() != fields ) || ( The::playlist()->currentSearchTerm() != searchTerm ) )
0812         updateProxy = true;
0813 
0814     m_searchTerm = searchTerm;
0815     m_fields = fields;
0816     m_filter = filter;
0817 
0818     int row = The::playlist()->find( m_searchTerm, m_fields );
0819     if( row != -1 )
0820     {
0821         //select this track
0822         QModelIndex index = model()->index( row, 0 );
0823         QItemSelection selItems( index, index );
0824         selectionModel()->select( selItems, QItemSelectionModel::SelectCurrent );
0825     }
0826 
0827     //instead of kicking the proxy right away, start a small timeout.
0828     //this stops us from updating it for each letter of a long search term,
0829     //and since it does not affect any views, this is fine. Worst case is that
0830     //a navigator skips to a track form the old search if the track change happens
0831     //before this  timeout. Only start count if values have actually changed!
0832     if ( updateProxy )
0833         startProxyUpdateTimeout();
0834 }
0835 
0836 void Playlist::PrettyListView::findNext( const QString & searchTerm, int fields )
0837 {
0838     DEBUG_BLOCK
0839     QList<int> selected = selectedRows();
0840 
0841     bool updateProxy = false;
0842     if ( ( The::playlist()->currentSearchFields() != fields ) || ( The::playlist()->currentSearchTerm() != searchTerm ) )
0843         updateProxy = true;
0844 
0845     int currentRow = -1;
0846     if( selected.size() > 0 )
0847         currentRow = selected.last();
0848 
0849     int row = The::playlist()->findNext( searchTerm, currentRow, fields );
0850     if( row != -1 )
0851     {
0852         //select this track
0853 
0854         QModelIndex index = model()->index( row, 0 );
0855         QItemSelection selItems( index, index );
0856         selectionModel()->select( selItems, QItemSelectionModel::SelectCurrent );
0857 
0858         QModelIndex foundIndex = model()->index( row, 0, QModelIndex() );
0859         setCurrentIndex( foundIndex );
0860         if ( foundIndex.isValid() )
0861             scrollTo( foundIndex, QAbstractItemView::PositionAtCenter );
0862 
0863         Q_EMIT( found() );
0864     }
0865     else
0866         Q_EMIT( notFound() );
0867 
0868     if ( updateProxy )
0869         The::playlist()->filterUpdated();
0870 }
0871 
0872 void Playlist::PrettyListView::findPrevious( const QString & searchTerm, int fields )
0873 {
0874     DEBUG_BLOCK
0875     QList<int> selected = selectedRows();
0876 
0877     bool updateProxy = false;
0878     if ( ( The::playlist()->currentSearchFields() != fields ) || ( The::playlist()->currentSearchTerm() != searchTerm ) )
0879         updateProxy = true;
0880 
0881     int currentRow = model()->rowCount();
0882     if( selected.size() > 0 )
0883         currentRow = selected.first();
0884 
0885     int row = The::playlist()->findPrevious( searchTerm, currentRow, fields );
0886     if( row != -1 )
0887     {
0888         //select this track
0889 
0890         QModelIndex index = model()->index( row, 0 );
0891         QItemSelection selItems( index, index );
0892         selectionModel()->select( selItems, QItemSelectionModel::SelectCurrent );
0893 
0894         QModelIndex foundIndex = model()->index( row, 0, QModelIndex() );
0895         setCurrentIndex( foundIndex );
0896         if ( foundIndex.isValid() )
0897             scrollTo( foundIndex, QAbstractItemView::PositionAtCenter );
0898 
0899         Q_EMIT( found() );
0900     }
0901     else
0902         Q_EMIT( notFound() );
0903 
0904     if ( updateProxy )
0905         The::playlist()->filterUpdated();
0906 }
0907 
0908 void Playlist::PrettyListView::clearSearchTerm()
0909 {
0910     DEBUG_BLOCK
0911 
0912     // Choose a focus item, to scroll to later.
0913     QModelIndex focusIndex;
0914     QModelIndexList selected = selectedIndexes();
0915     if( !selected.isEmpty() )
0916         focusIndex = selected.first();
0917     else
0918         focusIndex = indexAt( QPoint( 0, 0 ) );
0919 
0920     // Remember the focus item id, because the row numbers change when we reset the filter.
0921     quint64 focusItemId = The::playlist()->idAt( focusIndex.row() );
0922 
0923     The::playlist()->clearSearchTerm();
0924     The::playlist()->filterUpdated();
0925 
0926     // Now scroll to the focus item.
0927     QModelIndex newIndex = model()->index( The::playlist()->rowForId( focusItemId ), 0, QModelIndex() );
0928     if ( newIndex.isValid() )
0929         scrollTo( newIndex, QAbstractItemView::PositionAtCenter );
0930 }
0931 
0932 void Playlist::PrettyListView::startProxyUpdateTimeout()
0933 {
0934     DEBUG_BLOCK
0935     if ( m_proxyUpdateTimer->isActive() )
0936         m_proxyUpdateTimer->stop();
0937 
0938     m_proxyUpdateTimer->setInterval( 200 );
0939     m_proxyUpdateTimer->start();
0940 }
0941 
0942 void Playlist::PrettyListView::updateProxyTimeout()
0943 {
0944     DEBUG_BLOCK
0945     The::playlist()->filterUpdated();
0946 
0947     int row = The::playlist()->find( m_searchTerm, m_fields );
0948     if( row != -1 )
0949     {
0950         QModelIndex foundIndex = model()->index( row, 0, QModelIndex() );
0951         setCurrentIndex( foundIndex );
0952 
0953         if ( !m_filter )
0954         {
0955             if ( foundIndex.isValid() )
0956                 scrollTo( foundIndex, QAbstractItemView::PositionAtCenter );
0957         }
0958 
0959         Q_EMIT( found() );
0960     }
0961     else
0962         Q_EMIT( notFound() );
0963 }
0964 
0965 void Playlist::PrettyListView::showOnlyMatches( bool onlyMatches )
0966 {
0967     m_showOnlyMatches = onlyMatches;
0968 
0969     The::playlist()->showOnlyMatches( onlyMatches );
0970 }
0971 
0972 // Handle scrolling to newly inserted playlist items.
0973 // Warning, this slot is connected to the 'rowsInserted' signal of the *bottom* model,
0974 // not the normal top model.
0975 // The reason: FilterProxy can Q_EMIT *A LOT* (thousands) of 'rowsInserted' signals when its
0976 // search string changes. For that case we don't want to do any scrollTo() at all.
0977 void
0978 Playlist::PrettyListView::bottomModelRowsInserted( const QModelIndex& parent, int start, int end )
0979 {
0980     Q_UNUSED( parent )
0981     Q_UNUSED( end )
0982 
0983     // skip scrolling if tracks were added while playlist is in dynamicMode
0984     if( m_rowsInsertedScrollItem == 0 && !AmarokConfig::dynamicMode() )
0985     {
0986         m_rowsInsertedScrollItem = Playlist::ModelStack::instance()->bottom()->idAt( start );
0987         QTimer::singleShot( 0, this, &Playlist::PrettyListView::bottomModelRowsInsertedScroll );
0988     }
0989 }
0990 
0991 void Playlist::PrettyListView::bottomModelRowsInsertedScroll()
0992 {
0993     DEBUG_BLOCK
0994 
0995     if( m_rowsInsertedScrollItem )
0996     {   // Note: we don't bother handling the case "first inserted item in bottom model
0997         // does not have a row in the top 'model()' due to FilterProxy" nicely.
0998         int firstRowInserted = The::playlist()->rowForId( m_rowsInsertedScrollItem );    // In the *top* model.
0999         QModelIndex index = model()->index( firstRowInserted, 0 );
1000 
1001         if( index.isValid() )
1002             scrollTo( index, QAbstractItemView::PositionAtCenter );
1003 
1004         m_rowsInsertedScrollItem = 0;
1005     }
1006 }
1007 
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 
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