transmission/qt/DetailsDialog.cc

1580 lines
40 KiB
C++

/*
* 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 <cassert>
#include <climits> /* INT_MAX */
#include <ctime>
#include <QDateTime>
#include <QDesktopServices>
#include <QEvent>
#include <QFont>
#include <QFontMetrics>
#include <QHeaderView>
#include <QHostAddress>
#include <QInputDialog>
#include <QItemSelectionModel>
#include <QLabel>
#include <QList>
#include <QMap>
#include <QMessageBox>
#include <QResizeEvent>
#include <QStringList>
#include <QStyle>
#include <QTreeWidgetItem>
#include <libtransmission/transmission.h>
#include <libtransmission/utils.h> // 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<PeerItem const*>(&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<int> 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<int> 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<int> 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<Torrent const*> 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, 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<QString, QTreeWidgetItem*> peers2;
QList<QTreeWidgetItem*> 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<PeerItem*>(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<QDoubleSpinBox const*>(spin);
if (d != nullptr)
{
mySession.torrentSet(myIds, key, d->value());
}
else
{
mySession.torrentSet(myIds, key, qobject_cast<QSpinBox const*>(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<int> 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<TrackerInfo>();
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<int> ids;
ids << trackerInfo.torrentId;
QPair<int, QString> 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<int, int> torrentId_to_trackerIds;
for (QModelIndex const& i : selectedRows)
{
TrackerInfo const inf = ui.trackersView->model()->data(i, TrackerModel::TrackerRole).value<TrackerInfo>();
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<int> 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>, int)), SLOT(onFilePriorityChanged(QSet<int>, int)));
connect(ui.filesView, SIGNAL(wantedChanged(QSet<int>, bool)), SLOT(onFileWantedChanged(QSet<int>, 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<int> 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<int> 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;
}
}
}