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"
34 #include <QApplication>
36 #include <QHeaderView>
37 #include <QHostAddress>
40 #include <QMessageBox>
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"
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
);
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
)
95 , m_properties(parent
)
98 const bool columnLoaded
= loadSettings();
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
);
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
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
))
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
);
174 setContextMenuPolicy(Qt::CustomContextMenu
);
175 connect(this, &QWidget::customContextMenuRequested
, this, &PeerListWidget::showPeerListMenu
);
177 setSortingEnabled(true);
178 // IP to Hostname resolver
179 updatePeerHostNameResolutionState();
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()
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())
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))
217 setColumnHidden(i
, !checked
);
219 if (checked
&& (columnWidth(i
) <= 5))
220 resizeColumnToContents(i
);
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
);
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())
249 m_resolver
= new Net::ReverseResolution(this);
250 connect(m_resolver
, &Net::ReverseResolution::ipResolved
, this, &PeerListWidget::handleResolved
);
251 loadPeers(m_properties
->getCurrentTorrent());
257 m_resolver
= nullptr;
261 void PeerListWidget::updatePeerCountryResolutionState()
263 const bool resolveCountries
= Preferences::instance()->resolvePeerCountries();
264 if (resolveCountries
== m_resolveCountries
)
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
);
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
);
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();
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
));
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
);
376 selectedPeers
<< (ip
+ u
':' + port
);
379 QApplication::clipboard()->setText(selectedPeers
.join(u
'\n'));
382 void PeerListWidget::clear()
385 m_I2PPeerItems
.clear();
387 const int nbrows
= m_listModel
->rowCount();
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
)
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())
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();
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
));
448 m_I2PPeerItems
.append(m_listModel
->item(row
, PeerListColumns::IP
));
452 itemIter
= m_peerItems
.insert(peerEndpoint
, m_listModel
->item(row
, PeerListColumns::IP
));
453 m_itemsByIP
[peerEndpoint
.address
.ip
].insert(itemIter
.value());
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
]]
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());
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
544 for (int i
= 0, iMax
= header()->count(); i
< iMax
; ++i
)
546 if (!isColumnHidden(i
))
553 void PeerListWidget::handleResolved(const QHostAddress
&ip
, const QString
&hostname
) const
555 if (hostname
.isEmpty())
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
);
568 m_proxyModel
->setSortRole(PeerListSortModel::UnderlyingDataRole
);
571 void PeerListWidget::wheelEvent(QWheelEvent
*event
)
573 if (event
->modifiers() & Qt::ShiftModifier
)
575 // Shift + scroll = horizontal scroll
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
);
584 QTreeView::wheelEvent(event
); // event delegated to base class