source: trunk/qt/details.cc @ 11209

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

switch trackerRemove and trackerReplace rpc calls to use tracker id instead of announce urls as identifiers

  • Property svn:keywords set to Date Rev Author Id
File size: 40.5 KB
Line 
1/*
2 * This file Copyright (C) Mnemosyne LLC
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2
6 * as published by the Free Software Foundation.
7 *
8 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
9 *
10 * $Id: details.cc 11209 2010-09-14 06:23:48Z Longinus00 $
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 "session.h"
55#include "squeezelabel.h"
56#include "torrent.h"
57#include "torrent-model.h"
58#include "tracker-delegate.h"
59#include "tracker-model.h"
60#include "tracker-model-filter.h"
61
62class Prefs;
63class Session;
64
65/****
66*****
67****/
68
69namespace
70{
71    const int REFRESH_INTERVAL_MSEC = 4000;
72
73    enum // peer columns
74    {
75        COL_LOCK,
76        COL_UP,
77        COL_DOWN,
78        COL_PERCENT,
79        COL_STATUS,
80        COL_ADDRESS,
81        COL_CLIENT,
82        N_COLUMNS
83    };
84}
85
86/***
87****
88***/
89
90class PeerItem: public QTreeWidgetItem
91{
92        Peer peer;
93        QString collatedAddress;
94        QString status;
95
96    public:
97        virtual ~PeerItem( ) { }
98        PeerItem( const Peer& p ) {
99            peer = p;
100            int q[4];
101            if( sscanf( p.address.toUtf8().constData(), "%d.%d.%d.%d", q+0, q+1, q+2, q+3 ) == 4 )
102                collatedAddress.sprintf( "%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3] );
103            else
104                collatedAddress = p.address;
105        }
106    public:
107        void refresh( const Peer& p ) { peer = p; }
108        void setStatus( const QString& s ) { status = s; }
109        virtual bool operator< ( const QTreeWidgetItem & other ) const {
110            const PeerItem * i = dynamic_cast<const PeerItem*>(&other);
111            QTreeWidget * tw( treeWidget( ) );
112            const int column = tw ? tw->sortColumn() : 0;
113            switch( column ) {
114                case COL_UP: return peer.rateToPeer < i->peer.rateToPeer;
115                case COL_DOWN: return peer.rateToClient < i->peer.rateToClient;
116                case COL_PERCENT: return peer.progress < i->peer.progress;
117                case COL_STATUS: return status < i->status;
118                case COL_CLIENT: return peer.clientName < i->peer.clientName;
119                case COL_LOCK: return peer.isEncrypted && !i->peer.isEncrypted;
120                default: return collatedAddress < i->collatedAddress;
121            }
122        }
123};
124
125/***
126****
127***/
128
129QIcon
130Details :: getStockIcon( const QString& freedesktop_name, int fallback )
131{
132    QIcon icon = QIcon::fromTheme( freedesktop_name );
133
134    if( icon.isNull( ) )
135        icon = style()->standardIcon( QStyle::StandardPixmap( fallback ), 0, this );
136
137    return icon;
138}
139
140Details :: Details( Session& session, Prefs& prefs, TorrentModel& model, QWidget * parent ):
141    QDialog( parent, Qt::Dialog ),
142    mySession( session ),
143    myPrefs( prefs ),
144    myModel( model ),
145    myChangedTorrents( false ),
146    myHavePendingRefresh( false )
147{
148    QVBoxLayout * layout = new QVBoxLayout( this );
149
150    setWindowTitle( tr( "Torrent Properties" ) );
151
152    QTabWidget * t = new QTabWidget( this );
153    QWidget * w;
154    t->addTab( w = createInfoTab( ),      tr( "Information" ) );
155    myWidgets << w;
156    t->addTab( w = createPeersTab( ),     tr( "Peers" ) );
157    myWidgets << w;
158    t->addTab( w = createTrackerTab( ),   tr( "Tracker" ) );
159    myWidgets << w;
160    t->addTab( w = createFilesTab( ),     tr( "Files" ) );
161    myWidgets << w;
162    t->addTab( w = createOptionsTab( ),   tr( "Options" ) );
163    myWidgets << w;
164    layout->addWidget( t );
165
166    QDialogButtonBox * buttons = new QDialogButtonBox( QDialogButtonBox::Close, Qt::Horizontal, this );
167    connect( buttons, SIGNAL(rejected()), this, SLOT(close()));
168    layout->addWidget( buttons );
169    QWidget::setAttribute( Qt::WA_DeleteOnClose, true );
170
171    QList<int> initKeys;
172    initKeys << Prefs :: SHOW_TRACKER_SCRAPES
173             << Prefs :: SHOW_BACKUP_TRACKERS;
174    foreach( int key, initKeys )
175        refreshPref( key );
176
177    connect( &myTimer, SIGNAL(timeout()), this, SLOT(onTimer()));
178    connect( &myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(int)) );
179
180    onTimer( );
181    myTimer.setSingleShot( false );
182    myTimer.start( REFRESH_INTERVAL_MSEC );
183}
184
185Details :: ~Details( )
186{
187    myTrackerDelegate->deleteLater();
188    myTrackerFilter->deleteLater();
189    myTrackerModel->deleteLater();
190}
191
192void
193Details :: setIds( const QSet<int>& ids )
194{
195    if( ids == myIds )
196        return;
197
198    myChangedTorrents = true;
199
200    // stop listening to the old torrents
201    foreach( int id, myIds ) {
202        const Torrent * tor = myModel.getTorrentFromId( id );
203        if( tor )
204            disconnect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) );
205    }
206
207    myFileTreeView->clear( );
208    myIds = ids;
209    myTrackerModel->refresh( myModel, myIds );
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.isValid() ? latest.secsTo( qdt_now ) : -1;
503        if( seconds < 0 )
504            string = none;
505        else if( seconds < 5 )
506            string = tr( "Active now" );
507        else
508            string = tr( "%1 ago" ).arg( Formatter::timeToString( seconds ) );
509    }
510    myLastActivityLabel->setText( string );
511
512
513    if( torrents.empty( ) )
514        string = none;
515    else {
516        string = torrents[0]->getError( );
517        foreach( const Torrent * t, torrents ) {
518            if( string != t->getError( ) ) {
519                string = mixed;
520                break;
521            }
522        }
523    }
524    if( string.isEmpty( ) )
525        string = none;
526    myErrorLabel->setText( string );
527
528
529    ///
530    /// information tab
531    ///
532
533    // mySizeLabel
534    if( torrents.empty( ) )
535        string = none;
536    else {
537        int pieces = 0;
538        uint64_t size = 0;
539        uint32_t pieceSize = torrents[0]->pieceSize( );
540        foreach( const Torrent * t, torrents ) {
541            pieces += t->pieceCount( );
542            size += t->totalSize( );
543            if( pieceSize != t->pieceSize( ) )
544                pieceSize = 0;
545        }
546        if( !size )
547            string = none;
548        else if( pieceSize > 0 )
549            string = tr( "%1 (%Ln pieces @ %2)", "", pieces )
550                     .arg( Formatter::sizeToString( size ) )
551                     .arg( Formatter::memToString( pieceSize ) );
552        else
553            string = tr( "%1 (%Ln pieces)", "", pieces )
554                     .arg( Formatter::sizeToString( size ) );
555    }
556    mySizeLabel->setText( string );
557
558    // myHashLabel
559    if( torrents.empty( ) )
560        string = none;
561    else {
562        string = torrents[0]->hashString( );
563        foreach( const Torrent * t, torrents ) {
564            if( string != t->hashString( ) ) {
565                string = mixed;
566                break;
567            }
568        }
569    }
570    myHashLabel->setText( string );
571
572    // myPrivacyLabel
573    if( torrents.empty( ) )
574        string = none;
575    else {
576        bool b = torrents[0]->isPrivate( );
577        string = b ? tr( "Private to this tracker -- DHT and PEX disabled" )
578                   : tr( "Public torrent" );
579        foreach( const Torrent * t, torrents ) {
580            if( b != t->isPrivate( ) ) {
581                string = mixed;
582                break;
583            }
584        }
585    }
586    myPrivacyLabel->setText( string );
587
588    // myCommentBrowser
589    if( torrents.empty( ) )
590        string = none;
591    else {
592        string = torrents[0]->comment( );
593        foreach( const Torrent * t, torrents ) {
594            if( string != t->comment( ) ) {
595                string = mixed;
596                break;
597            }
598        }
599    }
600    myCommentBrowser->setText( string );
601    myCommentBrowser->setMaximumHeight( QWIDGETSIZE_MAX );
602
603    // myOriginLabel
604    if( torrents.empty( ) )
605        string = none;
606    else {
607        bool mixed_creator=false, mixed_date=false;
608        const QString creator = torrents[0]->creator();
609        const QString date = torrents[0]->dateCreated().toString();
610        foreach( const Torrent * t, torrents ) {
611            mixed_creator |= ( creator != t->creator() );
612            mixed_date |=  ( date != t->dateCreated().toString() );
613        }
614        if( mixed_creator && mixed_date )
615            string = mixed;
616        else if( mixed_date )
617            string = tr( "Created by %1" ).arg( creator );
618        else if( mixed_creator || creator.isEmpty( ) )
619            string = tr( "Created on %1" ).arg( date );
620        else
621            string = tr( "Created by %1 on %2" ).arg( creator ).arg( date );
622    }
623    myOriginLabel->setText( string );
624
625    // myLocationLabel
626    if( torrents.empty( ) )
627        string = none;
628    else {
629        string = torrents[0]->getPath( );
630        foreach( const Torrent * t, torrents ) {
631            if( string != t->getPath( ) ) {
632                string = mixed;
633                break;
634            }
635        }
636    }
637    myLocationLabel->setText( string );
638
639
640    ///
641    ///  Options Tab
642    ///
643
644    if( myChangedTorrents && !torrents.empty( ) )
645    {
646        int i;
647        const Torrent * baseline = *torrents.begin();
648        const Torrent * tor;
649        bool uniform;
650        bool baselineFlag;
651        int baselineInt;
652
653        // mySessionLimitCheck
654        uniform = true;
655        baselineFlag = baseline->honorsSessionLimits( );
656        foreach( tor, torrents ) if( baselineFlag != tor->honorsSessionLimits( ) ) { uniform = false; break; }
657        mySessionLimitCheck->setChecked( uniform && baselineFlag );
658
659        // mySingleDownCheck
660        uniform = true;
661        baselineFlag = baseline->downloadIsLimited( );
662        foreach( tor, torrents ) if( baselineFlag != tor->downloadIsLimited( ) ) { uniform = false; break; }
663        mySingleDownCheck->setChecked( uniform && baselineFlag );
664
665        // mySingleUpCheck
666        uniform = true;
667        baselineFlag = baseline->uploadIsLimited( );
668        foreach( tor, torrents ) if( baselineFlag != tor->uploadIsLimited( ) ) { uniform = false; break; }
669        mySingleUpCheck->setChecked( uniform && baselineFlag );
670
671        // myBandwidthPriorityCombo
672        uniform = true;
673        baselineInt = baseline->getBandwidthPriority( );
674        foreach( tor, torrents ) if ( baselineInt != tor->getBandwidthPriority( ) ) { uniform = false; break; }
675        if( uniform )
676            i = myBandwidthPriorityCombo->findData( baselineInt );
677        else
678            i = -1;
679        myBandwidthPriorityCombo->blockSignals( true );
680        myBandwidthPriorityCombo->setCurrentIndex( i );
681        myBandwidthPriorityCombo->blockSignals( false );
682
683        mySingleDownSpin->blockSignals( true );
684        mySingleDownSpin->setValue( (int)tor->downloadLimit().KBps() );
685        mySingleDownSpin->blockSignals( false );
686
687        mySingleUpSpin->blockSignals( true );
688        mySingleUpSpin->setValue( (int)tor->uploadLimit().KBps() );
689        mySingleUpSpin->blockSignals( false );
690
691        myPeerLimitSpin->blockSignals( true );
692        myPeerLimitSpin->setValue( tor->peerLimit() );
693        myPeerLimitSpin->blockSignals( false );
694    }
695
696    {
697        const Torrent * tor;
698
699        // ratio
700        bool uniform = true;
701        int baselineInt = torrents[0]->seedRatioMode( );
702        foreach( tor, torrents ) if( baselineInt != tor->seedRatioMode( ) ) { uniform = false; break; }
703
704        myRatioCombo->blockSignals( true );
705        myRatioCombo->setCurrentIndex( uniform ? myRatioCombo->findData( baselineInt ) : -1 );
706        myRatioSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) );
707        myRatioCombo->blockSignals( false );
708
709        myRatioSpin->blockSignals( true );
710        myRatioSpin->setValue( tor->seedRatioLimit( ) );
711        myRatioSpin->blockSignals( false );
712
713        // idle
714        uniform = true;
715        baselineInt = torrents[0]->seedIdleMode( );
716        foreach( tor, torrents ) if( baselineInt != tor->seedIdleMode( ) ) { uniform = false; break; }
717
718        myIdleCombo->blockSignals( true );
719        myIdleCombo->setCurrentIndex( uniform ? myIdleCombo->findData( baselineInt ) : -1 );
720        myIdleSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) );
721        myIdleCombo->blockSignals( false );
722
723        myIdleSpin->blockSignals( true );
724        myIdleSpin->setValue( tor->seedIdleLimit( ) );
725        myIdleSpin->blockSignals( false );
726    }
727
728    ///
729    ///  Tracker tab
730    ///
731
732    myTrackerModel->refresh( myModel, myIds );
733
734    ///
735    ///  Peers tab
736    ///
737
738    QMap<QString,QTreeWidgetItem*> peers2;
739    QList<QTreeWidgetItem*> newItems;
740    foreach( const Torrent * t, torrents )
741    {
742        const QString idStr( QString::number( t->id( ) ) );
743        PeerList peers = t->peers( );
744
745        foreach( const Peer& peer, peers )
746        {
747            const QString key = idStr + ":" + peer.address;
748            PeerItem * item = (PeerItem*) myPeers.value( key, 0 );
749
750            if( item == 0 ) // new peer has connected
751            {
752                static const QIcon myEncryptionIcon( ":/icons/encrypted.png" );
753                static const QIcon myEmptyIcon;
754                item = new PeerItem( peer );
755                item->setTextAlignment( COL_UP, Qt::AlignRight );
756                item->setTextAlignment( COL_DOWN, Qt::AlignRight );
757                item->setTextAlignment( COL_PERCENT, Qt::AlignRight );
758                item->setIcon( COL_LOCK, peer.isEncrypted ? myEncryptionIcon : myEmptyIcon );
759                item->setToolTip( COL_LOCK, peer.isEncrypted ? tr( "Encrypted connection" ) : "" );
760                item->setText( COL_ADDRESS, peer.address );
761                item->setText( COL_CLIENT, peer.clientName );
762                newItems << item;
763            }
764
765            const QString code = peer.flagStr;
766            item->setStatus( code );
767            item->refresh( peer );
768
769            QString codeTip;
770            foreach( QChar ch, code ) {
771                QString txt;
772                switch( ch.toAscii() ) {
773                    case 'O': txt = tr( "Optimistic unchoke" ); break;
774                    case 'D': txt = tr( "Downloading from this peer" ); break;
775                    case 'd': txt = tr( "We would download from this peer if they would let us" ); break;
776                    case 'U': txt = tr( "Uploading to peer" ); break;
777                    case 'u': txt = tr( "We would upload to this peer if they asked" ); break;
778                    case 'K': txt = tr( "Peer has unchoked us, but we're not interested" ); break;
779                    case '?': txt = tr( "We unchoked this peer, but they're not interested" ); break;
780                    case 'E': txt = tr( "Encrypted connection" ); break;
781                    case 'H': txt = tr( "Peer was discovered through DHT" ); break;
782                    case 'X': txt = tr( "Peer was discovered through Peer Exchange (PEX)" ); break;
783                    case 'I': txt = tr( "Peer is an incoming connection" ); break;
784                }
785                if( !txt.isEmpty( ) )
786                    codeTip += QString("%1: %2\n").arg(ch).arg(txt);
787            }
788
789            if( !codeTip.isEmpty() )
790                codeTip.resize( codeTip.size()-1 ); // eat the trailing linefeed
791
792            item->setText( COL_UP, peer.rateToPeer.isZero() ? "" : Formatter::speedToString( peer.rateToPeer ) );
793            item->setText( COL_DOWN, peer.rateToClient.isZero() ? "" : Formatter::speedToString( peer.rateToClient ) );
794            item->setText( COL_PERCENT, peer.progress > 0 ? QString( "%1%" ).arg( (int)( peer.progress * 100.0 ) ) : "" );
795            item->setText( COL_STATUS, code );
796            item->setToolTip( COL_STATUS, codeTip );
797
798            peers2.insert( key, item );
799        }
800    }
801    myPeerTree->addTopLevelItems( newItems );
802    foreach( QString key, myPeers.keys() ) {
803        if( !peers2.contains( key ) ) { // old peer has disconnected
804            QTreeWidgetItem * item = myPeers.value( key, 0 );
805            myPeerTree->takeTopLevelItem( myPeerTree->indexOfTopLevelItem( item ) );
806            delete item;
807        }
808    }
809    myPeers = peers2;
810
811    if( single )
812        myFileTreeView->update( torrents[0]->files( ) , myChangedTorrents );
813    else
814        myFileTreeView->clear( );
815
816    myChangedTorrents = false;
817    myHavePendingRefresh = false;
818    foreach( QWidget * w, myWidgets )
819        w->setEnabled( true );
820}
821
822void
823Details :: enableWhenChecked( QCheckBox * box, QWidget * w )
824{
825    connect( box, SIGNAL(toggled(bool)), w, SLOT(setEnabled(bool)) );
826    w->setEnabled( box->isChecked( ) );
827}
828
829
830/***
831****
832***/
833
834QWidget *
835Details :: createInfoTab( )
836{
837    HIG * hig = new HIG( this );
838
839    hig->addSectionTitle( tr( "Activity" ) );
840    hig->addRow( tr( "Torrent size:" ), mySizeLabel = new SqueezeLabel );
841    hig->addRow( tr( "Have:" ), myHaveLabel = new SqueezeLabel );
842    hig->addRow( tr( "Availability:" ), myAvailabilityLabel = new SqueezeLabel );
843    hig->addRow( tr( "Downloaded:" ), myDownloadedLabel = new SqueezeLabel );
844    hig->addRow( tr( "Uploaded:" ), myUploadedLabel = new SqueezeLabel );
845    hig->addRow( tr( "Ratio:" ), myRatioLabel = new SqueezeLabel );
846    hig->addRow( tr( "State:" ), myStateLabel = new SqueezeLabel );
847    hig->addRow( tr( "Running time:" ), myRunTimeLabel = new SqueezeLabel );
848    hig->addRow( tr( "Remaining time:" ), myETALabel = new SqueezeLabel );
849    hig->addRow( tr( "Last activity:" ), myLastActivityLabel = new SqueezeLabel );
850    hig->addRow( tr( "Error:" ), myErrorLabel = new SqueezeLabel );
851    hig->addSectionDivider( );
852
853    hig->addSectionDivider( );
854    hig->addSectionTitle( tr( "Details" ) );
855    hig->addRow( tr( "Location:" ), myLocationLabel = new SqueezeLabel );
856    hig->addRow( tr( "Hash:" ), myHashLabel = new SqueezeLabel );
857    hig->addRow( tr( "Privacy:" ), myPrivacyLabel = new SqueezeLabel );
858    hig->addRow( tr( "Origin:" ), myOriginLabel = new SqueezeLabel );
859    myOriginLabel->setMinimumWidth( 325 ); // stop long origin strings from resizing the widgit
860    hig->addRow( tr( "Comment:" ), myCommentBrowser = new QTextBrowser );
861    const int h = QFontMetrics(myCommentBrowser->font()).lineSpacing() * 4;
862    myCommentBrowser->setFixedHeight( h );
863
864    hig->finish( );
865
866    return hig;
867}
868
869/***
870****
871***/
872
873void
874Details :: onShowTrackerScrapesToggled( bool val )
875{
876    myPrefs.set( Prefs::SHOW_TRACKER_SCRAPES, val );
877}
878
879void
880Details :: onShowBackupTrackersToggled( bool val )
881{
882    myPrefs.set( Prefs::SHOW_BACKUP_TRACKERS, val );
883}
884
885void
886Details :: onHonorsSessionLimitsToggled( bool val )
887{
888    mySession.torrentSet( myIds, "honorsSessionLimits", val );
889    getNewData( );
890}
891void
892Details :: onDownloadLimitedToggled( bool val )
893{
894    mySession.torrentSet( myIds, "downloadLimited", val );
895    getNewData( );
896}
897void
898Details :: onDownloadLimitChanged( int val )
899{
900    mySession.torrentSet( myIds, "downloadLimit", val );
901    getNewData( );
902}
903void
904Details :: onUploadLimitedToggled( bool val )
905{
906    mySession.torrentSet( myIds, "uploadLimited", val );
907    getNewData( );
908}
909void
910Details :: onUploadLimitChanged( int val )
911{
912    mySession.torrentSet( myIds, "uploadLimit", val );
913    getNewData( );
914}
915
916void
917Details :: onIdleModeChanged( int index )
918{
919    const int val = myIdleCombo->itemData( index ).toInt( );
920    mySession.torrentSet( myIds, "seedIdleMode", val );
921    getNewData( );
922}
923
924void
925Details :: onIdleLimitChanged( int val )
926{
927    mySession.torrentSet( myIds, "seedIdleLimit", val );
928    getNewData( );
929}
930
931void
932Details :: onRatioModeChanged( int index )
933{
934    const int val = myRatioCombo->itemData( index ).toInt( );
935    mySession.torrentSet( myIds, "seedRatioMode", val );
936}
937
938void
939Details :: onRatioLimitChanged( double val )
940{
941    mySession.torrentSet( myIds, "seedRatioLimit", val );
942    getNewData( );
943}
944
945void
946Details :: onMaxPeersChanged( int val )
947{
948    mySession.torrentSet( myIds, "peer-limit", val );
949    getNewData( );
950}
951
952void
953Details :: onBandwidthPriorityChanged( int index )
954{
955    if( index != -1 )
956    {
957        const int priority = myBandwidthPriorityCombo->itemData(index).toInt( );
958        mySession.torrentSet( myIds, "bandwidthPriority", priority );
959        getNewData( );
960    }
961}
962
963void
964Details :: onTrackerSelectionChanged( )
965{
966    const int selectionCount = myTrackerView->selectionModel()->selectedRows().size();
967    myEditTrackerButton->setEnabled( selectionCount == 1 );
968    myRemoveTrackerButton->setEnabled( selectionCount > 0 );
969}
970
971void
972Details :: onAddTrackerClicked( )
973{
974    bool ok = false;
975    const QString url = QInputDialog::getText( this,
976                                               tr( "Add URL " ),
977                                               tr( "Add tracker announce URL:" ),
978                                               QLineEdit::Normal, QString(), &ok );
979    if( !ok )
980    {
981        // user pressed "cancel" -- noop
982    }
983    else if( !QUrl(url).isValid( ) )
984    {
985        QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( url ) );
986    }
987    else
988    {
989        QSet<int> ids;
990
991        foreach( int id, myIds )
992            if( myTrackerModel->find( id, url ) == -1 )
993                ids.insert( id );
994
995        if( ids.empty( ) ) // all the torrents already have this tracker
996        {
997            QMessageBox::warning( this, tr( "Error" ), tr( "Tracker already exists." ) );
998        }
999        else
1000        {
1001            QStringList urls;
1002            urls << url;
1003            mySession.torrentSet( ids, "trackerAdd", urls );
1004            getNewData( );
1005        }
1006    }
1007}
1008
1009void
1010Details :: onEditTrackerClicked( )
1011{
1012    QItemSelectionModel * selectionModel = myTrackerView->selectionModel( );
1013    QModelIndexList selectedRows = selectionModel->selectedRows( );
1014    assert( selectedRows.size( ) == 1 );
1015    QModelIndex i = selectionModel->currentIndex( );
1016    const TrackerInfo trackerInfo = myTrackerView->model()->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>();
1017
1018    bool ok = false;
1019    const QString newval = QInputDialog::getText( this,
1020                                                  tr( "Edit URL " ),
1021                                                  tr( "Edit tracker announce URL:" ),
1022                                                  QLineEdit::Normal,
1023                                                  trackerInfo.st.announce, &ok );
1024
1025    if( !ok )
1026    {
1027        // user pressed "cancel" -- noop
1028    }
1029    else if( !QUrl(newval).isValid( ) )
1030    {
1031        QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( newval ) );
1032    }
1033    else
1034    {
1035        QSet<int> ids;
1036        ids << trackerInfo.torrentId;
1037
1038        const QPair<int,QString> idUrl = qMakePair( trackerInfo.st.id, newval );
1039
1040        mySession.torrentSet( ids, "trackerReplace", idUrl );
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,int> torrentId_to_trackerIds;
1052    foreach( QModelIndex i, selectedRows )
1053    {
1054        const TrackerInfo inf = myTrackerView->model()->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>();
1055        torrentId_to_trackerIds.insertMulti( inf.torrentId, inf.st.id );
1056    }
1057
1058    // batch all of a tracker's torrents into one command
1059    foreach( int id, torrentId_to_trackerIds.uniqueKeys( ) )
1060    {
1061        QSet<int> ids;
1062        ids << id;
1063        mySession.torrentSet( ids, "trackerRemove", torrentId_to_trackerIds.values( id ) );
1064    }
1065
1066    selectionModel->clearSelection( );
1067    getNewData( );
1068}
1069
1070QWidget *
1071Details :: createOptionsTab( )
1072{
1073    QSpinBox * s;
1074    QCheckBox * c;
1075    QComboBox * m;
1076    QHBoxLayout * h;
1077    QDoubleSpinBox * ds;
1078    const QString speed_K_str = Formatter::unitStr( Formatter::SPEED, Formatter::KB );
1079
1080    HIG * hig = new HIG( this );
1081    hig->addSectionTitle( tr( "Speed" ) );
1082
1083    c = new QCheckBox( tr( "Honor global &limits" ) );
1084    mySessionLimitCheck = c;
1085    hig->addWideControl( c );
1086    connect( c, SIGNAL(clicked(bool)), this, SLOT(onHonorsSessionLimitsToggled(bool)) );
1087
1088    c = new QCheckBox( tr( "Limit &download speed (%1):" ).arg( speed_K_str ) );
1089    mySingleDownCheck = c;
1090    s = new QSpinBox( );
1091    s->setSingleStep( 5 );
1092    s->setRange( 0, INT_MAX );
1093    mySingleDownSpin = s;
1094    hig->addRow( c, s );
1095    enableWhenChecked( c, s );
1096    connect( c, SIGNAL(clicked(bool)), this, SLOT(onDownloadLimitedToggled(bool)) );
1097    connect( s, SIGNAL(valueChanged(int)), this, SLOT(onDownloadLimitChanged(int)));
1098
1099    c = new QCheckBox( tr( "Limit &upload speed (%1):" ).arg( speed_K_str ) );
1100    mySingleUpCheck = c;
1101    s = new QSpinBox( );
1102    s->setSingleStep( 5 );
1103    s->setRange( 0, INT_MAX );
1104    mySingleUpSpin = s;
1105    hig->addRow( c, s );
1106    enableWhenChecked( c, s );
1107    connect( c, SIGNAL(clicked(bool)), this, SLOT(onUploadLimitedToggled(bool)) );
1108    connect( s, SIGNAL(valueChanged(int)), this, SLOT(onUploadLimitChanged(int)));
1109
1110    m = new QComboBox;
1111    m->addItem( tr( "High" ),   TR_PRI_HIGH );
1112    m->addItem( tr( "Normal" ), TR_PRI_NORMAL );
1113    m->addItem( tr( "Low" ),    TR_PRI_LOW );
1114    connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onBandwidthPriorityChanged(int)));
1115    hig->addRow( tr( "Torrent &priority:" ), m );
1116    myBandwidthPriorityCombo = m;
1117
1118    hig->addSectionDivider( );
1119    hig->addSectionTitle( tr( "Seeding Limits" ) );
1120
1121    h = new QHBoxLayout( );
1122    h->setSpacing( HIG :: PAD );
1123    m = new QComboBox;
1124    m->addItem( tr( "Use Global Settings" ),      TR_RATIOLIMIT_GLOBAL );
1125    m->addItem( tr( "Seed regardless of ratio" ), TR_RATIOLIMIT_UNLIMITED );
1126    m->addItem( tr( "Stop seeding at ratio:" ),   TR_RATIOLIMIT_SINGLE );
1127    connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onRatioModeChanged(int)));
1128    h->addWidget( myRatioCombo = m );
1129    ds = new QDoubleSpinBox( );
1130    ds->setRange( 0.5, INT_MAX );
1131    connect( ds, SIGNAL(valueChanged(double)), this, SLOT(onRatioLimitChanged(double)));
1132    h->addWidget( myRatioSpin = ds );
1133    hig->addRow( tr( "&Ratio:" ), h, m );
1134
1135    h = new QHBoxLayout( );
1136    h->setSpacing( HIG :: PAD );
1137    m = new QComboBox;
1138    m->addItem( tr( "Use Global Settings" ),                 TR_IDLELIMIT_GLOBAL );
1139    m->addItem( tr( "Seed regardless of activity" ),         TR_IDLELIMIT_UNLIMITED );
1140    m->addItem( tr( "Stop seeding if idle for N minutes:" ), TR_IDLELIMIT_SINGLE );
1141    connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onIdleModeChanged(int)));
1142    h->addWidget( myIdleCombo = m );
1143    s = new QSpinBox( );
1144    s->setSingleStep( 5 );
1145    s->setRange( 1, 9999 );
1146    connect( s, SIGNAL(valueChanged(int)), this, SLOT(onIdleLimitChanged(int)));
1147    h->addWidget( myIdleSpin = s );
1148    hig->addRow( tr( "&Idle:" ), h, m );
1149
1150
1151    hig->addSectionDivider( );
1152    hig->addSectionTitle( tr( "Peer Connections" ) );
1153
1154    s = new QSpinBox( );
1155    s->setSingleStep( 5 );
1156    s->setRange( 1, 300 );
1157    connect( s, SIGNAL(valueChanged(int)), this, SLOT(onMaxPeersChanged(int)));
1158    myPeerLimitSpin = s;
1159    hig->addRow( tr( "&Maximum peers:" ), s );
1160
1161    hig->finish( );
1162
1163    return hig;
1164}
1165
1166/***
1167****
1168***/
1169
1170QWidget *
1171Details :: createTrackerTab( )
1172{
1173    QCheckBox * c;
1174    QPushButton * p;
1175    QWidget * top = new QWidget;
1176    QVBoxLayout * v = new QVBoxLayout( top );
1177    QHBoxLayout * h = new QHBoxLayout();
1178    QVBoxLayout * v2 = new QVBoxLayout();
1179
1180    v->setSpacing( HIG::PAD_BIG );
1181    v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG );
1182
1183    h->setSpacing( HIG::PAD );
1184    h->setContentsMargins( HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL );
1185
1186    v2->setSpacing( HIG::PAD );
1187
1188    myTrackerModel = new TrackerModel;
1189    myTrackerFilter = new TrackerModelFilter;
1190    myTrackerFilter->setSourceModel( myTrackerModel );
1191    myTrackerView = new QTreeView;
1192    myTrackerView->setModel( myTrackerFilter );
1193    myTrackerView->setHeaderHidden( true );
1194    myTrackerView->setSelectionMode( QTreeWidget::ExtendedSelection );
1195    myTrackerView->setRootIsDecorated( false );
1196    myTrackerView->setIndentation( 2 );
1197    myTrackerView->setItemsExpandable( false );
1198    myTrackerView->setAlternatingRowColors( true );
1199    myTrackerView->setItemDelegate( myTrackerDelegate = new TrackerDelegate( ) );
1200    connect( myTrackerView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(onTrackerSelectionChanged()));
1201    h->addWidget( myTrackerView, 1 );
1202
1203    p = new QPushButton();
1204    p->setIcon( getStockIcon( "list-add", QStyle::SP_DialogOpenButton ) );
1205    p->setToolTip( "Add Tracker" );
1206    myAddTrackerButton = p;
1207    v2->addWidget( p, 1 );
1208    connect( p, SIGNAL(clicked(bool)), this, SLOT(onAddTrackerClicked()));
1209
1210    p = new QPushButton();
1211    p->setIcon( getStockIcon( "document-properties", QStyle::SP_DesktopIcon ) );
1212    p->setToolTip( "Edit Tracker" );
1213    myAddTrackerButton = p;
1214    p->setEnabled( false );
1215    myEditTrackerButton = p;
1216    v2->addWidget( p, 1 );
1217    connect( p, SIGNAL(clicked(bool)), this, SLOT(onEditTrackerClicked()));
1218
1219    p = new QPushButton();
1220    p->setIcon( getStockIcon( "list-remove", QStyle::SP_TrashIcon ) );
1221    p->setToolTip( "Remove Trackers" );
1222    p->setEnabled( false );
1223    myRemoveTrackerButton = p;
1224    v2->addWidget( p, 1 );
1225    connect( p, SIGNAL(clicked(bool)), this, SLOT(onRemoveTrackerClicked()));
1226
1227    v2->addStretch( 1 );
1228
1229    h->addLayout( v2, 1 );
1230    h->setStretch( 1, 0 );
1231
1232    v->addLayout( h, 1 );
1233
1234    c = new QCheckBox( tr( "Show &more details" ) );
1235    c->setChecked( myPrefs.getBool( Prefs::SHOW_TRACKER_SCRAPES ) );
1236    myShowTrackerScrapesCheck = c;
1237    v->addWidget( c, 1 );
1238    connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowTrackerScrapesToggled(bool)) );
1239
1240    c = new QCheckBox( tr( "Show &backup trackers" ) );
1241    c->setChecked( myPrefs.getBool( Prefs::SHOW_BACKUP_TRACKERS ) );
1242    myShowBackupTrackersCheck = c;
1243    v->addWidget( c, 1 );
1244    connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowBackupTrackersToggled(bool)) );
1245
1246    return top;
1247}
1248
1249/***
1250****
1251***/
1252
1253QWidget *
1254Details :: createPeersTab( )
1255{
1256    QWidget * top = new QWidget;
1257    QVBoxLayout * v = new QVBoxLayout( top );
1258    v->setSpacing( HIG :: PAD_BIG );
1259    v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG );
1260
1261    QStringList headers;
1262    headers << QString() << tr("Up") << tr("Down") << tr("%") << tr("Status") << tr("Address") << tr("Client");
1263    myPeerTree = new QTreeWidget;
1264    myPeerTree->setUniformRowHeights( true );
1265    myPeerTree->setHeaderLabels( headers );
1266    myPeerTree->setColumnWidth( 0, 20 );
1267    myPeerTree->setSortingEnabled( true );
1268    myPeerTree->sortByColumn( COL_ADDRESS, Qt::AscendingOrder );
1269    myPeerTree->setRootIsDecorated( false );
1270    myPeerTree->setTextElideMode( Qt::ElideRight );
1271    v->addWidget( myPeerTree, 1 );
1272
1273    const QFontMetrics m( font( ) );
1274    QSize size = m.size( 0, "1024 MiB/s" );
1275    myPeerTree->setColumnWidth( COL_UP, size.width( ) );
1276    myPeerTree->setColumnWidth( COL_DOWN, size.width( ) );
1277    size = m.size( 0, " 100% " );
1278    myPeerTree->setColumnWidth( COL_PERCENT, size.width( ) );
1279    size = m.size( 0, "ODUK?EXI" );
1280    myPeerTree->setColumnWidth( COL_STATUS, size.width( ) );
1281    size = m.size( 0, "888.888.888.888" );
1282    myPeerTree->setColumnWidth( COL_ADDRESS, size.width( ) );
1283    size = m.size( 0, "Some BitTorrent Client" );
1284    myPeerTree->setColumnWidth( COL_CLIENT, size.width( ) );
1285    myPeerTree->setAlternatingRowColors( true );
1286
1287    return top;
1288}
1289
1290/***
1291****
1292***/
1293
1294QWidget *
1295Details :: createFilesTab( )
1296{
1297    myFileTreeView = new FileTreeView( );
1298
1299    connect( myFileTreeView, SIGNAL(      priorityChanged(const QSet<int>&, int)),
1300             this,           SLOT(  onFilePriorityChanged(const QSet<int>&, int)));
1301
1302    connect( myFileTreeView, SIGNAL(      wantedChanged(const QSet<int>&, bool)),
1303             this,           SLOT(  onFileWantedChanged(const QSet<int>&, bool)));
1304
1305    return myFileTreeView;
1306}
1307
1308void
1309Details :: onFilePriorityChanged( const QSet<int>& indices, int priority )
1310{
1311    QString key;
1312    switch( priority ) {
1313        case TR_PRI_LOW:   key = "priority-low"; break;
1314        case TR_PRI_HIGH:  key = "priority-high"; break;
1315        default:           key = "priority-normal"; break;
1316    }
1317    mySession.torrentSet( myIds, key, indices.toList( ) );
1318    getNewData( );
1319}
1320
1321void
1322Details :: onFileWantedChanged( const QSet<int>& indices, bool wanted )
1323{
1324    QString key( wanted ? "files-wanted" : "files-unwanted" );
1325    mySession.torrentSet( myIds, key, indices.toList( ) );
1326    getNewData( );
1327}
Note: See TracBrowser for help on using the repository browser.