transmission/qt/details.cc

1459 lines
40 KiB
C++

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