/* * This file Copyright (C) 2009-2014 Mnemosyne LLC * * It may be used under the GNU Public License v2 or v3 licenses, * or any future license endorsed by Mnemosyne LLC. * * $Id$ */ #include #include /* INT_MAX */ #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 ())); } 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 ())); } 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 ())); } } 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); // myUploadedLabel 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 string = none; if (!torrents.empty ()) { string = torrents[0]->hashString (); foreach (const Torrent * t, torrents) { if (string != t->hashString ()) { string = mixed; break; } } } myHashLabel->setText (string); // myPrivacyLabel string = none; if (!torrents.empty ()) { 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 string = none; if (!torrents.empty ()) { string = torrents[0]->comment (); foreach (const Torrent * t, torrents) { if (string != t->comment ()) { string = mixed; break; } } } if (myCommentBrowser->toPlainText() != string) { myCommentBrowser->setText (string); myCommentBrowser->setMaximumHeight (QWIDGETSIZE_MAX); } // myOriginLabel string = none; if (!torrents.empty ()) { 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 string = none; if (!torrents.empty ()) { 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; bool uniform; bool baselineFlag; int baselineInt; const Torrent * tor; const Torrent * baseline = *torrents.begin (); // 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.unicode ()) { 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) myFileTreeView->clear (); if (single) myFileTreeView->update (torrents[0]->files (), myChangedTorrents); 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&))); connect (myFileTreeView, SIGNAL (openRequested (const QString&)), this, SLOT (onOpenRequested (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); } void Details :: onOpenRequested (const QString& path) { if (!mySession.isLocal ()) return; foreach (const int id, myIds) { const Torrent * const tor = myModel.getTorrentFromId (id); if (tor == NULL) continue; const QString localFilePath = tor->getPath () + "/" + path; if (!QFile::exists (localFilePath)) continue; if (QDesktopServices::openUrl (QUrl::fromLocalFile (localFilePath))) break; } }