source: trunk/qt/filterbar.cc @ 11078

Last change on this file since 11078 was 11078, checked in by charles, 12 years ago

(trunk qt) fix icon spacing issue in Oxygen theme

File size: 18.5 KB
Line 
1/*
2 * This file Copyright (C) 2010 Mnemosyne LLC
3 *
4 * This file is licensed by the GPL version 2.  Works owned by the
5 * Transmission project are granted a special exemption to clause 2(b)
6 * so that the bulk of its code can remain under the MIT license.
7 * This exemption does not extend to derived works not owned by
8 * the Transmission project.
9 *
10 * $Id$
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 "prefs.h"
21#include "qticonloader.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( 4, 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        const int count = modelIndex.data( TorrentCountRole ).toInt();
187        if( count >= 0 ) {
188            const QString text = QString::number( count);
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        QString 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( QtIconLoader::icon( "system-run" ), tr( "Active" ) );
231    row->setData( FilterMode::SHOW_ACTIVE, ActivityRole );
232    model->appendRow( row );
233
234    row = new QStandardItem( QtIconLoader::icon( "go-down" ), tr( "Downloading" ) );
235    row->setData( FilterMode::SHOW_DOWNLOADING, ActivityRole );
236    model->appendRow( row );
237
238    row = new QStandardItem( QtIconLoader::icon( "go-up" ), tr( "Seeding" ) );
239    row->setData( FilterMode::SHOW_SEEDING, ActivityRole );
240    model->appendRow( row );
241
242    row = new QStandardItem( QtIconLoader::icon( "media-playback-pause", blankIcon ), tr( "Paused" ) );
243    row->setData( FilterMode::SHOW_PAUSED, ActivityRole );
244    model->appendRow( row );
245
246    row = new QStandardItem( blankIcon, tr( "Queued" ) );
247    row->setData( FilterMode::SHOW_QUEUED, ActivityRole );
248    model->appendRow( row );
249
250    row = new QStandardItem( QtIconLoader::icon( "view-refresh", blankIcon ), tr( "Verifying" ) );
251    row->setData( FilterMode::SHOW_VERIFYING, ActivityRole );
252    model->appendRow( row );
253
254    row = new QStandardItem( QtIconLoader::icon( "dialog-error", blankIcon ), tr( "Error" ) );
255    row->setData( FilterMode::SHOW_ERROR, ActivityRole );
256    model->appendRow( row );
257
258    c->setModel( model );
259    return c;
260}
261
262/****
263*****
264***** 
265*****
266****/
267
268namespace
269{
270    QString readableHostName( const QString host )
271    {
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;
280    }
281}
282
283void
284FilterBar :: refreshTrackers( )
285{
286    Favicons& favicons = dynamic_cast<MyApp*>(QApplication::instance())->favicons;
287    const int firstTrackerRow = 2; // skip over the "All" and separator...
288
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();
296    }
297
298    // pull the new stats from the torrent model...
299    QSet<QString> newHosts;
300    QMap<QString,int> torrentsPerHost;
301    for( int row=0; ; ++row )
302    {
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> torrentHosts;
309        foreach( QString tracker, trackers )
310            torrentHosts.insert( Favicons::getHost( QUrl( tracker ) ) );
311        foreach( QString host, torrentHosts ) {
312            newHosts.insert( host );
313            ++torrentsPerHost[host];
314        }
315    }
316
317    // update the "All" row
318    myTrackerModel->setData( myTrackerModel->index(0,0), myTorrents.rowCount(), TorrentCountRole );
319
320    // rows to update
321    foreach( QString host, oldHosts & newHosts )
322    {
323        const QString name = readableHostName( host );
324        QStandardItem * row = myTrackerModel->findItems(name).front();
325        row->setData( torrentsPerHost[host], TorrentCountRole );
326        row->setData( favicons.findFromHost(host), Qt::DecorationRole );
327    }
328
329    // rows to remove
330    foreach( QString host, oldHosts - newHosts ) {
331        const QString name = readableHostName( host );
332        QStandardItem * item = myTrackerModel->findItems(name).front();
333        if( !item->data(TrackerRole).toString().isEmpty() ) // don't remove "All"
334            myTrackerModel->removeRows( item->row(), 1 );
335    }
336
337    // rows to add
338    bool anyAdded = false;
339    foreach( QString host, newHosts - oldHosts )
340    {
341        const QString name = readableHostName( host );
342
343        // find the sorted position to add this row
344        int i = firstTrackerRow;
345        for( int n=myTrackerModel->rowCount(); i<n; ++i )
346            if( myTrackerModel->index(i,0).data(Qt::DisplayRole).toString() > name )
347                break;
348
349        // add the row
350        QStandardItem * row = new QStandardItem( favicons.findFromHost( host ), readableHostName( host ) );
351        row->setData( torrentsPerHost[host], TorrentCountRole );
352        row->setData( favicons.findFromHost(host), Qt::DecorationRole );
353        row->setData( host, TrackerRole );
354        myTrackerModel->insertRow( i, row );
355        anyAdded = true;
356    }
357
358    if( anyAdded ) // the one added might match our filter...
359        refreshPref( Prefs::FILTER_TRACKERS );
360}
361
362
363QComboBox*
364FilterBar :: createTrackerCombo( QStandardItemModel * model )
365{
366    QComboBox * c = new FilterBarComboBox( this );
367    FilterBarComboBoxDelegate * delegate = new FilterBarComboBoxDelegate( 0, c );
368    c->setItemDelegate( delegate );
369
370    QStandardItem * row = new QStandardItem( tr( "All" ) );
371    row->setData( "", TrackerRole );
372    row->setData( myTorrents.rowCount(), TorrentCountRole );
373    model->appendRow( row );
374
375    model->appendRow( new QStandardItem ); // separator
376    delegate->setSeparator( model, model->index( 1, 0 ) );
377
378    c->setModel( model );
379    return c;
380}
381
382/****
383*****
384***** 
385*****
386****/
387
388FilterBar :: FilterBar( Prefs& prefs, TorrentModel& torrents, TorrentFilter& filter, QWidget * parent ):
389    QWidget( parent ),
390    myPrefs( prefs ),
391    myTorrents( torrents ),
392    myFilter( filter ),
393    myRecountTimer( new QTimer( this ) ),
394    myIsBootstrapping( true )
395{
396    QHBoxLayout * h = new QHBoxLayout( this );
397    int hmargin = style()->pixelMetric( QStyle::PM_LayoutHorizontalSpacing );
398
399    h->setSpacing( 0 );
400    h->setContentsMargins( 2, 2, 2, 2 );
401    h->addWidget( new QLabel( tr( "Show:" ), this ) );
402    h->addSpacing( hmargin );
403
404    myActivityCombo = createActivityCombo( );
405    h->addWidget( myActivityCombo, 1 );
406    h->addSpacing( hmargin );
407
408    myTrackerModel = new QStandardItemModel;
409    myTrackerCombo = createTrackerCombo( myTrackerModel );
410    h->addWidget( myTrackerCombo, 1 );
411    h->addSpacing( hmargin*2 );
412   
413    myLineEdit = new QLineEdit( this );
414    h->addWidget( myLineEdit );
415    connect( myLineEdit, SIGNAL(textChanged(QString)), this, SLOT(onTextChanged(QString)));
416
417    QPushButton * p = new QPushButton;
418    QIcon icon = QtIconLoader::icon( "edit-clear" );
419    if( icon.isNull( ) )
420        icon = style()->standardIcon( QStyle::SP_DialogCloseButton );
421    int iconSize = style()->pixelMetric( QStyle::PM_SmallIconSize );
422    p->setIconSize( QSize( iconSize, iconSize ) );
423    p->setIcon( icon );
424    p->setFlat( true );
425    h->addWidget( p );
426    connect( p, SIGNAL(clicked(bool)), myLineEdit, SLOT(clear()));
427
428    // listen for changes from the other players
429    connect( &myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(int)));
430    connect( myActivityCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onActivityIndexChanged(int)));
431    connect( myTrackerCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onTrackerIndexChanged(int)));
432    connect( &myTorrents, SIGNAL(modelReset()), this, SLOT(onTorrentModelReset()));
433    connect( &myTorrents, SIGNAL(rowsInserted(const QModelIndex&,int,int)), this, SLOT(onTorrentModelRowsInserted(const QModelIndex&,int,int)));
434    connect( &myTorrents, SIGNAL(rowsRemoved(const QModelIndex&,int,int)), this, SLOT(onTorrentModelRowsRemoved(const QModelIndex&,int,int)));
435    connect( &myTorrents, SIGNAL(dataChanged(const QModelIndex&,const QModelIndex&)), this, SLOT(onTorrentModelDataChanged(const QModelIndex&,const QModelIndex&)));
436    connect( myRecountTimer, SIGNAL(timeout()), this, SLOT(recount()) );
437
438    recountSoon( );
439    refreshTrackers( );
440    myIsBootstrapping = false;
441
442    // initialize our state
443    QList<int> initKeys;
444    initKeys << Prefs :: FILTER_MODE
445             << Prefs :: FILTER_TRACKERS;
446    foreach( int key, initKeys )
447        refreshPref( key );
448}
449
450FilterBar :: ~FilterBar( )
451{
452    delete myRecountTimer;
453}
454
455/***
456****
457***/
458
459void
460FilterBar :: refreshPref( int key )
461{
462    switch( key )
463    {
464        case Prefs :: FILTER_MODE: {
465            const FilterMode m = myPrefs.get<FilterMode>( key );
466            QAbstractItemModel * model = myActivityCombo->model( );
467            QModelIndexList indices = model->match( model->index(0,0), ActivityRole, m.mode(), -1 );
468            myActivityCombo->setCurrentIndex( indices.isEmpty() ? 0 : indices.first().row( ) );
469            break;
470        }
471
472        case Prefs :: FILTER_TRACKERS: {
473            const QString tracker = myPrefs.getString( key );
474            QModelIndexList indices = myTrackerModel->match( myTrackerModel->index(0,0), TrackerRole, tracker, 1, Qt::MatchFixedString );
475            if( !indices.isEmpty( )  )
476                myTrackerCombo->setCurrentIndex( indices.first().row() );
477            else { // hm, we don't seem to have this tracker anymore...
478                const bool isBootstrapping = myTrackerModel->rowCount( ) <= 2;
479                if( !isBootstrapping )
480                    myPrefs.set( key, "" );
481            }
482            break;
483        }
484
485        case Prefs :: FILTER_TEXT:
486            myLineEdit->setText( myPrefs.getString( key ) );
487            break;
488    }
489}
490
491void
492FilterBar :: onTextChanged( const QString& str )
493{
494    if( !myIsBootstrapping )
495        myPrefs.set( Prefs::FILTER_TEXT, str.trimmed( ) );
496}
497
498void
499FilterBar :: onTrackerIndexChanged( int i )
500{
501    if( !myIsBootstrapping )
502    {
503        QString str;
504        const bool isTracker = !myTrackerCombo->itemData(i,TrackerRole).toString().isEmpty();
505        if( isTracker )
506            str = myTrackerCombo->itemData(i,TrackerRole).toString();
507        else // show all
508            str = "";
509        myPrefs.set( Prefs::FILTER_TRACKERS, str );
510    }
511}
512
513void
514FilterBar :: onActivityIndexChanged( int i )
515{
516    if( !myIsBootstrapping )
517    {
518        const FilterMode mode = myActivityCombo->itemData( i, ActivityRole ).toInt( );
519        myPrefs.set( Prefs::FILTER_MODE, mode );
520    }
521}
522
523/***
524****
525***/
526
527void FilterBar :: onTorrentModelReset( ) { recountSoon( ); }
528void FilterBar :: onTorrentModelRowsInserted( const QModelIndex&, int, int ) { recountSoon( ); }
529void FilterBar :: onTorrentModelRowsRemoved( const QModelIndex&, int, int ) { recountSoon( ); }
530void FilterBar :: onTorrentModelDataChanged( const QModelIndex&, const QModelIndex& ) { recountSoon( ); }
531
532void
533FilterBar :: recountSoon( )
534{
535    if( !myRecountTimer->isActive( ) )
536    {
537        myRecountTimer->setSingleShot( true );
538        myRecountTimer->start( 500 );
539    }
540}
541void
542FilterBar :: recount ( )
543{
544    // recount the activity combobox...
545    for( int i=0, n=FilterMode::NUM_MODES; i<n; ++i )
546    {
547        const FilterMode m( i );
548        QAbstractItemModel * model = myActivityCombo->model( );
549        QModelIndexList indices = model->match( model->index(0,0), ActivityRole, m.mode(), -1 );
550        if( !indices.isEmpty( ) ) {
551            const int count = myFilter.count( m );
552            model->setData( indices.first(), count, TorrentCountRole );
553        }
554    }
555
556    refreshTrackers( );
557}
Note: See TracBrowser for help on using the repository browser.