transmission/qt/DetailsDialog.cc

1335 lines
38 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.
*
* $Id$
*/
#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
{
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
};
int
measureViewItem (QAbstractItemView * view, const QString& text)
{
QStyleOptionViewItemV4 option;
option.features = QStyleOptionViewItemV2::HasDisplay;
option.text = text;
return view->style ()->sizeFromContents (QStyle::CT_ItemViewItem, &option,
QSize (QWIDGETSIZE_MAX, QWIDGETSIZE_MAX), view).width ();
}
}
/***
****
***/
class PeerItem: public QTreeWidgetItem
{
Peer peer;
mutable QString collatedAddress;
QString status;
public:
PeerItem (const Peer& p): peer(p) {}
virtual ~PeerItem () {}
public:
void refresh (const Peer& p)
{
if (p.address != peer.address)
collatedAddress.clear ();
peer = p;
}
void setStatus (const QString& s) { status = s; }
virtual bool operator< (const QTreeWidgetItem & other) const
{
const PeerItem * i = dynamic_cast<const PeerItem*> (&other);
QTreeWidget * tw (treeWidget ());
const int column = tw ? 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:
const QString& address () const
{
if (collatedAddress.isEmpty ())
{
QHostAddress ipAddress;
if (ipAddress.setAddress (peer.address))
{
if (ipAddress.protocol () == QAbstractSocket::IPv4Protocol)
{
const quint32 ipv4Address = ipAddress.toIPv4Address ();
collatedAddress = QLatin1String ("1-") +
QString::fromLatin1 (QByteArray::number (ipv4Address, 16).rightJustified (8, '0'));
}
else if (ipAddress.protocol () == QAbstractSocket::IPv6Protocol)
{
const Q_IPV6ADDR 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 (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;
}
DetailsDialog::DetailsDialog (Session & session,
Prefs & prefs,
const TorrentModel& model,
QWidget * parent):
QDialog (parent, Qt::Dialog),
mySession (session),
myPrefs (prefs),
myModel (model),
myChangedTorrents (false),
myHavePendingRefresh (false)
{
ui.setupUi(this);
initInfoTab ();
initPeersTab ();
initTrackerTab ();
initFilesTab ();
initOptionsTab ();
adjustSize ();
ui.commentBrowser->setMaximumHeight (QWIDGETSIZE_MAX);
setAttribute (Qt::WA_DeleteOnClose, true);
QList<int> initKeys;
initKeys << Prefs::SHOW_TRACKER_SCRAPES
<< Prefs::SHOW_BACKUP_TRACKERS;
for (const 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);
}
DetailsDialog::~DetailsDialog ()
{
myTrackerDelegate->deleteLater ();
myTrackerFilter->deleteLater ();
myTrackerModel->deleteLater ();
}
void
DetailsDialog::setIds (const QSet<int>& ids)
{
if (ids == myIds)
return;
myChangedTorrents = true;
// stop listening to the old torrents
for (const int id: myIds)
{
const Torrent * tor = myModel.getTorrentFromId (id);
if (tor)
disconnect (tor, SIGNAL (torrentChanged (int)), this, SLOT (onTorrentChanged ()));
}
ui.filesView->clear ();
myIds = ids;
myTrackerModel->refresh (myModel, myIds);
// listen to the new torrents
for (const int id: myIds)
{
const Torrent * tor = myModel.getTorrentFromId (id);
if (tor)
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 ());
const QItemSelection selection (selectionModel->selection ());
const QModelIndex 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;
}
}
/***
****
***/
QString
DetailsDialog::timeToStringRounded (int seconds)
{
if (seconds > 60)
seconds -= (seconds % 60);
return Formatter::timeToString (seconds);
}
void
DetailsDialog::onTimer ()
{
getNewData ();
}
void
DetailsDialog::getNewData ()
{
if (!myIds.empty ())
{
QSet<int> infos;
for (const 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
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);
}
}
}
void
DetailsDialog::refresh ()
{
const int n = myIds.size ();
const bool single = n == 1;
const QString blank;
const QFontMetrics fm (fontMetrics ());
QList<const Torrent*> torrents;
QString string;
const QString none = tr ("None");
const QString mixed = tr ("Mixed");
const QString unknown = tr ("Unknown");
// build a list of torrents
for (const 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 ();
for (const Torrent * const 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 ();
}
ui.stateValueLabel->setText (string);
const QString stateString = string;
// myHaveLabel
double sizeWhenDone = 0;
double available = 0;
if (torrents.empty ())
{
string = none;
}
else
{
double leftUntilDone = 0;
int64_t haveTotal = 0;
int64_t haveVerified = 0;
int64_t haveUnverified = 0;
int64_t verifiedPieces = 0;
for (const Torrent * const 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)
{
//: 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)
{
//: 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 (const Torrent * const 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;
}
ui.downloadedValueLabel->setText (string);
// myUploadedLabel
if (torrents.empty ())
{
string = none;
}
else
{
uint64_t u = 0;
uint64_t d = 0;
for (const Torrent * 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);
const QDateTime qdt_now = QDateTime::currentDateTime ();
// myRunTimeLabel
if (torrents.empty ())
{
string = none;
}
else
{
bool allPaused = true;
QDateTime baseline = torrents[0]->lastStarted ();
for (const Torrent * 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 (const Torrent * 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 (const Torrent * const 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));
}
ui.lastActivityValueLabel->setText (string);
if (torrents.empty ())
{
string = none;
}
else
{
string = torrents[0]->getError ();
for (const Torrent * 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 (const Torrent * const 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));
}
ui.sizeValueLabel->setText (string);
// myHashLabel
string = none;
if (!torrents.empty ())
{
string = torrents[0]->hashString ();
for (const Torrent * 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 (const Torrent * 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 (const Torrent * 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;
const QString creator = torrents[0]->creator ();
const QString date = torrents[0]->dateCreated ().toString ();
for (const Torrent * const t: torrents)
{
mixed_creator |= (creator != t->creator ());
mixed_date |= (date != t->dateCreated ().toString ());
}
const bool empty_creator = creator.isEmpty ();
const bool 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 (const Torrent * 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;
const Torrent& baseline = *torrents.front ();
// mySessionLimitCheck
uniform = true;
baselineFlag = baseline.honorsSessionLimits ();
for (const Torrent * const tor: torrents) if (baselineFlag != tor->honorsSessionLimits ()) { uniform = false; break; }
ui.sessionLimitCheck->setChecked (uniform && baselineFlag);
// mySingleDownCheck
uniform = true;
baselineFlag = baseline.downloadIsLimited ();
for (const Torrent * const tor: torrents) if (baselineFlag != tor->downloadIsLimited ()) { uniform = false; break; }
ui.singleDownCheck->setChecked (uniform && baselineFlag);
// mySingleUpCheck
uniform = true;
baselineFlag = baseline.uploadIsLimited ();
for (const Torrent * const tor: torrents) if (baselineFlag != tor->uploadIsLimited ()) { uniform = false; break; }
ui.singleUpCheck->setChecked (uniform && baselineFlag);
// myBandwidthPriorityCombo
uniform = true;
baselineInt = baseline.getBandwidthPriority ();
for (const Torrent * 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 ())
{
const Torrent& baseline = *torrents.front ();
// ratio
bool uniform = true;
int baselineInt = baseline.seedRatioMode ();
for (const Torrent * 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 (const Torrent * 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 (const Torrent * const t: torrents)
{
const QString idStr (QString::number (t->id ()));
PeerList peers = t->peers ();
for (const Peer& peer: peers)
{
const QString key = idStr + QLatin1Char (':') + peer.address;
PeerItem * item = static_cast<PeerItem*> (myPeers.value (key, 0));
if (item == 0) // new peer has connected
{
static const QIcon myEncryptionIcon (QLatin1String (":/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") : QString ());
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;
for (const 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::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 (const QString& key: myPeers.keys ())
{
if (!peers2.contains (key)) // old peer has disconnected
{
QTreeWidgetItem * item = myPeers.value (key, 0);
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 ()
{
const int 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 ()
{
const QObject * spin = sender ();
const tr_quark key = spin->property (PREF_KEY).toInt ();
const QDoubleSpinBox * d = qobject_cast<const QDoubleSpinBox*> (spin);
if (d)
mySession.torrentSet (myIds, key, d->value ());
else
mySession.torrentSet (myIds, key, qobject_cast<const QSpinBox*> (spin)->value ());
getNewData ();
}
void
DetailsDialog::onUploadLimitedToggled (bool val)
{
mySession.torrentSet (myIds, TR_KEY_uploadLimited, val);
getNewData ();
}
void
DetailsDialog::onIdleModeChanged (int index)
{
const int 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)
const QString unitsSuffix = tr (" minute(s)", 0, ui.idleSpin->value ());
if (ui.idleSpin->suffix () != unitsSuffix)
ui.idleSpin->setSuffix (unitsSuffix);
}
void
DetailsDialog::onRatioModeChanged (int index)
{
const int val = ui.ratioCombo->itemData (index).toInt ();
mySession.torrentSet (myIds, TR_KEY_seedRatioMode, val);
}
void
DetailsDialog::onBandwidthPriorityChanged (int index)
{
if (index != -1)
{
const int priority = ui.bandwidthPriorityCombo->itemData (index).toInt ();
mySession.torrentSet (myIds, TR_KEY_bandwidthPriority, priority);
getNewData ();
}
}
void
DetailsDialog::onTrackerSelectionChanged ()
{
const int selectionCount = ui.trackersView->selectionModel ()->selectedRows ().size ();
ui.editTrackerButton->setEnabled (selectionCount == 1);
ui.removeTrackerButton->setEnabled (selectionCount > 0);
}
void
DetailsDialog::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<int> ids;
for (const 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
DetailsDialog::onEditTrackerClicked ()
{
QItemSelectionModel * selectionModel = ui.trackersView->selectionModel ();
QModelIndexList selectedRows = selectionModel->selectedRows ();
assert (selectedRows.size () == 1);
QModelIndex i = selectionModel->currentIndex ();
const TrackerInfo trackerInfo = ui.trackersView->model ()->data (i, TrackerModel::TrackerRole).value<TrackerInfo> ();
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<int> ids;
ids << trackerInfo.torrentId;
const QPair<int,QString> 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 (const QModelIndex& i: selectedRows)
{
const TrackerInfo 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 (const int 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 ()
{
const QString 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, QLatin1String ("1024 MiB/s")));
ui.peersView->setColumnWidth (COL_DOWN, measureViewItem (ui.peersView, QLatin1String ("1024 MiB/s")));
ui.peersView->setColumnWidth (COL_PERCENT, measureViewItem (ui.peersView, QLatin1String ("100%")));
ui.peersView->setColumnWidth (COL_STATUS, measureViewItem (ui.peersView, QLatin1String ("ODUK?EXI")));
ui.peersView->setColumnWidth (COL_ADDRESS, measureViewItem (ui.peersView, 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 (const QSet<int>& 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 (const QSet<int>& indices, bool wanted)
{
const tr_quark key = wanted ? TR_KEY_files_wanted : TR_KEY_files_unwanted;
mySession.torrentSet (myIds, key, indices.toList ());
getNewData ();
}
void
DetailsDialog::onPathEdited (const QString& oldpath, const QString& newname)
{
mySession.torrentRenamePath (myIds, oldpath, newname);
}
void
DetailsDialog::onOpenRequested (const QString& path)
{
if (!mySession.isLocal ())
return;
for (const int id: myIds)
{
const Torrent * const tor = myModel.getTorrentFromId (id);
if (tor == NULL)
continue;
const QString localFilePath = tor->getPath () + QLatin1Char ('/') + path;
if (!QFile::exists (localFilePath))
continue;
if (QDesktopServices::openUrl (QUrl::fromLocalFile (localFilePath)))
break;
}
}