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 $
19 #include <QDialogButtonBox>
20 #include <QDoubleSpinBox>
23 #include <QFontMetrics>
24 #include <QHBoxLayout>
25 #include <QHBoxLayout>
26 #include <QHeaderView>
27 #include <QInputDialog>
28 #include <QItemSelectionModel>
32 #include <QMessageBox>
33 #include <QPushButton>
34 #include <QRadioButton>
35 #include <QResizeEvent>
37 #include <QStringList>
40 #include <QTextBrowser>
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()
51 #include "file-tree.h"
52 #include "formatter.h"
56 #include "squeezelabel.h"
58 #include "torrent-model.h"
59 #include "tracker-delegate.h"
60 #include "tracker-model.h"
61 #include "tracker-model-filter.h"
72 const int REFRESH_INTERVAL_MSEC
= 4000;
74 const char * PREF_KEY( "pref-key" );
93 class PeerItem
: public QTreeWidgetItem
96 QString collatedAddress
;
100 virtual ~PeerItem( ) { }
101 PeerItem( const Peer
& p
) {
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] );
107 collatedAddress
= p
.address
;
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;
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
;
133 Details :: getStockIcon( const QString
& freedesktop_name
, int fallback
)
135 QIcon icon
= QIcon::fromTheme( freedesktop_name
);
138 icon
= style()->standardIcon( QStyle::StandardPixmap( fallback
), 0, this );
143 Details :: Details( Session
& session
, Prefs
& prefs
, TorrentModel
& model
, QWidget
* parent
):
144 QDialog( parent
, Qt::Dialog
),
145 mySession( session
),
148 myChangedTorrents( false ),
149 myHavePendingRefresh( false )
151 QVBoxLayout
* layout
= new QVBoxLayout( this );
153 setWindowTitle( tr( "Torrent Properties" ) );
155 QTabWidget
* t
= new QTabWidget( this );
157 t
->addTab( w
= createInfoTab( ), tr( "Information" ) );
159 t
->addTab( w
= createPeersTab( ), tr( "Peers" ) );
161 t
->addTab( w
= createTrackerTab( ), tr( "Tracker" ) );
163 t
->addTab( w
= createFilesTab( ), tr( "Files" ) );
165 t
->addTab( w
= createOptionsTab( ), tr( "Options" ) );
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 );
175 initKeys
<< Prefs :: SHOW_TRACKER_SCRAPES
176 << Prefs :: SHOW_BACKUP_TRACKERS
;
177 foreach( int key
, initKeys
)
180 connect( &myTimer
, SIGNAL(timeout()), this, SLOT(onTimer()));
181 connect( &myPrefs
, SIGNAL(changed(int)), this, SLOT(refreshPref(int)) );
184 myTimer
.setSingleShot( false );
185 myTimer
.start( REFRESH_INTERVAL_MSEC
);
188 Details :: ~Details( )
190 myTrackerDelegate
->deleteLater();
191 myTrackerFilter
->deleteLater();
192 myTrackerModel
->deleteLater();
196 Details :: setIds( const QSet
<int>& ids
)
201 myChangedTorrents
= true;
203 // stop listening to the old torrents
204 foreach( int id
, myIds
) {
205 const Torrent
* tor
= myModel
.getTorrentFromId( id
);
207 disconnect( tor
, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) );
210 myFileTreeView
->clear( );
212 myTrackerModel
->refresh( myModel
, myIds
);
214 // listen to the new torrents
215 foreach( int id
, myIds
) {
216 const Torrent
* tor
= myModel
.getTorrentFromId( id
);
218 connect( tor
, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) );
221 foreach( QWidget
* w
, myWidgets
)
222 w
->setEnabled( false );
228 Details :: refreshPref( int 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
);
246 case Prefs :: SHOW_BACKUP_TRACKERS
:
247 myTrackerFilter
->setShowBackupTrackers( myPrefs
.getBool( key
) );
261 Details :: timeToStringRounded( int seconds
)
263 if( seconds
> 60 ) seconds
-= ( seconds
% 60 );
264 return Formatter::timeToString ( seconds
);
268 Details :: onTimer( )
274 Details :: getNewData( )
276 if( !myIds
.empty( ) )
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
);
291 Details :: onTorrentChanged( )
293 if( !myHavePendingRefresh
) {
294 myHavePendingRefresh
= true;
295 QTimer::singleShot( 100, this, SLOT(refresh()));
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 );
333 Details :: refresh( )
335 const int n
= myIds
.size( );
336 const bool single
= n
== 1;
338 const QFontMetrics
fm( fontMetrics( ) );
339 QList
<const Torrent
*> torrents
;
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
);
357 if( torrents
.empty( ) )
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
)
368 if( activity
!= TR_STATUS_STOPPED
)
369 allPaused
= allFinished
= false;
370 if( !t
->isFinished( ) )
375 else if( allFinished
)
376 string
= tr( "Finished" );
378 string
= tr( "Paused" );
380 string
= torrents
[0]->activityString( );
382 myStateLabel
->setText( string
);
383 const QString stateString
= string
;
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( ) )
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( );
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
) )
427 string
= tr( "%1 of %2 (%3%), %4 Unverified" )
428 .arg( Formatter::sizeToString( haveVerified
+ haveUnverified
) )
429 .arg( Formatter::sizeToString( sizeWhenDone
) )
431 .arg( Formatter::sizeToString( haveUnverified
) );
435 myHaveLabel
->setText( string
);
437 // myAvailabilityLabel
438 if( torrents
.empty( ) )
441 if( sizeWhenDone
== 0 )
444 string
= QString( "%1%" ).arg( Formatter::percentToString( ( 100.0 * available
) / sizeWhenDone
) );
446 myAvailabilityLabel
->setText( string
);
449 if( torrents
.empty( ) )
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
);
461 string
= tr( "%1 (%2 corrupt)" ).arg( dstr
).arg( fstr
);
465 myDownloadedLabel
->setText( string
);
467 if( torrents
.empty( ) )
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( );
485 if( torrents
.empty( ) )
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( ) )
497 string
= stateString
; // paused || finished
498 else if( baseline
.isNull( ) )
501 string
= Formatter::timeToString( baseline
.secsTo( qdt_now
) );
503 myRunTimeLabel
->setText( string
);
508 if( torrents
.empty( ) )
511 int baseline
= torrents
[0]->getETA( );
512 foreach( const Torrent
* t
, torrents
) {
513 if( baseline
!= t
->getETA( ) ) {
518 if( string
.isEmpty( ) ) {
520 string
= tr( "Unknown" );
522 string
= Formatter::timeToString( baseline
);
525 myETALabel
->setText( string
);
528 // myLastActivityLabel
529 if( torrents
.empty( ) )
532 QDateTime latest
= torrents
[0]->lastActivity( );
533 foreach( const Torrent
* t
, torrents
) {
534 const QDateTime dt
= t
->lastActivity( );
538 const int seconds
= latest
.isValid() ? latest
.secsTo( qdt_now
) : -1;
541 else if( seconds
< 5 )
542 string
= tr( "Active now" );
544 string
= tr( "%1 ago" ).arg( Formatter::timeToString( seconds
) );
546 myLastActivityLabel
->setText( string
);
549 if( torrents
.empty( ) )
552 string
= torrents
[0]->getError( );
553 foreach( const Torrent
* t
, torrents
) {
554 if( string
!= t
->getError( ) ) {
560 if( string
.isEmpty( ) )
562 myErrorLabel
->setText( string
);
570 if( torrents
.empty( ) )
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( ) )
584 else if( pieceSize
> 0 )
585 string
= tr( "%1 (%Ln pieces @ %2)", "", pieces
)
586 .arg( Formatter::sizeToString( size
) )
587 .arg( Formatter::memToString( pieceSize
) );
589 string
= tr( "%1 (%Ln pieces)", "", pieces
)
590 .arg( Formatter::sizeToString( size
) );
592 mySizeLabel
->setText( string
);
595 if( torrents
.empty( ) )
598 string
= torrents
[0]->hashString( );
599 foreach( const Torrent
* t
, torrents
) {
600 if( string
!= t
->hashString( ) ) {
606 myHashLabel
->setText( string
);
609 if( torrents
.empty( ) )
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( ) ) {
622 myPrivacyLabel
->setText( string
);
625 if( torrents
.empty( ) )
628 string
= torrents
[0]->comment( );
629 foreach( const Torrent
* t
, torrents
) {
630 if( string
!= t
->comment( ) ) {
636 myCommentBrowser
->setText( string
);
637 myCommentBrowser
->setMaximumHeight( QWIDGETSIZE_MAX
);
640 if( torrents
.empty( ) )
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
)
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" );
659 string
= tr( "Created by %1 on %2" ).arg( creator
).arg( date
);
661 myOriginLabel
->setText( string
);
664 if( torrents
.empty( ) )
667 string
= torrents
[0]->getPath( );
668 foreach( const Torrent
* t
, torrents
) {
669 if( string
!= t
->getPath( ) ) {
675 myLocationLabel
->setText( string
);
682 if( myChangedTorrents
&& !torrents
.empty( ) )
685 const Torrent
* baseline
= *torrents
.begin();
691 // mySessionLimitCheck
693 baselineFlag
= baseline
->honorsSessionLimits( );
694 foreach( tor
, torrents
) if( baselineFlag
!= tor
->honorsSessionLimits( ) ) { uniform
= false; break; }
695 mySessionLimitCheck
->setChecked( uniform
&& baselineFlag
);
699 baselineFlag
= baseline
->downloadIsLimited( );
700 foreach( tor
, torrents
) if( baselineFlag
!= tor
->downloadIsLimited( ) ) { uniform
= false; break; }
701 mySingleDownCheck
->setChecked( uniform
&& baselineFlag
);
705 baselineFlag
= baseline
->uploadIsLimited( );
706 foreach( tor
, torrents
) if( baselineFlag
!= tor
->uploadIsLimited( ) ) { uniform
= false; break; }
707 mySingleUpCheck
->setChecked( uniform
&& baselineFlag
);
709 // myBandwidthPriorityCombo
711 baselineInt
= baseline
->getBandwidthPriority( );
712 foreach( tor
, torrents
) if ( baselineInt
!= tor
->getBandwidthPriority( ) ) { uniform
= false; break; }
714 i
= myBandwidthPriorityCombo
->findData( baselineInt
);
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( ) )
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( ) );
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( ) );
753 myTrackerModel
->refresh( myModel
, myIds
);
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
);
786 const QString code
= peer
.flagStr
;
787 item
->setStatus( code
);
788 item
->refresh( peer
);
791 foreach( QChar ch
, code
) {
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
) );
834 myFileTreeView
->update( torrents
[0]->files( ) , myChangedTorrents
);
836 myFileTreeView
->clear( );
838 myChangedTorrents
= false;
839 myHavePendingRefresh
= false;
840 foreach( QWidget
* w
, myWidgets
)
841 w
->setEnabled( true );
845 Details :: enableWhenChecked( QCheckBox
* box
, QWidget
* w
)
847 connect( box
, SIGNAL(toggled(bool)), w
, SLOT(setEnabled(bool)) );
848 w
->setEnabled( box
->isChecked( ) );
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
);
895 Details :: onShowTrackerScrapesToggled( bool val
)
897 myPrefs
.set( Prefs::SHOW_TRACKER_SCRAPES
, val
);
901 Details :: onShowBackupTrackersToggled( bool val
)
903 myPrefs
.set( Prefs::SHOW_BACKUP_TRACKERS
, val
);
907 Details :: onHonorsSessionLimitsToggled( bool val
)
909 mySession
.torrentSet( myIds
, "honorsSessionLimits", val
);
913 Details :: onDownloadLimitedToggled( bool val
)
915 mySession
.torrentSet( myIds
, "downloadLimited", val
);
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
);
925 mySession
.torrentSet( myIds
, key
, d
->value( ) );
927 mySession
.torrentSet( myIds
, key
, qobject_cast
<const QSpinBox
*>(spin
)->value( ) );
932 Details :: onUploadLimitedToggled( bool val
)
934 mySession
.torrentSet( myIds
, "uploadLimited", val
);
939 Details :: onIdleModeChanged( int index
)
941 const int val
= myIdleCombo
->itemData( index
).toInt( );
942 mySession
.torrentSet( myIds
, "seedIdleMode", val
);
947 Details :: onRatioModeChanged( int index
)
949 const int val
= myRatioCombo
->itemData( index
).toInt( );
950 mySession
.torrentSet( myIds
, "seedRatioMode", val
);
954 Details :: onBandwidthPriorityChanged( int index
)
958 const int priority
= myBandwidthPriorityCombo
->itemData(index
).toInt( );
959 mySession
.torrentSet( myIds
, "bandwidthPriority", priority
);
965 Details :: onTrackerSelectionChanged( )
967 const int selectionCount
= myTrackerView
->selectionModel()->selectedRows().size();
968 myEditTrackerButton
->setEnabled( selectionCount
== 1 );
969 myRemoveTrackerButton
->setEnabled( selectionCount
> 0 );
973 Details :: onAddTrackerClicked( )
976 const QString url
= QInputDialog::getText( this,
978 tr( "Add tracker announce URL:" ),
979 QLineEdit::Normal
, QString(), &ok
);
982 // user pressed "cancel" -- noop
984 else if( !QUrl(url
).isValid( ) )
986 QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( url
) );
992 foreach( int id
, myIds
)
993 if( myTrackerModel
->find( id
, url
) == -1 )
996 if( ids
.empty( ) ) // all the torrents already have this tracker
998 QMessageBox::warning( this, tr( "Error" ), tr( "Tracker already exists." ) );
1004 mySession
.torrentSet( ids
, "trackerAdd", urls
);
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
>();
1020 const QString newval
= QInputDialog::getText( this,
1022 tr( "Edit tracker announce URL:" ),
1024 trackerInfo
.st
.announce
, &ok
);
1028 // user pressed "cancel" -- noop
1030 else if( !QUrl(newval
).isValid( ) )
1032 QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( newval
) );
1037 ids
<< trackerInfo
.torrentId
;
1039 const QPair
<int,QString
> idUrl
= qMakePair( trackerInfo
.st
.id
, newval
);
1041 mySession
.torrentSet( ids
, "trackerReplace", idUrl
);
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( ) )
1064 mySession
.torrentSet( ids
, "trackerRemove", torrentId_to_trackerIds
.values( id
) );
1067 selectionModel
->clearSelection( );
1072 Details :: createOptionsTab( )
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" ) );
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()));
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
);
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
);
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
);
1177 Details :: createTrackerTab( )
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)) );
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 );
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
;
1315 Details :: onFilePriorityChanged( const QSet
<int>& indices
, int priority
)
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( ) );
1328 Details :: onFileWantedChanged( const QSet
<int>& indices
, bool wanted
)
1330 QString
key( wanted
? "files-wanted" : "files-unwanted" );
1331 mySession
.torrentSet( myIds
, key
, indices
.toList( ) );