PageRenderTime 1034ms CodeModel.GetById 121ms app.highlight 797ms RepoModel.GetById 81ms app.codeStats 0ms

/src/libtomahawk/playlist/TrackView.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 1026 lines | 776 code | 213 blank | 37 comment | 135 complexity | e0f1a531305d31a1cda34d99e52caf5a MD5 | raw file
   1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
   2 *
   3 *   Copyright 2010-2015, Christian Muehlhaeuser <muesli@tomahawk-player.org>
   4 *   Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
   5 *
   6 *   Tomahawk is free software: you can redistribute it and/or modify
   7 *   it under the terms of the GNU General Public License as published by
   8 *   the Free Software Foundation, either version 3 of the License, or
   9 *   (at your option) any later version.
  10 *
  11 *   Tomahawk is distributed in the hope that it will be useful,
  12 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
  13 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14 *   GNU General Public License for more details.
  15 *
  16 *   You should have received a copy of the GNU General Public License
  17 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
  18 */
  19
  20#include "TrackView.h"
  21
  22#include "ViewHeader.h"
  23#include "PlayableModel.h"
  24#include "PlayableProxyModel.h"
  25#include "PlayableItem.h"
  26#include "DownloadManager.h"
  27#include "DropJob.h"
  28#include "Result.h"
  29#include "Source.h"
  30#include "TomahawkSettings.h"
  31#include "audio/AudioEngine.h"
  32#include "widgets/OverlayWidget.h"
  33#include "utils/TomahawkUtilsGui.h"
  34#include "utils/Closure.h"
  35#include "utils/AnimatedSpinner.h"
  36#include "utils/Logger.h"
  37#include "InboxModel.h"
  38
  39#include <QKeyEvent>
  40#include <QPainter>
  41#include <QScrollBar>
  42#include <QDrag>
  43
  44// HACK
  45#include <QTableView>
  46
  47#define SCROLL_TIMEOUT 280
  48
  49using namespace Tomahawk;
  50
  51TrackView::TrackView( QWidget* parent )
  52    : QTreeView( parent )
  53    , m_model( 0 )
  54    , m_proxyModel( 0 )
  55    , m_delegate( 0 )
  56    , m_header( new ViewHeader( this ) )
  57    , m_overlay( new OverlayWidget( this ) )
  58    , m_loadingSpinner( new LoadingSpinner( this ) )
  59    , m_resizing( false )
  60    , m_dragging( false )
  61    , m_alternatingRowColors( false )
  62    , m_autoExpanding( true )
  63    , m_contextMenu( new ContextMenu( this ) )
  64{
  65    setFrameShape( QFrame::NoFrame );
  66    setAttribute( Qt::WA_MacShowFocusRect, 0 );
  67
  68    setContentsMargins( 0, 0, 0, 0 );
  69    setMouseTracking( true );
  70    setSelectionMode( QAbstractItemView::ExtendedSelection );
  71    setSelectionBehavior( QAbstractItemView::SelectRows );
  72    setDragEnabled( true );
  73    setDropIndicatorShown( false );
  74    setDragDropMode( QAbstractItemView::InternalMove );
  75    setDragDropOverwriteMode( false );
  76    setAllColumnsShowFocus( true );
  77    setVerticalScrollMode( QAbstractItemView::ScrollPerPixel );
  78    setRootIsDecorated( false );
  79    setUniformRowHeights( true );
  80    setAlternatingRowColors( m_alternatingRowColors );
  81    setAutoResize( false );
  82    setEditTriggers( NoEditTriggers );
  83
  84    setHeader( m_header );
  85
  86    // HACK: enable moving of first column: QTBUG-33974 / https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
  87    QTableView unused;
  88    unused.setVerticalHeader( header() );
  89    header()->setParent( this );
  90    unused.setVerticalHeader( new QHeaderView( Qt::Horizontal, &unused ) );
  91
  92    setSortingEnabled( true );
  93    sortByColumn( -1 );
  94    setContextMenuPolicy( Qt::CustomContextMenu );
  95
  96    m_timer.setInterval( SCROLL_TIMEOUT );
  97
  98    // enable those connects if you want to enable lazily loading extra information for visible items
  99//    connect( verticalScrollBar(), SIGNAL( rangeChanged( int, int ) ), SLOT( onViewChanged() ) );
 100//    connect( verticalScrollBar(), SIGNAL( valueChanged( int ) ), SLOT( onViewChanged() ) );
 101//    connect( &m_timer, SIGNAL( timeout() ), SLOT( onScrollTimeout() ) );
 102
 103    connect( this, SIGNAL( doubleClicked( QModelIndex ) ), SLOT( onItemActivated( QModelIndex ) ) );
 104    connect( this, SIGNAL( customContextMenuRequested( const QPoint& ) ), SLOT( onCustomContextMenu( const QPoint& ) ) );
 105    connect( m_contextMenu, SIGNAL( triggered( int ) ), SLOT( onMenuTriggered( int ) ) );
 106
 107    setProxyModel( new PlayableProxyModel( this ) );
 108}
 109
 110
 111TrackView::~TrackView()
 112{
 113    tDebug() << Q_FUNC_INFO << ( m_guid.isEmpty() ? QString( "with empty guid" ) : QString( "with guid %1" ).arg( m_guid ) );
 114
 115    if ( !m_guid.isEmpty() && proxyModel()->playlistInterface() )
 116    {
 117        tDebug() << Q_FUNC_INFO << "Storing shuffle & random mode settings for guid" << m_guid;
 118
 119        TomahawkSettings* s = TomahawkSettings::instance();
 120        s->setShuffleState( m_guid, proxyModel()->playlistInterface()->shuffled() );
 121        s->setRepeatMode( m_guid, proxyModel()->playlistInterface()->repeatMode() );
 122    }
 123}
 124
 125
 126QString
 127TrackView::guid() const
 128{
 129    if ( m_guid.isEmpty() )
 130        return QString();
 131
 132    return QString( "%1/%2" ).arg( m_guid ).arg( m_proxyModel->columnCount() );
 133}
 134
 135
 136void
 137TrackView::setGuid( const QString& newguid )
 138{
 139    if ( newguid == m_guid )
 140        return;
 141
 142    if ( !newguid.isEmpty() )
 143    {
 144        tDebug() << Q_FUNC_INFO << "Setting guid on header" << newguid << "for a view with" << m_proxyModel->columnCount() << "columns";
 145
 146        m_guid = newguid;
 147        m_header->setGuid( guid() );
 148
 149        if ( !m_guid.isEmpty() && proxyModel()->playlistInterface() )
 150        {
 151            tDebug() << Q_FUNC_INFO << "Restoring shuffle & random mode settings for guid" << m_guid;
 152
 153            TomahawkSettings* s = TomahawkSettings::instance();
 154            proxyModel()->playlistInterface()->setShuffled( s->shuffleState( m_guid ) );
 155            proxyModel()->playlistInterface()->setRepeatMode( s->repeatMode( m_guid ) );
 156        }
 157    }
 158}
 159
 160
 161void
 162TrackView::setProxyModel( PlayableProxyModel* model )
 163{
 164    if ( m_proxyModel )
 165    {
 166        disconnect( m_proxyModel, SIGNAL( rowsAboutToBeInserted( QModelIndex, int, int ) ), this, SLOT( onModelFilling() ) );
 167        disconnect( m_proxyModel, SIGNAL( rowsRemoved( QModelIndex, int, int ) ), this, SLOT( onModelEmptyCheck() ) );
 168        disconnect( m_proxyModel, SIGNAL( filterChanged( QString ) ), this, SLOT( onFilterChanged( QString ) ) );
 169        disconnect( m_proxyModel, SIGNAL( rowsInserted( QModelIndex, int, int ) ), this, SLOT( onViewChanged() ) );
 170        disconnect( m_proxyModel, SIGNAL( rowsInserted( QModelIndex, int, int ) ), this, SLOT( verifySize() ) );
 171        disconnect( m_proxyModel, SIGNAL( rowsRemoved( QModelIndex, int, int ) ), this, SLOT( verifySize() ) );
 172        disconnect( m_proxyModel, SIGNAL( expandRequest( QPersistentModelIndex ) ), this, SLOT( expand( QPersistentModelIndex ) ) );
 173        disconnect( m_proxyModel, SIGNAL( selectRequest( QPersistentModelIndex ) ), this, SLOT( select( QPersistentModelIndex ) ) );
 174        disconnect( m_proxyModel, SIGNAL( currentIndexChanged( QModelIndex, QModelIndex ) ), this, SLOT( onCurrentIndexChanged( QModelIndex, QModelIndex ) ) );
 175    }
 176
 177    m_proxyModel = model;
 178
 179    connect( m_proxyModel, SIGNAL( rowsAboutToBeInserted( QModelIndex, int, int ) ), SLOT( onModelFilling() ) );
 180    connect( m_proxyModel, SIGNAL( rowsRemoved( QModelIndex, int, int ) ), SLOT( onModelEmptyCheck() ) );
 181    connect( m_proxyModel, SIGNAL( filterChanged( QString ) ), SLOT( onFilterChanged( QString ) ) );
 182    connect( m_proxyModel, SIGNAL( rowsInserted( QModelIndex, int, int ) ), SLOT( onViewChanged() ) );
 183    connect( m_proxyModel, SIGNAL( rowsInserted( QModelIndex, int, int ) ), SLOT( verifySize() ) );
 184    connect( m_proxyModel, SIGNAL( rowsRemoved( QModelIndex, int, int ) ), SLOT( verifySize() ) );
 185    connect( m_proxyModel, SIGNAL( expandRequest( QPersistentModelIndex ) ), SLOT( expand( QPersistentModelIndex ) ) );
 186    connect( m_proxyModel, SIGNAL( selectRequest( QPersistentModelIndex ) ), SLOT( select( QPersistentModelIndex ) ) );
 187    connect( m_proxyModel, SIGNAL( currentIndexChanged( QModelIndex, QModelIndex ) ), SLOT( onCurrentIndexChanged( QModelIndex, QModelIndex ) ) );
 188
 189    m_delegate = new PlaylistItemDelegate( this, m_proxyModel );
 190    QTreeView::setItemDelegate( m_delegate );
 191    QTreeView::setModel( m_proxyModel );
 192}
 193
 194
 195void
 196TrackView::setModel( QAbstractItemModel* model )
 197{
 198    Q_UNUSED( model );
 199    tDebug() << "Explicitly use setPlayableModel instead";
 200    Q_ASSERT( false );
 201}
 202
 203
 204void
 205TrackView::setPlaylistItemDelegate( PlaylistItemDelegate* delegate )
 206{
 207    if ( m_delegate )
 208        delete m_delegate;
 209
 210    m_delegate = delegate;
 211    QTreeView::setItemDelegate( delegate );
 212
 213    verifySize();
 214}
 215
 216
 217void
 218TrackView::setPlayableModel( PlayableModel* model )
 219{
 220    if ( m_model )
 221    {
 222        disconnect( m_model, SIGNAL( loadingStarted() ), m_loadingSpinner, SLOT( fadeIn() ) );
 223        disconnect( m_model, SIGNAL( loadingFinished() ), m_loadingSpinner, SLOT( fadeOut() ) );
 224        disconnect( m_model, SIGNAL( changed() ), this, SIGNAL( modelChanged() ) );
 225    }
 226
 227    m_model = model;
 228
 229    if ( m_proxyModel )
 230    {
 231        m_proxyModel->setSourcePlayableModel( m_model );
 232    }
 233
 234    setAcceptDrops( true );
 235    m_header->setDefaultColumnWeights( m_proxyModel->columnWeights() );
 236    setGuid( m_proxyModel->guid() );
 237
 238    switch( m_proxyModel->style() )
 239    {
 240        case PlayableProxyModel::SingleColumn:
 241            setHeaderHidden( true );
 242            setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
 243        break;
 244
 245        default:
 246            setHeaderHidden( false );
 247            setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded );
 248    }
 249
 250    connect( m_model, SIGNAL( loadingStarted() ), m_loadingSpinner, SLOT( fadeIn() ) );
 251    connect( m_model, SIGNAL( loadingFinished() ), m_loadingSpinner, SLOT( fadeOut() ) );
 252    connect( m_model, SIGNAL( changed() ), SIGNAL( modelChanged() ) );
 253
 254    if ( m_model->isLoading() )
 255        m_loadingSpinner->fadeIn();
 256
 257    if ( m_autoExpanding )
 258    {
 259        expandAll();
 260        selectFirstTrack();
 261    }
 262
 263    onViewChanged();
 264    emit modelChanged();
 265}
 266
 267
 268void
 269TrackView::setEmptyTip( const QString& tip )
 270{
 271    m_emptyTip = tip;
 272    m_overlay->setText( tip );
 273}
 274
 275
 276void
 277TrackView::onModelFilling()
 278{
 279    QTreeView::setAlternatingRowColors( m_alternatingRowColors );
 280}
 281
 282
 283void
 284TrackView::onModelEmptyCheck()
 285{
 286    if ( !m_proxyModel->rowCount( QModelIndex() ) )
 287        QTreeView::setAlternatingRowColors( false );
 288}
 289
 290
 291void
 292TrackView::onCurrentIndexChanged( const QModelIndex& newIndex, const QModelIndex& oldIndex )
 293{
 294    if ( selectedIndexes().count() == 1 && currentIndex() == oldIndex )
 295    {
 296        selectionModel()->select( newIndex, QItemSelectionModel::SelectCurrent );
 297        currentChanged( newIndex, oldIndex );
 298        setCurrentIndex( newIndex );
 299    }
 300}
 301
 302
 303void
 304TrackView::onViewChanged()
 305{
 306    if ( m_timer.isActive() )
 307        m_timer.stop();
 308
 309    m_timer.start();
 310}
 311
 312
 313void
 314TrackView::onScrollTimeout()
 315{
 316    if ( m_timer.isActive() )
 317        m_timer.stop();
 318
 319    QModelIndex left = indexAt( viewport()->rect().topLeft() );
 320    while ( left.isValid() && left.parent().isValid() )
 321        left = left.parent();
 322
 323    QModelIndex right = indexAt( viewport()->rect().bottomLeft() );
 324    while ( right.isValid() && right.parent().isValid() )
 325        right = right.parent();
 326
 327    int max = m_proxyModel->playlistInterface()->trackCount();
 328    if ( right.isValid() )
 329        max = right.row();
 330
 331    if ( !max )
 332        return;
 333
 334    //FIXME
 335    for ( int i = left.row(); i <= max; i++ )
 336    {
 337        m_proxyModel->updateDetailedInfo( m_proxyModel->index( i, 0 ) );
 338    }
 339}
 340
 341
 342void
 343TrackView::startPlayingFromStart()
 344{
 345    if ( m_proxyModel->rowCount() == 0 )
 346        return;
 347
 348    const QModelIndex index = m_proxyModel->index( 0, 0 );
 349    startAutoPlay( index );
 350}
 351
 352
 353void
 354TrackView::autoPlayResolveFinished( const query_ptr& query, int row )
 355{
 356    Q_ASSERT( !query.isNull() );
 357    Q_ASSERT( row >= 0 );
 358
 359    if ( query.isNull() || row < 0  || query != m_autoPlaying )
 360        return;
 361
 362    const QModelIndex index = m_proxyModel->index( row, 0 );
 363    if ( query->playable() )
 364    {
 365        onItemActivated( index );
 366        return;
 367    }
 368
 369    // Try the next one..
 370    const QModelIndex sib = index.sibling( index.row() + 1, index.column() );
 371    if ( sib.isValid() )
 372        startAutoPlay( sib );
 373}
 374
 375
 376void
 377TrackView::currentChanged( const QModelIndex& current, const QModelIndex& previous )
 378{
 379    QTreeView::currentChanged( current, previous );
 380    if ( !m_model )
 381        return;
 382
 383    PlayableItem* item = m_model->itemFromIndex( m_proxyModel->mapToSource( current ) );
 384    if ( item && item->query() )
 385    {
 386        emit querySelected( item->query() );
 387    }
 388    else
 389    {
 390        emit querySelected( query_ptr() );
 391    }
 392}
 393
 394
 395void
 396TrackView::onItemActivated( const QModelIndex& index )
 397{
 398    if ( !index.isValid() )
 399        return;
 400
 401    tryToPlayItem( index );
 402    emit itemActivated( index );
 403}
 404
 405
 406void
 407TrackView::startAutoPlay( const QModelIndex& index )
 408{
 409    if ( tryToPlayItem( index ) )
 410        return;
 411
 412    // item isn't playable but still resolving
 413    PlayableItem* item = m_model->itemFromIndex( m_proxyModel->mapToSource( index ) );
 414    if ( item && !item->query().isNull() && !item->query()->resolvingFinished() )
 415    {
 416        m_autoPlaying = item->query(); // So we can kill it if user starts autoplaying this playlist again
 417        NewClosure( item->query().data(), SIGNAL( resolvingFinished( bool ) ), this, SLOT( autoPlayResolveFinished( Tomahawk::query_ptr, int ) ),
 418                    item->query(), index.row() );
 419        return;
 420    }
 421
 422    // not playable at all, try next
 423    const QModelIndex sib = index.sibling( index.row() + 1, index.column() );
 424    if ( sib.isValid() )
 425        startAutoPlay( sib );
 426}
 427
 428
 429bool
 430TrackView::tryToPlayItem( const QModelIndex& index )
 431{
 432    PlayableItem* item = m_model->itemFromIndex( m_proxyModel->mapToSource( index ) );
 433    if ( item && !item->query().isNull() )
 434    {
 435        m_model->setCurrentIndex( m_proxyModel->mapToSource( index ) );
 436        AudioEngine::instance()->playItem( playlistInterface(), item->query() );
 437
 438        return true;
 439    }
 440
 441    return false;
 442}
 443
 444
 445void
 446TrackView::keyPressEvent( QKeyEvent* event )
 447{
 448    QTreeView::keyPressEvent( event );
 449
 450    if ( !model() )
 451        return;
 452
 453    if ( event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return )
 454    {
 455        onItemActivated( currentIndex() );
 456    }
 457    if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
 458    {
 459        tDebug() << "Removing selected items from playlist";
 460        deleteSelectedItems();
 461    }
 462}
 463
 464
 465void
 466TrackView::onItemResized( const QModelIndex& index )
 467{
 468    m_delegate->updateRowSize( index );
 469}
 470
 471
 472void
 473TrackView::playItem()
 474{
 475    onItemActivated( m_contextMenuIndex );
 476}
 477
 478
 479void
 480TrackView::resizeEvent( QResizeEvent* event )
 481{
 482    QTreeView::resizeEvent( event );
 483
 484    int sortSection = m_header->sortIndicatorSection();
 485    Qt::SortOrder sortOrder = m_header->sortIndicatorOrder();
 486
 487    if ( m_header->checkState() && sortSection >= 0 )
 488    {
 489        // restoreState keeps overwriting our previous sort-order
 490        sortByColumn( sortSection, sortOrder );
 491    }
 492
 493    if ( !model() )
 494        return;
 495
 496    if ( model()->columnCount() == 1 )
 497    {
 498        m_header->resizeSection( 0, event->size().width() );
 499    }
 500}
 501
 502
 503bool
 504TrackView::eventFilter( QObject* obj, QEvent* event )
 505{
 506    if ( event->type() == QEvent::DragEnter )
 507    {
 508        QDragEnterEvent* e = static_cast<QDragEnterEvent*>(event);
 509        dragEnterEvent( e );
 510        return true;
 511    }
 512    if ( event->type() == QEvent::DragMove )
 513    {
 514        QDragMoveEvent* e = static_cast<QDragMoveEvent*>(event);
 515        dragMoveEvent( e );
 516        return true;
 517    }
 518    if ( event->type() == QEvent::DragLeave )
 519    {
 520        QDragLeaveEvent* e = static_cast<QDragLeaveEvent*>(event);
 521        dragLeaveEvent( e );
 522        return true;
 523    }
 524    if ( event->type() == QEvent::Drop )
 525    {
 526        QDropEvent* e = static_cast<QDropEvent*>(event);
 527        dropEvent( e );
 528        return true;
 529    }
 530
 531    return QObject::eventFilter( obj, event );
 532}
 533
 534
 535void
 536TrackView::dragEnterEvent( QDragEnterEvent* event )
 537{
 538    tDebug() << Q_FUNC_INFO;
 539    QTreeView::dragEnterEvent( event );
 540
 541    if ( !model() || model()->isReadOnly() || model()->isLoading() )
 542    {
 543        event->ignore();
 544        return;
 545    }
 546
 547    if ( DropJob::acceptsMimeData( event->mimeData() ) )
 548    {
 549        m_dragging = true;
 550        m_dropRect = QRect();
 551
 552        event->acceptProposedAction();
 553    }
 554}
 555
 556
 557void
 558TrackView::dragMoveEvent( QDragMoveEvent* event )
 559{
 560    QTreeView::dragMoveEvent( event );
 561
 562    if ( !model() || model()->isReadOnly() || model()->isLoading() )
 563    {
 564        event->ignore();
 565        return;
 566    }
 567
 568    if ( DropJob::acceptsMimeData( event->mimeData() ) )
 569    {
 570        setDirtyRegion( m_dropRect );
 571        const QPoint pos = event->pos();
 572        QModelIndex index = indexAt( pos );
 573        bool pastLast = false;
 574
 575        if ( !index.isValid() && proxyModel()->rowCount( QModelIndex() ) > 0 )
 576        {
 577            index = proxyModel()->index( proxyModel()->rowCount( QModelIndex() ) - 1, 0, QModelIndex() );
 578            pastLast = true;
 579        }
 580
 581        if ( index.isValid() )
 582        {
 583            const QRect rect = visualRect( index );
 584            m_dropRect = rect;
 585
 586            // indicate that the item will be inserted above the current place
 587            const int gap = 5; // FIXME constant
 588            int yHeight = ( pastLast ? rect.bottom() : rect.top() ) - gap / 2;
 589            m_dropRect = QRect( 0, yHeight, width(), gap );
 590
 591            event->acceptProposedAction();
 592        }
 593
 594        setDirtyRegion( m_dropRect );
 595    }
 596}
 597
 598
 599void
 600TrackView::dragLeaveEvent( QDragLeaveEvent* event )
 601{
 602    QTreeView::dragLeaveEvent( event );
 603
 604    m_dragging = false;
 605    setDirtyRegion( m_dropRect );
 606}
 607
 608
 609void
 610TrackView::dropEvent( QDropEvent* event )
 611{
 612    tDebug() << Q_FUNC_INFO;
 613    QTreeView::dropEvent( event );
 614
 615    if ( event->isAccepted() )
 616    {
 617        tDebug() << "Ignoring accepted event!";
 618    }
 619    else if ( event->source() != this )
 620    {
 621        // This code shouldn't be required when the PlayableModel properly accepts the incoming drop.
 622        // If we remove it, the queue for some reason doesn't accept the drops yet, though.
 623        if ( DropJob::acceptsMimeData( event->mimeData() ) )
 624        {
 625            const QPoint pos = event->pos();
 626            const QModelIndex index = indexAt( pos );
 627
 628            if ( !model()->isReadOnly() && !model()->isLoading() )
 629            {
 630                tDebug() << Q_FUNC_INFO << "Drop Event accepted at row:" << index.row();
 631                event->acceptProposedAction();
 632                model()->dropMimeData( event->mimeData(), event->proposedAction(), index.row(), 0, index.parent() );
 633            }
 634        }
 635    }
 636
 637    m_dragging = false;
 638}
 639
 640
 641void
 642TrackView::leaveEvent( QEvent* event )
 643{
 644    QTreeView::leaveEvent( event );
 645
 646    m_delegate->resetHoverIndex();
 647}
 648
 649
 650void
 651TrackView::paintEvent( QPaintEvent* event )
 652{
 653    QTreeView::paintEvent( event );
 654    QPainter painter( viewport() );
 655
 656    if ( m_dragging )
 657    {
 658        // draw drop indicator
 659        {
 660            // draw indicator for inserting items
 661            QBrush blendedBrush = viewOptions().palette.brush( QPalette::Normal, QPalette::Highlight );
 662            QColor color = blendedBrush.color();
 663
 664            const int y = ( m_dropRect.top() + m_dropRect.bottom() ) / 2;
 665            const int thickness = m_dropRect.height() / 2;
 666
 667            int alpha = 255;
 668            const int alphaDec = alpha / ( thickness + 1 );
 669            for ( int i = 0; i < thickness; i++ )
 670            {
 671                color.setAlpha( alpha );
 672                alpha -= alphaDec;
 673                painter.setPen( color );
 674                painter.drawLine( 0, y - i, width(), y - i );
 675                painter.drawLine( 0, y + i, width(), y + i );
 676            }
 677        }
 678    }
 679}
 680
 681
 682void
 683TrackView::wheelEvent( QWheelEvent* event )
 684{
 685    QTreeView::wheelEvent( event );
 686
 687    m_delegate->resetHoverIndex();
 688}
 689
 690
 691void
 692TrackView::onFilterChanged( const QString& )
 693{
 694    if ( !selectedIndexes().isEmpty() )
 695        scrollTo( selectedIndexes().at( 0 ), QAbstractItemView::PositionAtCenter );
 696
 697    if ( !filter().isEmpty() && !proxyModel()->playlistInterface()->trackCount() && model()->trackCount() )
 698    {
 699        m_overlay->setText( tr( "Sorry, your filter '%1' did not match any results." ).arg( filter() ) );
 700        m_overlay->show();
 701    }
 702    else
 703    {
 704        if ( model()->trackCount() )
 705        {
 706            m_overlay->hide();
 707        }
 708        else
 709        {
 710            m_overlay->setText( m_emptyTip );
 711            m_overlay->show();
 712        }
 713    }
 714}
 715
 716
 717void
 718TrackView::startDrag( Qt::DropActions supportedActions )
 719{
 720    QList<QPersistentModelIndex> pindexes;
 721    QModelIndexList indexes;
 722    foreach( const QModelIndex& idx, selectedIndexes() )
 723    {
 724        if ( ( m_proxyModel->flags( idx ) & Qt::ItemIsDragEnabled ) )
 725        {
 726            indexes << idx;
 727            pindexes << idx;
 728        }
 729    }
 730
 731    if ( indexes.isEmpty() )
 732        return;
 733
 734    tDebug() << "Dragging" << indexes.count() << "indexes";
 735    QMimeData* data = m_proxyModel->mimeData( indexes );
 736    if ( !data )
 737        return;
 738
 739    QDrag* drag = new QDrag( this );
 740    drag->setMimeData( data );
 741    const QPixmap p = TomahawkUtils::createDragPixmap( TomahawkUtils::MediaTypeTrack, indexes.count() );
 742    drag->setPixmap( p );
 743    drag->setHotSpot( QPoint( -20, -20 ) );
 744
 745    Qt::DropAction action = drag->exec( supportedActions, Qt::CopyAction );
 746    if ( action == Qt::MoveAction )
 747    {
 748        m_proxyModel->removeIndexes( pindexes );
 749    }
 750
 751    // delete drag; FIXME? On OSX it doesn't seem to get deleted automatically.
 752}
 753
 754
 755void
 756TrackView::onCustomContextMenu( const QPoint& pos )
 757{
 758    m_contextMenu->clear();
 759    m_contextMenu->setPlaylistInterface( playlistInterface() );
 760
 761    QModelIndex idx = indexAt( pos );
 762    idx = idx.sibling( idx.row(), 0 );
 763    setContextMenuIndex( idx );
 764
 765    if ( !idx.isValid() )
 766        return;
 767
 768    if ( model() && !model()->isReadOnly() )
 769        m_contextMenu->setSupportedActions( m_contextMenu->supportedActions() | ContextMenu::ActionDelete );
 770    if ( model() && qobject_cast< InboxModel* >( model() ) )
 771        m_contextMenu->setSupportedActions( m_contextMenu->supportedActions() | ContextMenu::ActionMarkListened
 772                                                                              | ContextMenu::ActionDelete );
 773
 774    if ( proxyModel()->style() != PlayableProxyModel::Collection )
 775    {
 776        bool allDownloaded = true;
 777        bool noneDownloadable = true;
 778        bool downloadable = false;
 779        foreach ( const QModelIndex& index, selectedIndexes() )
 780        {
 781            if ( index.column() )
 782                continue;
 783
 784            PlayableItem* item = proxyModel()->itemFromIndex( proxyModel()->mapToSource( index ) );
 785
 786            if( item->query()->results().isEmpty() )
 787                continue;
 788
 789            downloadable = !item->query()->results().first()->downloadFormats().isEmpty();
 790            if ( downloadable )
 791            {
 792                noneDownloadable = false;
 793            }
 794
 795            if ( downloadable && DownloadManager::instance()->localFileForDownload( item->query()->results().first()->downloadFormats().first().url.toString() ).isEmpty() )
 796            {
 797                allDownloaded = false;
 798            }
 799
 800            if ( !allDownloaded || !noneDownloadable )
 801            {
 802                break;
 803            }
 804        }
 805
 806        if ( !allDownloaded || !noneDownloadable )
 807        {
 808            m_contextMenu->setSupportedActions( m_contextMenu->supportedActions() | ContextMenu::ActionDownload );
 809        }
 810    }
 811
 812    QList<query_ptr> queries;
 813    foreach ( const QModelIndex& index, selectedIndexes() )
 814    {
 815        if ( index.column() )
 816            continue;
 817
 818        PlayableItem* item = proxyModel()->itemFromIndex( proxyModel()->mapToSource( index ) );
 819        if ( item && !item->query().isNull() )
 820        {
 821            queries << item->query();
 822        }
 823    }
 824
 825    m_contextMenu->setQueries( queries );
 826    m_contextMenu->exec( viewport()->mapToGlobal( pos ) );
 827}
 828
 829
 830void
 831TrackView::onMenuTriggered( int action )
 832{
 833    switch ( action )
 834    {
 835        case ContextMenu::ActionPlay:
 836            onItemActivated( m_contextMenuIndex );
 837            break;
 838
 839        case ContextMenu::ActionDelete:
 840            deleteSelectedItems();
 841            break;
 842
 843        case ContextMenu::ActionDownload:
 844            downloadSelectedItems();
 845            break;
 846
 847        default:
 848            break;
 849    }
 850}
 851
 852
 853Tomahawk::playlistinterface_ptr
 854TrackView::playlistInterface() const
 855{
 856    return proxyModel()->playlistInterface();
 857}
 858
 859
 860void
 861TrackView::setPlaylistInterface( const Tomahawk::playlistinterface_ptr& playlistInterface )
 862{
 863    proxyModel()->setPlaylistInterface( playlistInterface );
 864}
 865
 866
 867QString
 868TrackView::title() const
 869{
 870    return model()->title();
 871}
 872
 873
 874QString
 875TrackView::description() const
 876{
 877    return model()->description();
 878}
 879
 880
 881QPixmap
 882TrackView::pixmap() const
 883{
 884    return model()->icon();
 885}
 886
 887
 888bool
 889TrackView::jumpToCurrentTrack()
 890{
 891    scrollTo( proxyModel()->currentIndex(), QAbstractItemView::PositionAtCenter );
 892    selectionModel()->select( QModelIndex(), QItemSelectionModel::SelectCurrent );
 893    select( proxyModel()->currentIndex() );
 894    selectionModel()->select( proxyModel()->currentIndex(), QItemSelectionModel::SelectCurrent );
 895    return true;
 896}
 897
 898
 899bool
 900TrackView::setFilter( const QString& filter )
 901{
 902    ViewPage::setFilter( filter );
 903    m_proxyModel->setFilter( filter );
 904    return true;
 905}
 906
 907
 908void
 909TrackView::deleteSelectedItems()
 910{
 911    if ( !model()->isReadOnly() )
 912    {
 913        proxyModel()->removeIndexes( selectedIndexes() );
 914    }
 915    else
 916    {
 917        tDebug() << Q_FUNC_INFO << "Error: Model is read-only!";
 918    }
 919}
 920
 921
 922void
 923TrackView::downloadSelectedItems()
 924{
 925    foreach ( const QModelIndex& index, selectedIndexes() )
 926    {
 927        if ( index.column() )
 928            continue;
 929
 930        PlayableItem* item = proxyModel()->itemFromIndex( proxyModel()->mapToSource( index ) );
 931
 932        if ( !item )
 933            continue;
 934
 935        if ( item->query()->results().isEmpty() || item->query()->results().first()->downloadFormats().isEmpty() )
 936            continue;
 937
 938        if ( !DownloadManager::instance()->localFileForDownload( item->query()->results().first()->downloadFormats().first().url.toString() ).isEmpty() )
 939            continue;
 940
 941        DownloadManager::instance()->addJob( item->result()->toDownloadJob( item->result()->downloadFormats().first() ) );
 942    }
 943}
 944
 945
 946void
 947TrackView::verifySize()
 948{
 949    if ( !autoResize() || !m_proxyModel || !m_proxyModel->rowCount() )
 950        return;
 951
 952    unsigned int height = 0;
 953    for ( int i = 0; i < m_proxyModel->rowCount(); i++ )
 954    {
 955        height += indexRowSizeHint( m_proxyModel->index( i, 0 ) );
 956    }
 957
 958    setFixedHeight( height + contentsMargins().top() + contentsMargins().bottom() );
 959}
 960
 961
 962void
 963TrackView::setAutoResize( bool b )
 964{
 965    m_autoResize = b;
 966
 967    if ( m_autoResize )
 968        setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
 969}
 970
 971
 972void
 973TrackView::setAlternatingRowColors( bool enable )
 974{
 975    m_alternatingRowColors = enable;
 976    QTreeView::setAlternatingRowColors( enable );
 977}
 978
 979
 980void
 981TrackView::expand( const QPersistentModelIndex& idx )
 982{
 983    QTreeView::expand( idx );
 984}
 985
 986
 987void
 988TrackView::select( const QPersistentModelIndex& idx )
 989{
 990    if ( !selectedIndexes().isEmpty() )
 991        return;
 992
 993//    selectionModel()->select( idx, QItemSelectionModel::SelectCurrent );
 994    currentChanged( idx, QModelIndex() );
 995}
 996
 997
 998void
 999TrackView::selectFirstTrack()
1000{
1001    if ( !m_proxyModel->rowCount() )
1002        return;
1003    if ( !selectedIndexes().isEmpty() )
1004        return;
1005
1006    QModelIndex idx = m_proxyModel->index( 0, 0, QModelIndex() );
1007    PlayableItem* item = m_model->itemFromIndex( m_proxyModel->mapToSource( idx ) );
1008    if ( item->source() )
1009    {
1010        idx = m_proxyModel->index( 0, 0, idx );
1011        item = m_model->itemFromIndex( m_proxyModel->mapToSource( idx ) );
1012    }
1013
1014    if ( item->query() )
1015    {
1016//        selectionModel()->select( idx, QItemSelectionModel::SelectCurrent );
1017        currentChanged( idx, QModelIndex() );
1018    }
1019}
1020
1021
1022PlayableModel*
1023TrackView::model() const
1024{
1025    return m_model.data();
1026}