source: trunk/qt/details.cc @ 11061

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

(trunk qt) #3454 "sync trackers tab with GTK+ client" --

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