source: branches/2.7x/qt/details.cc @ 13942

Last change on this file since 13942 was 13942, checked in by jordan, 9 years ago

(2.7x) backport r13940 to fix bug #5270 in 2.7x

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