Merge g363 via qt_peers_directionarrow-25+knots

This commit is contained in:
Luke Dashjr 2025-03-05 03:27:08 +00:00
commit e5d5a5f696
9 changed files with 156 additions and 23 deletions

View File

@ -411,7 +411,7 @@ void BitcoinApplication::initializeResult(bool success, interfaces::BlockAndHead
// Log this only after AppInitMain finishes, as then logging setup is guaranteed complete
qInfo() << "Platform customization:" << platformStyle->getName();
clientModel = new ClientModel(node(), optionsModel);
clientModel = new ClientModel(node(), optionsModel, *platformStyle);
window->setClientModel(clientModel, &tip_info);
// If '-min' option passed, start window minimized (iconified) or minimized to tray

View File

@ -33,7 +33,7 @@
static SteadyClock::time_point g_last_header_tip_update_notification{};
static SteadyClock::time_point g_last_block_tip_update_notification{};
ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QObject *parent) :
ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, const PlatformStyle& platform_style, QObject *parent) :
QObject(parent),
m_node(node),
optionsModel(_optionsModel),
@ -42,7 +42,7 @@ ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QO
cachedBestHeaderHeight = -1;
cachedBestHeaderTime = -1;
peerTableModel = new PeerTableModel(m_node, this);
peerTableModel = new PeerTableModel(m_node, platform_style, this);
m_peer_table_sort_proxy = new PeerTableSortProxy(this);
m_peer_table_sort_proxy->setSourceModel(peerTableModel);

View File

@ -20,6 +20,7 @@ class CBlockIndex;
class OptionsModel;
class PeerTableModel;
class PeerTableSortProxy;
class PlatformStyle;
enum class SynchronizationState;
struct LocalServiceInfo;
@ -58,7 +59,7 @@ class ClientModel : public QObject
Q_OBJECT
public:
explicit ClientModel(interfaces::Node& node, OptionsModel *optionsModel, QObject *parent = nullptr);
explicit ClientModel(interfaces::Node& node, OptionsModel *optionsModel, const PlatformStyle&, QObject *parent = nullptr);
~ClientModel();
void stop();

View File

@ -6,29 +6,144 @@
#include <qt/guiconstants.h>
#include <qt/guiutil.h>
#include <qt/platformstyle.h>
#include <interfaces/node.h>
#include <utility>
#include <QBrush>
#include <QFont>
#include <QFontInfo>
#include <QImage>
#include <QPainter>
#include <QPixmap>
#include <QList>
#include <QTimer>
PeerTableModel::PeerTableModel(interfaces::Node& node, QObject* parent)
PeerTableModel::PeerTableModel(interfaces::Node& node, const PlatformStyle& platform_style, QObject* parent)
: QAbstractTableModel(parent),
m_node(node)
m_node(node),
m_platform_style(platform_style)
{
// set up timer for auto refresh
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &PeerTableModel::refresh);
timer->setInterval(MODEL_UPDATE_DELAY);
DrawIcons();
// load initial data
refresh();
}
PeerTableModel::~PeerTableModel() = default;
void PeerTableModel::DrawIcons()
{
static constexpr auto SIZE = 32;
static constexpr auto ARROW_HEIGHT = SIZE * 2 / 3;
QImage icon_in(SIZE, SIZE, QImage::Format_Alpha8);
icon_in.fill(Qt::transparent);
QImage icon_out(icon_in);
QPainter icon_in_painter(&icon_in);
QPainter icon_out_painter(&icon_out);
// Arrow
auto DrawArrow = [](const int x, QPainter& icon_painter) {
icon_painter.setBrush(Qt::SolidPattern);
QPoint shape[] = {
{x, ARROW_HEIGHT / 2},
{(SIZE-1) - x, 0},
{(SIZE-1) - x, ARROW_HEIGHT-1},
};
icon_painter.drawConvexPolygon(shape, 3);
};
DrawArrow(0, icon_in_painter);
DrawArrow(SIZE-1, icon_out_painter);
{
//: Label on inbound connection icon
const QString label_in = tr("IN");
//: Label on outbound connection icon
const QString label_out = tr("OUT");
QImage scratch(SIZE, SIZE, QImage::Format_Alpha8);
QPainter scratch_painter(&scratch);
QFont font; // NOTE: Application default font
font.setBold(true);
auto CheckSize = [&](const QImage& icon, const QString& text, const bool align_right) {
// Make sure it's at least able to fit (width only)
if (scratch_painter.boundingRect(0, 0, SIZE, SIZE, 0, text).width() > SIZE) {
return false;
}
// Draw text on the scratch image
// NOTE: QImage::fill doesn't like QPainter being active
scratch_painter.setCompositionMode(QPainter::CompositionMode_Source);
scratch_painter.fillRect(0, 0, SIZE, SIZE, Qt::transparent);
scratch_painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
scratch_painter.drawText(0, SIZE, text);
int text_offset_x = 0;
if (align_right) {
// Figure out how far right we can shift it
for (int col = SIZE-1; col >= 0; --col) {
bool any_pixels = false;
for (int row = SIZE-1; row >= 0; --row) {
int opacity = qAlpha(scratch.pixel(col, row));
if (opacity > 0) {
any_pixels = true;
break;
}
}
if (any_pixels) {
text_offset_x = (SIZE-1) - col;
break;
}
}
}
// Check if there's any overlap
for (int row = 0; row < SIZE; ++row) {
for (int col = text_offset_x; col < SIZE; ++col) {
int opacity = qAlpha(icon.pixel(col, row));
if (col >= text_offset_x) {
opacity += qAlpha(scratch.pixel(col - text_offset_x, row));
}
if (opacity > 0xff) {
// Overlap found, we're done
return false;
}
}
}
return true;
};
int font_size = SIZE;
while (font_size > 1) {
font.setPixelSize(--font_size);
scratch_painter.setFont(font);
if (CheckSize(icon_in , label_in , /* align_right= */ false) &&
CheckSize(icon_out, label_out, /* align_right= */ true)) break;
}
icon_in_painter .drawText(0, 0, SIZE, SIZE, Qt::AlignLeft | Qt::AlignBottom, label_in);
icon_out_painter.drawText(0, 0, SIZE, SIZE, Qt::AlignRight | Qt::AlignBottom, label_out);
}
m_icon_conn_in = m_platform_style.TextColorIcon(QIcon(QPixmap::fromImage(icon_in)));
m_icon_conn_out = m_platform_style.TextColorIcon(QIcon(QPixmap::fromImage(icon_out)));
}
void PeerTableModel::updatePalette()
{
m_icon_conn_in = m_platform_style.TextColorIcon(m_icon_conn_in);
m_icon_conn_out = m_platform_style.TextColorIcon(m_icon_conn_out);
if (m_peers_data.empty()) return;
Q_EMIT dataChanged(
createIndex(0, Direction),
createIndex(m_peers_data.size() - 1, Direction),
QVector<int>{Qt::DecorationRole}
);
}
void PeerTableModel::startAutoRefresh()
{
timer->start();
@ -72,11 +187,7 @@ QVariant PeerTableModel::data(const QModelIndex& index, int role) const
case Address:
return QString::fromStdString(rec->nodeStats.m_addr_name);
case Direction:
return QString(rec->nodeStats.fInbound ?
//: An Inbound Connection from a Peer.
tr("Inbound") :
//: An Outbound Connection to a Peer.
tr("Outbound"));
return {};
case ConnectionType:
return GUIUtil::ConnectionTypeToQString(rec->nodeStats.m_conn_type, /*prepend_direction=*/false);
case Network:
@ -95,10 +206,10 @@ QVariant PeerTableModel::data(const QModelIndex& index, int role) const
switch (column) {
case NetNodeId:
case Age:
case Direction:
return QVariant(Qt::AlignRight | Qt::AlignVCenter);
case Address:
return {};
case Direction:
case ConnectionType:
case Network:
return QVariant(Qt::AlignCenter);
@ -112,6 +223,8 @@ QVariant PeerTableModel::data(const QModelIndex& index, int role) const
assert(false);
} else if (role == StatsRole) {
return QVariant::fromValue(rec);
} else if (index.column() == Direction && role == Qt::DecorationRole) {
return rec->nodeStats.fInbound ? m_icon_conn_in : m_icon_conn_out;
}
return QVariant();

View File

@ -9,12 +9,14 @@
#include <net.h>
#include <QAbstractTableModel>
#include <QIcon>
#include <QList>
#include <QModelIndex>
#include <QStringList>
#include <QVariant>
class PeerTablePriv;
class PlatformStyle;
namespace interfaces {
class Node;
@ -40,16 +42,17 @@ class PeerTableModel : public QAbstractTableModel
Q_OBJECT
public:
explicit PeerTableModel(interfaces::Node& node, QObject* parent);
explicit PeerTableModel(interfaces::Node& node, const PlatformStyle&, QObject* parent);
~PeerTableModel();
void startAutoRefresh();
void stopAutoRefresh();
// See also RPCConsole::ColumnWidths in rpcconsole.h
enum ColumnIndex {
NetNodeId = 0,
Age,
Address,
Direction,
Address,
ConnectionType,
Network,
Ping,
@ -74,24 +77,26 @@ public:
public Q_SLOTS:
void refresh();
void updatePalette();
private:
//! Internal peer data structure.
QList<CNodeCombinedStats> m_peers_data{};
interfaces::Node& m_node;
const PlatformStyle& m_platform_style;
void DrawIcons();
QIcon m_icon_conn_in, m_icon_conn_out;
const QStringList columns{
/*: Title of Peers Table column which contains a
unique number used to identify a connection. */
tr("Peer"),
tr("id"),
/*: Title of Peers Table column which indicates the duration (length of time)
since the peer connection started. */
tr("Age"),
"", // Direction column has no title
/*: Title of Peers Table column which contains the
IP/Onion/I2P address of the connected peer. */
tr("Address"),
/*: Title of Peers Table column which indicates the direction
the peer connection was initiated from. */
tr("Direction"),
/*: Title of Peers Table column which describes the type of
peer connection. The "type" describes why the connection exists. */
tr("Type"),
@ -106,7 +111,7 @@ private:
tr("Sent"),
/*: Title of Peers Table column which indicates the total amount of
network information we have received from the peer. */
tr("Received"),
tr("Recv'd"),
/*: Title of Peers Table column which contains the peer's
User Agent string. */
tr("User Agent")};

View File

@ -30,6 +30,7 @@
#include <QAbstractItemModel>
#include <QDateTime>
#include <QFont>
#include <QFontMetrics>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLatin1String>
@ -687,6 +688,7 @@ void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_
connect(model, &ClientModel::mempoolSizeChanged, this, &RPCConsole::setMempoolSize);
// set up peer table
clientModel->getPeerTableModel()->updatePalette();
ui->peerWidget->setModel(model->peerTableSortProxy());
ui->peerWidget->verticalHeader()->hide();
ui->peerWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
@ -694,11 +696,19 @@ void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_
ui->peerWidget->setContextMenuPolicy(Qt::CustomContextMenu);
if (!ui->peerWidget->horizontalHeader()->restoreState(m_peer_widget_header_state)) {
const QFontMetrics fm = ui->peerWidget->fontMetrics();
ui->peerWidget->setColumnWidth(PeerTableModel::NetNodeId, GUIUtil::TextWidth(fm, QStringLiteral("99999")));
ui->peerWidget->setColumnWidth(PeerTableModel::Age, GUIUtil::TextWidth(fm, GUIUtil::FormatPeerAge(std::chrono::hours{23976 /* 999 days */})));
ui->peerWidget->setColumnWidth(PeerTableModel::Direction, DIRECTION_COLUMN_WIDTH);
ui->peerWidget->setColumnWidth(PeerTableModel::Address, ADDRESS_COLUMN_WIDTH);
ui->peerWidget->setColumnWidth(PeerTableModel::ConnectionType, GUIUtil::TextWidth(fm, GUIUtil::ConnectionTypeToQString(ConnectionType::ADDR_FETCH /* TODO: Find the WIDEST string? */, /*prepend_direction=*/false)));
const auto bytesize_width = GUIUtil::TextWidth(fm, GUIUtil::formatBytes(999'000'000'000) + QStringLiteral("x"));
ui->peerWidget->setColumnWidth(PeerTableModel::Network, GUIUtil::TextWidth(fm, qvariant_cast<QString>(model->peerTableSortProxy()->headerData(PeerTableModel::ColumnIndex::Network, Qt::Horizontal, Qt::DisplayRole)) /* TODO: Find the WIDEST string? */ + QStringLiteral("x")));
ui->peerWidget->setColumnWidth(PeerTableModel::Subversion, SUBVERSION_COLUMN_WIDTH);
ui->peerWidget->setColumnWidth(PeerTableModel::Ping, PING_COLUMN_WIDTH);
ui->peerWidget->setColumnWidth(PeerTableModel::Sent, bytesize_width);
ui->peerWidget->setColumnWidth(PeerTableModel::Received, bytesize_width);
}
ui->peerWidget->horizontalHeader()->setSectionResizeMode(PeerTableModel::Age, QHeaderView::ResizeToContents);
ui->peerWidget->horizontalHeader()->setStretchLastSection(true);
ui->peerWidget->setItemDelegateForColumn(PeerTableModel::NetNodeId, new PeerIdViewDelegate(this));
@ -734,7 +744,6 @@ void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_
ui->banlistWidget->setColumnWidth(BanTableModel::Address, BANSUBNET_COLUMN_WIDTH);
ui->banlistWidget->setColumnWidth(BanTableModel::Bantime, BANTIME_COLUMN_WIDTH);
}
ui->banlistWidget->horizontalHeader()->setSectionResizeMode(BanTableModel::Address, QHeaderView::ResizeToContents);
ui->banlistWidget->horizontalHeader()->setStretchLastSection(true);
// create ban table context menu
@ -948,6 +957,10 @@ void RPCConsole::changeEvent(QEvent* e)
QUrl(ICON_MAPPING[i].url),
platformStyle->SingleColorImage(ICON_MAPPING[i].source).scaled(QSize(consoleFontSize * 2, consoleFontSize * 2), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
}
if (clientModel && clientModel->getPeerTableModel()) {
clientModel->getPeerTableModel()->updatePalette();
}
}
QWidget::changeEvent(e);

View File

@ -152,6 +152,7 @@ private:
enum ColumnWidths
{
DIRECTION_COLUMN_WIDTH = 32,
ADDRESS_COLUMN_WIDTH = 200,
SUBVERSION_COLUMN_WIDTH = 150,
PING_COLUMN_WIDTH = 80,

View File

@ -130,7 +130,7 @@ void TestAddAddressesToSendBook(interfaces::Node& node)
OptionsModel optionsModel(node);
bilingual_str error;
QVERIFY(optionsModel.Init(error));
ClientModel clientModel(node, &optionsModel);
ClientModel clientModel(node, &optionsModel, *platformStyle);
WalletContext& context = *node.walletLoader().context();
AddWallet(context, wallet);
WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel, platformStyle.get());

View File

@ -268,7 +268,7 @@ public:
MiniGUI(interfaces::Node& node, const PlatformStyle* platformStyle) : sendCoinsDialog(platformStyle), transactionView(platformStyle), optionsModel(node) {
bilingual_str error;
QVERIFY(optionsModel.Init(error));
clientModel = std::make_unique<ClientModel>(node, &optionsModel);
clientModel = std::make_unique<ClientModel>(node, &optionsModel, *platformStyle);
}
void initModelForWallet(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet, const PlatformStyle* platformStyle)