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 13076 2011-11-05 15:45:38Z 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    myCommentBrowser->setText( string );
637    myCommentBrowser->setMaximumHeight( QWIDGETSIZE_MAX );
638
639    // myOriginLabel
640    if( torrents.empty( ) )
641        string = none;
642    else {
643        bool mixed_creator=false, mixed_date=false;
644        const QString creator = torrents[0]->creator();
645        const QString date = torrents[0]->dateCreated().toString();
646        foreach( const Torrent * t, torrents ) {
647            mixed_creator |= ( creator != t->creator() );
648            mixed_date |=  ( date != t->dateCreated().toString() );
649        }
650        if( mixed_creator && mixed_date )
651            string = mixed;
652        else if( mixed_date && !creator.isEmpty())
653            string = tr( "Created by %1" ).arg( creator );
654        else if( mixed_creator && !date.isEmpty())
655            string = tr( "Created on %1" ).arg( date );
656        else if( creator.isEmpty() && date.isEmpty())
657            string = tr( "N/A" );
658        else
659            string = tr( "Created by %1 on %2" ).arg( creator ).arg( date );
660    }
661    myOriginLabel->setText( string );
662
663    // myLocationLabel
664    if( torrents.empty( ) )
665        string = none;
666    else {
667        string = torrents[0]->getPath( );
668        foreach( const Torrent * t, torrents ) {
669            if( string != t->getPath( ) ) {
670                string = mixed;
671                break;
672            }
673        }
674    }
675    myLocationLabel->setText( string );
676
677
678    ///
679    ///  Options Tab
680    ///
681
682    if( myChangedTorrents && !torrents.empty( ) )
683    {
684        int i;
685        const Torrent * baseline = *torrents.begin();
686        const Torrent * tor;
687        bool uniform;
688        bool baselineFlag;
689        int baselineInt;
690
691        // mySessionLimitCheck
692        uniform = true;
693        baselineFlag = baseline->honorsSessionLimits( );
694        foreach( tor, torrents ) if( baselineFlag != tor->honorsSessionLimits( ) ) { uniform = false; break; }
695        mySessionLimitCheck->setChecked( uniform && baselineFlag );
696
697        // mySingleDownCheck
698        uniform = true;
699        baselineFlag = baseline->downloadIsLimited( );
700        foreach( tor, torrents ) if( baselineFlag != tor->downloadIsLimited( ) ) { uniform = false; break; }
701        mySingleDownCheck->setChecked( uniform && baselineFlag );
702
703        // mySingleUpCheck
704        uniform = true;
705        baselineFlag = baseline->uploadIsLimited( );
706        foreach( tor, torrents ) if( baselineFlag != tor->uploadIsLimited( ) ) { uniform = false; break; }
707        mySingleUpCheck->setChecked( uniform && baselineFlag );
708
709        // myBandwidthPriorityCombo
710        uniform = true;
711        baselineInt = baseline->getBandwidthPriority( );
712        foreach( tor, torrents ) if ( baselineInt != tor->getBandwidthPriority( ) ) { uniform = false; break; }
713        if( uniform )
714            i = myBandwidthPriorityCombo->findData( baselineInt );
715        else
716            i = -1;
717        setIfIdle( myBandwidthPriorityCombo, i );
718
719        setIfIdle( mySingleDownSpin, int(tor->downloadLimit().KBps()) );
720        setIfIdle( mySingleUpSpin, int(tor->uploadLimit().KBps()) );
721        setIfIdle( myPeerLimitSpin, tor->peerLimit() );
722    }
723
724    if( !torrents.empty( ) )
725    {
726        const Torrent * tor;
727
728        // ratio
729        bool uniform = true;
730        int baselineInt = torrents[0]->seedRatioMode( );
731        foreach( tor, torrents ) if( baselineInt != tor->seedRatioMode( ) ) { uniform = false; break; }
732
733        setIfIdle( myRatioCombo, uniform ? myRatioCombo->findData( baselineInt ) : -1 );
734        myRatioSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) );
735
736        setIfIdle( myRatioSpin, tor->seedRatioLimit( ) );
737
738        // idle
739        uniform = true;
740        baselineInt = torrents[0]->seedIdleMode( );
741        foreach( tor, torrents ) if( baselineInt != tor->seedIdleMode( ) ) { uniform = false; break; }
742
743        setIfIdle( myIdleCombo, uniform ? myIdleCombo->findData( baselineInt ) : -1 );
744        myIdleSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) );
745
746        setIfIdle( myIdleSpin, tor->seedIdleLimit( ) );
747    }
748
749    ///
750    ///  Tracker tab
751    ///
752
753    myTrackerModel->refresh( myModel, myIds );
754
755    ///
756    ///  Peers tab
757    ///
758
759    QMap<QString,QTreeWidgetItem*> peers2;
760    QList<QTreeWidgetItem*> newItems;
761    foreach( const Torrent * t, torrents )
762    {
763        const QString idStr( QString::number( t->id( ) ) );
764        PeerList peers = t->peers( );
765
766        foreach( const Peer& peer, peers )
767        {
768            const QString key = idStr + ":" + peer.address;
769            PeerItem * item = (PeerItem*) myPeers.value( key, 0 );
770
771            if( item == 0 ) // new peer has connected
772            {
773                static const QIcon myEncryptionIcon( ":/icons/encrypted.png" );
774                static const QIcon myEmptyIcon;
775                item = new PeerItem( peer );
776                item->setTextAlignment( COL_UP, Qt::AlignRight|Qt::AlignVCenter );
777                item->setTextAlignment( COL_DOWN, Qt::AlignRight|Qt::AlignVCenter );
778                item->setTextAlignment( COL_PERCENT, Qt::AlignRight|Qt::AlignVCenter );
779                item->setIcon( COL_LOCK, peer.isEncrypted ? myEncryptionIcon : myEmptyIcon );
780                item->setToolTip( COL_LOCK, peer.isEncrypted ? tr( "Encrypted connection" ) : "" );
781                item->setText( COL_ADDRESS, peer.address );
782                item->setText( COL_CLIENT, peer.clientName );
783                newItems << item;
784            }
785
786            const QString code = peer.flagStr;
787            item->setStatus( code );
788            item->refresh( peer );
789
790            QString codeTip;
791            foreach( QChar ch, code ) {
792                QString txt;
793                switch( ch.toAscii() ) {
794                    case 'O': txt = tr( "Optimistic unchoke" ); break;
795                    case 'D': txt = tr( "Downloading from this peer" ); break;
796                    case 'd': txt = tr( "We would download from this peer if they would let us" ); break;
797                    case 'U': txt = tr( "Uploading to peer" ); break;
798                    case 'u': txt = tr( "We would upload to this peer if they asked" ); break;
799                    case 'K': txt = tr( "Peer has unchoked us, but we're not interested" ); break;
800                    case '?': txt = tr( "We unchoked this peer, but they're not interested" ); break;
801                    case 'E': txt = tr( "Encrypted connection" ); break;
802                    case 'H': txt = tr( "Peer was discovered through DHT" ); break;
803                    case 'X': txt = tr( "Peer was discovered through Peer Exchange (PEX)" ); break;
804                    case 'I': txt = tr( "Peer is an incoming connection" ); break;
805                    case 'T': txt = tr( "Peer is connected over uTP" ); break;
806                }
807                if( !txt.isEmpty( ) )
808                    codeTip += QString("%1: %2\n").arg(ch).arg(txt);
809            }
810
811            if( !codeTip.isEmpty() )
812                codeTip.resize( codeTip.size()-1 ); // eat the trailing linefeed
813
814            item->setText( COL_UP, peer.rateToPeer.isZero() ? "" : Formatter::speedToString( peer.rateToPeer ) );
815            item->setText( COL_DOWN, peer.rateToClient.isZero() ? "" : Formatter::speedToString( peer.rateToClient ) );
816            item->setText( COL_PERCENT, peer.progress > 0 ? QString( "%1%" ).arg( (int)( peer.progress * 100.0 ) ) : "" );
817            item->setText( COL_STATUS, code );
818            item->setToolTip( COL_STATUS, codeTip );
819
820            peers2.insert( key, item );
821        }
822    }
823    myPeerTree->addTopLevelItems( newItems );
824    foreach( QString key, myPeers.keys() ) {
825        if( !peers2.contains( key ) ) { // old peer has disconnected
826            QTreeWidgetItem * item = myPeers.value( key, 0 );
827            myPeerTree->takeTopLevelItem( myPeerTree->indexOfTopLevelItem( item ) );
828            delete item;
829        }
830    }
831    myPeers = peers2;
832
833    if( single )
834        myFileTreeView->update( torrents[0]->files( ) , myChangedTorrents );
835    else
836        myFileTreeView->clear( );
837
838    myChangedTorrents = false;
839    myHavePendingRefresh = false;
840    foreach( QWidget * w, myWidgets )
841        w->setEnabled( true );
842}
843
844void
845Details :: enableWhenChecked( QCheckBox * box, QWidget * w )
846{
847    connect( box, SIGNAL(toggled(bool)), w, SLOT(setEnabled(bool)) );
848    w->setEnabled( box->isChecked( ) );
849}
850
851
852/***
853****
854***/
855
856QWidget *
857Details :: createInfoTab( )
858{
859    HIG * hig = new HIG( this );
860
861    hig->addSectionTitle( tr( "Activity" ) );
862    hig->addRow( tr( "Have:" ), myHaveLabel = new SqueezeLabel );
863    hig->addRow( tr( "Availability:" ), myAvailabilityLabel = new SqueezeLabel );
864    hig->addRow( tr( "Downloaded:" ), myDownloadedLabel = new SqueezeLabel );
865    hig->addRow( tr( "Uploaded:" ), myUploadedLabel = new SqueezeLabel );
866    hig->addRow( tr( "State:" ), myStateLabel = new SqueezeLabel );
867    hig->addRow( tr( "Running time:" ), myRunTimeLabel = new SqueezeLabel );
868    hig->addRow( tr( "Remaining time:" ), myETALabel = new SqueezeLabel );
869    hig->addRow( tr( "Last activity:" ), myLastActivityLabel = new SqueezeLabel );
870    hig->addRow( tr( "Error:" ), myErrorLabel = new SqueezeLabel );
871    hig->addSectionDivider( );
872
873    hig->addSectionDivider( );
874    hig->addSectionTitle( tr( "Details" ) );
875    hig->addRow( tr( "Size:" ), mySizeLabel = new SqueezeLabel );
876    hig->addRow( tr( "Location:" ), myLocationLabel = new SqueezeLabel );
877    hig->addRow( tr( "Hash:" ), myHashLabel = new SqueezeLabel );
878    hig->addRow( tr( "Privacy:" ), myPrivacyLabel = new SqueezeLabel );
879    hig->addRow( tr( "Origin:" ), myOriginLabel = new SqueezeLabel );
880    myOriginLabel->setMinimumWidth( 325 ); // stop long origin strings from resizing the widgit
881    hig->addRow( tr( "Comment:" ), myCommentBrowser = new QTextBrowser );
882    const int h = QFontMetrics(myCommentBrowser->font()).lineSpacing() * 4;
883    myCommentBrowser->setFixedHeight( h );
884
885    hig->finish( );
886
887    return hig;
888}
889
890/***
891****
892***/
893
894void
895Details :: onShowTrackerScrapesToggled( bool val )
896{
897    myPrefs.set( Prefs::SHOW_TRACKER_SCRAPES, val );
898}
899
900void
901Details :: onShowBackupTrackersToggled( bool val )
902{
903    myPrefs.set( Prefs::SHOW_BACKUP_TRACKERS, val );
904}
905
906void
907Details :: onHonorsSessionLimitsToggled( bool val )
908{
909    mySession.torrentSet( myIds, "honorsSessionLimits", val );
910    getNewData( );
911}
912void
913Details :: onDownloadLimitedToggled( bool val )
914{
915    mySession.torrentSet( myIds, "downloadLimited", val );
916    getNewData( );
917}
918void
919Details :: onSpinBoxEditingFinished( )
920{
921    const QObject * spin = sender();
922    const QString key = spin->property( PREF_KEY ).toString( );
923    const QDoubleSpinBox * d = qobject_cast<const QDoubleSpinBox*>( spin );
924    if( d )
925        mySession.torrentSet( myIds, key, d->value( ) );
926    else
927        mySession.torrentSet( myIds, key, qobject_cast<const QSpinBox*>(spin)->value( ) );
928    getNewData( );
929}
930
931void
932Details :: onUploadLimitedToggled( bool val )
933{
934    mySession.torrentSet( myIds, "uploadLimited", val );
935    getNewData( );
936}
937
938void
939Details :: onIdleModeChanged( int index )
940{
941    const int val = myIdleCombo->itemData( index ).toInt( );
942    mySession.torrentSet( myIds, "seedIdleMode", val );
943    getNewData( );
944}
945
946void
947Details :: onRatioModeChanged( int index )
948{
949    const int val = myRatioCombo->itemData( index ).toInt( );
950    mySession.torrentSet( myIds, "seedRatioMode", val );
951}
952
953void
954Details :: onBandwidthPriorityChanged( int index )
955{
956    if( index != -1 )
957    {
958        const int priority = myBandwidthPriorityCombo->itemData(index).toInt( );
959        mySession.torrentSet( myIds, "bandwidthPriority", priority );
960        getNewData( );
961    }
962}
963
964void
965Details :: onTrackerSelectionChanged( )
966{
967    const int selectionCount = myTrackerView->selectionModel()->selectedRows().size();
968    myEditTrackerButton->setEnabled( selectionCount == 1 );
969    myRemoveTrackerButton->setEnabled( selectionCount > 0 );
970}
971
972void
973Details :: onAddTrackerClicked( )
974{
975    bool ok = false;
976    const QString url = QInputDialog::getText( this,
977                                               tr( "Add URL " ),
978                                               tr( "Add tracker announce URL:" ),
979                                               QLineEdit::Normal, QString(), &ok );
980    if( !ok )
981    {
982        // user pressed "cancel" -- noop
983    }
984    else if( !QUrl(url).isValid( ) )
985    {
986        QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( url ) );
987    }
988    else
989    {
990        QSet<int> ids;
991
992        foreach( int id, myIds )
993            if( myTrackerModel->find( id, url ) == -1 )
994                ids.insert( id );
995
996        if( ids.empty( ) ) // all the torrents already have this tracker
997        {
998            QMessageBox::warning( this, tr( "Error" ), tr( "Tracker already exists." ) );
999        }
1000        else
1001        {
1002            QStringList urls;
1003            urls << url;
1004            mySession.torrentSet( ids, "trackerAdd", urls );
1005            getNewData( );
1006        }
1007    }
1008}
1009
1010void
1011Details :: onEditTrackerClicked( )
1012{
1013    QItemSelectionModel * selectionModel = myTrackerView->selectionModel( );
1014    QModelIndexList selectedRows = selectionModel->selectedRows( );
1015    assert( selectedRows.size( ) == 1 );
1016    QModelIndex i = selectionModel->currentIndex( );
1017    const TrackerInfo trackerInfo = myTrackerView->model()->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>();
1018
1019    bool ok = false;
1020    const QString newval = QInputDialog::getText( this,
1021                                                  tr( "Edit URL " ),
1022                                                  tr( "Edit tracker announce URL:" ),
1023                                                  QLineEdit::Normal,
1024                                                  trackerInfo.st.announce, &ok );
1025
1026    if( !ok )
1027    {
1028        // user pressed "cancel" -- noop
1029    }
1030    else if( !QUrl(newval).isValid( ) )
1031    {
1032        QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( newval ) );
1033    }
1034    else
1035    {
1036        QSet<int> ids;
1037        ids << trackerInfo.torrentId;
1038
1039        const QPair<int,QString> idUrl = qMakePair( trackerInfo.st.id, newval );
1040
1041        mySession.torrentSet( ids, "trackerReplace", idUrl );
1042        getNewData( );
1043    }
1044}
1045
1046void
1047Details :: onRemoveTrackerClicked( )
1048{
1049    // make a map of torrentIds to announce URLs to remove
1050    QItemSelectionModel * selectionModel = myTrackerView->selectionModel( );
1051    QModelIndexList selectedRows = selectionModel->selectedRows( );
1052    QMap<int,int> torrentId_to_trackerIds;
1053    foreach( QModelIndex i, selectedRows )
1054    {
1055        const TrackerInfo inf = myTrackerView->model()->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>();
1056        torrentId_to_trackerIds.insertMulti( inf.torrentId, inf.st.id );
1057    }
1058
1059    // batch all of a tracker's torrents into one command
1060    foreach( int id, torrentId_to_trackerIds.uniqueKeys( ) )
1061    {
1062        QSet<int> ids;
1063        ids << id;
1064        mySession.torrentSet( ids, "trackerRemove", torrentId_to_trackerIds.values( id ) );
1065    }
1066
1067    selectionModel->clearSelection( );
1068    getNewData( );
1069}
1070
1071QWidget *
1072Details :: createOptionsTab( )
1073{
1074    QSpinBox * s;
1075    QCheckBox * c;
1076    QComboBox * m;
1077    QHBoxLayout * h;
1078    QDoubleSpinBox * ds;
1079    const QString speed_K_str = Formatter::unitStr( Formatter::SPEED, Formatter::KB );
1080
1081    HIG * hig = new HIG( this );
1082    hig->addSectionTitle( tr( "Speed" ) );
1083
1084    c = new QCheckBox( tr( "Honor global &limits" ) );
1085    mySessionLimitCheck = c;
1086    hig->addWideControl( c );
1087    connect( c, SIGNAL(clicked(bool)), this, SLOT(onHonorsSessionLimitsToggled(bool)) );
1088
1089    c = new QCheckBox( tr( "Limit &download speed (%1):" ).arg( speed_K_str ) );
1090    mySingleDownCheck = c;
1091    s = new QSpinBox( );
1092    s->setProperty( PREF_KEY, QString( "downloadLimit" ) );
1093    s->setSingleStep( 5 );
1094    s->setRange( 0, INT_MAX );
1095    mySingleDownSpin = s;
1096    hig->addRow( c, s );
1097    enableWhenChecked( c, s );
1098    connect( c, SIGNAL(clicked(bool)), this, SLOT(onDownloadLimitedToggled(bool)) );
1099    connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished()));
1100
1101    c = new QCheckBox( tr( "Limit &upload speed (%1):" ).arg( speed_K_str ) );
1102    mySingleUpCheck = c;
1103    s = new QSpinBox( );
1104    s->setSingleStep( 5 );
1105    s->setRange( 0, INT_MAX );
1106    s->setProperty( PREF_KEY, QString( "uploadLimit" ) );
1107    mySingleUpSpin = s;
1108    hig->addRow( c, s );
1109    enableWhenChecked( c, s );
1110    connect( c, SIGNAL(clicked(bool)), this, SLOT(onUploadLimitedToggled(bool)) );
1111    connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished()));
1112
1113    m = new QComboBox;
1114    m->addItem( tr( "High" ),   TR_PRI_HIGH );
1115    m->addItem( tr( "Normal" ), TR_PRI_NORMAL );
1116    m->addItem( tr( "Low" ),    TR_PRI_LOW );
1117    connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onBandwidthPriorityChanged(int)));
1118    hig->addRow( tr( "Torrent &priority:" ), m );
1119    myBandwidthPriorityCombo = m;
1120
1121    hig->addSectionDivider( );
1122    hig->addSectionTitle( tr( "Seeding Limits" ) );
1123
1124    h = new QHBoxLayout( );
1125    h->setSpacing( HIG :: PAD );
1126    m = new QComboBox;
1127    m->addItem( tr( "Use Global Settings" ),      TR_RATIOLIMIT_GLOBAL );
1128    m->addItem( tr( "Seed regardless of ratio" ), TR_RATIOLIMIT_UNLIMITED );
1129    m->addItem( tr( "Stop seeding at ratio:" ),   TR_RATIOLIMIT_SINGLE );
1130    connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onRatioModeChanged(int)));
1131    h->addWidget( myRatioCombo = m );
1132    ds = new QDoubleSpinBox( );
1133    ds->setRange( 0.5, INT_MAX );
1134    ds->setProperty( PREF_KEY, QString( "seedRatioLimit" ) );
1135    connect( ds, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished()));
1136    h->addWidget( myRatioSpin = ds );
1137    hig->addRow( tr( "&Ratio:" ), h, m );
1138
1139    h = new QHBoxLayout( );
1140    h->setSpacing( HIG :: PAD );
1141    m = new QComboBox;
1142    m->addItem( tr( "Use Global Settings" ),                 TR_IDLELIMIT_GLOBAL );
1143    m->addItem( tr( "Seed regardless of activity" ),         TR_IDLELIMIT_UNLIMITED );
1144    m->addItem( tr( "Stop seeding if idle for N minutes:" ), TR_IDLELIMIT_SINGLE );
1145    connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onIdleModeChanged(int)));
1146    h->addWidget( myIdleCombo = m );
1147    s = new QSpinBox( );
1148    s->setSingleStep( 5 );
1149    s->setRange( 1, 9999 );
1150    s->setProperty( PREF_KEY, QString( "seedIdleLimit" ) );
1151    connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished()));
1152    h->addWidget( myIdleSpin = s );
1153    hig->addRow( tr( "&Idle:" ), h, m );
1154
1155
1156    hig->addSectionDivider( );
1157    hig->addSectionTitle( tr( "Peer Connections" ) );
1158
1159    s = new QSpinBox( );
1160    s->setSingleStep( 5 );
1161    s->setRange( 1, 300 );
1162    s->setProperty( PREF_KEY, QString( "peer-limit" ) );
1163    connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished()));
1164    myPeerLimitSpin = s;
1165    hig->addRow( tr( "&Maximum peers:" ), s );
1166
1167    hig->finish( );
1168
1169    return hig;
1170}
1171
1172/***
1173****
1174***/
1175
1176QWidget *
1177Details :: createTrackerTab( )
1178{
1179    QCheckBox * c;
1180    QPushButton * p;
1181    QWidget * top = new QWidget;
1182    QVBoxLayout * v = new QVBoxLayout( top );
1183    QHBoxLayout * h = new QHBoxLayout();
1184    QVBoxLayout * v2 = new QVBoxLayout();
1185
1186    v->setSpacing( HIG::PAD_BIG );
1187    v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG );
1188
1189    h->setSpacing( HIG::PAD );
1190    h->setContentsMargins( HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL );
1191
1192    v2->setSpacing( HIG::PAD );
1193
1194    myTrackerModel = new TrackerModel;
1195    myTrackerFilter = new TrackerModelFilter;
1196    myTrackerFilter->setSourceModel( myTrackerModel );
1197    myTrackerView = new QTreeView;
1198    myTrackerView->setModel( myTrackerFilter );
1199    myTrackerView->setHeaderHidden( true );
1200    myTrackerView->setSelectionMode( QTreeWidget::ExtendedSelection );
1201    myTrackerView->setRootIsDecorated( false );
1202    myTrackerView->setIndentation( 2 );
1203    myTrackerView->setItemsExpandable( false );
1204    myTrackerView->setAlternatingRowColors( true );
1205    myTrackerView->setItemDelegate( myTrackerDelegate = new TrackerDelegate( ) );
1206    connect( myTrackerView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(onTrackerSelectionChanged()));
1207    h->addWidget( myTrackerView, 1 );
1208
1209    p = new QPushButton();
1210    p->setIcon( getStockIcon( "list-add", QStyle::SP_DialogOpenButton ) );
1211    p->setToolTip( "Add Tracker" );
1212    myAddTrackerButton = p;
1213    v2->addWidget( p, 1 );
1214    connect( p, SIGNAL(clicked(bool)), this, SLOT(onAddTrackerClicked()));
1215
1216    p = new QPushButton();
1217    p->setIcon( getStockIcon( "document-properties", QStyle::SP_DesktopIcon ) );
1218    p->setToolTip( "Edit Tracker" );
1219    myAddTrackerButton = p;
1220    p->setEnabled( false );
1221    myEditTrackerButton = p;
1222    v2->addWidget( p, 1 );
1223    connect( p, SIGNAL(clicked(bool)), this, SLOT(onEditTrackerClicked()));
1224
1225    p = new QPushButton();
1226    p->setIcon( getStockIcon( "list-remove", QStyle::SP_TrashIcon ) );
1227    p->setToolTip( "Remove Trackers" );
1228    p->setEnabled( false );
1229    myRemoveTrackerButton = p;
1230    v2->addWidget( p, 1 );
1231    connect( p, SIGNAL(clicked(bool)), this, SLOT(onRemoveTrackerClicked()));
1232
1233    v2->addStretch( 1 );
1234
1235    h->addLayout( v2, 1 );
1236    h->setStretch( 1, 0 );
1237
1238    v->addLayout( h, 1 );
1239
1240    c = new QCheckBox( tr( "Show &more details" ) );
1241    c->setChecked( myPrefs.getBool( Prefs::SHOW_TRACKER_SCRAPES ) );
1242    myShowTrackerScrapesCheck = c;
1243    v->addWidget( c, 1 );
1244    connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowTrackerScrapesToggled(bool)) );
1245
1246    c = new QCheckBox( tr( "Show &backup trackers" ) );
1247    c->setChecked( myPrefs.getBool( Prefs::SHOW_BACKUP_TRACKERS ) );
1248    myShowBackupTrackersCheck = c;
1249    v->addWidget( c, 1 );
1250    connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowBackupTrackersToggled(bool)) );
1251
1252    return top;
1253}
1254
1255/***
1256****
1257***/
1258
1259QWidget *
1260Details :: createPeersTab( )
1261{
1262    QWidget * top = new QWidget;
1263    QVBoxLayout * v = new QVBoxLayout( top );
1264    v->setSpacing( HIG :: PAD_BIG );
1265    v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG );
1266
1267    QStringList headers;
1268    headers << QString() << tr("Up") << tr("Down") << tr("%") << tr("Status") << tr("Address") << tr("Client");
1269    myPeerTree = new QTreeWidget;
1270    myPeerTree->setUniformRowHeights( true );
1271    myPeerTree->setHeaderLabels( headers );
1272    myPeerTree->setColumnWidth( 0, 20 );
1273    myPeerTree->setSortingEnabled( true );
1274    myPeerTree->sortByColumn( COL_ADDRESS, Qt::AscendingOrder );
1275    myPeerTree->setRootIsDecorated( false );
1276    myPeerTree->setTextElideMode( Qt::ElideRight );
1277    v->addWidget( myPeerTree, 1 );
1278
1279    const QFontMetrics m( font( ) );
1280    QSize size = m.size( 0, "1024 MiB/s" );
1281    myPeerTree->setColumnWidth( COL_UP, size.width( ) );
1282    myPeerTree->setColumnWidth( COL_DOWN, size.width( ) );
1283    size = m.size( 0, " 100% " );
1284    myPeerTree->setColumnWidth( COL_PERCENT, size.width( ) );
1285    size = m.size( 0, "ODUK?EXI" );
1286    myPeerTree->setColumnWidth( COL_STATUS, size.width( ) );
1287    size = m.size( 0, "888.888.888.888" );
1288    myPeerTree->setColumnWidth( COL_ADDRESS, size.width( ) );
1289    size = m.size( 0, "Some BitTorrent Client" );
1290    myPeerTree->setColumnWidth( COL_CLIENT, size.width( ) );
1291    myPeerTree->setAlternatingRowColors( true );
1292
1293    return top;
1294}
1295
1296/***
1297****
1298***/
1299
1300QWidget *
1301Details :: createFilesTab( )
1302{
1303    myFileTreeView = new FileTreeView( );
1304
1305    connect( myFileTreeView, SIGNAL(      priorityChanged(const QSet<int>&, int)),
1306             this,           SLOT(  onFilePriorityChanged(const QSet<int>&, int)));
1307
1308    connect( myFileTreeView, SIGNAL(      wantedChanged(const QSet<int>&, bool)),
1309             this,           SLOT(  onFileWantedChanged(const QSet<int>&, bool)));
1310
1311    return myFileTreeView;
1312}
1313
1314void
1315Details :: onFilePriorityChanged( const QSet<int>& indices, int priority )
1316{
1317    QString key;
1318    switch( priority ) {
1319        case TR_PRI_LOW:   key = "priority-low"; break;
1320        case TR_PRI_HIGH:  key = "priority-high"; break;
1321        default:           key = "priority-normal"; break;
1322    }
1323    mySession.torrentSet( myIds, key, indices.toList( ) );
1324    getNewData( );
1325}
1326
1327void
1328Details :: onFileWantedChanged( const QSet<int>& indices, bool wanted )
1329{
1330    QString key( wanted ? "files-wanted" : "files-unwanted" );
1331    mySession.torrentSet( myIds, key, indices.toList( ) );
1332    getNewData( );
1333}
1334