PageRenderTime 64ms CodeModel.GetById 15ms app.highlight 42ms RepoModel.GetById 2ms app.codeStats 0ms

/src/libtomahawk/playlist/dynamic/DynamicView.cpp

http://github.com/tomahawk-player/tomahawk
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}