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