diff --git a/CMakeLists.txt b/CMakeLists.txt index f0f3bc5..e8b9f75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND WIN32) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) endif () -find_package(Qt6 REQUIRED COMPONENTS Widgets Network LinguistTools) +find_package(Qt6 REQUIRED COMPONENTS Widgets Network LinguistTools Charts) if (NKR_CROSS) set_property(TARGET Qt6::moc PROPERTY IMPORTED_LOCATION /usr/bin/moc) @@ -196,7 +196,6 @@ set(PROJECT_SOURCES include/ui/profile/edit_ssh.h include/ui/profile/edit_ssh.ui include/configs/proxy/SSHBean.h - src/sys/macos/MacOS.cpp include/sys/macos/MacOS.h src/sys/linux/LinuxCap.cpp @@ -210,6 +209,8 @@ set(PROJECT_SOURCES src/stats/connectionLister/connectionLister.cpp src/configs/proxy/Json2Bean.cpp include/sys/windows/vcCheck.h + include/ui/utils/TrafficChart.h + include/ui/utils/CustomChartView.h ) # Qt exe @@ -261,7 +262,7 @@ target_sources(nekoray PRIVATE ${CMAKE_BINARY_DIR}/translations.qrc) # Target Link target_link_libraries(nekoray PRIVATE - Qt6::Widgets Qt6::Network + Qt6::Widgets Qt6::Network Qt6::Charts Threads::Threads ${NKR_EXTERNAL_TARGETS} ${PLATFORM_LIBRARIES} diff --git a/include/global/NekoGui_DataStore.hpp b/include/global/NekoGui_DataStore.hpp index f8e95ee..927831f 100644 --- a/include/global/NekoGui_DataStore.hpp +++ b/include/global/NekoGui_DataStore.hpp @@ -63,7 +63,6 @@ namespace NekoGui { QString log_level = "info"; QString test_latency_url = "http://cp.cloudflare.com/"; int test_concurrent = 10; - int traffic_loop_interval = 500; bool disable_traffic_stats = false; int current_group = 0; // group id QString mux_protocol = "smux"; diff --git a/include/ui/mainwindow.h b/include/ui/mainwindow.h index d2ac97f..dab3eeb 100644 --- a/include/ui/mainwindow.h +++ b/include/ui/mainwindow.h @@ -4,6 +4,7 @@ #include "include/global/NekoGui.hpp" #include "include/stats/connections/connectionLister.hpp" +#include "utils/TrafficChart.h" #ifndef MW_INTERFACE @@ -52,6 +53,8 @@ public: void refresh_status(const QString &traffic_update = ""); + void update_traffic_graph(int proxyDl, int proxyUp, int directDl, int directUp); + void neko_start(int _id = -1); void neko_stop(bool crash = false, bool sem = false, bool manual = false); @@ -182,6 +185,8 @@ private: QMutex mu_download_update; // int toolTipID; + // + TrafficChart* trafficGraph; QList> get_now_selected_list(); diff --git a/include/ui/mainwindow.ui b/include/ui/mainwindow.ui index c256699..3f9dbe1 100644 --- a/include/ui/mainwindow.ui +++ b/include/ui/mainwindow.ui @@ -447,6 +447,31 @@ + + + Traffic Graph + + + + 0 + + + QLayout::SizeConstraint::SetDefaultConstraint + + + 1 + + + 0 + + + 1 + + + 0 + + + @@ -505,7 +530,7 @@ 0 0 800 - 17 + 25 diff --git a/include/ui/setting/dialog_basic_settings.ui b/include/ui/setting/dialog_basic_settings.ui index d0bb0b9..d88c37f 100644 --- a/include/ui/setting/dialog_basic_settings.ui +++ b/include/ui/setting/dialog_basic_settings.ui @@ -209,48 +209,27 @@ - - - + + + - Statistics refresh rate + Font - - - - - 500ms - - - - - 1s - - - - - 2s - - - - - 3s - - - - - 5s - - - - - Off - - + + + + + + + Font Size + + + + @@ -397,20 +376,6 @@ - - - - Font Size - - - - - - - Font - - - @@ -424,12 +389,6 @@ - - - - - - diff --git a/include/ui/utils/CustomChartView.h b/include/ui/utils/CustomChartView.h new file mode 100644 index 0000000..fd41873 --- /dev/null +++ b/include/ui/utils/CustomChartView.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +class CustomChartView : public QChartView +{ + Q_OBJECT + +public: + explicit CustomChartView(QChart *chart, QWidget *parent = nullptr) + : QChartView(chart, parent), + tooltipTimer(new QTimer(this)), + lastMousePos(QPoint(-1, -1)) + { + setMouseTracking(true); + tooltipTimer->setInterval(200); + tooltipTimer->setSingleShot(true); + connect(tooltipTimer, &QTimer::timeout, this, [=]{emit mouseStopEvent(lastMousePos);}); + } + + signals: + void mouseStopEvent(QPoint pos); + + void mouseStartMoving(); + +protected: + void mouseMoveEvent(QMouseEvent *event) override + { + lastMousePos = event->pos(); + event->ignore(); + emit mouseStartMoving(); + tooltipTimer->start(); + } + +private: + QTimer *tooltipTimer; + QPoint lastMousePos; +}; \ No newline at end of file diff --git a/include/ui/utils/TrafficChart.h b/include/ui/utils/TrafficChart.h new file mode 100644 index 0000000..861f69f --- /dev/null +++ b/include/ui/utils/TrafficChart.h @@ -0,0 +1,310 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CustomChartView.h" + +class TrafficChart +{ +private: + QChart *chart; + CustomChartView *chartView; + + QVector proxyDlRaw; + QVector proxyUlRaw; + QVector directDlRaw; + QVector directUlRaw; + + QSplineSeries *proxyDlLine; + QSplineSeries *proxyUpLine; + QSplineSeries *directDlLine; + QSplineSeries *directUpLine; + std::multiset dataSet; + + QDateTimeAxis *timeAxis; + QValueAxis *valueAxis; + + QMutex tooltipMu; + + int scaleFactor = 1; + int intervalRange = 90; + + void updateMagnitude() + { + auto currMax = dataSet.rbegin().operator*(); + if (currMax == 0) return; + auto prevScale = scaleFactor; + // calc new magnitude + if (currMax <= 1000) + { + scaleFactor = 1; + } + else if (currMax <= 1000000) + { + scaleFactor = 1000; + } + else if (currMax <= 1000000000) + { + scaleFactor = 1000000; + } + else + { + scaleFactor = 1000000000; + } + valueAxis->setMax(static_cast(currMax) / static_cast(scaleFactor) * 1.1); + if (prevScale == scaleFactor) return; + valueAxis->setLabelFormat(getRateLabel()); + scaleData(); + } + + QString getRateLabel() const + { + if (scaleFactor == 1) return "%.0f B/s"; + if (scaleFactor == 1000) return "%.2f KB/s"; + if (scaleFactor == 1000000) return "%.2f MB/s"; + if (scaleFactor == 1000000000) return "%.2f GB/s"; + return "%.0f ?/s"; + } + + void scaleData() const + { + auto data = proxyDlLine->points(); + for (int i = 0; i < data.size(); i++) + { + data[i] = {data[i].x(), static_cast(proxyDlRaw[i]) / scaleFactor}; + } + proxyDlLine->replace(data); + + data = proxyUpLine->points(); + for (int i = 0; i < data.size(); i++) + { + data[i] = {data[i].x(), static_cast(proxyUlRaw[i]) / scaleFactor}; + } + proxyUpLine->replace(data); + + data = directDlLine->points(); + for (int i = 0; i < data.size(); i++) + { + data[i] = {data[i].x(), static_cast(directDlRaw[i]) / scaleFactor}; + } + directDlLine->replace(data); + + data = directUpLine->points(); + for (int i = 0; i < data.size(); i++) + { + data[i] = {data[i].x(), static_cast(directUlRaw[i]) / scaleFactor}; + } + directUpLine->replace(data); + } + +public: + explicit TrafficChart() + { + // init + chart = new QChart; + updateTheme(); + chart->setTitle("Traffic Rate"); + chart->legend()->setVisible(true); + chart->legend()->setAlignment(Qt::AlignBottom); + chart->setMargins(QMargins(0, 0, 0, 0)); + + proxyDlLine = new QSplineSeries; + proxyDlLine->setName("Proxy Dl"); + proxyDlLine->setColor(Qt::darkBlue); + auto pen = proxyDlLine->pen(); + pen.setWidth(2); + proxyDlLine->setPen(pen); + chart->addSeries(proxyDlLine); + + proxyUpLine = new QSplineSeries; + proxyUpLine->setName("Proxy Ul"); + proxyUpLine->setColor(Qt::darkRed); + pen = proxyUpLine->pen(); + pen.setWidth(2); + proxyUpLine->setPen(pen); + chart->addSeries(proxyUpLine); + + directDlLine = new QSplineSeries; + directDlLine->setName("Direct Dl"); + directDlLine->setColor(Qt::darkGreen); + pen = directDlLine->pen(); + pen.setWidth(2); + directDlLine->setPen(pen); + chart->addSeries(directDlLine); + + directUpLine = new QSplineSeries; + directUpLine->setName("Direct Ul"); + directUpLine->setColor(Qt::darkYellow); + pen = directUpLine->pen(); + pen.setWidth(2); + directUpLine->setPen(pen); + chart->addSeries(directUpLine); + + timeAxis = new QDateTimeAxis; + timeAxis->setFormat("hh:mm:ss"); + timeAxis->setTickCount(10); + chart->addAxis(timeAxis, Qt::AlignBottom); + proxyDlLine->attachAxis(timeAxis); + proxyUpLine->attachAxis(timeAxis); + directDlLine->attachAxis(timeAxis); + directUpLine->attachAxis(timeAxis); + + valueAxis = new QValueAxis; + valueAxis->setLabelFormat("%.0f B/s"); + valueAxis->setMin(0); + valueAxis->setMax(1000); + chart->addAxis(valueAxis, Qt::AlignLeft); + proxyDlLine->attachAxis(valueAxis); + proxyUpLine->attachAxis(valueAxis); + directDlLine->attachAxis(valueAxis); + directUpLine->attachAxis(valueAxis); + + // initial values + auto now = QDateTime::currentDateTime(); + timeAxis->setRange(now.addSecs(-intervalRange + 1), now); + for (int i = 0; i < intervalRange; ++i) + { + proxyDlLine->append(now.addSecs(-intervalRange+i+1).toMSecsSinceEpoch(), 0); + proxyUpLine->append(now.addSecs(-intervalRange+i+1).toMSecsSinceEpoch(), 0); + directDlLine->append(now.addSecs(-intervalRange+i+1).toMSecsSinceEpoch(), 0); + directUpLine->append(now.addSecs(-intervalRange+i+1).toMSecsSinceEpoch(), 0); + + proxyDlRaw << 0; + proxyUlRaw << 0; + directDlRaw << 0; + directUlRaw << 0; + + dataSet.insert(0); + dataSet.insert(0); + dataSet.insert(0); + dataSet.insert(0); + } + + chartView = new CustomChartView(chart); + chartView->setRenderHint(QPainter::Antialiasing); + + QObject::connect(chartView, &CustomChartView::mouseStopEvent, chartView, [=](const QPoint pos) + { + if (!chartView->rect().contains(chartView->mapFromGlobal(QCursor::pos()))) return; + auto x = chart->mapToValue(pos).x(); + int idx = -1; + int mn=5000000; + for (int i=0;icount();i++) + { + if (auto dif = abs(proxyDlLine->at(i).x()-x); dif <= 500000) + { + if (dif < mn) + { + mn = dif; + idx = i; + } + else if (idx > 0) break; + } + } + if (idx == -1) return; + const auto format = getRateLabel(); + const auto data = QString::asprintf( + QString("Proxy Dl: " + format + "\nProxy Ul: " + format + "\nDirect Dl: " + format + "\nDirect Ul: " + format).toStdString().c_str(), + proxyDlLine->at(idx).y(), proxyUpLine->at(idx).y(), directDlLine->at(idx).y(), directUpLine->at(idx).y()); + QToolTip::showText(chartView->mapToGlobal(pos), data); + }); + + QObject::connect(chartView, &CustomChartView::mouseStartMoving, chartView, [=] + { + if (QToolTip::isVisible()) + { + QToolTip::hideText(); + } + }); + + for (QLegendMarker* marker : chart->legend()->markers()) + { + QObject::connect(marker, &QLegendMarker::clicked, chartView, [=] + { + auto series = static_cast(marker->series()); + double alpha; + if (series->isVisible()) + { + series->hide(); + marker->setVisible(true); + alpha = 0.5; + } else + { + series->show(); + alpha = 1.0; + } + QBrush brush = marker->labelBrush(); + QColor color = brush.color(); + color.setAlphaF(alpha); + brush.setColor(color); + marker->setLabelBrush(brush); + + brush = marker->brush(); + color = brush.color(); + color.setAlphaF(alpha); + brush.setColor(color); + marker->setBrush(brush); + + QPen markerPen = marker->pen(); + color = markerPen.color(); + color.setAlphaF(alpha); + markerPen.setColor(color); + marker->setPen(markerPen); + }); + } + }; + + [[nodiscard]] QChartView* getChartView() const + { + return chartView; + } + + void updateChart(int pDl, int pUl, int dDl, int dUl) + { + auto now = QDateTime::currentDateTime(); + dataSet.insert(pDl); + dataSet.insert(pUl); + dataSet.insert(dDl); + dataSet.insert(dUl); + + dataSet.erase(dataSet.find(proxyDlRaw.first())); + dataSet.erase(dataSet.find(proxyUlRaw.first())); + dataSet.erase(dataSet.find(directDlRaw.first())); + dataSet.erase(dataSet.find(directUlRaw.first())); + + proxyDlLine->remove(0); + proxyUpLine->remove(0); + directDlLine->remove(0); + directUpLine->remove(0); + + proxyDlRaw.removeFirst(); + proxyUlRaw.removeFirst(); + directDlRaw.removeFirst(); + directUlRaw.removeFirst(); + + proxyDlLine->append(now.toMSecsSinceEpoch(), static_cast(pDl) / static_cast(scaleFactor)); + proxyUpLine->append(now.toMSecsSinceEpoch(), static_cast(pUl) / static_cast(scaleFactor)); + directDlLine->append(now.toMSecsSinceEpoch(), static_cast(dDl) / static_cast(scaleFactor)); + directUpLine->append(now.toMSecsSinceEpoch(), static_cast(dUl) / static_cast(scaleFactor)); + + proxyDlRaw << pDl; + proxyUlRaw << pUl; + directDlRaw << dDl; + directUlRaw << dUl; + + timeAxis->setRange(now.addSecs(-intervalRange + 1), now); + + updateMagnitude(); + } + + void updateTheme() + { + chart->setTheme(qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark ? QChart::ChartThemeDark : QChart::ChartThemeLight); + } +}; diff --git a/src/global/NekoGui.cpp b/src/global/NekoGui.cpp index c3f4f51..d17bdae 100644 --- a/src/global/NekoGui.cpp +++ b/src/global/NekoGui.cpp @@ -249,7 +249,6 @@ namespace NekoGui { _add(new configItem("mux_concurrency", &mux_concurrency, itemType::integer)); _add(new configItem("mux_padding", &mux_padding, itemType::boolean)); _add(new configItem("mux_default_on", &mux_default_on, itemType::boolean)); - _add(new configItem("traffic_loop_interval", &traffic_loop_interval, itemType::integer)); _add(new configItem("test_concurrent", &test_concurrent, itemType::integer)); _add(new configItem("theme", &theme, itemType::string)); _add(new configItem("custom_inbound", &custom_inbound, itemType::string)); diff --git a/src/stats/connectionLister/connectionLister.cpp b/src/stats/connectionLister/connectionLister.cpp index e6e6907..29238ff 100644 --- a/src/stats/connectionLister/connectionLister.cpp +++ b/src/stats/connectionLister/connectionLister.cpp @@ -26,8 +26,7 @@ namespace NekoGui_traffic while (true) { if (stop) return; - auto sleep_ms = NekoGui::dataStore->traffic_loop_interval; - QThread::msleep(sleep_ms); + QThread::msleep(1000); if (suspend || !NekoGui::dataStore->enable_stats) continue; diff --git a/src/stats/traffic/TrafficLooper.cpp b/src/stats/traffic/TrafficLooper.cpp index 0a5f153..57c4a8a 100644 --- a/src/stats/traffic/TrafficLooper.cpp +++ b/src/stats/traffic/TrafficLooper.cpp @@ -56,15 +56,13 @@ namespace NekoGui_traffic { } void TrafficLooper::Loop() { - if (NekoGui::dataStore->disable_traffic_stats) { - return; - } elapsedTimer.start(); while (true) { - auto sleep_ms = NekoGui::dataStore->traffic_loop_interval; - if (sleep_ms < 500 || sleep_ms > 5000) sleep_ms = 1000; - QThread::msleep(sleep_ms); - if (NekoGui::dataStore->traffic_loop_interval == 0) continue; // user disabled + QThread::msleep(1000); // refresh every one second + + if (NekoGui::dataStore->disable_traffic_stats) { + continue; + } // profile start and stop if (!loop_enabled) { @@ -76,6 +74,11 @@ namespace NekoGui_traffic { m->refresh_status("STOP"); }); } + runOnUiThread([=] + { + auto m = GetMainWindow(); + m->update_traffic_graph(0, 0, 0, 0); + }); continue; } else { // 开始 @@ -96,6 +99,7 @@ namespace NekoGui_traffic { auto m = GetMainWindow(); if (proxy != nullptr) { m->refresh_status(QObject::tr("Proxy: %1\nDirect: %2").arg(proxy->DisplaySpeed(), direct->DisplaySpeed())); + m->update_traffic_graph(proxy->downlink_rate, proxy->uplink_rate, direct->downlink_rate, direct->uplink_rate); } for (const auto &item: items) { if (item->id < 0) continue; diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 71c10dc..5b33b64 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include <3rdparty/QHotkey/qhotkey.h> #include @@ -173,6 +174,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this, [=](const Qt::ColorScheme& scheme) { new SyntaxHighlighter(scheme == Qt::ColorScheme::Dark, qvLogDocument); themeManager->ApplyTheme(NekoGui::dataStore->theme, true); + if (trafficGraph) trafficGraph->updateTheme(); }); connect(themeManager, &ThemeManager::themeChanged, this, [=](const QString& theme){ if (theme.toLower().contains("vista")) { @@ -247,6 +249,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi } }); + // setup Traffic Graph + trafficGraph = new TrafficChart(); + ui->graph_tab->layout()->addWidget(trafficGraph->getChartView()); + // table UI ui->proxyListTable->callback_save_order = [=] { auto group = NekoGui::profileManager->CurrentGroup(); @@ -1128,6 +1134,11 @@ void MainWindow::refresh_status(const QString &traffic_update) { icon_status = icon_status_new; } +void MainWindow::update_traffic_graph(int proxyDl, int proxyUp, int directDl, int directUp) +{ + trafficGraph->updateChart(proxyDl, proxyUp, directDl, directUp); +} + // table显示 // refresh_groups -> show_group -> refresh_proxy_list diff --git a/src/ui/mainwindow_grpc.cpp b/src/ui/mainwindow_grpc.cpp index 7dba3ea..9ec287f 100644 --- a/src/ui/mainwindow_grpc.cpp +++ b/src/ui/mainwindow_grpc.cpp @@ -415,13 +415,11 @@ void MainWindow::neko_stop(bool crash, bool sem, bool manual) { NekoGui_traffic::connection_lister->suspend = true; UpdateConnectionListWithRecreate({}); NekoGui_traffic::trafficLooper->loop_mutex.lock(); - if (NekoGui::dataStore->traffic_loop_interval != 0) { - NekoGui_traffic::trafficLooper->UpdateAll(); - for (const auto &item: NekoGui_traffic::trafficLooper->items) { - if (item->id < 0) continue; - NekoGui::profileManager->GetProfile(item->id)->Save(); - refresh_proxy_list(item->id); - } + NekoGui_traffic::trafficLooper->UpdateAll(); + for (const auto &item: NekoGui_traffic::trafficLooper->items) { + if (item->id < 0) continue; + NekoGui::profileManager->GetProfile(item->id)->Save(); + refresh_proxy_list(item->id); } NekoGui_traffic::trafficLooper->loop_mutex.unlock(); diff --git a/src/ui/setting/dialog_basic_settings.cpp b/src/ui/setting/dialog_basic_settings.cpp index 5c33edf..c69e99d 100644 --- a/src/ui/setting/dialog_basic_settings.cpp +++ b/src/ui/setting/dialog_basic_settings.cpp @@ -48,20 +48,6 @@ DialogBasicSettings::DialogBasicSettings(QWidget *parent) D_LOAD_BOOL(start_minimal) D_LOAD_INT(max_log_line) // - if (NekoGui::dataStore->traffic_loop_interval == 500) { - ui->rfsh_r->setCurrentIndex(0); - } else if (NekoGui::dataStore->traffic_loop_interval == 1000) { - ui->rfsh_r->setCurrentIndex(1); - } else if (NekoGui::dataStore->traffic_loop_interval == 2000) { - ui->rfsh_r->setCurrentIndex(2); - } else if (NekoGui::dataStore->traffic_loop_interval == 3000) { - ui->rfsh_r->setCurrentIndex(3); - } else if (NekoGui::dataStore->traffic_loop_interval == 5000) { - ui->rfsh_r->setCurrentIndex(4); - } else { - ui->rfsh_r->setCurrentIndex(5); - } - // ui->language->setCurrentIndex(NekoGui::dataStore->language); connect(ui->language, static_cast(&QComboBox::currentIndexChanged), this, [=](int index) { CACHE.needRestart = true; @@ -190,20 +176,6 @@ void DialogBasicSettings::accept() { NekoGui::dataStore->max_log_line = 200; } - if (ui->rfsh_r->currentIndex() == 0) { - NekoGui::dataStore->traffic_loop_interval = 500; - } else if (ui->rfsh_r->currentIndex() == 1) { - NekoGui::dataStore->traffic_loop_interval = 1000; - } else if (ui->rfsh_r->currentIndex() == 2) { - NekoGui::dataStore->traffic_loop_interval = 2000; - } else if (ui->rfsh_r->currentIndex() == 3) { - NekoGui::dataStore->traffic_loop_interval = 3000; - } else if (ui->rfsh_r->currentIndex() == 4) { - NekoGui::dataStore->traffic_loop_interval = 5000; - } else { - NekoGui::dataStore->traffic_loop_interval = 0; - } - // Subscription if (ui->sub_auto_update_enable->isChecked()) {