source: trunk/qt/filterbar.cc @ 12549

Last change on this file since 12549 was 12549, checked in by jordan, 10 years ago

(trunk qt) #4357 "Wrong torrent count on tracker filterbar" -- handle special case described @ https://trac.transmissionbt.com/ticket/4357#comment:5

  • Property svn:keywords set to Date Rev Author Id
File size: 19.2 KB
Line 
1/*
2 * This file Copyright (C) Mnemosyne LLC
3 *
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.
7 *
8 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
9 *
10 * $Id: filterbar.cc 12549 2011-07-15 02:17:33Z jordan $
11 */
12
13#include <QString>
14#include <QtGui>
15
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"
25
26/****
27*****
28*****  DELEGATE
29*****
30****/
31
32enum
33{
34    TorrentCountRole = Qt::UserRole + 1,
35    ActivityRole,
36    TrackerRole
37};
38
39namespace
40{
41    int getHSpacing( QWidget * w )
42    {
43        return qMax( int(HIG::PAD_SMALL), w->style()->pixelMetric( QStyle::PM_LayoutHorizontalSpacing, 0, w ) );
44    }
45}
46
47FilterBarComboBoxDelegate :: FilterBarComboBoxDelegate( QObject * parent, QComboBox * combo ):
48    QItemDelegate( parent ),
49    myCombo( combo )
50{
51}
52
53bool
54FilterBarComboBoxDelegate :: isSeparator( const QModelIndex &index )
55{
56    return index.data(Qt::AccessibleDescriptionRole).toString() == QLatin1String("separator");
57}
58void
59FilterBarComboBoxDelegate :: setSeparator( QAbstractItemModel * model, const QModelIndex& index )
60{
61    model->setData( index, QString::fromLatin1("separator"), Qt::AccessibleDescriptionRole );
62
63    if( QStandardItemModel *m = qobject_cast<QStandardItemModel*>(model) )
64       if (QStandardItem *item = m->itemFromIndex(index))
65           item->setFlags(item->flags() & ~(Qt::ItemIsSelectable|Qt::ItemIsEnabled));
66}
67
68void
69FilterBarComboBoxDelegate :: paint( QPainter                    * painter,
70                                    const QStyleOptionViewItem  & option,
71                                    const QModelIndex           & index ) const
72{
73    if( isSeparator( index ) )
74    {
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);
82    }
83    else
84    {
85        QStyleOptionViewItem disabledOption = option;
86        disabledOption.state &= ~( QStyle::State_Enabled | QStyle::State_Selected );
87        QRect boundingBox = option.rect;
88
89        const int hmargin = getHSpacing( myCombo );
90        boundingBox.setLeft( boundingBox.left() + hmargin );
91        boundingBox.setRight( boundingBox.right() - hmargin );
92
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 );
100
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;
107
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 );
115    }
116}
117
118QSize
119FilterBarComboBoxDelegate :: sizeHint( const QStyleOptionViewItem & option,
120                                       const QModelIndex          & index ) const
121{
122    if( isSeparator( index ) )
123    {
124        const int pm = myCombo->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, 0, myCombo);
125        return QSize( pm, pm + 10 );
126    }
127    else
128    {
129        QStyle * s = myCombo->style( );
130        const int hmargin = getHSpacing( myCombo );
131
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;
138    }
139}
140
141/**
142***
143**/
144
145FilterBarComboBox :: FilterBarComboBox( QWidget * parent ):
146    QComboBox( parent )
147{
148}
149
150void
151FilterBarComboBox :: paintEvent( QPaintEvent * e )
152{
153    Q_UNUSED( e );
154
155    QStylePainter painter(this);
156    painter.setPen(palette().color(QPalette::Text));
157
158    // draw the combobox frame, focusrect and selected etc.
159    QStyleOptionComboBox opt;
160    initStyleOption(&opt);
161    painter.drawComplexControl(QStyle::CC_ComboBox, opt);
162
163    // draw the icon and text
164    const QModelIndex modelIndex = model()->index( currentIndex(), 0, rootModelIndex() );
165    if( modelIndex.isValid( ) )
166    {
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 );
171
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;
179        }
180        if( !pixmap.isNull() ) {
181            s->drawItemPixmap( &painter, rect, Qt::AlignLeft|Qt::AlignVCenter, pixmap );
182            rect.setLeft( rect.left() + pixmap.width() + hmargin );
183        }
184
185        // draw the count
186        QString text = modelIndex.data(TorrentCountRole).toString();
187        if( !text.isEmpty( ) )
188        {
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 );
195        }
196
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 );
201    }
202}
203
204/****
205*****
206*****  ACTIVITY
207*****
208****/
209
210QComboBox*
211FilterBar :: createActivityCombo( )
212{
213    QComboBox * c = new FilterBarComboBox( this );
214    FilterBarComboBoxDelegate * delegate = new FilterBarComboBoxDelegate( 0, c );
215    c->setItemDelegate( delegate );
216
217    QPixmap blankPixmap( c->iconSize( ) );
218    blankPixmap.fill( Qt::transparent );
219    QIcon blankIcon( blankPixmap );
220
221    QStandardItemModel * model = new QStandardItemModel;
222
223    QStandardItem * row = new QStandardItem( tr( "All" ) );
224    row->setData( FilterMode::SHOW_ALL, ActivityRole );
225    model->appendRow( row );
226
227    model->appendRow( new QStandardItem ); // separator
228    delegate->setSeparator( model, model->index( 1, 0 ) );
229
230    row = new QStandardItem( QIcon::fromTheme( "system-run", blankIcon ), tr( "Active" ) );
231    row->setData( FilterMode::SHOW_ACTIVE, ActivityRole );
232    model->appendRow( row );
233
234    row = new QStandardItem( QIcon::fromTheme( "go-down", blankIcon ), tr( "Downloading" ) );
235    row->setData( FilterMode::SHOW_DOWNLOADING, ActivityRole );
236    model->appendRow( row );
237
238    row = new QStandardItem( QIcon::fromTheme( "go-up", blankIcon ), tr( "Seeding" ) );
239    row->setData( FilterMode::SHOW_SEEDING, ActivityRole );
240    model->appendRow( row );
241
242    row = new QStandardItem( QIcon::fromTheme( "media-playback-pause", blankIcon ), tr( "Paused" ) );
243    row->setData( FilterMode::SHOW_PAUSED, ActivityRole );
244    model->appendRow( row );
245
246    row = new QStandardItem( blankIcon, tr( "Finished" ) );
247    row->setData( FilterMode::SHOW_FINISHED, ActivityRole );
248    model->appendRow( row );
249
250    row = new QStandardItem( blankIcon, tr( "Queued" ) );
251    row->setData( FilterMode::SHOW_QUEUED, ActivityRole );
252    model->appendRow( row );
253
254    row = new QStandardItem( QIcon::fromTheme( "view-refresh", blankIcon ), tr( "Verifying" ) );
255    row->setData( FilterMode::SHOW_VERIFYING, ActivityRole );
256    model->appendRow( row );
257
258    row = new QStandardItem( QIcon::fromTheme( "dialog-error", blankIcon ), tr( "Error" ) );
259    row->setData( FilterMode::SHOW_ERROR, ActivityRole );
260    model->appendRow( row );
261
262    c->setModel( model );
263    return c;
264}
265
266/****
267*****
268*****
269*****
270****/
271
272namespace
273{
274    QString readableHostName( const QString host )
275    {
276        // get the readable name...
277        QString name = host;
278        const int pos = name.lastIndexOf( '.' );
279        if( pos >= 0 )
280            name.truncate( pos );
281        if( !name.isEmpty( ) )
282            name[0] = name[0].toUpper( );
283        return name;
284    }
285}
286
287void
288FilterBar :: refreshTrackers( )
289{
290    Favicons& favicons = dynamic_cast<MyApp*>(QApplication::instance())->favicons;
291    const int firstTrackerRow = 2; // skip over the "All" and separator...
292
293    // pull info from the tracker model...
294    QSet<QString> oldHosts;
295    for( int row=firstTrackerRow; ; ++row ) {
296        QModelIndex index = myTrackerModel->index( row, 0 );
297        if( !index.isValid( ) )
298            break;
299        oldHosts << index.data(TrackerRole).toString();
300    }
301
302    // pull the new stats from the torrent model...
303    QSet<QString> newHosts;
304    QMap<QString,int> torrentsPerHost;
305    for( int row=0; ; ++row )
306    {
307        QModelIndex index = myTorrents.index( row, 0 );
308        if( !index.isValid( ) )
309            break;
310        const Torrent * tor = index.data( TorrentModel::TorrentRole ).value<const Torrent*>();
311        const QStringList trackers = tor->trackers( );
312        QSet<QString> torrentNames;
313        foreach( QString tracker, trackers ) {
314            const QString host = Favicons::getHost( QUrl( tracker ) );
315            if( host.isEmpty( ) )
316                qWarning() << "torrent" << qPrintable(tor->name()) << "has an invalid announce URL:" << tracker;
317            else {
318                newHosts.insert( host );
319                torrentNames.insert( readableHostName( host ) );
320            }
321        }
322        foreach( QString name, torrentNames )
323            ++torrentsPerHost[ name ];
324    }
325
326    // update the "All" row
327    myTrackerModel->setData( myTrackerModel->index(0,0), getCountString(myTorrents.rowCount()), TorrentCountRole );
328
329    // rows to update
330    foreach( QString host, oldHosts & newHosts )
331    {
332        const QString name = readableHostName( host );
333        QStandardItem * row = myTrackerModel->findItems(name).front();
334        row->setData( getCountString(torrentsPerHost[name]), TorrentCountRole );
335        row->setData( favicons.findFromHost(host), Qt::DecorationRole );
336    }
337
338    // rows to remove
339    foreach( QString host, oldHosts - newHosts ) {
340        const QString name = readableHostName( host );
341        QStandardItem * item = myTrackerModel->findItems(name).front();
342        if( !item->data(TrackerRole).toString().isEmpty() ) // don't remove "All"
343            myTrackerModel->removeRows( item->row(), 1 );
344    }
345
346    // rows to add
347    bool anyAdded = false;
348    foreach( QString host, newHosts - oldHosts )
349    {
350        const QString name = readableHostName( host );
351
352        if( !myTrackerModel->findItems(name).isEmpty() )
353            continue;
354
355        // find the sorted position to add this row
356        int i = firstTrackerRow;
357        for( int n=myTrackerModel->rowCount(); i<n; ++i ) {
358            const QString rowName = myTrackerModel->index(i,0).data(Qt::DisplayRole).toString();
359            if( rowName >= name )
360                break;
361        }
362
363        // add the row
364        QStandardItem * row = new QStandardItem( favicons.findFromHost( host ), name );
365        row->setData( getCountString(torrentsPerHost[host]), TorrentCountRole );
366        row->setData( favicons.findFromHost(host), Qt::DecorationRole );
367        row->setData( host, TrackerRole );
368        myTrackerModel->insertRow( i, row );
369        anyAdded = true;
370    }
371
372    if( anyAdded ) // the one added might match our filter...
373        refreshPref( Prefs::FILTER_TRACKERS );
374}
375
376
377QComboBox*
378FilterBar :: createTrackerCombo( QStandardItemModel * model )
379{
380    QComboBox * c = new FilterBarComboBox( this );
381    FilterBarComboBoxDelegate * delegate = new FilterBarComboBoxDelegate( 0, c );
382    c->setItemDelegate( delegate );
383
384    QStandardItem * row = new QStandardItem( tr( "All" ) );
385    row->setData( "", TrackerRole );
386    row->setData( getCountString(myTorrents.rowCount()), TorrentCountRole );
387    model->appendRow( row );
388
389    model->appendRow( new QStandardItem ); // separator
390    delegate->setSeparator( model, model->index( 1, 0 ) );
391
392    c->setModel( model );
393    return c;
394}
395
396/****
397*****
398*****
399*****
400****/
401
402FilterBar :: FilterBar( Prefs& prefs, TorrentModel& torrents, TorrentFilter& filter, QWidget * parent ):
403    QWidget( parent ),
404    myPrefs( prefs ),
405    myTorrents( torrents ),
406    myFilter( filter ),
407    myRecountTimer( new QTimer( this ) ),
408    myIsBootstrapping( true )
409{
410    QHBoxLayout * h = new QHBoxLayout( this );
411    const int hmargin = qMax( int(HIG::PAD), style()->pixelMetric( QStyle::PM_LayoutHorizontalSpacing ) );
412
413    h->setSpacing( 0 );
414    h->setContentsMargins( 2, 2, 2, 2 );
415    h->addWidget( new QLabel( tr( "Show:" ), this ) );
416    h->addSpacing( hmargin );
417
418    myActivityCombo = createActivityCombo( );
419    h->addWidget( myActivityCombo, 1 );
420    h->addSpacing( hmargin );
421
422    myTrackerModel = new QStandardItemModel;
423    myTrackerCombo = createTrackerCombo( myTrackerModel );
424    h->addWidget( myTrackerCombo, 1 );
425    h->addSpacing( hmargin*2 );
426
427    myLineEdit = new QLineEdit( this );
428    h->addWidget( myLineEdit );
429    connect( myLineEdit, SIGNAL(textChanged(QString)), this, SLOT(onTextChanged(QString)));
430
431    QPushButton * p = new QPushButton;
432    QIcon icon = QIcon::fromTheme( "edit-clear", style()->standardIcon( QStyle::SP_DialogCloseButton ) );
433    int iconSize = style()->pixelMetric( QStyle::PM_SmallIconSize );
434    p->setIconSize( QSize( iconSize, iconSize ) );
435    p->setIcon( icon );
436    p->setFlat( true );
437    h->addWidget( p );
438    connect( p, SIGNAL(clicked(bool)), myLineEdit, SLOT(clear()));
439
440    // listen for changes from the other players
441    connect( &myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(int)));
442    connect( myActivityCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onActivityIndexChanged(int)));
443    connect( myTrackerCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onTrackerIndexChanged(int)));
444    connect( &myTorrents, SIGNAL(modelReset()), this, SLOT(onTorrentModelReset()));
445    connect( &myTorrents, SIGNAL(rowsInserted(const QModelIndex&,int,int)), this, SLOT(onTorrentModelRowsInserted(const QModelIndex&,int,int)));
446    connect( &myTorrents, SIGNAL(rowsRemoved(const QModelIndex&,int,int)), this, SLOT(onTorrentModelRowsRemoved(const QModelIndex&,int,int)));
447    connect( &myTorrents, SIGNAL(dataChanged(const QModelIndex&,const QModelIndex&)), this, SLOT(onTorrentModelDataChanged(const QModelIndex&,const QModelIndex&)));
448    connect( myRecountTimer, SIGNAL(timeout()), this, SLOT(recount()) );
449
450    recountSoon( );
451    refreshTrackers( );
452    myIsBootstrapping = false;
453
454    // initialize our state
455    QList<int> initKeys;
456    initKeys << Prefs :: FILTER_MODE
457             << Prefs :: FILTER_TRACKERS;
458    foreach( int key, initKeys )
459        refreshPref( key );
460}
461
462FilterBar :: ~FilterBar( )
463{
464    delete myRecountTimer;
465}
466
467/***
468****
469***/
470
471void
472FilterBar :: refreshPref( int key )
473{
474    switch( key )
475    {
476        case Prefs :: FILTER_MODE: {
477            const FilterMode m = myPrefs.get<FilterMode>( key );
478            QAbstractItemModel * model = myActivityCombo->model( );
479            QModelIndexList indices = model->match( model->index(0,0), ActivityRole, m.mode(), -1 );
480            myActivityCombo->setCurrentIndex( indices.isEmpty() ? 0 : indices.first().row( ) );
481            break;
482        }
483
484        case Prefs :: FILTER_TRACKERS: {
485            const QString tracker = myPrefs.getString( key );
486            const QString name = readableHostName( tracker );
487            QList<QStandardItem*> rows = myTrackerModel->findItems(name);
488            if( !rows.isEmpty() )
489                myTrackerCombo->setCurrentIndex( rows.front()->row() );
490            else { // hm, we don't seem to have this tracker anymore...
491                const bool isBootstrapping = myTrackerModel->rowCount( ) <= 2;
492                if( !isBootstrapping )
493                    myPrefs.set( key, "" );
494            }
495            break;
496        }
497
498        case Prefs :: FILTER_TEXT:
499            myLineEdit->setText( myPrefs.getString( key ) );
500            break;
501    }
502}
503
504void
505FilterBar :: onTextChanged( const QString& str )
506{
507    if( !myIsBootstrapping )
508        myPrefs.set( Prefs::FILTER_TEXT, str.trimmed( ) );
509}
510
511void
512FilterBar :: onTrackerIndexChanged( int i )
513{
514    if( !myIsBootstrapping )
515    {
516        QString str;
517        const bool isTracker = !myTrackerCombo->itemData(i,TrackerRole).toString().isEmpty();
518        if( !isTracker ) // show all
519            str = "";
520        else {
521            str = myTrackerCombo->itemData(i,TrackerRole).toString();
522            const int pos = str.lastIndexOf( '.' );
523            if( pos >= 0 )
524              str.truncate( pos+1 );
525        }
526        myPrefs.set( Prefs::FILTER_TRACKERS, str );
527    }
528}
529
530void
531FilterBar :: onActivityIndexChanged( int i )
532{
533    if( !myIsBootstrapping )
534    {
535        const FilterMode mode = myActivityCombo->itemData( i, ActivityRole ).toInt( );
536        myPrefs.set( Prefs::FILTER_MODE, mode );
537    }
538}
539
540/***
541****
542***/
543
544void FilterBar :: onTorrentModelReset( ) { recountSoon( ); }
545void FilterBar :: onTorrentModelRowsInserted( const QModelIndex&, int, int ) { recountSoon( ); }
546void FilterBar :: onTorrentModelRowsRemoved( const QModelIndex&, int, int ) { recountSoon( ); }
547void FilterBar :: onTorrentModelDataChanged( const QModelIndex&, const QModelIndex& ) { recountSoon( ); }
548
549void
550FilterBar :: recountSoon( )
551{
552    if( !myRecountTimer->isActive( ) )
553    {
554        myRecountTimer->setSingleShot( true );
555        myRecountTimer->start( 500 );
556    }
557}
558void
559FilterBar :: recount ( )
560{
561    // recount the activity combobox...
562    for( int i=0, n=FilterMode::NUM_MODES; i<n; ++i )
563    {
564        const FilterMode m( i );
565        QAbstractItemModel * model = myActivityCombo->model( );
566        QModelIndexList indices = model->match( model->index(0,0), ActivityRole, m.mode(), -1 );
567        if( !indices.isEmpty( ) )
568            model->setData( indices.first(), getCountString(myFilter.count(m)), TorrentCountRole );
569    }
570
571    refreshTrackers( );
572}
573
574QString
575FilterBar :: getCountString( int n ) const
576{
577    return QString("%L1").arg(n);
578}
Note: See TracBrowser for help on using the repository browser.