/* * This file Copyright (C) Mnemosyne LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * $Id$ */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // tr_getRatio() #include "details.h" #include "file-tree.h" #include "formatter.h" #include "hig.h" #include "prefs.h" #include "session.h" #include "squeezelabel.h" #include "torrent.h" #include "torrent-model.h" #include "tracker-delegate.h" #include "tracker-model.h" #include "tracker-model-filter.h" class Prefs; class Session; /**** ***** ****/ namespace { const int REFRESH_INTERVAL_MSEC = 4000; const char * PREF_KEY( "pref-key" ); enum // peer columns { COL_LOCK, COL_UP, COL_DOWN, COL_PERCENT, COL_STATUS, COL_ADDRESS, COL_CLIENT, N_COLUMNS }; } /*** **** ***/ class PeerItem: public QTreeWidgetItem { Peer peer; QString collatedAddress; QString status; public: virtual ~PeerItem( ) { } PeerItem( const Peer& p ) { peer = p; int q[4]; if( sscanf( p.address.toUtf8().constData(), "%d.%d.%d.%d", q+0, q+1, q+2, q+3 ) == 4 ) collatedAddress.sprintf( "%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3] ); else collatedAddress = p.address; } public: void refresh( const Peer& p ) { peer = p; } void setStatus( const QString& s ) { status = s; } virtual bool operator< ( const QTreeWidgetItem & other ) const { const PeerItem * i = dynamic_cast(&other); QTreeWidget * tw( treeWidget( ) ); const int column = tw ? tw->sortColumn() : 0; switch( column ) { case COL_UP: return peer.rateToPeer < i->peer.rateToPeer; case COL_DOWN: return peer.rateToClient < i->peer.rateToClient; case COL_PERCENT: return peer.progress < i->peer.progress; case COL_STATUS: return status < i->status; case COL_CLIENT: return peer.clientName < i->peer.clientName; case COL_LOCK: return peer.isEncrypted && !i->peer.isEncrypted; default: return collatedAddress < i->collatedAddress; } } }; /*** **** ***/ QIcon Details :: getStockIcon( const QString& freedesktop_name, int fallback ) { QIcon icon = QIcon::fromTheme( freedesktop_name ); if( icon.isNull( ) ) icon = style()->standardIcon( QStyle::StandardPixmap( fallback ), 0, this ); return icon; } Details :: Details( Session& session, Prefs& prefs, TorrentModel& model, QWidget * parent ): QDialog( parent, Qt::Dialog ), mySession( session ), myPrefs( prefs ), myModel( model ), myChangedTorrents( false ), myHavePendingRefresh( false ) { QVBoxLayout * layout = new QVBoxLayout( this ); setWindowTitle( tr( "Torrent Properties" ) ); QTabWidget * t = new QTabWidget( this ); QWidget * w; t->addTab( w = createInfoTab( ), tr( "Information" ) ); myWidgets << w; t->addTab( w = createPeersTab( ), tr( "Peers" ) ); myWidgets << w; t->addTab( w = createTrackerTab( ), tr( "Tracker" ) ); myWidgets << w; t->addTab( w = createFilesTab( ), tr( "Files" ) ); myWidgets << w; t->addTab( w = createOptionsTab( ), tr( "Options" ) ); myWidgets << w; layout->addWidget( t ); QDialogButtonBox * buttons = new QDialogButtonBox( QDialogButtonBox::Close, Qt::Horizontal, this ); connect( buttons, SIGNAL(rejected()), this, SLOT(close())); layout->addWidget( buttons ); QWidget::setAttribute( Qt::WA_DeleteOnClose, true ); QList initKeys; initKeys << Prefs :: SHOW_TRACKER_SCRAPES << Prefs :: SHOW_BACKUP_TRACKERS; foreach( int key, initKeys ) refreshPref( key ); connect( &myTimer, SIGNAL(timeout()), this, SLOT(onTimer())); connect( &myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(int)) ); onTimer( ); myTimer.setSingleShot( false ); myTimer.start( REFRESH_INTERVAL_MSEC ); } Details :: ~Details( ) { myTrackerDelegate->deleteLater(); myTrackerFilter->deleteLater(); myTrackerModel->deleteLater(); } void Details :: setIds( const QSet& ids ) { if( ids == myIds ) return; myChangedTorrents = true; // stop listening to the old torrents foreach( int id, myIds ) { const Torrent * tor = myModel.getTorrentFromId( id ); if( tor ) { disconnect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) ); disconnect( tor, SIGNAL(torrentFileListRebuilt(int)), this, SLOT(onTorrentFileListRebuilt()) ); } } myFileTreeView->clear( ); myIds = ids; myTrackerModel->refresh( myModel, myIds ); // listen to the new torrents foreach( int id, myIds ) { const Torrent * tor = myModel.getTorrentFromId( id ); if( tor ) { connect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged()) ); connect( tor, SIGNAL(torrentFileListRebuilt(int)), this, SLOT(onTorrentFileListRebuilt()) ); } } foreach( QWidget * w, myWidgets ) w->setEnabled( false ); onTimer( ); } void Details :: refreshPref( int key ) { QString str; switch( key ) { case Prefs :: SHOW_TRACKER_SCRAPES: { QItemSelectionModel * selectionModel( myTrackerView->selectionModel( ) ); const QItemSelection selection( selectionModel->selection( ) ); const QModelIndex currentIndex( selectionModel->currentIndex( ) ); myTrackerDelegate->setShowMore( myPrefs.getBool( key ) ); selectionModel->clear( ); myTrackerView->reset( ); selectionModel->select( selection, QItemSelectionModel::Select ); selectionModel->setCurrentIndex( currentIndex, QItemSelectionModel::NoUpdate ); break; } case Prefs :: SHOW_BACKUP_TRACKERS: myTrackerFilter->setShowBackupTrackers( myPrefs.getBool( key ) ); break; default: break; } } /*** **** ***/ QString Details :: timeToStringRounded( int seconds ) { if( seconds > 60 ) seconds -= ( seconds % 60 ); return Formatter::timeToString ( seconds ); } void Details :: onTimer( ) { getNewData( ); } void Details :: getNewData( ) { if( !myIds.empty( ) ) { QSet infos; foreach( int id, myIds ) { const Torrent * tor = myModel.getTorrentFromId( id ); if( tor->isMagnet() ) infos.insert( tor->id() ); } if( !infos.isEmpty() ) mySession.initTorrents( infos ); mySession.refreshExtraStats( myIds ); } } void Details :: onTorrentChanged( ) { if( !myHavePendingRefresh ) { myHavePendingRefresh = true; QTimer::singleShot( 100, this, SLOT(refresh())); } } void Details :: onTorrentFileListRebuilt( ) { myFilesDirty = true; onTorrentChanged( ); } namespace { void setIfIdle( QComboBox * box, int i ) { if( !box->hasFocus( ) ) { box->blockSignals( true ); box->setCurrentIndex( i ); box->blockSignals( false ); } } void setIfIdle( QDoubleSpinBox * spin, double value ) { if( !spin->hasFocus( ) ) { spin->blockSignals( true ); spin->setValue( value ); spin->blockSignals( false ); } } void setIfIdle( QSpinBox * spin, int value ) { if( !spin->hasFocus( ) ) { spin->blockSignals( true ); spin->setValue( value ); spin->blockSignals( false ); } } } void Details :: refresh( ) { const int n = myIds.size( ); const bool single = n == 1; const QString blank; const QFontMetrics fm( fontMetrics( ) ); QList torrents; QString string; const QString none = tr( "None" ); const QString mixed = tr( "Mixed" ); const QString unknown = tr( "Unknown" ); // build a list of torrents foreach( int id, myIds ) { const Torrent * tor = myModel.getTorrentFromId( id ); if( tor ) torrents << tor; } /// /// activity tab /// // myStateLabel if( torrents.empty( ) ) string = none; else { bool isMixed = false; bool allPaused = true; bool allFinished = true; const tr_torrent_activity baseline = torrents[0]->getActivity( ); foreach( const Torrent * t, torrents ) { const tr_torrent_activity activity = t->getActivity( ); if( activity != baseline ) isMixed = true; if( activity != TR_STATUS_STOPPED ) allPaused = allFinished = false; if( !t->isFinished( ) ) allFinished = false; } if( isMixed ) string = mixed; else if( allFinished ) string = tr( "Finished" ); else if( allPaused ) string = tr( "Paused" ); else string = torrents[0]->activityString( ); } myStateLabel->setText( string ); const QString stateString = string; // myHaveLabel double sizeWhenDone = 0; double leftUntilDone = 0; double available = 0; int64_t haveTotal = 0; int64_t haveVerified = 0; int64_t haveUnverified = 0; int64_t verifiedPieces = 0; if( torrents.empty( ) ) string = none; else { foreach( const Torrent * t, torrents ) { if( t->hasMetadata( ) ) { haveTotal += t->haveTotal( ); haveUnverified += t->haveUnverified( ); const uint64_t v = t->haveVerified( ); haveVerified += v; if( t->pieceSize( ) ) verifiedPieces += v / t->pieceSize( ); sizeWhenDone += t->sizeWhenDone( ); leftUntilDone += t->leftUntilDone( ); available += t->sizeWhenDone() - t->leftUntilDone() + t->desiredAvailable(); } } { const double d = 100.0 * ( sizeWhenDone ? ( sizeWhenDone - leftUntilDone ) / sizeWhenDone : 1 ); QString pct = Formatter::percentToString( d ); if( !haveUnverified && !leftUntilDone ) { string = tr( "%1 (100%)" ) .arg( Formatter::sizeToString( haveVerified ) ); } else if( !haveUnverified ) { string = tr( "%1 of %2 (%3%)" ) .arg( Formatter::sizeToString( haveVerified ) ) .arg( Formatter::sizeToString( sizeWhenDone ) ) .arg( pct ); } else { string = tr( "%1 of %2 (%3%), %4 Unverified" ) .arg( Formatter::sizeToString( haveVerified + haveUnverified ) ) .arg( Formatter::sizeToString( sizeWhenDone ) ) .arg( pct ) .arg( Formatter::sizeToString( haveUnverified ) ); } } } myHaveLabel->setText( string ); // myAvailabilityLabel if( torrents.empty( ) ) string = none; else { if( sizeWhenDone == 0 ) string = none; else string = QString( "%1%" ).arg( Formatter::percentToString( ( 100.0 * available ) / sizeWhenDone ) ); } myAvailabilityLabel->setText( string ); // myDownloadedLabel if( torrents.empty( ) ) string = none; else { uint64_t d = 0; uint64_t f = 0; foreach( const Torrent * t, torrents ) { d += t->downloadedEver( ); f += t->failedEver( ); } const QString dstr = Formatter::sizeToString( d ); const QString fstr = Formatter::sizeToString( f ); if( f ) string = tr( "%1 (%2 corrupt)" ).arg( dstr ).arg( fstr ); else string = dstr; } myDownloadedLabel->setText( string ); if( torrents.empty( ) ) string = none; else { uint64_t u = 0; uint64_t d = 0; foreach( const Torrent * t, torrents ) { u += t->uploadedEver( ); d += t->downloadedEver( ); } string = tr( "%1 (Ratio: %2)" ) .arg( Formatter::sizeToString( u ) ) .arg( Formatter::ratioToString( tr_getRatio( u, d ) ) ); } myUploadedLabel->setText( string ); const QDateTime qdt_now = QDateTime::currentDateTime( ); // myRunTimeLabel if( torrents.empty( ) ) string = none; else { bool allPaused = true; QDateTime baseline = torrents[0]->lastStarted( ); foreach( const Torrent * t, torrents ) { if( baseline != t->lastStarted( ) ) baseline = QDateTime( ); if( !t->isPaused( ) ) allPaused = false; } if( allPaused ) string = stateString; // paused || finished else if( baseline.isNull( ) ) string = mixed; else string = Formatter::timeToString( baseline.secsTo( qdt_now ) ); } myRunTimeLabel->setText( string ); // myETALabel string.clear( ); if( torrents.empty( ) ) string = none; else { int baseline = torrents[0]->getETA( ); foreach( const Torrent * t, torrents ) { if( baseline != t->getETA( ) ) { string = mixed; break; } } if( string.isEmpty( ) ) { if( baseline < 0 ) string = tr( "Unknown" ); else string = Formatter::timeToString( baseline ); } } myETALabel->setText( string ); // myLastActivityLabel if( torrents.empty( ) ) string = none; else { QDateTime latest = torrents[0]->lastActivity( ); foreach( const Torrent * t, torrents ) { const QDateTime dt = t->lastActivity( ); if( latest < dt ) latest = dt; } const int seconds = latest.isValid() ? latest.secsTo( qdt_now ) : -1; if( seconds < 0 ) string = none; else if( seconds < 5 ) string = tr( "Active now" ); else string = tr( "%1 ago" ).arg( Formatter::timeToString( seconds ) ); } myLastActivityLabel->setText( string ); if( torrents.empty( ) ) string = none; else { string = torrents[0]->getError( ); foreach( const Torrent * t, torrents ) { if( string != t->getError( ) ) { string = mixed; break; } } } if( string.isEmpty( ) ) string = none; myErrorLabel->setText( string ); /// /// information tab /// // mySizeLabel if( torrents.empty( ) ) string = none; else { int pieces = 0; uint64_t size = 0; uint32_t pieceSize = torrents[0]->pieceSize( ); foreach( const Torrent * t, torrents ) { pieces += t->pieceCount( ); size += t->totalSize( ); if( pieceSize != t->pieceSize( ) ) pieceSize = 0; } if( !size ) string = none; else if( pieceSize > 0 ) string = tr( "%1 (%Ln pieces @ %2)", "", pieces ) .arg( Formatter::sizeToString( size ) ) .arg( Formatter::memToString( pieceSize ) ); else string = tr( "%1 (%Ln pieces)", "", pieces ) .arg( Formatter::sizeToString( size ) ); } mySizeLabel->setText( string ); // myHashLabel if( torrents.empty( ) ) string = none; else { string = torrents[0]->hashString( ); foreach( const Torrent * t, torrents ) { if( string != t->hashString( ) ) { string = mixed; break; } } } myHashLabel->setText( string ); // myPrivacyLabel if( torrents.empty( ) ) string = none; else { bool b = torrents[0]->isPrivate( ); string = b ? tr( "Private to this tracker -- DHT and PEX disabled" ) : tr( "Public torrent" ); foreach( const Torrent * t, torrents ) { if( b != t->isPrivate( ) ) { string = mixed; break; } } } myPrivacyLabel->setText( string ); // myCommentBrowser if( torrents.empty( ) ) string = none; else { string = torrents[0]->comment( ); foreach( const Torrent * t, torrents ) { if( string != t->comment( ) ) { string = mixed; break; } } } myCommentBrowser->setText( string ); myCommentBrowser->setMaximumHeight( QWIDGETSIZE_MAX ); // myOriginLabel if( torrents.empty( ) ) string = none; else { bool mixed_creator=false, mixed_date=false; const QString creator = torrents[0]->creator(); const QString date = torrents[0]->dateCreated().toString(); foreach( const Torrent * t, torrents ) { mixed_creator |= ( creator != t->creator() ); mixed_date |= ( date != t->dateCreated().toString() ); } if( mixed_creator && mixed_date ) string = mixed; else if( mixed_date && !creator.isEmpty()) string = tr( "Created by %1" ).arg( creator ); else if( mixed_creator && !date.isEmpty()) string = tr( "Created on %1" ).arg( date ); else if( creator.isEmpty() && date.isEmpty()) string = tr( "N/A" ); else string = tr( "Created by %1 on %2" ).arg( creator ).arg( date ); } myOriginLabel->setText( string ); // myLocationLabel if( torrents.empty( ) ) string = none; else { string = torrents[0]->getPath( ); foreach( const Torrent * t, torrents ) { if( string != t->getPath( ) ) { string = mixed; break; } } } myLocationLabel->setText( string ); /// /// Options Tab /// if( myChangedTorrents && !torrents.empty( ) ) { int i; const Torrent * baseline = *torrents.begin(); const Torrent * tor; bool uniform; bool baselineFlag; int baselineInt; // mySessionLimitCheck uniform = true; baselineFlag = baseline->honorsSessionLimits( ); foreach( tor, torrents ) if( baselineFlag != tor->honorsSessionLimits( ) ) { uniform = false; break; } mySessionLimitCheck->setChecked( uniform && baselineFlag ); // mySingleDownCheck uniform = true; baselineFlag = baseline->downloadIsLimited( ); foreach( tor, torrents ) if( baselineFlag != tor->downloadIsLimited( ) ) { uniform = false; break; } mySingleDownCheck->setChecked( uniform && baselineFlag ); // mySingleUpCheck uniform = true; baselineFlag = baseline->uploadIsLimited( ); foreach( tor, torrents ) if( baselineFlag != tor->uploadIsLimited( ) ) { uniform = false; break; } mySingleUpCheck->setChecked( uniform && baselineFlag ); // myBandwidthPriorityCombo uniform = true; baselineInt = baseline->getBandwidthPriority( ); foreach( tor, torrents ) if ( baselineInt != tor->getBandwidthPriority( ) ) { uniform = false; break; } if( uniform ) i = myBandwidthPriorityCombo->findData( baselineInt ); else i = -1; setIfIdle( myBandwidthPriorityCombo, i ); setIfIdle( mySingleDownSpin, int(tor->downloadLimit().KBps()) ); setIfIdle( mySingleUpSpin, int(tor->uploadLimit().KBps()) ); setIfIdle( myPeerLimitSpin, tor->peerLimit() ); } if( !torrents.empty( ) ) { const Torrent * tor; // ratio bool uniform = true; int baselineInt = torrents[0]->seedRatioMode( ); foreach( tor, torrents ) if( baselineInt != tor->seedRatioMode( ) ) { uniform = false; break; } setIfIdle( myRatioCombo, uniform ? myRatioCombo->findData( baselineInt ) : -1 ); myRatioSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) ); setIfIdle( myRatioSpin, tor->seedRatioLimit( ) ); // idle uniform = true; baselineInt = torrents[0]->seedIdleMode( ); foreach( tor, torrents ) if( baselineInt != tor->seedIdleMode( ) ) { uniform = false; break; } setIfIdle( myIdleCombo, uniform ? myIdleCombo->findData( baselineInt ) : -1 ); myIdleSpin->setVisible( uniform && ( baselineInt == TR_RATIOLIMIT_SINGLE ) ); setIfIdle( myIdleSpin, tor->seedIdleLimit( ) ); } /// /// Tracker tab /// myTrackerModel->refresh( myModel, myIds ); /// /// Peers tab /// QMap peers2; QList newItems; foreach( const Torrent * t, torrents ) { const QString idStr( QString::number( t->id( ) ) ); PeerList peers = t->peers( ); foreach( const Peer& peer, peers ) { const QString key = idStr + ":" + peer.address; PeerItem * item = (PeerItem*) myPeers.value( key, 0 ); if( item == 0 ) // new peer has connected { static const QIcon myEncryptionIcon( ":/icons/encrypted.png" ); static const QIcon myEmptyIcon; item = new PeerItem( peer ); item->setTextAlignment( COL_UP, Qt::AlignRight|Qt::AlignVCenter ); item->setTextAlignment( COL_DOWN, Qt::AlignRight|Qt::AlignVCenter ); item->setTextAlignment( COL_PERCENT, Qt::AlignRight|Qt::AlignVCenter ); item->setIcon( COL_LOCK, peer.isEncrypted ? myEncryptionIcon : myEmptyIcon ); item->setToolTip( COL_LOCK, peer.isEncrypted ? tr( "Encrypted connection" ) : "" ); item->setText( COL_ADDRESS, peer.address ); item->setText( COL_CLIENT, peer.clientName ); newItems << item; } const QString code = peer.flagStr; item->setStatus( code ); item->refresh( peer ); QString codeTip; foreach( QChar ch, code ) { QString txt; switch( ch.toAscii() ) { case 'O': txt = tr( "Optimistic unchoke" ); break; case 'D': txt = tr( "Downloading from this peer" ); break; case 'd': txt = tr( "We would download from this peer if they would let us" ); break; case 'U': txt = tr( "Uploading to peer" ); break; case 'u': txt = tr( "We would upload to this peer if they asked" ); break; case 'K': txt = tr( "Peer has unchoked us, but we're not interested" ); break; case '?': txt = tr( "We unchoked this peer, but they're not interested" ); break; case 'E': txt = tr( "Encrypted connection" ); break; case 'H': txt = tr( "Peer was discovered through DHT" ); break; case 'X': txt = tr( "Peer was discovered through Peer Exchange (PEX)" ); break; case 'I': txt = tr( "Peer is an incoming connection" ); break; case 'T': txt = tr( "Peer is connected over uTP" ); break; } if( !txt.isEmpty( ) ) codeTip += QString("%1: %2\n").arg(ch).arg(txt); } if( !codeTip.isEmpty() ) codeTip.resize( codeTip.size()-1 ); // eat the trailing linefeed item->setText( COL_UP, peer.rateToPeer.isZero() ? "" : Formatter::speedToString( peer.rateToPeer ) ); item->setText( COL_DOWN, peer.rateToClient.isZero() ? "" : Formatter::speedToString( peer.rateToClient ) ); item->setText( COL_PERCENT, peer.progress > 0 ? QString( "%1%" ).arg( (int)( peer.progress * 100.0 ) ) : "" ); item->setText( COL_STATUS, code ); item->setToolTip( COL_STATUS, codeTip ); peers2.insert( key, item ); } } myPeerTree->addTopLevelItems( newItems ); foreach( QString key, myPeers.keys() ) { if( !peers2.contains( key ) ) { // old peer has disconnected QTreeWidgetItem * item = myPeers.value( key, 0 ); myPeerTree->takeTopLevelItem( myPeerTree->indexOfTopLevelItem( item ) ); delete item; } } myPeers = peers2; if( !single || myFilesDirty ) myFileTreeView->clear( ); if( single ) myFileTreeView->update( torrents[0]->files( ) , myChangedTorrents ); myFilesDirty = false; myChangedTorrents = false; myHavePendingRefresh = false; foreach( QWidget * w, myWidgets ) w->setEnabled( true ); } void Details :: enableWhenChecked( QCheckBox * box, QWidget * w ) { connect( box, SIGNAL(toggled(bool)), w, SLOT(setEnabled(bool)) ); w->setEnabled( box->isChecked( ) ); } /*** **** ***/ QWidget * Details :: createInfoTab( ) { HIG * hig = new HIG( this ); hig->addSectionTitle( tr( "Activity" ) ); hig->addRow( tr( "Have:" ), myHaveLabel = new SqueezeLabel ); hig->addRow( tr( "Availability:" ), myAvailabilityLabel = new SqueezeLabel ); hig->addRow( tr( "Downloaded:" ), myDownloadedLabel = new SqueezeLabel ); hig->addRow( tr( "Uploaded:" ), myUploadedLabel = new SqueezeLabel ); hig->addRow( tr( "State:" ), myStateLabel = new SqueezeLabel ); hig->addRow( tr( "Running time:" ), myRunTimeLabel = new SqueezeLabel ); hig->addRow( tr( "Remaining time:" ), myETALabel = new SqueezeLabel ); hig->addRow( tr( "Last activity:" ), myLastActivityLabel = new SqueezeLabel ); hig->addRow( tr( "Error:" ), myErrorLabel = new SqueezeLabel ); hig->addSectionDivider( ); hig->addSectionDivider( ); hig->addSectionTitle( tr( "Details" ) ); hig->addRow( tr( "Size:" ), mySizeLabel = new SqueezeLabel ); hig->addRow( tr( "Location:" ), myLocationLabel = new SqueezeLabel ); hig->addRow( tr( "Hash:" ), myHashLabel = new SqueezeLabel ); hig->addRow( tr( "Privacy:" ), myPrivacyLabel = new SqueezeLabel ); hig->addRow( tr( "Origin:" ), myOriginLabel = new SqueezeLabel ); myOriginLabel->setMinimumWidth( 325 ); // stop long origin strings from resizing the widgit hig->addRow( tr( "Comment:" ), myCommentBrowser = new QTextBrowser ); const int h = QFontMetrics(myCommentBrowser->font()).lineSpacing() * 4; myCommentBrowser->setFixedHeight( h ); hig->finish( ); return hig; } /*** **** ***/ void Details :: onShowTrackerScrapesToggled (bool val) { myPrefs.set (Prefs::SHOW_TRACKER_SCRAPES, val); } void Details :: onShowBackupTrackersToggled (bool val) { myPrefs.set (Prefs::SHOW_BACKUP_TRACKERS, val); } void Details :: onHonorsSessionLimitsToggled (bool val) { mySession.torrentSet (myIds, TR_KEY_honorsSessionLimits, val); getNewData (); } void Details :: onDownloadLimitedToggled (bool val) { mySession.torrentSet (myIds, TR_KEY_downloadLimited, val); getNewData (); } void Details :: onSpinBoxEditingFinished () { const QObject * spin = sender(); const tr_quark key = spin->property(PREF_KEY).toInt(); const QDoubleSpinBox * d = qobject_cast( spin ); if (d) mySession.torrentSet( myIds, key, d->value( ) ); else mySession.torrentSet( myIds, key, qobject_cast(spin)->value( ) ); getNewData( ); } void Details :: onUploadLimitedToggled (bool val) { mySession.torrentSet (myIds, TR_KEY_uploadLimited, val); getNewData (); } void Details :: onIdleModeChanged (int index) { const int val = myIdleCombo->itemData(index).toInt(); mySession.torrentSet (myIds, TR_KEY_seedIdleMode, val); getNewData (); } void Details :: onRatioModeChanged (int index) { const int val = myRatioCombo->itemData(index).toInt(); mySession.torrentSet (myIds, TR_KEY_seedRatioMode, val); } void Details :: onBandwidthPriorityChanged (int index) { if( index != -1 ) { const int priority = myBandwidthPriorityCombo->itemData(index).toInt(); mySession.torrentSet( myIds, TR_KEY_bandwidthPriority, priority ); getNewData( ); } } void Details :: onTrackerSelectionChanged () { const int selectionCount = myTrackerView->selectionModel()->selectedRows().size(); myEditTrackerButton->setEnabled (selectionCount == 1); myRemoveTrackerButton->setEnabled (selectionCount > 0); } void Details :: onAddTrackerClicked () { bool ok = false; const QString url = QInputDialog::getText (this, tr("Add URL "), tr("Add tracker announce URL:"), QLineEdit::Normal, QString(), &ok); if(!ok) { // user pressed "cancel" -- noop } else if (!QUrl(url).isValid()) { QMessageBox::warning( this, tr("Error"), tr("Invalid URL \"%1\"").arg(url)); } else { QSet ids; foreach (int id, myIds) if (myTrackerModel->find(id,url) == -1) ids.insert (id); if (ids.empty()) // all the torrents already have this tracker { QMessageBox::warning (this, tr("Error"), tr("Tracker already exists.")); } else { QStringList urls; urls << url; mySession.torrentSet (ids, TR_KEY_trackerAdd, urls); getNewData (); } } } void Details :: onEditTrackerClicked () { QItemSelectionModel * selectionModel = myTrackerView->selectionModel(); QModelIndexList selectedRows = selectionModel->selectedRows(); assert (selectedRows.size() == 1); QModelIndex i = selectionModel->currentIndex(); const TrackerInfo trackerInfo = myTrackerView->model()->data(i, TrackerModel::TrackerRole).value(); bool ok = false; const QString newval = QInputDialog::getText (this, tr("Edit URL "), tr("Edit tracker announce URL:"), QLineEdit::Normal, trackerInfo.st.announce, &ok); if (!ok) { // user pressed "cancel" -- noop } else if( !QUrl(newval).isValid( ) ) { QMessageBox::warning (this, tr("Error"), tr("Invalid URL \"%1\"").arg(newval)); } else { QSet ids; ids << trackerInfo.torrentId; const QPair idUrl = qMakePair (trackerInfo.st.id, newval); mySession.torrentSet (ids, TR_KEY_trackerReplace, idUrl); getNewData (); } } void Details :: onRemoveTrackerClicked( ) { // make a map of torrentIds to announce URLs to remove QItemSelectionModel * selectionModel = myTrackerView->selectionModel( ); QModelIndexList selectedRows = selectionModel->selectedRows( ); QMap torrentId_to_trackerIds; foreach( QModelIndex i, selectedRows ) { const TrackerInfo inf = myTrackerView->model()->data( i, TrackerModel::TrackerRole ).value(); torrentId_to_trackerIds.insertMulti( inf.torrentId, inf.st.id ); } // batch all of a tracker's torrents into one command foreach( int id, torrentId_to_trackerIds.uniqueKeys( ) ) { QSet ids; ids << id; mySession.torrentSet( ids, TR_KEY_trackerRemove, torrentId_to_trackerIds.values( id ) ); } selectionModel->clearSelection( ); getNewData( ); } QWidget * Details :: createOptionsTab( ) { QSpinBox * s; QCheckBox * c; QComboBox * m; QHBoxLayout * h; QDoubleSpinBox * ds; const QString speed_K_str = Formatter::unitStr( Formatter::SPEED, Formatter::KB ); HIG * hig = new HIG( this ); hig->addSectionTitle( tr( "Speed" ) ); c = new QCheckBox( tr( "Honor global &limits" ) ); mySessionLimitCheck = c; hig->addWideControl( c ); connect( c, SIGNAL(clicked(bool)), this, SLOT(onHonorsSessionLimitsToggled(bool)) ); c = new QCheckBox( tr( "Limit &download speed (%1):" ).arg( speed_K_str ) ); mySingleDownCheck = c; s = new QSpinBox( ); s->setProperty (PREF_KEY, TR_KEY_downloadLimit); s->setSingleStep( 5 ); s->setRange( 0, INT_MAX ); mySingleDownSpin = s; hig->addRow( c, s ); enableWhenChecked( c, s ); connect( c, SIGNAL(clicked(bool)), this, SLOT(onDownloadLimitedToggled(bool)) ); connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); c = new QCheckBox( tr( "Limit &upload speed (%1):" ).arg( speed_K_str ) ); mySingleUpCheck = c; s = new QSpinBox( ); s->setSingleStep( 5 ); s->setRange( 0, INT_MAX ); s->setProperty( PREF_KEY, TR_KEY_uploadLimit ); mySingleUpSpin = s; hig->addRow( c, s ); enableWhenChecked( c, s ); connect( c, SIGNAL(clicked(bool)), this, SLOT(onUploadLimitedToggled(bool)) ); connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); m = new QComboBox; m->addItem( tr( "High" ), TR_PRI_HIGH ); m->addItem( tr( "Normal" ), TR_PRI_NORMAL ); m->addItem( tr( "Low" ), TR_PRI_LOW ); connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onBandwidthPriorityChanged(int))); hig->addRow( tr( "Torrent &priority:" ), m ); myBandwidthPriorityCombo = m; hig->addSectionDivider( ); hig->addSectionTitle( tr( "Seeding Limits" ) ); h = new QHBoxLayout( ); h->setSpacing( HIG :: PAD ); m = new QComboBox; m->addItem( tr( "Use Global Settings" ), TR_RATIOLIMIT_GLOBAL ); m->addItem( tr( "Seed regardless of ratio" ), TR_RATIOLIMIT_UNLIMITED ); m->addItem( tr( "Stop seeding at ratio:" ), TR_RATIOLIMIT_SINGLE ); connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onRatioModeChanged(int))); h->addWidget( myRatioCombo = m ); ds = new QDoubleSpinBox( ); ds->setRange( 0.5, INT_MAX ); ds->setProperty( PREF_KEY, TR_KEY_seedRatioLimit ); connect( ds, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); h->addWidget( myRatioSpin = ds ); hig->addRow( tr( "&Ratio:" ), h, m ); h = new QHBoxLayout( ); h->setSpacing( HIG :: PAD ); m = new QComboBox; m->addItem( tr( "Use Global Settings" ), TR_IDLELIMIT_GLOBAL ); m->addItem( tr( "Seed regardless of activity" ), TR_IDLELIMIT_UNLIMITED ); m->addItem( tr( "Stop seeding if idle for N minutes:" ), TR_IDLELIMIT_SINGLE ); connect( m, SIGNAL(currentIndexChanged(int)), this, SLOT(onIdleModeChanged(int))); h->addWidget( myIdleCombo = m ); s = new QSpinBox( ); s->setSingleStep( 5 ); s->setRange( 1, 9999 ); s->setProperty( PREF_KEY, TR_KEY_seedIdleLimit ); connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); h->addWidget( myIdleSpin = s ); hig->addRow( tr( "&Idle:" ), h, m ); hig->addSectionDivider( ); hig->addSectionTitle( tr( "Peer Connections" ) ); s = new QSpinBox( ); s->setSingleStep( 5 ); s->setRange( 1, 300 ); s->setProperty( PREF_KEY, TR_KEY_peer_limit ); connect( s, SIGNAL(editingFinished()), this, SLOT(onSpinBoxEditingFinished())); myPeerLimitSpin = s; hig->addRow( tr( "&Maximum peers:" ), s ); hig->finish( ); return hig; } /*** **** ***/ QWidget * Details :: createTrackerTab( ) { QCheckBox * c; QPushButton * p; QWidget * top = new QWidget; QVBoxLayout * v = new QVBoxLayout( top ); QHBoxLayout * h = new QHBoxLayout(); QVBoxLayout * v2 = new QVBoxLayout(); v->setSpacing( HIG::PAD_BIG ); v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG ); h->setSpacing( HIG::PAD ); h->setContentsMargins( HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL ); v2->setSpacing( HIG::PAD ); myTrackerModel = new TrackerModel; myTrackerFilter = new TrackerModelFilter; myTrackerFilter->setSourceModel( myTrackerModel ); myTrackerView = new QTreeView; myTrackerView->setModel( myTrackerFilter ); myTrackerView->setHeaderHidden( true ); myTrackerView->setSelectionMode( QTreeWidget::ExtendedSelection ); myTrackerView->setRootIsDecorated( false ); myTrackerView->setIndentation( 2 ); myTrackerView->setItemsExpandable( false ); myTrackerView->setAlternatingRowColors( true ); myTrackerView->setItemDelegate( myTrackerDelegate = new TrackerDelegate( ) ); connect( myTrackerView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(onTrackerSelectionChanged())); h->addWidget( myTrackerView, 1 ); p = new QPushButton(); p->setIcon( getStockIcon( "list-add", QStyle::SP_DialogOpenButton ) ); p->setToolTip( tr( "Add Tracker" )); myAddTrackerButton = p; v2->addWidget( p, 1 ); connect( p, SIGNAL(clicked(bool)), this, SLOT(onAddTrackerClicked())); p = new QPushButton(); p->setIcon( getStockIcon( "document-properties", QStyle::SP_DesktopIcon ) ); p->setToolTip( tr( "Edit Tracker" )); myAddTrackerButton = p; p->setEnabled( false ); myEditTrackerButton = p; v2->addWidget( p, 1 ); connect( p, SIGNAL(clicked(bool)), this, SLOT(onEditTrackerClicked())); p = new QPushButton(); p->setIcon( getStockIcon( "list-remove", QStyle::SP_TrashIcon ) ); p->setToolTip( tr( "Remove Trackers" )); p->setEnabled( false ); myRemoveTrackerButton = p; v2->addWidget( p, 1 ); connect( p, SIGNAL(clicked(bool)), this, SLOT(onRemoveTrackerClicked())); v2->addStretch( 1 ); h->addLayout( v2, 1 ); h->setStretch( 1, 0 ); v->addLayout( h, 1 ); c = new QCheckBox( tr( "Show &more details" ) ); c->setChecked( myPrefs.getBool( Prefs::SHOW_TRACKER_SCRAPES ) ); myShowTrackerScrapesCheck = c; v->addWidget( c, 1 ); connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowTrackerScrapesToggled(bool)) ); c = new QCheckBox( tr( "Show &backup trackers" ) ); c->setChecked( myPrefs.getBool( Prefs::SHOW_BACKUP_TRACKERS ) ); myShowBackupTrackersCheck = c; v->addWidget( c, 1 ); connect( c, SIGNAL(clicked(bool)), this, SLOT(onShowBackupTrackersToggled(bool)) ); return top; } /*** **** ***/ QWidget * Details :: createPeersTab( ) { QWidget * top = new QWidget; QVBoxLayout * v = new QVBoxLayout( top ); v->setSpacing( HIG :: PAD_BIG ); v->setContentsMargins( HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG ); QStringList headers; headers << QString() << tr("Up") << tr("Down") << tr("%") << tr("Status") << tr("Address") << tr("Client"); myPeerTree = new QTreeWidget; myPeerTree->setUniformRowHeights( true ); myPeerTree->setHeaderLabels( headers ); myPeerTree->setColumnWidth( 0, 20 ); myPeerTree->setSortingEnabled( true ); myPeerTree->sortByColumn( COL_ADDRESS, Qt::AscendingOrder ); myPeerTree->setRootIsDecorated( false ); myPeerTree->setTextElideMode( Qt::ElideRight ); v->addWidget( myPeerTree, 1 ); const QFontMetrics m( font( ) ); QSize size = m.size( 0, "1024 MiB/s" ); myPeerTree->setColumnWidth( COL_UP, size.width( ) ); myPeerTree->setColumnWidth( COL_DOWN, size.width( ) ); size = m.size( 0, " 100% " ); myPeerTree->setColumnWidth( COL_PERCENT, size.width( ) ); size = m.size( 0, "ODUK?EXI" ); myPeerTree->setColumnWidth( COL_STATUS, size.width( ) ); size = m.size( 0, "888.888.888.888" ); myPeerTree->setColumnWidth( COL_ADDRESS, size.width( ) ); size = m.size( 0, "Some BitTorrent Client" ); myPeerTree->setColumnWidth( COL_CLIENT, size.width( ) ); myPeerTree->setAlternatingRowColors( true ); return top; } /*** **** ***/ QWidget * Details :: createFilesTab( ) { myFileTreeView = new FileTreeView( ); connect( myFileTreeView, SIGNAL( priorityChanged(const QSet&, int)), this, SLOT( onFilePriorityChanged(const QSet&, int))); connect( myFileTreeView, SIGNAL( wantedChanged(const QSet&, bool)), this, SLOT( onFileWantedChanged(const QSet&, bool))); connect( myFileTreeView, SIGNAL( pathEdited(const QString&, const QString&)), this, SLOT (onPathEdited(const QString&, const QString&))); return myFileTreeView; } void Details :: onFilePriorityChanged (const QSet& indices, int priority) { tr_quark key; switch (priority) { case TR_PRI_LOW: key = TR_KEY_priority_low; break; case TR_PRI_HIGH: key = TR_KEY_priority_high; break; default: key = TR_KEY_priority_normal; break; } mySession.torrentSet (myIds, key, indices.toList()); getNewData( ); } void Details :: onFileWantedChanged (const QSet& indices, bool wanted) { const tr_quark key = wanted ? TR_KEY_files_wanted : TR_KEY_files_unwanted; mySession.torrentSet (myIds, key, indices.toList()); getNewData (); } void Details :: onPathEdited (const QString& oldpath, const QString& newname) { mySession.torrentRenamePath (myIds, oldpath, newname); }