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