diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 35e4ab4ecd5..2d4bb24c830 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -802,6 +802,40 @@ QString formatBytes(uint64_t bytes) return QObject::tr("%1 GB").arg(bytes / 1'000'000'000); } +QString formatBytesps(float val) +{ + if (val < 10) + //: "Byes 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 fb0dbba88b6..cff45f96425 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -235,6 +235,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 7e12410c801..a072e5540cf 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 @@ -21,16 +24,23 @@ TrafficGraphWidget::TrafficGraphWidget(QWidget *parent) : QWidget(parent), timer(nullptr), + tt_timer(nullptr), fMax(0.0f), nMins(0), vSamplesIn(), vSamplesOut(), + vTimeStamp(), nLastBytesIn(0), nLastBytesOut(0), clientModel(nullptr) { 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) @@ -47,22 +57,66 @@ int TrafficGraphWidget::getGraphRangeMins() const return nMins; } +int TrafficGraphWidget::y_value(float value) +{ + int h = height() - YMARGIN * 2; + return YMARGIN + h - (h * 1.0 * value / fMax); +} + void TrafficGraphWidget::paintPath(QPainterPath &path, QQueue &samples) { int sampleCount = samples.size(); - if(sampleCount > 0) { + if (sampleCount > 0) { int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2; int x = XMARGIN + w; path.moveTo(x, YMARGIN + h); for(int i = 0; i < sampleCount; ++i) { x = XMARGIN + w - w * i / DESIRED_SAMPLES; - int y = YMARGIN + h - (int)(h * samples.at(i) / fMax); + int y = y_value(samples.at(i)); path.lineTo(x, y); } path.lineTo(x, YMARGIN + h); } } +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); @@ -84,7 +138,7 @@ void TrafficGraphWidget::paintEvent(QPaintEvent *) // draw lines painter.setPen(axisCol); - painter.drawText(XMARGIN, YMARGIN + h - h * val / fMax-yMarginText, QString("%1 %2").arg(val).arg(units)); + painter.drawText(XMARGIN, y_value(val)-yMarginText, QString("%1 %2").arg(val).arg(units)); for(float y = val; y < fMax; y += val) { int yy = YMARGIN + h - h * y / fMax; painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); @@ -94,13 +148,13 @@ void TrafficGraphWidget::paintEvent(QPaintEvent *) axisCol = axisCol.darker(); val = pow(10.0f, base - 1); painter.setPen(axisCol); - painter.drawText(XMARGIN, YMARGIN + h - h * val / fMax-yMarginText, QString("%1 %2").arg(val).arg(units)); + painter.drawText(XMARGIN, y_value(val)-yMarginText, QString("%1 %2").arg(val).arg(units)); int count = 1; for(float y = val; y < fMax; y += val, count++) { // don't overwrite lines drawn above if(count % 10 == 0) continue; - int yy = YMARGIN + h - h * y / fMax; + int yy = y_value(y); painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); } } @@ -120,18 +174,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 = GetTimeMillis(); + 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; @@ -141,6 +240,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) { @@ -150,6 +252,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(); } @@ -159,8 +262,7 @@ void TrafficGraphWidget::setGraphRangeMins(int mins) int msecsPerSample = nMins * 60 * 1000 / DESIRED_SAMPLES; timer->stop(); timer->setInterval(msecsPerSample); - - clear(); + timer->start(); } void TrafficGraphWidget::clear() @@ -169,6 +271,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 2d8c825815a..1dbc34eb2e8 100644 --- a/src/qt/trafficgraphwidget.h +++ b/src/qt/trafficgraphwidget.h @@ -26,9 +26,16 @@ class TrafficGraphWidget : public QWidget protected: void paintEvent(QPaintEvent *) override; + int y_value(float value); + 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 setGraphRangeMins(int mins); void clear(); @@ -36,10 +43,12 @@ public Q_SLOTS: void paintPath(QPainterPath &path, QQueue &samples); QTimer *timer; + QTimer *tt_timer; float fMax; int nMins; QQueue vSamplesIn; QQueue vSamplesOut; + QQueue vTimeStamp; quint64 nLastBytesIn; quint64 nLastBytesOut; ClientModel *clientModel; diff --git a/src/util/time.cpp b/src/util/time.cpp index eda710b12cc..2d3f8eb7c4c 100644 --- a/src/util/time.cpp +++ b/src/util/time.cpp @@ -155,6 +155,17 @@ std::string FormatISO8601Date(int64_t nTime) { return strprintf("%04i-%02i-%02i", ts.tm_year + 1900, ts.tm_mon + 1, ts.tm_mday); } +std::string FormatISO8601Time(int64_t nTime) { + struct tm ts; + time_t time_val = nTime; +#ifdef HAVE_GMTIME_R + gmtime_r(&time_val, &ts); +#else + gmtime_s(&ts, &time_val); +#endif + return strprintf("%02i:%02i:%02iZ", ts.tm_hour, ts.tm_min, ts.tm_sec); +} + int64_t ParseISO8601DateTime(const std::string& str) { static const boost::posix_time::ptime epoch = boost::posix_time::from_time_t(0); diff --git a/src/util/time.h b/src/util/time.h index 4aee01967da..095df4a8d6a 100644 --- a/src/util/time.h +++ b/src/util/time.h @@ -74,6 +74,7 @@ T GetTime(); */ std::string FormatISO8601DateTime(int64_t nTime); std::string FormatISO8601Date(int64_t nTime); +std::string FormatISO8601Time(int64_t nTime); int64_t ParseISO8601DateTime(const std::string& str); /**