// Copyright (c) 2011-2022 The Limenka developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #include #endif #include #include #include #include #include #include #include #include #define DESIRED_SAMPLES 800 #define XMARGIN 10 #define YMARGIN 10 TrafficGraphWidget::TrafficGraphWidget(QWidget* parent) : QWidget(parent), vSamplesIn(), 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) { clientModel = model; if(model) { nLastBytesIn = model->node().getTotalBytesRecv(); nLastBytesOut = model->node().getTotalBytesSent(); } } std::chrono::minutes TrafficGraphWidget::getGraphRange() const { return m_range; } int TrafficGraphWidget::y_value(float value) { if (fMax == 0) return 0; int h = height() - YMARGIN * 2; return YMARGIN + h - (h * 1.0 * (fToggle ? (pow(value, 0.30102) / pow(fMax, 0.30102)) : (value / fMax))); } void TrafficGraphWidget::paintPath(QPainterPath &path, QQueue &samples) { int sampleCount = samples.size(); 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 = y_value(samples.at(i)); path.lineTo(x, y); } path.lineTo(x, YMARGIN + h); } } void TrafficGraphWidget::mousePressEvent(QMouseEvent *event) { QWidget::mousePressEvent(event); fToggle = !fToggle; update(); } void TrafficGraphWidget::mouseMoveEvent(QMouseEvent *event) { QWidget::mouseMoveEvent(event); static int last_x = -1; static int last_y = -1; #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) const QPointF event_local_pos = event->position(); const QPointF event_global_pos = event->globalPosition(); #else const QPointF event_local_pos = event->localPos(); const QPointF event_global_pos = event->screenPos(); #endif int x = qRound(event_local_pos.x()); int y = qRound(event_local_pos.y()); x_offset = qRound(event_global_pos.x()) - x; y_offset = qRound(event_global_pos.y()) - 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 = std::max(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); painter.fillRect(rect(), Qt::black); if(fMax <= 0.0f) return; QColor axisCol(Qt::gray); int h = height() - YMARGIN * 2; painter.setPen(axisCol); painter.drawLine(XMARGIN, YMARGIN + h, width() - XMARGIN, YMARGIN + h); // decide what order of magnitude we are int base = std::floor(std::log10(fMax)); float val = std::pow(10.0f, base); const QString units = tr("kB/s"); const float yMarginText = 2.0; // if we drew 10 or 3 fewer lines, break them up at the next lower order of magnitude if(fMax / val <= (fToggle ? 10.0f : 3.0f)) { float oldval = val; val = pow(10.0f, base - 1); painter.setPen(axisCol.darker()); painter.drawText(XMARGIN, y_value(val)-yMarginText, QString("%1 %2").arg(val).arg(units)); if (fToggle) { int yy = y_value(val*0.1); painter.drawText(XMARGIN, yy-yMarginText, QString("%1 %2").arg(val*0.1).arg(units)); painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); } int count = 1; for(float y = val; y < (!fToggle || fMax / val < 20 ? fMax : oldval); y += val, count++) { if(count % 10 == 0) continue; int yy = y_value(y); painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); } val = oldval; } // draw lines painter.setPen(axisCol); for(float y = val; y < fMax; y += val) { int yy = y_value(y); painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); } painter.drawText(XMARGIN, y_value(val)-yMarginText, QString("%1 %2").arg(val).arg(units)); painter.setRenderHint(QPainter::Antialiasing); if(!vSamplesIn.empty()) { QPainterPath p; paintPath(p, vSamplesIn); painter.fillPath(p, QColor(0, 255, 0, 128)); painter.setPen(Qt::green); painter.drawPath(p); } if(!vSamplesOut.empty()) { QPainterPath p; paintPath(p, vSamplesOut); painter.fillPath(p, QColor(255, 0, 0, 128)); 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(std::max(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) / 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; while(vSamplesIn.size() > DESIRED_SAMPLES) { vSamplesIn.pop_back(); } 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) { if(f > tmax) tmax = f; } for (const float f : vSamplesOut) { if(f > tmax) tmax = f; } fMax = tmax; if (ttpoint >=0 && ttpoint < vTimeStamp.size()) ttpoint++; // Move the selected point to the left update(); } void TrafficGraphWidget::setGraphRange(std::chrono::minutes new_range) { m_range = new_range; const auto msecs_per_sample{std::chrono::duration_cast(m_range) / DESIRED_SAMPLES}; timer->stop(); timer->setInterval(msecs_per_sample); clear(); } void TrafficGraphWidget::clear() { timer->stop(); vSamplesOut.clear(); vSamplesIn.clear(); vTimeStamp.clear(); fMax = 0.0f; if(clientModel) { nLastBytesIn = clientModel->node().getTotalBytesRecv(); nLastBytesOut = clientModel->node().getTotalBytesSent(); } timer->start(); }