source: trunk/qt/details.cc @ 11065

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

(trunk qt) fix a couple of small memory leaks detected by valgrind

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