diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 61055e3535..3987687c16 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -968,6 +968,40 @@ QString formatBytes(uint64_t bytes) return QObject::tr("%1 GB").arg(bytes / 1'000'000'000); } +QString formatBytesps(float val) +{ + if (val < 10) + //: "Bytes per second" + return QObject::tr("%1 B/s").arg(0.01 * int(val * 100)); + if (val < 100) + //: "Bytes per second" + return QObject::tr("%1 B/s").arg(0.1 * int(val * 10)); + if (val < 1'000) + //: "Bytes per second" + return QObject::tr("%1 B/s").arg((int)val); + if (val < 10'000) + //: "Kilobytes per second" + return QObject::tr("%1 kB/s").arg(0.01 * ((int)val / 10)); + if (val < 100'000) + //: "Kilobytes per second" + return QObject::tr("%1 kB/s").arg(0.1 * ((int)val / 100)); + if (val < 1'000'000) + //: "Kilobytes per second" + return QObject::tr("%1 kB/s").arg((int)val / 1'000); + if (val < 10'000'000) + //: "Megabytes per second" + return QObject::tr("%1 MB/s").arg(0.01 * ((int)val / 10'000)); + if (val < 100'000'000) + //: "Megabytes per second" + return QObject::tr("%1 MB/s").arg(0.1 * ((int)val / 100'000)); + if (val < 10'000'000'000) + //: "Megabytes per second" + return QObject::tr("%1 MB/s").arg((long)val / 1'000'000); + + //: "Gigabytes per second" + return QObject::tr("%1 GB/s").arg((long)val / 1'000'000'000); +} + qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize, qreal font_size) { while(font_size >= minPointSize) { font.setPointSizeF(font_size); diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 4addfda43b..bcf7dc29a0 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -288,6 +288,7 @@ namespace GUIUtil QString formatNiceTimeOffset(qint64 secs); QString formatBytes(uint64_t bytes); + QString formatBytesps(float bytes); qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize = 4, qreal startPointSize = 14); diff --git a/src/qt/trafficgraphwidget.cpp b/src/qt/trafficgraphwidget.cpp index 9db1913388..8a52baebb3 100644 --- a/src/qt/trafficgraphwidget.cpp +++ b/src/qt/trafficgraphwidget.cpp @@ -5,11 +5,14 @@ #include #include #include +#include +#include #include #include #include #include +#include #include #include @@ -25,7 +28,12 @@ TrafficGraphWidget::TrafficGraphWidget(QWidget* parent) vSamplesOut() { timer = new QTimer(this); + tt_timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &TrafficGraphWidget::updateRates); + connect(tt_timer, &QTimer::timeout, this, &TrafficGraphWidget::updateToolTip); + tt_timer->setInterval(500); + tt_timer->start(); + setMouseTracking(true); } void TrafficGraphWidget::setClientModel(ClientModel *model) @@ -48,7 +56,7 @@ int TrafficGraphWidget::y_value(float value) void TrafficGraphWidget::paintPath(QPainterPath &path, QQueue &samples) { int sampleCount = samples.size(); - if(sampleCount > 0 && fMax > 0) { + if(sampleCount > 0) { int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2; int x = XMARGIN + w; path.moveTo(x, YMARGIN + h); @@ -68,6 +76,44 @@ void TrafficGraphWidget::mousePressEvent(QMouseEvent *event) update(); } +float floatmax(float a, float b) +{ + if (a > b) return a; + else return b; +} + +void TrafficGraphWidget::mouseMoveEvent(QMouseEvent *event) +{ + QWidget::mouseMoveEvent(event); + static int last_x = -1; + static int last_y = -1; + int x = event->x(); + int y = event->y(); + x_offset = event->globalX() - x; + y_offset = event->globalY() - y; + if (last_x == x && last_y == y) return; // Do nothing if mouse hasn't moved + int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2; + int i = (w + XMARGIN - x) * DESIRED_SAMPLES / w; + unsigned int smallest_distance = 50; int closest_i = -1; + int sampleSize = vTimeStamp.size(); + if (sampleSize && i >= -10 && i < sampleSize + 2 && y <= h + YMARGIN + 3) { + for (int test_i = std::max(i - 2, 0); test_i < std::min(i + 10, sampleSize); test_i++) { + float val = floatmax(vSamplesIn.at(test_i), vSamplesOut.at(test_i)); + int y_data = y_value(val); + unsigned int distance = abs(y - y_data); + if (distance < smallest_distance) { + smallest_distance = distance; + closest_i = test_i; + } + } + } + if (ttpoint != closest_i) { + ttpoint = closest_i; + update(); // Calls paintEvent() to draw or delete the highlighted point + } + last_x = x; last_y = y; +} + void TrafficGraphWidget::paintEvent(QPaintEvent *) { QPainter painter(this); @@ -130,18 +176,63 @@ void TrafficGraphWidget::paintEvent(QPaintEvent *) painter.setPen(Qt::red); painter.drawPath(p); } + int sampleCount = vTimeStamp.size(); + if (ttpoint >= 0 && ttpoint < sampleCount) { + painter.setPen(Qt::yellow); + int w = width() - XMARGIN * 2; + int x = XMARGIN + w - w * ttpoint / DESIRED_SAMPLES; + int y = y_value(floatmax(vSamplesIn.at(ttpoint), vSamplesOut.at(ttpoint))); + painter.drawEllipse(QPointF(x, y), 3, 3); + QString strTime; + int64_t sampleTime = vTimeStamp.at(ttpoint); + int age = GetTime() - sampleTime/1000; + if (age < 60*60*23) + strTime = QString::fromStdString(FormatISO8601Time(sampleTime/1000)); + else + strTime = QString::fromStdString(FormatISO8601DateTime(sampleTime/1000)); + int milliseconds_between_samples = 1000; + if (ttpoint > 0) + milliseconds_between_samples = std::min(milliseconds_between_samples, int(vTimeStamp.at(ttpoint-1) - sampleTime)); + if (ttpoint + 1 < sampleCount) + milliseconds_between_samples = std::min(milliseconds_between_samples, int(sampleTime - vTimeStamp.at(ttpoint+1))); + if (milliseconds_between_samples < 1000) + strTime += QString::fromStdString(strprintf(".%03d", (sampleTime%1000))); + QString strData = tr("In") + " " + GUIUtil::formatBytesps(vSamplesIn.at(ttpoint)*1000) + "\n" + tr("Out") + " " + GUIUtil::formatBytesps(vSamplesOut.at(ttpoint)*1000); + // Line below allows ToolTip to move faster than once every 10 seconds. + QToolTip::showText(QPoint(x + x_offset, y + y_offset), strTime + "\n. " + strData); + QToolTip::showText(QPoint(x + x_offset, y + y_offset), strTime + "\n " + strData); + tt_time = GetTime(); + } else + QToolTip::hideText(); +} + +void TrafficGraphWidget::updateToolTip() +{ + if (!QToolTip::isVisible()) { + if (ttpoint >= 0) { // Remove the yellow circle if the ToolTip has gone due to mouse moving elsewhere. + ttpoint = -1; + update(); + } + } else if (GetTime() >= tt_time + 9) { // ToolTip is about to expire so refresh it. + update(); + } } void TrafficGraphWidget::updateRates() { if(!clientModel) return; + int64_t nTime = TicksSinceEpoch(SystemClock::now()); + static int64_t nLastTime = nTime - timer->interval(); + int nRealInterval = nTime - nLastTime; quint64 bytesIn = clientModel->node().getTotalBytesRecv(), bytesOut = clientModel->node().getTotalBytesSent(); - float in_rate_kilobytes_per_sec = static_cast(bytesIn - nLastBytesIn) / timer->interval(); - float out_rate_kilobytes_per_sec = static_cast(bytesOut - nLastBytesOut) / timer->interval(); + float in_rate_kilobytes_per_sec = static_cast(bytesIn - nLastBytesIn) / nRealInterval; + float out_rate_kilobytes_per_sec = static_cast(bytesOut - nLastBytesOut) / nRealInterval; vSamplesIn.push_front(in_rate_kilobytes_per_sec); vSamplesOut.push_front(out_rate_kilobytes_per_sec); + vTimeStamp.push_front(nLastTime); + nLastTime = nTime; nLastBytesIn = bytesIn; nLastBytesOut = bytesOut; @@ -151,6 +242,9 @@ void TrafficGraphWidget::updateRates() while(vSamplesOut.size() > DESIRED_SAMPLES) { vSamplesOut.pop_back(); } + while(vTimeStamp.size() > DESIRED_SAMPLES) { + vTimeStamp.pop_back(); + } float tmax = 0.0f; for (const float f : vSamplesIn) { @@ -160,6 +254,7 @@ void TrafficGraphWidget::updateRates() if(f > tmax) tmax = f; } fMax = tmax; + if (ttpoint >=0 && ttpoint < vTimeStamp.size()) ttpoint++; // Move the selected point to the left update(); } @@ -179,6 +274,7 @@ void TrafficGraphWidget::clear() vSamplesOut.clear(); vSamplesIn.clear(); + vTimeStamp.clear(); fMax = 0.0f; if(clientModel) { diff --git a/src/qt/trafficgraphwidget.h b/src/qt/trafficgraphwidget.h index f4250ecc85..ab4eb3f259 100644 --- a/src/qt/trafficgraphwidget.h +++ b/src/qt/trafficgraphwidget.h @@ -31,9 +31,15 @@ protected: int y_value(float value); void mousePressEvent(QMouseEvent *event) override; bool fToggle = true; + void mouseMoveEvent(QMouseEvent *event) override; + int ttpoint = -1; + int x_offset = 0; + int y_offset = 0; + int64_t tt_time = 0; public Q_SLOTS: void updateRates(); + void updateToolTip(); void setGraphRange(std::chrono::minutes new_range); void clear(); @@ -41,10 +47,12 @@ private: void paintPath(QPainterPath &path, QQueue &samples); QTimer* timer{nullptr}; + QTimer* tt_timer{nullptr}; float fMax{0.0f}; std::chrono::minutes m_range{0}; QQueue vSamplesIn; QQueue vSamplesOut; + QQueue vTimeStamp; quint64 nLastBytesIn{0}; quint64 nLastBytesOut{0}; ClientModel* clientModel{nullptr}; diff --git a/src/util/time.cpp b/src/util/time.cpp index e20f30a474..41782994a9 100644 --- a/src/util/time.cpp +++ b/src/util/time.cpp @@ -87,6 +87,14 @@ std::optional ParseISO8601DateTime(std::string_view str) return int64_t{TicksSinceEpoch(tp)}; } +std::string FormatISO8601Time(int64_t nTime) +{ + const std::chrono::sys_seconds secs{std::chrono::seconds{nTime}}; + const auto days{std::chrono::floor(secs)}; + const std::chrono::hh_mm_ss hms{secs - days}; + return strprintf("%02i:%02i:%02iZ", hms.hours().count(), hms.minutes().count(), hms.seconds().count()); +} + struct timeval MillisToTimeval(int64_t nTimeout) { struct timeval timeout; diff --git a/src/util/time.h b/src/util/time.h index 27cbe50581..77c042e2a1 100644 --- a/src/util/time.h +++ b/src/util/time.h @@ -107,6 +107,7 @@ T GetTime() */ std::string FormatISO8601DateTime(int64_t nTime); std::string FormatISO8601Date(int64_t nTime); +std::string FormatISO8601Time(int64_t nTime); std::optional ParseISO8601DateTime(std::string_view str); /**