Transmission: update to 2.76
[tomato.git] / release / src / router / transmission / qt / filterbar.cc
blob20f20fef716b630d2070dcfbac0ab1cea563510e
1 /*
2 * This file Copyright (C) Mnemosyne LLC
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2
6 * as published by the Free Software Foundation.
8 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
10 * $Id: filterbar.cc 13746 2013-01-03 23:50:42Z jordan $
13 #include <QString>
14 #include <QtGui>
16 #include "app.h"
17 #include "favicon.h"
18 #include "filters.h"
19 #include "filterbar.h"
20 #include "hig.h"
21 #include "prefs.h"
22 #include "torrent-filter.h"
23 #include "torrent-model.h"
24 #include "utils.h"
26 /****
27 *****
28 ***** DELEGATE
29 *****
30 ****/
32 enum
34 TorrentCountRole = Qt::UserRole + 1,
35 ActivityRole,
36 TrackerRole
39 namespace
41 int getHSpacing( QWidget * w )
43 return qMax( int(HIG::PAD_SMALL), w->style()->pixelMetric( QStyle::PM_LayoutHorizontalSpacing, 0, w ) );
47 FilterBarComboBoxDelegate :: FilterBarComboBoxDelegate( QObject * parent, QComboBox * combo ):
48 QItemDelegate( parent ),
49 myCombo( combo )
53 bool
54 FilterBarComboBoxDelegate :: isSeparator( const QModelIndex &index )
56 return index.data(Qt::AccessibleDescriptionRole).toString() == QLatin1String("separator");
58 void
59 FilterBarComboBoxDelegate :: setSeparator( QAbstractItemModel * model, const QModelIndex& index )
61 model->setData( index, QString::fromLatin1("separator"), Qt::AccessibleDescriptionRole );
63 if( QStandardItemModel *m = qobject_cast<QStandardItemModel*>(model) )
64 if (QStandardItem *item = m->itemFromIndex(index))
65 item->setFlags(item->flags() & ~(Qt::ItemIsSelectable|Qt::ItemIsEnabled));
68 void
69 FilterBarComboBoxDelegate :: paint( QPainter * painter,
70 const QStyleOptionViewItem & option,
71 const QModelIndex & index ) const
73 if( isSeparator( index ) )
75 QRect rect = option.rect;
76 if (const QStyleOptionViewItemV3 *v3 = qstyleoption_cast<const QStyleOptionViewItemV3*>(&option))
77 if (const QAbstractItemView *view = qobject_cast<const QAbstractItemView*>(v3->widget))
78 rect.setWidth(view->viewport()->width());
79 QStyleOption opt;
80 opt.rect = rect;
81 myCombo->style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, painter, myCombo);
83 else
85 QStyleOptionViewItem disabledOption = option;
86 disabledOption.state &= ~( QStyle::State_Enabled | QStyle::State_Selected );
87 QRect boundingBox = option.rect;
89 const int hmargin = getHSpacing( myCombo );
90 boundingBox.setLeft( boundingBox.left() + hmargin );
91 boundingBox.setRight( boundingBox.right() - hmargin );
93 QRect decorationRect = rect( option, index, Qt::DecorationRole );
94 decorationRect.moveLeft( decorationRect.left( ) );
95 decorationRect.setSize( myCombo->iconSize( ) );
96 decorationRect = QStyle::alignedRect( Qt::LeftToRight,
97 Qt::AlignLeft|Qt::AlignVCenter,
98 decorationRect.size(), boundingBox );
99 boundingBox.setLeft( decorationRect.right() + hmargin );
101 QRect countRect = rect( option, index, TorrentCountRole );
102 countRect = QStyle::alignedRect( Qt::LeftToRight,
103 Qt::AlignRight|Qt::AlignVCenter,
104 countRect.size(), boundingBox );
105 boundingBox.setRight( countRect.left() - hmargin );
106 const QRect displayRect = boundingBox;
108 drawBackground( painter, option, index );
109 QStyleOptionViewItem option2 = option;
110 option2.decorationSize = myCombo->iconSize( );
111 drawDecoration( painter, option, decorationRect, decoration(option2,index.data(Qt::DecorationRole)) );
112 drawDisplay( painter, option, displayRect, index.data(Qt::DisplayRole).toString() );
113 drawDisplay( painter, disabledOption, countRect, index.data(TorrentCountRole).toString() );
114 drawFocus( painter, option, displayRect|countRect );
118 QSize
119 FilterBarComboBoxDelegate :: sizeHint( const QStyleOptionViewItem & option,
120 const QModelIndex & index ) const
122 if( isSeparator( index ) )
124 const int pm = myCombo->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, 0, myCombo);
125 return QSize( pm, pm + 10 );
127 else
129 QStyle * s = myCombo->style( );
130 const int hmargin = getHSpacing( myCombo );
132 QSize size = QItemDelegate::sizeHint( option, index );
133 size.setHeight( qMax( size.height(), myCombo->iconSize().height() + 6 ) );
134 size.rwidth() += s->pixelMetric( QStyle::PM_FocusFrameHMargin, 0, myCombo );
135 size.rwidth() += rect(option,index,TorrentCountRole).width();
136 size.rwidth() += hmargin * 4;
137 return size;
145 FilterBarComboBox :: FilterBarComboBox( QWidget * parent ):
146 QComboBox( parent )
150 void
151 FilterBarComboBox :: paintEvent( QPaintEvent * e )
153 Q_UNUSED( e );
155 QStylePainter painter(this);
156 painter.setPen(palette().color(QPalette::Text));
158 // draw the combobox frame, focusrect and selected etc.
159 QStyleOptionComboBox opt;
160 initStyleOption(&opt);
161 painter.drawComplexControl(QStyle::CC_ComboBox, opt);
163 // draw the icon and text
164 const QModelIndex modelIndex = model()->index( currentIndex(), 0, rootModelIndex() );
165 if( modelIndex.isValid( ) )
167 QStyle * s = style();
168 QRect rect = s->subControlRect( QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField, this );
169 const int hmargin = getHSpacing( this );
170 rect.setRight( rect.right() - hmargin );
172 // draw the icon
173 QPixmap pixmap;
174 QVariant variant = modelIndex.data( Qt::DecorationRole );
175 switch( variant.type( ) ) {
176 case QVariant::Pixmap: pixmap = qvariant_cast<QPixmap>(variant); break;
177 case QVariant::Icon: pixmap = qvariant_cast<QIcon>(variant).pixmap(iconSize()); break;
178 default: break;
180 if( !pixmap.isNull() ) {
181 s->drawItemPixmap( &painter, rect, Qt::AlignLeft|Qt::AlignVCenter, pixmap );
182 rect.setLeft( rect.left() + pixmap.width() + hmargin );
185 // draw the count
186 QString text = modelIndex.data(TorrentCountRole).toString();
187 if( !text.isEmpty( ) )
189 const QPen pen = painter.pen( );
190 painter.setPen( opt.palette.color( QPalette::Disabled, QPalette::Text ) );
191 QRect r = s->itemTextRect( painter.fontMetrics(), rect, Qt::AlignRight|Qt::AlignVCenter, false, text );
192 painter.drawText( r, 0, text );
193 rect.setRight( r.left() - hmargin );
194 painter.setPen( pen );
197 // draw the text
198 text = modelIndex.data( Qt::DisplayRole ).toString();
199 text = painter.fontMetrics().elidedText ( text, Qt::ElideRight, rect.width() );
200 s->drawItemText( &painter, rect, Qt::AlignLeft|Qt::AlignVCenter, opt.palette, true, text );
204 /****
205 *****
206 ***** ACTIVITY
207 *****
208 ****/
210 QComboBox*
211 FilterBar :: createActivityCombo( )
213 QComboBox * c = new FilterBarComboBox( this );
214 FilterBarComboBoxDelegate * delegate = new FilterBarComboBoxDelegate( 0, c );
215 c->setItemDelegate( delegate );
217 QPixmap blankPixmap( c->iconSize( ) );
218 blankPixmap.fill( Qt::transparent );
219 QIcon blankIcon( blankPixmap );
221 QStandardItemModel * model = new QStandardItemModel;
223 QStandardItem * row = new QStandardItem( tr( "All" ) );
224 row->setData( FilterMode::SHOW_ALL, ActivityRole );
225 model->appendRow( row );
227 model->appendRow( new QStandardItem ); // separator
228 delegate->setSeparator( model, model->index( 1, 0 ) );
230 row = new QStandardItem( QIcon::fromTheme( "system-run", blankIcon ), tr( "Active" ) );
231 row->setData( FilterMode::SHOW_ACTIVE, ActivityRole );
232 model->appendRow( row );
234 row = new QStandardItem( QIcon::fromTheme( "go-down", blankIcon ), tr( "Downloading" ) );
235 row->setData( FilterMode::SHOW_DOWNLOADING, ActivityRole );
236 model->appendRow( row );
238 row = new QStandardItem( QIcon::fromTheme( "go-up", blankIcon ), tr( "Seeding" ) );
239 row->setData( FilterMode::SHOW_SEEDING, ActivityRole );
240 model->appendRow( row );
242 row = new QStandardItem( QIcon::fromTheme( "media-playback-pause", blankIcon ), tr( "Paused" ) );
243 row->setData( FilterMode::SHOW_PAUSED, ActivityRole );
244 model->appendRow( row );
246 row = new QStandardItem( blankIcon, tr( "Finished" ) );
247 row->setData( FilterMode::SHOW_FINISHED, ActivityRole );
248 model->appendRow( row );
250 row = new QStandardItem( QIcon::fromTheme( "view-refresh", blankIcon ), tr( "Verifying" ) );
251 row->setData( FilterMode::SHOW_VERIFYING, ActivityRole );
252 model->appendRow( row );
254 row = new QStandardItem( QIcon::fromTheme( "dialog-error", blankIcon ), tr( "Error" ) );
255 row->setData( FilterMode::SHOW_ERROR, ActivityRole );
256 model->appendRow( row );
258 c->setModel( model );
259 return c;
262 /****
263 *****
264 *****
265 *****
266 ****/
268 namespace
270 QString readableHostName( const QString host )
272 // get the readable name...
273 QString name = host;
274 const int pos = name.lastIndexOf( '.' );
275 if( pos >= 0 )
276 name.truncate( pos );
277 if( !name.isEmpty( ) )
278 name[0] = name[0].toUpper( );
279 return name;
283 void
284 FilterBar :: refreshTrackers( )
286 Favicons& favicons = dynamic_cast<MyApp*>(QApplication::instance())->favicons;
287 const int firstTrackerRow = 2; // skip over the "All" and separator...
289 // pull info from the tracker model...
290 QSet<QString> oldHosts;
291 for( int row=firstTrackerRow; ; ++row ) {
292 QModelIndex index = myTrackerModel->index( row, 0 );
293 if( !index.isValid( ) )
294 break;
295 oldHosts << index.data(TrackerRole).toString();
298 // pull the new stats from the torrent model...
299 QSet<QString> newHosts;
300 QMap<QString,int> torrentsPerHost;
301 for( int row=0; ; ++row )
303 QModelIndex index = myTorrents.index( row, 0 );
304 if( !index.isValid( ) )
305 break;
306 const Torrent * tor = index.data( TorrentModel::TorrentRole ).value<const Torrent*>();
307 const QStringList trackers = tor->trackers( );
308 QSet<QString> torrentNames;
309 foreach( QString tracker, trackers ) {
310 const QString host = Favicons::getHost( QUrl( tracker ) );
311 if( host.isEmpty( ) )
312 qWarning() << "torrent" << qPrintable(tor->name()) << "has an invalid announce URL:" << tracker;
313 else {
314 newHosts.insert( host );
315 torrentNames.insert( readableHostName( host ) );
318 foreach( QString name, torrentNames )
319 ++torrentsPerHost[ name ];
322 // update the "All" row
323 myTrackerModel->setData( myTrackerModel->index(0,0), getCountString(myTorrents.rowCount()), TorrentCountRole );
325 // rows to update
326 foreach( QString host, oldHosts & newHosts )
328 const QString name = readableHostName( host );
329 QStandardItem * row = myTrackerModel->findItems(name).front();
330 row->setData( getCountString(torrentsPerHost[name]), TorrentCountRole );
331 row->setData( favicons.findFromHost(host), Qt::DecorationRole );
334 // rows to remove
335 foreach( QString host, oldHosts - newHosts ) {
336 const QString name = readableHostName( host );
337 QStandardItem * item = myTrackerModel->findItems(name).front();
338 if( !item->data(TrackerRole).toString().isEmpty() ) // don't remove "All"
339 myTrackerModel->removeRows( item->row(), 1 );
342 // rows to add
343 bool anyAdded = false;
344 foreach( QString host, newHosts - oldHosts )
346 const QString name = readableHostName( host );
348 if( !myTrackerModel->findItems(name).isEmpty() )
349 continue;
351 // find the sorted position to add this row
352 int i = firstTrackerRow;
353 for( int n=myTrackerModel->rowCount(); i<n; ++i ) {
354 const QString rowName = myTrackerModel->index(i,0).data(Qt::DisplayRole).toString();
355 if( rowName >= name )
356 break;
359 // add the row
360 QStandardItem * row = new QStandardItem( favicons.findFromHost( host ), name );
361 row->setData( getCountString(torrentsPerHost[host]), TorrentCountRole );
362 row->setData( favicons.findFromHost(host), Qt::DecorationRole );
363 row->setData( host, TrackerRole );
364 myTrackerModel->insertRow( i, row );
365 anyAdded = true;
368 if( anyAdded ) // the one added might match our filter...
369 refreshPref( Prefs::FILTER_TRACKERS );
373 QComboBox*
374 FilterBar :: createTrackerCombo( QStandardItemModel * model )
376 QComboBox * c = new FilterBarComboBox( this );
377 FilterBarComboBoxDelegate * delegate = new FilterBarComboBoxDelegate( 0, c );
378 c->setItemDelegate( delegate );
380 QStandardItem * row = new QStandardItem( tr( "All" ) );
381 row->setData( "", TrackerRole );
382 row->setData( getCountString(myTorrents.rowCount()), TorrentCountRole );
383 model->appendRow( row );
385 model->appendRow( new QStandardItem ); // separator
386 delegate->setSeparator( model, model->index( 1, 0 ) );
388 c->setModel( model );
389 return c;
392 /****
393 *****
394 *****
395 *****
396 ****/
398 FilterBar :: FilterBar( Prefs& prefs, TorrentModel& torrents, TorrentFilter& filter, QWidget * parent ):
399 QWidget( parent ),
400 myPrefs( prefs ),
401 myTorrents( torrents ),
402 myFilter( filter ),
403 myRecountTimer( new QTimer( this ) ),
404 myIsBootstrapping( true )
406 QHBoxLayout * h = new QHBoxLayout( this );
407 const int hmargin = qMax( int(HIG::PAD), style()->pixelMetric( QStyle::PM_LayoutHorizontalSpacing ) );
409 h->setSpacing( 0 );
410 h->setContentsMargins( 2, 2, 2, 2 );
411 h->addWidget( new QLabel( tr( "Show:" ), this ) );
412 h->addSpacing( hmargin );
414 myActivityCombo = createActivityCombo( );
415 h->addWidget( myActivityCombo, 1 );
416 h->addSpacing( hmargin );
418 myTrackerModel = new QStandardItemModel;
419 myTrackerCombo = createTrackerCombo( myTrackerModel );
420 h->addWidget( myTrackerCombo, 1 );
421 h->addSpacing( hmargin*2 );
423 myLineEdit = new QLineEdit( this );
424 h->addWidget( myLineEdit );
425 connect( myLineEdit, SIGNAL(textChanged(QString)), this, SLOT(onTextChanged(QString)));
427 QPushButton * p = new QPushButton;
428 QIcon icon = QIcon::fromTheme( "edit-clear", style()->standardIcon( QStyle::SP_DialogCloseButton ) );
429 int iconSize = style()->pixelMetric( QStyle::PM_SmallIconSize );
430 p->setIconSize( QSize( iconSize, iconSize ) );
431 p->setIcon( icon );
432 p->setFlat( true );
433 h->addWidget( p );
434 connect( p, SIGNAL(clicked(bool)), myLineEdit, SLOT(clear()));
436 // listen for changes from the other players
437 connect( &myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(int)));
438 connect( myActivityCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onActivityIndexChanged(int)));
439 connect( myTrackerCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onTrackerIndexChanged(int)));
440 connect( &myTorrents, SIGNAL(modelReset()), this, SLOT(onTorrentModelReset()));
441 connect( &myTorrents, SIGNAL(rowsInserted(const QModelIndex&,int,int)), this, SLOT(onTorrentModelRowsInserted(const QModelIndex&,int,int)));
442 connect( &myTorrents, SIGNAL(rowsRemoved(const QModelIndex&,int,int)), this, SLOT(onTorrentModelRowsRemoved(const QModelIndex&,int,int)));
443 connect( &myTorrents, SIGNAL(dataChanged(const QModelIndex&,const QModelIndex&)), this, SLOT(onTorrentModelDataChanged(const QModelIndex&,const QModelIndex&)));
444 connect( myRecountTimer, SIGNAL(timeout()), this, SLOT(recount()) );
446 recountSoon( );
447 refreshTrackers( );
448 myIsBootstrapping = false;
450 // initialize our state
451 QList<int> initKeys;
452 initKeys << Prefs :: FILTER_MODE
453 << Prefs :: FILTER_TRACKERS;
454 foreach( int key, initKeys )
455 refreshPref( key );
458 FilterBar :: ~FilterBar( )
460 delete myRecountTimer;
463 /***
464 ****
465 ***/
467 void
468 FilterBar :: refreshPref( int key )
470 switch( key )
472 case Prefs :: FILTER_MODE: {
473 const FilterMode m = myPrefs.get<FilterMode>( key );
474 QAbstractItemModel * model = myActivityCombo->model( );
475 QModelIndexList indices = model->match( model->index(0,0), ActivityRole, m.mode(), -1 );
476 myActivityCombo->setCurrentIndex( indices.isEmpty() ? 0 : indices.first().row( ) );
477 break;
480 case Prefs :: FILTER_TRACKERS: {
481 const QString tracker = myPrefs.getString( key );
482 const QString name = readableHostName( tracker );
483 QList<QStandardItem*> rows = myTrackerModel->findItems(name);
484 if( !rows.isEmpty() )
485 myTrackerCombo->setCurrentIndex( rows.front()->row() );
486 else { // hm, we don't seem to have this tracker anymore...
487 const bool isBootstrapping = myTrackerModel->rowCount( ) <= 2;
488 if( !isBootstrapping )
489 myPrefs.set( key, "" );
491 break;
496 void
497 FilterBar :: onTextChanged( const QString& str )
499 if( !myIsBootstrapping )
500 myPrefs.set( Prefs::FILTER_TEXT, str.trimmed( ) );
503 void
504 FilterBar :: onTrackerIndexChanged( int i )
506 if( !myIsBootstrapping )
508 QString str;
509 const bool isTracker = !myTrackerCombo->itemData(i,TrackerRole).toString().isEmpty();
510 if( !isTracker ) // show all
511 str = "";
512 else {
513 str = myTrackerCombo->itemData(i,TrackerRole).toString();
514 const int pos = str.lastIndexOf( '.' );
515 if( pos >= 0 )
516 str.truncate( pos+1 );
518 myPrefs.set( Prefs::FILTER_TRACKERS, str );
522 void
523 FilterBar :: onActivityIndexChanged( int i )
525 if( !myIsBootstrapping )
527 const FilterMode mode = myActivityCombo->itemData( i, ActivityRole ).toInt( );
528 myPrefs.set( Prefs::FILTER_MODE, mode );
532 /***
533 ****
534 ***/
536 void FilterBar :: onTorrentModelReset( ) { recountSoon( ); }
537 void FilterBar :: onTorrentModelRowsInserted( const QModelIndex&, int, int ) { recountSoon( ); }
538 void FilterBar :: onTorrentModelRowsRemoved( const QModelIndex&, int, int ) { recountSoon( ); }
539 void FilterBar :: onTorrentModelDataChanged( const QModelIndex&, const QModelIndex& ) { recountSoon( ); }
541 void
542 FilterBar :: recountSoon( )
544 if( !myRecountTimer->isActive( ) )
546 myRecountTimer->setSingleShot( true );
547 myRecountTimer->start( 500 );
550 void
551 FilterBar :: recount ( )
553 // recount the activity combobox...
554 for( int i=0, n=FilterMode::NUM_MODES; i<n; ++i )
556 const FilterMode m( i );
557 QAbstractItemModel * model = myActivityCombo->model( );
558 QModelIndexList indices = model->match( model->index(0,0), ActivityRole, m.mode(), -1 );
559 if( !indices.isEmpty( ) )
560 model->setData( indices.first(), getCountString(myFilter.count(m)), TorrentCountRole );
563 refreshTrackers( );
566 QString
567 FilterBar :: getCountString( int n ) const
569 return QString("%L1").arg(n);