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 $
19 #include "filterbar.h"
22 #include "torrent-filter.h"
23 #include "torrent-model.h"
34 TorrentCountRole
= Qt::UserRole
+ 1,
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
),
54 FilterBarComboBoxDelegate :: isSeparator( const QModelIndex
&index
)
56 return index
.data(Qt::AccessibleDescriptionRole
).toString() == QLatin1String("separator");
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
));
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());
81 myCombo
->style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator
, &opt
, painter
, myCombo
);
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
);
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 );
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;
145 FilterBarComboBox :: FilterBarComboBox( QWidget
* parent
):
151 FilterBarComboBox :: paintEvent( QPaintEvent
* 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
);
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;
180 if( !pixmap
.isNull() ) {
181 s
->drawItemPixmap( &painter
, rect
, Qt::AlignLeft
|Qt::AlignVCenter
, pixmap
);
182 rect
.setLeft( rect
.left() + pixmap
.width() + hmargin
);
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
);
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
);
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
);
270 QString
readableHostName( const QString host
)
272 // get the readable name...
274 const int pos
= name
.lastIndexOf( '.' );
276 name
.truncate( pos
);
277 if( !name
.isEmpty( ) )
278 name
[0] = name
[0].toUpper( );
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( ) )
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( ) )
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
;
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
);
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
);
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 );
343 bool anyAdded
= false;
344 foreach( QString host
, newHosts
- oldHosts
)
346 const QString name
= readableHostName( host
);
348 if( !myTrackerModel
->findItems(name
).isEmpty() )
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
)
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
);
368 if( anyAdded
) // the one added might match our filter...
369 refreshPref( Prefs::FILTER_TRACKERS
);
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
);
398 FilterBar :: FilterBar( Prefs
& prefs
, TorrentModel
& torrents
, TorrentFilter
& filter
, QWidget
* parent
):
401 myTorrents( torrents
),
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
) );
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
) );
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()) );
448 myIsBootstrapping
= false;
450 // initialize our state
452 initKeys
<< Prefs :: FILTER_MODE
453 << Prefs :: FILTER_TRACKERS
;
454 foreach( int key
, initKeys
)
458 FilterBar :: ~FilterBar( )
460 delete myRecountTimer
;
468 FilterBar :: refreshPref( int 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( ) );
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
, "" );
497 FilterBar :: onTextChanged( const QString
& str
)
499 if( !myIsBootstrapping
)
500 myPrefs
.set( Prefs::FILTER_TEXT
, str
.trimmed( ) );
504 FilterBar :: onTrackerIndexChanged( int i
)
506 if( !myIsBootstrapping
)
509 const bool isTracker
= !myTrackerCombo
->itemData(i
,TrackerRole
).toString().isEmpty();
510 if( !isTracker
) // show all
513 str
= myTrackerCombo
->itemData(i
,TrackerRole
).toString();
514 const int pos
= str
.lastIndexOf( '.' );
516 str
.truncate( pos
+1 );
518 myPrefs
.set( Prefs::FILTER_TRACKERS
, str
);
523 FilterBar :: onActivityIndexChanged( int i
)
525 if( !myIsBootstrapping
)
527 const FilterMode mode
= myActivityCombo
->itemData( i
, ActivityRole
).toInt( );
528 myPrefs
.set( Prefs::FILTER_MODE
, mode
);
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( ); }
542 FilterBar :: recountSoon( )
544 if( !myRecountTimer
->isActive( ) )
546 myRecountTimer
->setSingleShot( true );
547 myRecountTimer
->start( 500 );
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
);
567 FilterBar :: getCountString( int n
) const
569 return QString("%L1").arg(n
);