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 12607 2011-08-01 22:24:24Z 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( QIcon::fromTheme( "view-refresh", blankIcon ), tr( "Verifying" ) ); 251 row->setData( FilterMode::SHOW_VERIFYING, ActivityRole ); 252 model->appendRow( row ); 253 254 row = new QStandardItem( QIcon::fromTheme( "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> 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 ) ); 316 } 317 } 318 foreach( QString name, torrentNames ) 319 ++torrentsPerHost[ name ]; 320 } 321 322 // update the "All" row 323 myTrackerModel->setData( myTrackerModel->index(0,0), getCountString(myTorrents.rowCount()), TorrentCountRole ); 324 325 // rows to update 326 foreach( QString host, oldHosts & newHosts ) 327 { 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 ); 332 } 333 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 ); 340 } 341 342 // rows to add 343 bool anyAdded = false; 344 foreach( QString host, newHosts - oldHosts ) 345 { 346 const QString name = readableHostName( host ); 347 348 if( !myTrackerModel->findItems(name).isEmpty() ) 349 continue; 350 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; 357 } 358 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; 366 } 367 368 if( anyAdded ) // the one added might match our filter... 369 refreshPref( Prefs::FILTER_TRACKERS ); 370} 371 372 373QComboBox* 374FilterBar :: createTrackerCombo( QStandardItemModel * model ) 375{ 376 QComboBox * c = new FilterBarComboBox( this ); 377 FilterBarComboBoxDelegate * delegate = new FilterBarComboBoxDelegate( 0, c ); 378 c->setItemDelegate( delegate ); 379 380 QStandardItem * row = new QStandardItem( tr( "All" ) ); 381 row->setData( "", TrackerRole ); 382 row->setData( getCountString(myTorrents.rowCount()), TorrentCountRole ); 383 model->appendRow( row ); 384 385 model->appendRow( new QStandardItem ); // separator 386 delegate->setSeparator( model, model->index( 1, 0 ) ); 387 388 c->setModel( model ); 389 return c; 390} 391 392/**** 393***** 394***** 395***** 396****/ 397 398FilterBar :: 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 ) 405{ 406 QHBoxLayout * h = new QHBoxLayout( this ); 407 const int hmargin = qMax( int(HIG::PAD), style()->pixelMetric( QStyle::PM_LayoutHorizontalSpacing ) ); 408 409 h->setSpacing( 0 ); 410 h->setContentsMargins( 2, 2, 2, 2 ); 411 h->addWidget( new QLabel( tr( "Show:" ), this ) ); 412 h->addSpacing( hmargin ); 413 414 myActivityCombo = createActivityCombo( ); 415 h->addWidget( myActivityCombo, 1 ); 416 h->addSpacing( hmargin ); 417 418 myTrackerModel = new QStandardItemModel; 419 myTrackerCombo = createTrackerCombo( myTrackerModel ); 420 h->addWidget( myTrackerCombo, 1 ); 421 h->addSpacing( hmargin*2 ); 422 423 myLineEdit = new QLineEdit( this ); 424 h->addWidget( myLineEdit ); 425 connect( myLineEdit, SIGNAL(textChanged(QString)), this, SLOT(onTextChanged(QString))); 426 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())); 435 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()) ); 445 446 recountSoon( ); 447 refreshTrackers( ); 448 myIsBootstrapping = false; 449 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 ); 456} 457 458FilterBar :: ~FilterBar( ) 459{ 460 delete myRecountTimer; 461} 462 463/*** 464**** 465***/ 466 467void 468FilterBar :: refreshPref( int key ) 469{ 470 switch( key ) 471 { 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; 478 } 479 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, "" ); 490 } 491 break; 492 } 493 494 case Prefs :: FILTER_TEXT: 495 myLineEdit->setText( myPrefs.getString( key ) ); 496 break; 497 } 498} 499 500void 501FilterBar :: onTextChanged( const QString& str ) 502{ 503 if( !myIsBootstrapping ) 504 myPrefs.set( Prefs::FILTER_TEXT, str.trimmed( ) ); 505} 506 507void 508FilterBar :: onTrackerIndexChanged( int i ) 509{ 510 if( !myIsBootstrapping ) 511 { 512 QString str; 513 const bool isTracker = !myTrackerCombo->itemData(i,TrackerRole).toString().isEmpty(); 514 if( !isTracker ) // show all 515 str = ""; 516 else { 517 str = myTrackerCombo->itemData(i,TrackerRole).toString(); 518 const int pos = str.lastIndexOf( '.' ); 519 if( pos >= 0 ) 520 str.truncate( pos+1 ); 521 } 522 myPrefs.set( Prefs::FILTER_TRACKERS, str ); 523 } 524} 525 526void 527FilterBar :: onActivityIndexChanged( int i ) 528{ 529 if( !myIsBootstrapping ) 530 { 531 const FilterMode mode = myActivityCombo->itemData( i, ActivityRole ).toInt( ); 532 myPrefs.set( Prefs::FILTER_MODE, mode ); 533 } 534} 535 536/*** 537**** 538***/ 539 540void FilterBar :: onTorrentModelReset( ) { recountSoon( ); } 541void FilterBar :: onTorrentModelRowsInserted( const QModelIndex&, int, int ) { recountSoon( ); } 542void FilterBar :: onTorrentModelRowsRemoved( const QModelIndex&, int, int ) { recountSoon( ); } 543void FilterBar :: onTorrentModelDataChanged( const QModelIndex&, const QModelIndex& ) { recountSoon( ); } 544 545void 546FilterBar :: recountSoon( ) 547{ 548 if( !myRecountTimer->isActive( ) ) 549 { 550 myRecountTimer->setSingleShot( true ); 551 myRecountTimer->start( 500 ); 552 } 553} 554void 555FilterBar :: recount ( ) 556{ 557 // recount the activity combobox... 558 for( int i=0, n=FilterMode::NUM_MODES; i<n; ++i ) 559 { 560 const FilterMode m( i ); 561 QAbstractItemModel * model = myActivityCombo->model( ); 562 QModelIndexList indices = model->match( model->index(0,0), ActivityRole, m.mode(), -1 ); 563 if( !indices.isEmpty( ) ) 564 model->setData( indices.first(), getCountString(myFilter.count(m)), TorrentCountRole ); 565 } 566 567 refreshTrackers( ); 568} 569 570QString 571FilterBar :: getCountString( int n ) const 572{ 573 return QString("%L1").arg(n); 574} 575