source: trunk/qt/details.cc @ 10822

Last change on this file since 10822 was 10822, checked in by Longinus00, 12 years ago

#3298:Prettier formating of percentages

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