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: details.cc 13076 2011-11-05 15:45:38Z jordan $ 11 */ 12 13#include <cassert> 14#include <ctime> 15 16#include <QCheckBox> 17#include <QComboBox> 18#include <QDateTime> 19#include <QDialogButtonBox> 20#include <QDoubleSpinBox> 21#include <QEvent> 22#include <QFont> 23#include <QFontMetrics> 24#include <QHBoxLayout> 25#include <QHBoxLayout> 26#include <QHeaderView> 27#include <QInputDialog> 28#include <QItemSelectionModel> 29#include <QLabel> 30#include <QList> 31#include <QMap> 32#include <QMessageBox> 33#include <QPushButton> 34#include <QRadioButton> 35#include <QResizeEvent> 36#include <QSpinBox> 37#include <QStringList> 38#include <QStyle> 39#include <QTabWidget> 40#include <QTextBrowser> 41#include <QTreeView> 42#include <QTreeWidget> 43#include <QTreeWidgetItem> 44#include <QVBoxLayout> 45 46#include <libtransmission/transmission.h> 47#include <libtransmission/bencode.h> 48#include <libtransmission/utils.h> // tr_getRatio() 49 50#include "details.h" 51#include "file-tree.h" 52#include "formatter.h" 53#include "hig.h" 54#include "prefs.h" 55#include "session.h" 56#include "squeezelabel.h" 57#include "torrent.h" 58#include "torrent-model.h" 59#include "tracker-delegate.h" 60#include "tracker-model.h" 61#include "tracker-model-filter.h" 62 63class Prefs; 64class Session; 65 66/**** 67***** 68****/ 69 70namespace 71{ 72 const int REFRESH_INTERVAL_MSEC = 4000; 73 74 const char * PREF_KEY( "pref-key" ); 75 76 enum // peer columns 77 { 78 COL_LOCK, 79 COL_UP, 80 COL_DOWN, 81 COL_PERCENT, 82 COL_STATUS, 83 COL_ADDRESS, 84 COL_CLIENT, 85 N_COLUMNS 86 }; 87} 88 89/*** 90**** 91***/ 92 93class PeerItem: public QTreeWidgetItem 94{ 95 Peer peer; 96 QString collatedAddress; 97 QString status; 98 99 public: 100 virtual ~PeerItem( ) { } 101 PeerItem( const Peer& p ) { 102 peer = p; 103 int q[4]; 104 if( sscanf( p.address.toUtf8().constData(), "%d.%d.%d.%d", q+0, q+1, q+2, q+3 ) == 4 ) 105 collatedAddress.sprintf( "%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3] ); 106 else 107 collatedAddress = p.address; 108 } 109 public: 110 void refresh( const Peer& p ) { peer = p; } 111 void setStatus( const QString& s ) { status = s; } 112 virtual bool operator< ( const QTreeWidgetItem & other ) const { 113 const PeerItem * i = dynamic_cast<const PeerItem*>(&other); 114 QTreeWidget * tw( treeWidget( ) ); 115 const int column = tw ? tw->sortColumn() : 0; 116 switch( column ) { 117 case COL_UP: return peer.rateToPeer < i->peer.rateToPeer; 118 case COL_DOWN: return peer.rateToClient < i->peer.rateToClient; 119 case COL_PERCENT: return peer.progress < i->peer.progress; 120 case COL_STATUS: return status < i->status; 121 case COL_CLIENT: return peer.clientName < i->peer.clientName; 122 case COL_LOCK: return peer.isEncrypted && !i->peer.isEncrypted; 123 default: return collatedAddress < i->collatedAddress; 124 } 125 } 126}; 127 128/*** 129**** 130***/ 131 132QIcon 133Details :: getStockIcon( const QString& freedesktop_name, int fallback ) 134{ 135 QIcon icon = QIcon::fromTheme( freedesktop_name ); 136 137 if( icon.isNull( ) ) 138 icon = style()->standardIcon( QStyle::StandardPixmap( fallback ), 0, this ); 139 140 return icon; 141} 142 143Details :: Details( Session& session, Prefs& prefs, TorrentModel& model, QWidget * parent ): 144 QDialog( parent, Qt::Dialog ), 145 mySession( session ), 146 myPrefs( prefs ), 147 myModel( model ), 148 myChangedTorrents( false ), 149 myHavePendingRefresh( false ) 150{ 151 QVBoxLayout * layout = new QVBoxLayout( this ); 152 153 setWindowTitle( tr( "Torrent Properties" ) ); 154 155 QTabWidget * t = new QTabWidget( this ); 156 QWidget * w; 157 t->addTab( w = createInfoTab( ), tr( "Information" ) ); 158 myWidgets << w; 159 t->addTab( w = createPeersTab( ), tr( "Peers" ) ); 160 myWidgets << w; 161 t->addTab( w = createTrackerTab( ), tr( "Tracker" ) ); 162 myWidgets << w; 163 t->addTab( w = createFilesTab( ), tr( "Files" ) ); 164 myWidgets << w; 165 t->addTab( w = createOptionsTab( ), tr( "Options" ) ); 166 myWidgets << w; 167 layout->addWidget( t ); 168 169 QDialogButtonBox * buttons = new QDialogButtonBox( QDialogButtonBox::Close, Qt::Horizontal, this ); 170 connect( buttons, SIGNAL(rejected()), this, SLOT(close())); 171 layout->addWidget( buttons ); 172 QWidget::setAttribute( Qt::WA_DeleteOnClose, true ); 173 174 QList<int> initKeys; 175 initKeys << Prefs :: SHOW_TRACKER_SCRAPES 176 << Prefs :: SHOW_BACKUP_TRACKERS; 177 foreach( int key, initKeys ) 178 refreshPref( key ); 179 180 connect( &myTimer, SIGNAL(timeout()), this, SLOT(onTimer())); 181 connect( &myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(int)) ); 182 183 onTimer( ); 184 myTimer.setSingleShot( false ); 185 myTimer.start( REFRESH_INTERVAL_MSEC ); 186} 187 188Details :: ~Details( ) 189{ 190 myTrackerDelegate->deleteLater(); 191 myTrackerFilter->deleteLater(); 192 myTrackerModel->deleteLater(); 193} 194 195void 196Details :: setIds( const QSet<int>& ids ) 197{ 198 if( ids == myIds ) 199 return; 200 201 myChangedTorrents = true; 202 203 // stop listening to the old torrents 204 foreach( int id, myIds ) { 205 const Torrent * tor = myModel.getTorrentFromId( id ); 206 if( tor ) 207 disconnect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) ); 208 } 209 210 myFileTreeView->clear( ); 211 myIds = ids; 212 myTrackerModel->refresh( myModel, myIds ); 213 214 // listen to the new torrents 215 foreach( int id, myIds ) { 216 const Torrent * tor = myModel.getTorrentFromId( id ); 217 if( tor ) 218 connect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) ); 219 } 220 221 foreach( QWidget * w, myWidgets ) 222 w->setEnabled( false ); 223 224 onTimer( ); 225} 226 227void 228Details :: refreshPref( int key ) 229{ 230 QString str; 231 232 switch( key ) 233 { 234 case Prefs :: SHOW_TRACKER_SCRAPES: { 235 QItemSelectionModel * selectionModel( myTrackerView->selectionModel( ) ); 236 const QItemSelection selection( selectionModel->selection( ) ); 237 const QModelIndex currentIndex( selectionModel->currentIndex( ) ); 238 myTrackerDelegate->setShowMore( myPrefs.getBool( key ) ); 239 selectionModel->clear( ); 240 myTrackerView->reset( ); 241 selectionModel->select( selection, QItemSelectionModel::Select ); 242 selectionModel->setCurrentIndex( currentIndex, QItemSelectionModel::NoUpdate ); 243 break; 244 } 245 246 case Prefs :: SHOW_BACKUP_TRACKERS: 247 myTrackerFilter->setShowBackupTrackers( myPrefs.getBool( key ) ); 248 break; 249 250 default: 251 break; 252 } 253} 254 255 256/*** 257**** 258***/ 259 260QString 261Details :: timeToStringRounded( int seconds ) 262{ 263 if( seconds > 60 ) seconds -= ( seconds % 60 ); 264 return Formatter::timeToString ( seconds ); 265} 266 267void 268Details :: onTimer( ) 269{ 270 getNewData( ); 271} 272 273void 274Details :: getNewData( ) 275{ 276 if( !myIds.empty( ) ) 277 { 278 QSet<int> infos; 279 foreach( int id, myIds ) { 280 const Torrent * tor = myModel.getTorrentFromId( id ); 281 if( tor->isMagnet() ) 282 infos.insert( tor->id() ); 283 } 284 if( !infos.isEmpty() ) 285 mySession.initTorrents( infos ); 286 mySession.refreshExtraStats( myIds ); 287 } 288} 289 290void 291Details :: onTorrentChanged( ) 292{ 293 if( !myHavePendingRefresh ) { 294 myHavePendingRefresh = true; 295 QTimer::singleShot( 100, this, SLOT(refresh())); 296 } 297} 298 299namespace 300{ 301 void setIfIdle( QComboBox * box, int i ) 302 { 303 if( !box->hasFocus( ) ) 304 { 305 box->blockSignals( true ); 306 box->setCurrentIndex( i ); 307 box->blockSignals( false ); 308 } 309 } 310 311 void setIfIdle( QDoubleSpinBox * spin, double value ) 312 { 313 if( !spin->hasFocus( ) ) 314 { 315 spin->blockSignals( true ); 316 spin->setValue( value ); 317 spin->blockSignals( false ); 318 } 319 } 320 321 void setIfIdle( QSpinBox * spin, int value ) 322 { 323 if( !spin->hasFocus( ) ) 324 { 325 spin->blockSignals( true ); 326 spin->setValue( value ); 327 spin->blockSignals( false ); 328 } 329 } 330} 331 332void 333Details :: refresh( ) 334{ 335 const int n = myIds.size( ); 336 const bool single = n == 1; 337 const QString blank; 338 const QFontMetrics fm( fontMetrics( ) ); 339 QList<const Torrent*> torrents; 340 QString string; 341 const QString none = tr( "None" ); 342 const QString mixed = tr( "Mixed" ); 343 const QString unknown = tr( "Unknown" ); 344 345 // build a list of torrents 346 foreach( int id, myIds ) { 347 const Torrent * tor = myModel.getTorrentFromId( id ); 348 if( tor ) 349 torrents << tor; 350 } 351 352 /// 353 /// activity tab 354 /// 355 356 // myStateLabel 357 if( torrents.empty( ) ) 358 string = none; 359 else { 360 bool isMixed = false; 361 bool allPaused = true; 362 bool allFinished = true; 363 const tr_torrent_activity baseline = torrents[0]->getActivity( ); 364 foreach( const Torrent * t, torrents ) { 365 const tr_torrent_activity activity = t->getActivity( ); 366 if( activity != baseline ) 367 isMixed = true; 368 if( activity != TR_STATUS_STOPPED ) 369 allPaused = allFinished = false; 370 if( !t->isFinished( ) ) 371 allFinished = false; 372 } 373 if( isMixed ) 374 string = mixed; 375 else if( allFinished ) 376 string = tr( "Finished" ); 377 else if( allPaused ) 378 string = tr( "Paused" ); 379 else 380 string = torrents[0]->activityString( ); 381 } 382 myStateLabel->setText( string ); 383 const QString stateString = string; 384 385 // myHaveLabel 386 double sizeWhenDone = 0; 387 double leftUntilDone = 0; 388 double available = 0; 389 int64_t haveTotal = 0; 390 int64_t haveVerified = 0; 391 int64_t haveUnverified = 0; 392 int64_t verifiedPieces = 0; 393 if( torrents.empty( ) ) 394 string = none; 395 else { 396 foreach( const Torrent * t, torrents ) { 397 if( t->hasMetadata( ) ) { 398 haveTotal += t->haveTotal( ); 399 haveUnverified += t->haveUnverified( ); 400 const uint64_t v = t->haveVerified( ); 401 haveVerified += v; 402 if( t->pieceSize( ) ) 403 verifiedPieces += v / t->pieceSize( ); 404 sizeWhenDone += t->sizeWhenDone( ); 405 leftUntilDone += t->leftUntilDone( ); 406 available += t->sizeWhenDone() - t->leftUntilDone() + t->desiredAvailable(); 407 } 408 } 409 { 410 const double d = 100.0 * ( sizeWhenDone ? ( sizeWhenDone - leftUntilDone ) / sizeWhenDone : 1 ); 411 QString pct = Formatter::percentToString( d ); 412 413 if( !haveUnverified && !leftUntilDone ) 414 { 415 string = tr( "%1 (100%)" ) 416 .arg( Formatter::sizeToString( haveVerified ) ); 417 } 418 else if( !haveUnverified ) 419 { 420 string = tr( "%1 of %2 (%3%)" ) 421 .arg( Formatter::sizeToString( haveVerified ) ) 422 .arg( Formatter::sizeToString( sizeWhenDone ) ) 423 .arg( pct ); 424 } 425 else 426 { 427 string = tr( "%1 of %2 (%3%), %4 Unverified" ) 428 .arg( Formatter::sizeToString( haveVerified + haveUnverified ) ) 429 .arg( Formatter::sizeToString( sizeWhenDone ) ) 430 .arg( pct ) 431 .arg( Formatter::sizeToString( haveUnverified ) ); 432 } 433 } 434 } 435 myHaveLabel->setText( string ); 436 437 // myAvailabilityLabel 438 if( torrents.empty( ) ) 439 string = none; 440 else { 441 if( sizeWhenDone == 0 ) 442 string = none; 443 else 444 string = QString( "%1%" ).arg( Formatter::percentToString( ( 100.0 * available ) / sizeWhenDone ) ); 445 } 446 myAvailabilityLabel->setText( string ); 447 448 // myDownloadedLabel 449 if( torrents.empty( ) ) 450 string = none; 451 else { 452 uint64_t d = 0; 453 uint64_t f = 0; 454 foreach( const Torrent * t, torrents ) { 455 d += t->downloadedEver( ); 456 f += t->failedEver( ); 457 } 458 const QString dstr = Formatter::sizeToString( d ); 459 const QString fstr = Formatter::sizeToString( f ); 460 if( f ) 461 string = tr( "%1 (%2 corrupt)" ).arg( dstr ).arg( fstr ); 462 else 463 string = dstr; 464 } 465 myDownloadedLabel->setText( string ); 466 467 if( torrents.empty( ) ) 468 string = none; 469 else { 470 uint64_t u = 0; 471 uint64_t d = 0; 472 foreach( const Torrent * t, torrents ) { 473 u += t->uploadedEver( ); 474 d += t->downloadedEver( ); 475 } 476 string = tr( "%1 (Ratio: %2)" ) 477 .arg( Formatter::sizeToString( u ) ) 478 .arg( Formatter::ratioToString( tr_getRatio( u, d ) ) ); 479 } 480 myUploadedLabel->setText( string ); 481 482 const QDateTime qdt_now = QDateTime::currentDateTime( ); 483 484 // myRunTimeLabel 485 if( torrents.empty( ) ) 486 string = none; 487 else { 488 bool allPaused = true; 489 QDateTime baseline = torrents[0]->lastStarted( ); 490 foreach( const Torrent * t, torrents ) { 491 if( baseline != t->lastStarted( ) ) 492 baseline = QDateTime( ); 493 if( !t->isPaused( ) ) 494 allPaused = false; 495 } 496 if( allPaused ) 497 string = stateString; // paused || finished 498 else if( baseline.isNull( ) ) 499 string = mixed; 500 else 501 string = Formatter::timeToString( baseline.secsTo( qdt_now ) ); 502 } 503 myRunTimeLabel->setText( string ); 504 505 506 // myETALabel 507 string.clear( ); 508 if( torrents.empty( ) ) 509 string = none; 510 else { 511 int baseline = torrents[0]->getETA( ); 512 foreach( const Torrent * t, torrents ) { 513 if( baseline != t->getETA( ) ) { 514 string = mixed; 515 break; 516 } 517 } 518 if( string.isEmpty( ) ) { 519 if( baseline < 0 ) 520 string = tr( "Unknown" ); 521 else 522 string = Formatter::timeToString( baseline ); 523 } 524 } 525 myETALabel->setText( string ); 526 527 528 // myLastActivityLabel 529 if( torrents.empty( ) ) 530 string = none; 531 else { 532 QDateTime latest = torrents[0]->lastActivity( ); 533 foreach( const Torrent * t, torrents ) { 534 const QDateTime dt = t->lastActivity( ); 535 if( latest < dt ) 536 latest = dt; 537 } 538 const int seconds = latest.isValid() ? latest.secsTo( qdt_now ) : -1; 539 if( seconds < 0 ) 540 string = none; 541 else if( seconds < 5 ) 542 string = tr( "Active now" ); 543 else 544 string = tr( "%1 ago" ).arg( Formatter::timeToString( seconds ) ); 545 } 546 myLastActivityLabel->setText( string ); 547 548 549 if( torrents.empty( ) ) 550 string = none; 551 else { 552 string = torrents[0]->getError( ); 553 foreach( const Torrent * t, torrents ) { 554 if( string != t->getError( ) ) { 555 string = mixed; 556 break; 557 } 558 } 559 } 560 if( string.isEmpty( ) ) 561 string = none; 562 myErrorLabel->setText( string ); 563 564 565 /// 566 /// information tab 567 /// 568 569 // mySizeLabel 570 if( torrents.empty( ) ) 571 string = none; 572 else { 573 int pieces = 0; 574 uint64_t size = 0; 575 uint32_t pieceSize = torrents[0]->pieceSize( ); 576 foreach( const Torrent * t, torrents ) { 577 pieces += t->pieceCount( ); 578 size += t->totalSize( ); 579 if( pieceSize != t->pieceSize( ) ) 580 pieceSize = 0; 581 } 582 if( !size ) 583 string = none; 584 else if( pieceSize > 0 ) 585 string = tr( "%1 (%Ln pieces @ %2)", "", pieces ) 586 .arg( Formatter::sizeToString( size ) ) 587 .arg( Formatter::memToString( pieceSize ) ); 588 else 589 string = tr( "%1 (%Ln pieces)", "", pieces ) 590 .arg( Formatter::sizeToString( size ) ); 591 } 592 mySizeLabel->setText( string ); 593 594 // myHashLabel 595 if( torrents.empty( ) ) 596 string = none; 597 else { 598 string = torrents[0]->hashString( ); 599 foreach( const Torrent * t, torrents ) { 600 if( string != t->hashString( ) ) { 601 string = mixed; 602 break; 603 } 604 } 605 } 606 myHashLabel->setText( string ); 607 608 // myPrivacyLabel 609 if( torrents.empty( ) ) 610 string = none; 611 else { 612 bool b = torrents[0]->isPrivate( ); 613 string = b ? tr( "Private to this tracker -- DHT and PEX disabled" ) 614 : tr( "Public torrent" ); 615 foreach( const Torrent * t, torrents ) { 616 if( b != t->isPrivate( ) ) { 617 string = mixed; 618 break; 619 } 620 } 621 } 622 myPrivacyLabel->setText( string ); 623 624 // myCommentBrowser 625 if( torrents.empty( ) ) 626 string = none; 627 else { 628 string = torrents[0]->comment( ); 629 foreach( const Torrent * t, torrents ) { 630 if( string != t->comment( ) ) { 631 string = mixed; 632 break; 633 } 634 } 635 } 636 myCommentBrowser->setText( string ); 637 myCommentBrowser->setMaximumHeight( QWIDGETSIZE_MAX ); 638 639 // myOriginLabel 640 if( torrents.empty( ) ) 641 string = none; 642 else { 643 bool mixed_creator=false, mixed_date=false; 644 const QString creator = torrents[0]->creator(); 645 const QString date = torrents[0]->dateCreated().toString(); 646 foreach( const Torrent * t, torrents ) { 647 mixed_creator |= ( creator != t->creator() ); 648 mixed_date |= ( date != t->dateCreated().toString() ); 649 } 650 if( mixed_creator && mixed_date ) 651 string = mixed; 652 else if( mixed_date && !creator.isEmpty()) 653 string = tr( "Created by %1" ).arg( creator ); 654 else if( mixed_creator && !date.isEmpty()) 655 string = tr( "Created on %1" ).arg( date ); 656 else if( creator.isEmpty() && date.isEmpty()) 657 string = tr( "N/A" ); 658 else 659 string = tr( "Created by %1 on %2" ).arg( creator ).arg( date ); 660 } 661 myOriginLabel->setText( string ); 662 663 // myLocationLabel 664 if( torrents.empty( ) ) 665 string = none; 666 else { 667 string = torrents[0]->getPath( ); 668 foreach( const Torrent * t, torrents ) { 669 if( string != t->getPath( ) ) { 670 string = mixed; 671 break; 672 } 673 } 674 } 675 myLocationLabel->setText( string ); 676 677 678 /// 679 /// Options Tab 680 /// 681 682 if( myChangedTorrents && !torrents.empty( ) ) 683 { 684 int i; 685 const Torrent * baseline = *torrents.begin(); 686 const Torrent * tor; 687 bool uniform; 688 bool baselineFlag; 689 int baselineInt; 690 691 // mySessionLimitCheck 692 uniform = true; 693 baselineFlag = baseline->honorsSessionLimits( ); 694 foreach( tor, torrents ) if( baselineFlag != tor->honorsSessionLimits( ) ) { uniform = false; break; } 695 mySessionLimitCheck->setChecked( uniform && baselineFlag ); 696 697 // mySingleDownCheck 698 uniform = true; 699 baselineFlag = baseline->downloadIsLimited( ); 700 foreach( tor, torrents ) if( baselineFlag != tor->downloadIsLimited( ) ) { uniform = false; break; } 701 mySingleDownCheck->setChecked( uniform && baselineFlag ); 702 703 // mySingleUpCheck 704 uniform = true; 705 baselineFlag = baseline->uploadIsLimited( ); 706 foreach( tor, torrents ) if( baselineFlag != tor->uploadIsLimited( ) ) { uniform = false; break; } 707 mySingleUpCheck->setChecked( uniform && baselineFlag ); 708 709 // myBandwidthPriorityCombo 710 uniform = true; 711 baselineInt = baseline->getBandwidthPriority( ); 712 foreach( tor, torrents ) if ( baselineInt != tor->getBandwidthPriority( ) ) { uniform = false; break; } 713 if( uniform ) 714 i = myBandwidthPriorityCombo->findData( baselineInt ); 715 else 716 i = -1; 717 setIfIdle( myBandwidthPriorityCombo, i ); 718 719 setIfIdle( mySingleDownSpin, int(tor->downloadLimit().KBps()) ); 720 setIfIdle( mySingleUpSpin, int(tor->uploadLimit().KBps()) ); 721 setIfIdle( myPeerLimitSpin, tor->peerLimit() ); 722 } 723 724 if( !torrents.empty( ) ) 725 { 726 const Torrent * tor; 727 728 // ratio 729 bool uniform = true; 730 int baselineInt = torrents[0]->seedRatioMode( ); 731 foreach( tor, torrents ) if( baselineInt != tor->seedRatioMode( ) ) { uniform = false; break; } 732 733 setIfIdle( myRatioCombo, uniform ? myRatioCombo->findData( baselineInt ) : -1 ); 734 myRatioSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) ); 735 736 setIfIdle( myRatioSpin, tor->seedRatioLimit( ) ); 737 738 // idle 739 uniform = true; 740 baselineInt = torrents[0]->seedIdleMode( ); 741 foreach( tor, torrents ) if( baselineInt != tor->seedIdleMode( ) ) { uniform = false; break; } 742 743 setIfIdle( myIdleCombo, uniform ? myIdleCombo->findData( baselineInt ) : -1 ); 744 myIdleSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) ); 745 746 setIfIdle( myIdleSpin, tor->seedIdleLimit( ) ); 747 } 748 749 /// 750 /// Tracker tab 751 /// 752 753 myTrackerModel->refresh( myModel, myIds ); 754 755 /// 756 /// Peers tab 757 /// 758 759 QMap<QString,QTreeWidgetItem*> peers2; 760 QList<QTreeWidgetItem*> newItems; 761 foreach( const Torrent * t, torrents ) 762 { 763 const QString idStr( QString::number( t->id( ) ) ); 764 PeerList peers = t->peers( ); 765 766 foreach( const Peer& peer, peers ) 767 { 768 const QString key = idStr + ":" + peer.address; 769 PeerItem * item = (PeerItem*) myPeers.value( key, 0 ); 770 771 if( item == 0 ) // new peer has connected 772 { 773 static const QIcon myEncryptionIcon( ":/icons/encrypted.png" ); 774 static const QIcon myEmptyIcon; 775 item = new PeerItem( peer ); 776 item->setTextAlignment( COL_UP, Qt::AlignRight|Qt::AlignVCenter ); 777 item->setTextAlignment( COL_DOWN, Qt::AlignRight|Qt::AlignVCenter ); 778 item->setTextAlignment( COL_PERCENT, Qt::AlignRight|Qt::AlignVCenter ); 779 item->setIcon( COL_LOCK, peer.isEncrypted ? myEncryptionIcon : myEmptyIcon ); 780 item->setToolTip( COL_LOCK, peer.isEncrypted ? tr( "Encrypted connection" ) : "" ); 781 item->setText( COL_ADDRESS, peer.address ); 782 item->setText( COL_CLIENT, peer.clientName ); 783 newItems << item; 784 } 785 786 const QString code = peer.flagStr; 787 item->setStatus( code ); 788 item->refresh( peer ); 789 790 QString codeTip; 791 foreach( QChar ch, code ) { 792 QString txt; 793 switch( ch.toAscii() ) { 794 case 'O': txt = tr( "Optimistic unchoke" ); break; 795 case 'D': txt = tr( "Downloading from this peer" ); break; 796 case 'd': txt = tr( "We would download from this peer if they would let us" ); break; 797 case 'U': txt = tr( "Uploading to peer" ); break; 798 case 'u': txt = tr( "We would upload to this peer if they asked" ); break; 799 case 'K': txt = tr( "Peer has unchoked us, but we're not interested" ); break; 800 case '?': txt = tr( "We unchoked this peer, but they're not interested" ); break; 801 case 'E': txt = tr( "Encrypted connection" ); break; 802 case 'H': txt = tr( "Peer was discovered through DHT" ); break; 803 case 'X': txt = tr( "Peer was discovered through Peer Exchange (PEX)" ); break; 804 case 'I': txt = tr( "Peer is an incoming connection" ); break; 805 case 'T': txt = tr( "Peer is connected over uTP" ); break; 806 } 807 if( !txt.isEmpty( ) ) 808 codeTip += QString("%1: %2\n").arg(ch).arg(txt); 809 } 810 811 if( !codeTip.isEmpty() ) 812 codeTip.resize( codeTip.size()-1 ); // eat the trailing linefeed 813 814 item->setText( COL_UP, peer.rateToPeer.isZero() ? "" : Formatter::speedToString( peer.rateToPeer ) ); 815 item->setText( COL_DOWN, peer.rateToClient.isZero() ? "" : Formatter::speedToString( peer.rateToClient ) ); 816 item->setText( COL_PERCENT, peer.progress > 0 ? QString( "%1%" ).arg( (int)( peer.progress * 100.0 ) ) : "" ); 817 item->setText( COL_STATUS, code ); 818 item->setToolTip( COL_STATUS, codeTip ); 819 820 peers2.insert( key, item ); 821 } 822 } 823 myPeerTree->addTopLevelItems( newItems ); 824 foreach( QString key, myPeers.keys() ) { 825 if( !peers2.contains( key ) ) { // old peer has disconnected 826 QTreeWidgetItem * item = myPeers.value( key, 0 ); 827 myPeerTree->takeTopLevelItem( myPeerTree->indexOfTopLevelItem( item ) ); 828 delete item; 829 } 830 } 831 myPeers = peers2; 832 833 if( single ) 834 myFileTreeView->update( torrents[0]->files( ) , myChangedTorrents ); 835 else 836 myFileTreeView->clear( ); 837 838 myChangedTorrents = false; 839 myHavePendingRefresh = false; 840 foreach( QWidget * w, myWidgets ) 841 w->setEnabled( true ); 842} 843 844void 845Details :: enableWhenChecked( QCheckBox * box, QWidget * w ) 846{ 847 connect( box, SIGNAL(toggled(bool)), w, SLOT(setEnabled(bool)) ); 848 w->setEnabled( box->isChecked( ) ); 849} 850 851 852/*** 853**** 854***/ 855 856QWidget * 857Details :: createInfoTab( ) 858{ 859 HIG * hig = new HIG( this ); 860 861 hig->addSectionTitle( tr( "Activity" ) ); 862 hig->addRow( tr( "Have:" ), myHaveLabel = new SqueezeLabel ); 863 hig->addRow( tr( "Availability:" ), myAvailabilityLabel = new SqueezeLabel ); 864 hig->addRow( tr( "Downloaded:" ), myDownloadedLabel = new SqueezeLabel ); 865 hig->addRow( tr( "Uploaded:" ), myUploadedLabel = new SqueezeLabel ); 866 hig->addRow( tr( "State:" ), myStateLabel = new SqueezeLabel ); 867 hig->addRow( tr( "Running time:" ), myRunTimeLabel = new SqueezeLabel ); 868 hig->addRow( tr( "Remaining time:" ), myETALabel = new SqueezeLabel ); 869 hig->addRow( tr( "Last activity:" ), myLastActivityLabel = new SqueezeLabel ); 870 hig->addRow( tr( "Error:" ), myErrorLabel = new SqueezeLabel ); 871 hig->addSectionDivider( ); 872 873 hig->addSectionDivider( ); 874 hig->addSectionTitle( tr( "Details" ) ); 875 hig->addRow( tr( "Size:" ), mySizeLabel = new SqueezeLabel ); 876 hig->addRow( tr( "Location:" ), myLocationLabel = new SqueezeLabel ); 877 hig->addRow( tr( "Hash:" ), myHashLabel = new SqueezeLabel ); 878 hig->addRow( tr( "Privacy:" ), myPrivacyLabel = new SqueezeLabel ); 879 hig->addRow( tr( "Origin:" ), myOriginLabel = new SqueezeLabel ); 880 myOriginLabel->setMinimumWidth( 325 ); // stop long origin strings from resizing the widgit 881 hig->addRow( tr( "Comment:" ), myCommentBrowser = new QTextBrowser ); 882 const int h = QFontMetrics(myCommentBrowser->font()).lineSpacing() * 4; 883 myCommentBrowser->setFixedHeight( h ); 884 885 hig->finish( ); 886 887 return hig; 888} 889 890/*** 891**** 892***/ 893 894void 895Details :: onShowTrackerScrapesToggled( bool val ) 896{ 897 myPrefs.set( Prefs::SHOW_TRACKER_SCRAPES, val ); 898} 899 900void 901Details :: onShowBackupTrackersToggled( bool val ) 902{ 903 myPrefs.set( Prefs::SHOW_BACKUP_TRACKERS, val ); 904} 905 906void 907Details :: onHonorsSessionLimitsToggled( bool val ) 908{ 909 mySession.torrentSet( myIds, "honorsSessionLimits", val ); 910 getNewData( ); 911} 912void 913Details :: onDownloadLimitedToggled( bool val ) 914{ 915 mySession.torrentSet( myIds, "downloadLimited", val ); 916 getNewData( ); 917} 918void 919Details :: onSpinBoxEditingFinished( ) 920{ 921 const QObject * spin = sender(); 922 const QString key = spin->property( PREF_KEY ).toString( ); 923 const QDoubleSpinBox * d = qobject_cast<const QDoubleSpinBox*>( spin ); 924 if( d ) 925 mySession.torrentSet( myIds, key, d->value( ) ); 926 else 927 mySession.torrentSet( myIds, key, qobject_cast<const QSpinBox*>(spin)->value( ) ); 928 getNewData( ); 929} 930 931void 932Details :: onUploadLimitedToggled( bool val ) 933{ 934 mySession.torrentSet( myIds, "uploadLimited", val ); 935 getNewData( ); 936} 937 938void 939Details :: onIdleModeChanged( int index ) 940{ 941 const int val = myIdleCombo->itemData( index ).toInt( ); 942 mySession.torrentSet( myIds, "seedIdleMode", val ); 943 getNewData( ); 944} 945 946void 947Details :: onRatioModeChanged( int index ) 948{ 949 const int val = myRatioCombo->itemData( index ).toInt( ); 950 mySession.torrentSet( myIds, "seedRatioMode", val ); 951} 952 953void 954Details :: onBandwidthPriorityChanged( int index ) 955{ 956 if( index != -1 ) 957 { 958 const int priority = myBandwidthPriorityCombo->itemData(index).toInt( ); 959 mySession.torrentSet( myIds, "bandwidthPriority", priority ); 960 getNewData( ); 961 } 962} 963 964void 965Details :: onTrackerSelectionChanged( ) 966{ 967 const int selectionCount = myTrackerView->selectionModel()->selectedRows().size(); 968 myEditTrackerButton->setEnabled( selectionCount == 1 ); 969 myRemoveTrackerButton->setEnabled( selectionCount > 0 ); 970} 971 972void 973Details :: onAddTrackerClicked( ) 974{ 975 bool ok = false; 976 const QString url = QInputDialog::getText( this, 977 tr( "Add URL " ), 978 tr( "Add tracker announce URL:" ), 979 QLineEdit::Normal, QString(), &ok ); 980 if( !ok ) 981 { 982 // user pressed "cancel" -- noop 983 } 984 else if( !QUrl(url).isValid( ) ) 985 { 986 QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( url ) ); 987 } 988 else 989 { 990 QSet<int> ids; 991 992 foreach( int id, myIds ) 993 if( myTrackerModel->find( id, url ) == -1 ) 994 ids.insert( id ); 995 996 if( ids.empty( ) ) // all the torrents already have this tracker 997 { 998 QMessageBox::warning( this, tr( "Error" ), tr( "Tracker already exists." ) ); 999 } 1000 else 1001 { 1002 QStringList urls; 1003 urls << url; 1004 mySession.torrentSet( ids, "trackerAdd", urls ); 1005 getNewData( ); 1006 } 1007 } 1008} 1009 1010void 1011Details :: onEditTrackerClicked( ) 1012{ 1013 QItemSelectionModel * selectionModel = myTrackerView->selectionModel( ); 1014 QModelIndexList selectedRows = selectionModel->selectedRows( ); 1015 assert( selectedRows.size( ) == 1 ); 1016 QModelIndex i = selectionModel->currentIndex( ); 1017 const TrackerInfo trackerInfo = myTrackerView->model()->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>(); 1018 1019 bool ok = false; 1020 const QString newval = QInputDialog::getText( this, 1021 tr( "Edit URL " ), 1022 tr( "Edit tracker announce URL:" ), 1023 QLineEdit::Normal, 1024 trackerInfo.st.announce, &ok ); 1025 1026 if( !ok ) 1027 { 1028 // user pressed "cancel" -- noop 1029 } 1030 else if( !QUrl(newval).isValid( ) ) 1031 { 1032 QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( newval ) ); 1033 } 1034 else 1035 { 1036 QSet<int> ids; 1037 ids << trackerInfo.torrentId; 1038 1039 const QPair<int,QString> idUrl = qMakePair( trackerInfo.st.id, newval ); 1040 1041 mySession.torrentSet( ids, "trackerReplace", idUrl ); 1042 getNewData( ); 1043 } 1044} 1045 1046void 1047Details :: onRemoveTrackerClicked( ) 1048{ 1049 // make a map of torrentIds to announce URLs to remove 1050 QItemSelectionModel * selectionModel = myTrackerView->selectionModel( ); 1051 QModelIndexList selectedRows = selectionModel->selectedRows( ); 1052 QMap<int,int> torrentId_to_trackerIds; 1053 foreach( QModelIndex i, selectedRows ) 1054 { 1055 const TrackerInfo inf = myTrackerView->model()->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>(); 1056 torrentId_to_trackerIds.insertMulti( inf.torrentId, inf.st.id ); 1057 } 1058 1059 // batch all of a tracker's torrents into one command 1060 foreach( int id, torrentId_to_trackerIds.uniqueKeys( ) ) 1061 { 1062 QSet<int> ids; 1063 ids << id; 1064 mySession.torrentSet( ids, "trackerRemove", torrentId_to_trackerIds.values( id ) ); 1065 } 1066 1067 selectionModel->clearSelection( ); 1068 getNewData( ); 1069} 1070 1071QWidget * 1072Details :: createOptionsTab( ) 1073{ 1074 QSpinBox * s; 1075 QCheckBox * c; 1076 QComboBox * m; 1077 QHBoxLayout * h; 1078 QDoubleSpinBox * ds; 1079 const QString speed_K_str = Formatter::unitStr( Formatter::SPEED, Formatter::KB ); 1080 1081 HIG * hig = new HIG( this ); 1082 hig->addSectionTitle( tr( "Speed" ) ); 1083 1084 c = new QCheckBox( tr( "Honor global &limits" ) ); 1085 mySessionLimitCheck = c; 1086 hig->addWideControl( c ); 1087 connect( c, SIGNAL(clicked(bool)), this, SLOT(onHonorsSessionLimitsToggled(bool)) ); 1088 1089 c = new QCheckBox( tr( "Limit &download speed (%1):" ).arg( speed_K_str ) ); 1090 mySingleDownCheck = c; 1091 s = new QSpinBox( ); 1092 s->setProperty( PREF_KEY, QString( "downloadLimit" ) ); 1093 s->setSingleStep( 5 ); 1094 s->setRange( 0, INT_MAX ); 1095 mySingleDownSpin = s; 1096 hig->addRow( c, s ); 1097 enableWhenChecked( c, s ); 1098 connect( c, SIGNAL(clicked(bool)), this, SLOT(onDownloadLimitedToggled(bool)) ); 1099 connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); 1100 1101 c = new QCheckBox( tr( "Limit &upload speed (%1):" ).arg( speed_K_str ) ); 1102 mySingleUpCheck = c; 1103 s = new QSpinBox( ); 1104 s->setSingleStep( 5 ); 1105 s->setRange( 0, INT_MAX ); 1106 s->setProperty( PREF_KEY, QString( "uploadLimit" ) ); 1107 mySingleUpSpin = s; 1108 hig->addRow( c, s ); 1109 enableWhenChecked( c, s ); 1110 connect( c, SIGNAL(clicked(bool)), this, SLOT(onUploadLimitedToggled(bool)) ); 1111 connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); 1112 1113 m = new QComboBox; 1114 m->addItem( tr( "High" ), TR_PRI_HIGH ); 1115 m->addItem( tr( "Normal" ), TR_PRI_NORMAL ); 1116 m->addItem( tr( "Low" ), TR_PRI_LOW ); 1117 connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onBandwidthPriorityChanged(int))); 1118 hig->addRow( tr( "Torrent &priority:" ), m ); 1119 myBandwidthPriorityCombo = m; 1120 1121 hig->addSectionDivider( ); 1122 hig->addSectionTitle( tr( "Seeding Limits" ) ); 1123 1124 h = new QHBoxLayout( ); 1125 h->setSpacing( HIG :: PAD ); 1126 m = new QComboBox; 1127 m->addItem( tr( "Use Global Settings" ), TR_RATIOLIMIT_GLOBAL ); 1128 m->addItem( tr( "Seed regardless of ratio" ), TR_RATIOLIMIT_UNLIMITED ); 1129 m->addItem( tr( "Stop seeding at ratio:" ), TR_RATIOLIMIT_SINGLE ); 1130 connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onRatioModeChanged(int))); 1131 h->addWidget( myRatioCombo = m ); 1132 ds = new QDoubleSpinBox( ); 1133 ds->setRange( 0.5, INT_MAX ); 1134 ds->setProperty( PREF_KEY, QString( "seedRatioLimit" ) ); 1135 connect( ds, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); 1136 h->addWidget( myRatioSpin = ds ); 1137 hig->addRow( tr( "&Ratio:" ), h, m ); 1138 1139 h = new QHBoxLayout( ); 1140 h->setSpacing( HIG :: PAD ); 1141 m = new QComboBox; 1142 m->addItem( tr( "Use Global Settings" ), TR_IDLELIMIT_GLOBAL ); 1143 m->addItem( tr( "Seed regardless of activity" ), TR_IDLELIMIT_UNLIMITED ); 1144 m->addItem( tr( "Stop seeding if idle for N minutes:" ), TR_IDLELIMIT_SINGLE ); 1145 connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onIdleModeChanged(int))); 1146 h->addWidget( myIdleCombo = m ); 1147 s = new QSpinBox( ); 1148 s->setSingleStep( 5 ); 1149 s->setRange( 1, 9999 ); 1150 s->setProperty( PREF_KEY, QString( "seedIdleLimit" ) ); 1151 connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); 1152 h->addWidget( myIdleSpin = s ); 1153 hig->addRow( tr( "&Idle:" ), h, m ); 1154 1155 1156 hig->addSectionDivider( ); 1157 hig->addSectionTitle( tr( "Peer Connections" ) ); 1158 1159 s = new QSpinBox( ); 1160 s->setSingleStep( 5 ); 1161 s->setRange( 1, 300 ); 1162 s->setProperty( PREF_KEY, QString( "peer-limit" ) ); 1163 connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); 1164 myPeerLimitSpin = s; 1165 hig->addRow( tr( "&Maximum peers:" ), s ); 1166 1167 hig->finish( ); 1168 1169 return hig; 1170} 1171 1172/*** 1173**** 1174***/ 1175 1176QWidget * 1177Details :: createTrackerTab( ) 1178{ 1179 QCheckBox * c; 1180 QPushButton * p; 1181 QWidget * top = new QWidget; 1182 QVBoxLayout * v = new QVBoxLayout( top ); 1183 QHBoxLayout * h = new QHBoxLayout(); 1184 QVBoxLayout * v2 = new QVBoxLayout(); 1185 1186 v->setSpacing( HIG::PAD_BIG ); 1187 v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG ); 1188 1189 h->setSpacing( HIG::PAD ); 1190 h->setContentsMargins( HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL ); 1191 1192 v2->setSpacing( HIG::PAD ); 1193 1194 myTrackerModel = new TrackerModel; 1195 myTrackerFilter = new TrackerModelFilter; 1196 myTrackerFilter->setSourceModel( myTrackerModel ); 1197 myTrackerView = new QTreeView; 1198 myTrackerView->setModel( myTrackerFilter ); 1199 myTrackerView->setHeaderHidden( true ); 1200 myTrackerView->setSelectionMode( QTreeWidget::ExtendedSelection ); 1201 myTrackerView->setRootIsDecorated( false ); 1202 myTrackerView->setIndentation( 2 ); 1203 myTrackerView->setItemsExpandable( false ); 1204 myTrackerView->setAlternatingRowColors( true ); 1205 myTrackerView->setItemDelegate( myTrackerDelegate = new TrackerDelegate( ) ); 1206 connect( myTrackerView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(onTrackerSelectionChanged())); 1207 h->addWidget( myTrackerView, 1 ); 1208 1209 p = new QPushButton(); 1210 p->setIcon( getStockIcon( "list-add", QStyle::SP_DialogOpenButton ) ); 1211 p->setToolTip( "Add Tracker" ); 1212 myAddTrackerButton = p; 1213 v2->addWidget( p, 1 ); 1214 connect( p, SIGNAL(clicked(bool)), this, SLOT(onAddTrackerClicked())); 1215 1216 p = new QPushButton(); 1217 p->setIcon( getStockIcon( "document-properties", QStyle::SP_DesktopIcon ) ); 1218 p->setToolTip( "Edit Tracker" ); 1219 myAddTrackerButton = p; 1220 p->setEnabled( false ); 1221 myEditTrackerButton = p; 1222 v2->addWidget( p, 1 ); 1223 connect( p, SIGNAL(clicked(bool)), this, SLOT(onEditTrackerClicked())); 1224 1225 p = new QPushButton(); 1226 p->setIcon( getStockIcon( "list-remove", QStyle::SP_TrashIcon ) ); 1227 p->setToolTip( "Remove Trackers" ); 1228 p->setEnabled( false ); 1229 myRemoveTrackerButton = p; 1230 v2->addWidget( p, 1 ); 1231 connect( p, SIGNAL(clicked(bool)), this, SLOT(onRemoveTrackerClicked())); 1232 1233 v2->addStretch( 1 ); 1234 1235 h->addLayout( v2, 1 ); 1236 h->setStretch( 1, 0 ); 1237 1238 v->addLayout( h, 1 ); 1239 1240 c = new QCheckBox( tr( "Show &more details" ) ); 1241 c->setChecked( myPrefs.getBool( Prefs::SHOW_TRACKER_SCRAPES ) ); 1242 myShowTrackerScrapesCheck = c; 1243 v->addWidget( c, 1 ); 1244 connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowTrackerScrapesToggled(bool)) ); 1245 1246 c = new QCheckBox( tr( "Show &backup trackers" ) ); 1247 c->setChecked( myPrefs.getBool( Prefs::SHOW_BACKUP_TRACKERS ) ); 1248 myShowBackupTrackersCheck = c; 1249 v->addWidget( c, 1 ); 1250 connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowBackupTrackersToggled(bool)) ); 1251 1252 return top; 1253} 1254 1255/*** 1256**** 1257***/ 1258 1259QWidget * 1260Details :: createPeersTab( ) 1261{ 1262 QWidget * top = new QWidget; 1263 QVBoxLayout * v = new QVBoxLayout( top ); 1264 v->setSpacing( HIG :: PAD_BIG ); 1265 v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG ); 1266 1267 QStringList headers; 1268 headers << QString() << tr("Up") << tr("Down") << tr("%") << tr("Status") << tr("Address") << tr("Client"); 1269 myPeerTree = new QTreeWidget; 1270 myPeerTree->setUniformRowHeights( true ); 1271 myPeerTree->setHeaderLabels( headers ); 1272 myPeerTree->setColumnWidth( 0, 20 ); 1273 myPeerTree->setSortingEnabled( true ); 1274 myPeerTree->sortByColumn( COL_ADDRESS, Qt::AscendingOrder ); 1275 myPeerTree->setRootIsDecorated( false ); 1276 myPeerTree->setTextElideMode( Qt::ElideRight ); 1277 v->addWidget( myPeerTree, 1 ); 1278 1279 const QFontMetrics m( font( ) ); 1280 QSize size = m.size( 0, "1024 MiB/s" ); 1281 myPeerTree->setColumnWidth( COL_UP, size.width( ) ); 1282 myPeerTree->setColumnWidth( COL_DOWN, size.width( ) ); 1283 size = m.size( 0, " 100% " ); 1284 myPeerTree->setColumnWidth( COL_PERCENT, size.width( ) ); 1285 size = m.size( 0, "ODUK?EXI" ); 1286 myPeerTree->setColumnWidth( COL_STATUS, size.width( ) ); 1287 size = m.size( 0, "888.888.888.888" ); 1288 myPeerTree->setColumnWidth( COL_ADDRESS, size.width( ) ); 1289 size = m.size( 0, "Some BitTorrent Client" ); 1290 myPeerTree->setColumnWidth( COL_CLIENT, size.width( ) ); 1291 myPeerTree->setAlternatingRowColors( true ); 1292 1293 return top; 1294} 1295 1296/*** 1297**** 1298***/ 1299 1300QWidget * 1301Details :: createFilesTab( ) 1302{ 1303 myFileTreeView = new FileTreeView( ); 1304 1305 connect( myFileTreeView, SIGNAL( priorityChanged(const QSet<int>&, int)), 1306 this, SLOT( onFilePriorityChanged(const QSet<int>&, int))); 1307 1308 connect( myFileTreeView, SIGNAL( wantedChanged(const QSet<int>&, bool)), 1309 this, SLOT( onFileWantedChanged(const QSet<int>&, bool))); 1310 1311 return myFileTreeView; 1312} 1313 1314void 1315Details :: onFilePriorityChanged( const QSet<int>& indices, int priority ) 1316{ 1317 QString key; 1318 switch( priority ) { 1319 case TR_PRI_LOW: key = "priority-low"; break; 1320 case TR_PRI_HIGH: key = "priority-high"; break; 1321 default: key = "priority-normal"; break; 1322 } 1323 mySession.torrentSet( myIds, key, indices.toList( ) ); 1324 getNewData( ); 1325} 1326 1327void 1328Details :: onFileWantedChanged( const QSet<int>& indices, bool wanted ) 1329{ 1330 QString key( wanted ? "files-wanted" : "files-unwanted" ); 1331 mySession.torrentSet( myIds, key, indices.toList( ) ); 1332 getNewData( ); 1333} 1334