diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 8cac28400f1..f77bc00eb2b 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -387,7 +387,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); #ifdef ENABLE_WALLET if (WalletModel::isWalletEnabled()) { diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 4327d317875..5c2ae2dbcef 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -30,7 +30,7 @@ static int64_t nLastHeaderTipUpdateNotification = 0; static int64_t nLastBlockTipUpdateNotification = 0; -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), @@ -41,7 +41,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); diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h index 846691c0c02..a38f0e737a9 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -18,6 +18,7 @@ class CBlockIndex; class OptionsModel; class PeerTableModel; class PeerTableSortProxy; +class PlatformStyle; enum class SynchronizationState; namespace interfaces { @@ -49,7 +50,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(); interfaces::Node& node() const { return m_node; } diff --git a/src/qt/peertablemodel.cpp b/src/qt/peertablemodel.cpp index 41c389d9cc6..b03d5423d47 100644 --- a/src/qt/peertablemodel.cpp +++ b/src/qt/peertablemodel.cpp @@ -6,17 +6,25 @@ #include #include +#include #include #include +#include +#include +#include +#include +#include +#include #include #include -PeerTableModel::PeerTableModel(interfaces::Node& node, QObject* parent) : +PeerTableModel::PeerTableModel(interfaces::Node& node, const PlatformStyle& platform_style, QObject* parent) : QAbstractTableModel(parent), m_node(node), + m_platform_style(platform_style), timer(nullptr) { // set up timer for auto refresh @@ -24,6 +32,8 @@ PeerTableModel::PeerTableModel(interfaces::Node& node, QObject* parent) : connect(timer, &QTimer::timeout, this, &PeerTableModel::refresh); timer->setInterval(MODEL_UPDATE_DELAY); + DrawIcons(); + // load initial data refresh(); } @@ -33,6 +43,111 @@ PeerTableModel::~PeerTableModel() // Intentionally left empty } +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{Qt::DecorationRole} + ); +} + void PeerTableModel::startAutoRefresh() { timer->start(); @@ -76,11 +191,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: @@ -99,10 +210,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); @@ -116,6 +227,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(); diff --git a/src/qt/peertablemodel.h b/src/qt/peertablemodel.h index e2515de7754..11d34b7337e 100644 --- a/src/qt/peertablemodel.h +++ b/src/qt/peertablemodel.h @@ -9,12 +9,14 @@ #include #include +#include #include #include #include #include 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,11 +77,15 @@ class PeerTableModel : public QAbstractTableModel public Q_SLOTS: void refresh(); + void updatePalette(); private: //! Internal peer data structure. QList 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. */ @@ -86,12 +93,10 @@ public Q_SLOTS: /*: 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"), diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index eb69fabe89e..8af27358096 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -682,6 +682,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); @@ -689,6 +690,7 @@ void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_ ui->peerWidget->setContextMenuPolicy(Qt::CustomContextMenu); if (!ui->peerWidget->horizontalHeader()->restoreState(m_peer_widget_header_state)) { + ui->peerWidget->setColumnWidth(PeerTableModel::Direction, DIRECTION_COLUMN_WIDTH); ui->peerWidget->setColumnWidth(PeerTableModel::Address, ADDRESS_COLUMN_WIDTH); ui->peerWidget->setColumnWidth(PeerTableModel::Subversion, SUBVERSION_COLUMN_WIDTH); ui->peerWidget->setColumnWidth(PeerTableModel::Ping, PING_COLUMN_WIDTH); @@ -934,6 +936,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); diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h index 528e2bef7d5..e2fd8bbf4b9 100644 --- a/src/qt/rpcconsole.h +++ b/src/qt/rpcconsole.h @@ -144,6 +144,7 @@ public Q_SLOTS: enum ColumnWidths { + DIRECTION_COLUMN_WIDTH = 32, ADDRESS_COLUMN_WIDTH = 200, SUBVERSION_COLUMN_WIDTH = 150, PING_COLUMN_WIDTH = 80, diff --git a/src/qt/test/addressbooktests.cpp b/src/qt/test/addressbooktests.cpp index 66637a5dcfb..1b59cd511c8 100644 --- a/src/qt/test/addressbooktests.cpp +++ b/src/qt/test/addressbooktests.cpp @@ -123,7 +123,7 @@ void TestAddAddressesToSendBook(interfaces::Node& node) // Initialize relevant QT models. std::unique_ptr platformStyle(PlatformStyle::instantiate("other")); OptionsModel optionsModel; - 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()); diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index c4cd0f4cd17..7ea85b41397 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -185,7 +185,7 @@ void TestGUI(interfaces::Node& node) SendCoinsDialog sendCoinsDialog(platformStyle.get()); TransactionView transactionView(platformStyle.get()); OptionsModel optionsModel; - 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());