/src/libtomahawk/playlist/dynamic/DynamicView.cpp
C++ | 360 lines | 260 code | 58 blank | 42 comment | 40 complexity | 7ff1fc4cb03ab653d972d2bf0f6c6900 MD5 | raw file
1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> === 2 * 3 * Copyright 2010-2011, Leo Franchi <lfranchi@kde.org> 4 * Copyright 2010-2011, Jeff Mitchell <jeff@tomahawk-player.org> 5 * Copyright 2014, Christian Muehlhaeuser <muesli@tomahawk-player.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 "DynamicView.h" 22 23#include "../PlaylistModel.h" 24#include "../PlayableProxyModel.h" 25#include "DynamicModel.h" 26#include "widgets/OverlayWidget.h" 27#include "utils/Logger.h" 28#include "Source.h" 29 30#include <QApplication> 31#include <QPainter> 32#include <QPaintEvent> 33#include <QPaintEngine> 34#include <QScrollBar> 35 36 37using namespace Tomahawk; 38 39#define FADE_LENGTH 800 40#define SLIDE_LENGTH 300 41#define SLIDE_OFFSET 500 42#define LONG_MULT 0 // to avoid superfast slides when the length is long, make it longer incrementally 43 44 45DynamicView::DynamicView( QWidget* parent ) 46 : TrackView( parent ) 47 , m_onDemand( false ) 48 , m_checkOnCollapse( false ) 49 , m_working( false ) 50 , m_fadebg( false ) 51 , m_fadeOnly( false ) 52{ 53 setAcceptDrops( false ); 54 setSortingEnabled( false ); 55 56 m_fadeOutAnim.setDuration( FADE_LENGTH ); 57 m_fadeOutAnim.setCurveShape( QTimeLine::LinearCurve ); 58 m_fadeOutAnim.setFrameRange( 100, 0 ); 59 m_fadeOutAnim.setUpdateInterval( 5 ); 60 61 QEasingCurve curve( QEasingCurve::OutBounce ); 62 curve.setAmplitude( .25 ); 63 m_slideAnim.setEasingCurve( curve ); 64 m_slideAnim.setDirection( QTimeLine::Forward ); 65 m_fadeOutAnim.setUpdateInterval( 5 ); 66 67 connect( &m_fadeOutAnim, SIGNAL( frameChanged( int ) ), viewport(), SLOT( update() ) ); 68 connect( &m_fadeOutAnim, SIGNAL( finished() ), this, SLOT( animFinished() ) ); 69} 70 71 72DynamicView::~DynamicView() 73{ 74} 75 76 77void 78DynamicView::setDynamicModel( DynamicModel* model ) 79{ 80 m_model = model; 81 TrackView::setPlayableModel( m_model ); 82 setAcceptDrops( false ); 83 84 connect( m_model, SIGNAL( itemCountChanged( unsigned int ) ), SLOT( onTrackCountChanged( unsigned int ) ) ); 85 connect( m_model, SIGNAL( checkForOverflow() ), SLOT( checkForOverflow() ) ); 86} 87 88 89void 90DynamicView::setOnDemand( bool onDemand ) 91{ 92 m_onDemand = onDemand; 93 94 if ( m_onDemand ) 95 setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); 96 else 97 setVerticalScrollBarPolicy( Qt::ScrollBarAsNeeded ); 98} 99 100 101void 102DynamicView::setReadOnly( bool readOnly ) 103{ 104 m_readOnly = readOnly; 105} 106 107 108void 109DynamicView::showMessageTimeout( const QString& title, const QString& body ) 110{ 111 m_title = title; 112 m_body = body; 113 114 overlay()->setText( QString( "%1:\n\n%2" ).arg( m_title, m_body ) ); 115 overlay()->show( 10 ); 116} 117 118 119void 120DynamicView::showMessage( const QString& message ) 121{ 122 overlay()->setText( message ); 123 overlay()->show(); 124} 125 126 127void 128DynamicView::setDynamicWorking( bool working ) 129{ 130 m_working = working; 131 if ( working ) 132 overlay()->hide(); 133 else 134 onTrackCountChanged( proxyModel()->rowCount() ); 135} 136 137 138void 139DynamicView::onTrackCountChanged( unsigned int tracks ) 140{ 141 if ( tracks == 0 && !m_working ) 142 { 143 if ( m_onDemand ) 144 { 145 if ( !m_readOnly ) 146 overlay()->setText( tr( "Add some filters above to seed this station!" ) ); 147 else 148 return; // when viewing a read-only station, don't show anything 149 } else 150 if ( m_readOnly ) 151 overlay()->setText( tr( "Press Generate to get started!" ) ); 152 else 153 overlay()->setText( tr( "Add some filters above, and press Generate to get started!" ) ); 154 if ( !overlay()->shown() ) 155 overlay()->show(); 156 } 157 else { 158 overlay()->hide(); 159 } 160} 161 162 163void 164DynamicView::checkForOverflow() 165{ 166 if ( !m_onDemand || proxyModel()->rowCount( QModelIndex() ) == 0 ) 167 return; 168 169 if ( m_fadeOutAnim.state() == QTimeLine::Running ) 170 m_checkOnCollapse = true; 171 172 /// We don't want stations to grow forever, because we don't want the view to have to scroll 173 /// So if there are too many tracks, we remove some that have already been played 174 /// Our threshold is 4 rows to the end. That's when we collapse. 175 QModelIndex last = proxyModel()->index( proxyModel()->rowCount( QModelIndex() ) - 1, 0, QModelIndex() ); 176 QRect lastRect = visualRect( last ); 177 qDebug() << "Checking viewport height of" << viewport()->height() << "and last track bottom:" << lastRect.bottomLeft().y() << "under threshold" << 4 * lastRect.height(); 178 if ( viewport()->height() - lastRect.bottomLeft().y() <= ( 4 * lastRect.height() ) ) 179 { 180 qDebug() << "Deciding to remove some tracks from this station"; 181 182 // figure out how many to remove. lets get rid of 1/3rd of the backlog, visually. 183 int toRemove = ( viewport()->height() / 3 ) / lastRect.height(); 184 qDebug() << "Decided to remove" << toRemove << "rows!"; 185 collapseEntries( 0, toRemove, proxyModel()->rowCount( QModelIndex() ) - toRemove ); 186 } 187} 188 189 190void 191DynamicView::collapseEntries( int startRow, int num, int numToKeep ) 192{ 193 qDebug() << "BEGINNING TO COLLAPSE FROM" << startRow << num << numToKeep; 194 if ( m_fadeOutAnim.state() == QTimeLine::Running ) 195 { 196 qDebug() << "COLLAPSING TWICE, aborting!"; 197 return; 198 } 199 200 /// Two options: Either we are overflowing our view, or we're not. If we are, it's because the search for a playable track 201 /// went past the limit of the view. Just fade out from the beginning to the end in that case. otherwise, animate a slide 202 int realNum = num; 203 QModelIndex last = indexAt( QPoint( 3, viewport()->height() - 3 ) ); 204 if ( last.isValid() && last.row() < startRow + num ) 205 { 206 m_fadeOnly = true; 207 realNum = last.row() - startRow; 208 } else { 209 m_fadeOnly = false; 210 } 211 212 // we capture the image of the rows we're going to collapse 213 // then we capture the image of the target row we're going to animate downwards 214 // then we fade the first image out while sliding the second image up. 215 QModelIndex topLeft = proxyModel()->index( startRow, 0, QModelIndex() ); 216 QModelIndex bottomRight = proxyModel()->index( startRow + realNum - 1, proxyModel()->columnCount( QModelIndex() ) - 1, QModelIndex() ); 217 QItemSelection sel( topLeft, bottomRight ); 218 qDebug() << "Created selection from:" << startRow << "to" << startRow + realNum - 1; 219 QRect fadingRect = visualRegionForSelection( sel ).boundingRect(); 220 QRect fadingRectViewport = fadingRect; // all values that we use in paintEvent() have to be in viewport coords 221 fadingRect.moveTo( viewport()->mapTo( this, fadingRect.topLeft() ) ); 222 //fadingRect.setBottom( qMin( fadingRect.bottom(), viewport()->mapTo( this, viewport()->rect().bottomLeft() ).y() ) ); // limit what we capture to the viewport rect, if the last item is partially obscured 223 224 m_fadingIndexes = QPixmap::grabWidget( this, fadingRect ); // but all values we use to grab the widgetr have to be in scrollarea coords :( 225 m_fadingPointAnchor = QPoint( 0, fadingRectViewport.topLeft().y() ); 226 227 // get the background 228 m_bg = backgroundBetween( m_fadingIndexes.rect(), startRow ); 229 230 m_fadeOutAnim.start(); 231 232 qDebug() << "Grabbed fading indexes from rect:" << fadingRect << m_fadingIndexes.size() << "ANCHORED:" << m_fadingPointAnchor; 233 234 if ( !m_fadeOnly ) 235 { 236 /// sanity checks. make sure we have all the rows we need 237 int firstSlider = startRow + realNum; 238 qDebug() << "Sliding from" << firstSlider << "number:" << numToKeep - 1 << "rowcount is:" << proxyModel()->rowCount(); 239 // we may have removed some rows since we first started counting, so adjust 240 //Q_ASSERT( firstSlider + numToKeep - 1 <= proxyModel()->rowCount() ); 241 if ( firstSlider + numToKeep - 1 >= proxyModel()->rowCount() ) 242 { 243 if ( numToKeep == 1 ) 244 { 245 // we just want the last row 246 firstSlider = proxyModel()->rowCount(); 247 } 248 } 249 250 topLeft = proxyModel()->index( startRow + realNum, 0, QModelIndex() ); 251 bottomRight = proxyModel()->index( startRow + realNum + numToKeep - 1, proxyModel()->columnCount( QModelIndex() ) - 1, QModelIndex() ); 252 QRect slidingRect = visualRegionForSelection( QItemSelection( topLeft, bottomRight ) ).boundingRect(); 253 QRect slidingRectViewport = slidingRect; 254 // map internal view coord to external qscrollarea 255 slidingRect.moveTo( viewport()->mapTo( this, slidingRect.topLeft() ) ); 256 257 m_slidingIndex = QPixmap::grabWidget( this, slidingRect ); 258 m_bottomAnchor = QPoint( 0, slidingRectViewport.topLeft().y() ); 259 m_bottomOfAnim = QPoint( 0, slidingRectViewport.bottomLeft().y() ); 260 qDebug() << "Grabbed sliding index from rect:" << slidingRect << m_slidingIndex.size(); 261 262 // slide from the current position to the new one 263 int frameRange = fadingRect.topLeft().y() - slidingRect.topLeft().y(); 264 m_slideAnim.setDuration( SLIDE_LENGTH + frameRange * LONG_MULT ); 265 m_slideAnim.setFrameRange( slidingRectViewport.topLeft().y(), fadingRectViewport.topLeft().y() ); 266 267 QTimer::singleShot( SLIDE_OFFSET, &m_slideAnim, SLOT( start() ) ); 268 } 269 270 // delete the actual indices 271 QModelIndexList todel; 272 for( int i = 0; i < num; i++ ) 273 { 274 for( int k = 0; k < proxyModel()->columnCount( QModelIndex() ); k++ ) 275 { 276 todel << proxyModel()->index( startRow + i, k ); 277 } 278 } 279 proxyModel()->removeIndexes( todel ); 280} 281 282 283QPixmap 284DynamicView::backgroundBetween( QRect rect, int rowStart ) 285{ 286 QPixmap bg = QPixmap( rect.size() ); 287 bg.fill( Qt::white ); 288 QPainter p( &bg ); 289 QStyleOptionViewItem opt = viewOptions(); 290 // code taken from QTreeViewPrivate::paintAlternatingRowColors 291 m_fadebg = !style()->styleHint( QStyle::SH_ItemView_PaintAlternatingRowColorsForEmptyArea, &opt ); 292 // qDebug() << "PAINTING ALTERNATING ROW BG!: " << fadingRectViewport; 293 int rowHeight = itemDelegate()->sizeHint( opt, QModelIndex() ).height() + 1; 294 int y = 0; 295 int current = rowStart; 296 while( y <= rect.bottomLeft().y() ) 297 { 298 opt.rect.setRect(0, y, viewport()->width(), rowHeight); 299 // qDebug() << "PAINTING BG ROW IN RECT" << y << "to" << y + rowHeight << ":" << opt.rect; 300 if ( current & 1 ) 301 { 302 opt.features |= QStyleOptionViewItem::Alternate; 303 } else { 304 opt.features &= ~QStyleOptionViewItem::Alternate; 305 } 306 ++current; 307 style()->drawPrimitive( QStyle::PE_PanelItemViewRow, &opt, &p ); 308 y += rowHeight; 309 } 310 311 return bg; 312} 313 314 315void 316DynamicView::animFinished() 317{ 318 if ( m_checkOnCollapse ) 319 checkForOverflow(); 320 m_checkOnCollapse = false; 321} 322 323 324void 325DynamicView::paintEvent( QPaintEvent* event ) 326{ 327 TrackView::paintEvent(event); 328 329 QPainter p( viewport() ); 330 if ( m_fadeOutAnim.state() == QTimeLine::Running ) 331 { // both run together 332 p.save(); 333 QRect bg = m_fadingIndexes.rect(); 334 bg.moveTo( m_fadingPointAnchor ); // cover up the background 335 p.fillRect( bg, Qt::white ); 336 if ( m_fadebg ) 337 { 338 p.save(); 339 p.setOpacity( 1 - m_fadeOutAnim.currentValue() ); 340 } 341 p.drawPixmap( bg, m_bg ); 342 if ( m_fadebg ) 343 { 344 p.restore(); 345 } 346 // qDebug() << "FAST SETOPACITY:" << p.paintEngine()->hasFeature(QPaintEngine::ConstantOpacity); 347 p.setOpacity( 1 - m_fadeOutAnim.currentValue() ); 348 p.drawPixmap( m_fadingPointAnchor, m_fadingIndexes ); 349 p.restore(); 350 351 if ( m_slideAnim.state() == QTimeLine::Running ) 352 { 353 // draw the collapsing entry 354 p.drawPixmap( 0, m_slideAnim.currentFrame(), m_slidingIndex ); 355 } else if ( m_fadeOutAnim.state() == QTimeLine::Running && !m_fadeOnly ) 356 { 357 p.drawPixmap( 0, m_bottomAnchor.y(), m_slidingIndex ); 358 } 359 } 360}