source: trunk/qt/details.cc @ 10457

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

(trunk) poke at details yet again

  • Property svn:keywords set to Date Rev Author Id
File size: 37.5 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 10457 2010-04-06 04:54:51Z charles $
11 */
12
13#include <cassert>
14#include <ctime>
15#include <iostream>
16
17#include <QCheckBox>
18#include <QComboBox>
19#include <QDateTime>
20#include <QDialogButtonBox>
21#include <QDoubleSpinBox>
22#include <QEvent>
23#include <QFont>
24#include <QFontMetrics>
25#include <QHBoxLayout>
26#include <QHBoxLayout>
27#include <QHeaderView>
28#include <QLabel>
29#include <QLocale>
30#include <QPushButton>
31#include <QRadioButton>
32#include <QResizeEvent>
33#include <QSpinBox>
34#include <QStyle>
35#include <QTabWidget>
36#include <QTextBrowser>
37#include <QTreeView>
38#include <QTreeWidget>
39#include <QTreeWidgetItem>
40#include <QVBoxLayout>
41
42#include <libtransmission/transmission.h>
43
44#include "details.h"
45#include "file-tree.h"
46#include "hig.h"
47#include "prefs.h"
48#include "session.h"
49#include "squeezelabel.h"
50#include "torrent.h"
51#include "torrent-model.h"
52#include "utils.h"
53
54class Prefs;
55class Session;
56
57/****
58*****
59****/
60
61namespace
62{
63    const int REFRESH_INTERVAL_MSEC = 4000;
64
65    enum // peer columns
66    {
67        COL_LOCK,
68        COL_UP,
69        COL_DOWN,
70        COL_PERCENT,
71        COL_STATUS,
72        COL_ADDRESS,
73        COL_CLIENT,
74        N_COLUMNS
75    };
76}
77
78/***
79****
80***/
81
82class PeerItem: public QTreeWidgetItem
83{
84        Peer peer;
85        QString collatedAddress;
86        QString status;
87
88    public:
89        virtual ~PeerItem( ) { }
90        PeerItem( const Peer& p ) {
91            peer = p;
92            int q[4];
93            if( sscanf( p.address.toUtf8().constData(), "%d.%d.%d.%d", q+0, q+1, q+2, q+3 ) == 4 )
94                collatedAddress.sprintf( "%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3] );
95            else
96                collatedAddress = p.address;
97        }
98    public:
99        void refresh( const Peer& p ) { peer = p; }
100        void setStatus( const QString& s ) { status = s; }
101        virtual bool operator< ( const QTreeWidgetItem & other ) const {
102            const PeerItem * i = dynamic_cast<const PeerItem*>(&other);
103            QTreeWidget * tw( treeWidget( ) );
104            const int column = tw ? tw->sortColumn() : 0;
105            switch( column ) {
106                case COL_UP: return peer.rateToPeer < i->peer.rateToPeer;
107                case COL_DOWN: return peer.rateToClient < i->peer.rateToClient;
108                case COL_PERCENT: return peer.progress < i->peer.progress;
109                case COL_STATUS: return status < i->status;
110                case COL_CLIENT: return peer.clientName < i->peer.clientName;
111                case COL_LOCK: return peer.isEncrypted && !i->peer.isEncrypted;
112                default: return collatedAddress < i->collatedAddress;
113            }
114        }
115};
116
117/***
118****
119***/
120
121Details :: Details( Session& session, Prefs& prefs, TorrentModel& model, QWidget * parent ):
122    QDialog( parent, Qt::Dialog ),
123    mySession( session ),
124    myPrefs( prefs ),
125    myModel( model ),
126    myHavePendingRefresh( false )
127{
128    QVBoxLayout * layout = new QVBoxLayout( this );
129
130    setWindowTitle( tr( "Torrent Properties" ) );
131
132    QTabWidget * t = new QTabWidget( this );
133    QWidget * w;
134    t->addTab( w = createInfoTab( ),      tr( "Information" ) );
135    myWidgets << w;
136    t->addTab( w = createPeersTab( ),     tr( "Peers" ) );
137    myWidgets << w;
138    t->addTab( w = createTrackerTab( ),   tr( "Tracker" ) );
139    myWidgets << w;
140    t->addTab( w = createFilesTab( ),     tr( "Files" ) );
141    myWidgets << w;
142    t->addTab( w = createOptionsTab( ),   tr( "Options" ) );
143    myWidgets << w;
144    layout->addWidget( t );
145
146    QDialogButtonBox * buttons = new QDialogButtonBox( QDialogButtonBox::Close, Qt::Horizontal, this );
147    connect( buttons, SIGNAL(rejected()), this, SLOT(deleteLater()));
148    layout->addWidget( buttons );
149
150    connect( &myTimer, SIGNAL(timeout()), this, SLOT(onTimer()));
151
152    onTimer( );
153    myTimer.setSingleShot( false );
154    myTimer.start( REFRESH_INTERVAL_MSEC );
155}
156
157Details :: ~Details( )
158{
159}
160
161void
162Details :: setIds( const QSet<int>& ids )
163{
164    if( ids == myIds )
165        return;
166
167    // stop listening to the old torrents
168    foreach( int id, myIds ) {
169        const Torrent * tor = myModel.getTorrentFromId( id );
170        if( tor )
171            disconnect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) );
172    }
173
174    myFileTreeView->clear( );
175
176    myIds = ids;
177
178    // listen to the new torrents
179    foreach( int id, myIds ) {
180        const Torrent * tor = myModel.getTorrentFromId( id );
181        if( tor )
182            connect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) );
183    }
184
185    foreach( QWidget * w, myWidgets )
186        w->setEnabled( false );
187
188    onTimer( );
189}
190
191/***
192****
193***/
194
195QString
196Details :: timeToStringRounded( int seconds )
197{
198    if( seconds > 60 ) seconds -= ( seconds % 60 );
199    return Utils::timeToString ( seconds );
200}
201
202void
203Details :: onTimer( )
204{
205    if( !myIds.empty( ) )
206        mySession.refreshExtraStats( myIds );
207}
208
209void
210Details :: onTorrentChanged( )
211{
212    if( !myHavePendingRefresh ) {
213        myHavePendingRefresh = true;
214        QTimer::singleShot( 100, this, SLOT(refresh()));
215    }
216}
217
218
219void
220Details :: refresh( )
221{
222    QLocale locale;
223    const int n = myIds.size( );
224    const bool single = n == 1;
225    const QString blank;
226    const QFontMetrics fm( fontMetrics( ) );
227    QList<const Torrent*> torrents;
228    QString string;
229    const QString none = tr( "None" );
230    const QString mixed = tr( "Mixed" );
231    const QString unknown = tr( "Unknown" );
232
233    // build a list of torrents
234    foreach( int id, myIds ) {
235        const Torrent * tor = myModel.getTorrentFromId( id );
236        if( tor )
237            torrents << tor;
238    }
239
240    ///
241    ///  activity tab
242    ///
243
244    // myStateLabel
245    if( torrents.empty( ) )
246        string = none;
247    else {
248        bool isMixed = false;
249        bool allPaused = true;
250        bool allFinished = true;
251        const tr_torrent_activity activity = torrents[0]->getActivity( );
252        foreach( const Torrent * t, torrents ) {
253            if( activity != t->getActivity( ) )
254                isMixed = true;
255            if( activity != TR_STATUS_STOPPED )
256                allPaused = allFinished = false;
257            if( !t->isFinished( ) )
258                allFinished = false;
259        }
260        if( isMixed )
261            string = mixed;
262        else if( allFinished )
263            string = tr( "Finished" );
264        else if( allPaused )
265            string = tr( "Paused" );
266        else
267            string = torrents[0]->activityString( );
268    }
269    myStateLabel->setText( string );
270    const QString stateString = string;
271
272    // myHaveLabel
273    double sizeWhenDone = 0;
274    double leftUntilDone = 0;
275    double available = 0;
276    int64_t haveTotal = 0;
277    int64_t haveVerified = 0;
278    int64_t haveUnverified = 0;
279    int64_t verifiedPieces = 0;
280    foreach( const Torrent * t, torrents ) {
281        if( t->hasMetadata( ) ) {
282            haveTotal += t->haveTotal( );
283            haveUnverified += t->haveUnverified( );
284            const uint64_t v = t->haveVerified( );
285            haveVerified += v;
286            verifiedPieces += v / t->pieceSize( );
287            sizeWhenDone += t->sizeWhenDone( );
288            leftUntilDone += t->leftUntilDone( );
289            available += t->sizeWhenDone() - t->leftUntilDone() + t->desiredAvailable();
290        }
291    }
292    if( !haveVerified && !haveUnverified )
293        string = none;
294    else {
295        const double d = 100.0 * ( sizeWhenDone ? ( sizeWhenDone - leftUntilDone ) / sizeWhenDone : 1 );
296        QString pct = locale.toString( d, 'f', 2 );
297        if( !haveUnverified )
298            string = tr( "%1 (%2%)" )
299                         .arg( Utils :: sizeToString( haveVerified + haveUnverified ) )
300                         .arg( pct );
301        else
302            string = tr( "%1 (%2%); %3 Unverified" )
303                         .arg( Utils :: sizeToString( haveVerified + haveUnverified ) )
304                         .arg( pct )
305                         .arg( Utils :: sizeToString( haveUnverified ) );
306    }
307    myHaveLabel->setText( string );
308
309    // myAvailabilityLabel
310    if( sizeWhenDone < 1 )
311        string = none;
312    else
313        string.sprintf( "%'.1f%%", ( 100.0 * available ) / sizeWhenDone );
314    myAvailabilityLabel->setText( string );
315
316    // myDownloadedLabel
317    if( torrents.empty( ) )
318        string = none;
319    else {
320        uint64_t d=0, f=0;
321        foreach( const Torrent * t, torrents ) {
322            d += t->downloadedEver( );
323            f += t->failedEver( );
324        }
325        const QString dstr = Utils::sizeToString( d );
326        const QString fstr = Utils::sizeToString( f );
327        if( f )
328            string = tr( "%1 (+%2 corrupt)" ).arg( dstr ).arg( fstr );
329        else
330            string = dstr;
331    }
332    myDownloadedLabel->setText( string );
333
334    uint64_t sum = 0;
335    foreach( const Torrent * t, torrents ) sum += t->uploadedEver( );
336    myUploadedLabel->setText( Utils::sizeToString( sum ) );
337
338    double d = 0;
339    foreach( const Torrent *t, torrents ) d += t->ratio( );
340    myRatioLabel->setText( Utils :: ratioToString( d / n ) );
341
342    const QDateTime qdt_now = QDateTime::currentDateTime( );
343
344    // myRunTimeLabel
345    if( torrents.empty( ) )
346        string = none;
347    else {
348        bool allPaused = true;
349        QDateTime baseline = torrents[0]->lastStarted( );
350        foreach( const Torrent * t, torrents ) {
351            if( baseline != t->lastStarted( ) )
352                baseline = QDateTime( );
353            if( !t->isPaused( ) )
354                allPaused = false;
355        }
356        if( allPaused )
357            string = stateString; // paused || finished
358        else if( baseline.isNull( ) )
359            string = mixed;
360        else
361            string = Utils::timeToString( baseline.secsTo( qdt_now ) );
362    }
363    myRunTimeLabel->setText( string );
364
365
366    // myETALabel
367    string.clear( );
368    if( torrents.empty( ) )
369        string = none;
370    else {
371        int baseline = torrents[0]->getETA( );
372        foreach( const Torrent * t, torrents ) {
373            if( baseline != t->getETA( ) ) {
374                string = mixed;
375                break;
376            }
377        }
378        if( string.isEmpty( ) ) {
379            if( baseline < 0 )
380                string = tr( "Unknown" );
381            else
382                string = Utils::timeToString( baseline );
383       }
384    }
385    myETALabel->setText( string );
386
387
388    // myLastActivityLabel
389    if( torrents.empty( ) )
390        string = none;
391    else {
392        QDateTime latest = torrents[0]->lastActivity( );
393        foreach( const Torrent * t, torrents ) {
394            const QDateTime dt = t->lastActivity( );
395            if( latest < dt )
396                latest = dt;
397        }
398        const int seconds = latest.secsTo( qdt_now );
399        if( seconds < 5 )
400            string = tr( "Active now" );
401        else
402            string = tr( "%1 ago" ).arg( Utils::timeToString( seconds ) );
403    }
404    myLastActivityLabel->setText( string );
405
406
407    if( torrents.empty( ) )
408        string = none;
409    else {
410        string = torrents[0]->getError( );
411        foreach( const Torrent * t, torrents ) {
412            if( string != t->getError( ) ) {
413                string = mixed;
414                break;
415            }
416        }
417    }
418    if( string.isEmpty( ) )
419        string = none;
420    myErrorLabel->setText( string );
421
422
423    ///
424    /// information tab
425    ///
426
427    // mySizeLabel
428    if( torrents.empty( ) )
429        string = none;
430    else {
431        int pieces = 0;
432        uint64_t size = 0;
433        uint32_t pieceSize = torrents[0]->pieceSize( );
434        foreach( const Torrent * t, torrents ) {
435            pieces += t->pieceCount( );
436            size += t->totalSize( );
437            if( pieceSize != t->pieceSize( ) )
438                pieceSize = 0;
439        }
440        if( !size )
441            string = none;
442        else if( pieceSize > 0 )
443            string = tr( "%1 (%Ln pieces @ %2)", "", pieces )
444                     .arg( Utils::sizeToString( size ) )
445                     .arg( Utils::sizeToString( pieceSize ) );
446        else
447            string = tr( "%1 (%Ln pieces)", "", pieces )
448                     .arg( Utils::sizeToString( size ) );
449    }
450    mySizeLabel->setText( string );
451
452    // myHashLabel
453    if( torrents.empty( ) )
454        string = none;
455    else {
456        string = torrents[0]->hashString( );
457        foreach( const Torrent * t, torrents ) {
458            if( string != t->hashString( ) ) {
459                string = mixed;
460                break;
461            }
462        }
463    }
464    myHashLabel->setText( string );
465
466    // myPrivacyLabel
467    if( torrents.empty( ) )
468        string = none;
469    else {
470        bool b = torrents[0]->isPrivate( );
471        string = b ? tr( "Private to this tracker -- DHT and PEX disabled" )
472                   : tr( "Public torrent" );
473        foreach( const Torrent * t, torrents ) {
474            if( b != t->isPrivate( ) ) {
475                string = mixed;
476                break;
477            }
478        }
479    }
480    myPrivacyLabel->setText( string );
481
482    // myCommentBrowser
483    if( torrents.empty( ) )
484        string = none;
485    else {
486        string = torrents[0]->comment( );
487        foreach( const Torrent * t, torrents ) {
488            if( string != t->comment( ) ) {
489                string = mixed;
490                break;
491            }
492        }
493    }
494    myCommentBrowser->setText( string );
495
496    // myOriginLabel
497    if( torrents.empty( ) )
498        string = none;
499    else {
500        bool mixed_creator=false, mixed_date=false;
501        const QString creator = torrents[0]->creator();
502        const QString date = torrents[0]->dateCreated().toString();
503        foreach( const Torrent * t, torrents ) {
504            mixed_creator |= ( creator != t->creator() );
505            mixed_date |=  ( date != t->dateCreated().toString() );
506        }
507        if( mixed_creator && mixed_date )
508            string = mixed;
509        else if( mixed_date )
510            string = tr( "Created by %1" ).arg( creator );
511        else if( mixed_creator || creator.isEmpty( ) )
512            string = tr( "Created on %1" ).arg( date );
513        else
514            string = tr( "Created by %1 on %2" ).arg( creator ).arg( date );
515    }
516    myOriginLabel->setText( string );
517
518    // myLocationLabel
519    if( torrents.empty( ) )
520        string = none;
521    else {
522        string = torrents[0]->getPath( );
523        foreach( const Torrent * t, torrents ) {
524            if( string != t->getPath( ) ) {
525                string = mixed;
526                break;
527            }
528        }
529    }
530    myLocationLabel->setText( string );
531
532
533    ///
534    ///  Options Tab
535    ///
536
537    if( !torrents.empty( ) )
538    {
539        int i;
540        const Torrent * baseline = *torrents.begin();
541        const Torrent * tor;
542        bool uniform;
543        bool baselineFlag;
544        int baselineInt;
545
546        // mySessionLimitCheck
547        uniform = true;
548        baselineFlag = baseline->honorsSessionLimits( );
549        foreach( tor, torrents ) if( baselineFlag != tor->honorsSessionLimits( ) ) { uniform = false; break; }
550        mySessionLimitCheck->setChecked( uniform && baselineFlag );
551
552        // mySingleDownCheck
553        uniform = true;
554        baselineFlag = baseline->downloadIsLimited( );
555        foreach( tor, torrents ) if( baselineFlag != tor->downloadIsLimited( ) ) { uniform = false; break; }
556        mySingleDownCheck->setChecked( uniform && baselineFlag );
557
558        // mySingleUpCheck
559        uniform = true;
560        baselineFlag = baseline->uploadIsLimited( );
561        foreach( tor, torrents ) if( baselineFlag != tor->uploadIsLimited( ) ) { uniform = false; break; }
562        mySingleUpCheck->setChecked( uniform && baselineFlag );
563
564        // myBandwidthPriorityCombo
565        uniform = true;
566        baselineInt = baseline->getBandwidthPriority( );
567        foreach( tor, torrents ) if ( baselineInt != tor->getBandwidthPriority( ) ) { uniform = false; break; }
568        if( uniform )
569            i = myBandwidthPriorityCombo->findData( baselineInt );
570        else
571            i = -1;
572        myBandwidthPriorityCombo->blockSignals( true );
573        myBandwidthPriorityCombo->setCurrentIndex( i );
574        myBandwidthPriorityCombo->blockSignals( false );
575
576        mySingleDownSpin->blockSignals( true );
577        mySingleDownSpin->setValue( int(tor->downloadLimit().kbps()) );
578        mySingleDownSpin->blockSignals( false );
579
580        mySingleUpSpin->blockSignals( true );
581        mySingleUpSpin->setValue( int(tor->uploadLimit().kbps()) );
582        mySingleUpSpin->blockSignals( false );
583
584        myPeerLimitSpin->blockSignals( true );
585        myPeerLimitSpin->setValue( tor->peerLimit() );
586        myPeerLimitSpin->blockSignals( false );
587
588        // ratio radios
589        uniform = true;
590        baselineInt = tor->seedRatioMode( );
591        foreach( tor, torrents ) if( baselineInt != tor->seedRatioMode( ) ) { uniform = false; break; }
592        if( !uniform ) {
593            mySeedGlobalRadio->setChecked( false );
594            mySeedCustomRadio->setChecked( false );
595            mySeedForeverRadio->setChecked( false );
596        } else {
597            QRadioButton * rb;
598            switch( baselineInt ) {
599                case TR_RATIOLIMIT_GLOBAL:    rb = mySeedGlobalRadio; break;
600                case TR_RATIOLIMIT_SINGLE:    rb = mySeedCustomRadio; break;
601                case TR_RATIOLIMIT_UNLIMITED: rb = mySeedForeverRadio; break;
602            }
603            rb->setChecked( true );
604        }
605
606        mySeedCustomSpin->blockSignals( true );
607        mySeedCustomSpin->setValue( tor->seedRatioLimit( ) );
608        mySeedCustomSpin->blockSignals( false );
609    }
610
611    // tracker tab
612    //
613    QMap<QString,QTreeWidgetItem*> trackers2;
614    QList<QTreeWidgetItem*> newItems2;
615    const time_t now( time( 0 ) );
616    const bool showBackup = myPrefs.getBool( Prefs::SHOW_BACKUP_TRACKERS );
617    const bool showScrape = myPrefs.getBool( Prefs::SHOW_TRACKER_SCRAPES );
618    foreach( const Torrent * t, torrents )
619    {
620        const QString idStr( QString::number( t->id( ) ) );
621        TrackerStatsList trackerStats = t->trackerStats( );
622
623        foreach( const TrackerStat& trackerStat, trackerStats )
624        {
625            const QString key( idStr + ":" + QString::number(trackerStat.id) );
626            QTreeWidgetItem * item = (QTreeWidgetItem*) myTrackerStats.value( key, 0 );
627            QString str;
628
629            if( item == 0 ) // new tracker
630            {
631                item = new QTreeWidgetItem( myTrackerTree );
632                newItems2 << item;
633            }
634            str = trackerStat.host;
635            if( showBackup || !trackerStat.isBackup)
636            {
637                if( trackerStat.hasAnnounced )
638                {
639                    const QString tstr( timeToStringRounded( now - trackerStat.lastAnnounceTime ) );
640                    str += "\n";
641                    if( trackerStat.lastAnnounceSucceeded )
642                    {
643                        str += tr( "Got a list of %1 peers %2 ago" )
644                            .arg( trackerStat.lastAnnouncePeerCount )
645                            .arg( tstr );
646                    }
647                    else if( trackerStat.lastAnnounceTimedOut )
648                    {
649                        str += tr( "Peer list request timed out %1 ago; will retry" )
650                            .arg( tstr );
651                    }
652                    else
653                    {
654                        str += tr( "Got an error %1 ago" )
655                            .arg( tstr );
656                    }
657                }
658                switch( trackerStat.announceState )
659                {
660                    case TR_TRACKER_INACTIVE:
661                        if( trackerStat.hasAnnounced )
662                        {
663                            str += "\n";
664                            str += tr( "No updates scheduled" );
665                        }
666                        break;
667                    case TR_TRACKER_WAITING:
668                        {
669                            const QString tstr( timeToStringRounded( trackerStat.nextAnnounceTime - now ) );
670                            str += "\n";
671                            str += tr( "Asking for more peers in %1" )
672                                .arg( tstr );
673                        }
674                        break;
675                    case TR_TRACKER_QUEUED:
676                        str += "\n";
677                        str += tr( "Queued to ask for more peers" );
678                        break;
679                    case TR_TRACKER_ACTIVE:
680                        {
681                            const QString tstr( timeToStringRounded( now - trackerStat.lastAnnounceStartTime ) );
682                            str += "\n";
683                            str += tr( "Asking for more peers now... %1" )
684                                .arg( tstr );
685                        }
686                        break;
687                }
688                if( showScrape )
689                {
690                    if( trackerStat.hasScraped )
691                    {
692                        const QString tstr( timeToStringRounded( now - trackerStat.lastScrapeTime ) );
693                        str += "\n";
694                        if( trackerStat.lastScrapeSucceeded )
695                        {
696                            str += tr( "Tracker had %1 seeders and %2 leechers %3 ago" )
697                                .arg( trackerStat.seederCount )
698                                .arg( trackerStat.leecherCount )
699                                .arg( tstr );
700                        }
701                        else
702                        {
703                            str += tr( "Got a scrape error %1 ago" )
704                                .arg( tstr );
705                        }
706                    }
707                    switch( trackerStat.scrapeState )
708                    {
709                        case TR_TRACKER_INACTIVE:
710                            break;
711                        case TR_TRACKER_WAITING:
712                            {
713                                const QString tstr( timeToStringRounded( trackerStat.nextScrapeTime - now ) );
714                                str += "\n";
715                                str += tr( "Asking for peer counts in %1" )
716                                    .arg( tstr );
717                            }
718                            break;
719                        case TR_TRACKER_QUEUED:
720                            str += "\n";
721                            str += tr( "Queued to ask for peer counts" );
722                            break;
723                        case TR_TRACKER_ACTIVE:
724                            {
725                                const QString tstr( timeToStringRounded( now - trackerStat.lastScrapeStartTime ) );
726                                str += "\n";
727                                str += tr( "Asking for peer counts now... %1" )
728                                    .arg( tstr );
729                            }
730                            break;
731                    }
732                }
733            }
734
735            item->setText( 0, str );
736
737            trackers2.insert( key, item );
738        }
739    }
740    myTrackerTree->addTopLevelItems( newItems2 );
741    foreach( QString key, myTrackerStats.keys() ) {
742        if( !trackers2.contains( key ) ) { // tracker has disappeared
743            QTreeWidgetItem * item = myTrackerStats.value( key, 0 );
744            myTrackerTree->takeTopLevelItem( myTrackerTree->indexOfTopLevelItem( item ) );
745            delete item;
746        }
747    }
748    myTrackerStats = trackers2;
749
750    ///
751    ///  Peers tab
752    ///
753
754    QMap<QString,QTreeWidgetItem*> peers2;
755    QList<QTreeWidgetItem*> newItems;
756    foreach( const Torrent * t, torrents )
757    {
758        const QString idStr( QString::number( t->id( ) ) );
759        PeerList peers = t->peers( );
760
761        foreach( const Peer& peer, peers )
762        {
763            const QString key = idStr + ":" + peer.address;
764            PeerItem * item = (PeerItem*) myPeers.value( key, 0 );
765
766            if( item == 0 ) // new peer has connected
767            {
768                static const QIcon myEncryptionIcon( ":/icons/encrypted.png" );
769                static const QIcon myEmptyIcon;
770                item = new PeerItem( peer );
771                item->setTextAlignment( COL_UP, Qt::AlignRight );
772                item->setTextAlignment( COL_DOWN, Qt::AlignRight );
773                item->setTextAlignment( COL_PERCENT, Qt::AlignRight );
774                item->setIcon( COL_LOCK, peer.isEncrypted ? myEncryptionIcon : myEmptyIcon );
775                item->setToolTip( COL_LOCK, peer.isEncrypted ? tr( "Encrypted connection" ) : "" );
776                item->setText( COL_ADDRESS, peer.address );
777                item->setText( COL_CLIENT, peer.clientName );
778                newItems << item;
779            }
780
781            QString code;
782            if( peer.isDownloadingFrom )                           { code += 'D'; }
783            else if( peer.clientIsInterested )                     { code += 'd'; }
784            if( peer.isUploadingTo )                               { code += 'U'; }
785            else if( peer.peerIsInterested )                       { code += 'u'; }
786            if( !peer.clientIsChoked && !peer.clientIsInterested ) { code += 'K'; }
787            if( !peer.peerIsChoked && !peer.peerIsInterested )     { code += '?'; }
788            if( peer.isEncrypted )                                 { code += 'E'; }
789            if( peer.isIncoming )                                  { code += 'I'; }
790            item->setStatus( code );
791            item->refresh( peer );
792
793            QString codeTip;
794            foreach( QChar ch, code ) {
795                QString txt;
796                switch( ch.toAscii() ) {
797                    case 'O': txt = tr( "Optimistic unchoke" ); break;
798                    case 'D': txt = tr( "Downloading from this peer" ); break;
799                    case 'd': txt = tr( "We would download from this peer if they would let us" ); break;
800                    case 'U': txt = tr( "Uploading to peer" ); break;
801                    case 'u': txt = tr( "We would upload to this peer if they asked" ); break;
802                    case 'K': txt = tr( "Peer has unchoked us, but we're not interested" ); break;
803                    case '?': txt = tr( "We unchoked this peer, but they're not interested" ); break;
804                    case 'E': txt = tr( "Encrypted connection" ); break;
805                    case 'X': txt = tr( "Peer was discovered through Peer Exchange (PEX)" ); break;
806                    case 'I': txt = tr( "Peer is an incoming connection" ); break;
807                }
808                if( !txt.isEmpty( ) )
809                    codeTip += QString("%1: %2\n").arg(ch).arg(txt);
810            }
811
812            if( !codeTip.isEmpty() )
813                codeTip.resize( codeTip.size()-1 ); // eat the trailing linefeed
814
815            item->setText( COL_UP, peer.rateToPeer.isZero() ? "" : Utils::speedToString( peer.rateToPeer ) );
816            item->setText( COL_DOWN, peer.rateToClient.isZero() ? "" : Utils::speedToString( peer.rateToClient ) );
817            item->setText( COL_PERCENT, peer.progress > 0 ? QString( "%1%" ).arg( locale.toString((int)(peer.progress*100.0))) : "" );
818            item->setText( COL_STATUS, code );
819            item->setToolTip( COL_STATUS, codeTip );
820
821            peers2.insert( key, item );
822        }
823    }
824    myPeerTree->addTopLevelItems( newItems );
825    foreach( QString key, myPeers.keys() ) {
826        if( !peers2.contains( key ) ) { // old peer has disconnected
827            QTreeWidgetItem * item = myPeers.value( key, 0 );
828            myPeerTree->takeTopLevelItem( myPeerTree->indexOfTopLevelItem( item ) );
829            delete item;
830        }
831    }
832    myPeers = peers2;
833
834    if( single )
835        myFileTreeView->update( torrents[0]->files( ) );
836    else
837        myFileTreeView->clear( );
838
839    myHavePendingRefresh = false;
840    foreach( QWidget * w, myWidgets )
841        w->setEnabled( true );
842}
843
844void
845Details :: enableWhenChecked( QCheckBox * box, QWidget * w )
846{
847    connect( box, SIGNAL(toggled(bool)), w, SLOT(setEnabled(bool)) );
848    w->setEnabled( box->isChecked( ) );
849}
850
851
852/***
853****
854***/
855
856QWidget *
857Details :: createInfoTab( )
858{
859    HIG * hig = new HIG( this );
860
861    hig->addSectionTitle( tr( "Activity" ) );
862    hig->addRow( tr( "Torrent size:" ), mySizeLabel = new SqueezeLabel );
863    hig->addRow( tr( "Have:" ), myHaveLabel = new SqueezeLabel );
864    hig->addRow( tr( "Availability:" ), myAvailabilityLabel = new SqueezeLabel );
865    hig->addRow( tr( "Downloaded:" ), myDownloadedLabel = new SqueezeLabel );
866    hig->addRow( tr( "Uploaded:" ), myUploadedLabel = new SqueezeLabel );
867    hig->addRow( tr( "Ratio:" ), myRatioLabel = new SqueezeLabel );
868    hig->addRow( tr( "State:" ), myStateLabel = new SqueezeLabel );
869    hig->addRow( tr( "Running time:" ), myRunTimeLabel = new SqueezeLabel );
870    hig->addRow( tr( "Remaining time:" ), myETALabel = new SqueezeLabel );
871    hig->addRow( tr( "Last activity:" ), myLastActivityLabel = new SqueezeLabel );
872    hig->addRow( tr( "Error:" ), myErrorLabel = new SqueezeLabel );
873    hig->addSectionDivider( );
874
875    hig->addSectionDivider( );
876    hig->addSectionTitle( tr( "Details" ) );
877    hig->addRow( tr( "Location:" ), myLocationLabel = new SqueezeLabel );
878    hig->addRow( tr( "Hash:" ), myHashLabel = new SqueezeLabel );
879    hig->addRow( tr( "Privacy:" ), myPrivacyLabel = new SqueezeLabel );
880    hig->addRow( tr( "Origin:" ), myOriginLabel = new SqueezeLabel );
881    hig->addRow( tr( "Comment:" ), myCommentBrowser = new QTextBrowser );
882    const int h = QFontMetrics(myCommentBrowser->font()).lineSpacing() * 4;
883    myCommentBrowser->setMinimumHeight( h );
884    myCommentBrowser->setMaximumHeight( h );
885
886    hig->finish( );
887
888    return hig;
889}
890
891/***
892****
893***/
894
895void
896Details :: onShowBackupTrackersToggled( bool val )
897{
898    myPrefs.set( Prefs::SHOW_BACKUP_TRACKERS, val );
899}
900
901void
902Details :: onShowTrackerScrapesToggled( bool val )
903{
904    myPrefs.set( Prefs::SHOW_TRACKER_SCRAPES, val );
905}
906
907void
908Details :: onHonorsSessionLimitsToggled( bool val )
909{
910    mySession.torrentSet( myIds, "honorsSessionLimits", val );
911}
912void
913Details :: onDownloadLimitedToggled( bool val )
914{
915    mySession.torrentSet( myIds, "downloadLimited", val );
916}
917void
918Details :: onDownloadLimitChanged( int val )
919{
920    mySession.torrentSet( myIds, "downloadLimit", val );
921}
922void
923Details :: onUploadLimitedToggled( bool val )
924{
925    mySession.torrentSet( myIds, "uploadLimited", val );
926}
927void
928Details :: onUploadLimitChanged( int val )
929{
930    mySession.torrentSet( myIds, "uploadLimit", val );
931}
932
933#define RATIO_KEY "seedRatioMode"
934
935void
936Details :: onSeedUntilChanged( bool b )
937{
938    if( b )
939        mySession.torrentSet( myIds, RATIO_KEY, sender()->property(RATIO_KEY).toInt() );
940}
941
942void
943Details :: onSeedRatioLimitChanged( double val )
944{
945    QSet<int> ids;
946
947    foreach( int id, myIds ) {
948        const Torrent * tor = myModel.getTorrentFromId( id );
949        if( tor && tor->seedRatioLimit( ) )
950            ids.insert( id );
951    }
952
953    if( !ids.empty( ) )
954        mySession.torrentSet( ids, "seedRatioLimit", val );
955}
956
957void
958Details :: onMaxPeersChanged( int val )
959{
960    mySession.torrentSet( myIds, "peer-limit", val );
961}
962
963void
964Details :: onBandwidthPriorityChanged( int index )
965{
966    if( index != -1 )
967    {
968        const int priority = myBandwidthPriorityCombo->itemData(index).toInt( );
969        mySession.torrentSet( myIds, "bandwidthPriority", priority );
970    }
971}
972
973QWidget *
974Details :: createOptionsTab( )
975{
976    //QWidget * l;
977    QSpinBox * s;
978    QCheckBox * c;
979    QComboBox * m;
980    QHBoxLayout * h;
981    QRadioButton * r;
982    QDoubleSpinBox * ds;
983
984    HIG * hig = new HIG( this );
985    hig->addSectionTitle( tr( "Speed" ) );
986
987    c = new QCheckBox( tr( "Honor global &limits" ) );
988    mySessionLimitCheck = c;
989    hig->addWideControl( c );
990    connect( c, SIGNAL(clicked(bool)), this, SLOT(onHonorsSessionLimitsToggled(bool)) );
991
992    c = new QCheckBox( tr( "Limit &download speed (KB/s):" ) );
993    mySingleDownCheck = c;
994    s = new QSpinBox( );
995    mySingleDownSpin = s;
996    s->setRange( 0, INT_MAX );
997    hig->addRow( c, s );
998    enableWhenChecked( c, s );
999    connect( c, SIGNAL(clicked(bool)), this, SLOT(onDownloadLimitedToggled(bool)) );
1000    connect( s, SIGNAL(valueChanged(int)), this, SLOT(onDownloadLimitChanged(int)));
1001
1002    c = new QCheckBox( tr( "Limit &upload speed (KB/s):" ) );
1003    mySingleUpCheck = c;
1004    s = new QSpinBox( );
1005    mySingleUpSpin = s;
1006    s->setRange( 0, INT_MAX );
1007    hig->addRow( c, s );
1008    enableWhenChecked( c, s );
1009    connect( c, SIGNAL(clicked(bool)), this, SLOT(onUploadLimitedToggled(bool)) );
1010    connect( s, SIGNAL(valueChanged(int)), this, SLOT(onUploadLimitChanged(int)));
1011
1012    m = new QComboBox;
1013    m->addItem( tr( "High" ),   TR_PRI_HIGH );
1014    m->addItem( tr( "Normal" ), TR_PRI_NORMAL );
1015    m->addItem( tr( "Low" ),    TR_PRI_LOW );
1016    connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onBandwidthPriorityChanged(int)));
1017    hig->addRow( tr( "Torrent &priority:" ), m );
1018    myBandwidthPriorityCombo = m;
1019
1020    hig->addSectionDivider( );
1021    hig->addSectionTitle( tr( "Seed-Until Ratio" ) );
1022
1023    r = new QRadioButton( tr( "Use &global settings" ) );
1024    r->setProperty( RATIO_KEY, TR_RATIOLIMIT_GLOBAL );
1025    connect( r, SIGNAL(clicked(bool)), this, SLOT(onSeedUntilChanged(bool)));
1026    mySeedGlobalRadio = r;
1027    hig->addWideControl( r );
1028
1029    r = new QRadioButton( tr( "Seed &regardless of ratio" ) );
1030    r->setProperty( RATIO_KEY, TR_RATIOLIMIT_UNLIMITED );
1031    connect( r, SIGNAL(clicked(bool)), this, SLOT(onSeedUntilChanged(bool)));
1032    mySeedForeverRadio = r;
1033    hig->addWideControl( r );
1034
1035    h = new QHBoxLayout( );
1036    h->setSpacing( HIG :: PAD );
1037    r = new QRadioButton( tr( "&Seed torrent until its ratio reaches:" ) );
1038    r->setProperty( RATIO_KEY, TR_RATIOLIMIT_SINGLE );
1039    connect( r, SIGNAL(clicked(bool)), this, SLOT(onSeedUntilChanged(bool)));
1040    mySeedCustomRadio = r;
1041    h->addWidget( r );
1042    ds = new QDoubleSpinBox( );
1043    ds->setRange( 0.5, INT_MAX );
1044    connect( ds, SIGNAL(valueChanged(double)), this, SLOT(onSeedRatioLimitChanged(double)));
1045    mySeedCustomSpin = ds;
1046    h->addWidget( ds );
1047    hig->addWideControl( h );
1048
1049    hig->addSectionDivider( );
1050    hig->addSectionTitle( tr( "Peer Connections" ) );
1051
1052    s = new QSpinBox( );
1053    s->setRange( 1, 300 );
1054    connect( s, SIGNAL(valueChanged(int)), this, SLOT(onMaxPeersChanged(int)));
1055    myPeerLimitSpin = s;
1056    hig->addRow( tr( "&Maximum peers:" ), s );
1057
1058    hig->finish( );
1059
1060    return hig;
1061}
1062
1063/***
1064****
1065***/
1066
1067QWidget *
1068Details :: createTrackerTab( )
1069{
1070    QCheckBox * c;
1071    QWidget * top = new QWidget;
1072    QVBoxLayout * v = new QVBoxLayout( top );
1073
1074    v->setSpacing( HIG :: PAD_BIG );
1075    v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG );
1076
1077    QStringList headers;
1078    headers << tr("Trackers");
1079    myTrackerTree = new QTreeWidget;
1080    myTrackerTree->setHeaderLabels( headers );
1081    myTrackerTree->setSelectionMode( QTreeWidget::NoSelection );
1082    myTrackerTree->setRootIsDecorated( false );
1083    myTrackerTree->setTextElideMode( Qt::ElideRight );
1084    myTrackerTree->setAlternatingRowColors( true );
1085    v->addWidget( myTrackerTree, 1 );
1086
1087    c = new QCheckBox( tr( "Show &more details" ) );
1088    c->setChecked( myPrefs.getBool( Prefs::SHOW_TRACKER_SCRAPES ) );
1089    myShowTrackerScrapesCheck = c;
1090    v->addWidget( c, 1 );
1091    connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowTrackerScrapesToggled(bool)) );
1092
1093    c = new QCheckBox( tr( "Show &backup trackers" ) );
1094    c->setChecked( myPrefs.getBool( Prefs::SHOW_BACKUP_TRACKERS ) );
1095    myShowBackupTrackersCheck = c;
1096    v->addWidget( c, 1 );
1097    connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowBackupTrackersToggled(bool)) );
1098
1099    return top;
1100}
1101
1102/***
1103****
1104***/
1105
1106QWidget *
1107Details :: createPeersTab( )
1108{
1109    QWidget * top = new QWidget;
1110    QVBoxLayout * v = new QVBoxLayout( top );
1111    v->setSpacing( HIG :: PAD_BIG );
1112    v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG );
1113
1114    QStringList headers;
1115    headers << QString() << tr("Up") << tr("Down") << tr("%") << tr("Status") << tr("Address") << tr("Client");
1116    myPeerTree = new QTreeWidget;
1117    myPeerTree->setUniformRowHeights( true );
1118    myPeerTree->setHeaderLabels( headers );
1119    myPeerTree->setColumnWidth( 0, 20 );
1120    myPeerTree->setSortingEnabled( true );
1121    myPeerTree->setRootIsDecorated( false );
1122    myPeerTree->setTextElideMode( Qt::ElideRight );
1123    v->addWidget( myPeerTree, 1 );
1124
1125    const QFontMetrics m( font( ) );
1126    QSize size = m.size( 0, "1024 MB/s" );
1127    myPeerTree->setColumnWidth( COL_UP, size.width( ) );
1128    myPeerTree->setColumnWidth( COL_DOWN, size.width( ) );
1129    size = m.size( 0, " 100% " );
1130    myPeerTree->setColumnWidth( COL_PERCENT, size.width( ) );
1131    size = m.size( 0, "ODUK?EXI" );
1132    myPeerTree->setColumnWidth( COL_STATUS, size.width( ) );
1133    size = m.size( 0, "888.888.888.888" );
1134    myPeerTree->setColumnWidth( COL_ADDRESS, size.width( ) );
1135    size = m.size( 0, "Some BitTorrent Client" );
1136    myPeerTree->setColumnWidth( COL_CLIENT, size.width( ) );
1137    myPeerTree->setAlternatingRowColors( true );
1138
1139    return top;
1140}
1141
1142/***
1143****
1144***/
1145
1146QWidget *
1147Details :: createFilesTab( )
1148{
1149    myFileTreeView = new FileTreeView( );
1150
1151    connect( myFileTreeView, SIGNAL(      priorityChanged(const QSet<int>&, int)),
1152             this,           SLOT(  onFilePriorityChanged(const QSet<int>&, int)));
1153
1154    connect( myFileTreeView, SIGNAL(      wantedChanged(const QSet<int>&, bool)),
1155             this,           SLOT(  onFileWantedChanged(const QSet<int>&, bool)));
1156
1157    return myFileTreeView;
1158}
1159
1160void
1161Details :: onFilePriorityChanged( const QSet<int>& indices, int priority )
1162{
1163    QString key;
1164    switch( priority ) {
1165        case TR_PRI_LOW:   key = "priority-low"; break;
1166        case TR_PRI_HIGH:  key = "priority-high"; break;
1167        default:           key = "priority-normal"; break;
1168    }
1169    mySession.torrentSet( myIds, key, indices.toList( ) );
1170}
1171
1172void
1173Details :: onFileWantedChanged( const QSet<int>& indices, bool wanted )
1174{
1175    QString key( wanted ? "files-wanted" : "files-unwanted" );
1176    mySession.torrentSet( myIds, key, indices.toList( ) );
1177}
Note: See TracBrowser for help on using the repository browser.