source: trunk/qt/details.cc @ 14525

Last change on this file since 14525 was 14525, checked in by mikedld, 6 years ago

Fix some issues revealed by coverity

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