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