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"
33 #include <QApplication>
37 #include <QHeaderView>
41 #include <QMessageBox>
43 #include <QStringList>
44 #include <QTreeWidgetItem>
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
)
63 #ifdef QBT_USES_LIBTORRENT2
64 setColumnHidden(TrackerListModel::COL_PROTOCOL
, true); // Must be set before calling loadSettings()
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));
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
);
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()
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())
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
165 if (trackerURLs
.contains(entry
.url
))
170 adjustedTrackers
.append(entry
);
173 m_model
->torrent()->replaceTrackers(adjustedTrackers
);
176 void TrackerListWidget::increaseSelectedTrackerTiers()
178 const QModelIndexList trackerIndexes
= getSelectedTrackerRows();
179 if (trackerIndexes
.isEmpty())
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
198 if (trackerURLs
.contains(entry
.url
))
200 if (entry
.tier
< std::numeric_limits
<decltype(entry
.tier
)>::max())
203 adjustedTrackers
.append(entry
);
206 m_model
->torrent()->replaceTrackers(adjustedTrackers
);
209 void TrackerListWidget::openAddTrackersDialog()
214 auto *dialog
= new TrackersAdditionDialog(this, torrent());
215 dialog
->setAttribute(Qt::WA_DeleteOnClose
);
219 void TrackerListWidget::copyTrackerUrl()
224 const QModelIndexList selectedTrackerIndexes
= getSelectedTrackerRows();
225 if (selectedTrackerIndexes
.isEmpty())
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()
245 const QModelIndexList selectedTrackerIndexes
= getSelectedTrackerRows();
246 if (selectedTrackerIndexes
.isEmpty())
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()
264 const QModelIndexList selectedTrackerIndexes
= getSelectedTrackerRows();
265 if (selectedTrackerIndexes
.isEmpty())
268 // During multi-select only process item selected last
269 const QUrl trackerURL
= selectedTrackerIndexes
.last().siblingAtColumn(TrackerListModel::COL_URL
).data().toString();
272 const QUrl newTrackerURL
= AutoExpandableDialog::getText(this
273 , tr("Tracker editing"), tr("Tracker URL:")
274 , QLineEdit::Normal
, trackerURL
.toString(), &ok
).trimmed();
278 if (!newTrackerURL
.isValid())
280 QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL entered is invalid."));
284 if (newTrackerURL
== trackerURL
)
287 const QList
<BitTorrent::TrackerEntryStatus
> trackers
= torrent()->trackers();
288 QList
<BitTorrent::TrackerEntry
> entries
;
289 entries
.reserve(trackers
.size());
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."));
302 BitTorrent::TrackerEntry entry
308 if (!match
&& (trackerURL
== url
))
311 entry
.url
= newTrackerURL
.toString();
313 entries
.append(entry
);
316 torrent()->replaceTrackers(entries
);
319 void TrackerListWidget::reannounceSelected()
324 const auto &selectedItemIndexes
= selectedIndexes();
325 if (selectedItemIndexes
.isEmpty())
328 QSet
<QString
> trackerURLs
;
329 for (const QModelIndex
&index
: selectedItemIndexes
)
331 if (index
.parent().isValid())
334 if ((index
.row() < TrackerListModel::STICKY_ROW_COUNT
))
337 if (index
.row() == TrackerListModel::ROW_DHT
)
338 torrent()->forceDHTAnnounce();
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()
360 QMenu
*menu
= new QMenu(this);
361 menu
->setAttribute(Qt::WA_DeleteOnClose
);
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")
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
414 for (int i
= 0, iMax
= header()->count(); i
< iMax
; ++i
)
416 if (!isColumnHidden(i
))
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))
438 setColumnHidden(i
, !checked
);
440 if (checked
&& (columnWidth(i
) <= 5))
441 resizeColumnToContents(i
);
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
);
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
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
);
477 QTreeView::wheelEvent(event
); // event delegated to base class