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

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