source: trunk/qt/details.cc @ 14387

Last change on this file since 14387 was 14387, checked in by mikedld, 8 years ago

Rework torrent details dialog in Qt client to load from .ui

Make squeeze labels (used for values on Information tab) display tooltip
on hover if their text doesn't fit. Make selective labels (same as in
GTK+ client) text selectable with keyboard in addition to mouse.
Prevent dialog width growth occured before to fit long error texts.

  • Property svn:keywords set to Date Rev Author Id
File size: 35.9 KB
Line 
1/*
2 * This file Copyright (C) 2009-2014 Mnemosyne LLC
3 *
4 * It may be used under the GNU Public License v2 or v3 licenses,
5 * or any future license endorsed by Mnemosyne LLC.
6 *
7 * $Id: details.cc 14387 2014-12-14 18:12:21Z mikedld $
8 */
9
10#include <cassert>
11#include <climits> /* INT_MAX */
12#include <ctime>
13
14#include <QDateTime>
15#include <QDesktopServices>
16#include <QEvent>
17#include <QFont>
18#include <QFontMetrics>
19#include <QHeaderView>
20#include <QHostAddress>
21#include <QInputDialog>
22#include <QItemSelectionModel>
23#include <QLabel>
24#include <QList>
25#include <QMap>
26#include <QMessageBox>
27#include <QResizeEvent>
28#include <QStringList>
29#include <QStyle>
30#include <QTreeWidgetItem>
31
32#include <libtransmission/transmission.h>
33#include <libtransmission/utils.h> // tr_getRatio ()
34
35#include "details.h"
36#include "file-tree.h"
37#include "formatter.h"
38#include "hig.h"
39#include "prefs.h"
40#include "session.h"
41#include "squeezelabel.h"
42#include "torrent.h"
43#include "torrent-model.h"
44#include "tracker-delegate.h"
45#include "tracker-model.h"
46#include "tracker-model-filter.h"
47#include "utils.h"
48
49class Prefs;
50class Session;
51
52/****
53*****
54****/
55
56namespace
57{
58  const int REFRESH_INTERVAL_MSEC = 4000;
59
60  const char * PREF_KEY ("pref-key");
61
62  enum // peer columns
63  {
64    COL_LOCK,
65    COL_UP,
66    COL_DOWN,
67    COL_PERCENT,
68    COL_STATUS,
69    COL_ADDRESS,
70    COL_CLIENT,
71    N_COLUMNS
72  };
73
74  int
75  measureViewItem (QAbstractItemView * view, const QString& text)
76  {
77    QStyleOptionViewItemV4 option;
78    option.features = QStyleOptionViewItemV2::HasDisplay;
79    option.text = text;
80    return view->style ()->sizeFromContents (QStyle::CT_ItemViewItem, &option,
81      QSize (QWIDGETSIZE_MAX, QWIDGETSIZE_MAX), view).width ();
82  }
83}
84
85/***
86****
87***/
88
89class PeerItem: public QTreeWidgetItem
90{
91    Peer peer;
92    mutable QString collatedAddress;
93    QString status;
94
95  public:
96    PeerItem (const Peer& p): peer(p) {}
97    virtual ~PeerItem () {}
98
99  public:
100    void refresh (const Peer& p)
101    {
102      if (p.address != peer.address)
103        collatedAddress.clear ();
104      peer = p;
105    }
106
107    void setStatus (const QString& s) { status = s; }
108
109    virtual bool operator< (const QTreeWidgetItem & other) const
110    {
111      const PeerItem * i = dynamic_cast<const PeerItem*> (&other);
112      QTreeWidget * tw (treeWidget ());
113      const int column = tw ? tw->sortColumn () : 0;
114      switch (column)
115        {
116          case COL_UP: return peer.rateToPeer < i->peer.rateToPeer;
117          case COL_DOWN: return peer.rateToClient < i->peer.rateToClient;
118          case COL_PERCENT: return peer.progress < i->peer.progress;
119          case COL_STATUS: return status < i->status;
120          case COL_CLIENT: return peer.clientName < i->peer.clientName;
121          case COL_LOCK: return peer.isEncrypted && !i->peer.isEncrypted;
122          default: return address () < i->address ();
123        }
124    }
125
126  private:
127    const QString& address () const
128    {
129      if (collatedAddress.isEmpty ())
130        {
131          QHostAddress ipAddress;
132          if (ipAddress.setAddress (peer.address))
133            {
134              if (ipAddress.protocol () == QAbstractSocket::IPv4Protocol)
135                {
136                  const quint32 ipv4Address = ipAddress.toIPv4Address ();
137                  collatedAddress = QLatin1String ("1-") +
138                    QString::fromLatin1 (QByteArray::number (ipv4Address, 16).rightJustified (8, '0'));
139                }
140              else if (ipAddress.protocol () == QAbstractSocket::IPv6Protocol)
141                {
142                  const Q_IPV6ADDR ipv6Address = ipAddress.toIPv6Address ();
143                  QByteArray tmp (16, '\0');
144                  for (int i = 0; i < 16; ++i)
145                    tmp[i] = ipv6Address[i];
146                  collatedAddress = QLatin1String ("2-") + QString::fromLatin1 (tmp.toHex ());
147                }
148            }
149
150          if (collatedAddress.isEmpty ())
151            collatedAddress = QLatin1String ("3-") + peer.address.toLower ();
152        }
153
154      return collatedAddress;
155    }
156};
157
158/***
159****
160***/
161
162QIcon
163Details::getStockIcon (const QString& freedesktop_name, int fallback)
164{
165  QIcon icon = QIcon::fromTheme (freedesktop_name);
166
167  if (icon.isNull ())
168    icon = style ()->standardIcon (QStyle::StandardPixmap (fallback), 0, this);
169
170  return icon;
171}
172
173Details::Details (Session       & session,
174                  Prefs         & prefs,
175                  TorrentModel  & model,
176                  QWidget       * parent):
177  QDialog (parent, Qt::Dialog),
178  mySession (session),
179  myPrefs (prefs),
180  myModel (model),
181  myChangedTorrents (false),
182  myHavePendingRefresh (false)
183{
184  ui.setupUi(this);
185
186  initInfoTab ();
187  initPeersTab ();
188  initTrackerTab ();
189  initFilesTab ();
190  initOptionsTab ();
191
192  adjustSize ();
193  ui.commentBrowser->setMaximumHeight (QWIDGETSIZE_MAX);
194
195  setAttribute (Qt::WA_DeleteOnClose, true);
196
197  QList<int> initKeys;
198  initKeys << Prefs::SHOW_TRACKER_SCRAPES
199           << Prefs::SHOW_BACKUP_TRACKERS;
200  foreach (int key, initKeys)
201    refreshPref (key);
202
203  connect (&myTimer, SIGNAL (timeout ()), this, SLOT (onTimer ()));
204  connect (&myPrefs, SIGNAL (changed (int)), this, SLOT (refreshPref (int)));
205
206  onTimer ();
207  myTimer.setSingleShot (false);
208  myTimer.start (REFRESH_INTERVAL_MSEC);
209}
210
211Details::~Details ()
212{
213  myTrackerDelegate->deleteLater ();
214  myTrackerFilter->deleteLater ();
215  myTrackerModel->deleteLater ();
216}
217
218void
219Details::setIds (const QSet<int>& ids)
220{
221  if (ids == myIds)
222    return;
223
224  myChangedTorrents = true;
225
226  // stop listening to the old torrents
227  foreach (int id, myIds)
228    {
229      const Torrent * tor = myModel.getTorrentFromId (id);
230      if (tor)
231        disconnect (tor, SIGNAL (torrentChanged (int)), this, SLOT (onTorrentChanged ()));
232    }
233
234  ui.filesView->clear ();
235  myIds = ids;
236  myTrackerModel->refresh (myModel, myIds);
237
238  // listen to the new torrents
239  foreach (int id, myIds)
240    {
241      const Torrent * tor = myModel.getTorrentFromId (id);
242      if (tor)
243        connect (tor, SIGNAL (torrentChanged (int)), this, SLOT (onTorrentChanged ()));
244    }
245
246  for (int i = 0; i < ui.tabs->count (); ++i)
247    ui.tabs->widget (i)->setEnabled (false);
248
249  onTimer ();
250}
251
252void
253Details::refreshPref (int key)
254{
255  QString str;
256
257  switch (key)
258    {
259      case Prefs::SHOW_TRACKER_SCRAPES:
260        {
261          QItemSelectionModel * selectionModel (ui.trackersView->selectionModel ());
262          const QItemSelection selection (selectionModel->selection ());
263          const QModelIndex currentIndex (selectionModel->currentIndex ());
264          myTrackerDelegate->setShowMore (myPrefs.getBool (key));
265          selectionModel->clear ();
266          ui.trackersView->reset ();
267          selectionModel->select (selection, QItemSelectionModel::Select);
268          selectionModel->setCurrentIndex (currentIndex, QItemSelectionModel::NoUpdate);
269          break;
270        }
271
272      case Prefs::SHOW_BACKUP_TRACKERS:
273        myTrackerFilter->setShowBackupTrackers (myPrefs.getBool (key));
274        break;
275
276      default:
277        break;
278    }
279}
280
281
282/***
283****
284***/
285
286QString
287Details::timeToStringRounded (int seconds)
288{
289  if (seconds > 60)
290    seconds -= (seconds % 60);
291
292  return Formatter::timeToString (seconds);
293}
294
295void
296Details::onTimer ()
297{
298  getNewData ();
299}
300
301void
302Details::getNewData ()
303{
304  if (!myIds.empty ())
305    {
306      QSet<int> infos;
307      foreach (int id, myIds)
308        {
309          const Torrent * tor = myModel.getTorrentFromId (id);
310          if (tor->isMagnet ())
311            infos.insert (tor->id ());
312        }
313
314      if (!infos.isEmpty ())
315        mySession.initTorrents (infos);
316      mySession.refreshExtraStats (myIds);
317    }
318}
319
320void
321Details::onTorrentChanged ()
322{
323  if (!myHavePendingRefresh)
324    {
325      myHavePendingRefresh = true;
326      QTimer::singleShot (100, this, SLOT (refresh ()));
327    }
328}
329
330namespace
331{
332  void setIfIdle (QComboBox * box, int i)
333  {
334    if (!box->hasFocus ())
335      {
336        box->blockSignals (true);
337        box->setCurrentIndex (i);
338        box->blockSignals (false);
339      }
340  }
341
342  void setIfIdle (QDoubleSpinBox * spin, double value)
343  {
344    if (!spin->hasFocus ())
345      {
346        spin->blockSignals (true);
347        spin->setValue (value);
348        spin->blockSignals (false);
349      }
350  }
351
352  void setIfIdle (QSpinBox * spin, int value)
353  {
354    if (!spin->hasFocus ())
355      {
356        spin->blockSignals (true);
357        spin->setValue (value);
358        spin->blockSignals (false);
359      }
360  }
361}
362
363void
364Details::refresh ()
365{
366  const int n = myIds.size ();
367  const bool single = n == 1;
368  const QString blank;
369  const QFontMetrics fm (fontMetrics ());
370  QList<const Torrent*> torrents;
371  QString string;
372  const QString none = tr ("None");
373  const QString mixed = tr ("Mixed");
374  const QString unknown = tr ("Unknown");
375
376  // build a list of torrents
377  foreach (int id, myIds)
378    {
379      const Torrent * tor = myModel.getTorrentFromId (id);
380      if (tor)
381        torrents << tor;
382    }
383
384  ///
385  ///  activity tab
386  ///
387
388  // myStateLabel
389  if (torrents.empty ())
390    {
391      string = none;
392    }
393  else
394    {
395      bool isMixed = false;
396      bool allPaused = true;
397      bool allFinished = true;
398      const tr_torrent_activity baseline = torrents[0]->getActivity ();
399      foreach (const Torrent * t, torrents)
400        {
401          const tr_torrent_activity activity = t->getActivity ();
402          if (activity != baseline)
403            isMixed = true;
404          if (activity != TR_STATUS_STOPPED)
405            allPaused = allFinished = false;
406          if (!t->isFinished ())
407            allFinished = false;
408        }
409
410      if (isMixed)
411        string = mixed;
412      else if (allFinished)
413        string = tr ("Finished");
414      else if (allPaused)
415        string = tr ("Paused");
416      else
417        string = torrents[0]->activityString ();
418    }
419  ui.stateValueLabel->setText (string);
420  const QString stateString = string;
421
422  // myHaveLabel
423  double sizeWhenDone = 0;
424  double available = 0;
425  if (torrents.empty ())
426    {
427      string = none;
428    }
429  else
430    {
431      double leftUntilDone = 0;
432      int64_t haveTotal = 0;
433      int64_t haveVerified = 0;
434      int64_t haveUnverified = 0;
435      int64_t verifiedPieces = 0;
436
437      foreach (const Torrent * t, torrents)
438        {
439          if (t->hasMetadata ())
440            {
441              haveTotal += t->haveTotal ();
442              haveUnverified += t->haveUnverified ();
443              const uint64_t v = t->haveVerified ();
444              haveVerified += v;
445              if (t->pieceSize ())
446                verifiedPieces += v / t->pieceSize ();
447              sizeWhenDone += t->sizeWhenDone ();
448              leftUntilDone += t->leftUntilDone ();
449              available += t->sizeWhenDone () - t->leftUntilDone () + t->desiredAvailable ();
450            }
451        }
452
453      const double d = 100.0 * (sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
454      QString pct = Formatter::percentToString (d);
455
456      if (!haveUnverified && !leftUntilDone)
457        {
458          string = tr ("%1 (100%)")
459                     .arg (Formatter::sizeToString (haveVerified));
460        }
461      else if (!haveUnverified)
462        {
463          string = tr ("%1 of %2 (%3%)")
464                     .arg (Formatter::sizeToString (haveVerified))
465                     .arg (Formatter::sizeToString (sizeWhenDone))
466                     .arg (pct);
467        }
468      else
469        {
470          string = tr ("%1 of %2 (%3%), %4 Unverified")
471                     .arg (Formatter::sizeToString (haveVerified + haveUnverified))
472                     .arg (Formatter::sizeToString (sizeWhenDone))
473                     .arg (pct)
474                     .arg (Formatter::sizeToString (haveUnverified));
475        }
476    }
477  ui.haveValueLabel->setText (string);
478
479  // myAvailabilityLabel
480  if (torrents.empty ())
481    string = none;
482  else if (sizeWhenDone == 0)
483    string = none;
484  else
485    string = QString ("%1%").arg (Formatter::percentToString ( (100.0 * available) / sizeWhenDone));
486  ui.availabilityValueLabel->setText (string);
487
488  // myDownloadedLabel
489  if (torrents.empty ())
490    {
491      string = none;
492    }
493  else
494    {
495      uint64_t d = 0;
496      uint64_t f = 0;
497      foreach (const Torrent * t, torrents)
498        {
499          d += t->downloadedEver ();
500          f += t->failedEver ();
501        }
502      const QString dstr = Formatter::sizeToString (d);
503      const QString fstr = Formatter::sizeToString (f);
504      if (f)
505        string = tr ("%1 (%2 corrupt)").arg (dstr).arg (fstr);
506      else
507        string = dstr;
508    }
509  ui.downloadedValueLabel->setText (string);
510
511  //  myUploadedLabel
512  if (torrents.empty ())
513    {
514      string = none;
515    }
516  else
517    {
518      uint64_t u = 0;
519      uint64_t d = 0;
520      foreach (const Torrent * t, torrents)
521        {
522          u += t->uploadedEver ();
523          d += t->downloadedEver ();
524        }
525      string = tr ("%1 (Ratio: %2)")
526                 .arg (Formatter::sizeToString (u))
527                 .arg (Formatter::ratioToString (tr_getRatio (u, d)));
528    }
529  ui.uploadedValueLabel->setText (string);
530
531  const QDateTime qdt_now = QDateTime::currentDateTime ();
532
533  // myRunTimeLabel
534  if (torrents.empty ())
535    {
536      string = none;
537    }
538  else
539    {
540      bool allPaused = true;
541      QDateTime baseline = torrents[0]->lastStarted ();
542      foreach (const Torrent * t, torrents)
543        {
544          if (baseline != t->lastStarted ())
545            baseline = QDateTime ();
546          if (!t->isPaused ())
547            allPaused = false;
548        }
549
550      if (allPaused)
551        string = stateString; // paused || finished
552      else if (baseline.isNull ())
553        string = mixed;
554      else
555        string = Formatter::timeToString (baseline.secsTo (qdt_now));
556    }
557  ui.runningTimeValueLabel->setText (string);
558
559
560  // myETALabel
561  string.clear ();
562  if (torrents.empty ())
563    {
564      string = none;
565    }
566  else
567    {
568      int baseline = torrents[0]->getETA ();
569      foreach (const Torrent * t, torrents)
570        {
571          if (baseline != t->getETA ())
572            {
573              string = mixed;
574              break;
575            }
576        }
577
578      if (string.isEmpty ())
579        {
580          if (baseline < 0)
581            string = tr ("Unknown");
582          else
583            string = Formatter::timeToString (baseline);
584       }
585    }
586  ui.remainingTimeValueLabel->setText (string);
587
588
589  // myLastActivityLabel
590  if (torrents.empty ())
591    {
592      string = none;
593    }
594  else
595    {
596      QDateTime latest = torrents[0]->lastActivity ();
597      foreach (const Torrent * t, torrents)
598        {
599          const QDateTime dt = t->lastActivity ();
600          if (latest < dt)
601            latest = dt;
602        }
603
604      const int seconds = latest.isValid () ? latest.secsTo (qdt_now) : -1;
605      if (seconds < 0)
606        string = none;
607      else if (seconds < 5)
608        string = tr ("Active now");
609      else
610        string = tr ("%1 ago").arg (Formatter::timeToString (seconds));
611    }
612  ui.lastActivityValueLabel->setText (string);
613
614
615  if (torrents.empty ())
616    {
617      string = none;
618    }
619  else
620    {
621      string = torrents[0]->getError ();
622      foreach (const Torrent * t, torrents)
623        {
624          if (string != t->getError ())
625            {
626              string = mixed;
627              break;
628            }
629        }
630    }
631  if (string.isEmpty ())
632    string = none;
633  ui.errorValueLabel->setText (string);
634
635
636  ///
637  /// information tab
638  ///
639
640  // mySizeLabel
641  if (torrents.empty ())
642    {
643      string = none;
644    }
645  else
646    {
647      int pieces = 0;
648      uint64_t size = 0;
649      uint32_t pieceSize = torrents[0]->pieceSize ();
650      foreach (const Torrent * t, torrents)
651        {
652          pieces += t->pieceCount ();
653          size += t->totalSize ();
654          if (pieceSize != t->pieceSize ())
655            pieceSize = 0;
656        }
657
658      if (!size)
659        string = none;
660      else if (pieceSize > 0)
661        string = tr ("%1 (%Ln pieces @ %2)", "", pieces)
662                   .arg (Formatter::sizeToString (size))
663                   .arg (Formatter::memToString (pieceSize));
664      else
665        string = tr ("%1 (%Ln pieces)", "", pieces)
666                   .arg (Formatter::sizeToString (size));
667    }
668  ui.sizeValueLabel->setText (string);
669
670  // myHashLabel
671  string = none;
672  if (!torrents.empty ())
673    {
674      string = torrents[0]->hashString ();
675      foreach (const Torrent * t, torrents)
676        {
677          if (string != t->hashString ())
678            {
679              string = mixed;
680              break;
681            }
682        }
683    }
684  ui.hashValueLabel->setText (string);
685
686  // myPrivacyLabel
687  string = none;
688  if (!torrents.empty ())
689    {
690      bool b = torrents[0]->isPrivate ();
691      string = b ? tr ("Private to this tracker -- DHT and PEX disabled")
692                 : tr ("Public torrent");
693      foreach (const Torrent * t, torrents)
694        {
695          if (b != t->isPrivate ())
696            {
697              string = mixed;
698              break;
699            }
700        }
701    }
702  ui.privacyValueLabel->setText (string);
703
704  // myCommentBrowser
705  string = none;
706  bool isCommentMixed = false;
707  if (!torrents.empty ())
708    {
709      string = torrents[0]->comment ();
710      foreach (const Torrent * t, torrents)
711        {
712          if (string != t->comment ())
713            {
714              string = mixed;
715              isCommentMixed = true;
716              break;
717            }
718        }
719    }
720  if (ui.commentBrowser->toPlainText() != string)
721    {
722      ui.commentBrowser->setText (string);
723    }
724  ui.commentBrowser->setEnabled (!isCommentMixed && !string.isEmpty ());
725
726  // myOriginLabel
727  string = none;
728  if (!torrents.empty ())
729    {
730      bool mixed_creator=false, mixed_date=false;
731      const QString creator = torrents[0]->creator ();
732      const QString date = torrents[0]->dateCreated ().toString ();
733      foreach (const Torrent * t, torrents)
734        {
735          mixed_creator |= (creator != t->creator ());
736          mixed_date |= (date != t->dateCreated ().toString ());
737        }
738
739      if (mixed_creator && mixed_date)
740        string = mixed;
741      else if (mixed_date && !creator.isEmpty ())
742        string = tr ("Created by %1").arg (creator);
743      else if (mixed_creator && !date.isEmpty ())
744        string = tr ("Created on %1").arg (date);
745      else if (creator.isEmpty () && date.isEmpty ())
746        string = tr ("N/A");
747      else
748        string = tr ("Created by %1 on %2").arg (creator).arg (date);
749    }
750  ui.originValueLabel->setText (string);
751
752  // myLocationLabel
753  string = none;
754  if (!torrents.empty ())
755    {
756      string = torrents[0]->getPath ();
757      foreach (const Torrent * t, torrents)
758        {
759          if (string != t->getPath ())
760            {
761              string = mixed;
762              break;
763            }
764        }
765    }
766  ui.locationValueLabel->setText (string);
767
768
769  ///
770  ///  Options Tab
771  ///
772
773  if (myChangedTorrents && !torrents.empty ())
774    {
775      int i;
776      bool uniform;
777      bool baselineFlag;
778      int baselineInt;
779      const Torrent * tor;
780      const Torrent * baseline = *torrents.begin ();
781
782      // mySessionLimitCheck
783      uniform = true;
784      baselineFlag = baseline->honorsSessionLimits ();
785      foreach (tor, torrents) if (baselineFlag != tor->honorsSessionLimits ()) { uniform = false; break; }
786      ui.sessionLimitCheck->setChecked (uniform && baselineFlag);
787
788      // mySingleDownCheck
789      uniform = true;
790      baselineFlag = baseline->downloadIsLimited ();
791      foreach (tor, torrents) if (baselineFlag != tor->downloadIsLimited ()) { uniform = false; break; }
792      ui.singleDownCheck->setChecked (uniform && baselineFlag);
793
794      // mySingleUpCheck
795      uniform = true;
796      baselineFlag = baseline->uploadIsLimited ();
797      foreach (tor, torrents) if (baselineFlag != tor->uploadIsLimited ()) { uniform = false; break; }
798      ui.singleUpCheck->setChecked (uniform && baselineFlag);
799
800      // myBandwidthPriorityCombo
801      uniform = true;
802      baselineInt = baseline->getBandwidthPriority ();
803      foreach (tor, torrents) if (baselineInt != tor->getBandwidthPriority ()) { uniform = false; break; }
804      if (uniform)
805        i = ui.bandwidthPriorityCombo->findData (baselineInt);
806      else
807        i = -1;
808      setIfIdle (ui.bandwidthPriorityCombo, i);
809
810      setIfIdle (ui.singleDownSpin, int (tor->downloadLimit ().KBps ()));
811      setIfIdle (ui.singleUpSpin, int (tor->uploadLimit ().KBps ()));
812      setIfIdle (ui.peerLimitSpin, tor->peerLimit ());
813    }
814
815  if (!torrents.empty ())
816    {
817      const Torrent * tor;
818
819      // ratio
820      bool uniform = true;
821      int baselineInt = torrents[0]->seedRatioMode ();
822      foreach (tor, torrents) if (baselineInt != tor->seedRatioMode ()) { uniform = false; break; }
823
824      setIfIdle (ui.ratioCombo, uniform ? ui.ratioCombo->findData (baselineInt) : -1);
825      ui.ratioSpin->setVisible (uniform && (baselineInt == TR_RATIOLIMIT_SINGLE));
826
827      setIfIdle (ui.ratioSpin, tor->seedRatioLimit ());
828
829      // idle
830      uniform = true;
831      baselineInt = torrents[0]->seedIdleMode ();
832      foreach (tor, torrents) if (baselineInt != tor->seedIdleMode ()) { uniform = false; break; }
833
834      setIfIdle (ui.idleCombo, uniform ? ui.idleCombo->findData (baselineInt) : -1);
835      ui.idleSpin->setVisible (uniform && (baselineInt == TR_RATIOLIMIT_SINGLE));
836
837      setIfIdle (ui.idleSpin, tor->seedIdleLimit ());
838    }
839
840  ///
841  ///  Tracker tab
842  ///
843
844  myTrackerModel->refresh (myModel, myIds);
845
846  ///
847  ///  Peers tab
848  ///
849
850  QMap<QString,QTreeWidgetItem*> peers2;
851  QList<QTreeWidgetItem*> newItems;
852  foreach (const Torrent * t, torrents)
853    {
854      const QString idStr (QString::number (t->id ()));
855      PeerList peers = t->peers ();
856
857      foreach (const Peer& peer, peers)
858        {
859          const QString key = idStr + ":" + peer.address;
860          PeerItem * item = static_cast<PeerItem*> (myPeers.value (key, 0));
861
862          if (item == 0) // new peer has connected
863            {
864              static const QIcon myEncryptionIcon (":/icons/encrypted.png");
865              static const QIcon myEmptyIcon;
866              item = new PeerItem (peer);
867              item->setTextAlignment (COL_UP, Qt::AlignRight|Qt::AlignVCenter);
868              item->setTextAlignment (COL_DOWN, Qt::AlignRight|Qt::AlignVCenter);
869              item->setTextAlignment (COL_PERCENT, Qt::AlignRight|Qt::AlignVCenter);
870              item->setIcon (COL_LOCK, peer.isEncrypted ? myEncryptionIcon : myEmptyIcon);
871              item->setToolTip (COL_LOCK, peer.isEncrypted ? tr ("Encrypted connection") : "");
872              item->setText (COL_ADDRESS, peer.address);
873              item->setText (COL_CLIENT, peer.clientName);
874              newItems << item;
875            }
876
877          const QString code = peer.flagStr;
878          item->setStatus (code);
879          item->refresh (peer);
880
881          QString codeTip;
882          foreach (QChar ch, code)
883            {
884              QString txt;
885              switch (ch.unicode ())
886                {
887                  case 'O': txt = tr ("Optimistic unchoke"); break;
888                  case 'D': txt = tr ("Downloading from this peer"); break;
889                  case 'd': txt = tr ("We would download from this peer if they would let us"); break;
890                  case 'U': txt = tr ("Uploading to peer"); break;
891                  case 'u': txt = tr ("We would upload to this peer if they asked"); break;
892                  case 'K': txt = tr ("Peer has unchoked us, but we're not interested"); break;
893                  case '?': txt = tr ("We unchoked this peer, but they're not interested"); break;
894                  case 'E': txt = tr ("Encrypted connection"); break;
895                  case 'H': txt = tr ("Peer was discovered through DHT"); break;
896                  case 'X': txt = tr ("Peer was discovered through Peer Exchange (PEX)"); break;
897                  case 'I': txt = tr ("Peer is an incoming connection"); break;
898                  case 'T': txt = tr ("Peer is connected over uTP"); break;
899                }
900
901              if (!txt.isEmpty ())
902                codeTip += QString ("%1: %2\n").arg (ch).arg (txt);
903            }
904
905          if (!codeTip.isEmpty ())
906            codeTip.resize (codeTip.size ()-1); // eat the trailing linefeed
907
908          item->setText (COL_UP, peer.rateToPeer.isZero () ? "" : Formatter::speedToString (peer.rateToPeer));
909          item->setText (COL_DOWN, peer.rateToClient.isZero () ? "" : Formatter::speedToString (peer.rateToClient));
910          item->setText (COL_PERCENT, peer.progress > 0 ? QString ("%1%").arg ( (int) (peer.progress * 100.0)) : "");
911          item->setText (COL_STATUS, code);
912          item->setToolTip (COL_STATUS, codeTip);
913
914          peers2.insert (key, item);
915        }
916    }
917
918  ui.peersView->addTopLevelItems (newItems);
919  foreach (QString key, myPeers.keys ())
920    {
921      if (!peers2.contains (key)) // old peer has disconnected
922        {
923          QTreeWidgetItem * item = myPeers.value (key, 0);
924          ui.peersView->takeTopLevelItem (ui.peersView->indexOfTopLevelItem (item));
925          delete item;
926        }
927    }
928  myPeers = peers2;
929
930  if (!single)
931    ui.filesView->clear ();
932  if (single)
933    ui.filesView->update (torrents[0]->files (), myChangedTorrents);
934
935  myChangedTorrents = false;
936  myHavePendingRefresh = false;
937  for (int i = 0; i < ui.tabs->count (); ++i)
938    ui.tabs->widget (i)->setEnabled (true);
939}
940
941/***
942****
943***/
944
945void
946Details::initInfoTab ()
947{
948  const int h = QFontMetrics (ui.commentBrowser->font ()).lineSpacing () * 4;
949  ui.commentBrowser->setFixedHeight (h);
950}
951
952/***
953****
954***/
955
956void
957Details::onShowTrackerScrapesToggled (bool val)
958{
959  myPrefs.set (Prefs::SHOW_TRACKER_SCRAPES, val);
960}
961
962void
963Details::onShowBackupTrackersToggled (bool val)
964{
965  myPrefs.set (Prefs::SHOW_BACKUP_TRACKERS, val);
966}
967
968void
969Details::onHonorsSessionLimitsToggled (bool val)
970{
971  mySession.torrentSet (myIds, TR_KEY_honorsSessionLimits, val);
972  getNewData ();
973}
974void
975Details::onDownloadLimitedToggled (bool val)
976{
977  mySession.torrentSet (myIds, TR_KEY_downloadLimited, val);
978  getNewData ();
979}
980void
981Details::onSpinBoxEditingFinished ()
982{
983  const QObject * spin = sender ();
984  const tr_quark key = spin->property (PREF_KEY).toInt ();
985  const QDoubleSpinBox * d = qobject_cast<const QDoubleSpinBox*> (spin);
986  if (d)
987    mySession.torrentSet (myIds, key, d->value ());
988  else
989    mySession.torrentSet (myIds, key, qobject_cast<const QSpinBox*> (spin)->value ());
990  getNewData ();
991}
992
993void
994Details::onUploadLimitedToggled (bool val)
995{
996  mySession.torrentSet (myIds, TR_KEY_uploadLimited, val);
997  getNewData ();
998}
999
1000void
1001Details::onIdleModeChanged (int index)
1002{
1003  const int val = ui.idleCombo->itemData (index).toInt ();
1004  mySession.torrentSet (myIds, TR_KEY_seedIdleMode, val);
1005  getNewData ();
1006}
1007
1008void
1009Details::onRatioModeChanged (int index)
1010{
1011  const int val = ui.ratioCombo->itemData (index).toInt ();
1012  mySession.torrentSet (myIds, TR_KEY_seedRatioMode, val);
1013}
1014
1015void
1016Details::onBandwidthPriorityChanged (int index)
1017{
1018  if (index != -1)
1019    {
1020      const int priority = ui.bandwidthPriorityCombo->itemData (index).toInt ();
1021      mySession.torrentSet (myIds, TR_KEY_bandwidthPriority, priority);
1022      getNewData ();
1023    }
1024}
1025
1026void
1027Details::onTrackerSelectionChanged ()
1028{
1029  const int selectionCount = ui.trackersView->selectionModel ()->selectedRows ().size ();
1030  ui.editTrackerButton->setEnabled (selectionCount == 1);
1031  ui.removeTrackerButton->setEnabled (selectionCount > 0);
1032}
1033
1034void
1035Details::onAddTrackerClicked ()
1036{
1037  bool ok = false;
1038  const QString url = QInputDialog::getText (this,
1039                                             tr ("Add URL "),
1040                                             tr ("Add tracker announce URL:"),
1041                                             QLineEdit::Normal, QString (), &ok);
1042  if (!ok)
1043    {
1044      // user pressed "cancel" -- noop
1045    }
1046  else if (!QUrl (url).isValid ())
1047    {
1048      QMessageBox::warning (this, tr ("Error"), tr ("Invalid URL \"%1\"").arg (url));
1049    }
1050  else
1051    {
1052      QSet<int> ids;
1053
1054      foreach (int id, myIds)
1055        if (myTrackerModel->find (id,url) == -1)
1056          ids.insert (id);
1057
1058      if (ids.empty ()) // all the torrents already have this tracker
1059        {
1060          QMessageBox::warning (this, tr ("Error"), tr ("Tracker already exists."));
1061        }
1062        else
1063        {
1064          QStringList urls;
1065          urls << url;
1066          mySession.torrentSet (ids, TR_KEY_trackerAdd, urls);
1067          getNewData ();
1068        }
1069    }
1070}
1071
1072void
1073Details::onEditTrackerClicked ()
1074{
1075  QItemSelectionModel * selectionModel = ui.trackersView->selectionModel ();
1076  QModelIndexList selectedRows = selectionModel->selectedRows ();
1077  assert (selectedRows.size () == 1);
1078  QModelIndex i = selectionModel->currentIndex ();
1079  const TrackerInfo trackerInfo = ui.trackersView->model ()->data (i, TrackerModel::TrackerRole).value<TrackerInfo> ();
1080
1081  bool ok = false;
1082  const QString newval = QInputDialog::getText (this,
1083                                                tr ("Edit URL "),
1084                                                tr ("Edit tracker announce URL:"),
1085                                                QLineEdit::Normal,
1086                                                trackerInfo.st.announce, &ok);
1087
1088  if (!ok)
1089    {
1090      // user pressed "cancel" -- noop
1091    }
1092  else if (!QUrl (newval).isValid ())
1093    {
1094      QMessageBox::warning (this, tr ("Error"), tr ("Invalid URL \"%1\"").arg (newval));
1095    }
1096    else
1097    {
1098      QSet<int> ids;
1099      ids << trackerInfo.torrentId;
1100
1101      const QPair<int,QString> idUrl = qMakePair (trackerInfo.st.id, newval);
1102
1103      mySession.torrentSet (ids, TR_KEY_trackerReplace, idUrl);
1104      getNewData ();
1105    }
1106}
1107
1108void
1109Details::onRemoveTrackerClicked ()
1110{
1111  // make a map of torrentIds to announce URLs to remove
1112  QItemSelectionModel * selectionModel = ui.trackersView->selectionModel ();
1113  QModelIndexList selectedRows = selectionModel->selectedRows ();
1114  QMap<int,int> torrentId_to_trackerIds;
1115  foreach (QModelIndex i, selectedRows)
1116    {
1117      const TrackerInfo inf = ui.trackersView->model ()->data (i, TrackerModel::TrackerRole).value<TrackerInfo> ();
1118      torrentId_to_trackerIds.insertMulti (inf.torrentId, inf.st.id);
1119    }
1120
1121  // batch all of a tracker's torrents into one command
1122  foreach (int id, torrentId_to_trackerIds.uniqueKeys ())
1123    {
1124      QSet<int> ids;
1125      ids << id;
1126      mySession.torrentSet (ids, TR_KEY_trackerRemove, torrentId_to_trackerIds.values (id));
1127    }
1128
1129  selectionModel->clearSelection ();
1130  getNewData ();
1131}
1132
1133void
1134Details::initOptionsTab ()
1135{
1136  const QString speed_K_str = Formatter::unitStr (Formatter::SPEED, Formatter::KB);
1137
1138  ui.singleDownCheck->setText (ui.singleDownCheck->text ().arg(speed_K_str));
1139  ui.singleUpCheck->setText (ui.singleUpCheck->text ().arg(speed_K_str));
1140
1141  ui.singleDownSpin->setProperty (PREF_KEY, TR_KEY_downloadLimit);
1142  ui.singleUpSpin->setProperty (PREF_KEY, TR_KEY_uploadLimit);
1143  ui.ratioSpin->setProperty (PREF_KEY, TR_KEY_seedRatioLimit);
1144  ui.idleSpin->setProperty (PREF_KEY, TR_KEY_seedIdleLimit);
1145  ui.peerLimitSpin->setProperty (PREF_KEY, TR_KEY_peer_limit);
1146
1147  ui.bandwidthPriorityCombo->addItem (tr ("High"),   TR_PRI_HIGH);
1148  ui.bandwidthPriorityCombo->addItem (tr ("Normal"), TR_PRI_NORMAL);
1149  ui.bandwidthPriorityCombo->addItem (tr ("Low"),    TR_PRI_LOW);
1150
1151  ui.ratioCombo->addItem (tr ("Use Global Settings"),      TR_RATIOLIMIT_GLOBAL);
1152  ui.ratioCombo->addItem (tr ("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED);
1153  ui.ratioCombo->addItem (tr ("Stop seeding at ratio:"),   TR_RATIOLIMIT_SINGLE);
1154
1155  ui.idleCombo->addItem (tr ("Use Global Settings"),                 TR_IDLELIMIT_GLOBAL);
1156  ui.idleCombo->addItem (tr ("Seed regardless of activity"),         TR_IDLELIMIT_UNLIMITED);
1157  ui.idleCombo->addItem (tr ("Stop seeding if idle for N minutes:"), TR_IDLELIMIT_SINGLE);
1158
1159  connect (ui.sessionLimitCheck, SIGNAL (clicked (bool)), SLOT (onHonorsSessionLimitsToggled (bool)));
1160  connect (ui.singleDownCheck, SIGNAL (clicked (bool)), SLOT (onDownloadLimitedToggled (bool)));
1161  connect (ui.singleDownSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
1162  connect (ui.singleUpCheck, SIGNAL (clicked (bool)), SLOT (onUploadLimitedToggled (bool)));
1163  connect (ui.singleUpSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
1164  connect (ui.bandwidthPriorityCombo, SIGNAL (currentIndexChanged (int)), SLOT (onBandwidthPriorityChanged (int)));
1165  connect (ui.ratioCombo, SIGNAL (currentIndexChanged (int)), SLOT (onRatioModeChanged (int)));
1166  connect (ui.ratioSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
1167  connect (ui.idleCombo, SIGNAL (currentIndexChanged (int)), SLOT (onIdleModeChanged (int)));
1168  connect (ui.idleSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
1169  connect (ui.peerLimitSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
1170}
1171
1172/***
1173****
1174***/
1175
1176void
1177Details::initTrackerTab ()
1178{
1179  myTrackerModel = new TrackerModel ();
1180  myTrackerFilter = new TrackerModelFilter ();
1181  myTrackerFilter->setSourceModel (myTrackerModel);
1182  myTrackerDelegate = new TrackerDelegate ();
1183
1184  ui.trackersView->setModel (myTrackerFilter);
1185  ui.trackersView->setItemDelegate (myTrackerDelegate);
1186
1187  ui.addTrackerButton->setIcon (getStockIcon ("list-add", QStyle::SP_DialogOpenButton));
1188  ui.editTrackerButton->setIcon (getStockIcon ("document-properties", QStyle::SP_DesktopIcon));
1189  ui.removeTrackerButton->setIcon (getStockIcon ("list-remove", QStyle::SP_TrashIcon));
1190
1191  ui.showTrackerScrapesCheck->setChecked (myPrefs.getBool (Prefs::SHOW_TRACKER_SCRAPES));
1192  ui.showBackupTrackersCheck->setChecked (myPrefs.getBool (Prefs::SHOW_BACKUP_TRACKERS));
1193
1194  connect (ui.trackersView->selectionModel (), SIGNAL (selectionChanged (QItemSelection, QItemSelection)),
1195    SLOT (onTrackerSelectionChanged ()));
1196  connect (ui.addTrackerButton, SIGNAL (clicked ()), SLOT (onAddTrackerClicked ()));
1197  connect (ui.editTrackerButton, SIGNAL (clicked ()), SLOT (onEditTrackerClicked ()));
1198  connect (ui.removeTrackerButton, SIGNAL (clicked ()), SLOT (onRemoveTrackerClicked ()));
1199  connect (ui.showTrackerScrapesCheck, SIGNAL (clicked (bool)), SLOT (onShowTrackerScrapesToggled (bool)));
1200  connect (ui.showBackupTrackersCheck, SIGNAL (clicked (bool)), SLOT (onShowBackupTrackersToggled (bool)));
1201
1202  onTrackerSelectionChanged ();
1203}
1204
1205/***
1206****
1207***/
1208
1209void
1210Details::initPeersTab ()
1211{
1212  QStringList headers;
1213  headers << QString () << tr ("Up") << tr ("Down") << tr ("%") << tr ("Status") << tr ("Address") << tr ("Client");
1214
1215  ui.peersView->setHeaderLabels (headers);
1216  ui.peersView->sortByColumn (COL_ADDRESS, Qt::AscendingOrder);
1217
1218  ui.peersView->setColumnWidth (COL_LOCK, 20);
1219  ui.peersView->setColumnWidth (COL_UP, measureViewItem (ui.peersView, "1024 MiB/s"));
1220  ui.peersView->setColumnWidth (COL_DOWN, measureViewItem (ui.peersView, "1024 MiB/s"));
1221  ui.peersView->setColumnWidth (COL_PERCENT, measureViewItem (ui.peersView, "100%"));
1222  ui.peersView->setColumnWidth (COL_STATUS, measureViewItem (ui.peersView, "ODUK?EXI"));
1223  ui.peersView->setColumnWidth (COL_ADDRESS, measureViewItem (ui.peersView, "888.888.888.888"));
1224}
1225
1226/***
1227****
1228***/
1229
1230void
1231Details::initFilesTab ()
1232{
1233  connect (ui.filesView, SIGNAL (priorityChanged (QSet<int>, int)), SLOT (onFilePriorityChanged (QSet<int>, int)));
1234  connect (ui.filesView, SIGNAL (wantedChanged (QSet<int>, bool)), SLOT (onFileWantedChanged (QSet<int>, bool)));
1235  connect (ui.filesView, SIGNAL (pathEdited (QString, QString)), SLOT (onPathEdited (QString, QString)));
1236  connect (ui.filesView, SIGNAL (openRequested (QString)), SLOT (onOpenRequested (QString)));
1237}
1238
1239void
1240Details::onFilePriorityChanged (const QSet<int>& indices, int priority)
1241{
1242  tr_quark key;
1243
1244  switch (priority)
1245    {
1246      case TR_PRI_LOW:
1247        key = TR_KEY_priority_low;
1248        break;
1249
1250      case TR_PRI_HIGH:
1251        key = TR_KEY_priority_high;
1252        break;
1253
1254      default:
1255        key = TR_KEY_priority_normal;
1256        break;
1257    }
1258
1259  mySession.torrentSet (myIds, key, indices.toList ());
1260  getNewData ();
1261}
1262
1263void
1264Details::onFileWantedChanged (const QSet<int>& indices, bool wanted)
1265{
1266  const tr_quark key = wanted ? TR_KEY_files_wanted : TR_KEY_files_unwanted;
1267  mySession.torrentSet (myIds, key, indices.toList ());
1268  getNewData ();
1269}
1270
1271void
1272Details::onPathEdited (const QString& oldpath, const QString& newname)
1273{
1274  mySession.torrentRenamePath (myIds, oldpath, newname);
1275}
1276
1277void
1278Details::onOpenRequested (const QString& path)
1279{
1280  if (!mySession.isLocal ())
1281    return;
1282
1283  foreach (const int id, myIds)
1284    {
1285      const Torrent * const tor = myModel.getTorrentFromId (id);
1286      if (tor == NULL)
1287        continue;
1288
1289      const QString localFilePath = tor->getPath () + "/" + path;
1290      if (!QFile::exists (localFilePath))
1291        continue;
1292
1293      if (QDesktopServices::openUrl (QUrl::fromLocalFile (localFilePath)))
1294        break;
1295    }
1296}
Note: See TracBrowser for help on using the repository browser.