Enable customizing the save statistics time interval
[qBittorrent.git] / src / gui / properties / peerlistwidget.cpp
blobf7c4d0d5a92806874d96b2863103d3161897b02d
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 "peerlistwidget.h"
32 #include <algorithm>
34 #include <QApplication>
35 #include <QClipboard>
36 #include <QHeaderView>
37 #include <QHostAddress>
38 #include <QList>
39 #include <QMenu>
40 #include <QMessageBox>
41 #include <QPointer>
42 #include <QSet>
43 #include <QShortcut>
44 #include <QSortFilterProxyModel>
45 #include <QStandardItemModel>
46 #include <QWheelEvent>
48 #include "base/bittorrent/peeraddress.h"
49 #include "base/bittorrent/peerinfo.h"
50 #include "base/bittorrent/session.h"
51 #include "base/bittorrent/torrent.h"
52 #include "base/bittorrent/torrentinfo.h"
53 #include "base/global.h"
54 #include "base/logger.h"
55 #include "base/net/geoipmanager.h"
56 #include "base/net/reverseresolution.h"
57 #include "base/preferences.h"
58 #include "base/utils/misc.h"
59 #include "base/utils/string.h"
60 #include "gui/uithememanager.h"
61 #include "peerlistsortmodel.h"
62 #include "peersadditiondialog.h"
63 #include "propertieswidget.h"
65 struct PeerEndpoint
67 BitTorrent::PeerAddress address;
68 QString connectionType; // matches return type of `PeerInfo::connectionType()`
70 friend bool operator==(const PeerEndpoint &left, const PeerEndpoint &right) = default;
73 std::size_t qHash(const PeerEndpoint &peerEndpoint, const std::size_t seed = 0)
75 return qHashMulti(seed, peerEndpoint.address, peerEndpoint.connectionType);
78 namespace
80 void setModelData(QStandardItemModel *model, const int row, const int column, const QString &displayData
81 , const QVariant &underlyingData, const Qt::Alignment textAlignmentData = {}, const QString &toolTip = {})
83 const QMap<int, QVariant> data = {
84 {Qt::DisplayRole, displayData},
85 {PeerListSortModel::UnderlyingDataRole, underlyingData},
86 {Qt::TextAlignmentRole, QVariant {textAlignmentData}},
87 {Qt::ToolTipRole, toolTip}};
89 model->setItemData(model->index(row, column), data);
93 PeerListWidget::PeerListWidget(PropertiesWidget *parent)
94 : QTreeView(parent)
95 , m_properties(parent)
97 // Load settings
98 const bool columnLoaded = loadSettings();
99 // Visual settings
100 setUniformRowHeights(true);
101 setRootIsDecorated(false);
102 setItemsExpandable(false);
103 setAllColumnsShowFocus(true);
104 setEditTriggers(QAbstractItemView::NoEditTriggers);
105 setSelectionMode(QAbstractItemView::ExtendedSelection);
106 header()->setFirstSectionMovable(true);
107 header()->setStretchLastSection(false);
108 header()->setTextElideMode(Qt::ElideRight);
110 // List Model
111 m_listModel = new QStandardItemModel(0, PeerListColumns::COL_COUNT, this);
112 m_listModel->setHeaderData(PeerListColumns::COUNTRY, Qt::Horizontal, tr("Country/Region")); // Country flag column
113 m_listModel->setHeaderData(PeerListColumns::IP, Qt::Horizontal, tr("IP/Address"));
114 m_listModel->setHeaderData(PeerListColumns::PORT, Qt::Horizontal, tr("Port"));
115 m_listModel->setHeaderData(PeerListColumns::FLAGS, Qt::Horizontal, tr("Flags"));
116 m_listModel->setHeaderData(PeerListColumns::CONNECTION, Qt::Horizontal, tr("Connection"));
117 m_listModel->setHeaderData(PeerListColumns::CLIENT, Qt::Horizontal, tr("Client", "i.e.: Client application"));
118 m_listModel->setHeaderData(PeerListColumns::PEERID_CLIENT, Qt::Horizontal, tr("Peer ID Client", "i.e.: Client resolved from Peer ID"));
119 m_listModel->setHeaderData(PeerListColumns::PROGRESS, Qt::Horizontal, tr("Progress", "i.e: % downloaded"));
120 m_listModel->setHeaderData(PeerListColumns::DOWN_SPEED, Qt::Horizontal, tr("Down Speed", "i.e: Download speed"));
121 m_listModel->setHeaderData(PeerListColumns::UP_SPEED, Qt::Horizontal, tr("Up Speed", "i.e: Upload speed"));
122 m_listModel->setHeaderData(PeerListColumns::TOT_DOWN, Qt::Horizontal, tr("Downloaded", "i.e: total data downloaded"));
123 m_listModel->setHeaderData(PeerListColumns::TOT_UP, Qt::Horizontal, tr("Uploaded", "i.e: total data uploaded"));
124 m_listModel->setHeaderData(PeerListColumns::RELEVANCE, Qt::Horizontal, tr("Relevance", "i.e: How relevant this peer is to us. How many pieces it has that we don't."));
125 m_listModel->setHeaderData(PeerListColumns::DOWNLOADING_PIECE, Qt::Horizontal, tr("Files", "i.e. files that are being downloaded right now"));
126 // Set header text alignment
127 m_listModel->setHeaderData(PeerListColumns::PORT, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
128 m_listModel->setHeaderData(PeerListColumns::PROGRESS, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
129 m_listModel->setHeaderData(PeerListColumns::DOWN_SPEED, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
130 m_listModel->setHeaderData(PeerListColumns::UP_SPEED, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
131 m_listModel->setHeaderData(PeerListColumns::TOT_DOWN, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
132 m_listModel->setHeaderData(PeerListColumns::TOT_UP, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
133 m_listModel->setHeaderData(PeerListColumns::RELEVANCE, Qt::Horizontal, QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
134 // Proxy model to support sorting without actually altering the underlying model
135 m_proxyModel = new PeerListSortModel(this);
136 m_proxyModel->setDynamicSortFilter(true);
137 m_proxyModel->setSourceModel(m_listModel);
138 m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
139 setModel(m_proxyModel);
141 hideColumn(PeerListColumns::IP_HIDDEN);
142 hideColumn(PeerListColumns::COL_COUNT);
144 // Default hidden columns
145 if (!columnLoaded)
147 hideColumn(PeerListColumns::PEERID_CLIENT);
150 m_resolveCountries = Preferences::instance()->resolvePeerCountries();
151 if (!m_resolveCountries)
152 hideColumn(PeerListColumns::COUNTRY);
153 // Ensure that at least one column is visible at all times
154 bool atLeastOne = false;
155 for (int i = 0; i < PeerListColumns::IP_HIDDEN; ++i)
157 if (!isColumnHidden(i))
159 atLeastOne = true;
160 break;
163 if (!atLeastOne)
164 setColumnHidden(PeerListColumns::IP, false);
165 // To also mitigate the above issue, we have to resize each column when
166 // its size is 0, because explicitly 'showing' the column isn't enough
167 // in the above scenario.
168 for (int i = 0; i < PeerListColumns::IP_HIDDEN; ++i)
170 if ((columnWidth(i) <= 0) && !isColumnHidden(i))
171 resizeColumnToContents(i);
173 // Context menu
174 setContextMenuPolicy(Qt::CustomContextMenu);
175 connect(this, &QWidget::customContextMenuRequested, this, &PeerListWidget::showPeerListMenu);
176 // Enable sorting
177 setSortingEnabled(true);
178 // IP to Hostname resolver
179 updatePeerHostNameResolutionState();
180 // SIGNAL/SLOT
181 header()->setContextMenuPolicy(Qt::CustomContextMenu);
182 connect(header(), &QWidget::customContextMenuRequested, this, &PeerListWidget::displayColumnHeaderMenu);
183 connect(header(), &QHeaderView::sectionClicked, this, &PeerListWidget::handleSortColumnChanged);
184 connect(header(), &QHeaderView::sectionMoved, this, &PeerListWidget::saveSettings);
185 connect(header(), &QHeaderView::sectionResized, this, &PeerListWidget::saveSettings);
186 connect(header(), &QHeaderView::sortIndicatorChanged, this, &PeerListWidget::saveSettings);
187 handleSortColumnChanged(header()->sortIndicatorSection());
188 const auto *copyHotkey = new QShortcut(QKeySequence::Copy, this, nullptr, nullptr, Qt::WidgetShortcut);
189 connect(copyHotkey, &QShortcut::activated, this, &PeerListWidget::copySelectedPeers);
190 const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
191 connect(deleteHotkey, &QShortcut::activated, this, &PeerListWidget::banSelectedPeers);
194 PeerListWidget::~PeerListWidget()
196 saveSettings();
199 void PeerListWidget::displayColumnHeaderMenu()
201 QMenu *menu = new QMenu(this);
202 menu->setAttribute(Qt::WA_DeleteOnClose);
203 menu->setTitle(tr("Column visibility"));
204 menu->setToolTipsVisible(true);
206 for (int i = 0; i < PeerListColumns::IP_HIDDEN; ++i)
208 if ((i == PeerListColumns::COUNTRY) && !Preferences::instance()->resolvePeerCountries())
209 continue;
211 const auto columnName = m_listModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
212 QAction *action = menu->addAction(columnName, this, [this, i](const bool checked)
214 if (!checked && (visibleColumnsCount() <= 1))
215 return;
217 setColumnHidden(i, !checked);
219 if (checked && (columnWidth(i) <= 5))
220 resizeColumnToContents(i);
222 saveSettings();
224 action->setCheckable(true);
225 action->setChecked(!isColumnHidden(i));
228 menu->addSeparator();
229 QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
231 for (int i = 0, count = header()->count(); i < count; ++i)
233 if (!isColumnHidden(i))
234 resizeColumnToContents(i);
236 saveSettings();
238 resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
240 menu->popup(QCursor::pos());
243 void PeerListWidget::updatePeerHostNameResolutionState()
245 if (Preferences::instance()->resolvePeerHostNames())
247 if (!m_resolver)
249 m_resolver = new Net::ReverseResolution(this);
250 connect(m_resolver, &Net::ReverseResolution::ipResolved, this, &PeerListWidget::handleResolved);
251 loadPeers(m_properties->getCurrentTorrent());
254 else
256 delete m_resolver;
257 m_resolver = nullptr;
261 void PeerListWidget::updatePeerCountryResolutionState()
263 const bool resolveCountries = Preferences::instance()->resolvePeerCountries();
264 if (resolveCountries == m_resolveCountries)
265 return;
267 m_resolveCountries = resolveCountries;
268 if (m_resolveCountries)
270 loadPeers(m_properties->getCurrentTorrent());
271 showColumn(PeerListColumns::COUNTRY);
272 if (columnWidth(PeerListColumns::COUNTRY) <= 0)
273 resizeColumnToContents(PeerListColumns::COUNTRY);
275 else
277 hideColumn(PeerListColumns::COUNTRY);
281 void PeerListWidget::showPeerListMenu()
283 BitTorrent::Torrent *torrent = m_properties->getCurrentTorrent();
284 if (!torrent) return;
286 auto *menu = new QMenu(this);
287 menu->setAttribute(Qt::WA_DeleteOnClose);
288 menu->setToolTipsVisible(true);
290 QAction *addNewPeer = menu->addAction(UIThemeManager::instance()->getIcon(u"peers-add"_s), tr("Add peers...")
291 , this, [this, torrent]()
293 const QList<BitTorrent::PeerAddress> peersList = PeersAdditionDialog::askForPeers(this);
294 const int peerCount = std::count_if(peersList.cbegin(), peersList.cend(), [torrent](const BitTorrent::PeerAddress &peer)
296 return torrent->connectPeer(peer);
298 if (peerCount < peersList.length())
299 QMessageBox::information(this, tr("Adding peers"), tr("Some peers cannot be added. Check the Log for details."));
300 else if (peerCount > 0)
301 QMessageBox::information(this, tr("Adding peers"), tr("Peers are added to this torrent."));
303 QAction *copyPeers = menu->addAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("Copy IP:port")
304 , this, &PeerListWidget::copySelectedPeers);
305 menu->addSeparator();
306 QAction *banPeers = menu->addAction(UIThemeManager::instance()->getIcon(u"peers-remove"_s), tr("Ban peer permanently")
307 , this, &PeerListWidget::banSelectedPeers);
309 // disable actions
310 const auto disableAction = [](QAction *action, const QString &tooltip)
312 action->setEnabled(false);
313 action->setToolTip(tooltip);
316 if (torrent->isPrivate())
317 disableAction(addNewPeer, tr("Cannot add peers to a private torrent"));
318 else if (torrent->isChecking())
319 disableAction(addNewPeer, tr("Cannot add peers when the torrent is checking"));
320 else if (torrent->isQueued())
321 disableAction(addNewPeer, tr("Cannot add peers when the torrent is queued"));
323 if (selectionModel()->selectedRows().isEmpty())
325 const QString tooltip = tr("No peer was selected");
326 disableAction(copyPeers, tooltip);
327 disableAction(banPeers, tooltip);
330 menu->popup(QCursor::pos());
333 void PeerListWidget::banSelectedPeers()
335 // Store selected rows first as selected peers may disconnect
336 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
338 QList<QString> selectedIPs;
339 selectedIPs.reserve(selectedIndexes.size());
341 for (const QModelIndex &index : selectedIndexes)
343 const int row = m_proxyModel->mapToSource(index).row();
344 const QString ip = m_listModel->item(row, PeerListColumns::IP_HIDDEN)->text();
345 selectedIPs += ip;
348 // Confirm before banning peer
349 const QMessageBox::StandardButton btn = QMessageBox::question(this, tr("Ban peer permanently")
350 , tr("Are you sure you want to permanently ban the selected peers?"));
351 if (btn != QMessageBox::Yes) return;
353 for (const QString &ip : selectedIPs)
355 BitTorrent::Session::instance()->banIP(ip);
356 LogMsg(tr("Peer \"%1\" is manually banned").arg(ip));
358 // Refresh list
359 loadPeers(m_properties->getCurrentTorrent());
362 void PeerListWidget::copySelectedPeers()
364 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
365 QStringList selectedPeers;
367 for (const QModelIndex &index : selectedIndexes)
369 const int row = m_proxyModel->mapToSource(index).row();
370 const QString ip = m_listModel->item(row, PeerListColumns::IP_HIDDEN)->text();
371 const QString port = m_listModel->item(row, PeerListColumns::PORT)->text();
373 if (!ip.contains(u'.')) // IPv6
374 selectedPeers << (u'[' + ip + u"]:" + port);
375 else // IPv4
376 selectedPeers << (ip + u':' + port);
379 QApplication::clipboard()->setText(selectedPeers.join(u'\n'));
382 void PeerListWidget::clear()
384 m_peerItems.clear();
385 m_I2PPeerItems.clear();
386 m_itemsByIP.clear();
387 const int nbrows = m_listModel->rowCount();
388 if (nbrows > 0)
389 m_listModel->removeRows(0, nbrows);
392 bool PeerListWidget::loadSettings()
394 return header()->restoreState(Preferences::instance()->getPeerListState());
397 void PeerListWidget::saveSettings() const
399 Preferences::instance()->setPeerListState(header()->saveState());
402 void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
404 if (!torrent)
405 return;
407 using TorrentPtr = QPointer<const BitTorrent::Torrent>;
408 torrent->fetchPeerInfo([this, torrent = TorrentPtr(torrent)](const QList<BitTorrent::PeerInfo> &peers)
410 if (torrent != m_properties->getCurrentTorrent())
411 return;
413 // Remove I2P peers since they will be completely reloaded.
414 for (const QStandardItem *item : asConst(m_I2PPeerItems))
415 m_listModel->removeRow(item->row());
416 m_I2PPeerItems.clear();
418 QSet<PeerEndpoint> existingPeers;
419 existingPeers.reserve(m_peerItems.size());
420 for (auto i = m_peerItems.cbegin(); i != m_peerItems.cend(); ++i)
421 existingPeers.insert(i.key());
423 const bool hideZeroValues = Preferences::instance()->getHideZeroValues();
424 for (const BitTorrent::PeerInfo &peer : peers)
426 const PeerEndpoint peerEndpoint {peer.address(), peer.connectionType()};
428 auto itemIter = m_peerItems.find(peerEndpoint);
429 const bool isNewPeer = (itemIter == m_peerItems.end());
430 const int row = isNewPeer ? m_listModel->rowCount() : (*itemIter)->row();
431 if (isNewPeer)
433 m_listModel->insertRow(row);
435 const bool useI2PSocket = peer.useI2PSocket();
437 const QString peerIPString = useI2PSocket ? peer.I2PAddress() : peerEndpoint.address.ip.toString();
438 setModelData(m_listModel, row, PeerListColumns::IP, peerIPString, peerIPString, {}, peerIPString);
440 const QString peerIPHiddenString = useI2PSocket ? QString() : peerEndpoint.address.ip.toString();
441 setModelData(m_listModel, row, PeerListColumns::IP_HIDDEN, peerIPHiddenString, peerIPHiddenString);
443 const QString peerPortString = useI2PSocket ? tr("N/A") : QString::number(peer.address().port);
444 setModelData(m_listModel, row, PeerListColumns::PORT, peerPortString, peer.address().port, (Qt::AlignRight | Qt::AlignVCenter));
446 if (useI2PSocket)
448 m_I2PPeerItems.append(m_listModel->item(row, PeerListColumns::IP));
450 else
452 itemIter = m_peerItems.insert(peerEndpoint, m_listModel->item(row, PeerListColumns::IP));
453 m_itemsByIP[peerEndpoint.address.ip].insert(itemIter.value());
456 else
458 existingPeers.remove(peerEndpoint);
461 updatePeer(row, torrent, peer, hideZeroValues);
464 // Remove peers that are gone
465 for (const PeerEndpoint &peerEndpoint : asConst(existingPeers))
467 QStandardItem *item = m_peerItems.take(peerEndpoint);
469 const auto items = m_itemsByIP.find(peerEndpoint.address.ip);
470 Q_ASSERT(items != m_itemsByIP.end());
471 if (items == m_itemsByIP.end()) [[unlikely]]
472 continue;
474 items->remove(item);
475 if (items->isEmpty())
476 m_itemsByIP.erase(items);
478 m_listModel->removeRow(item->row());
483 void PeerListWidget::updatePeer(const int row, const BitTorrent::Torrent *torrent, const BitTorrent::PeerInfo &peer, const bool hideZeroValues)
485 const Qt::Alignment intDataTextAlignment = Qt::AlignRight | Qt::AlignVCenter;
487 const QString client = peer.client().toHtmlEscaped();
488 setModelData(m_listModel, row, PeerListColumns::CLIENT, client, client, {}, client);
490 const QString peerIdClient = peer.peerIdClient().toHtmlEscaped();
491 setModelData(m_listModel, row, PeerListColumns::PEERID_CLIENT, peerIdClient, peerIdClient);
493 const QString downSpeed = (hideZeroValues && (peer.payloadDownSpeed() <= 0))
494 ? QString() : Utils::Misc::friendlyUnit(peer.payloadDownSpeed(), true);
495 setModelData(m_listModel, row, PeerListColumns::DOWN_SPEED, downSpeed, peer.payloadDownSpeed(), intDataTextAlignment);
497 const QString upSpeed = (hideZeroValues && (peer.payloadUpSpeed() <= 0))
498 ? QString() : Utils::Misc::friendlyUnit(peer.payloadUpSpeed(), true);
499 setModelData(m_listModel, row, PeerListColumns::UP_SPEED, upSpeed, peer.payloadUpSpeed(), intDataTextAlignment);
501 const QString totalDown = (hideZeroValues && (peer.totalDownload() <= 0))
502 ? QString() : Utils::Misc::friendlyUnit(peer.totalDownload());
503 setModelData(m_listModel, row, PeerListColumns::TOT_DOWN, totalDown, peer.totalDownload(), intDataTextAlignment);
505 const QString totalUp = (hideZeroValues && (peer.totalUpload() <= 0))
506 ? QString() : Utils::Misc::friendlyUnit(peer.totalUpload());
507 setModelData(m_listModel, row, PeerListColumns::TOT_UP, totalUp, peer.totalUpload(), intDataTextAlignment);
509 setModelData(m_listModel, row, PeerListColumns::CONNECTION, peer.connectionType(), peer.connectionType());
510 setModelData(m_listModel, row, PeerListColumns::FLAGS, peer.flags(), peer.flags(), {}, peer.flagsDescription());
511 setModelData(m_listModel, row, PeerListColumns::PROGRESS, (Utils::String::fromDouble(peer.progress() * 100, 1) + u'%')
512 , peer.progress(), intDataTextAlignment);
513 setModelData(m_listModel, row, PeerListColumns::RELEVANCE, (Utils::String::fromDouble(peer.relevance() * 100, 1) + u'%')
514 , peer.relevance(), intDataTextAlignment);
516 const PathList filePaths = torrent->info().filesForPiece(peer.downloadingPieceIndex());
517 QStringList downloadingFiles;
518 downloadingFiles.reserve(filePaths.size());
519 for (const Path &filePath : filePaths)
520 downloadingFiles.append(filePath.toString());
522 const QString downloadingFilesDisplayValue = downloadingFiles.join(u';');
523 setModelData(m_listModel, row, PeerListColumns::DOWNLOADING_PIECE, downloadingFilesDisplayValue
524 , downloadingFilesDisplayValue, {}, downloadingFiles.join(u'\n'));
526 if (!peer.useI2PSocket() && m_resolver)
527 m_resolver->resolve(peer.address().ip);
529 if (m_resolveCountries)
531 const QIcon icon = UIThemeManager::instance()->getFlagIcon(peer.country());
532 if (!icon.isNull())
534 m_listModel->setData(m_listModel->index(row, PeerListColumns::COUNTRY), icon, Qt::DecorationRole);
535 const QString countryName = Net::GeoIPManager::CountryName(peer.country());
536 m_listModel->setData(m_listModel->index(row, PeerListColumns::COUNTRY), countryName, Qt::ToolTipRole);
541 int PeerListWidget::visibleColumnsCount() const
543 int count = 0;
544 for (int i = 0, iMax = header()->count(); i < iMax; ++i)
546 if (!isColumnHidden(i))
547 ++count;
550 return count;
553 void PeerListWidget::handleResolved(const QHostAddress &ip, const QString &hostname) const
555 if (hostname.isEmpty())
556 return;
558 const QSet<QStandardItem *> items = m_itemsByIP.value(ip);
559 for (QStandardItem *item : items)
560 item->setData(hostname, Qt::DisplayRole);
563 void PeerListWidget::handleSortColumnChanged(const int col)
565 if (col == PeerListColumns::COUNTRY)
566 m_proxyModel->setSortRole(Qt::ToolTipRole);
567 else
568 m_proxyModel->setSortRole(PeerListSortModel::UnderlyingDataRole);
571 void PeerListWidget::wheelEvent(QWheelEvent *event)
573 if (event->modifiers() & Qt::ShiftModifier)
575 // Shift + scroll = horizontal scroll
576 event->accept();
577 QWheelEvent scrollHEvent {event->position(), event->globalPosition()
578 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
579 , event->modifiers(), event->phase(), event->inverted(), event->source()};
580 QTreeView::wheelEvent(&scrollHEvent);
581 return;
584 QTreeView::wheelEvent(event); // event delegated to base class