/* * This file Copyright (C) 2009-2015 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. * */ #include #include /* INT_MAX */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // tr_getRatio() #include "ColumnResizer.h" #include "DetailsDialog.h" #include "Formatter.h" #include "Prefs.h" #include "Session.h" #include "SqueezeLabel.h" #include "Torrent.h" #include "TorrentModel.h" #include "TrackerDelegate.h" #include "TrackerModel.h" #include "TrackerModelFilter.h" #include "Utils.h" class Prefs; class Session; /**** ***** ****/ namespace { int const REFRESH_INTERVAL_MSEC = 4000; char const* PREF_KEY("pref-key"); enum // peer columns { COL_LOCK, COL_UP, COL_DOWN, COL_PERCENT, COL_STATUS, COL_ADDRESS, COL_CLIENT, N_COLUMNS }; int measureViewItem(QTreeWidget* view, int column, QString const& text) { QTreeWidgetItem const* headerItem = view->headerItem(); int const itemWidth = Utils::measureViewItem(view, text); int const headerWidth = Utils::measureHeaderItem(view->header(), headerItem->text(column)); return std::max(itemWidth, headerWidth); } } // namespace /*** **** ***/ class PeerItem : public QTreeWidgetItem { Peer peer; QString mutable collatedAddress; QString status; public: PeerItem(Peer const& p) : peer(p) { } virtual ~PeerItem() { } public: void refresh(Peer const& p) { if (p.address != peer.address) { collatedAddress.clear(); } peer = p; } void setStatus(QString const& s) { status = s; } virtual bool operator <(QTreeWidgetItem const& other) const { PeerItem const* i = dynamic_cast(&other); QTreeWidget* tw(treeWidget()); int const column = tw != nullptr ? tw->sortColumn() : 0; assert(i != nullptr); 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 address() < i->address(); } } private: QString const& address() const { if (collatedAddress.isEmpty()) { QHostAddress ipAddress; if (ipAddress.setAddress(peer.address)) { if (ipAddress.protocol() == QAbstractSocket::IPv4Protocol) { quint32 const ipv4Address = ipAddress.toIPv4Address(); collatedAddress = QLatin1String("1-") + QString::fromLatin1(QByteArray::number(ipv4Address, 16). rightJustified(8, '0')); } else if (ipAddress.protocol() == QAbstractSocket::IPv6Protocol) { Q_IPV6ADDR const ipv6Address = ipAddress.toIPv6Address(); QByteArray tmp(16, '\0'); for (int i = 0; i < 16; ++i) { tmp[i] = ipv6Address[i]; } collatedAddress = QLatin1String("2-") + QString::fromLatin1(tmp.toHex()); } } if (collatedAddress.isEmpty()) { collatedAddress = QLatin1String("3-") + peer.address.toLower(); } } return collatedAddress; } }; /*** **** ***/ QIcon DetailsDialog::getStockIcon(QString const& freedesktop_name, int fallback) { QIcon icon = QIcon::fromTheme(freedesktop_name); if (icon.isNull()) { icon = style()->standardIcon(QStyle::StandardPixmap(fallback), nullptr, this); } return icon; } DetailsDialog::DetailsDialog(Session& session, Prefs& prefs, TorrentModel const& model, QWidget* parent) : BaseDialog(parent), mySession(session), myPrefs(prefs), myModel(model), myChangedTorrents(false), myHavePendingRefresh(false) { ui.setupUi(this); initInfoTab(); initPeersTab(); initTrackerTab(); initFilesTab(); initOptionsTab(); adjustSize(); ui.commentBrowser->setMaximumHeight(QWIDGETSIZE_MAX); QList initKeys; initKeys << Prefs::SHOW_TRACKER_SCRAPES << Prefs::SHOW_BACKUP_TRACKERS; for (int const 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); } DetailsDialog::~DetailsDialog() { myTrackerDelegate->deleteLater(); myTrackerFilter->deleteLater(); myTrackerModel->deleteLater(); } void DetailsDialog::setIds(QSet const& ids) { if (ids == myIds) { return; } myChangedTorrents = true; // stop listening to the old torrents for (int const id : myIds) { Torrent const* tor = myModel.getTorrentFromId(id); if (tor != nullptr) { disconnect(tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged())); } } ui.filesView->clear(); myIds = ids; myTrackerModel->refresh(myModel, myIds); // listen to the new torrents for (int const id : myIds) { Torrent const* tor = myModel.getTorrentFromId(id); if (tor != nullptr) { connect(tor, SIGNAL(torrentChanged(int)), this, SLOT(onTorrentChanged())); } } for (int i = 0; i < ui.tabs->count(); ++i) { ui.tabs->widget(i)->setEnabled(false); } onTimer(); } void DetailsDialog::refreshPref(int key) { QString str; switch (key) { case Prefs::SHOW_TRACKER_SCRAPES: { QItemSelectionModel* selectionModel(ui.trackersView->selectionModel()); QItemSelection const selection(selectionModel->selection()); QModelIndex const currentIndex(selectionModel->currentIndex()); myTrackerDelegate->setShowMore(myPrefs.getBool(key)); selectionModel->clear(); ui.trackersView->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; } } /*** **** ***/ void DetailsDialog::onTimer() { getNewData(); } void DetailsDialog::getNewData() { if (!myIds.empty()) { QSet infos; for (int const id : myIds) { Torrent const* tor = myModel.getTorrentFromId(id); if (tor->isMagnet()) { infos.insert(tor->id()); } } if (!infos.isEmpty()) { mySession.initTorrents(infos); } mySession.refreshExtraStats(myIds); } } void DetailsDialog::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); } } } // namespace void DetailsDialog::refresh() { int const n = myIds.size(); bool const single = n == 1; QString const blank; QFontMetrics const fm(fontMetrics()); QList torrents; QString string; QString const none = tr("None"); QString const mixed = tr("Mixed"); QString const unknown = tr("Unknown"); // build a list of torrents for (int const id : myIds) { Torrent const* tor = myModel.getTorrentFromId(id); if (tor != nullptr) { torrents << tor; } } /// /// activity tab /// // myStateLabel if (torrents.empty()) { string = none; } else { bool isMixed = false; bool allPaused = true; bool allFinished = true; tr_torrent_activity const baseline = torrents[0]->getActivity(); for (Torrent const* const t : torrents) { tr_torrent_activity const 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(); } } ui.stateValueLabel->setText(string); QString const stateString = string; // myHaveLabel uint64_t sizeWhenDone = 0; uint64_t available = 0; if (torrents.empty()) { string = none; } else { uint64_t leftUntilDone = 0; int64_t haveTotal = 0; int64_t haveVerified = 0; int64_t haveUnverified = 0; int64_t verifiedPieces = 0; for (Torrent const* const t : torrents) { if (t->hasMetadata()) { haveTotal += t->haveTotal(); haveUnverified += t->haveUnverified(); uint64_t const 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(); } } double const d = 100.0 * (sizeWhenDone != 0 ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1); QString pct = Formatter::percentToString(d); if (haveUnverified == 0 && leftUntilDone == 0) { //: Text following the "Have:" label in torrent properties dialog; //: %1 is amount of downloaded and verified data string = tr("%1 (100%)").arg(Formatter::sizeToString(haveVerified)); } else if (haveUnverified == 0) { //: Text following the "Have:" label in torrent properties dialog; //: %1 is amount of downloaded and verified data, //: %2 is overall size of torrent data, //: %3 is percentage (%1/%2*100) string = tr("%1 of %2 (%3%)").arg(Formatter::sizeToString(haveVerified)).arg(Formatter::sizeToString(sizeWhenDone)). arg(pct); } else { //: Text following the "Have:" label in torrent properties dialog; //: %1 is amount of downloaded data (both verified and unverified), //: %2 is overall size of torrent data, //: %3 is percentage (%1/%2*100), //: %4 is amount of downloaded but not yet verified data string = tr("%1 of %2 (%3%), %4 Unverified").arg(Formatter::sizeToString(haveVerified + haveUnverified)). arg(Formatter::sizeToString(sizeWhenDone)).arg(pct).arg(Formatter::sizeToString(haveUnverified)); } } ui.haveValueLabel->setText(string); // myAvailabilityLabel if (torrents.empty()) { string = none; } else if (sizeWhenDone == 0) { string = none; } else { string = QString::fromLatin1("%1%").arg(Formatter::percentToString((100.0 * available) / sizeWhenDone)); } ui.availabilityValueLabel->setText(string); // myDownloadedLabel if (torrents.empty()) { string = none; } else { uint64_t d = 0; uint64_t f = 0; for (Torrent const* const t : torrents) { d += t->downloadedEver(); f += t->failedEver(); } QString const dstr = Formatter::sizeToString(d); QString const fstr = Formatter::sizeToString(f); if (f != 0) { string = tr("%1 (%2 corrupt)").arg(dstr).arg(fstr); } else { string = dstr; } } ui.downloadedValueLabel->setText(string); // myUploadedLabel if (torrents.empty()) { string = none; } else { uint64_t u = 0; uint64_t d = 0; for (Torrent const* const t : torrents) { u += t->uploadedEver(); d += t->downloadedEver(); } string = tr("%1 (Ratio: %2)").arg(Formatter::sizeToString(u)).arg(Formatter::ratioToString(tr_getRatio(u, d))); } ui.uploadedValueLabel->setText(string); QDateTime const qdt_now = QDateTime::currentDateTime(); // myRunTimeLabel if (torrents.empty()) { string = none; } else { bool allPaused = true; QDateTime baseline = torrents[0]->lastStarted(); for (Torrent const* const 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)); } } ui.runningTimeValueLabel->setText(string); // myETALabel string.clear(); if (torrents.empty()) { string = none; } else { int baseline = torrents[0]->getETA(); for (Torrent const* const t : torrents) { if (baseline != t->getETA()) { string = mixed; break; } } if (string.isEmpty()) { if (baseline < 0) { string = tr("Unknown"); } else { string = Formatter::timeToString(baseline); } } } ui.remainingTimeValueLabel->setText(string); // myLastActivityLabel if (torrents.empty()) { string = none; } else { QDateTime latest = torrents[0]->lastActivity(); for (Torrent const* const t : torrents) { QDateTime const dt = t->lastActivity(); if (latest < dt) { latest = dt; } } int const 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)); } } ui.lastActivityValueLabel->setText(string); if (torrents.empty()) { string = none; } else { string = torrents[0]->getError(); for (Torrent const* const t : torrents) { if (string != t->getError()) { string = mixed; break; } } } if (string.isEmpty()) { string = none; } ui.errorValueLabel->setText(string); /// /// information tab /// // mySizeLabel if (torrents.empty()) { string = none; } else { int pieces = 0; uint64_t size = 0; uint32_t pieceSize = torrents[0]->pieceSize(); for (Torrent const* const t : torrents) { pieces += t->pieceCount(); size += t->totalSize(); if (pieceSize != t->pieceSize()) { pieceSize = 0; } } if (size == 0) { 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)); } } ui.sizeValueLabel->setText(string); // myHashLabel string = none; if (!torrents.empty()) { string = torrents[0]->hashString(); for (Torrent const* const t : torrents) { if (string != t->hashString()) { string = mixed; break; } } } ui.hashValueLabel->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"); for (Torrent const* const t : torrents) { if (b != t->isPrivate()) { string = mixed; break; } } } ui.privacyValueLabel->setText(string); // myCommentBrowser string = none; bool isCommentMixed = false; if (!torrents.empty()) { string = torrents[0]->comment(); for (Torrent const* const t : torrents) { if (string != t->comment()) { string = mixed; isCommentMixed = true; break; } } } if (ui.commentBrowser->toPlainText() != string) { ui.commentBrowser->setText(string); } ui.commentBrowser->setEnabled(!isCommentMixed && !string.isEmpty()); // myOriginLabel string = none; if (!torrents.empty()) { bool mixed_creator = false; bool mixed_date = false; QString const creator = torrents[0]->creator(); QString const date = torrents[0]->dateCreated().toString(); for (Torrent const* const t : torrents) { mixed_creator |= (creator != t->creator()); mixed_date |= (date != t->dateCreated().toString()); } bool const empty_creator = creator.isEmpty(); bool const empty_date = date.isEmpty(); if (mixed_creator || mixed_date) { string = mixed; } else if (empty_creator && empty_date) { string = tr("N/A"); } else if (empty_date && !empty_creator) { string = tr("Created by %1").arg(creator); } else if (empty_creator && !empty_date) { string = tr("Created on %1").arg(date); } else { string = tr("Created by %1 on %2").arg(creator).arg(date); } } ui.originValueLabel->setText(string); // myLocationLabel string = none; if (!torrents.empty()) { string = torrents[0]->getPath(); for (Torrent const* const t : torrents) { if (string != t->getPath()) { string = mixed; break; } } } ui.locationValueLabel->setText(string); /// /// Options Tab /// if (myChangedTorrents && !torrents.empty()) { int i; bool uniform; bool baselineFlag; int baselineInt; Torrent const& baseline = *torrents.front(); // mySessionLimitCheck uniform = true; baselineFlag = baseline.honorsSessionLimits(); for (Torrent const* const tor : torrents) { if (baselineFlag != tor->honorsSessionLimits()) { uniform = false; break; } } ui.sessionLimitCheck->setChecked(uniform && baselineFlag); // mySingleDownCheck uniform = true; baselineFlag = baseline.downloadIsLimited(); for (Torrent const* const tor : torrents) { if (baselineFlag != tor->downloadIsLimited()) { uniform = false; break; } } ui.singleDownCheck->setChecked(uniform && baselineFlag); // mySingleUpCheck uniform = true; baselineFlag = baseline.uploadIsLimited(); for (Torrent const* const tor : torrents) { if (baselineFlag != tor->uploadIsLimited()) { uniform = false; break; } } ui.singleUpCheck->setChecked(uniform && baselineFlag); // myBandwidthPriorityCombo uniform = true; baselineInt = baseline.getBandwidthPriority(); for (Torrent const* const tor : torrents) { if (baselineInt != tor->getBandwidthPriority()) { uniform = false; break; } } if (uniform) { i = ui.bandwidthPriorityCombo->findData(baselineInt); } else { i = -1; } setIfIdle(ui.bandwidthPriorityCombo, i); setIfIdle(ui.singleDownSpin, int(baseline.downloadLimit().KBps())); setIfIdle(ui.singleUpSpin, int(baseline.uploadLimit().KBps())); setIfIdle(ui.peerLimitSpin, baseline.peerLimit()); } if (!torrents.empty()) { Torrent const& baseline = *torrents.front(); // ratio bool uniform = true; int baselineInt = baseline.seedRatioMode(); for (Torrent const* const tor : torrents) { if (baselineInt != tor->seedRatioMode()) { uniform = false; break; } } setIfIdle(ui.ratioCombo, uniform ? ui.ratioCombo->findData(baselineInt) : -1); ui.ratioSpin->setVisible(uniform && baselineInt == TR_RATIOLIMIT_SINGLE); setIfIdle(ui.ratioSpin, baseline.seedRatioLimit()); // idle uniform = true; baselineInt = baseline.seedIdleMode(); for (Torrent const* const tor : torrents) { if (baselineInt != tor->seedIdleMode()) { uniform = false; break; } } setIfIdle(ui.idleCombo, uniform ? ui.idleCombo->findData(baselineInt) : -1); ui.idleSpin->setVisible(uniform && baselineInt == TR_RATIOLIMIT_SINGLE); setIfIdle(ui.idleSpin, baseline.seedIdleLimit()); onIdleLimitChanged(); } /// /// Tracker tab /// myTrackerModel->refresh(myModel, myIds); /// /// Peers tab /// QMap peers2; QList newItems; for (Torrent const* const t : torrents) { QString const idStr(QString::number(t->id())); PeerList peers = t->peers(); for (Peer const& peer : peers) { QString const key = idStr + QLatin1Char(':') + peer.address; PeerItem* item = static_cast(myPeers.value(key, nullptr)); if (item == nullptr) // new peer has connected { static QIcon const myEncryptionIcon(QLatin1String(":/icons/encrypted.png")); static QIcon const 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") : QString()); item->setText(COL_ADDRESS, peer.address); item->setText(COL_CLIENT, peer.clientName); newItems << item; } QString const code = peer.flagStr; item->setStatus(code); item->refresh(peer); QString codeTip; for (QChar const 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::fromLatin1("%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() ? QString() : Formatter::speedToString(peer.rateToPeer)); item->setText(COL_DOWN, peer.rateToClient.isZero() ? QString() : Formatter::speedToString(peer.rateToClient)); item->setText(COL_PERCENT, peer.progress > 0 ? QString::fromLatin1("%1%").arg(int(peer.progress * 100.0)) : QString()); item->setText(COL_STATUS, code); item->setToolTip(COL_STATUS, codeTip); peers2.insert(key, item); } } ui.peersView->addTopLevelItems(newItems); for (QString const& key : myPeers.keys()) { if (!peers2.contains(key)) // old peer has disconnected { QTreeWidgetItem* item = myPeers.value(key, nullptr); ui.peersView->takeTopLevelItem(ui.peersView->indexOfTopLevelItem(item)); delete item; } } myPeers = peers2; if (!single) { ui.filesView->clear(); } if (single) { ui.filesView->update(torrents[0]->files(), myChangedTorrents); } myChangedTorrents = false; myHavePendingRefresh = false; for (int i = 0; i < ui.tabs->count(); ++i) { ui.tabs->widget(i)->setEnabled(true); } } /*** **** ***/ void DetailsDialog::initInfoTab() { int const h = QFontMetrics(ui.commentBrowser->font()).lineSpacing() * 4; ui.commentBrowser->setFixedHeight(h); ColumnResizer* cr(new ColumnResizer(this)); cr->addLayout(ui.activitySectionLayout); cr->addLayout(ui.detailsSectionLayout); cr->update(); } /*** **** ***/ void DetailsDialog::onShowTrackerScrapesToggled(bool val) { myPrefs.set(Prefs::SHOW_TRACKER_SCRAPES, val); } void DetailsDialog::onShowBackupTrackersToggled(bool val) { myPrefs.set(Prefs::SHOW_BACKUP_TRACKERS, val); } void DetailsDialog::onHonorsSessionLimitsToggled(bool val) { mySession.torrentSet(myIds, TR_KEY_honorsSessionLimits, val); getNewData(); } void DetailsDialog::onDownloadLimitedToggled(bool val) { mySession.torrentSet(myIds, TR_KEY_downloadLimited, val); getNewData(); } void DetailsDialog::onSpinBoxEditingFinished() { QObject const* spin = sender(); tr_quark const key = spin->property(PREF_KEY).toInt(); QDoubleSpinBox const* d = qobject_cast(spin); if (d != nullptr) { mySession.torrentSet(myIds, key, d->value()); } else { mySession.torrentSet(myIds, key, qobject_cast(spin)->value()); } getNewData(); } void DetailsDialog::onUploadLimitedToggled(bool val) { mySession.torrentSet(myIds, TR_KEY_uploadLimited, val); getNewData(); } void DetailsDialog::onIdleModeChanged(int index) { int const val = ui.idleCombo->itemData(index).toInt(); mySession.torrentSet(myIds, TR_KEY_seedIdleMode, val); getNewData(); } void DetailsDialog::onIdleLimitChanged() { //: Spin box suffix, "Stop seeding if idle for: [ 5 minutes ]" (includes leading space after the number, if needed) QString const unitsSuffix = tr(" minute(s)", nullptr, ui.idleSpin->value()); if (ui.idleSpin->suffix() != unitsSuffix) { ui.idleSpin->setSuffix(unitsSuffix); } } void DetailsDialog::onRatioModeChanged(int index) { int const val = ui.ratioCombo->itemData(index).toInt(); mySession.torrentSet(myIds, TR_KEY_seedRatioMode, val); } void DetailsDialog::onBandwidthPriorityChanged(int index) { if (index != -1) { int const priority = ui.bandwidthPriorityCombo->itemData(index).toInt(); mySession.torrentSet(myIds, TR_KEY_bandwidthPriority, priority); getNewData(); } } void DetailsDialog::onTrackerSelectionChanged() { int const selectionCount = ui.trackersView->selectionModel()->selectedRows().size(); ui.editTrackerButton->setEnabled(selectionCount == 1); ui.removeTrackerButton->setEnabled(selectionCount > 0); } void DetailsDialog::onAddTrackerClicked() { bool ok = false; QString const 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; for (int const 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 DetailsDialog::onEditTrackerClicked() { QItemSelectionModel* selectionModel = ui.trackersView->selectionModel(); QModelIndexList selectedRows = selectionModel->selectedRows(); assert(selectedRows.size() == 1); QModelIndex i = selectionModel->currentIndex(); TrackerInfo const trackerInfo = ui.trackersView->model()->data(i, TrackerModel::TrackerRole).value(); bool ok = false; QString const 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; QPair const idUrl = qMakePair(trackerInfo.st.id, newval); mySession.torrentSet(ids, TR_KEY_trackerReplace, idUrl); getNewData(); } } void DetailsDialog::onRemoveTrackerClicked() { // make a map of torrentIds to announce URLs to remove QItemSelectionModel* selectionModel = ui.trackersView->selectionModel(); QModelIndexList selectedRows = selectionModel->selectedRows(); QMap torrentId_to_trackerIds; for (QModelIndex const& i : selectedRows) { TrackerInfo const inf = ui.trackersView->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 for (int const id : torrentId_to_trackerIds.uniqueKeys()) { QSet ids; ids << id; mySession.torrentSet(ids, TR_KEY_trackerRemove, torrentId_to_trackerIds.values(id)); } selectionModel->clearSelection(); getNewData(); } void DetailsDialog::initOptionsTab() { QString const speed_K_str = Formatter::unitStr(Formatter::SPEED, Formatter::KB); ui.singleDownSpin->setSuffix(QString::fromLatin1(" %1").arg(speed_K_str)); ui.singleUpSpin->setSuffix(QString::fromLatin1(" %1").arg(speed_K_str)); ui.singleDownSpin->setProperty(PREF_KEY, TR_KEY_downloadLimit); ui.singleUpSpin->setProperty(PREF_KEY, TR_KEY_uploadLimit); ui.ratioSpin->setProperty(PREF_KEY, TR_KEY_seedRatioLimit); ui.idleSpin->setProperty(PREF_KEY, TR_KEY_seedIdleLimit); ui.peerLimitSpin->setProperty(PREF_KEY, TR_KEY_peer_limit); ui.bandwidthPriorityCombo->addItem(tr("High"), TR_PRI_HIGH); ui.bandwidthPriorityCombo->addItem(tr("Normal"), TR_PRI_NORMAL); ui.bandwidthPriorityCombo->addItem(tr("Low"), TR_PRI_LOW); ui.ratioCombo->addItem(tr("Use Global Settings"), TR_RATIOLIMIT_GLOBAL); ui.ratioCombo->addItem(tr("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED); ui.ratioCombo->addItem(tr("Stop seeding at ratio:"), TR_RATIOLIMIT_SINGLE); ui.idleCombo->addItem(tr("Use Global Settings"), TR_IDLELIMIT_GLOBAL); ui.idleCombo->addItem(tr("Seed regardless of activity"), TR_IDLELIMIT_UNLIMITED); ui.idleCombo->addItem(tr("Stop seeding if idle for:"), TR_IDLELIMIT_SINGLE); ColumnResizer* cr(new ColumnResizer(this)); cr->addLayout(ui.speedSectionLayout); cr->addLayout(ui.seedingLimitsSectionRatioLayout); cr->addLayout(ui.seedingLimitsSectionIdleLayout); cr->addLayout(ui.peerConnectionsSectionLayout); cr->update(); connect(ui.sessionLimitCheck, SIGNAL(clicked(bool)), SLOT(onHonorsSessionLimitsToggled(bool))); connect(ui.singleDownCheck, SIGNAL(clicked(bool)), SLOT(onDownloadLimitedToggled(bool))); connect(ui.singleDownSpin, SIGNAL(editingFinished()), SLOT(onSpinBoxEditingFinished())); connect(ui.singleUpCheck, SIGNAL(clicked(bool)), SLOT(onUploadLimitedToggled(bool))); connect(ui.singleUpSpin, SIGNAL(editingFinished()), SLOT(onSpinBoxEditingFinished())); connect(ui.bandwidthPriorityCombo, SIGNAL(currentIndexChanged(int)), SLOT(onBandwidthPriorityChanged(int))); connect(ui.ratioCombo, SIGNAL(currentIndexChanged(int)), SLOT(onRatioModeChanged(int))); connect(ui.ratioSpin, SIGNAL(editingFinished()), SLOT(onSpinBoxEditingFinished())); connect(ui.idleCombo, SIGNAL(currentIndexChanged(int)), SLOT(onIdleModeChanged(int))); connect(ui.idleSpin, SIGNAL(editingFinished()), SLOT(onSpinBoxEditingFinished())); connect(ui.idleSpin, SIGNAL(valueChanged(int)), SLOT(onIdleLimitChanged())); connect(ui.peerLimitSpin, SIGNAL(editingFinished()), SLOT(onSpinBoxEditingFinished())); } /*** **** ***/ void DetailsDialog::initTrackerTab() { myTrackerModel = new TrackerModel(); myTrackerFilter = new TrackerModelFilter(); myTrackerFilter->setSourceModel(myTrackerModel); myTrackerDelegate = new TrackerDelegate(); ui.trackersView->setModel(myTrackerFilter); ui.trackersView->setItemDelegate(myTrackerDelegate); ui.addTrackerButton->setIcon(getStockIcon(QLatin1String("list-add"), QStyle::SP_DialogOpenButton)); ui.editTrackerButton->setIcon(getStockIcon(QLatin1String("document-properties"), QStyle::SP_DesktopIcon)); ui.removeTrackerButton->setIcon(getStockIcon(QLatin1String("list-remove"), QStyle::SP_TrashIcon)); ui.showTrackerScrapesCheck->setChecked(myPrefs.getBool(Prefs::SHOW_TRACKER_SCRAPES)); ui.showBackupTrackersCheck->setChecked(myPrefs.getBool(Prefs::SHOW_BACKUP_TRACKERS)); connect(ui.trackersView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(onTrackerSelectionChanged())); connect(ui.addTrackerButton, SIGNAL(clicked()), SLOT(onAddTrackerClicked())); connect(ui.editTrackerButton, SIGNAL(clicked()), SLOT(onEditTrackerClicked())); connect(ui.removeTrackerButton, SIGNAL(clicked()), SLOT(onRemoveTrackerClicked())); connect(ui.showTrackerScrapesCheck, SIGNAL(clicked(bool)), SLOT(onShowTrackerScrapesToggled(bool))); connect(ui.showBackupTrackersCheck, SIGNAL(clicked(bool)), SLOT(onShowBackupTrackersToggled(bool))); onTrackerSelectionChanged(); } /*** **** ***/ void DetailsDialog::initPeersTab() { QStringList headers; headers << QString() << tr("Up") << tr("Down") << tr("%") << tr("Status") << tr("Address") << tr("Client"); ui.peersView->setHeaderLabels(headers); ui.peersView->sortByColumn(COL_ADDRESS, Qt::AscendingOrder); ui.peersView->setColumnWidth(COL_LOCK, 20); ui.peersView->setColumnWidth(COL_UP, measureViewItem(ui.peersView, COL_UP, QLatin1String("1024 MiB/s"))); ui.peersView->setColumnWidth(COL_DOWN, measureViewItem(ui.peersView, COL_DOWN, QLatin1String("1024 MiB/s"))); ui.peersView->setColumnWidth(COL_PERCENT, measureViewItem(ui.peersView, COL_PERCENT, QLatin1String("100%"))); ui.peersView->setColumnWidth(COL_STATUS, measureViewItem(ui.peersView, COL_STATUS, QLatin1String("ODUK?EXI"))); ui.peersView->setColumnWidth(COL_ADDRESS, measureViewItem(ui.peersView, COL_ADDRESS, QLatin1String("888.888.888.888"))); } /*** **** ***/ void DetailsDialog::initFilesTab() { connect(ui.filesView, SIGNAL(priorityChanged(QSet, int)), SLOT(onFilePriorityChanged(QSet, int))); connect(ui.filesView, SIGNAL(wantedChanged(QSet, bool)), SLOT(onFileWantedChanged(QSet, bool))); connect(ui.filesView, SIGNAL(pathEdited(QString, QString)), SLOT(onPathEdited(QString, QString))); connect(ui.filesView, SIGNAL(openRequested(QString)), SLOT(onOpenRequested(QString))); } void DetailsDialog::onFilePriorityChanged(QSet const& 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 DetailsDialog::onFileWantedChanged(QSet const& indices, bool wanted) { tr_quark const key = wanted ? TR_KEY_files_wanted : TR_KEY_files_unwanted; mySession.torrentSet(myIds, key, indices.toList()); getNewData(); } void DetailsDialog::onPathEdited(QString const& oldpath, QString const& newname) { mySession.torrentRenamePath(myIds, oldpath, newname); } void DetailsDialog::onOpenRequested(QString const& path) { if (!mySession.isLocal()) { return; } for (int const id : myIds) { Torrent const* const tor = myModel.getTorrentFromId(id); if (tor == nullptr) { continue; } QString const localFilePath = tor->getPath() + QLatin1Char('/') + path; if (!QFile::exists(localFilePath)) { continue; } if (QDesktopServices::openUrl(QUrl::fromLocalFile(localFilePath))) { break; } } }