Change URL seed error message
[qBittorrent.git] / src / gui / trackerlist / trackerlistwidget.cpp
blob51ef35c9c57a7c2fd7c081a6e694eb5fdbca7617
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "trackerlistwidget.h"
32 #include <QAction>
33 #include <QApplication>
34 #include <QClipboard>
35 #include <QColor>
36 #include <QDebug>
37 #include <QHeaderView>
38 #include <QList>
39 #include <QLocale>
40 #include <QMenu>
41 #include <QMessageBox>
42 #include <QShortcut>
43 #include <QStringList>
44 #include <QTreeWidgetItem>
45 #include <QUrl>
46 #include <QWheelEvent>
48 #include "base/bittorrent/session.h"
49 #include "base/bittorrent/torrent.h"
50 #include "base/bittorrent/trackerentrystatus.h"
51 #include "base/global.h"
52 #include "base/preferences.h"
53 #include "gui/autoexpandabledialog.h"
54 #include "gui/trackersadditiondialog.h"
55 #include "gui/uithememanager.h"
56 #include "trackerlistitemdelegate.h"
57 #include "trackerlistmodel.h"
58 #include "trackerlistsortmodel.h"
60 TrackerListWidget::TrackerListWidget(QWidget *parent)
61 : QTreeView(parent)
63 #ifdef QBT_USES_LIBTORRENT2
64 setColumnHidden(TrackerListModel::COL_PROTOCOL, true); // Must be set before calling loadSettings()
65 #endif
67 setExpandsOnDoubleClick(false);
68 setAllColumnsShowFocus(true);
69 setSelectionMode(QAbstractItemView::ExtendedSelection);
70 setSortingEnabled(true);
71 setUniformRowHeights(true);
72 setContextMenuPolicy(Qt::CustomContextMenu);
74 header()->setSortIndicator(0, Qt::AscendingOrder);
75 header()->setFirstSectionMovable(true);
76 header()->setStretchLastSection(false); // Must be set after loadSettings() in order to work
77 header()->setTextElideMode(Qt::ElideRight);
78 header()->setContextMenuPolicy(Qt::CustomContextMenu);
80 m_model = new TrackerListModel(BitTorrent::Session::instance(), this);
81 auto *sortModel = new TrackerListSortModel(m_model, this);
82 QTreeView::setModel(sortModel);
84 setItemDelegate(new TrackerListItemDelegate(this));
86 loadSettings();
88 // Ensure that at least one column is visible at all times
89 if (visibleColumnsCount() == 0)
90 setColumnHidden(TrackerListModel::COL_URL, false);
91 // To also mitigate the above issue, we have to resize each column when
92 // its size is 0, because explicitly 'showing' the column isn't enough
93 // in the above scenario.
94 for (int i = 0; i < TrackerListModel::COL_COUNT; ++i)
96 if ((columnWidth(i) <= 0) && !isColumnHidden(i))
97 resizeColumnToContents(i);
100 connect(this, &QWidget::customContextMenuRequested, this, &TrackerListWidget::showTrackerListMenu);
101 connect(header(), &QWidget::customContextMenuRequested, this, &TrackerListWidget::displayColumnHeaderMenu);
102 connect(header(), &QHeaderView::sectionMoved, this, &TrackerListWidget::saveSettings);
103 connect(header(), &QHeaderView::sectionResized, this, &TrackerListWidget::saveSettings);
104 connect(header(), &QHeaderView::sortIndicatorChanged, this, &TrackerListWidget::saveSettings);
106 // Set hotkeys
107 const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
108 connect(editHotkey, &QShortcut::activated, this, &TrackerListWidget::editSelectedTracker);
109 const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
110 connect(deleteHotkey, &QShortcut::activated, this, &TrackerListWidget::deleteSelectedTrackers);
111 const auto *copyHotkey = new QShortcut(QKeySequence::Copy, this, nullptr, nullptr, Qt::WidgetShortcut);
112 connect(copyHotkey, &QShortcut::activated, this, &TrackerListWidget::copyTrackerUrl);
114 connect(this, &QAbstractItemView::doubleClicked, this, &TrackerListWidget::editSelectedTracker);
117 TrackerListWidget::~TrackerListWidget()
119 saveSettings();
122 void TrackerListWidget::setTorrent(BitTorrent::Torrent *torrent)
124 m_model->setTorrent(torrent);
127 BitTorrent::Torrent *TrackerListWidget::torrent() const
129 return m_model->torrent();
132 QModelIndexList TrackerListWidget::getSelectedTrackerRows() const
134 QModelIndexList selectedItemIndexes = selectionModel()->selectedRows();
135 selectedItemIndexes.removeIf([](const QModelIndex &index)
137 return (index.parent().isValid() || (index.row() < TrackerListModel::STICKY_ROW_COUNT));
140 return selectedItemIndexes;
143 void TrackerListWidget::decreaseSelectedTrackerTiers()
145 const QModelIndexList trackerIndexes = getSelectedTrackerRows();
146 if (trackerIndexes.isEmpty())
147 return;
149 QSet<QString> trackerURLs;
150 trackerURLs.reserve(trackerIndexes.size());
151 for (const QModelIndex &index : trackerIndexes)
152 trackerURLs.insert(index.siblingAtColumn(TrackerListModel::COL_URL).data().toString());
154 const QList<BitTorrent::TrackerEntryStatus> trackers = m_model->torrent()->trackers();
155 QList<BitTorrent::TrackerEntry> adjustedTrackers;
156 adjustedTrackers.reserve(trackers.size());
158 for (const BitTorrent::TrackerEntryStatus &status : trackers)
160 BitTorrent::TrackerEntry entry
162 .url = status.url,
163 .tier = status.tier
165 if (trackerURLs.contains(entry.url))
167 if (entry.tier > 0)
168 --entry.tier;
170 adjustedTrackers.append(entry);
173 m_model->torrent()->replaceTrackers(adjustedTrackers);
176 void TrackerListWidget::increaseSelectedTrackerTiers()
178 const QModelIndexList trackerIndexes = getSelectedTrackerRows();
179 if (trackerIndexes.isEmpty())
180 return;
182 QSet<QString> trackerURLs;
183 trackerURLs.reserve(trackerIndexes.size());
184 for (const QModelIndex &index : trackerIndexes)
185 trackerURLs.insert(index.siblingAtColumn(TrackerListModel::COL_URL).data().toString());
187 const QList<BitTorrent::TrackerEntryStatus> trackers = m_model->torrent()->trackers();
188 QList<BitTorrent::TrackerEntry> adjustedTrackers;
189 adjustedTrackers.reserve(trackers.size());
191 for (const BitTorrent::TrackerEntryStatus &status : trackers)
193 BitTorrent::TrackerEntry entry
195 .url = status.url,
196 .tier = status.tier
198 if (trackerURLs.contains(entry.url))
200 if (entry.tier < std::numeric_limits<decltype(entry.tier)>::max())
201 ++entry.tier;
203 adjustedTrackers.append(entry);
206 m_model->torrent()->replaceTrackers(adjustedTrackers);
209 void TrackerListWidget::openAddTrackersDialog()
211 if (!torrent())
212 return;
214 auto *dialog = new TrackersAdditionDialog(this, torrent());
215 dialog->setAttribute(Qt::WA_DeleteOnClose);
216 dialog->open();
219 void TrackerListWidget::copyTrackerUrl()
221 if (!torrent())
222 return;
224 const QModelIndexList selectedTrackerIndexes = getSelectedTrackerRows();
225 if (selectedTrackerIndexes.isEmpty())
226 return;
228 QStringList urlsToCopy;
229 for (const QModelIndex &index : selectedTrackerIndexes)
231 const QString &trackerURL = index.siblingAtColumn(TrackerListModel::COL_URL).data().toString();
232 qDebug() << "Copy:" << qUtf8Printable(trackerURL);
233 urlsToCopy.append(trackerURL);
236 QApplication::clipboard()->setText(urlsToCopy.join(u'\n'));
240 void TrackerListWidget::deleteSelectedTrackers()
242 if (!torrent())
243 return;
245 const QModelIndexList selectedTrackerIndexes = getSelectedTrackerRows();
246 if (selectedTrackerIndexes.isEmpty())
247 return;
249 QStringList urlsToRemove;
250 for (const QModelIndex &index : selectedTrackerIndexes)
252 const QString trackerURL = index.siblingAtColumn(TrackerListModel::COL_URL).data().toString();
253 urlsToRemove.append(trackerURL);
256 torrent()->removeTrackers(urlsToRemove);
259 void TrackerListWidget::editSelectedTracker()
261 if (!torrent())
262 return;
264 const QModelIndexList selectedTrackerIndexes = getSelectedTrackerRows();
265 if (selectedTrackerIndexes.isEmpty())
266 return;
268 // During multi-select only process item selected last
269 const QUrl trackerURL = selectedTrackerIndexes.last().siblingAtColumn(TrackerListModel::COL_URL).data().toString();
271 bool ok = false;
272 const QUrl newTrackerURL = AutoExpandableDialog::getText(this
273 , tr("Tracker editing"), tr("Tracker URL:")
274 , QLineEdit::Normal, trackerURL.toString(), &ok).trimmed();
275 if (!ok)
276 return;
278 if (!newTrackerURL.isValid())
280 QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL entered is invalid."));
281 return;
284 if (newTrackerURL == trackerURL)
285 return;
287 const QList<BitTorrent::TrackerEntryStatus> trackers = torrent()->trackers();
288 QList<BitTorrent::TrackerEntry> entries;
289 entries.reserve(trackers.size());
291 bool match = false;
292 for (const BitTorrent::TrackerEntryStatus &status : trackers)
294 const QUrl url {status.url};
296 if (newTrackerURL == url)
298 QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL already exists."));
299 return;
302 BitTorrent::TrackerEntry entry
304 .url = status.url,
305 .tier = status.tier
308 if (!match && (trackerURL == url))
310 match = true;
311 entry.url = newTrackerURL.toString();
313 entries.append(entry);
316 torrent()->replaceTrackers(entries);
319 void TrackerListWidget::reannounceSelected()
321 if (!torrent())
322 return;
324 const auto &selectedItemIndexes = selectedIndexes();
325 if (selectedItemIndexes.isEmpty())
326 return;
328 QSet<QString> trackerURLs;
329 for (const QModelIndex &index : selectedItemIndexes)
331 if (index.parent().isValid())
332 continue;
334 if ((index.row() < TrackerListModel::STICKY_ROW_COUNT))
336 // DHT case
337 if (index.row() == TrackerListModel::ROW_DHT)
338 torrent()->forceDHTAnnounce();
340 continue;
343 trackerURLs.insert(index.siblingAtColumn(TrackerListModel::COL_URL).data().toString());
346 const QList<BitTorrent::TrackerEntryStatus> &trackers = m_model->torrent()->trackers();
347 for (qsizetype i = 0; i < trackers.size(); ++i)
349 const BitTorrent::TrackerEntryStatus &status = trackers.at(i);
350 if (trackerURLs.contains(status.url))
351 torrent()->forceReannounce(i);
355 void TrackerListWidget::showTrackerListMenu()
357 if (!torrent())
358 return;
360 QMenu *menu = new QMenu(this);
361 menu->setAttribute(Qt::WA_DeleteOnClose);
363 // Add actions
364 menu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("Add trackers...")
365 , this, &TrackerListWidget::openAddTrackersDialog);
367 if (!getSelectedTrackerRows().isEmpty())
369 menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s),tr("Edit tracker URL...")
370 , this, &TrackerListWidget::editSelectedTracker);
371 menu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s, u"list-remove"_s), tr("Remove tracker")
372 , this, &TrackerListWidget::deleteSelectedTrackers);
373 menu->addAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("Copy tracker URL")
374 , this, &TrackerListWidget::copyTrackerUrl);
375 if (!torrent()->isStopped())
377 menu->addAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"view-refresh"_s), tr("Force reannounce to selected trackers")
378 , this, &TrackerListWidget::reannounceSelected);
382 if (!torrent()->isStopped())
384 menu->addSeparator();
385 menu->addAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"view-refresh"_s), tr("Force reannounce to all trackers")
386 , this, [this]()
388 torrent()->forceReannounce();
389 torrent()->forceDHTAnnounce();
393 menu->popup(QCursor::pos());
396 void TrackerListWidget::setModel([[maybe_unused]] QAbstractItemModel *model)
398 Q_ASSERT_X(false, Q_FUNC_INFO, "Changing the model of TrackerListWidget is not allowed.");
401 void TrackerListWidget::loadSettings()
403 header()->restoreState(Preferences::instance()->getTrackerListState());
406 void TrackerListWidget::saveSettings() const
408 Preferences::instance()->setTrackerListState(header()->saveState());
411 int TrackerListWidget::visibleColumnsCount() const
413 int count = 0;
414 for (int i = 0, iMax = header()->count(); i < iMax; ++i)
416 if (!isColumnHidden(i))
417 ++count;
420 return count;
423 void TrackerListWidget::displayColumnHeaderMenu()
425 QMenu *menu = new QMenu(this);
426 menu->setAttribute(Qt::WA_DeleteOnClose);
427 menu->setTitle(tr("Column visibility"));
428 menu->setToolTipsVisible(true);
430 for (int i = 0; i < TrackerListModel::COL_COUNT; ++i)
432 QAction *action = menu->addAction(model()->headerData(i, Qt::Horizontal).toString(), this
433 , [this, i](const bool checked)
435 if (!checked && (visibleColumnsCount() <= 1))
436 return;
438 setColumnHidden(i, !checked);
440 if (checked && (columnWidth(i) <= 5))
441 resizeColumnToContents(i);
443 saveSettings();
445 action->setCheckable(true);
446 action->setChecked(!isColumnHidden(i));
449 menu->addSeparator();
450 QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
452 for (int i = 0, count = header()->count(); i < count; ++i)
454 if (!isColumnHidden(i))
455 resizeColumnToContents(i);
457 saveSettings();
459 resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
461 menu->popup(QCursor::pos());
464 void TrackerListWidget::wheelEvent(QWheelEvent *event)
466 if (event->modifiers() & Qt::ShiftModifier)
468 // Shift + scroll = horizontal scroll
469 event->accept();
470 QWheelEvent scrollHEvent {event->position(), event->globalPosition()
471 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
472 , event->modifiers(), event->phase(), event->inverted(), event->source()};
473 QTreeView::wheelEvent(&scrollHEvent);
474 return;
477 QTreeView::wheelEvent(event); // event delegated to base class