Lots of work on album grouping in the playlist. Now handles ragging around stuff...
[amarok.git] / src / Statistics.cpp
blobf007141bda997f59845ec7ef282f1e2a1088e61f
1 /***************************************************************************
2 * copyright : (C) 2005-2007 Seb Ruiz <ruiz@kde.org> *
3 **************************************************************************/
5 /***************************************************************************
6 * *
7 * This program 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 2 of the License, or *
10 * (at your option) any later version. *
11 * *
12 ***************************************************************************/
14 #include "Statistics.h"
16 #include "amarok.h" //oldForeach macro
17 #include "amarokconfig.h"
18 #include "browserToolBar.h" //search toolbar
19 #include "debug.h"
21 #include "playlist/PlaylistModel.h"
22 #include "querybuilder.h"
23 #include "tagdialog.h" //showContextMenu()
24 #include "TheInstances.h"
26 #include <k3multipledrag.h>
27 #include <k3urldrag.h> //startDrag()
28 #include <KApplication>
29 #include <KIconLoader>
30 #include <KLocale>
31 #include <KMenu>
32 #include <KWindowSystem>
34 #include <kvbox.h>
35 #include <q3header.h>
36 #include <Q3PopupMenu>
37 #include <q3simplerichtext.h>
38 #include <QDateTime>
39 #include <QPainter>
40 #include <QTimer>
41 #include <QToolButton>
42 #include <QToolTip>
45 namespace Amarok {
47 /*
48 * Transform to be usable within HTML/HTML attributes
50 QString escapeHTMLAttr( const QString &s )
52 return QString(s).replace( "%", "%25" ).replace( "'", "%27" ).replace( "\"", "%22" ).
53 replace( "#", "%23" ).replace( "?", "%3F" );
55 QString unescapeHTMLAttr( const QString &s )
57 return QString(s).replace( "%3F", "?" ).replace( "%23", "#" ).replace( "%22", "\"" ).
58 replace( "%27", "'" ).replace( "%25", "%" );
61 /**
62 * Function that must be used when separating contextBrowser escaped urls
63 * detail can contain track/discnumber
65 void albumArtistTrackFromUrl( QString url, QString &artist, QString &album, QString &detail )
67 if ( !url.contains("@@@") ) return;
68 //KHTML removes the trailing space!
69 if ( url.endsWith( " @@@" ) )
70 url += ' ';
72 const QStringList list = url.split( " @@@ ", QString::KeepEmptyParts );
74 int size = list.count();
76 if( size<=0 )
77 error() << "size<=0";
79 artist = size > 0 ? unescapeHTMLAttr( list[0] ) : "";
80 album = size > 1 ? unescapeHTMLAttr( list[1] ) : "";
81 detail = size > 2 ? unescapeHTMLAttr( list[2] ) : "";
86 //////////////////////////////////////////////////////////////////////////////////////////
87 /// CLASS Statistics
88 //////////////////////////////////////////////////////////////////////////////////////////
90 Statistics *Statistics::s_instance = 0;
92 Statistics::Statistics( QWidget *parent, const char *name )
93 : KDialog( parent )
94 , m_timer( new QTimer( this ) )
96 setModal( false );
97 setButtons( Close );
98 setDefaultButton( Close );
99 showButtonSeparator( true );
100 setObjectName( name );
102 s_instance = this;
104 // Gives the window a small title bar, and skips a taskbar entry
105 #ifdef Q_WS_X11
106 KWindowSystem::setType( winId(), NET::Utility );
107 KWindowSystem::setState( winId(), NET::SkipTaskbar );
108 #endif
110 kapp->setTopWidget( this );
111 setCaption( KDialog::makeStandardCaption( i18n("Collection Statistics") ) );
112 setInitialSize( QSize( 400, 550 ) );
114 KVBox *box = new KVBox( this );
115 setMainWidget( box );
117 { //<Search LineEdit>
118 KToolBar *bar = new Browser::ToolBar( box );
119 bar->setIconDimensions( 22 ); //looks more sensible
120 bar->setMovable( false ); //removes the ugly frame
121 bar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
123 QToolButton *clearButton = new QToolButton( bar );
124 clearButton->setIcon( KIcon("edit-clear-locationbar") );
125 clearButton->setToolTip( i18n( "Clear search field" ) );
127 m_lineEdit = new KLineEdit( bar );
128 m_lineEdit->setClickMessage( i18n( "Enter search terms here" ) );
130 m_lineEdit->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Minimum);
131 m_lineEdit->setFrame( QFrame::Sunken );
132 m_lineEdit->installEventFilter( this ); //we intercept keyEvents
134 connect( clearButton,SIGNAL( clicked() ) , m_lineEdit , SLOT( clear() ) );
135 connect( m_timer, SIGNAL( timeout() ) , SLOT( slotSetFilter() ) );
136 connect( m_lineEdit, SIGNAL( textChanged( const QString& ) ), SLOT( slotSetFilterTimeout() ) );
137 connect( m_lineEdit, SIGNAL( returnPressed() ) , SLOT( slotSetFilter() ) );
138 } //</Search LineEdit>
140 m_listView = new StatisticsList( box );
143 Statistics::~Statistics()
145 s_instance = 0;
148 void
149 Statistics::slotSetFilterTimeout() //SLOT
151 m_timer->setSingleShot( true ); //stops the timer for us first
152 m_timer->start( 280 );
155 void
156 Statistics::slotSetFilter() //SLOT
158 m_timer->stop();
159 m_listView->setFilter( m_lineEdit->text() );
160 if( m_listView->childCount() > 1 )
161 m_listView->renderView();
162 else
163 m_listView->refreshView();
167 //////////////////////////////////////////////////////////////////////////////////////////
168 /// CLASS StatisticsList
169 //////////////////////////////////////////////////////////////////////////////////////////
171 StatisticsList::StatisticsList( QWidget *parent, const char *name )
172 : K3ListView( parent )
173 , m_currentItem( 0 )
174 , m_expanded( false )
176 header()->hide();
178 addColumn( i18n("Name") );
179 setResizeMode( Q3ListView::LastColumn );
180 setSelectionMode( Q3ListView::Extended );
181 setSorting( -1 );
182 setObjectName( name );
184 setAcceptDrops( false );
185 setDragEnabled( true );
187 connect( this, SIGNAL( onItem( Q3ListViewItem*) ), SLOT( startHover( Q3ListViewItem* ) ) );
188 connect( this, SIGNAL( onViewport() ), SLOT( clearHover() ) );
189 connect( this, SIGNAL( clicked( Q3ListViewItem*) ), SLOT( itemClicked( Q3ListViewItem* ) ) );
190 connect( this, SIGNAL( contextMenuRequested( Q3ListViewItem *, const QPoint &, int ) ),
191 this, SLOT( showContextMenu( Q3ListViewItem *, const QPoint &, int ) ) );
193 if( CollectionDB::instance()->isEmpty() )
194 return;
196 renderView();
199 void
200 StatisticsList::startDrag()
202 // there is only one item ever selected in this tool. maybe this needs to change
204 DEBUG_FUNC_INFO
206 KUrl::List list;
207 K3MultipleDrag *drag = new K3MultipleDrag( this );
209 Q3ListViewItemIterator it( this, Q3ListViewItemIterator::Selected );
211 StatisticsDetailedItem *item = dynamic_cast<StatisticsDetailedItem*>(*it);
213 if ( !item )
214 return;
216 if( item->itemType() == StatisticsDetailedItem::TRACK )
218 list += KUrl( item->url() );
219 drag->addDragObject( new K3URLDrag( list, viewport() ) );
220 drag->setPixmap( CollectionDB::createDragPixmap(list),
221 QPoint( CollectionDB::DRAGPIXMAP_OFFSET_X,
222 CollectionDB::DRAGPIXMAP_OFFSET_Y ) );
224 else
226 Q3TextDrag *textdrag = new Q3TextDrag( '\n' + item->getSQL(), 0 );
227 textdrag->setSubtype( "amarok-sql" );
228 drag->addDragObject( textdrag );
229 drag->setPixmap( CollectionDB::createDragPixmapFromSQL( item->getSQL() ),
230 QPoint( CollectionDB::DRAGPIXMAP_OFFSET_X,
231 CollectionDB::DRAGPIXMAP_OFFSET_Y ) );
234 clearSelection();
235 drag->dragCopy();
238 void
239 StatisticsList::refreshView()
241 if( m_expanded )
243 if( !firstChild() )
245 error() << "Statistics: uh oh, no first child!";
246 return;
248 while( firstChild()->firstChild() )
249 delete firstChild()->firstChild();
251 expandInformation( static_cast<StatisticsItem*>(firstChild()), true /*refresh*/ );
253 else
254 renderView();
257 void
258 StatisticsList::renderView()
260 m_expanded = false;
262 //ensure cleanliness - this function is not just called from the ctor, but also when returning to the initial display
263 while( firstChild() )
264 delete firstChild();
265 m_currentItem = 0;
267 QueryBuilder qb;
268 QStringList a;
270 qb.clear();
271 qb.addReturnFunctionValue( QueryBuilder::funcCount, QueryBuilder::tabSong, QueryBuilder::valURL );
272 qb.setOptions( QueryBuilder::optRemoveDuplicates );
273 a = qb.run();
275 m_trackItem = new StatisticsItem( i18n("Favorite Tracks"), this, 0 );
276 m_trackItem->setSubtext( i18np("%1 track", "%1 tracks", a[0].toInt()) );
278 qb.clear();
279 qb.addReturnFunctionValue( QueryBuilder::funcSum, QueryBuilder::tabStats, QueryBuilder::valPlayCounter );
280 a = qb.run();
282 m_mostplayedItem = new StatisticsItem( i18n("Most Played Tracks"), this, m_trackItem );
283 m_mostplayedItem->setSubtext( i18np("%1 play", "%1 plays", a[0].toInt()) );
285 qb.clear();
286 //qb.addReturnFunctionValue( QueryBuilder::funcCount, QueryBuilder::tabArtist, QueryBuilder::valID );
287 //qb.setOptions( QueryBuilder::optRemoveDuplicates );
288 //a = qb.run();
289 qb.setOptions( QueryBuilder::optRemoveDuplicates );
290 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valArtistID );
291 //I can't get the correct value w/o using a subquery, and querybuilder doesn't support those
292 a = QStringList( QString::number( qb.run().count() ) );
294 m_artistItem = new StatisticsItem( i18n("Favorite Artists"), this, m_mostplayedItem );
295 m_artistItem->setSubtext( i18np("%1 artist", "%1 artists", a[0].toInt()) );
297 qb.clear();
298 //qb.addReturnFunctionValue( QueryBuilder::funcCount, QueryBuilder::tabAlbum, QueryBuilder::valID );
299 //qb.setOptions( QueryBuilder::optRemoveDuplicates );
300 //a = qb.run();
301 qb.setOptions( QueryBuilder::optRemoveDuplicates );
302 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valAlbumID );
303 //I can't get the correct value w/o using a subquery, and querybuilder doesn't support those
304 a = QStringList( QString::number( qb.run().count() ) );
306 m_albumItem = new StatisticsItem( i18n("Favorite Albums"), this, m_artistItem );
307 m_albumItem->setSubtext( i18np("%1 album", "%1 albums", a[0].toInt()) );
309 qb.clear();
310 //qb.addReturnFunctionValue( QueryBuilder::funcCount, QueryBuilder::tabGenre, QueryBuilder::valID );
311 //qb.setOptions( QueryBuilder::optRemoveDuplicates );
312 //a = qb.run();
313 qb.setOptions( QueryBuilder::optRemoveDuplicates );
314 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valGenreID );
315 //I can't get the correct value w/o using a subquery, and querybuilder doesn't support those
316 a = QStringList(QString::number( qb.run().count() ));
318 m_genreItem = new StatisticsItem( i18n("Favorite Genres"), this, m_albumItem );
319 m_genreItem->setSubtext( i18np("%1 genre", "%1 genres", a[0].toInt()) );
321 qb.clear();
322 qb.addReturnFunctionValue( QueryBuilder::funcMin, QueryBuilder::tabStats, QueryBuilder::valCreateDate );
323 qb.setOptions( QueryBuilder::optRemoveDuplicates );
324 a = qb.run();
325 QDateTime firstPlay = QDateTime::currentDateTime();
326 if ( a[0].toUInt() )
327 firstPlay.setTime_t( a[0].toUInt() );
329 m_newestItem = new StatisticsItem( i18n("Newest Items"), this, m_genreItem );
330 m_newestItem->setSubtext( i18n("First played %1", Amarok::verboseTimeSince( firstPlay ) ) );
332 m_trackItem ->setIcon( Amarok::icon("track") );
333 m_mostplayedItem->setIcon( Amarok::icon("mostplayed") );
334 m_artistItem ->setIcon( Amarok::icon("artist") );
335 m_albumItem ->setIcon( Amarok::icon("album") );
336 m_genreItem ->setIcon( Amarok::icon("favourite_genres") );
337 m_newestItem ->setIcon( Amarok::icon("clock") );
340 void
341 StatisticsList::itemClicked( Q3ListViewItem *item ) //SLOT
343 if( !item )
344 return;
346 if( item->depth() != 0 ) //not very flexible, *shrug*
347 return;
349 #define item static_cast<StatisticsItem*>(item)
351 if( item->isExpanded() )
353 renderView();
354 return;
357 expandInformation( item );
358 item->setOpen( true );
360 #undef item
363 void
364 StatisticsList::expandInformation( StatisticsItem *item, bool refresh )
366 m_expanded = true;
367 KLocale *locale = new KLocale( "locale" );
369 QueryBuilder qb;
371 StatisticsDetailedItem *m_last = 0;
372 uint c = 1;
374 if( item == m_trackItem )
376 if( !refresh ) {
377 delete m_newestItem;
378 delete m_genreItem;
379 delete m_albumItem;
380 delete m_artistItem;
381 delete m_mostplayedItem;
384 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valTitle );
385 qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valName );
386 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL );
387 qb.addReturnValue( QueryBuilder::tabStats, QueryBuilder::valScore );
388 qb.addReturnValue( QueryBuilder::tabStats, QueryBuilder::valRating );
389 qb.addNumericFilter( QueryBuilder::tabStats, QueryBuilder::valForFavoriteSorting(), "0", QueryBuilder::modeGreater );
390 qb.setGoogleFilter( QueryBuilder::tabSong | QueryBuilder::tabArtist, m_filter );
391 qb.sortByFavorite();
392 qb.setLimit( 0, 50 );
393 QStringList fave = qb.run();
395 for( int i=0; i < fave.count(); i += qb.countReturnValues() )
397 QString name = i18n("%1. %2 - %3", QString::number(c),
398 fave[i].isEmpty() ? i18n( "Unknown" ) : fave[i],
399 fave[i+1].isEmpty() ? i18n( "Unknown" ) : fave[i+1]);
400 QString score = locale->formatNumber( fave[i+3].toDouble(), 0 );
401 QString rating = locale->formatNumber( fave[i+4].toDouble() / 2.0, 1 );
402 m_last = new StatisticsDetailedItem( name, subText( score, rating ), item, m_last );
403 m_last->setItemType( StatisticsDetailedItem::TRACK );
404 m_last->setUrl( fave[i+2] );
405 c++;
409 else if( item == m_mostplayedItem )
411 if( !refresh ) {
412 delete m_newestItem;
413 delete m_genreItem;
414 delete m_albumItem;
415 delete m_artistItem;
416 delete m_trackItem;
419 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valTitle );
420 qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valName );
421 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL );
422 qb.addReturnValue( QueryBuilder::tabStats, QueryBuilder::valPlayCounter );
423 qb.addNumericFilter( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, "0", QueryBuilder::modeGreater );
424 qb.setGoogleFilter( QueryBuilder::tabSong | QueryBuilder::tabArtist, m_filter );
425 qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, true );
426 qb.setLimit( 0, 50 );
427 QStringList fave = qb.run();
429 for( int i=0; i < fave.count(); i += qb.countReturnValues() )
431 QString name = i18n("%1. %2 - %3", QString::number(c),
432 fave[i].isEmpty() ? i18n( "Unknown" ) : fave[i],
433 fave[i+1].isEmpty() ? i18n( "Unknown" ) : fave[i+1]);
434 double plays = fave[i+3].toDouble();
435 QString subtext = i18n("%1: %2", i18n( "Playcount" ), plays );
436 m_last = new StatisticsDetailedItem( name, subtext, item, m_last );
437 m_last->setItemType( StatisticsDetailedItem::TRACK );
438 m_last->setUrl( fave[i+2] );
439 c++;
443 else if( item == m_artistItem )
445 if( !refresh ) {
446 delete m_newestItem;
447 delete m_genreItem;
448 delete m_albumItem;
449 delete m_mostplayedItem;
450 delete m_trackItem;
453 qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valName );
454 qb.addReturnFunctionValue( QueryBuilder::funcAvg, QueryBuilder::tabStats, QueryBuilder::valScore );
455 qb.addReturnFunctionValue( QueryBuilder::funcAvg, QueryBuilder::tabStats, QueryBuilder::valRating );
456 qb.sortByFavoriteAvg();
457 // only artists with more than 3 tracks
458 qb.having( QueryBuilder::tabArtist, QueryBuilder::valID, QueryBuilder::funcCount, QueryBuilder::modeGreater, "3" );
459 qb.setGoogleFilter( QueryBuilder::tabArtist, m_filter );
460 qb.groupBy( QueryBuilder::tabArtist, QueryBuilder::valName);
461 qb.setLimit( 0, 50 );
462 QStringList fave = qb.run();
464 for( int i=0; i < fave.count(); i += qb.countReturnValues() )
466 QString name = i18n("%1. %2", QString::number(c), fave[i].isEmpty() ? i18n( "Unknown" ) : fave[i] );
467 QString score = locale->formatNumber( fave[i+1].toDouble(), 2 );
468 QString rating = locale->formatNumber( fave[i+2].toDouble() / 2.0, 2 );
469 m_last = new StatisticsDetailedItem( name, subText( score, rating ), item, m_last );
470 m_last->setItemType( StatisticsDetailedItem::ARTIST );
471 QString url = QString("%1").arg( fave[i] );
472 m_last->setUrl( url );
473 c++;
477 else if( item == m_albumItem )
479 if( !refresh ) {
480 delete m_newestItem;
481 delete m_genreItem;
482 delete m_artistItem;
483 delete m_mostplayedItem;
484 delete m_trackItem;
487 qb.addReturnValue( QueryBuilder::tabAlbum, QueryBuilder::valName );
488 qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valName );
489 qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valID );
490 qb.addReturnValue( QueryBuilder::tabAlbum, QueryBuilder::valID );
491 qb.addReturnFunctionValue( QueryBuilder::funcAvg, QueryBuilder::tabStats, QueryBuilder::valScore );
492 qb.addReturnFunctionValue( QueryBuilder::funcAvg, QueryBuilder::tabStats, QueryBuilder::valRating );
493 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valIsCompilation );
494 // only albums with more than 3 tracks
495 qb.having( QueryBuilder::tabAlbum, QueryBuilder::valID, QueryBuilder::funcCount, QueryBuilder::modeGreater, "3" );
496 qb.setGoogleFilter( QueryBuilder::tabAlbum | QueryBuilder::tabArtist, m_filter );
497 qb.sortByFavoriteAvg();
498 qb.excludeMatch( QueryBuilder::tabAlbum, i18n( "Unknown" ) );
499 qb.groupBy( QueryBuilder::tabAlbum, QueryBuilder::valID );
500 qb.groupBy( QueryBuilder::tabAlbum, QueryBuilder::valName );
501 qb.groupBy( QueryBuilder::tabArtist, QueryBuilder::valID );
502 qb.groupBy( QueryBuilder::tabArtist, QueryBuilder::valName );
503 qb.groupBy( QueryBuilder::tabSong, QueryBuilder::valIsCompilation );
505 qb.setLimit( 0, 50 );
506 QStringList fave = qb.run();
508 const QString trueValue = CollectionDB::instance()->boolT();
509 for( int i=0; i < fave.count(); i += qb.countReturnValues() )
511 const bool isSampler = (fave[i+6] == trueValue);
512 QString name = i18n("%1. %2 - %3", QString::number(c),
513 fave[i].isEmpty() ? i18n( "Unknown" ) : fave[i],
514 isSampler ? i18n( "Various Artists" ) :
515 ( fave[i+1].isEmpty() ? i18n( "Unknown" ) : fave[i+1] ) );
516 QString score = locale->formatNumber( fave[i+4].toDouble(), 2 );
517 QString rating = locale->formatNumber( fave[i+5].toDouble() / 2.0, 2 );
519 m_last = new StatisticsDetailedItem( name, subText( score, rating ), item, m_last );
520 m_last->setItemType( StatisticsDetailedItem::ALBUM );
521 QString url = QString("%1 @@@ %2").arg( isSampler ? "0" : fave[i+2], fave[i+3] );
522 m_last->setUrl( url );
523 c++;
527 else if( item == m_genreItem )
529 if( !refresh ) {
530 delete m_newestItem;
531 delete m_albumItem;
532 delete m_artistItem;
533 delete m_mostplayedItem;
534 delete m_trackItem;
537 qb.addReturnValue( QueryBuilder::tabGenre, QueryBuilder::valName );
538 qb.addReturnFunctionValue( QueryBuilder::funcAvg, QueryBuilder::tabStats, QueryBuilder::valScore );
539 qb.addReturnFunctionValue( QueryBuilder::funcAvg, QueryBuilder::tabStats, QueryBuilder::valRating );
540 // only genres with more than 3 tracks
541 qb.having( QueryBuilder::tabGenre, QueryBuilder::valID, QueryBuilder::funcCount, QueryBuilder::modeGreater, "3" );
542 // only genres which have been played/rated
543 qb.setGoogleFilter( QueryBuilder::tabGenre, m_filter );
544 qb.sortByFavoriteAvg();
545 qb.groupBy( QueryBuilder::tabGenre, QueryBuilder::valName);
546 qb.setLimit( 0, 50 );
547 QStringList fave = qb.run();
549 for( int i=0; i < fave.count(); i += qb.countReturnValues() )
551 QString name = i18n("%1. %2", QString::number(c),
552 fave[i].isEmpty() ? i18n( "Unknown" ) : fave[i] );
553 QString score = locale->formatNumber( fave[i+1].toDouble(), 2 );
554 QString rating = locale->formatNumber( fave[i+2].toDouble() / 2.0, 2 );
556 m_last = new StatisticsDetailedItem( name, subText( score, rating ), item, m_last );
557 m_last->setItemType( StatisticsDetailedItem::GENRE );
558 QString url = QString("%1").arg( fave[i] );
559 m_last->setUrl( url );
560 c++;
564 else if( item == m_newestItem )
566 if( !refresh ) {
567 delete m_genreItem;
568 delete m_albumItem;
569 delete m_artistItem;
570 delete m_mostplayedItem;
571 delete m_trackItem;
574 qb.addReturnValue( QueryBuilder::tabAlbum, QueryBuilder::valName );
575 qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valName );
576 qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valID );
577 qb.addReturnValue( QueryBuilder::tabAlbum, QueryBuilder::valID );
578 qb.addReturnFunctionValue( QueryBuilder::funcMax, QueryBuilder::tabSong, QueryBuilder::valCreateDate );
579 qb.sortByFunction( QueryBuilder::funcMax, QueryBuilder::tabSong, QueryBuilder::valCreateDate, true );
580 qb.excludeMatch( QueryBuilder::tabAlbum, i18n( "Unknown" ) );
581 qb.setGoogleFilter( QueryBuilder::tabAlbum | QueryBuilder::tabArtist, m_filter );
582 qb.groupBy( QueryBuilder::tabAlbum, QueryBuilder::valName);
583 qb.groupBy( QueryBuilder::tabAlbum, QueryBuilder::valID);
584 qb.groupBy( QueryBuilder::tabArtist, QueryBuilder::valName);
585 qb.groupBy( QueryBuilder::tabArtist, QueryBuilder::valID);
586 qb.setOptions( QueryBuilder::optNoCompilations ); // samplers __need__ to be handled differently
587 qb.setLimit( 0, 50 );
588 QStringList newest = qb.run();
590 for( int i=0; i < newest.count(); i += qb.countReturnValues() )
592 QString name = i18n("%1. %2 - %3", QString::number(c),
593 newest[i].isEmpty() ? i18n( "Unknown" ) : newest[i],
594 newest[i+1].isEmpty() ? i18n( "Unknown" ) : newest[i+1] );
595 QDateTime added = QDateTime();
596 added.setTime_t( newest[i+4].toUInt() );
597 QString subtext = i18n("Added: %1", Amarok::verboseTimeSince( added ) );
598 m_last = new StatisticsDetailedItem( name, subtext, item, m_last );
599 m_last->setItemType( StatisticsDetailedItem::HISTORY );
600 QString url = QString("%1 @@@ %2").arg( newest[i+2] ).arg( newest[i+3] );
601 m_last->setUrl( url );
602 c++;
606 item->setExpanded( true );
607 repaintItem( item ); // Better than ::repaint(), flickers less
608 delete locale;
611 QString StatisticsList::subText( const QString &score, const QString &rating ) //static
613 if( AmarokConfig::useScores() && AmarokConfig::useRatings() )
614 return i18n( "Score: %1 Rating: %2", score, rating );
615 else if( AmarokConfig::useScores() )
616 return i18n( "Score: %1", score );
617 else if( AmarokConfig::useRatings() )
618 return i18n( "Rating: %1", rating );
619 else
620 return QString();
623 void
624 StatisticsList::startHover( Q3ListViewItem *item ) //SLOT
626 if( m_currentItem && item != m_currentItem )
627 static_cast<StatisticsItem*>(m_currentItem)->leaveHover();
629 if( item->depth() != 0 )
631 m_currentItem = 0;
632 return;
635 static_cast<StatisticsItem*>(item)->enterHover();
636 m_currentItem = item;
639 void
640 StatisticsList::clearHover() //SLOT
642 if( m_currentItem )
643 static_cast<StatisticsItem*>(m_currentItem)->leaveHover();
645 m_currentItem = 0;
648 void
649 StatisticsList::viewportPaintEvent( QPaintEvent *e )
651 if( e ) K3ListView::viewportPaintEvent( e );
653 if( CollectionDB::instance()->isEmpty() && e )
655 QPainter p( viewport() );
656 QString minimumText(i18n(
657 "<div align=center>"
658 "<h3>Statistics</h3>"
659 "You need a collection to use statistics! "
660 "Create a collection and then start playing "
661 "tracks to accumulate data on your play habits!"
662 "</div>" ) );
663 Q3SimpleRichText t( minimumText, QApplication::font() );
665 if ( t.width()+30 >= viewport()->width() || t.height()+30 >= viewport()->height() )
666 //too big, giving up
667 return;
669 const uint w = t.width();
670 const uint h = t.height();
671 const uint x = (viewport()->width() - w - 30) / 2 ;
672 const uint y = (viewport()->height() - h - 30) / 2 ;
674 p.setBrush( palette().background() );
675 p.drawRoundRect( x, y, w+30, h+30, (8*200)/w, (8*200)/h );
676 t.draw( &p, x+15, y+15, QRect(), palette() );
680 void
681 StatisticsList::showContextMenu( Q3ListViewItem *item, const QPoint &p, int ) //SLOT
683 if( !item || item->rtti() == StatisticsItem::RTTI ) return;
685 #define item static_cast<StatisticsDetailedItem*>(item)
687 bool hasSQL = !( item->itemType() == StatisticsDetailedItem::TRACK ); //track is url
689 Q3PopupMenu menu( this );
690 enum Actions { APPEND, QUEUE, INFO };
692 menu.insertItem( KIcon( Amarok::icon( "add_playlist" ) ), i18n( "&Append to Playlist" ), APPEND );
693 menu.insertItem( KIcon( Amarok::icon( "queue_track" ) ), i18n( "&Queue Track" ), QUEUE );
695 menu.addSeparator();
697 menu.insertItem( KIcon( Amarok::icon( "info" ) ), i18nc( "[only-singular]", "Edit Track &Information..." ), INFO );
700 //PORT 2.0
701 // switch( menu.exec( p ) )
702 // {
703 // case APPEND:
704 // hasSQL ?
705 // Playlist::instance()->insertMediaSql( item->getSQL() ):
706 // The::playlistModel()->insertMedia( KUrl( item->url() ) );
707 // break;
709 // case QUEUE:
710 // hasSQL ?
711 // Playlist::instance()->insertMediaSql( item->getSQL(), Playlist::Queue ):
712 // The::playlistModel()->insertMedia( KUrl( item->url() ), Playlist::Queue );
713 // break;
715 // case INFO:
716 // if( hasSQL )
717 // {
718 // TagDialog* dialog = new TagDialog( item->getURLs(), Statistics::instance() );
719 // dialog->show();
720 // }
721 // else
722 // {
723 // TagDialog* dialog = new TagDialog( KUrl( item->url() ), Statistics::instance() );
724 // dialog->show();
725 // }
726 // }
727 #undef item
730 //////////////////////////////////////////////////////////////////////////////////////////
731 /// CLASS StatisticsItem
732 //////////////////////////////////////////////////////////////////////////////////////////
734 StatisticsItem::StatisticsItem( QString text, StatisticsList *parent, K3ListViewItem *after, const char *name )
735 : K3ListViewItem( static_cast<K3ListView*>(parent), after, name )
736 , m_animTimer( new QTimer( this ) )
737 , m_animCount( 0 )
738 , m_isActive( false )
739 , m_isExpanded( false )
741 setDragEnabled( false );
742 setDropEnabled( false );
743 setSelectable( false );
745 setText( 0, text );
747 connect( m_animTimer, SIGNAL( timeout() ), this, SLOT( slotAnimTimer() ) );
750 void
751 StatisticsItem::setIcon( const QString &icon )
753 QString path = KIconLoader::global()->iconPath( icon, -KIconLoader::SizeHuge );
754 path.replace( "32x32", "48x48" ); //HACK fucking KIconLoader only returns 32x32 max. Why?
756 // debug() << "ICONPATH: " << path;
758 setPixmap( 0, path );
761 void
762 StatisticsItem::enterHover()
764 m_animEnter = true;
765 m_animCount = 0;
766 m_isActive = true;
767 m_animTimer->start( ANIM_INTERVAL );
770 void
771 StatisticsItem::leaveHover()
773 // This can happen if you enter and leave the tab quickly
774 if( m_animCount == 0 )
775 m_animCount = 1;
777 m_animEnter = false;
778 m_isActive = true;
779 m_animTimer->start( ANIM_INTERVAL );
782 void
783 StatisticsItem::slotAnimTimer()
785 if( m_animEnter )
787 m_animCount += 1;
788 listView()->repaintItem( this ); // Better than ::repaint(), flickers less
790 if( m_animCount >= ANIM_MAX )
791 m_animTimer->stop();
793 else
795 m_animCount -= 1;
796 listView()->repaintItem( this );
797 if( m_animCount <= 0 )
799 m_animTimer->stop();
800 m_isActive = false;
805 void
806 StatisticsItem::paintCell( QPainter *p, const QColorGroup &cg, int column, int width, int )
808 QColor fillColor, textColor;
810 if( m_isActive ) //glowing animation
812 fillColor = blendColors( cg.background(), cg.highlight(), static_cast<int>( m_animCount * 3.5 ) );
813 textColor = blendColors( cg.text(), cg.highlightedText(), static_cast<int>( m_animCount * 4.5 ) );
815 else //alternate colours
817 fillColor = isSelected() ? cg.highlight() : backgroundColor(0);
818 textColor = isSelected() ? cg.highlightedText() : cg.text();
821 p->fillRect( 0, 0, width, height(), fillColor );
823 K3ListView *lv = static_cast<K3ListView *>( listView() );
825 QFont font( p->font() );
826 font.setBold( true );
827 QFontMetrics fm( p->fontMetrics() );
829 int textHeight = height();
830 int text_x = 0;
832 p->setPen( textColor );
834 if( pixmap( column ) )
836 int y = (textHeight - pixmap(column)->height())/2;
837 p->drawPixmap( 0, y, *pixmap(column) );
838 text_x += pixmap(column)->width() + 4;
841 p->setFont( font );
842 QFontMetrics fmName( font );
844 QString name = text(column);
845 // we need to take into account that the item is indented, hence the itemMargin.
846 const int _width = width - text_x - lv->itemMargin()*2;
847 name = fmName.elidedText( name, Qt::ElideRight, _width );
849 p->drawText( text_x, 0, width, textHeight, Qt::AlignVCenter, name );
851 if( !m_subText.isEmpty() )
853 font.setBold( false );
854 p->setFont( font );
856 p->drawText( text_x, fmName.height() + 1, width, textHeight, Qt::AlignVCenter, m_subText );
859 if( m_isExpanded )
861 QPen pen( cg.highlight(), 1 );
862 p->setPen( pen );
863 int y = textHeight - 1;
864 p->drawLine( 0, y, width, y );
868 QColor
869 StatisticsItem::blendColors( const QColor& color1, const QColor& color2, int percent )
871 const float factor1 = ( 100 - ( float ) percent ) / 100;
872 const float factor2 = ( float ) percent / 100;
874 const int r = static_cast<int>( color1.red() * factor1 + color2.red() * factor2 );
875 const int g = static_cast<int>( color1.green() * factor1 + color2.green() * factor2 );
876 const int b = static_cast<int>( color1.blue() * factor1 + color2.blue() * factor2 );
878 QColor result;
879 result.setRgb( r, g, b );
881 return result;
884 //////////////////////////////////////////////////////////////////////////////////////////
885 /// CLASS StatisticsDetailedItem
886 //////////////////////////////////////////////////////////////////////////////////////////
888 StatisticsDetailedItem::StatisticsDetailedItem( const QString &text, const QString &subtext, StatisticsItem *parent,
889 StatisticsDetailedItem *after, const char *name )
890 : K3ListViewItem( parent, after, name )
891 , m_type( NONE )
892 , m_subText( subtext )
894 setDragEnabled( true );
895 setDropEnabled( false );
896 setSelectable( true );
898 setText( 0, text );
901 void
902 StatisticsDetailedItem::paintCell( QPainter *p, const QColorGroup &cg, int column, int width, int )
904 bool showDetails = !m_subText.isEmpty();
906 // use alternate background
907 p->fillRect( 0, 0, width, height(), isSelected() ? cg.highlight() : backgroundColor() );
909 K3ListView *lv = static_cast<K3ListView *>( listView() );
911 QFont font( p->font() );
912 QFontMetrics fm( p->fontMetrics() );
914 int text_x = 0;
915 int textHeight;
917 if( showDetails )
918 textHeight = fm.lineSpacing() + lv->itemMargin() + 1;
919 else
920 textHeight = height();
922 p->setPen( isSelected() ? cg.highlightedText() : cg.text() );
924 if( pixmap( column ) )
926 int y = (textHeight - pixmap(column)->height())/2;
927 if( showDetails ) y++;
928 p->drawPixmap( text_x, y, *pixmap(column) );
929 text_x += pixmap(column)->width() + 4;
932 p->setFont( font );
933 QFontMetrics fmName( font );
935 QString name = text(column);
936 const int _width = width - text_x - lv->itemMargin()*2;
937 name = fmName.elidedText( name, Qt::ElideRight, _width );
939 p->drawText( text_x, 0, width, textHeight, Qt::AlignVCenter, name );
941 if( showDetails )
943 const QColorGroup _cg = listView()->palette().disabled();
944 text_x = lv->treeStepSize() + 3;
945 font.setItalic( true );
946 p->setPen( isSelected() ? _cg.highlightedText() : _cg.text().dark() );
947 p->drawText( text_x, textHeight, width, fm.lineSpacing(), Qt::AlignVCenter, m_subText );
951 void
952 StatisticsDetailedItem::setup()
954 QFontMetrics fm( listView()->font() );
955 int margin = listView()->itemMargin()*2;
956 int h = fm.lineSpacing();
957 if ( h % 2 > 0 )
958 h++;
959 if( !m_subText.isEmpty() )
960 setHeight( h + fm.lineSpacing() + margin );
961 else
962 setHeight( h + margin );
965 QString
966 StatisticsDetailedItem::getSQL()
968 QueryBuilder qb;
969 QString query = QString();
970 QString artist, album, track; // track is unused here
971 Amarok::albumArtistTrackFromUrl( url(), artist, album, track );
973 if( itemType() == StatisticsDetailedItem::ALBUM || itemType() == StatisticsDetailedItem::HISTORY )
975 qb.initSQLDrag();
976 if ( artist != "0" )
977 qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valArtistID, artist );
978 qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valAlbumID, album );
979 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
982 else if( itemType() == StatisticsDetailedItem::ARTIST )
984 const uint artist_id = CollectionDB::instance()->artistID( url() );
986 qb.initSQLDrag();
987 qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valArtistID, QString::number( artist_id ) );
988 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
991 else if( itemType() == StatisticsDetailedItem::GENRE )
993 const uint genre_id = CollectionDB::instance()->genreID( url() );
995 qb.initSQLDrag();
996 qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valGenreID, QString::number( genre_id ) );
997 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
999 debug() << "DetailedStatisticsItem: query is: " << qb.query();
1001 return qb.query();
1004 KUrl::List
1005 StatisticsDetailedItem::getURLs()
1007 if( itemType() == StatisticsDetailedItem::TRACK )
1008 return KUrl::List( KUrl(url()) );
1010 QueryBuilder qb;
1011 QString query = QString();
1012 QString artist, album, track; // track is unused here
1013 Amarok::albumArtistTrackFromUrl( m_url, artist, album, track );
1015 qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL );
1017 if( itemType() == StatisticsDetailedItem::ALBUM || itemType() == StatisticsDetailedItem::HISTORY )
1019 if ( artist != "0" )
1020 qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valArtistID, artist );
1021 qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valAlbumID, album );
1022 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
1025 else if( itemType() == StatisticsDetailedItem::ARTIST )
1027 const uint artist_id = CollectionDB::instance()->artistID( url() );
1028 qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valArtistID, QString::number( artist_id ) );
1029 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
1032 else if( itemType() == StatisticsDetailedItem::GENRE )
1034 const uint genre_id = CollectionDB::instance()->genreID( url() );
1035 qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valGenreID, QString::number( genre_id ) );
1036 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
1039 QStringList values = qb.run();
1040 KUrl::List urls;
1041 oldForeach( values )
1042 urls += KUrl( *it );
1043 return urls;
1046 #include "Statistics.moc"