/src/sourcetree/sourcetreeview.cpp
C++ | 874 lines | 669 code | 168 blank | 37 comment | 126 complexity | 5b86d3f120ca1d16e67e82a7e266b04e MD5 | raw file
1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> === 2 * 3 * Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org> 4 * Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org> 5 * Copyright 2010-2012, Leo Franchi <lfranchi@kde.org> 6 * 7 * Tomahawk is free software: you can redistribute it and/or modify 8 * it under the terms of the GNU General Public License as published by 9 * the Free Software Foundation, either version 3 of the License, or 10 * (at your option) any later version. 11 * 12 * Tomahawk is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>. 19 */ 20 21#include "SourceTreeView.h" 22 23#include "ActionCollection.h" 24#include "Playlist.h" 25#include "ViewManager.h" 26#include "SourcesProxyModel.h" 27#include "SourceList.h" 28#include "SourceDelegate.h" 29#include "sourcetree/items/PlaylistItems.h" 30#include "sourcetree/items/SourceItem.h" 31#include "audio/AudioEngine.h" 32#include "SourcePlaylistInterface.h" 33#include "TomahawkSettings.h" 34#include "GlobalActionManager.h" 35#include "DropJob.h" 36#include "items/GenericPageItems.h" 37#include "items/TemporaryPageItem.h" 38#include "database/DatabaseCommand_SocialAction.h" 39#include "database/Database.h" 40#include "LatchManager.h" 41#include "utils/TomahawkUtilsGui.h" 42#include "utils/Logger.h" 43#include "utils/Closure.h" 44#include "widgets/SourceTreePopupDialog.h" 45 46#include <QAction> 47#include <QApplication> 48#include <QContextMenuEvent> 49#include <QDragEnterEvent> 50#include <QHeaderView> 51#include <QPainter> 52#include <QStyledItemDelegate> 53#include <QFileDialog> 54#include <QMessageBox> 55#include <QSize> 56 57using namespace Tomahawk; 58 59 60SourceTreeView::SourceTreeView( QWidget* parent ) 61 : QTreeView( parent ) 62 , m_latchManager( new LatchManager( this ) ) 63 , m_dragging( false ) 64{ 65 setProperty( "flattenBranches", QVariant( true ) ); 66 67 setFrameShape( QFrame::NoFrame ); 68 setAttribute( Qt::WA_MacShowFocusRect, 0 ); 69 setContentsMargins( 0, 0, 0, 0 ); 70 71 QFont fnt; 72 QFontMetrics fm( fnt ); 73 // This is sort of the longest string in there. With translations 74 // we will never get it right so setting it to something reasonable for the average case 75 setMinimumWidth( fm.width( "Track Album Artist Local Top10" ) ); 76 77 setHeaderHidden( true ); 78 setRootIsDecorated( true ); 79 setExpandsOnDoubleClick( false ); 80 81 setSelectionBehavior( QAbstractItemView::SelectRows ); 82 setDragDropMode( QAbstractItemView::DropOnly ); 83 setAcceptDrops( true ); 84 setDropIndicatorShown( false ); 85 setAllColumnsShowFocus( true ); 86 setUniformRowHeights( false ); 87 setIndentation( 0 ); 88 setSortingEnabled( true ); 89 sortByColumn( 0, Qt::AscendingOrder ); 90 setVerticalScrollMode( QTreeView::ScrollPerPixel ); 91 setMouseTracking( true ); 92 setEditTriggers( NoEditTriggers ); 93 setAutoExpandDelay( 500 ); 94 95 // TODO animation conflicts with the expanding-playlists-when-collection-is-null 96 // so investigate 97// setAnimated( true ); 98 99 m_delegate = new SourceDelegate( this ); 100 connect( m_delegate, SIGNAL( latchOn( Tomahawk::source_ptr ) ), SLOT( latchOnOrCatchUp( Tomahawk::source_ptr ) ) ); 101 connect( m_delegate, SIGNAL( latchOff( Tomahawk::source_ptr ) ), SLOT( latchOff( Tomahawk::source_ptr ) ) ); 102 connect( m_delegate, SIGNAL( toggleRealtimeLatch( Tomahawk::source_ptr, bool ) ), m_latchManager, SLOT( latchModeChangeRequest( Tomahawk::source_ptr,bool ) ) ); 103 connect( m_delegate, SIGNAL( clicked( QModelIndex ) ), SLOT( onItemActivated( QModelIndex ) ) ); 104 connect( m_delegate, SIGNAL( doubleClicked( QModelIndex ) ), SLOT( onItemDoubleClicked( QModelIndex ) ) ); 105 106 setItemDelegate( m_delegate ); 107 108 setContextMenuPolicy( Qt::CustomContextMenu ); 109 connect( this, SIGNAL( customContextMenuRequested( QPoint ) ), SLOT( onCustomContextMenu( QPoint ) ) ); 110 111 m_model = new SourcesModel( this ); 112 m_proxyModel = new SourcesProxyModel( m_model, this ); 113 connect( m_proxyModel, SIGNAL( selectRequest( QPersistentModelIndex ) ), SLOT( selectRequest( QPersistentModelIndex ) ), Qt::QueuedConnection ); 114 connect( m_proxyModel, SIGNAL( expandRequest( QPersistentModelIndex ) ), SLOT( expandRequest( QPersistentModelIndex ) ) ); 115 connect( m_proxyModel, SIGNAL( toggleExpandRequest( QPersistentModelIndex ) ), SLOT( toggleExpandRequest( QPersistentModelIndex ) ) ); 116 117 setModel( m_proxyModel ); 118 119 header()->setStretchLastSection( false ); 120 header()->setResizeMode( 0, QHeaderView::Stretch ); 121 122 connect( this, SIGNAL( expanded( QModelIndex ) ), SLOT( onItemExpanded( QModelIndex ) ) ); 123 connect( selectionModel(), SIGNAL( selectionChanged( QItemSelection, QItemSelection ) ), SLOT( onSelectionChanged() ) ); 124 125 showOfflineSources( TomahawkSettings::instance()->showOfflineSources() ); 126 127 // Light-blue sourcetree on osx 128#ifdef Q_WS_MAC 129 setStyleSheet( "SourceTreeView:active { background: #DDE4EB; } " 130 "SourceTreeView { background: #EDEDED; } " ); 131#endif 132 133 connect( this, SIGNAL( latchRequest( Tomahawk::source_ptr ) ), m_latchManager, SLOT( latchRequest( Tomahawk::source_ptr ) ) ); 134 connect( this, SIGNAL( unlatchRequest( Tomahawk::source_ptr ) ), m_latchManager, SLOT( unlatchRequest( Tomahawk::source_ptr ) ) ); 135 connect( this, SIGNAL( catchUpRequest() ), m_latchManager, SLOT( catchUpRequest() ) ); 136 connect( this, SIGNAL( latchModeChangeRequest( Tomahawk::source_ptr, bool ) ), m_latchManager, SLOT( latchModeChangeRequest( Tomahawk::source_ptr, bool ) ) ); 137 138 connect( ActionCollection::instance(), SIGNAL( privacyModeChanged() ), SLOT( repaint() ) ); 139} 140 141 142SourceTreeView::~SourceTreeView() 143{ 144} 145 146 147void 148SourceTreeView::setupMenus() 149{ 150 m_playlistMenu.clear(); 151 m_roPlaylistMenu.clear(); 152 m_latchMenu.clear(); 153 m_privacyMenu.clear(); 154 155 bool readonly = true; 156 SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt(); 157 158 if ( type == SourcesModel::StaticPlaylist || type == SourcesModel::AutomaticPlaylist || type == SourcesModel::Station ) 159 { 160 const PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex ); 161 const playlist_ptr playlist = item->playlist(); 162 163 if ( !playlist.isNull() ) 164 { 165 readonly = !playlist->author()->isLocal(); 166 } 167 } 168 169 QAction* latchOnAction = ActionCollection::instance()->getAction( "latchOn" ); 170 m_latchMenu.addAction( latchOnAction ); 171 172 m_privacyMenu.addAction( ActionCollection::instance()->getAction( "togglePrivacy" ) ); 173 174 if ( type == SourcesModel::Collection ) 175 { 176 SourceItem* item = itemFromIndex< SourceItem >( m_contextMenuIndex ); 177 source_ptr source = item->source(); 178 if ( !source.isNull() ) 179 { 180 if ( m_latchManager->isLatched( source ) ) 181 { 182 QAction* latchOffAction = ActionCollection::instance()->getAction( "latchOff" ); 183 m_latchMenu.addAction( latchOffAction ); 184 connect( latchOffAction, SIGNAL( triggered() ), SLOT( latchOff() ) ); 185 m_latchMenu.addSeparator(); 186 QAction* latchRealtimeAction = ActionCollection::instance()->getAction( "realtimeFollowingAlong" ); 187 latchRealtimeAction->setChecked( source->playlistInterface()->latchMode() == Tomahawk::PlaylistModes::RealTime ); 188 m_latchMenu.addAction( latchRealtimeAction ); 189 connect( latchRealtimeAction, SIGNAL( toggled( bool ) ), SLOT( latchModeToggled( bool ) ) ); 190 } 191 } 192 } 193 194 QAction* loadPlaylistAction = ActionCollection::instance()->getAction( "loadPlaylist" ); 195 m_playlistMenu.addAction( loadPlaylistAction ); 196 QAction* renamePlaylistAction = ActionCollection::instance()->getAction( "renamePlaylist" ); 197 m_playlistMenu.addAction( renamePlaylistAction ); 198 m_playlistMenu.addSeparator(); 199 200 QAction* copyPlaylistAction = m_playlistMenu.addAction( tr( "&Copy Link" ) ); 201 202 if ( type == SourcesModel::StaticPlaylist ) 203 { 204 QAction* exportPlaylist = m_playlistMenu.addAction( tr( "&Export Playlist") ); 205 connect( exportPlaylist, SIGNAL( triggered() ), this, SLOT( exportPlaylist() ) ); 206 } 207 208 QAction* deletePlaylistAction = m_playlistMenu.addAction( tr( "&Delete %1" ).arg( SourcesModel::rowTypeToString( type ) ) ); 209 210 QString addToText; 211 if ( type == SourcesModel::StaticPlaylist ) 212 addToText = tr( "Add to my Playlists" ); 213 if ( type == SourcesModel::AutomaticPlaylist ) 214 addToText = tr( "Add to my Automatic Playlists" ); 215 else if ( type == SourcesModel::Station ) 216 addToText = tr( "Add to my Stations" ); 217 218 QAction* addToLocalAction = m_roPlaylistMenu.addAction( addToText ); 219 220 m_roPlaylistMenu.addAction( copyPlaylistAction ); 221 deletePlaylistAction->setEnabled( !readonly ); 222 renamePlaylistAction->setEnabled( !readonly ); 223 addToLocalAction->setEnabled( readonly ); 224 225 // Handle any custom actions registered for playlists 226 if ( type == SourcesModel::StaticPlaylist && !readonly && 227 !ActionCollection::instance()->getAction( ActionCollection::LocalPlaylists ).isEmpty() ) 228 { 229 m_playlistMenu.addSeparator(); 230 231 const PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex ); 232 const playlist_ptr playlist = item->playlist(); 233 foreach ( QAction* action, ActionCollection::instance()->getAction( ActionCollection::LocalPlaylists ) ) 234 { 235 if ( QObject* notifier = ActionCollection::instance()->actionNotifier( action ) ) 236 { 237 QMetaObject::invokeMethod( notifier, "aboutToShow", Qt::DirectConnection, Q_ARG( QAction*, action ), Q_ARG( Tomahawk::playlist_ptr, playlist ) ); 238 } 239 240 action->setProperty( "payload", QVariant::fromValue< playlist_ptr >( playlist ) ); 241 m_playlistMenu.addAction( action ); 242 } 243 } 244 245 connect( loadPlaylistAction, SIGNAL( triggered() ), SLOT( loadPlaylist() ) ); 246 connect( renamePlaylistAction, SIGNAL( triggered() ), SLOT( renamePlaylist() ) ); 247 connect( deletePlaylistAction, SIGNAL( triggered() ), SLOT( deletePlaylist() ) ); 248 connect( copyPlaylistAction, SIGNAL( triggered() ), SLOT( copyPlaylistLink() ) ); 249 connect( addToLocalAction, SIGNAL( triggered() ), SLOT( addToLocal() ) ); 250 connect( latchOnAction, SIGNAL( triggered() ), SLOT( latchOnOrCatchUp() ), Qt::QueuedConnection ); 251} 252 253 254void 255SourceTreeView::showOfflineSources( bool offlineSourcesShown ) 256{ 257 m_proxyModel->showOfflineSources( offlineSourcesShown ); 258} 259 260 261void 262SourceTreeView::onSelectionChanged() 263{ 264 if ( currentIndex() != m_selectedIndex ) 265 { 266 selectionModel()->blockSignals( true ); 267 selectionModel()->select( m_selectedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current ); 268 selectionModel()->blockSignals( false ); 269 } 270} 271 272 273void 274SourceTreeView::onItemActivated( const QModelIndex& index ) 275{ 276 if ( !index.isValid() || !index.flags().testFlag( Qt::ItemIsEnabled ) ) 277 return; 278 279 SourceTreeItem* item = itemFromIndex< SourceTreeItem >( index ); 280 item->activate(); 281} 282 283 284void 285SourceTreeView::onItemDoubleClicked( const QModelIndex& idx ) 286{ 287 if ( !selectionModel()->selectedIndexes().contains( idx ) ) 288 onItemActivated( idx ); 289 290 SourceTreeItem* item = itemFromIndex< SourceTreeItem >( idx ); 291 item->doubleClicked(); 292} 293 294 295void 296SourceTreeView::onItemExpanded( const QModelIndex& idx ) 297{ 298 // make sure to expand children nodes for collections 299 if ( idx.data( SourcesModel::SourceTreeItemTypeRole ) == SourcesModel::Collection ) 300 { 301 for( int i = 0; i < model()->rowCount( idx ); i++ ) 302 { 303 setExpanded( model()->index( i, 0, idx ), true ); 304 } 305 } 306} 307 308 309void 310SourceTreeView::selectRequest( const QPersistentModelIndex& idx ) 311{ 312 m_selectedIndex = idx; 313 314 if ( !selectionModel()->selectedIndexes().contains( idx ) ) 315 { 316 scrollTo( idx, QTreeView::EnsureVisible ); 317 selectionModel()->select( idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current ); 318 } 319} 320 321 322void 323SourceTreeView::expandRequest( const QPersistentModelIndex& idx ) 324{ 325 expand( idx ); 326} 327 328 329void 330SourceTreeView::toggleExpandRequest( const QPersistentModelIndex& idx ) 331{ 332 if ( isExpanded( idx ) ) 333 collapse( idx ); 334 else 335 expand( idx ); 336} 337 338 339void 340SourceTreeView::loadPlaylist() 341{ 342 onItemActivated( m_contextMenuIndex ); 343} 344 345 346void 347SourceTreeView::deletePlaylist( const QModelIndex& idxIn ) 348{ 349 QModelIndex idx = idxIn.isValid() ? idxIn : m_contextMenuIndex; 350 if ( !idx.isValid() ) 351 return; 352 353 SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( idx, SourcesModel::SourceTreeItemTypeRole ).toInt(); 354 QString typeDesc; 355 switch ( type ) 356 { 357 case SourcesModel::StaticPlaylist: 358 typeDesc = tr( "playlist" ); 359 break; 360 361 case SourcesModel::AutomaticPlaylist: 362 typeDesc = tr( "automatic playlist" ); 363 break; 364 365 case SourcesModel::Station: 366 typeDesc = tr( "station" ); 367 break; 368 369 default: 370 Q_ASSERT( false ); 371 } 372 373 PlaylistItem* item = itemFromIndex< PlaylistItem >( idx ); 374 playlist_ptr playlist = item->playlist(); 375 QPoint rightCenter = viewport()->mapToGlobal( visualRect( idx ).topRight() + QPoint( 0, visualRect( idx ).height() / 2 ) ); 376#ifdef Q_OS_WIN 377 rightCenter = QApplication::activeWindow()->mapFromGlobal( rightCenter ); 378#endif 379 380 if ( playlist->hasCustomDeleter() ) 381 { 382 playlist->customDelete( rightCenter ); 383 } 384 else 385 { 386 if ( m_popupDialog.isNull() ) 387 { 388 m_popupDialog = QWeakPointer< SourceTreePopupDialog >( new SourceTreePopupDialog() ); 389 connect( m_popupDialog.data(), SIGNAL( result( bool ) ), this, SLOT( onDeletePlaylistResult( bool ) ) ); 390 } 391 392 m_popupDialog.data()->setMainText( tr( "Would you like to delete the %1 <b>\"%2\"</b>?", "e.g. Would you like to delete the playlist named Foobar?" ) 393 .arg( typeDesc ).arg( idx.data().toString() ) ); 394 m_popupDialog.data()->setOkButtonText( tr( "Delete" ) ); 395 m_popupDialog.data()->setProperty( "idx", QVariant::fromValue< QModelIndex >( idx ) ); 396 397 m_popupDialog.data()->move( rightCenter.x() - m_popupDialog.data()->offset(), rightCenter.y() - m_popupDialog.data()->sizeHint().height() / 2. ); 398 m_popupDialog.data()->show(); 399 } 400 401} 402 403 404void 405SourceTreeView::onDeletePlaylistResult( bool result ) 406{ 407 Q_ASSERT( !m_popupDialog.isNull() ); 408 409 const QModelIndex idx = m_popupDialog.data()->property( "idx" ).value< QModelIndex >(); 410 Q_ASSERT( idx.isValid() ); 411 412 if ( !result ) 413 return; 414 415 SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( idx, SourcesModel::SourceTreeItemTypeRole ).toInt(); 416 417 if ( type == SourcesModel::StaticPlaylist ) 418 { 419 PlaylistItem* item = itemFromIndex< PlaylistItem >( idx ); 420 playlist_ptr playlist = item->playlist(); 421 qDebug() << "Doing delete of playlist:" << playlist->title(); 422 Playlist::remove( playlist ); 423 } 424 else if ( type == SourcesModel::AutomaticPlaylist || type == SourcesModel::Station ) 425 { 426 DynamicPlaylistItem* item = itemFromIndex< DynamicPlaylistItem >( idx ); 427 dynplaylist_ptr playlist = item->dynPlaylist(); 428 qDebug() << "Doing delete of playlist:" << playlist->title(); 429 DynamicPlaylist::remove( playlist ); 430 } 431} 432 433 434void 435SourceTreeView::copyPlaylistLink() 436{ 437 QModelIndex idx = m_contextMenuIndex; 438 if ( !idx.isValid() ) 439 return; 440 441 SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt(); 442 if ( type == SourcesModel::AutomaticPlaylist || type == SourcesModel::Station ) 443 { 444 DynamicPlaylistItem* item = itemFromIndex< DynamicPlaylistItem >( m_contextMenuIndex ); 445 dynplaylist_ptr playlist = item->dynPlaylist(); 446 GlobalActionManager::instance()->copyPlaylistToClipboard( playlist ); 447 } 448 else if ( type == SourcesModel::StaticPlaylist ) 449 { 450 const PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex ); 451 const playlist_ptr playlist = item->playlist(); 452 453 GlobalActionManager::instance()->getShortLink( playlist ); 454 } 455} 456 457 458void 459SourceTreeView::exportPlaylist() 460{ 461 const QModelIndex idx = m_contextMenuIndex; 462 if ( !idx.isValid() ) 463 return; 464 465 const SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt(); 466 Q_ASSERT( type == SourcesModel::StaticPlaylist ); 467 if ( type != SourcesModel::StaticPlaylist ) 468 return; 469 470 const PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex ); 471 const playlist_ptr playlist = item->playlist(); 472 473 const QString suggestedFilename = TomahawkSettings::instance()->playlistDefaultPath() + "/" + playlist->title(); 474 const QString filename = QFileDialog::getSaveFileName( TomahawkUtils::tomahawkWindow(), tr( "Save XSPF" ), 475 suggestedFilename, tr( "Playlists (*.xspf)" ) ); 476 if ( !filename.isEmpty() ) 477 { 478 const QFileInfo playlistAbsoluteFilePath( filename ); 479 TomahawkSettings::instance()->setPlaylistDefaultPath( playlistAbsoluteFilePath.absolutePath() ); 480 GlobalActionManager::instance()->savePlaylistToFile( playlist, filename ); 481 } 482} 483 484 485void 486SourceTreeView::addToLocal() 487{ 488 QModelIndex idx = m_contextMenuIndex; 489 if ( !idx.isValid() ) 490 return; 491 492 SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt(); 493 if ( type == SourcesModel::AutomaticPlaylist || type == SourcesModel::Station ) 494 { 495 DynamicPlaylistItem* item = itemFromIndex< DynamicPlaylistItem >( m_contextMenuIndex ); 496 dynplaylist_ptr playlist = item->dynPlaylist(); 497 498 // copy to a link and then generate a new playlist from that 499 // this way we cheaply regenerate the needed controls 500 QString link = GlobalActionManager::instance()->copyPlaylistToClipboard( playlist ); 501 GlobalActionManager::instance()->parseTomahawkLink( link ); 502 } 503 else if ( type == SourcesModel::StaticPlaylist ) 504 { 505 PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex ); 506 playlist_ptr playlist = item->playlist(); 507 508 // just create the new playlist with the same values 509 QList< query_ptr > queries; 510 foreach( const plentry_ptr& e, playlist->entries() ) 511 queries << e->query(); 512 513 playlist_ptr newpl = Playlist::create( SourceList::instance()->getLocal(), uuid(), playlist->title(), playlist->info(), playlist->creator(), playlist->shared(), queries ); 514 } 515} 516 517 518void 519SourceTreeView::latchOnOrCatchUp() 520{ 521 disconnect( this, SLOT( latchOnOrCatchUp() ) ); 522 if ( !m_contextMenuIndex.isValid() ) 523 return; 524 525 SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt(); 526 if ( type != SourcesModel::Collection ) 527 return; 528 529 SourceItem* item = itemFromIndex< SourceItem >( m_contextMenuIndex ); 530 source_ptr source = item->source(); 531 532 latchOnOrCatchUp( source ); 533} 534 535 536void 537SourceTreeView::latchOff() 538{ 539 disconnect( this, SLOT( latchOff() ) ); 540 qDebug() << Q_FUNC_INFO; 541 if ( !m_contextMenuIndex.isValid() ) 542 return; 543 544 SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt(); 545 if ( type != SourcesModel::Collection ) 546 return; 547 548 const SourceItem* item = itemFromIndex< SourceItem >( m_contextMenuIndex ); 549 const source_ptr source = item->source(); 550 551 latchOff( source ); 552} 553 554 555void 556SourceTreeView::latchOnOrCatchUp( const Tomahawk::source_ptr& source ) 557{ 558 if ( m_latchManager->isLatched( source ) ) 559 emit catchUpRequest(); 560 else 561 emit latchRequest( source ); 562} 563 564 565void 566SourceTreeView::latchOff( const Tomahawk::source_ptr& source ) 567{ 568 emit unlatchRequest( source ); 569} 570 571 572void 573SourceTreeView::latchModeToggled( bool checked ) 574{ 575 576 disconnect( this, SLOT( latchOff() ) ); 577 qDebug() << Q_FUNC_INFO; 578 if ( !m_contextMenuIndex.isValid() ) 579 return; 580 581 SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt(); 582 if ( type != SourcesModel::Collection ) 583 return; 584 585 const SourceItem* item = itemFromIndex< SourceItem >( m_contextMenuIndex ); 586 const source_ptr source = item->source(); 587 emit latchModeChangeRequest( source, checked ); 588} 589 590 591void 592SourceTreeView::renamePlaylist() 593{ 594 if ( !m_contextMenuIndex.isValid() && !selectionModel()->selectedIndexes().isEmpty() ) 595 edit( selectionModel()->selectedIndexes().first() ); 596 else 597 edit( m_contextMenuIndex ); 598} 599 600 601void 602SourceTreeView::onCustomContextMenu( const QPoint& pos ) 603{ 604 qDebug() << Q_FUNC_INFO; 605 606 QModelIndex idx = m_contextMenuIndex = indexAt( pos ); 607 if ( !idx.isValid() ) 608 return; 609 610 setupMenus(); 611 612 const QList< QAction* > customActions = model()->data( m_contextMenuIndex, SourcesModel::CustomActionRole ).value< QList< QAction* > >(); 613 614 if ( model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ) == SourcesModel::StaticPlaylist || 615 model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ) == SourcesModel::AutomaticPlaylist || 616 model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ) == SourcesModel::Station ) 617 { 618 PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex ); 619 if ( item->playlist()->author()->isLocal() ) 620 m_playlistMenu.exec( mapToGlobal( pos ) ); 621 else 622 m_roPlaylistMenu.exec( mapToGlobal( pos ) ); 623 } 624 else if ( model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ) == SourcesModel::Collection ) 625 { 626 SourceItem* item = itemFromIndex< SourceItem >( m_contextMenuIndex ); 627 if ( !item->source().isNull() && !item->source()->isLocal() ) 628 m_latchMenu.exec( mapToGlobal( pos ) ); 629 else if ( !item->source().isNull() ) 630 m_privacyMenu.exec( mapToGlobal( pos ) ); 631 } 632 else if ( !customActions.isEmpty() ) 633 { 634 QMenu customMenu; 635 customMenu.addActions( customActions ); 636 customMenu.exec( mapToGlobal( pos ) ); 637 } 638} 639 640 641void 642SourceTreeView::dragEnterEvent( QDragEnterEvent* event ) 643{ 644 qDebug() << Q_FUNC_INFO; 645 QTreeView::dragEnterEvent( event ); 646 647 if ( DropJob::acceptsMimeData( event->mimeData(), DropJob::Track | DropJob::Artist | DropJob::Album | DropJob::Playlist, DropJob::Create ) ) 648 { 649 m_dragging = true; 650 m_dropRect = QRect(); 651 m_dropIndex = QPersistentModelIndex(); 652 653 event->setDropAction( Qt::CopyAction ); 654 event->acceptProposedAction(); 655 } 656} 657 658 659void 660SourceTreeView::dragLeaveEvent( QDragLeaveEvent* event ) 661{ 662 QTreeView::dragLeaveEvent( event ); 663 664 m_dragging = false; 665 setDirtyRegion( m_dropRect ); 666 667 m_delegate->dragLeaveEvent(); 668 dataChanged( m_dropIndex, m_dropIndex ); 669 m_dropIndex = QPersistentModelIndex(); 670} 671 672 673void 674SourceTreeView::dragMoveEvent( QDragMoveEvent* event ) 675{ 676 bool accept = false; 677 678 // Don't highlight the drop for a playlist, as it won't get added to the playlist but created generally 679 if ( DropJob::isDropType( DropJob::Playlist, event->mimeData() ) ) 680 { 681 event->accept(); 682 return; 683 } 684 685 QTreeView::dragMoveEvent( event ); 686 687 if ( DropJob::acceptsMimeData( event->mimeData(), DropJob::Track, DropJob::Append ) ) 688 { 689 setDirtyRegion( m_dropRect ); 690 const QPoint pos = event->pos(); 691 const QModelIndex index = indexAt( pos ); 692 dataChanged( m_dropIndex, m_dropIndex ); 693 m_dropIndex = QPersistentModelIndex( index ); 694 695 if ( index.isValid() ) 696 { 697 const QRect rect = visualRect( index ); 698 m_dropRect = rect; 699 700 SourceTreeItem* item = itemFromIndex< SourceTreeItem >( index ); 701 702 if ( item->willAcceptDrag( event->mimeData() ) ) 703 { 704 accept = true; 705 706 switch ( model()->data( index, SourcesModel::SourceTreeItemTypeRole ).toInt() ) 707 { 708 case SourcesModel::StaticPlaylist: 709 case SourcesModel::CategoryAdd: 710 m_delegate->hovered( index, event->mimeData() ); 711 dataChanged( index, index ); 712 break; 713 714 default: 715 break; 716 } 717 718 } 719 else 720 m_delegate->hovered( QModelIndex(), 0 ); 721 } 722 else 723 { 724 m_dropRect = QRect(); 725 } 726 727 if ( accept || DropJob::isDropType( DropJob::Playlist, event->mimeData() ) ) 728 { 729 // Playlists are accepted always since they can be dropped anywhere 730 //tDebug() << Q_FUNC_INFO << "Accepting"; 731 event->setDropAction( Qt::CopyAction ); 732 event->accept(); 733 } 734 else 735 { 736// tDebug() << Q_FUNC_INFO << "Ignoring"; 737 event->ignore(); 738 } 739 } 740 else if ( DropJob::acceptsMimeData( event->mimeData(), DropJob::Playlist | DropJob::Artist | DropJob::Album, DropJob::Create ) ) 741 { 742 event->setDropAction( Qt::CopyAction ); 743 event->accept(); 744 } 745 setDirtyRegion( m_dropRect ); 746} 747 748 749void 750SourceTreeView::dropEvent( QDropEvent* event ) 751{ 752 const QPoint pos = event->pos(); 753 const QModelIndex index = indexAt( pos ); 754 755 if ( model()->data( index, SourcesModel::SourceTreeItemTypeRole ).toInt() == SourcesModel::StaticPlaylist 756 || model()->data( index, SourcesModel::SourceTreeItemTypeRole ).toInt() == SourcesModel::LovedTracksPage 757 || model()->data( index, SourcesModel::SourceTreeItemTypeRole ).toInt() == SourcesModel::CategoryAdd ) 758 { 759 SourceTreeItem* item = itemFromIndex< SourceTreeItem >( index ); 760 Q_ASSERT( item ); 761 762 item->setDropType( m_delegate->hoveredDropType() ); 763 qDebug() << Q_FUNC_INFO << "dropType is " << m_delegate->hoveredDropType(); 764 } 765 766 // if it's a playlist drop, accept it anywhere in the sourcetree by manually parsing it. 767 if ( DropJob::isDropType( DropJob::Playlist, event->mimeData() ) ) 768 { 769 qDebug() << Q_FUNC_INFO << "Current Event"; 770 DropJob* dropThis = new DropJob; 771 dropThis->setDropTypes( DropJob::Playlist ); 772 dropThis->setDropAction( DropJob::Create ); 773 dropThis->parseMimeData( event->mimeData() ); 774 775 // Don't add it to the playlist under drop, it's a new playlist now 776 return; 777 } 778 779 // Need to fake the dropevent because the treeview would reject it if it is outside the item (on the tree) 780 if ( pos.x() < 100 ) 781 { 782 QDropEvent* newEvent = new QDropEvent( pos + QPoint( 100, 0 ), event->possibleActions(), event->mimeData(), event->mouseButtons(), event->keyboardModifiers(), event->type() ); 783 QTreeView::dropEvent( newEvent ); 784 delete newEvent; 785 } 786 else 787 { 788 QTreeView::dropEvent( event ); 789 } 790 791 m_dragging = false; 792 m_dropIndex = QPersistentModelIndex(); 793 m_delegate->dragLeaveEvent(); 794 dataChanged( index, index ); 795} 796 797 798void 799SourceTreeView::keyPressEvent( QKeyEvent* event ) 800{ 801 if ( ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace ) && !selectionModel()->selectedIndexes().isEmpty() ) 802 { 803 QModelIndex idx = selectionModel()->selectedIndexes().first(); 804 if ( model()->data( idx, SourcesModel::SourceTreeItemTypeRole ) == SourcesModel::StaticPlaylist || 805 model()->data( idx, SourcesModel::SourceTreeItemTypeRole ) == SourcesModel::AutomaticPlaylist || 806 model()->data( idx, SourcesModel::SourceTreeItemTypeRole ) == SourcesModel::Station ) 807 { 808 PlaylistItem* item = itemFromIndex< PlaylistItem >( idx ); 809 Q_ASSERT( item ); 810 811 if ( item->playlist()->author()->isLocal() ) 812 deletePlaylist( idx ); 813 } 814 event->accept(); 815 } 816 else 817 { 818 event->ignore(); 819 } 820 QTreeView::keyPressEvent( event ); 821} 822 823 824void 825SourceTreeView::paintEvent( QPaintEvent* event ) 826{ 827 if ( m_dragging && !m_dropRect.isEmpty() ) 828 { 829 QPainter painter( viewport() ); 830 const QRect itemRect = visualRect( m_dropIndex ); 831 832 QStyleOptionViewItemV4 opt; 833 opt.initFrom( this ); 834 opt.rect = itemRect; 835 opt.state = QStyle::State_Enabled | QStyle::State_Selected; 836 837 style()->drawPrimitive( QStyle::PE_PanelItemViewRow, &opt, &painter, this ); 838 } 839 840 QTreeView::paintEvent( event ); 841} 842 843 844void 845SourceTreeView::drawRow( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const 846{ 847 QTreeView::drawRow( painter, option, index ); 848} 849 850void 851SourceTreeView::drawBranches( QPainter *painter, const QRect &rect, const QModelIndex &index ) const 852{ 853 if( !QString( qApp->style()->metaObject()->className() ).toLower().contains( "qtcurve" ) ) 854 QTreeView::drawBranches( painter, rect, index ); 855} 856 857 858template< typename T > T* 859SourceTreeView::itemFromIndex( const QModelIndex& index ) const 860{ 861 Q_ASSERT( model()->data( index, SourcesModel::SourceTreeItemRole ).value< SourceTreeItem* >() ); 862 863 T* item = qobject_cast< T* >( model()->data( index, SourcesModel::SourceTreeItemRole ).value< SourceTreeItem* >() ); 864 Q_ASSERT( item ); 865 866 return item; 867} 868 869 870void 871SourceTreeView::update( const QModelIndex &index ) 872{ 873 dataChanged( index, index ); 874}