Transmission: update from 2.42 to 2.50
[tomato.git] / release / src / router / transmission / qt / details.cc
blob68c2a8aabdac82a8eda398b2ddb5cd6cc3d44205
1 /*
2 * This file Copyright (C) Mnemosyne LLC
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2
6 * as published by the Free Software Foundation.
8 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
10 * $Id: details.cc 13076 2011-11-05 15:45:38Z jordan $
13 #include <cassert>
14 #include <ctime>
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>
46 #include <libtransmission/transmission.h>
47 #include <libtransmission/bencode.h>
48 #include <libtransmission/utils.h> // tr_getRatio()
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"
63 class Prefs;
64 class Session;
66 /****
67 *****
68 ****/
70 namespace
72 const int REFRESH_INTERVAL_MSEC = 4000;
74 const char * PREF_KEY( "pref-key" );
76 enum // peer columns
78 COL_LOCK,
79 COL_UP,
80 COL_DOWN,
81 COL_PERCENT,
82 COL_STATUS,
83 COL_ADDRESS,
84 COL_CLIENT,
85 N_COLUMNS
89 /***
90 ****
91 ***/
93 class PeerItem: public QTreeWidgetItem
95 Peer peer;
96 QString collatedAddress;
97 QString status;
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;
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;
128 /***
129 ****
130 ***/
132 QIcon
133 Details :: getStockIcon( const QString& freedesktop_name, int fallback )
135 QIcon icon = QIcon::fromTheme( freedesktop_name );
137 if( icon.isNull( ) )
138 icon = style()->standardIcon( QStyle::StandardPixmap( fallback ), 0, this );
140 return icon;
143 Details :: 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 )
151 QVBoxLayout * layout = new QVBoxLayout( this );
153 setWindowTitle( tr( "Torrent Properties" ) );
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 );
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 );
174 QList<int> initKeys;
175 initKeys << Prefs :: SHOW_TRACKER_SCRAPES
176 << Prefs :: SHOW_BACKUP_TRACKERS;
177 foreach( int key, initKeys )
178 refreshPref( key );
180 connect( &myTimer, SIGNAL(timeout()), this, SLOT(onTimer()));
181 connect( &myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(int)) );
183 onTimer( );
184 myTimer.setSingleShot( false );
185 myTimer.start( REFRESH_INTERVAL_MSEC );
188 Details :: ~Details( )
190 myTrackerDelegate->deleteLater();
191 myTrackerFilter->deleteLater();
192 myTrackerModel->deleteLater();
195 void
196 Details :: setIds( const QSet<int>& ids )
198 if( ids == myIds )
199 return;
201 myChangedTorrents = true;
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()) );
210 myFileTreeView->clear( );
211 myIds = ids;
212 myTrackerModel->refresh( myModel, myIds );
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()) );
221 foreach( QWidget * w, myWidgets )
222 w->setEnabled( false );
224 onTimer( );
227 void
228 Details :: refreshPref( int key )
230 QString str;
232 switch( key )
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;
246 case Prefs :: SHOW_BACKUP_TRACKERS:
247 myTrackerFilter->setShowBackupTrackers( myPrefs.getBool( key ) );
248 break;
250 default:
251 break;
256 /***
257 ****
258 ***/
260 QString
261 Details :: timeToStringRounded( int seconds )
263 if( seconds > 60 ) seconds -= ( seconds % 60 );
264 return Formatter::timeToString ( seconds );
267 void
268 Details :: onTimer( )
270 getNewData( );
273 void
274 Details :: getNewData( )
276 if( !myIds.empty( ) )
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() );
284 if( !infos.isEmpty() )
285 mySession.initTorrents( infos );
286 mySession.refreshExtraStats( myIds );
290 void
291 Details :: onTorrentChanged( )
293 if( !myHavePendingRefresh ) {
294 myHavePendingRefresh = true;
295 QTimer::singleShot( 100, this, SLOT(refresh()));
299 namespace
301 void setIfIdle( QComboBox * box, int i )
303 if( !box->hasFocus( ) )
305 box->blockSignals( true );
306 box->setCurrentIndex( i );
307 box->blockSignals( false );
311 void setIfIdle( QDoubleSpinBox * spin, double value )
313 if( !spin->hasFocus( ) )
315 spin->blockSignals( true );
316 spin->setValue( value );
317 spin->blockSignals( false );
321 void setIfIdle( QSpinBox * spin, int value )
323 if( !spin->hasFocus( ) )
325 spin->blockSignals( true );
326 spin->setValue( value );
327 spin->blockSignals( false );
332 void
333 Details :: refresh( )
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" );
345 // build a list of torrents
346 foreach( int id, myIds ) {
347 const Torrent * tor = myModel.getTorrentFromId( id );
348 if( tor )
349 torrents << tor;
353 /// activity tab
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;
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( );
382 myStateLabel->setText( string );
383 const QString stateString = string;
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();
410 const double d = 100.0 * ( sizeWhenDone ? ( sizeWhenDone - leftUntilDone ) / sizeWhenDone : 1 );
411 QString pct = Formatter::percentToString( d );
413 if( !haveUnverified && !leftUntilDone )
415 string = tr( "%1 (100%)" )
416 .arg( Formatter::sizeToString( haveVerified ) );
418 else if( !haveUnverified )
420 string = tr( "%1 of %2 (%3%)" )
421 .arg( Formatter::sizeToString( haveVerified ) )
422 .arg( Formatter::sizeToString( sizeWhenDone ) )
423 .arg( pct );
425 else
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 ) );
435 myHaveLabel->setText( string );
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 ) );
446 myAvailabilityLabel->setText( string );
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( );
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;
465 myDownloadedLabel->setText( string );
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( );
476 string = tr( "%1 (Ratio: %2)" )
477 .arg( Formatter::sizeToString( u ) )
478 .arg( Formatter::ratioToString( tr_getRatio( u, d ) ) );
480 myUploadedLabel->setText( string );
482 const QDateTime qdt_now = QDateTime::currentDateTime( );
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;
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 ) );
503 myRunTimeLabel->setText( string );
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;
518 if( string.isEmpty( ) ) {
519 if( baseline < 0 )
520 string = tr( "Unknown" );
521 else
522 string = Formatter::timeToString( baseline );
525 myETALabel->setText( string );
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;
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 ) );
546 myLastActivityLabel->setText( string );
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;
560 if( string.isEmpty( ) )
561 string = none;
562 myErrorLabel->setText( string );
566 /// information tab
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;
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 ) );
592 mySizeLabel->setText( string );
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;
606 myHashLabel->setText( string );
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;
622 myPrivacyLabel->setText( string );
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;
636 myCommentBrowser->setText( string );
637 myCommentBrowser->setMaximumHeight( QWIDGETSIZE_MAX );
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() );
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 );
661 myOriginLabel->setText( string );
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;
675 myLocationLabel->setText( string );
679 /// Options Tab
682 if( myChangedTorrents && !torrents.empty( ) )
684 int i;
685 const Torrent * baseline = *torrents.begin();
686 const Torrent * tor;
687 bool uniform;
688 bool baselineFlag;
689 int baselineInt;
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 );
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 );
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 );
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 );
719 setIfIdle( mySingleDownSpin, int(tor->downloadLimit().KBps()) );
720 setIfIdle( mySingleUpSpin, int(tor->uploadLimit().KBps()) );
721 setIfIdle( myPeerLimitSpin, tor->peerLimit() );
724 if( !torrents.empty( ) )
726 const Torrent * tor;
728 // ratio
729 bool uniform = true;
730 int baselineInt = torrents[0]->seedRatioMode( );
731 foreach( tor, torrents ) if( baselineInt != tor->seedRatioMode( ) ) { uniform = false; break; }
733 setIfIdle( myRatioCombo, uniform ? myRatioCombo->findData( baselineInt ) : -1 );
734 myRatioSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) );
736 setIfIdle( myRatioSpin, tor->seedRatioLimit( ) );
738 // idle
739 uniform = true;
740 baselineInt = torrents[0]->seedIdleMode( );
741 foreach( tor, torrents ) if( baselineInt != tor->seedIdleMode( ) ) { uniform = false; break; }
743 setIfIdle( myIdleCombo, uniform ? myIdleCombo->findData( baselineInt ) : -1 );
744 myIdleSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) );
746 setIfIdle( myIdleSpin, tor->seedIdleLimit( ) );
750 /// Tracker tab
753 myTrackerModel->refresh( myModel, myIds );
756 /// Peers tab
759 QMap<QString,QTreeWidgetItem*> peers2;
760 QList<QTreeWidgetItem*> newItems;
761 foreach( const Torrent * t, torrents )
763 const QString idStr( QString::number( t->id( ) ) );
764 PeerList peers = t->peers( );
766 foreach( const Peer& peer, peers )
768 const QString key = idStr + ":" + peer.address;
769 PeerItem * item = (PeerItem*) myPeers.value( key, 0 );
771 if( item == 0 ) // new peer has connected
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;
786 const QString code = peer.flagStr;
787 item->setStatus( code );
788 item->refresh( peer );
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;
807 if( !txt.isEmpty( ) )
808 codeTip += QString("%1: %2\n").arg(ch).arg(txt);
811 if( !codeTip.isEmpty() )
812 codeTip.resize( codeTip.size()-1 ); // eat the trailing linefeed
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 );
820 peers2.insert( key, item );
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;
831 myPeers = peers2;
833 if( single )
834 myFileTreeView->update( torrents[0]->files( ) , myChangedTorrents );
835 else
836 myFileTreeView->clear( );
838 myChangedTorrents = false;
839 myHavePendingRefresh = false;
840 foreach( QWidget * w, myWidgets )
841 w->setEnabled( true );
844 void
845 Details :: enableWhenChecked( QCheckBox * box, QWidget * w )
847 connect( box, SIGNAL(toggled(bool)), w, SLOT(setEnabled(bool)) );
848 w->setEnabled( box->isChecked( ) );
852 /***
853 ****
854 ***/
856 QWidget *
857 Details :: createInfoTab( )
859 HIG * hig = new HIG( this );
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( );
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 );
885 hig->finish( );
887 return hig;
890 /***
891 ****
892 ***/
894 void
895 Details :: onShowTrackerScrapesToggled( bool val )
897 myPrefs.set( Prefs::SHOW_TRACKER_SCRAPES, val );
900 void
901 Details :: onShowBackupTrackersToggled( bool val )
903 myPrefs.set( Prefs::SHOW_BACKUP_TRACKERS, val );
906 void
907 Details :: onHonorsSessionLimitsToggled( bool val )
909 mySession.torrentSet( myIds, "honorsSessionLimits", val );
910 getNewData( );
912 void
913 Details :: onDownloadLimitedToggled( bool val )
915 mySession.torrentSet( myIds, "downloadLimited", val );
916 getNewData( );
918 void
919 Details :: onSpinBoxEditingFinished( )
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( );
931 void
932 Details :: onUploadLimitedToggled( bool val )
934 mySession.torrentSet( myIds, "uploadLimited", val );
935 getNewData( );
938 void
939 Details :: onIdleModeChanged( int index )
941 const int val = myIdleCombo->itemData( index ).toInt( );
942 mySession.torrentSet( myIds, "seedIdleMode", val );
943 getNewData( );
946 void
947 Details :: onRatioModeChanged( int index )
949 const int val = myRatioCombo->itemData( index ).toInt( );
950 mySession.torrentSet( myIds, "seedRatioMode", val );
953 void
954 Details :: onBandwidthPriorityChanged( int index )
956 if( index != -1 )
958 const int priority = myBandwidthPriorityCombo->itemData(index).toInt( );
959 mySession.torrentSet( myIds, "bandwidthPriority", priority );
960 getNewData( );
964 void
965 Details :: onTrackerSelectionChanged( )
967 const int selectionCount = myTrackerView->selectionModel()->selectedRows().size();
968 myEditTrackerButton->setEnabled( selectionCount == 1 );
969 myRemoveTrackerButton->setEnabled( selectionCount > 0 );
972 void
973 Details :: onAddTrackerClicked( )
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 )
982 // user pressed "cancel" -- noop
984 else if( !QUrl(url).isValid( ) )
986 QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( url ) );
988 else
990 QSet<int> ids;
992 foreach( int id, myIds )
993 if( myTrackerModel->find( id, url ) == -1 )
994 ids.insert( id );
996 if( ids.empty( ) ) // all the torrents already have this tracker
998 QMessageBox::warning( this, tr( "Error" ), tr( "Tracker already exists." ) );
1000 else
1002 QStringList urls;
1003 urls << url;
1004 mySession.torrentSet( ids, "trackerAdd", urls );
1005 getNewData( );
1010 void
1011 Details :: onEditTrackerClicked( )
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>();
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 );
1026 if( !ok )
1028 // user pressed "cancel" -- noop
1030 else if( !QUrl(newval).isValid( ) )
1032 QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( newval ) );
1034 else
1036 QSet<int> ids;
1037 ids << trackerInfo.torrentId;
1039 const QPair<int,QString> idUrl = qMakePair( trackerInfo.st.id, newval );
1041 mySession.torrentSet( ids, "trackerReplace", idUrl );
1042 getNewData( );
1046 void
1047 Details :: onRemoveTrackerClicked( )
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 )
1055 const TrackerInfo inf = myTrackerView->model()->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>();
1056 torrentId_to_trackerIds.insertMulti( inf.torrentId, inf.st.id );
1059 // batch all of a tracker's torrents into one command
1060 foreach( int id, torrentId_to_trackerIds.uniqueKeys( ) )
1062 QSet<int> ids;
1063 ids << id;
1064 mySession.torrentSet( ids, "trackerRemove", torrentId_to_trackerIds.values( id ) );
1067 selectionModel->clearSelection( );
1068 getNewData( );
1071 QWidget *
1072 Details :: createOptionsTab( )
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 );
1081 HIG * hig = new HIG( this );
1082 hig->addSectionTitle( tr( "Speed" ) );
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)) );
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()));
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()));
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;
1121 hig->addSectionDivider( );
1122 hig->addSectionTitle( tr( "Seeding Limits" ) );
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 );
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 );
1156 hig->addSectionDivider( );
1157 hig->addSectionTitle( tr( "Peer Connections" ) );
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 );
1167 hig->finish( );
1169 return hig;
1172 /***
1173 ****
1174 ***/
1176 QWidget *
1177 Details :: createTrackerTab( )
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();
1186 v->setSpacing( HIG::PAD_BIG );
1187 v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG );
1189 h->setSpacing( HIG::PAD );
1190 h->setContentsMargins( HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL );
1192 v2->setSpacing( HIG::PAD );
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 );
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()));
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()));
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()));
1233 v2->addStretch( 1 );
1235 h->addLayout( v2, 1 );
1236 h->setStretch( 1, 0 );
1238 v->addLayout( h, 1 );
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)) );
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)) );
1252 return top;
1255 /***
1256 ****
1257 ***/
1259 QWidget *
1260 Details :: createPeersTab( )
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 );
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 );
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 );
1293 return top;
1296 /***
1297 ****
1298 ***/
1300 QWidget *
1301 Details :: createFilesTab( )
1303 myFileTreeView = new FileTreeView( );
1305 connect( myFileTreeView, SIGNAL( priorityChanged(const QSet<int>&, int)),
1306 this, SLOT( onFilePriorityChanged(const QSet<int>&, int)));
1308 connect( myFileTreeView, SIGNAL( wantedChanged(const QSet<int>&, bool)),
1309 this, SLOT( onFileWantedChanged(const QSet<int>&, bool)));
1311 return myFileTreeView;
1314 void
1315 Details :: onFilePriorityChanged( const QSet<int>& indices, int priority )
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;
1323 mySession.torrentSet( myIds, key, indices.toList( ) );
1324 getNewData( );
1327 void
1328 Details :: onFileWantedChanged( const QSet<int>& indices, bool wanted )
1330 QString key( wanted ? "files-wanted" : "files-unwanted" );
1331 mySession.torrentSet( myIds, key, indices.toList( ) );
1332 getNewData( );