peertablemodel.cpp raw
1 // Copyright (c) 2011-2022 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <qt/peertablemodel.h>
6
7 #include <qt/guiconstants.h>
8 #include <qt/guiutil.h>
9 #include <qt/platformstyle.h>
10
11 #include <interfaces/node.h>
12
13 #include <utility>
14
15 #include <QBrush>
16 #include <QFont>
17 #include <QFontInfo>
18 #include <QImage>
19 #include <QPainter>
20 #include <QPixmap>
21 #include <QList>
22 #include <QTimer>
23
24 PeerTableModel::PeerTableModel(interfaces::Node& node, const PlatformStyle& platform_style, QObject* parent)
25 : QAbstractTableModel(parent),
26 m_node(node),
27 m_platform_style(platform_style)
28 {
29 // set up timer for auto refresh
30 timer = new QTimer(this);
31 connect(timer, &QTimer::timeout, this, &PeerTableModel::refresh);
32 timer->setInterval(MODEL_UPDATE_DELAY);
33
34 DrawIcons();
35
36 // load initial data
37 refresh();
38 }
39
40 PeerTableModel::~PeerTableModel() = default;
41
42 void PeerTableModel::DrawIcons()
43 {
44 static constexpr auto SIZE = 32;
45 static constexpr auto ARROW_HEIGHT = SIZE * 2 / 3;
46 QImage icon_in(SIZE, SIZE, QImage::Format_Alpha8);
47 icon_in.fill(Qt::transparent);
48 QImage icon_out(icon_in);
49 QPainter icon_in_painter(&icon_in);
50 QPainter icon_out_painter(&icon_out);
51
52 // Arrow
53 auto DrawArrow = [](const int x, QPainter& icon_painter) {
54 icon_painter.setBrush(Qt::SolidPattern);
55 QPoint shape[] = {
56 {x, ARROW_HEIGHT / 2},
57 {(SIZE-1) - x, 0},
58 {(SIZE-1) - x, ARROW_HEIGHT-1},
59 };
60 icon_painter.drawConvexPolygon(shape, 3);
61 };
62 DrawArrow(0, icon_in_painter);
63 DrawArrow(SIZE-1, icon_out_painter);
64
65 {
66 //: Label on inbound connection icon
67 const QString label_in = tr("IN");
68 //: Label on outbound connection icon
69 const QString label_out = tr("OUT");
70 QImage scratch(SIZE, SIZE, QImage::Format_Alpha8);
71 QPainter scratch_painter(&scratch);
72 QFont font; // NOTE: Application default font
73 font.setBold(true);
74 auto CheckSize = [&](const QImage& icon, const QString& text, const bool align_right) {
75 // Make sure it's at least able to fit (width only)
76 if (scratch_painter.boundingRect(0, 0, SIZE, SIZE, 0, text).width() > SIZE) {
77 return false;
78 }
79
80 // Draw text on the scratch image
81 // NOTE: QImage::fill doesn't like QPainter being active
82 scratch_painter.setCompositionMode(QPainter::CompositionMode_Source);
83 scratch_painter.fillRect(0, 0, SIZE, SIZE, Qt::transparent);
84 scratch_painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
85 scratch_painter.drawText(0, SIZE, text);
86
87 int text_offset_x = 0;
88 if (align_right) {
89 // Figure out how far right we can shift it
90 for (int col = SIZE-1; col >= 0; --col) {
91 bool any_pixels = false;
92 for (int row = SIZE-1; row >= 0; --row) {
93 int opacity = qAlpha(scratch.pixel(col, row));
94 if (opacity > 0) {
95 any_pixels = true;
96 break;
97 }
98 }
99 if (any_pixels) {
100 text_offset_x = (SIZE-1) - col;
101 break;
102 }
103 }
104 }
105
106 // Check if there's any overlap
107 for (int row = 0; row < SIZE; ++row) {
108 for (int col = text_offset_x; col < SIZE; ++col) {
109 int opacity = qAlpha(icon.pixel(col, row));
110 if (col >= text_offset_x) {
111 opacity += qAlpha(scratch.pixel(col - text_offset_x, row));
112 }
113 if (opacity > 0xff) {
114 // Overlap found, we're done
115 return false;
116 }
117 }
118 }
119 return true;
120 };
121 int font_size = SIZE;
122 while (font_size > 1) {
123 font.setPixelSize(--font_size);
124 scratch_painter.setFont(font);
125 if (CheckSize(icon_in , label_in , /* align_right= */ false) &&
126 CheckSize(icon_out, label_out, /* align_right= */ true)) break;
127 }
128 icon_in_painter .drawText(0, 0, SIZE, SIZE, Qt::AlignLeft | Qt::AlignBottom, label_in);
129 icon_out_painter.drawText(0, 0, SIZE, SIZE, Qt::AlignRight | Qt::AlignBottom, label_out);
130 }
131 m_icon_conn_in = m_platform_style.TextColorIcon(QIcon(QPixmap::fromImage(icon_in)));
132 m_icon_conn_out = m_platform_style.TextColorIcon(QIcon(QPixmap::fromImage(icon_out)));
133 }
134
135 void PeerTableModel::updatePalette()
136 {
137 m_icon_conn_in = m_platform_style.TextColorIcon(m_icon_conn_in);
138 m_icon_conn_out = m_platform_style.TextColorIcon(m_icon_conn_out);
139 if (m_peers_data.empty()) return;
140 Q_EMIT dataChanged(
141 createIndex(0, Direction),
142 createIndex(m_peers_data.size() - 1, Direction),
143 QVector<int>{Qt::DecorationRole}
144 );
145 }
146
147 void PeerTableModel::startAutoRefresh()
148 {
149 timer->start();
150 }
151
152 void PeerTableModel::stopAutoRefresh()
153 {
154 timer->stop();
155 }
156
157 int PeerTableModel::rowCount(const QModelIndex& parent) const
158 {
159 if (parent.isValid()) {
160 return 0;
161 }
162 return m_peers_data.size();
163 }
164
165 int PeerTableModel::columnCount(const QModelIndex& parent) const
166 {
167 if (parent.isValid()) {
168 return 0;
169 }
170 return columns.length();
171 }
172
173 QVariant PeerTableModel::data(const QModelIndex& index, int role) const
174 {
175 if(!index.isValid())
176 return QVariant();
177
178 CNodeCombinedStats *rec = static_cast<CNodeCombinedStats*>(index.internalPointer());
179
180 const auto column = static_cast<ColumnIndex>(index.column());
181 if (role == Qt::DisplayRole) {
182 switch (column) {
183 case NetNodeId:
184 return (qint64)rec->nodeStats.nodeid;
185 case Age:
186 return GUIUtil::FormatPeerAge(rec->nodeStats.m_connected);
187 case Address:
188 return QString::fromStdString(rec->nodeStats.m_addr_name);
189 case Direction:
190 return {};
191 case ConnectionType:
192 return GUIUtil::ConnectionTypeToQString(rec->nodeStats.m_conn_type, /*prepend_direction=*/false);
193 case Network:
194 return GUIUtil::NetworkToQString(rec->nodeStats.m_network);
195 case Ping:
196 return GUIUtil::formatPingTime(rec->nodeStats.m_min_ping_time);
197 case Sent:
198 return GUIUtil::formatBytes(rec->nodeStats.nSendBytes);
199 case Received:
200 return GUIUtil::formatBytes(rec->nodeStats.nRecvBytes);
201 case Subversion:
202 return QString::fromStdString(rec->nodeStats.cleanSubVer);
203 } // no default case, so the compiler can warn about missing cases
204 assert(false);
205 } else if (role == Qt::TextAlignmentRole) {
206 switch (column) {
207 case NetNodeId:
208 case Age:
209 case Direction:
210 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
211 case Address:
212 return {};
213 case ConnectionType:
214 case Network:
215 return QVariant(Qt::AlignCenter);
216 case Ping:
217 case Sent:
218 case Received:
219 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
220 case Subversion:
221 return {};
222 } // no default case, so the compiler can warn about missing cases
223 assert(false);
224 } else if (role == StatsRole) {
225 return QVariant::fromValue(rec);
226 } else if (index.column() == Direction && role == Qt::DecorationRole) {
227 return rec->nodeStats.fInbound ? m_icon_conn_in : m_icon_conn_out;
228 }
229
230 return QVariant();
231 }
232
233 QVariant PeerTableModel::headerData(int section, Qt::Orientation orientation, int role) const
234 {
235 if(orientation == Qt::Horizontal)
236 {
237 if(role == Qt::DisplayRole && section < columns.size())
238 {
239 return columns[section];
240 }
241 }
242 return QVariant();
243 }
244
245 Qt::ItemFlags PeerTableModel::flags(const QModelIndex &index) const
246 {
247 if (!index.isValid()) return Qt::NoItemFlags;
248
249 Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
250 return retval;
251 }
252
253 QModelIndex PeerTableModel::index(int row, int column, const QModelIndex& parent) const
254 {
255 Q_UNUSED(parent);
256
257 if (0 <= row && row < rowCount() && 0 <= column && column < columnCount()) {
258 return createIndex(row, column, const_cast<CNodeCombinedStats*>(&m_peers_data[row]));
259 }
260
261 return QModelIndex();
262 }
263
264 void PeerTableModel::refresh()
265 {
266 interfaces::Node::NodesStats nodes_stats;
267 m_node.getNodesStats(nodes_stats);
268 decltype(m_peers_data) new_peers_data;
269 new_peers_data.reserve(nodes_stats.size());
270 for (const auto& node_stats : nodes_stats) {
271 const CNodeCombinedStats stats{std::get<0>(node_stats), std::get<2>(node_stats), std::get<1>(node_stats)};
272 new_peers_data.append(stats);
273 }
274
275 // Handle peer addition or removal as suggested in Qt Docs. See:
276 // - https://doc.qt.io/qt-5/model-view-programming.html#inserting-and-removing-rows
277 // - https://doc.qt.io/qt-5/model-view-programming.html#resizable-models
278 // We take advantage of the fact that the std::vector returned
279 // by interfaces::Node::getNodesStats is sorted by nodeid.
280 for (int i = 0; i < m_peers_data.size();) {
281 if (i < new_peers_data.size() && m_peers_data.at(i).nodeStats.nodeid == new_peers_data.at(i).nodeStats.nodeid) {
282 ++i;
283 continue;
284 }
285 // A peer has been removed from the table.
286 beginRemoveRows(QModelIndex(), i, i);
287 m_peers_data.erase(m_peers_data.begin() + i);
288 endRemoveRows();
289 }
290
291 if (m_peers_data.size() < new_peers_data.size()) {
292 // Some peers have been added to the end of the table.
293 beginInsertRows(QModelIndex(), m_peers_data.size(), new_peers_data.size() - 1);
294 m_peers_data.swap(new_peers_data);
295 endInsertRows();
296 } else {
297 m_peers_data.swap(new_peers_data);
298 }
299
300 const auto top_left = index(0, 0);
301 const auto bottom_right = index(rowCount() - 1, columnCount() - 1);
302 Q_EMIT dataChanged(top_left, bottom_right);
303 }
304