1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2006  Christophe Dumez <chris@qbittorrent.org>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  *
19  * In addition, as a special exception, the copyright holders give permission to
20  * link this program with the OpenSSL project's "OpenSSL" library (or with
21  * modified versions of it that use the same license as the "OpenSSL" library),
22  * and distribute the linked executables. You must obey the GNU General Public
23  * License in all respects for all of the code used other than "OpenSSL".  If you
24  * modify file(s), you may extend this exception to your version of the file(s),
25  * but you are not obligated to do so. If you do not wish to do so, delete this
26  * exception statement from your version.
27  */
28 
29 #include "trackerlistwidget.h"
30 
31 #include <QAction>
32 #include <QApplication>
33 #include <QClipboard>
34 #include <QColor>
35 #include <QDebug>
36 #include <QHeaderView>
37 #include <QMenu>
38 #include <QMessageBox>
39 #include <QShortcut>
40 #include <QStringList>
41 #include <QTableView>
42 #include <QTreeWidgetItem>
43 #include <QUrl>
44 #include <QVector>
45 
46 #include "base/bittorrent/peerinfo.h"
47 #include "base/bittorrent/session.h"
48 #include "base/bittorrent/torrent.h"
49 #include "base/bittorrent/trackerentry.h"
50 #include "base/global.h"
51 #include "base/preferences.h"
52 #include "gui/autoexpandabledialog.h"
53 #include "gui/uithememanager.h"
54 #include "propertieswidget.h"
55 #include "trackersadditiondialog.h"
56 
57 #define NB_STICKY_ITEM 3
58 
TrackerListWidget(PropertiesWidget * properties)59 TrackerListWidget::TrackerListWidget(PropertiesWidget *properties)
60     : QTreeWidget()
61     , m_properties(properties)
62 {
63     // Set header
64     // Must be set before calling loadSettings() otherwise the header is reset on restart
65     setHeaderLabels(headerLabels());
66     // Load settings
67     loadSettings();
68     // Graphical settings
69     setRootIsDecorated(false);
70     setAllColumnsShowFocus(true);
71     setItemsExpandable(false);
72     setSelectionMode(QAbstractItemView::ExtendedSelection);
73     header()->setStretchLastSection(false); // Must be set after loadSettings() in order to work
74     // Ensure that at least one column is visible at all times
75     if (visibleColumnsCount() == 0)
76         setColumnHidden(COL_URL, false);
77     // To also mitigate the above issue, we have to resize each column when
78     // its size is 0, because explicitly 'showing' the column isn't enough
79     // in the above scenario.
80     for (int i = 0; i < COL_COUNT; ++i)
81         if ((columnWidth(i) <= 0) && !isColumnHidden(i))
82             resizeColumnToContents(i);
83     // Context menu
84     setContextMenuPolicy(Qt::CustomContextMenu);
85     connect(this, &QWidget::customContextMenuRequested, this, &TrackerListWidget::showTrackerListMenu);
86     // Header
87     header()->setContextMenuPolicy(Qt::CustomContextMenu);
88     connect(header(), &QWidget::customContextMenuRequested, this, &TrackerListWidget::displayToggleColumnsMenu);
89     connect(header(), &QHeaderView::sectionMoved, this, &TrackerListWidget::saveSettings);
90     connect(header(), &QHeaderView::sectionResized, this, &TrackerListWidget::saveSettings);
91     connect(header(), &QHeaderView::sortIndicatorChanged, this, &TrackerListWidget::saveSettings);
92 
93     // Set DHT, PeX, LSD items
94     m_DHTItem = new QTreeWidgetItem({ "",  "** [DHT] **", "", "0", "", "", "0" });
95     insertTopLevelItem(0, m_DHTItem);
96     setRowColor(0, QColor("grey"));
97     m_PEXItem = new QTreeWidgetItem({ "",  "** [PeX] **", "", "0", "", "", "0" });
98     insertTopLevelItem(1, m_PEXItem);
99     setRowColor(1, QColor("grey"));
100     m_LSDItem = new QTreeWidgetItem({ "",  "** [LSD] **", "", "0", "", "", "0" });
101     insertTopLevelItem(2, m_LSDItem);
102     setRowColor(2, QColor("grey"));
103 
104     // Set static items alignment
105     const Qt::Alignment alignment = (Qt::AlignRight | Qt::AlignVCenter);
106     m_DHTItem->setTextAlignment(COL_PEERS, alignment);
107     m_PEXItem->setTextAlignment(COL_PEERS, alignment);
108     m_LSDItem->setTextAlignment(COL_PEERS, alignment);
109     m_DHTItem->setTextAlignment(COL_SEEDS, alignment);
110     m_PEXItem->setTextAlignment(COL_SEEDS, alignment);
111     m_LSDItem->setTextAlignment(COL_SEEDS, alignment);
112     m_DHTItem->setTextAlignment(COL_LEECHES, alignment);
113     m_PEXItem->setTextAlignment(COL_LEECHES, alignment);
114     m_LSDItem->setTextAlignment(COL_LEECHES, alignment);
115     m_DHTItem->setTextAlignment(COL_DOWNLOADED, alignment);
116     m_PEXItem->setTextAlignment(COL_DOWNLOADED, alignment);
117     m_LSDItem->setTextAlignment(COL_DOWNLOADED, alignment);
118 
119     // Set header alignment
120     headerItem()->setTextAlignment(COL_TIER, alignment);
121     headerItem()->setTextAlignment(COL_PEERS, alignment);
122     headerItem()->setTextAlignment(COL_SEEDS, alignment);
123     headerItem()->setTextAlignment(COL_LEECHES, alignment);
124     headerItem()->setTextAlignment(COL_DOWNLOADED, alignment);
125 
126     // Set hotkeys
127     const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
128     connect(editHotkey, &QShortcut::activated, this, &TrackerListWidget::editSelectedTracker);
129     const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
130     connect(deleteHotkey, &QShortcut::activated, this, &TrackerListWidget::deleteSelectedTrackers);
131     const auto *copyHotkey = new QShortcut(QKeySequence::Copy, this, nullptr, nullptr, Qt::WidgetShortcut);
132     connect(copyHotkey, &QShortcut::activated, this, &TrackerListWidget::copyTrackerUrl);
133 
134     connect(this, &QAbstractItemView::doubleClicked, this, &TrackerListWidget::editSelectedTracker);
135 
136     // This hack fixes reordering of first column with Qt5.
137     // https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
138     QTableView unused;
139     unused.setVerticalHeader(header());
140     header()->setParent(this);
141     unused.setVerticalHeader(new QHeaderView(Qt::Horizontal));
142 }
143 
~TrackerListWidget()144 TrackerListWidget::~TrackerListWidget()
145 {
146     saveSettings();
147 }
148 
getSelectedTrackerItems() const149 QVector<QTreeWidgetItem *> TrackerListWidget::getSelectedTrackerItems() const
150 {
151     const QList<QTreeWidgetItem *> selectedTrackerItems = selectedItems();
152     QVector<QTreeWidgetItem *> selectedTrackers;
153     selectedTrackers.reserve(selectedTrackerItems.size());
154 
155     for (QTreeWidgetItem *item : selectedTrackerItems)
156     {
157         if (indexOfTopLevelItem(item) >= NB_STICKY_ITEM) // Ignore STICKY ITEMS
158             selectedTrackers << item;
159     }
160 
161     return selectedTrackers;
162 }
163 
setRowColor(const int row,const QColor & color)164 void TrackerListWidget::setRowColor(const int row, const QColor &color)
165 {
166     const int nbColumns = columnCount();
167     QTreeWidgetItem *item = topLevelItem(row);
168     for (int i = 0; i < nbColumns; ++i)
169         item->setData(i, Qt::ForegroundRole, color);
170 }
171 
moveSelectionUp()172 void TrackerListWidget::moveSelectionUp()
173 {
174     BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent();
175     if (!torrent)
176     {
177         clear();
178         return;
179     }
180     const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems();
181     if (selectedTrackerItems.isEmpty()) return;
182 
183     bool change = false;
184     for (QTreeWidgetItem *item : selectedTrackerItems)
185     {
186         int index = indexOfTopLevelItem(item);
187         if (index > NB_STICKY_ITEM)
188         {
189             insertTopLevelItem(index - 1, takeTopLevelItem(index));
190             change = true;
191         }
192     }
193     if (!change) return;
194 
195     // Restore selection
196     QItemSelectionModel *selection = selectionModel();
197     for (QTreeWidgetItem *item : selectedTrackerItems)
198         selection->select(indexFromItem(item), (QItemSelectionModel::Rows | QItemSelectionModel::Select));
199 
200     setSelectionModel(selection);
201     // Update torrent trackers
202     QVector<BitTorrent::TrackerEntry> trackers;
203     trackers.reserve(topLevelItemCount());
204     for (int i = NB_STICKY_ITEM; i < topLevelItemCount(); ++i)
205     {
206         const QString trackerURL = topLevelItem(i)->data(COL_URL, Qt::DisplayRole).toString();
207         trackers.append({trackerURL, (i - NB_STICKY_ITEM)});
208     }
209 
210     torrent->replaceTrackers(trackers);
211     // Reannounce
212     if (!torrent->isPaused())
213         torrent->forceReannounce();
214 }
215 
moveSelectionDown()216 void TrackerListWidget::moveSelectionDown()
217 {
218     BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent();
219     if (!torrent)
220     {
221         clear();
222         return;
223     }
224     const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems();
225     if (selectedTrackerItems.isEmpty()) return;
226 
227     bool change = false;
228     for (int i = selectedItems().size() - 1; i >= 0; --i)
229     {
230         int index = indexOfTopLevelItem(selectedTrackerItems.at(i));
231         if (index < (topLevelItemCount() - 1))
232         {
233             insertTopLevelItem(index + 1, takeTopLevelItem(index));
234             change = true;
235         }
236     }
237     if (!change) return;
238 
239     // Restore selection
240     QItemSelectionModel *selection = selectionModel();
241     for (QTreeWidgetItem *item : selectedTrackerItems)
242         selection->select(indexFromItem(item), (QItemSelectionModel::Rows | QItemSelectionModel::Select));
243 
244     setSelectionModel(selection);
245     // Update torrent trackers
246     QVector<BitTorrent::TrackerEntry> trackers;
247     trackers.reserve(topLevelItemCount());
248     for (int i = NB_STICKY_ITEM; i < topLevelItemCount(); ++i)
249     {
250         const QString trackerURL = topLevelItem(i)->data(COL_URL, Qt::DisplayRole).toString();
251         trackers.append({trackerURL, (i - NB_STICKY_ITEM)});
252     }
253 
254     torrent->replaceTrackers(trackers);
255     // Reannounce
256     if (!torrent->isPaused())
257         torrent->forceReannounce();
258 }
259 
clear()260 void TrackerListWidget::clear()
261 {
262     qDeleteAll(m_trackerItems);
263     m_trackerItems.clear();
264 
265     m_DHTItem->setText(COL_STATUS, "");
266     m_DHTItem->setText(COL_SEEDS, "");
267     m_DHTItem->setText(COL_LEECHES, "");
268     m_DHTItem->setText(COL_MSG, "");
269     m_PEXItem->setText(COL_STATUS, "");
270     m_PEXItem->setText(COL_SEEDS, "");
271     m_PEXItem->setText(COL_LEECHES, "");
272     m_PEXItem->setText(COL_MSG, "");
273     m_LSDItem->setText(COL_STATUS, "");
274     m_LSDItem->setText(COL_SEEDS, "");
275     m_LSDItem->setText(COL_LEECHES, "");
276     m_LSDItem->setText(COL_MSG, "");
277 }
278 
loadStickyItems(const BitTorrent::Torrent * torrent)279 void TrackerListWidget::loadStickyItems(const BitTorrent::Torrent *torrent)
280 {
281     const QString working {tr("Working")};
282     const QString disabled {tr("Disabled")};
283     const QString torrentDisabled {tr("Disabled for this torrent")};
284     const auto *session = BitTorrent::Session::instance();
285 
286     // load DHT information
287     if (torrent->isPrivate() || torrent->isDHTDisabled())
288         m_DHTItem->setText(COL_STATUS, torrentDisabled);
289     else if (!session->isDHTEnabled())
290         m_DHTItem->setText(COL_STATUS, disabled);
291     else
292         m_DHTItem->setText(COL_STATUS, working);
293 
294     // Load PeX Information
295     if (torrent->isPrivate() || torrent->isPEXDisabled())
296         m_PEXItem->setText(COL_STATUS, torrentDisabled);
297     else if (!session->isPeXEnabled())
298         m_PEXItem->setText(COL_STATUS, disabled);
299     else
300         m_PEXItem->setText(COL_STATUS, working);
301 
302     // Load LSD Information
303     if (torrent->isPrivate() || torrent->isLSDDisabled())
304         m_LSDItem->setText(COL_STATUS, torrentDisabled);
305     else if (!session->isLSDEnabled())
306         m_LSDItem->setText(COL_STATUS, disabled);
307     else
308         m_LSDItem->setText(COL_STATUS, working);
309 
310     if (torrent->isPrivate())
311     {
312         QString privateMsg = tr("This torrent is private");
313         m_DHTItem->setText(COL_MSG, privateMsg);
314         m_PEXItem->setText(COL_MSG, privateMsg);
315         m_LSDItem->setText(COL_MSG, privateMsg);
316     }
317 
318     // XXX: libtorrent should provide this info...
319     // Count peers from DHT, PeX, LSD
320     uint seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, peersDHT = 0, peersPeX = 0, peersLSD = 0;
321     for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers()))
322     {
323         if (peer.isConnecting()) continue;
324 
325         if (peer.fromDHT())
326         {
327             if (peer.isSeed())
328                 ++seedsDHT;
329             else
330                 ++peersDHT;
331         }
332         if (peer.fromPeX())
333         {
334             if (peer.isSeed())
335                 ++seedsPeX;
336             else
337                 ++peersPeX;
338         }
339         if (peer.fromLSD())
340         {
341             if (peer.isSeed())
342                 ++seedsLSD;
343             else
344                 ++peersLSD;
345         }
346     }
347 
348     m_DHTItem->setText(COL_SEEDS, QString::number(seedsDHT));
349     m_DHTItem->setText(COL_LEECHES, QString::number(peersDHT));
350     m_PEXItem->setText(COL_SEEDS, QString::number(seedsPeX));
351     m_PEXItem->setText(COL_LEECHES, QString::number(peersPeX));
352     m_LSDItem->setText(COL_SEEDS, QString::number(seedsLSD));
353     m_LSDItem->setText(COL_LEECHES, QString::number(peersLSD));
354 }
355 
loadTrackers()356 void TrackerListWidget::loadTrackers()
357 {
358     // Load trackers from torrent handle
359     const BitTorrent::Torrent *torrent = m_properties->getCurrentTorrent();
360     if (!torrent) return;
361 
362     loadStickyItems(torrent);
363 
364     // Load actual trackers information
365     QStringList oldTrackerURLs = m_trackerItems.keys();
366 
367     for (const BitTorrent::TrackerEntry &entry : asConst(torrent->trackers()))
368     {
369         const QString trackerURL = entry.url;
370 
371         QTreeWidgetItem *item = m_trackerItems.value(trackerURL, nullptr);
372         if (!item)
373         {
374             item = new QTreeWidgetItem();
375             item->setText(COL_URL, trackerURL);
376             addTopLevelItem(item);
377             m_trackerItems[trackerURL] = item;
378         }
379         else
380         {
381             oldTrackerURLs.removeOne(trackerURL);
382         }
383 
384         item->setText(COL_TIER, QString::number(entry.tier));
385 
386         switch (entry.status)
387         {
388         case BitTorrent::TrackerEntry::Working:
389             item->setText(COL_STATUS, tr("Working"));
390             break;
391         case BitTorrent::TrackerEntry::Updating:
392             item->setText(COL_STATUS, tr("Updating..."));
393             break;
394         case BitTorrent::TrackerEntry::NotWorking:
395             item->setText(COL_STATUS, tr("Not working"));
396             break;
397         case BitTorrent::TrackerEntry::NotContacted:
398             item->setText(COL_STATUS, tr("Not contacted yet"));
399             break;
400         }
401 
402         item->setText(COL_MSG, entry.message);
403         item->setText(COL_PEERS, ((entry.numPeers > -1)
404             ? QString::number(entry.numPeers)
405             : tr("N/A")));
406         item->setText(COL_SEEDS, ((entry.numSeeds > -1)
407             ? QString::number(entry.numSeeds)
408             : tr("N/A")));
409         item->setText(COL_LEECHES, ((entry.numLeeches > -1)
410             ? QString::number(entry.numLeeches)
411             : tr("N/A")));
412         item->setText(COL_DOWNLOADED, ((entry.numDownloaded > -1)
413             ? QString::number(entry.numDownloaded)
414             : tr("N/A")));
415 
416         const Qt::Alignment alignment = (Qt::AlignRight | Qt::AlignVCenter);
417         item->setTextAlignment(COL_TIER, alignment);
418         item->setTextAlignment(COL_PEERS, alignment);
419         item->setTextAlignment(COL_SEEDS, alignment);
420         item->setTextAlignment(COL_LEECHES, alignment);
421         item->setTextAlignment(COL_DOWNLOADED, alignment);
422     }
423 
424     // Remove old trackers
425     for (const QString &tracker : asConst(oldTrackerURLs))
426         delete m_trackerItems.take(tracker);
427 }
428 
429 // Ask the user for new trackers and add them to the torrent
askForTrackers()430 void TrackerListWidget::askForTrackers()
431 {
432     BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent();
433     if (!torrent) return;
434 
435     QVector<BitTorrent::TrackerEntry> trackers;
436     for (const QString &tracker : asConst(TrackersAdditionDialog::askForTrackers(this, torrent)))
437         trackers.append({tracker});
438 
439     torrent->addTrackers(trackers);
440 }
441 
copyTrackerUrl()442 void TrackerListWidget::copyTrackerUrl()
443 {
444     const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems();
445     if (selectedTrackerItems.isEmpty()) return;
446 
447     QStringList urlsToCopy;
448     for (const QTreeWidgetItem *item : selectedTrackerItems)
449     {
450         QString trackerURL = item->data(COL_URL, Qt::DisplayRole).toString();
451         qDebug() << QString("Copy: ") + trackerURL;
452         urlsToCopy << trackerURL;
453     }
454     QApplication::clipboard()->setText(urlsToCopy.join('\n'));
455 }
456 
457 
deleteSelectedTrackers()458 void TrackerListWidget::deleteSelectedTrackers()
459 {
460     BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent();
461     if (!torrent)
462     {
463         clear();
464         return;
465     }
466 
467     const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems();
468     if (selectedTrackerItems.isEmpty()) return;
469 
470     QStringList urlsToRemove;
471     for (const QTreeWidgetItem *item : selectedTrackerItems)
472     {
473         QString trackerURL = item->data(COL_URL, Qt::DisplayRole).toString();
474         urlsToRemove << trackerURL;
475         m_trackerItems.remove(trackerURL);
476         delete item;
477     }
478 
479     // Iterate over the trackers and remove the selected ones
480     const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
481     QVector<BitTorrent::TrackerEntry> remainingTrackers;
482     remainingTrackers.reserve(trackers.size());
483 
484     for (const BitTorrent::TrackerEntry &entry : trackers)
485     {
486         if (!urlsToRemove.contains(entry.url))
487             remainingTrackers.push_back(entry);
488     }
489 
490     torrent->replaceTrackers(remainingTrackers);
491 
492     if (!torrent->isPaused())
493         torrent->forceReannounce();
494 }
495 
editSelectedTracker()496 void TrackerListWidget::editSelectedTracker()
497 {
498     BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent();
499     if (!torrent) return;
500 
501     const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems();
502     if (selectedTrackerItems.isEmpty()) return;
503 
504     // During multi-select only process item selected last
505     const QUrl trackerURL = selectedTrackerItems.last()->text(COL_URL);
506 
507     bool ok = false;
508     const QUrl newTrackerURL = AutoExpandableDialog::getText(this, tr("Tracker editing"), tr("Tracker URL:"),
509                                                          QLineEdit::Normal, trackerURL.toString(), &ok).trimmed();
510     if (!ok) return;
511 
512     if (!newTrackerURL.isValid())
513     {
514         QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL entered is invalid."));
515         return;
516     }
517     if (newTrackerURL == trackerURL) return;
518 
519     QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
520     bool match = false;
521     for (BitTorrent::TrackerEntry &entry : trackers)
522     {
523         if (newTrackerURL == QUrl(entry.url))
524         {
525             QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL already exists."));
526             return;
527         }
528 
529         if (!match && (trackerURL == QUrl(entry.url)))
530         {
531             match = true;
532             entry.url = newTrackerURL.toString();
533         }
534     }
535 
536     torrent->replaceTrackers(trackers);
537 
538     if (!torrent->isPaused())
539         torrent->forceReannounce();
540 }
541 
reannounceSelected()542 void TrackerListWidget::reannounceSelected()
543 {
544     const QList<QTreeWidgetItem *> selItems = selectedItems();
545     if (selItems.isEmpty()) return;
546 
547     BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent();
548     if (!torrent) return;
549 
550     const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers();
551 
552     for (const QTreeWidgetItem *item : selItems)
553     {
554         // DHT case
555         if (item == m_DHTItem)
556         {
557             torrent->forceDHTAnnounce();
558             continue;
559         }
560 
561         // Trackers case
562         for (int i = 0; i < trackers.size(); ++i)
563         {
564             if (item->text(COL_URL) == trackers[i].url)
565             {
566                 torrent->forceReannounce(i);
567                 break;
568             }
569         }
570     }
571 
572     loadTrackers();
573 }
574 
showTrackerListMenu(const QPoint &)575 void TrackerListWidget::showTrackerListMenu(const QPoint &)
576 {
577     BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent();
578     if (!torrent) return;
579 
580     QMenu *menu = new QMenu(this);
581     menu->setAttribute(Qt::WA_DeleteOnClose);
582 
583     // Add actions
584     menu->addAction(UIThemeManager::instance()->getIcon("list-add"), tr("Add a new tracker...")
585         , this, &TrackerListWidget::askForTrackers);
586 
587     if (!getSelectedTrackerItems().isEmpty())
588     {
589         menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"),tr("Edit tracker URL...")
590             , this, &TrackerListWidget::editSelectedTracker);
591         menu->addAction(UIThemeManager::instance()->getIcon("list-remove"), tr("Remove tracker")
592             , this, &TrackerListWidget::deleteSelectedTrackers);
593         menu->addAction(UIThemeManager::instance()->getIcon("edit-copy"), tr("Copy tracker URL")
594             , this, &TrackerListWidget::copyTrackerUrl);
595     }
596 
597     if (!torrent->isPaused())
598     {
599         menu->addAction(UIThemeManager::instance()->getIcon("view-refresh"), tr("Force reannounce to selected trackers")
600             , this, &TrackerListWidget::reannounceSelected);
601         menu->addSeparator();
602         menu->addAction(UIThemeManager::instance()->getIcon("view-refresh"), tr("Force reannounce to all trackers")
603             , this, [this]()
604         {
605             BitTorrent::Torrent *h = m_properties->getCurrentTorrent();
606             h->forceReannounce();
607             h->forceDHTAnnounce();
608         });
609     }
610 
611     menu->popup(QCursor::pos());
612 }
613 
loadSettings()614 void TrackerListWidget::loadSettings()
615 {
616     header()->restoreState(Preferences::instance()->getPropTrackerListState());
617 }
618 
saveSettings() const619 void TrackerListWidget::saveSettings() const
620 {
621     Preferences::instance()->setPropTrackerListState(header()->saveState());
622 }
623 
headerLabels()624 QStringList TrackerListWidget::headerLabels()
625 {
626     return
627     {
628         tr("Tier")
629         , tr("URL")
630         , tr("Status")
631         , tr("Peers")
632         , tr("Seeds")
633         , tr("Leeches")
634         , tr("Downloaded")
635         , tr("Message")
636     };
637 }
638 
visibleColumnsCount() const639 int TrackerListWidget::visibleColumnsCount() const
640 {
641     int visibleCols = 0;
642     for (int i = 0; i < COL_COUNT; ++i)
643     {
644         if (!isColumnHidden(i))
645             ++visibleCols;
646     }
647 
648     return visibleCols;
649 }
650 
displayToggleColumnsMenu(const QPoint &)651 void TrackerListWidget::displayToggleColumnsMenu(const QPoint &)
652 {
653     QMenu *menu = new QMenu(this);
654     menu->setAttribute(Qt::WA_DeleteOnClose);
655     menu->setTitle(tr("Column visibility"));
656 
657     for (int i = 0; i < COL_COUNT; ++i)
658     {
659         QAction *myAct = menu->addAction(headerLabels().at(i));
660         myAct->setCheckable(true);
661         myAct->setChecked(!isColumnHidden(i));
662         myAct->setData(i);
663     }
664 
665     connect(menu, &QMenu::triggered, this, [this](const QAction *action)
666     {
667         const int col = action->data().toInt();
668         Q_ASSERT(visibleColumnsCount() > 0);
669 
670         if (!isColumnHidden(col) && (visibleColumnsCount() == 1))
671             return;
672 
673         setColumnHidden(col, !isColumnHidden(col));
674 
675         if (!isColumnHidden(col) && (columnWidth(col) <= 5))
676             resizeColumnToContents(col);
677 
678         saveSettings();
679     });
680 
681     menu->popup(QCursor::pos());
682 }
683