diff options
Diffstat (limited to 'ui/qt/io_graph_dialog.cpp')
-rw-r--r-- | ui/qt/io_graph_dialog.cpp | 1454 |
1 files changed, 1024 insertions, 430 deletions
diff --git a/ui/qt/io_graph_dialog.cpp b/ui/qt/io_graph_dialog.cpp index 002b98f9..8e7821a4 100644 --- a/ui/qt/io_graph_dialog.cpp +++ b/ui/qt/io_graph_dialog.cpp @@ -7,13 +7,14 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ +#define WS_LOG_DOMAIN LOG_DOMAIN_QTUI #include "io_graph_dialog.h" #include <ui_io_graph_dialog.h> #include "file.h" +#include "locale.h" #include <epan/stat_tap_ui.h> -#include "epan/stats_tree_priv.h" #include "epan/uat-int.h" #include <wsutil/utf8_entities.h> @@ -25,11 +26,16 @@ #include <ui/qt/utils/color_utils.h> #include <ui/qt/widgets/qcustomplot.h> +#include <ui/qt/widgets/qcp_string_legend_item.h> +#include <ui/qt/widgets/qcp_axis_ticker_si.h> #include "progress_frame.h" #include "main_application.h" +#include <ui/qt/main_window.h> #include <wsutil/filesystem.h> #include <wsutil/report_message.h> +#include <wsutil/nstime.h> +#include <wsutil/to_str.h> #include <ui/qt/utils/tango_colors.h> //provides some default colors #include <ui/qt/widgets/copy_from_profile_button.h> @@ -47,6 +53,8 @@ #include <QTimer> #include <QVariant> +#include <new> // std::bad_alloc + // Bugs and uncertainties: // - Regular (non-stacked) bar graphs are drawn on top of each other on the Z axis. // The QCP forum suggests drawing them side by side: @@ -54,15 +62,30 @@ // - We retap and redraw more than we should. // - Smoothing doesn't seem to match GTK+ // - Closing the color picker on macOS sends the dialog to the background. -// - The color picker triggers https://bugreports.qt.io/browse/QTBUG-58699. +// - X-axis time buckets are based on the file relative time, even in +// Time of Day / absolute time mode. (See io_graph_item.c/get_io_graph_index) +// Changing this would mean retapping when switching to ToD mode, though. // To do: // - Use scroll bars? -// - Scroll during live captures +// https://www.qcustomplot.com/index.php/tutorials/specialcases/scrollbar +// - Scroll during live captures (currently the graph auto rescales instead) // - Set ticks per pixel (e.g. pressing "2" sets 2 tpp). // - Explicitly handle missing values, e.g. via NAN. // - Add a "show missing" or "show zero" option to the UAT? // It would add yet another graph configuration column. +// - Increase max number of items (or make configurable) +// - Dark Mode support, e.g. +// https://www.qcustomplot.com/index.php/demos/barchartdemo +// - Multiple y-axes? +// https://www.qcustomplot.com/index.php/demos/multiaxisdemo +// https://www.qcustomplot.com/index.php/tutorials/specialcases/axistags + +// Scale factor to convert the units the interval is stored in to seconds. +// Must match what get_io_graph_index() in io_graph_item expects. +// Increase this in order to make smaller intervals possible. +const int SCALE = 1000000; +const double SCALE_F = (double)SCALE; const qreal graph_line_width_ = 1.0; @@ -77,15 +100,15 @@ const int stat_update_interval_ = 200; // ms // Saved graph settings typedef struct _io_graph_settings_t { - gboolean enabled; + bool enabled; char* name; char* dfilter; - guint color; - guint32 style; - guint32 yaxis; + unsigned color; + uint32_t style; + uint32_t yaxis; char* yfield; - guint32 sma_period; - guint32 y_axis_factor; + uint32_t sma_period; + uint32_t y_axis_factor; } io_graph_settings_t; static const value_string graph_style_vs[] = { @@ -105,7 +128,7 @@ static const value_string graph_style_vs[] = { { 0, NULL } }; -static const value_string y_axis_vs[] = { +static const value_string y_axis_packet_vs[] = { { IOG_ITEM_UNIT_PACKETS, "Packets" }, { IOG_ITEM_UNIT_BYTES, "Bytes" }, { IOG_ITEM_UNIT_BITS, "Bits" }, @@ -115,10 +138,25 @@ static const value_string y_axis_vs[] = { { IOG_ITEM_UNIT_CALC_MAX, "MAX(Y Field)" }, { IOG_ITEM_UNIT_CALC_MIN, "MIN(Y Field)" }, { IOG_ITEM_UNIT_CALC_AVERAGE, "AVG(Y Field)" }, + { IOG_ITEM_UNIT_CALC_THROUGHPUT, "THROUGHPUT(Y Field)" }, { IOG_ITEM_UNIT_CALC_LOAD, "LOAD(Y Field)" }, { 0, NULL } }; +static const value_string y_axis_event_vs[] = { + { IOG_ITEM_UNIT_PACKETS, "Events" }, + y_axis_packet_vs[1], + y_axis_packet_vs[2], + y_axis_packet_vs[3], + y_axis_packet_vs[4], + y_axis_packet_vs[5], + y_axis_packet_vs[6], + y_axis_packet_vs[7], + y_axis_packet_vs[8], + y_axis_packet_vs[9], + { 0, NULL } +}; + static const value_string moving_avg_vs[] = { { 0, "None" }, { 10, "10 interval SMA" }, @@ -131,24 +169,31 @@ static const value_string moving_avg_vs[] = { { 0, NULL } }; -static io_graph_settings_t *iog_settings_ = NULL; -static guint num_io_graphs_ = 0; -static uat_t *iog_uat_ = NULL; +static io_graph_settings_t *iog_settings_; +static unsigned num_io_graphs_; +static uat_t *iog_uat_; +// XXX - Multiple UatModels with the same uat can crash if one is +// edited, because the underlying uat_t* data changes but the +// record_errors and dirty_records lists do not. +static QPointer<UatModel> static_uat_model_; // y_axis_factor was added in 3.6. Provide backward compatibility. static const char *iog_uat_defaults_[] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, "1" }; +static char *decimal_point; + extern "C" { -//Allow the enable/disable field to be a checkbox, but for backwards compatibility, -//the strings have to be "Enabled"/"Disabled", not "TRUE"/"FALSE" +//Allow the enable/disable field to be a checkbox, but for backwards +//compatibility with pre-2.6 versions, the strings are "Enabled"/"Disabled", +//not "true"/"false". (Pre-4.4 versions require "true" to be all-caps.) #define UAT_BOOL_ENABLE_CB_DEF(basename,field_name,rec_t) \ -static void basename ## _ ## field_name ## _set_cb(void* rec, const char* buf, guint len, const void* UNUSED_PARAMETER(u1), const void* UNUSED_PARAMETER(u2)) {\ +static void basename ## _ ## field_name ## _set_cb(void* rec, const char* buf, unsigned len, const void* UNUSED_PARAMETER(u1), const void* UNUSED_PARAMETER(u2)) {\ char* tmp_str = g_strndup(buf,len); \ - if ((g_strcmp0(tmp_str, "Enabled") == 0) || \ - (g_strcmp0(tmp_str, "TRUE") == 0)) \ + if (tmp_str && ((g_strcmp0(tmp_str, "Enabled") == 0) || \ + (g_ascii_strcasecmp(tmp_str, "true") == 0))) \ ((rec_t*)rec)->field_name = 1; \ else \ ((rec_t*)rec)->field_name = 0; \ @@ -157,32 +202,33 @@ static void basename ## _ ## field_name ## _tostr_cb(void* rec, char** out_ptr, *out_ptr = ws_strdup_printf("%s",((rec_t*)rec)->field_name ? "Enabled" : "Disabled"); \ *out_len = (unsigned)strlen(*out_ptr); } -static bool uat_fld_chk_enable(void* u1 _U_, const char* strptr, guint len, const void* u2 _U_, const void* u3 _U_, char** err) +static bool uat_fld_chk_enable(void* u1 _U_, const char* strptr, unsigned len, const void* u2 _U_, const void* u3 _U_, char** err) { char* str = g_strndup(strptr,len); - if ((g_strcmp0(str, "Enabled") == 0) || + if (str && + ((g_strcmp0(str, "Enabled") == 0) || (g_strcmp0(str, "Disabled") == 0) || - (g_strcmp0(str, "TRUE") == 0) || //just for UAT functionality - (g_strcmp0(str, "FALSE") == 0)) { + (g_ascii_strcasecmp(str, "true") == 0) || //just for UAT functionality + (g_ascii_strcasecmp(str, "false") == 0))) { *err = NULL; g_free(str); - return TRUE; + return true; } //User should never see this unless they are manually modifying UAT *err = ws_strdup_printf("invalid value: %s (must be Enabled or Disabled)", str); g_free(str); - return FALSE; + return false; } #define UAT_FLD_BOOL_ENABLE(basename,field_name,title,desc) \ {#field_name, title, PT_TXTMOD_BOOL,{uat_fld_chk_enable,basename ## _ ## field_name ## _set_cb,basename ## _ ## field_name ## _tostr_cb},{0,0,0},0,desc,FLDFILL} //"Custom" handler for sma_period enumeration for backwards compatibility -static void io_graph_sma_period_set_cb(void* rec, const char* buf, guint len, const void* vs, const void* u2 _U_) +static void io_graph_sma_period_set_cb(void* rec, const char* buf, unsigned len, const void* vs, const void* u2 _U_) { - guint i; + unsigned i; char* str = g_strndup(buf,len); const char* cstr; ((io_graph_settings_t*)rec)->sma_period = 0; @@ -202,7 +248,7 @@ static void io_graph_sma_period_set_cb(void* rec, const char* buf, guint len, co for (i=0; (cstr = ((const value_string*)vs)[i].strptr) ;i++) { if (g_str_equal(cstr,str)) { - ((io_graph_settings_t*)rec)->sma_period = (guint32)((const value_string*)vs)[i].value; + ((io_graph_settings_t*)rec)->sma_period = (uint32_t)((const value_string*)vs)[i].value; g_free(str); return; } @@ -212,7 +258,7 @@ static void io_graph_sma_period_set_cb(void* rec, const char* buf, guint len, co //Duplicated because macro covers both functions static void io_graph_sma_period_tostr_cb(void* rec, char** out_ptr, unsigned* out_len, const void* vs, const void* u2 _U_) { - guint i; + unsigned i; for (i=0;((const value_string*)vs)[i].strptr;i++) { if (((const value_string*)vs)[i].value == ((io_graph_settings_t*)rec)->sma_period) { *out_ptr = g_strdup(((const value_string*)vs)[i].strptr); @@ -224,9 +270,9 @@ static void io_graph_sma_period_tostr_cb(void* rec, char** out_ptr, unsigned* ou *out_len = (unsigned)strlen("None"); } -static bool sma_period_chk_enum(void* u1 _U_, const char* strptr, guint len, const void* v, const void* u3 _U_, char** err) { +static bool sma_period_chk_enum(void* u1 _U_, const char* strptr, unsigned len, const void* v, const void* u3 _U_, char** err) { char *str = g_strndup(strptr,len); - guint i; + unsigned i; const value_string* vs = (const value_string *)v; //Original UAT had just raw numbers and not enumerated values with "interval SMA" @@ -246,13 +292,13 @@ static bool sma_period_chk_enum(void* u1 _U_, const char* strptr, guint len, con if (g_strcmp0(vs[i].strptr,str) == 0) { *err = NULL; g_free(str); - return TRUE; + return true; } } *err = ws_strdup_printf("invalid value: %s",str); g_free(str); - return FALSE; + return false; } #define UAT_FLD_SMA_PERIOD(basename,field_name,title,enum,desc) \ @@ -263,18 +309,33 @@ UAT_BOOL_ENABLE_CB_DEF(io_graph, enabled, io_graph_settings_t) UAT_CSTRING_CB_DEF(io_graph, name, io_graph_settings_t) UAT_DISPLAY_FILTER_CB_DEF(io_graph, dfilter, io_graph_settings_t) UAT_COLOR_CB_DEF(io_graph, color, io_graph_settings_t) -UAT_VS_DEF(io_graph, style, io_graph_settings_t, guint32, 0, "Line") -UAT_VS_DEF(io_graph, yaxis, io_graph_settings_t, guint32, 0, "Packets") +UAT_VS_DEF(io_graph, style, io_graph_settings_t, uint32_t, 0, "Line") +// XXX Need to use "Events" where appropriate. +UAT_VS_DEF(io_graph, yaxis, io_graph_settings_t, uint32_t, 0, "Packets") UAT_PROTO_FIELD_CB_DEF(io_graph, yfield, io_graph_settings_t) UAT_DEC_CB_DEF(io_graph, y_axis_factor, io_graph_settings_t) -static uat_field_t io_graph_fields[] = { +static uat_field_t io_graph_packet_fields[] = { + UAT_FLD_BOOL_ENABLE(io_graph, enabled, "Enabled", "Graph visibility"), + UAT_FLD_CSTRING(io_graph, name, "Graph Name", "The name of the graph"), + UAT_FLD_DISPLAY_FILTER(io_graph, dfilter, "Display Filter", "Graph packets matching this display filter"), + UAT_FLD_COLOR(io_graph, color, "Color", "Graph color (#RRGGBB)"), + UAT_FLD_VS(io_graph, style, "Style", graph_style_vs, "Graph style (Line, Bars, etc.)"), + UAT_FLD_VS(io_graph, yaxis, "Y Axis", y_axis_packet_vs, "Y Axis units"), + UAT_FLD_PROTO_FIELD(io_graph, yfield, "Y Field", "Apply calculations to this field"), + UAT_FLD_SMA_PERIOD(io_graph, sma_period, "SMA Period", moving_avg_vs, "Simple moving average period"), + UAT_FLD_DEC(io_graph, y_axis_factor, "Y Axis Factor", "Y Axis Factor"), + + UAT_END_FIELDS +}; + +static uat_field_t io_graph_event_fields[] = { UAT_FLD_BOOL_ENABLE(io_graph, enabled, "Enabled", "Graph visibility"), UAT_FLD_CSTRING(io_graph, name, "Graph Name", "The name of the graph"), UAT_FLD_DISPLAY_FILTER(io_graph, dfilter, "Display Filter", "Graph packets matching this display filter"), UAT_FLD_COLOR(io_graph, color, "Color", "Graph color (#RRGGBB)"), UAT_FLD_VS(io_graph, style, "Style", graph_style_vs, "Graph style (Line, Bars, etc.)"), - UAT_FLD_VS(io_graph, yaxis, "Y Axis", y_axis_vs, "Y Axis units"), + UAT_FLD_VS(io_graph, yaxis, "Y Axis", y_axis_event_vs, "Y Axis units"), UAT_FLD_PROTO_FIELD(io_graph, yfield, "Y Field", "Apply calculations to this field"), UAT_FLD_SMA_PERIOD(io_graph, sma_period, "SMA Period", moving_avg_vs, "Simple moving average period"), UAT_FLD_DEC(io_graph, y_axis_factor, "Y Axis Factor", "Y Axis Factor"), @@ -306,16 +367,25 @@ static void io_graph_free_cb(void* p) { g_free(iogs->yfield); } +// If the uat changes outside the model, e.g. when changing profiles, +// we need to tell the UatModel. +static void io_graph_post_update_cb() { + if (static_uat_model_) { + static_uat_model_->reloadUat(); + } +} + } // extern "C" -IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf, QString displayFilter) : +IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf, QString displayFilter, + io_graph_item_unit_t value_units, QString yfield) : WiresharkDialog(parent, cf), ui(new Ui::IOGraphDialog), uat_model_(nullptr), uat_delegate_(nullptr), base_graph_(nullptr), tracer_(nullptr), - start_time_(0.0), + start_time_(NSTIME_INIT_ZERO), mouse_drags_(true), rubber_band_(nullptr), stat_timer_(nullptr), @@ -355,15 +425,17 @@ IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf, QString displayFi QPushButton *copy_bt = ui->buttonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole); connect (copy_bt, SIGNAL(clicked()), this, SLOT(copyAsCsvClicked())); - CopyFromProfileButton * copy_button = new CopyFromProfileButton(this, "io_graphs", tr("Copy graphs from another profile.")); - ui->buttonBox->addButton(copy_button, QDialogButtonBox::ActionRole); - connect(copy_button, &CopyFromProfileButton::copyProfile, this, &IOGraphDialog::copyFromProfile); + copy_profile_bt_ = new CopyFromProfileButton(this, "io_graphs", tr("Copy graphs from another profile.")); + ui->buttonBox->addButton(copy_profile_bt_, QDialogButtonBox::ActionRole); + connect(copy_profile_bt_, &CopyFromProfileButton::copyProfile, this, &IOGraphDialog::copyFromProfile); QPushButton *close_bt = ui->buttonBox->button(QDialogButtonBox::Close); if (close_bt) { close_bt->setDefault(true); } + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &IOGraphDialog::buttonBoxClicked); + ui->automaticUpdateCheckBox->setChecked(prefs.gui_io_graph_automatic_update ? true : false); ui->enableLegendCheckBox->setChecked(prefs.gui_io_graph_enable_legend ? true : false); @@ -373,22 +445,38 @@ IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf, QString displayFi stat_timer_->start(stat_update_interval_); // Intervals (ms) - ui->intervalComboBox->addItem(tr("1 ms"), 1); - ui->intervalComboBox->addItem(tr("2 ms"), 2); - ui->intervalComboBox->addItem(tr("5 ms"), 5); - ui->intervalComboBox->addItem(tr("10 ms"), 10); - ui->intervalComboBox->addItem(tr("20 ms"), 20); - ui->intervalComboBox->addItem(tr("50 ms"), 50); - ui->intervalComboBox->addItem(tr("100 ms"), 100); - ui->intervalComboBox->addItem(tr("200 ms"), 200); - ui->intervalComboBox->addItem(tr("500 ms"), 500); - ui->intervalComboBox->addItem(tr("1 sec"), 1000); - ui->intervalComboBox->addItem(tr("2 sec"), 2000); - ui->intervalComboBox->addItem(tr("5 sec"), 5000); - ui->intervalComboBox->addItem(tr("10 sec"), 10000); - ui->intervalComboBox->addItem(tr("1 min"), 60000); - ui->intervalComboBox->addItem(tr("10 min"), 600000); - ui->intervalComboBox->setCurrentIndex(9); + // #6441 asks for arbitrary values. We could probably do that with + // a QSpinBox, e.g. using QAbstractSpinBox::AdaptiveDecimalStepType + // or similar (it only exists starting in Qt 5.12) and suffix(), + // or something fancier with valueFromText() and textFromValue() to + // convert to and from SI prefixes. + ui->intervalComboBox->addItem(tr("1 μs"), SCALE / 1000000); + ui->intervalComboBox->addItem(tr("2 μs"), SCALE / 500000); + ui->intervalComboBox->addItem(tr("5 μs"), SCALE / 200000); + ui->intervalComboBox->addItem(tr("10 μs"), SCALE / 100000); + ui->intervalComboBox->addItem(tr("20 μs"), SCALE / 50000); + ui->intervalComboBox->addItem(tr("50 μs"), SCALE / 20000); + ui->intervalComboBox->addItem(tr("100 μs"), SCALE / 10000); + ui->intervalComboBox->addItem(tr("200 μs"), SCALE / 5000); + ui->intervalComboBox->addItem(tr("500 μs"), SCALE / 2000); + ui->intervalComboBox->addItem(tr("1 ms"), SCALE / 1000); + ui->intervalComboBox->addItem(tr("2 ms"), SCALE / 500); + ui->intervalComboBox->addItem(tr("5 ms"), SCALE / 200); + ui->intervalComboBox->addItem(tr("10 ms"), SCALE / 100); + ui->intervalComboBox->addItem(tr("20 ms"), SCALE / 50); + ui->intervalComboBox->addItem(tr("50 ms"), SCALE / 20); + ui->intervalComboBox->addItem(tr("100 ms"), SCALE / 10); + ui->intervalComboBox->addItem(tr("200 ms"), SCALE / 5); + ui->intervalComboBox->addItem(tr("500 ms"), SCALE / 2); + ui->intervalComboBox->addItem(tr("1 sec"), SCALE); + ui->intervalComboBox->addItem(tr("2 sec"), SCALE * 2); + ui->intervalComboBox->addItem(tr("5 sec"), SCALE * 5); + ui->intervalComboBox->addItem(tr("10 sec"), SCALE * 10); + ui->intervalComboBox->addItem(tr("1 min"), SCALE * 60); + ui->intervalComboBox->addItem(tr("2 min"), SCALE * 120); + ui->intervalComboBox->addItem(tr("5 min"), SCALE * 300); + ui->intervalComboBox->addItem(tr("10 min"), SCALE * 600); + ui->intervalComboBox->setCurrentIndex(18); ui->todCheckBox->setChecked(false); iop->xAxis->setTicker(number_ticker_); @@ -419,6 +507,9 @@ IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf, QString displayFi ctx_menu_.addAction(ui->actionCrosshairs); set_action_shortcuts_visible_in_context_menu(ctx_menu_.actions()); + iop->setContextMenuPolicy(Qt::CustomContextMenu); + connect(iop, &QCustomPlot::customContextMenuRequested, this, &IOGraphDialog::showContextMenu); + iop->xAxis->setLabel(tr("Time (s)")); iop->setMouseTracking(true); @@ -433,22 +524,23 @@ IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf, QString displayFi loadProfileGraphs(); bool filterExists = false; - QString graph_name = is_packet_configuration_namespace() ? tr("Filtered packets") : tr("Filtered events"); if (uat_model_->rowCount() > 0) { for (int i = 0; i < uat_model_->rowCount(); i++) { createIOGraph(i); - if (ioGraphs_.at(i)->filter().compare(displayFilter) == 0) + IOGraph *iog = ioGraphs_.at(i); + if (iog->filter().compare(displayFilter) == 0 && + iog->valueUnitField().compare(yfield) == 0 && + iog->valueUnits() == value_units) { filterExists = true; + } } - if (! filterExists && displayFilter.length() > 0) - addGraph(true, graph_name, displayFilter, ColorUtils::graphColor(uat_model_->rowCount()), - IOGraph::psLine, IOG_ITEM_UNIT_PACKETS, QString(), DEFAULT_MOVING_AVERAGE, DEFAULT_Y_AXIS_FACTOR); } else { addDefaultGraph(true, 0); addDefaultGraph(true, 1); - if (displayFilter.length() > 0) - addGraph(true, graph_name, displayFilter, ColorUtils::graphColor(uat_model_->rowCount()), - IOGraph::psLine, IOG_ITEM_UNIT_PACKETS, QString(), DEFAULT_MOVING_AVERAGE, DEFAULT_Y_AXIS_FACTOR); + } + + if (! filterExists && (!displayFilter.isEmpty() || !yfield.isEmpty())) { + addGraph(true, displayFilter, value_units, yfield); } toggleTracerStyle(true); @@ -460,14 +552,23 @@ IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf, QString displayFi ui->splitter->setStretchFactor(0, 95); ui->splitter->setStretchFactor(1, 5); + loadSplitterState(ui->splitter); //XXX - resize columns? + //ui->graphUat->header()->resizeSections(QHeaderView::ResizeToContents); ProgressFrame::addToButtonBox(ui->buttonBox, &parent); connect(iop, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(graphClicked(QMouseEvent*))); connect(iop, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(mouseMoved(QMouseEvent*))); connect(iop, SIGNAL(mouseRelease(QMouseEvent*)), this, SLOT(mouseReleased(QMouseEvent*))); + + connect(iop, &QCustomPlot::beforeReplot, this, &IOGraphDialog::updateLegend); + + MainWindow *main_window = qobject_cast<MainWindow *>(mainApp->mainWindow()); + if (main_window != nullptr) { + connect(main_window, &MainWindow::framesSelected, this, &IOGraphDialog::selectedFrameChanged); + } } IOGraphDialog::~IOGraphDialog() @@ -482,15 +583,18 @@ IOGraphDialog::~IOGraphDialog() void IOGraphDialog::copyFromProfile(QString filename) { - guint orig_data_len = iog_uat_->raw_data->len; + if (uat_model_ == nullptr) + return; - gchar *err = NULL; + char *err = NULL; + // uat_load appends rows to the current UAT, using filename. + // We should let the UatModel handle it, and have the UatModel + // call beginInsertRows() and endInsertRows(), so that we can + // just add the new rows instead of resetting the information. if (uat_load(iog_uat_, filename.toUtf8().constData(), &err)) { - iog_uat_->changed = TRUE; - uat_model_->reloadUat(); - for (guint i = orig_data_len; i < iog_uat_->raw_data->len; i++) { - createIOGraph(i); - } + iog_uat_->changed = true; + // uat_load calls the post update cb, which reloads the Uat. + //uat_model_->reloadUat(); } else { report_failure("Error while loading %s: %s", iog_uat_->name, err); g_free(err); @@ -499,6 +603,8 @@ void IOGraphDialog::copyFromProfile(QString filename) void IOGraphDialog::addGraph(bool checked, QString name, QString dfilter, QRgb color_idx, IOGraph::PlotStyles style, io_graph_item_unit_t value_units, QString yfield, int moving_average, int y_axis_factor) { + if (uat_model_ == nullptr) + return; QVariantList newRowData; newRowData.append(checked ? Qt::Checked : Qt::Unchecked); @@ -507,12 +613,12 @@ void IOGraphDialog::addGraph(bool checked, QString name, QString dfilter, QRgb c newRowData.append(QColor(color_idx)); newRowData.append(val_to_str_const(style, graph_style_vs, "None")); if (is_packet_configuration_namespace()) { - newRowData.append(val_to_str_const(value_units, y_axis_vs, "Packets")); + newRowData.append(val_to_str_const(value_units, y_axis_packet_vs, "Packets")); } else { - newRowData.append(val_to_str_const(value_units, y_axis_vs, "Events")); + newRowData.append(val_to_str_const(value_units, y_axis_event_vs, "Events")); } newRowData.append(yfield); - newRowData.append(val_to_str_const((guint32) moving_average, moving_avg_vs, "None")); + newRowData.append(val_to_str_const((uint32_t) moving_average, moving_avg_vs, "None")); newRowData.append(y_axis_factor); QModelIndex newIndex = uat_model_->appendEntry(newRowData); @@ -522,11 +628,36 @@ void IOGraphDialog::addGraph(bool checked, QString name, QString dfilter, QRgb c return; } ui->graphUat->setCurrentIndex(newIndex); - createIOGraph(newIndex.row()); +} + +void IOGraphDialog::addGraph(bool checked, QString dfilter, io_graph_item_unit_t value_units, QString yfield) +{ + if (uat_model_ == nullptr) + return; + + QString graph_name; + if (yfield.isEmpty()) { + if (!dfilter.isEmpty()) { + graph_name = is_packet_configuration_namespace() ? tr("Filtered packets") : tr("Filtered events"); + } else { + graph_name = is_packet_configuration_namespace() ? tr("All packets") : tr("All events"); + } + } else { + if (is_packet_configuration_namespace()) { + graph_name = QString(val_to_str_const(value_units, y_axis_packet_vs, "Unknown")).replace("Y Field", yfield); + } else { + graph_name = QString(val_to_str_const(value_units, y_axis_event_vs, "Unknown")).replace("Y Field", yfield); + } + } + addGraph(checked, std::move(graph_name), dfilter, ColorUtils::graphColor(uat_model_->rowCount()), + IOGraph::psLine, value_units, yfield, DEFAULT_MOVING_AVERAGE, DEFAULT_Y_AXIS_FACTOR); } void IOGraphDialog::addGraph(bool copy_from_current) { + if (uat_model_ == nullptr) + return; + const QModelIndex ¤t = ui->graphUat->currentIndex(); if (copy_from_current && !current.isValid()) return; @@ -540,8 +671,6 @@ void IOGraphDialog::addGraph(bool copy_from_current) qDebug() << "Failed to add a new record"; return; } - createIOGraph(copyIdx.row()); - ui->graphUat->setCurrentIndex(copyIdx); } else { addDefaultGraph(false); @@ -553,22 +682,19 @@ void IOGraphDialog::addGraph(bool copy_from_current) void IOGraphDialog::createIOGraph(int currentRow) { - // XXX - Should IOGraph have it's own list that has to sync with UAT? - ioGraphs_.append(new IOGraph(ui->ioPlot)); + // XXX - Should IOGraph have its own list that has to sync with UAT? + ioGraphs_.insert(currentRow, new IOGraph(ui->ioPlot)); IOGraph* iog = ioGraphs_[currentRow]; - connect(this, SIGNAL(recalcGraphData(capture_file *, bool)), iog, SLOT(recalcGraphData(capture_file *, bool))); - connect(this, SIGNAL(reloadValueUnitFields()), iog, SLOT(reloadValueUnitField())); - connect(&cap_file_, SIGNAL(captureEvent(CaptureEvent)), - iog, SLOT(captureEvent(CaptureEvent))); - connect(iog, SIGNAL(requestRetap()), this, SLOT(scheduleRetap())); - connect(iog, SIGNAL(requestRecalc()), this, SLOT(scheduleRecalc())); - connect(iog, SIGNAL(requestReplot()), this, SLOT(scheduleReplot())); + connect(this, &IOGraphDialog::recalcGraphData, iog, &IOGraph::recalcGraphData); + connect(this, &IOGraphDialog::reloadValueUnitFields, iog, &IOGraph::reloadValueUnitField); + connect(&cap_file_, &CaptureFile::captureEvent, iog, &IOGraph::captureEvent); + connect(iog, &IOGraph::requestRetap, this, [=]() { scheduleRetap(); }); + connect(iog, &IOGraph::requestRecalc, this, [=]() { scheduleRecalc(); }); + connect(iog, &IOGraph::requestReplot, this, [=]() { scheduleReplot(); }); syncGraphSettings(currentRow); - if (iog->visible()) { - scheduleRetap(); - } + iog->setNeedRetap(true); } void IOGraphDialog::addDefaultGraph(bool enabled, int idx) @@ -591,7 +717,7 @@ void IOGraphDialog::addDefaultGraph(bool enabled, int idx) IOGraph::psLine, IOG_ITEM_UNIT_PACKETS, QString(), DEFAULT_MOVING_AVERAGE, DEFAULT_Y_AXIS_FACTOR); break; default: - addGraph(enabled, tr("Access Denied"), "ct.error == \"AccessDenied\"", ColorUtils::graphColor(4), // 4 = red + addGraph(enabled, tr("All Execs"), "evt.type == \"execve\"", ColorUtils::graphColor(4), // 4 = red IOGraph::psDot, IOG_ITEM_UNIT_PACKETS, QString(), DEFAULT_MOVING_AVERAGE, DEFAULT_Y_AXIS_FACTOR); break; } @@ -613,11 +739,10 @@ void IOGraphDialog::syncGraphSettings(int row) { IOGraph *iog = ioGraphs_.value(row, Q_NULLPTR); - if (!uat_model_->index(row, colEnabled).isValid() || !iog) + if (!uat_model_ || !uat_model_->index(row, colEnabled).isValid() || !iog) return; bool visible = graphIsEnabled(row); - bool retap = !iog->visible() && visible; QString data_str; iog->setName(uat_model_->data(uat_model_->index(row, colName)).toString()); @@ -625,7 +750,11 @@ void IOGraphDialog::syncGraphSettings(int row) /* plot style depend on the value unit, so set it first. */ data_str = uat_model_->data(uat_model_->index(row, colYAxis)).toString(); - iog->setValueUnits((int) str_to_val(qUtf8Printable(data_str), y_axis_vs, IOG_ITEM_UNIT_PACKETS)); + if (is_packet_configuration_namespace()) { + iog->setValueUnits((int) str_to_val(qUtf8Printable(data_str), y_axis_packet_vs, IOG_ITEM_UNIT_PACKETS)); + } else { + iog->setValueUnits((int) str_to_val(qUtf8Printable(data_str), y_axis_event_vs, IOG_ITEM_UNIT_PACKETS)); + } iog->setValueUnitField(uat_model_->data(uat_model_->index(row, colYField)).toString()); iog->setColor(uat_model_->data(uat_model_->index(row, colColor), Qt::DecorationRole).value<QColor>().rgb()); @@ -642,7 +771,6 @@ void IOGraphDialog::syncGraphSettings(int row) if (!iog->configError().isEmpty()) { hint_err_ = iog->configError(); visible = false; - retap = false; } else { hint_err_.clear(); } @@ -650,18 +778,18 @@ void IOGraphDialog::syncGraphSettings(int row) iog->setVisible(visible); getGraphInfo(); - mouseMoved(NULL); // Update hint - updateLegend(); + updateHint(); if (visible) { - if (retap) { - scheduleRetap(); - } else { - scheduleReplot(); - } + scheduleReplot(); } } +qsizetype IOGraphDialog::graphCount() const +{ + return uat_model_ ? uat_model_->rowCount() : ioGraphs_.size(); +} + void IOGraphDialog::updateWidgets() { WiresharkDialog::updateWidgets(); @@ -671,9 +799,6 @@ void IOGraphDialog::scheduleReplot(bool now) { need_replot_ = true; if (now) updateStatistics(); - // A plot finished, force an update of the legend now in case a time unit - // was involved (which might append "(ms)" to the label). - updateLegend(); } void IOGraphDialog::scheduleRecalc(bool now) @@ -693,6 +818,27 @@ void IOGraphDialog::reloadFields() emit reloadValueUnitFields(); } +void IOGraphDialog::captureFileClosing() +{ + // The other buttons will be disabled when the model is set to null. + ui->newToolButton->setEnabled(false); + ui->intervalComboBox->setEnabled(false); + copy_profile_bt_->setEnabled(false); + if (uat_model_) { + applyChanges(); + disconnect(uat_model_, nullptr, this, nullptr); + } + // It would be nice to keep the information in the UAT about the graphs + // visible in a read-only state after closing, but if the view is just + // disabled, updating the model from elsewhere (e.g., other dialogs) + // will still change it, so we'd need to copy the information into + // a new model. + uat_model_ = nullptr; + ui->graphUat->setModel(nullptr); + ui->graphUat->setVisible(false); + WiresharkDialog::captureFileClosing(); +} + void IOGraphDialog::keyPressEvent(QKeyEvent *event) { int pan_pixels = event->modifiers() & Qt::ShiftModifier ? 1 : 10; @@ -764,19 +910,25 @@ void IOGraphDialog::keyPressEvent(QKeyEvent *event) QDialog::keyPressEvent(event); } -void IOGraphDialog::reject() +void IOGraphDialog::applyChanges() { - if (!uat_model_) + if (!static_uat_model_) return; // Changes to the I/O Graphs settings are always saved, // there is no possibility for "rejection". QString error; - if (uat_model_->applyChanges(error)) { + if (static_uat_model_->applyChanges(error)) { if (!error.isEmpty()) { report_failure("%s", qPrintable(error)); } } +} + +void IOGraphDialog::reject() +{ + if (uat_model_) + applyChanges(); QDialog::reject(); } @@ -881,12 +1033,12 @@ void IOGraphDialog::toggleTracerStyle(bool force_default) IOGraph *IOGraphDialog::currentActiveGraph() const { QModelIndex index = ui->graphUat->currentIndex(); - if (index.isValid()) { + if (index.isValid() && graphIsEnabled(index.row())) { return ioGraphs_.value(index.row(), NULL); } //if no currently selected item, go with first item enabled - for (int row = 0; row < uat_model_->rowCount(); row++) + for (int row = 0; row < graphCount(); row++) { if (graphIsEnabled(row)) { return ioGraphs_.value(row, NULL); @@ -898,8 +1050,13 @@ IOGraph *IOGraphDialog::currentActiveGraph() const bool IOGraphDialog::graphIsEnabled(int row) const { - Qt::CheckState state = static_cast<Qt::CheckState>(uat_model_->data(uat_model_->index(row, colEnabled), Qt::CheckStateRole).toInt()); - return state == Qt::Checked; + if (uat_model_) { + Qt::CheckState state = static_cast<Qt::CheckState>(uat_model_->data(uat_model_->index(row, colEnabled), Qt::CheckStateRole).toInt()); + return state == Qt::Checked; + } else { + IOGraph* iog = ioGraphs_.value(row, nullptr); + return (iog && iog->visible()); + } } // Scan through our graphs and gather information. @@ -909,7 +1066,7 @@ void IOGraphDialog::getGraphInfo() { base_graph_ = NULL; QCPBars *prev_bars = NULL; - start_time_ = 0.0; + nstime_set_zero(&start_time_); tracer_->setGraph(NULL); IOGraph *selectedGraph = currentActiveGraph(); @@ -931,9 +1088,9 @@ void IOGraphDialog::getGraphInfo() prev_bars = bars; } if (iog->visible() && iog->maxInterval() >= 0) { - double iog_start = iog->startOffset(); - if (start_time_ == 0.0 || iog_start < start_time_) { - start_time_ = iog_start; + nstime_t iog_start = iog->startTime(); + if (nstime_is_zero(&start_time_) || nstime_cmp(&iog_start, &start_time_) < 0) { + nstime_copy(&start_time_, &iog_start); } } @@ -946,9 +1103,78 @@ void IOGraphDialog::getGraphInfo() } } +void IOGraphDialog::updateHint() +{ + QCustomPlot *iop = ui->ioPlot; + QString hint; + + // XXX: ElidedLabel doesn't support rich text / HTML, we + // used to bold this error + if (!hint_err_.isEmpty()) { + hint += QString("%1 ").arg(hint_err_); + } + if (mouse_drags_) { + double ts = 0; + packet_num_ = 0; + int interval_packet = -1; + + if (tracer_->graph()) { + ts = tracer_->position->key(); + if (IOGraph *iog = currentActiveGraph()) { + interval_packet = iog->packetFromTime(ts - nstime_to_sec(&start_time_)); + } + } + + if (interval_packet < 0) { + hint += tr("Hover over the graph for details."); + } else { + QString msg = is_packet_configuration_namespace() ? tr("No packets in interval") : tr("No events in interval"); + QString val; + if (interval_packet > 0) { + packet_num_ = (uint32_t) interval_packet; + if (is_packet_configuration_namespace()) { + msg = QString("%1 %2") + .arg(!file_closed_ ? tr("Click to select packet") : tr("Packet")) + .arg(packet_num_); + } else { + msg = QString("%1 %2") + .arg(!file_closed_ ? tr("Click to select event") : tr("Event")) + .arg(packet_num_); + } + val = " = " + QString::number(tracer_->position->value(), 'g', 4); + } + // XXX - If Time of Day is selected, should we use ISO 8601 + // timestamps or something similar here instead of epoch time? + hint += tr("%1 (%2s%3).") + .arg(msg) + .arg(QString::number(ts, 'f', precision_)) + .arg(val); + } + iop->replot(QCustomPlot::rpQueuedReplot); + } else { + if (rubber_band_ && rubber_band_->isVisible()) { + QRectF zoom_ranges = getZoomRanges(rubber_band_->geometry()); + if (zoom_ranges.width() > 0.0 && zoom_ranges.height() > 0.0) { + hint += tr("Release to zoom, x = %1 to %2, y = %3 to %4") + .arg(zoom_ranges.x()) + .arg(zoom_ranges.x() + zoom_ranges.width()) + .arg(zoom_ranges.y()) + .arg(zoom_ranges.y() + zoom_ranges.height()); + } else { + hint += tr("Unable to select range."); + } + } else { + hint += tr("Click to select a portion of the graph."); + } + } + + ui->hintLabel->setText(hint); +} + void IOGraphDialog::updateLegend() { QCustomPlot *iop = ui->ioPlot; + QSet<format_size_units_e> format_units_set; QSet<QString> vu_label_set; QString intervalText = ui->intervalComboBox->itemText(ui->intervalComboBox->currentIndex()); @@ -956,49 +1182,60 @@ void IOGraphDialog::updateLegend() iop->yAxis->setLabel(QString()); // Find unique labels - if (uat_model_ != NULL) { - for (int row = 0; row < uat_model_->rowCount(); row++) { - IOGraph *iog = ioGraphs_.value(row, Q_NULLPTR); - if (graphIsEnabled(row) && iog) { - QString label(iog->valueUnitLabel()); - if (!iog->scaledValueUnit().isEmpty()) { - label += " (" + iog->scaledValueUnit() + ")"; - } - vu_label_set.insert(label); - } + for (int row = 0; row < graphCount(); row++) { + IOGraph *iog = ioGraphs_.value(row, Q_NULLPTR); + if (graphIsEnabled(row) && iog) { + QString label(iog->valueUnitLabel()); + vu_label_set.insert(label); + format_units_set.insert(iog->formatUnits()); } } // Nothing. if (vu_label_set.size() < 1) { + iop->legend->layer()->replot(); return; } + format_size_units_e format_units = FORMAT_SIZE_UNIT_NONE; + if (format_units_set.size() == 1) { + format_units = format_units_set.values().constFirst(); + } + + QSharedPointer<QCPAxisTickerSi> si_ticker = qSharedPointerDynamicCast<QCPAxisTickerSi>(iop->yAxis->ticker()); + if (format_units != FORMAT_SIZE_UNIT_NONE) { + if (si_ticker) { + si_ticker->setUnit(format_units); + } else { + iop->yAxis->setTicker(QSharedPointer<QCPAxisTickerSi>(new QCPAxisTickerSi(format_units, QString(), ui->logCheckBox->isChecked()))); + } + } else { + if (si_ticker) { + if (ui->logCheckBox->isChecked()) { + iop->yAxis->setTicker(QSharedPointer<QCPAxisTickerLog>(new QCPAxisTickerLog)); + } else { + iop->yAxis->setTicker(QSharedPointer<QCPAxisTicker>(new QCPAxisTicker)); + } + } + } + // All the same. Use the Y Axis label. if (vu_label_set.size() == 1) { - iop->yAxis->setLabel(vu_label_set.values()[0] + "/" + intervalText); - return; + iop->yAxis->setLabel(vu_label_set.values().constFirst() + "/" + intervalText); } - // Differing labels. Create a legend with a Title label at top. + // Create a legend with a Title label at top. // Legend Title thanks to: https://www.qcustomplot.com/index.php/support/forum/443 - QCPTextElement* legendTitle = qobject_cast<QCPTextElement*>(iop->legend->elementAt(0)); - if (legendTitle == NULL) { - legendTitle = new QCPTextElement(iop, QString("")); - iop->legend->insertRow(0); - iop->legend->addElement(0, 0, legendTitle); - } - legendTitle->setText(QString(intervalText + " Intervals ")); - - if (uat_model_ != NULL) { - for (int row = 0; row < uat_model_->rowCount(); row++) { - IOGraph *iog = ioGraphs_.value(row, Q_NULLPTR); - if (iog) { - if (graphIsEnabled(row)) { - iog->addToLegend(); - } else { - iog->removeFromLegend(); - } + iop->legend->clearItems(); + QCPStringLegendItem *legendTitle = new QCPStringLegendItem(iop->legend, QString(tr("%1 Intervals ").arg(intervalText))); + iop->legend->insertRow(0); + iop->legend->addElement(0, 0, legendTitle); + + for (int row = 0; row < graphCount(); row++) { + IOGraph *iog = ioGraphs_.value(row, Q_NULLPTR); + if (iog) { + if (graphIsEnabled(row)) { + iog->addToLegend(); } } } @@ -1037,19 +1274,37 @@ QRectF IOGraphDialog::getZoomRanges(QRect zoom_rect) return zoom_ranges; } +void IOGraphDialog::showContextMenu(const QPoint &pos) +{ + if (ui->ioPlot->legend->selectTest(pos, false) >= 0) { + QMenu *menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) + menu->addAction(tr("Move to top left"), this, &IOGraphDialog::moveLegend)->setData((Qt::AlignTop|Qt::AlignLeft).toInt()); + menu->addAction(tr("Move to top center"), this, &IOGraphDialog::moveLegend)->setData((Qt::AlignTop|Qt::AlignHCenter).toInt()); + menu->addAction(tr("Move to top right"), this, &IOGraphDialog::moveLegend)->setData((Qt::AlignTop|Qt::AlignRight).toInt()); + menu->addAction(tr("Move to bottom left"), this, &IOGraphDialog::moveLegend)->setData((Qt::AlignBottom|Qt::AlignLeft).toInt()); + menu->addAction(tr("Move to bottom center"), this, &IOGraphDialog::moveLegend)->setData((Qt::AlignBottom|Qt::AlignHCenter).toInt()); + menu->addAction(tr("Move to bottom right"), this, &IOGraphDialog::moveLegend)->setData((Qt::AlignBottom|Qt::AlignRight).toInt()); +#else + menu->addAction(tr("Move to top left"), this, &IOGraphDialog::moveLegend)->setData(static_cast<Qt::Alignment::Int>(Qt::AlignTop|Qt::AlignLeft)); + menu->addAction(tr("Move to top center"), this, &IOGraphDialog::moveLegend)->setData(static_cast<Qt::Alignment::Int>(Qt::AlignTop|Qt::AlignHCenter)); + menu->addAction(tr("Move to top right"), this, &IOGraphDialog::moveLegend)->setData(static_cast<Qt::Alignment::Int>(Qt::AlignTop|Qt::AlignRight)); + menu->addAction(tr("Move to bottom left"), this, &IOGraphDialog::moveLegend)->setData(static_cast<Qt::Alignment::Int>(Qt::AlignBottom|Qt::AlignLeft)); + menu->addAction(tr("Move to bottom center"), this, &IOGraphDialog::moveLegend)->setData(static_cast<Qt::Alignment::Int>(Qt::AlignBottom|Qt::AlignHCenter)); + menu->addAction(tr("Move to bottom right"), this, &IOGraphDialog::moveLegend)->setData(static_cast<Qt::Alignment::Int>(Qt::AlignBottom|Qt::AlignRight)); +#endif + menu->popup(ui->ioPlot->mapToGlobal(pos)); + } else { + ctx_menu_.popup(ui->ioPlot->mapToGlobal(pos)); + } +} + void IOGraphDialog::graphClicked(QMouseEvent *event) { QCustomPlot *iop = ui->ioPlot; - if (event->button() == Qt::RightButton) { - // XXX We should find some way to get ioPlot to handle a - // contextMenuEvent instead. -#if QT_VERSION >= QT_VERSION_CHECK(6, 0 ,0) - ctx_menu_.popup(event->globalPosition().toPoint()); -#else - ctx_menu_.popup(event->globalPos()); -#endif - } else if (mouse_drags_) { + if (mouse_drags_) { if (iop->axisRect()->rect().contains(event->pos())) { iop->setCursor(QCursor(Qt::ClosedHandCursor)); } @@ -1068,87 +1323,35 @@ void IOGraphDialog::graphClicked(QMouseEvent *event) void IOGraphDialog::mouseMoved(QMouseEvent *event) { QCustomPlot *iop = ui->ioPlot; - QString hint; Qt::CursorShape shape = Qt::ArrowCursor; - // XXX: ElidedLabel doesn't support rich text / HTML, we - // used to bold this error - if (!hint_err_.isEmpty()) { - hint += QString("%1 ").arg(hint_err_); - } - if (event) { - if (event->buttons().testFlag(Qt::LeftButton)) { - if (mouse_drags_) { - shape = Qt::ClosedHandCursor; - } else { - shape = Qt::CrossCursor; - } - } else if (iop->axisRect()->rect().contains(event->pos())) { - if (mouse_drags_) { - shape = Qt::OpenHandCursor; - } else { - shape = Qt::CrossCursor; - } + if (event->buttons().testFlag(Qt::LeftButton)) { + if (mouse_drags_) { + shape = Qt::ClosedHandCursor; + } else { + shape = Qt::CrossCursor; + } + } else if (iop->axisRect()->rect().contains(event->pos())) { + if (mouse_drags_) { + shape = Qt::OpenHandCursor; + } else { + shape = Qt::CrossCursor; } - iop->setCursor(QCursor(shape)); } + iop->setCursor(QCursor(shape)); if (mouse_drags_) { - double ts = 0; - packet_num_ = 0; - int interval_packet = -1; - - if (event && tracer_->graph()) { + if (tracer_->graph()) { tracer_->setGraphKey(iop->xAxis->pixelToCoord(event->pos().x())); - ts = tracer_->position->key(); - if (IOGraph *iog = currentActiveGraph()) { - interval_packet = iog->packetFromTime(ts - start_time_); - } } - if (interval_packet < 0) { - hint += tr("Hover over the graph for details."); - } else { - QString msg = is_packet_configuration_namespace() ? tr("No packets in interval") : tr("No events in interval"); - QString val; - if (interval_packet > 0) { - packet_num_ = (guint32) interval_packet; - if (is_packet_configuration_namespace()) { - msg = QString("%1 %2") - .arg(!file_closed_ ? tr("Click to select packet") : tr("Packet")) - .arg(packet_num_); - } else { - msg = QString("%1 %2") - .arg(!file_closed_ ? tr("Click to select event") : tr("Event")) - .arg(packet_num_); - } - val = " = " + QString::number(tracer_->position->value(), 'g', 4); - } - hint += tr("%1 (%2s%3).") - .arg(msg) - .arg(QString::number(ts, 'g', 4)) - .arg(val); - } - iop->replot(); } else { - if (event && rubber_band_ && rubber_band_->isVisible()) { + if (rubber_band_ && rubber_band_->isVisible()) { rubber_band_->setGeometry(QRect(rb_origin_, event->pos()).normalized()); - QRectF zoom_ranges = getZoomRanges(QRect(rb_origin_, event->pos())); - if (zoom_ranges.width() > 0.0 && zoom_ranges.height() > 0.0) { - hint += tr("Release to zoom, x = %1 to %2, y = %3 to %4") - .arg(zoom_ranges.x()) - .arg(zoom_ranges.x() + zoom_ranges.width()) - .arg(zoom_ranges.y()) - .arg(zoom_ranges.y() + zoom_ranges.height()); - } else { - hint += tr("Unable to select range."); - } - } else { - hint += tr("Click to select a portion of the graph."); } } - ui->hintLabel->setText(hint); + updateHint(); } void IOGraphDialog::mouseReleased(QMouseEvent *event) @@ -1172,51 +1375,99 @@ void IOGraphDialog::mouseReleased(QMouseEvent *event) } } +void IOGraphDialog::moveLegend() +{ + if (QAction *contextAction = qobject_cast<QAction*>(sender())) { + if (contextAction->data().canConvert<Qt::Alignment::Int>()) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) + ui->ioPlot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::Alignment::fromInt(contextAction->data().value<Qt::Alignment::Int>())); +#else + ui->ioPlot->axisRect()->insetLayout()->setInsetAlignment(0, static_cast<Qt::Alignment>(contextAction->data().value<Qt::Alignment::Int>())); +#endif + ui->ioPlot->replot(); + } + } +} + void IOGraphDialog::resetAxes() { QCustomPlot *iop = ui->ioPlot; - QCPRange x_range = iop->xAxis->scaleType() == QCPAxis::stLogarithmic ? - iop->xAxis->range().sanitizedForLogScale() : iop->xAxis->range(); - double pixel_pad = 10.0; // per side iop->rescaleAxes(true); + QCPRange x_range = iop->xAxis->scaleType() == QCPAxis::stLogarithmic ? + iop->xAxis->range().sanitizedForLogScale() : iop->xAxis->range(); double axis_pixels = iop->xAxis->axisRect()->width(); iop->xAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, x_range.center()); + QCPRange y_range = iop->yAxis->scaleType() == QCPAxis::stLogarithmic ? + iop->yAxis->range().sanitizedForLogScale() : iop->yAxis->range(); axis_pixels = iop->yAxis->axisRect()->height(); - iop->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, iop->yAxis->range().center()); + iop->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, y_range.center()); auto_axes_ = true; iop->replot(); } +void IOGraphDialog::selectedFrameChanged(QList<int> frames) +{ + if (frames.count() == 1 && cap_file_.isValid() && !file_closed_ && tracer_->graph() && cap_file_.packetInfo() != nullptr) { + packet_info *pinfo = cap_file_.packetInfo(); + if (pinfo->num != packet_num_) { + // This prevents being triggered by the IOG's own GoToPacketAction, + // although that is mostly harmless. + int interval = ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt(); + + /* + * setGraphKey (with Interpolation false, as it is by default) + * finds the nearest point to the key. Our buckets are derived + * from rounding down (XXX - which is appropriate for relative + * time but less so when absolute time of day is selected.) + * We could call get_io_graph_index() and then multiply to get + * the exact ts for the bucket, but it's fewer math operations + * operations simply to subtract half the interval. + * XXX - Getting the exact value would be superior if we wished + * to avoid doing anything in the case that the tracer is + * already pointing at the correct bucket. (Is the hint always + * correct in that case?) + */ +#if 0 + int64_t idx = get_io_graph_index(pinfo, interval); + double ts = (double)idx * interval / SCALE_F + nstime_to_sec(&start_time); +#endif + double key = nstime_to_sec(&pinfo->rel_ts) - (interval / (2 * SCALE_F)) + nstime_to_sec(&start_time_); + tracer_->setGraphKey(key); + ui->ioPlot->replot(); + updateHint(); + } + } +} + void IOGraphDialog::updateStatistics() { if (!isVisible()) return; - if (need_retap_ && !file_closed_ && prefs.gui_io_graph_automatic_update) { + /* XXX - If we're currently retapping, what we really want to do is + * abort the current tap and start over. process_specified_records() + * in file.c doesn't let us do that, because it doesn't know whether + * it's holding cf->read_lock for something that could be restarted + * (like tapping or dissection) or something that needs to run to + * completion (saving, printing.) + * + * So we wait and see if we're no longer tapping the next check. + */ + if (need_retap_ && !file_closed_ && !retapDepth() && prefs.gui_io_graph_automatic_update) { need_retap_ = false; - cap_file_.retapPackets(); + QTimer::singleShot(0, &cap_file_, &CaptureFile::retapPackets); // The user might have closed the window while tapping, which means // we might no longer exist. } else { if (need_recalc_ && !file_closed_ && prefs.gui_io_graph_automatic_update) { need_recalc_ = false; need_replot_ = true; - int enabled_graphs = 0; - if (uat_model_ != NULL) { - for (int row = 0; row < uat_model_->rowCount(); row++) { - if (graphIsEnabled(row)) { - ++enabled_graphs; - } - } - } - // With multiple visible graphs, disable Y scaling to avoid - // multiple, distinct units. - emit recalcGraphData(cap_file_.capFile(), enabled_graphs == 1); + emit recalcGraphData(cap_file_.capFile()); if (!tracer_->graph()) { if (base_graph_ && base_graph_->data()->size() > 0) { tracer_->setGraph(base_graph_); @@ -1239,11 +1490,15 @@ void IOGraphDialog::updateStatistics() void IOGraphDialog::loadProfileGraphs() { if (iog_uat_ == NULL) { + uat_field_t *io_graph_fields = io_graph_packet_fields; + if (!is_packet_configuration_namespace()) { + io_graph_fields = io_graph_event_fields; + } iog_uat_ = uat_new("I/O Graphs", sizeof(io_graph_settings_t), "io_graphs", - TRUE, + true, &iog_settings_, &num_io_graphs_, 0, /* doesn't affect anything that requires a GUI update */ @@ -1251,7 +1506,7 @@ void IOGraphDialog::loadProfileGraphs() io_graph_copy_cb, NULL, io_graph_free_cb, - NULL, + io_graph_post_update_cb, NULL, io_graph_fields); @@ -1263,16 +1518,26 @@ void IOGraphDialog::loadProfileGraphs() g_free(err); uat_clear(iog_uat_); } + + static_uat_model_ = new UatModel(mainApp, iog_uat_); + connect(mainApp, &MainApplication::profileChanging, IOGraphDialog::applyChanges); } - uat_model_ = new UatModel(NULL, iog_uat_); - uat_delegate_ = new UatDelegate; + uat_model_ = static_uat_model_; + uat_delegate_ = new UatDelegate(ui->graphUat); ui->graphUat->setModel(uat_model_); ui->graphUat->setItemDelegate(uat_delegate_); + ui->graphUat->setSelectionMode(QAbstractItemView::ContiguousSelection); + + ui->graphUat->setHeader(new ResizeHeaderView(Qt::Horizontal, ui->graphUat)); - connect(uat_model_, SIGNAL(dataChanged(QModelIndex,QModelIndex)), - this, SLOT(modelDataChanged(QModelIndex))); - connect(uat_model_, SIGNAL(modelReset()), this, SLOT(modelRowsReset())); + connect(uat_model_, &UatModel::dataChanged, this, &IOGraphDialog::modelDataChanged); + connect(uat_model_, &UatModel::modelReset, this, &IOGraphDialog::modelRowsReset); + connect(uat_model_, &UatModel::rowsInserted, this, &IOGraphDialog::modelRowsInserted); + connect(uat_model_, &UatModel::rowsRemoved, this, &IOGraphDialog::modelRowsRemoved); + connect(uat_model_, &UatModel::rowsMoved, this, &IOGraphDialog::modelRowsMoved); + + connect(ui->graphUat->selectionModel(), &QItemSelectionModel::selectionChanged, this, &IOGraphDialog::graphUatSelectionChanged); } // Slots @@ -1282,6 +1547,22 @@ void IOGraphDialog::on_intervalComboBox_currentIndexChanged(int) int interval = ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt(); bool need_retap = false; + precision_ = ceil(log10(SCALE_F / interval)); + if (precision_ < 0) { + precision_ = 0; + } + + // XXX - This is the default QCP date time format, but adding fractional + // seconds when our interval is small. Should we make it something else, + // like ISO 8601 (but still with a line break between time and date)? + // Note this is local time, with no time zone offset displayed. Should + // it be in UTC? (call setDateTimeSpec()) + if (precision_) { + datetime_ticker_->setDateTimeFormat("hh:mm:ss.z\ndd.MM.yy"); + } else { + datetime_ticker_->setDateTimeFormat("hh:mm:ss\ndd.MM.yy"); + } + if (uat_model_ != NULL) { for (int row = 0; row < uat_model_->rowCount(); row++) { IOGraph *iog = ioGraphs_.value(row, NULL); @@ -1289,6 +1570,8 @@ void IOGraphDialog::on_intervalComboBox_currentIndexChanged(int) iog->setInterval(interval); if (iog->visible()) { need_retap = true; + } else { + iog->setNeedRetap(true); } } } @@ -1297,13 +1580,12 @@ void IOGraphDialog::on_intervalComboBox_currentIndexChanged(int) if (need_retap) { scheduleRetap(true); } - - updateLegend(); } void IOGraphDialog::on_todCheckBox_toggled(bool checked) { - double orig_start = start_time_; + nstime_t orig_start; + nstime_copy(&orig_start, &start_time_); bool orig_auto = auto_axes_; if (checked) { @@ -1315,46 +1597,135 @@ void IOGraphDialog::on_todCheckBox_toggled(bool checked) scheduleRecalc(true); auto_axes_ = orig_auto; getGraphInfo(); - ui->ioPlot->xAxis->moveRange(start_time_ - orig_start); - mouseMoved(NULL); // Update hint + nstime_delta(&orig_start, &start_time_, &orig_start); + ui->ioPlot->xAxis->moveRange(nstime_to_sec(&orig_start)); + updateHint(); } void IOGraphDialog::modelRowsReset() { + foreach(IOGraph* iog, ioGraphs_) { + delete iog; + } + ioGraphs_.clear(); + + for (int i = 0; i < uat_model_->rowCount(); i++) { + createIOGraph(i); + } ui->deleteToolButton->setEnabled(false); ui->copyToolButton->setEnabled(false); ui->clearToolButton->setEnabled(uat_model_->rowCount() != 0); } -void IOGraphDialog::on_graphUat_currentItemChanged(const QModelIndex ¤t, const QModelIndex&) +void IOGraphDialog::modelRowsInserted(const QModelIndex &, int first, int last) { - if (current.isValid()) { + // first to last is inclusive + for (int i = first; i <= last; i++) { + createIOGraph(i); + } +} + +void IOGraphDialog::modelRowsRemoved(const QModelIndex &, int first, int last) +{ + // first to last is inclusive + for (int i = last; i >= first; i--) { + IOGraph *iog = ioGraphs_.takeAt(i); + delete iog; + } +} + +void IOGraphDialog::modelRowsMoved(const QModelIndex &source, int sourceStart, int sourceEnd, const QModelIndex &dest, int destinationRow) +{ + // The source and destination parent are always the same for UatModel. + ws_assert(source == dest); + // Either destinationRow < sourceStart, or destinationRow > sourceEnd. + // When moving rows down the same parent, the rows are placed _before_ + // destinationRow, otherwise it's the row to which items are moved. + if (destinationRow < sourceStart) { + for (int i = 0; i <= sourceEnd - sourceStart; i++) { + // When moving up the same parent, moving an earlier + // item doesn't change the row. + ioGraphs_.move(sourceStart + i, destinationRow + i); + } + } else { + for (int i = 0; i <= sourceEnd - sourceStart; i++) { + // When moving down the same parent, moving an earlier + // item means the next items move up (so all the moved + // rows are always at sourceStart.) + ioGraphs_.move(sourceStart, destinationRow - 1); + } + } + + // setting a QCPLayerable to its current layer moves it to the end + // as though it were the last added. Do that for all the plottables + // starting with the first one that changed, so that the graphs appear + // as though they were added in the current order. + // (moveToLayer() is the same thing but with a parameter to prepend + // instead, which would be faster if we're in the top half of the + // list, except that's a protected function. There's no function + // to swap layerables in a layer.) + IOGraph *iog; + for (int row = qMin(sourceStart, destinationRow); row < uat_model_->rowCount(); row++) { + iog = ioGraphs_.at(row); + if (iog->graph()) { + iog->graph()->setLayer(iog->graph()->layer()); + } else if (iog->bars()) { + iog->bars()->setLayer(iog->bars()->layer()); + } + } + ui->ioPlot->replot(); +} + +void IOGraphDialog::graphUatSelectionChanged(const QItemSelection&, const QItemSelection&) +{ + QModelIndexList selectedRows = ui->graphUat->selectionModel()->selectedRows(); + qsizetype num_selected = selectedRows.size(); + if (num_selected > 0) { + std::sort(selectedRows.begin(), selectedRows.end()); ui->deleteToolButton->setEnabled(true); ui->copyToolButton->setEnabled(true); - ui->clearToolButton->setEnabled(true); - ui->moveUpwardsToolButton->setEnabled(true); - ui->moveDownwardsToolButton->setEnabled(true); + ui->moveUpwardsToolButton->setEnabled(selectedRows.first().row() > 0); + ui->moveDownwardsToolButton->setEnabled(selectedRows.last().row() < uat_model_->rowCount() - 1); } else { ui->deleteToolButton->setEnabled(false); ui->copyToolButton->setEnabled(false); - ui->clearToolButton->setEnabled(false); ui->moveUpwardsToolButton->setEnabled(false); ui->moveDownwardsToolButton->setEnabled(false); } } -void IOGraphDialog::modelDataChanged(const QModelIndex &index) +void IOGraphDialog::on_graphUat_currentItemChanged(const QModelIndex ¤t, const QModelIndex&) +{ + if (current.isValid()) { + ui->clearToolButton->setEnabled(true); + if (graphIsEnabled(current.row())) { + // Try to set the tracer to the new current graph. + // If it's not enabled, don't try to switch from the + // old graph to the one in the first row. + getGraphInfo(); + } + } else { + ui->clearToolButton->setEnabled(false); + } +} + +void IOGraphDialog::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &) { bool recalc = false; - switch (index.column()) - { - case colYAxis: - case colSMAPeriod: - recalc = true; + for (int col = topLeft.column(); col <= bottomRight.column(); col++) { + switch (col) + { + case colYAxis: + case colSMAPeriod: + case colYAxisFactor: + recalc = true; + } } - syncGraphSettings(index.row()); + for (int row = topLeft.row(); row <= bottomRight.row(); row++) { + syncGraphSettings(row); + } if (recalc) { scheduleRecalc(true); @@ -1363,11 +1734,6 @@ void IOGraphDialog::modelDataChanged(const QModelIndex &index) } } -void IOGraphDialog::on_resetButton_clicked() -{ - resetAxes(); -} - void IOGraphDialog::on_newToolButton_clicked() { addGraph(); @@ -1375,70 +1741,95 @@ void IOGraphDialog::on_newToolButton_clicked() void IOGraphDialog::on_deleteToolButton_clicked() { - const QModelIndex ¤t = ui->graphUat->currentIndex(); - if (uat_model_ && current.isValid()) { - delete ioGraphs_[current.row()]; - ioGraphs_.remove(current.row()); + if (uat_model_ == nullptr) { + return; + } - if (!uat_model_->removeRows(current.row(), 1)) { - qDebug() << "Failed to remove row"; + for (const auto &range : ui->graphUat->selectionModel()->selection()) { + // Each QItemSelectionRange is contiguous + if (!range.isEmpty()) { + if (!uat_model_->removeRows(range.top(), range.bottom() - range.top() + 1)) { + qDebug() << "Failed to remove rows" << range.top() << "to" << range.bottom(); + } } } // We should probably be smarter about this. hint_err_.clear(); - mouseMoved(NULL); + updateHint(); } void IOGraphDialog::on_copyToolButton_clicked() { - addGraph(true); + if (uat_model_ == nullptr) { + return; + } + + QModelIndexList selectedRows = ui->graphUat->selectionModel()->selectedRows(); + if (selectedRows.size() > 0) { + std::sort(selectedRows.begin(), selectedRows.end()); + + QModelIndex copyIdx; + + for (const auto &idx : selectedRows) { + copyIdx = uat_model_->copyRow(idx); + if (!copyIdx.isValid()) + { + qDebug() << "Failed to copy row" << idx.row(); + } + } + ui->graphUat->setCurrentIndex(copyIdx); + } } void IOGraphDialog::on_clearToolButton_clicked() { if (uat_model_) { - foreach(IOGraph* iog, ioGraphs_) { - delete iog; - } - ioGraphs_.clear(); uat_model_->clearAll(); } hint_err_.clear(); - mouseMoved(NULL); + updateHint(); } void IOGraphDialog::on_moveUpwardsToolButton_clicked() { - const QModelIndex& current = ui->graphUat->currentIndex(); - if (uat_model_ && current.isValid()) { - - int current_row = current.row(); - if (current_row > 0){ - // Swap current row with the one above - IOGraph* temp = ioGraphs_[current_row - 1]; - ioGraphs_[current_row - 1] = ioGraphs_[current_row]; - ioGraphs_[current_row] = temp; + if (uat_model_ == nullptr) { + return; + } - uat_model_->moveRow(current_row, current_row - 1); + for (const auto &range : ui->graphUat->selectionModel()->selection()) { + // Each QItemSelectionRange is contiguous + if (!range.isEmpty() && range.top() > 0) { + // Swap range of rows with the row above the top + if (! uat_model_->moveRows(QModelIndex(), range.top(), range.bottom() - range.top() + 1, QModelIndex(), range.top() - 1)) { + qDebug() << "Failed to move up rows" << range.top() << "to" << range.bottom(); + } + // Our moveRows implementation calls begin/endMoveRows(), so + // range.top() already has the new row number. + ui->moveUpwardsToolButton->setEnabled(range.top() > 0); + ui->moveDownwardsToolButton->setEnabled(true); } } } void IOGraphDialog::on_moveDownwardsToolButton_clicked() { - const QModelIndex& current = ui->graphUat->currentIndex(); - if (uat_model_ && current.isValid()) { - - int current_row = current.row(); - if (current_row < uat_model_->rowCount() - 1) { - // Swap current row with the one below - IOGraph* temp = ioGraphs_[current_row + 1]; - ioGraphs_[current_row + 1] = ioGraphs_[current_row]; - ioGraphs_[current_row] = temp; + if (uat_model_ == nullptr) { + return; + } - uat_model_->moveRow(current_row, current_row + 1); + for (const auto &range : ui->graphUat->selectionModel()->selection()) { + // Each QItemSelectionRange is contiguous + if (!range.isEmpty() && range.bottom() + 1 < uat_model_->rowCount()) { + // Swap range of rows with the row below the top + if (! uat_model_->moveRows(QModelIndex(), range.top(), range.bottom() - range.top() + 1, QModelIndex(), range.bottom() + 1)) { + qDebug() << "Failed to move down rows" << range.top() << "to" << range.bottom(); + } + // Our moveRows implementation calls begin/endMoveRows, so + // range.bottom() already has the new row number. + ui->moveUpwardsToolButton->setEnabled(true); + ui->moveDownwardsToolButton->setEnabled(range.bottom() < uat_model_->rowCount() - 1); } } } @@ -1461,14 +1852,28 @@ void IOGraphDialog::on_zoomRadioButton_toggled(bool checked) void IOGraphDialog::on_logCheckBox_toggled(bool checked) { QCustomPlot *iop = ui->ioPlot; + QSharedPointer<QCPAxisTickerSi> si_ticker = qSharedPointerDynamicCast<QCPAxisTickerSi>(iop->yAxis->ticker()); + if (si_ticker != nullptr) { + si_ticker->setLog(checked); + } - iop->yAxis->setScaleType(checked ? QCPAxis::stLogarithmic : QCPAxis::stLinear); + if (checked) { + iop->yAxis->setScaleType(QCPAxis::stLogarithmic); + if (si_ticker == nullptr) { + iop->yAxis->setTicker(QSharedPointer<QCPAxisTickerLog>(new QCPAxisTickerLog)); + } + } else { + iop->yAxis->setScaleType(QCPAxis::stLinear); + if (si_ticker == nullptr) { + iop->yAxis->setTicker(QSharedPointer<QCPAxisTicker>(new QCPAxisTicker)); + } + } iop->replot(); } void IOGraphDialog::on_automaticUpdateCheckBox_toggled(bool checked) { - prefs.gui_io_graph_automatic_update = checked ? TRUE : FALSE; + prefs.gui_io_graph_automatic_update = checked ? true : false; prefs_main_write(); @@ -1480,16 +1885,16 @@ void IOGraphDialog::on_automaticUpdateCheckBox_toggled(bool checked) void IOGraphDialog::on_enableLegendCheckBox_toggled(bool checked) { - prefs.gui_io_graph_enable_legend = checked ? TRUE : FALSE; + prefs.gui_io_graph_enable_legend = checked ? true : false; prefs_main_write(); - updateLegend(); + ui->ioPlot->legend->layer()->replot(); } void IOGraphDialog::on_actionReset_triggered() { - on_resetButton_clicked(); + resetAxes(); } void IOGraphDialog::on_actionZoomIn_triggered() @@ -1585,7 +1990,7 @@ void IOGraphDialog::on_actionToggleTimeOrigin_triggered() void IOGraphDialog::on_actionCrosshairs_triggered() { - + toggleTracerStyle(); } void IOGraphDialog::on_buttonBox_helpRequested() @@ -1638,6 +2043,17 @@ void IOGraphDialog::on_buttonBox_accepted() } } +void IOGraphDialog::buttonBoxClicked(QAbstractButton *button) +{ + switch (ui->buttonBox->buttonRole(button)) { + case QDialogButtonBox::ResetRole: + resetAxes(); + break; + default: + break; + } +} + void IOGraphDialog::makeCsv(QTextStream &stream) const { QList<IOGraph *> activeGraphs; @@ -1646,25 +2062,47 @@ void IOGraphDialog::makeCsv(QTextStream &stream) const int max_interval = 0; stream << "\"Interval start\""; - if (uat_model_ != NULL) { - for (int row = 0; row < uat_model_->rowCount(); row++) { - if (graphIsEnabled(row) && ioGraphs_[row] != NULL) { - activeGraphs.append(ioGraphs_[row]); - if (max_interval < ioGraphs_[row]->maxInterval()) { - max_interval = ioGraphs_[row]->maxInterval(); - } - QString name = ioGraphs_[row]->name().toUtf8(); - name = QString("\"%1\"").arg(name.replace("\"", "\"\"")); // RFC 4180 - stream << "," << name; + for (int row = 0; row < graphCount(); row++) { + if (graphIsEnabled(row) && ioGraphs_[row] != NULL) { + activeGraphs.append(ioGraphs_[row]); + if (max_interval < ioGraphs_[row]->maxInterval()) { + max_interval = ioGraphs_[row]->maxInterval(); } + QString name = ioGraphs_[row]->name().toUtf8(); + name = QString("\"%1\"").arg(name.replace("\"", "\"\"")); // RFC 4180 + stream << "," << name; } } stream << '\n'; for (int interval = 0; interval <= max_interval; interval++) { - double interval_start = (double)interval * ((double)ui_interval / 1000.0); - stream << interval_start; + int64_t interval_start = (int64_t)interval * ui_interval; + if (qSharedPointerDynamicCast<QCPAxisTickerDateTime>(ui->ioPlot->xAxis->ticker()) != nullptr) { + nstime_t interval_time = NSTIME_INIT_SECS_USECS((time_t)(interval_start / SCALE), (int)(interval_start % SCALE)); + + nstime_add(&interval_time, &start_time_); + + static char time_string_buf[39]; + + if (decimal_point == nullptr) { + decimal_point = g_strdup(localeconv()->decimal_point); + } + // Should we convert to UTC for output, even if the graph axis has + // local time? + // The question of what precision to use is somewhat tricky. + // The buckets are aligned to the relative time start, not to + // absolute time, so the timestamp precision should be used instead + // of the bucket precision. We can save the precision of the + // start time timestamp for each graph, but we don't necessarily + // have a guarantee that all timestamps in the file have the same + // precision. Possibly nstime_t should store precision, cf. #15579 + format_nstime_as_iso8601(time_string_buf, sizeof time_string_buf, &interval_time, decimal_point, true, 9); // precision_); + + stream << time_string_buf; + } else { + stream << (double)interval_start / SCALE_F; + } foreach (IOGraph *iog, activeGraphs) { double value = 0.0; if (interval <= iog->maxInterval()) { @@ -1698,11 +2136,14 @@ bool IOGraphDialog::saveCsv(const QString &file_name) const IOGraph::IOGraph(QCustomPlot *parent) : parent_(parent), + tap_registered_(true), visible_(false), graph_(NULL), bars_(NULL), val_units_(IOG_ITEM_UNIT_FIRST), hf_index_(-1), + interval_(0), + start_time_(NSTIME_INIT_ZERO), cur_idx_(-1) { Q_ASSERT(parent_ != NULL); @@ -1723,11 +2164,12 @@ IOGraph::IOGraph(QCustomPlot *parent) : // error_string->str); // config_err_ = error_string->str; g_string_free(error_string, TRUE); + tap_registered_ = false; } } IOGraph::~IOGraph() { - remove_tap_listener(this); + removeTapListener(); if (graph_) { parent_->removeGraph(graph_); } @@ -1736,9 +2178,17 @@ IOGraph::~IOGraph() { } } +void IOGraph::removeTapListener() +{ + if (tap_registered_) { + remove_tap_listener(this); + tap_registered_ = false; + } +} + // Construct a full filter string from the display filter and value unit / Y axis. -// Check for errors and sets config_err_ if any are found. -void IOGraph::setFilter(const QString &filter) +// Check for errors and sets config_err_ and returns false if any are found. +bool IOGraph::setFilter(const QString &filter) { GString *error_string; QString full_filter(filter.trimmed()); @@ -1756,7 +2206,7 @@ void IOGraph::setFilter(const QString &filter) config_err_ = QString::fromUtf8(df_err->msg); df_error_free(&df_err); filter_ = full_filter; - return; + return false; } } @@ -1765,7 +2215,7 @@ void IOGraph::setFilter(const QString &filter) if (error_string) { config_err_ = error_string->str; g_string_free(error_string, TRUE); - return; + return false; } // Make sure vu_field_ survives edt tree pruning by adding it to our filter @@ -1778,17 +2228,36 @@ void IOGraph::setFilter(const QString &filter) } } - error_string = set_tap_dfilter(this, full_filter.toUtf8().constData()); - if (error_string) { - config_err_ = error_string->str; - g_string_free(error_string, TRUE); - return; - } else { - if (filter_.compare(filter) && visible_) { - emit requestRetap(); + if (full_filter_.compare(full_filter)) { + error_string = set_tap_dfilter(this, full_filter.toUtf8().constData()); + if (error_string) { + config_err_ = error_string->str; + g_string_free(error_string, TRUE); + return false; } + filter_ = filter; + full_filter_ = full_filter; + /* If we changed the tap filter the graph is visible, we need to + * retap. + * Note that setting the tap dfilter will mark the tap as needing a + * redraw, which will cause a recalculation (via tapDraw) via the + * (fairly long) main application timer. + */ + /* XXX - When changing from an advanced graph to one that doesn't + * use the field, we don't actually need to retap if filter and + * full_filter produce the same results. (We do have to retap + * regardless if changing _to_ an advanced graph, because the + * extra fields in the io_graph_item_t aren't filled in from the + * edt for the basic graph.) + * Checking that in full generality would require more optimization + * in the dfilter engine plus functions to compare filters, but + * we could test the simple case where filter and vu_field are + * the same string. + */ + setNeedRetap(true); } + return true; } void IOGraph::applyCurrentColor() @@ -1796,7 +2265,15 @@ void IOGraph::applyCurrentColor() if (graph_) { graph_->setPen(QPen(color_, graph_line_width_)); } else if (bars_) { - bars_->setPen(QPen(QBrush(ColorUtils::graphColor(0)), graph_line_width_)); // ...or omit it altogether? + bars_->setPen(QPen(color_.color().darker(110), graph_line_width_)); + // ...or omit it altogether? + // bars_->setPen(QPen(color_); + // XXX - We should do something like + // bars_->setPen(QPen(ColorUtils::alphaBlend(color_, palette().windowText(), 0.65)); + // to get a darker outline in light mode and a lighter outline in dark + // mode, but we don't yet respect dark mode in IOGraph (or anything + // that uses QCustomPlot) - see link below for how to set QCP colors: + // https://www.qcustomplot.com/index.php/demos/barchartdemo bars_->setBrush(color_); } } @@ -1812,7 +2289,28 @@ void IOGraph::setVisible(bool visible) bars_->setVisible(visible_); } if (old_visibility != visible_) { - emit requestReplot(); + if (visible_ && need_retap_) { + need_retap_ = false; + emit requestRetap(); + } else { + // XXX - If the number of enabled graphs changed to or from 1, we + // need to recalculate to possibly change the rescaling. (This is + // why QCP recommends doing scaling in the axis ticker instead.) + // If we can't determine the number of enabled graphs here, always + // request a recalculation instead of a replot. (At least until we + // change the scaling to be done in the ticker.) + //emit requestReplot(); + emit requestRecalc(); + } + } +} + +void IOGraph::setNeedRetap(bool retap) +{ + if (visible_ && retap) { + emit requestRetap(); + } else { + need_retap_ = retap; } } @@ -1827,7 +2325,7 @@ void IOGraph::setName(const QString &name) } } -QRgb IOGraph::color() +QRgb IOGraph::color() const { return color_.color().rgb(); } @@ -1840,14 +2338,24 @@ void IOGraph::setColor(const QRgb color) void IOGraph::setPlotStyle(int style) { + bool recalc = false; + bool shows_zero = showsZero(); + // Switch plottable if needed switch (style) { case psBar: case psStackedBar: if (graph_) { bars_ = new QCPBars(parent_->xAxis, parent_->yAxis); + // default widthType is wtPlotCoords. Scale with the interval + // size to prevent overlap. (Multiply this by a factor to have + // a gap between bars; the QCustomPlot default is 0.75.) + if (interval_) { + bars_->setWidth(interval_ / SCALE_F); + } parent_->removeGraph(graph_); graph_ = NULL; + recalc = true; } break; default: @@ -1855,6 +2363,7 @@ void IOGraph::setPlotStyle(int style) graph_ = parent_->addGraph(parent_->xAxis, parent_->yAxis); parent_->removePlottable(bars_); bars_ = NULL; + recalc = true; } break; } @@ -1930,13 +2439,26 @@ void IOGraph::setPlotStyle(int style) break; } + if (shows_zero != showsZero()) { + // recalculate if whether zero is added changed + recalc = true; + } + setName(name_); applyCurrentColor(); + + if (recalc) { + // switching the plottable requires recalculation to add the data + emit requestRecalc(); + } } -const QString IOGraph::valueUnitLabel() +QString IOGraph::valueUnitLabel() const { - return val_to_str_const(val_units_, y_axis_vs, "Unknown"); + if (is_packet_configuration_namespace()) { + return val_to_str_const(val_units_, y_axis_packet_vs, "Unknown"); + } + return val_to_str_const(val_units_, y_axis_event_vs, "Unknown"); } void IOGraph::setValueUnits(int val_units) @@ -1946,9 +2468,21 @@ void IOGraph::setValueUnits(int val_units) val_units_ = (io_graph_item_unit_t)val_units; if (old_val_units != val_units) { - setFilter(filter_); // Check config & prime vu field - if (val_units < IOG_ITEM_UNIT_CALC_SUM) { - emit requestRecalc(); + // If val_units changed, switching between a type that doesn't + // use the vu_field/hfi/edt to one of the advanced graphs that + // does requires a retap. setFilter will handle that, because + // the full filter strings will be different. + if (setFilter(filter_)) { // Check config & prime vu field + if (val_units == IOG_ITEM_UNIT_CALC_LOAD || + old_val_units == IOG_ITEM_UNIT_CALC_LOAD) { + // LOAD graphs fill in the io_graph_item_t differently + // than other advanced graphs, so we have to retap even + // if the filter is the same. (update_io_graph_item could + // instead calculate and store LOAD information for any + // advanced graph type, but the tradeoff might not be + // worth it.) + setNeedRetap(true); + } } } } @@ -1967,6 +2501,8 @@ void IOGraph::setValueUnitField(const QString &vu_field) } if (old_hf_index != hf_index_) { + // If the field changed, and val_units is a type that uses it, + // we need to retap. setFilter will handle that. setFilter(filter_); // Check config & prime vu field } } @@ -1993,25 +2529,44 @@ bool IOGraph::removeFromLegend() return false; } -double IOGraph::startOffset() +// This returns what graph key offset corresponds with relative time 0.0, +// i.e. when absolute times are used the difference between abs_ts and +// rel_ts of the first tapped packet. Generally the same for all graphs +// that are displayed and have some data, unless they're on the opposite +// sides of time references. +// XXX - If the graph spans a time reference, it's not clear how we want +// to switch from relative to absolute times. +double IOGraph::startOffset() const { - if (graph_ && qSharedPointerDynamicCast<QCPAxisTickerDateTime>(graph_->keyAxis()->ticker()) && graph_->data()->size() > 0) { - return graph_->data()->at(0)->key; + if (graph_ && qSharedPointerDynamicCast<QCPAxisTickerDateTime>(graph_->keyAxis()->ticker())) { + return nstime_to_sec(&start_time_); } - if (bars_ && qSharedPointerDynamicCast<QCPAxisTickerDateTime>(bars_->keyAxis()->ticker()) && bars_->data()->size() > 0) { - return bars_->data()->at(0)->key; + if (bars_ && qSharedPointerDynamicCast<QCPAxisTickerDateTime>(bars_->keyAxis()->ticker())) { + return nstime_to_sec(&start_time_); } return 0.0; } -int IOGraph::packetFromTime(double ts) +nstime_t IOGraph::startTime() const { - int idx = ts * 1000 / interval_; - if (idx >= 0 && idx < (int) cur_idx_) { + if (graph_ && qSharedPointerDynamicCast<QCPAxisTickerDateTime>(graph_->keyAxis()->ticker())) { + return start_time_; + } + if (bars_ && qSharedPointerDynamicCast<QCPAxisTickerDateTime>(bars_->keyAxis()->ticker())) { + return start_time_; + } + return nstime_t(NSTIME_INIT_ZERO); +} + +int IOGraph::packetFromTime(double ts) const +{ + int idx = ts * SCALE_F / interval_; + if (idx >= 0 && idx <= cur_idx_) { switch (val_units_) { case IOG_ITEM_UNIT_CALC_MAX: + return items_[idx].max_frame_in_invl; case IOG_ITEM_UNIT_CALC_MIN: - return items_[idx].extreme_frame_in_invl; + return items_[idx].min_frame_in_invl; default: return items_[idx].last_frame_in_invl; } @@ -2022,31 +2577,30 @@ int IOGraph::packetFromTime(double ts) void IOGraph::clearAllData() { cur_idx_ = -1; - reset_io_graph_items(items_, max_io_items_); + if (items_.size()) { + reset_io_graph_items(&items_[0], items_.size(), hf_index_); + } if (graph_) { graph_->data()->clear(); } if (bars_) { bars_->data()->clear(); } - start_time_ = 0.0; + nstime_set_zero(&start_time_); } -void IOGraph::recalcGraphData(capture_file *cap_file, bool enable_scaling) +void IOGraph::recalcGraphData(capture_file *cap_file) { /* Moving average variables */ unsigned int mavg_in_average_count = 0, mavg_left = 0; unsigned int mavg_to_remove = 0, mavg_to_add = 0; double mavg_cumulated = 0; - QCPAxis *x_axis = nullptr; if (graph_) { graph_->data()->clear(); - x_axis = graph_->keyAxis(); } if (bars_) { bars_->data()->clear(); - x_axis = bars_->keyAxis(); } if (moving_avg_period_ > 0 && cur_idx_ >= 0) { @@ -2054,7 +2608,7 @@ void IOGraph::recalcGraphData(capture_file *cap_file, bool enable_scaling) * just to make sure average on leftmost and rightmost displayed * values is as reliable as possible */ - guint64 warmup_interval = 0; + uint64_t warmup_interval = 0; // for (; warmup_interval < first_interval; warmup_interval += interval_) { // mavg_cumulated += get_it_value(io, i, (int)warmup_interval/interval_); @@ -2064,8 +2618,8 @@ void IOGraph::recalcGraphData(capture_file *cap_file, bool enable_scaling) mavg_cumulated += getItemValue((int)warmup_interval/interval_, cap_file); mavg_in_average_count++; for (warmup_interval = interval_; - ((warmup_interval < (0 + (moving_avg_period_ / 2) * (guint64)interval_)) && - (warmup_interval <= (cur_idx_ * (guint64)interval_))); + ((warmup_interval < (0 + (moving_avg_period_ / 2) * (uint64_t)interval_)) && + (warmup_interval <= (cur_idx_ * (uint64_t)interval_))); warmup_interval += interval_) { mavg_cumulated += getItemValue((int)warmup_interval / interval_, cap_file); @@ -2074,11 +2628,9 @@ void IOGraph::recalcGraphData(capture_file *cap_file, bool enable_scaling) mavg_to_add = (unsigned int)warmup_interval; } + double ts_offset = startOffset(); for (int i = 0; i <= cur_idx_; i++) { - double ts = (double) i * interval_ / 1000; - if (x_axis && qSharedPointerDynamicCast<QCPAxisTickerDateTime>(x_axis->ticker())) { - ts += start_time_; - } + double ts = (double) i * interval_ / SCALE_F + ts_offset; double val = getItemValue(i, cap_file); if (moving_avg_period_ > 0) { @@ -2112,73 +2664,46 @@ void IOGraph::recalcGraphData(capture_file *cap_file, bool enable_scaling) bars_->addData(ts, val); } } -// qDebug() << "=rgd i" << i << ts << val; - } - - // attempt to rescale time values to specific units - if (enable_scaling) { - calculateScaledValueUnit(); - } else { - scaled_value_unit_.clear(); } emit requestReplot(); } -void IOGraph::calculateScaledValueUnit() +format_size_units_e IOGraph::formatUnits() const { - // Reset unit and recalculate if needed. - scaled_value_unit_.clear(); - - // If there is no field, scaling is not possible. - if (hf_index_ < 0) { - return; - } - switch (val_units_) { + case IOG_ITEM_UNIT_PACKETS: + case IOG_ITEM_UNIT_CALC_FRAMES: + if (is_packet_configuration_namespace()) { + return FORMAT_SIZE_UNIT_PACKETS; + } + return FORMAT_SIZE_UNIT_EVENTS; + case IOG_ITEM_UNIT_BYTES: + return FORMAT_SIZE_UNIT_BYTES; + case IOG_ITEM_UNIT_BITS: + return FORMAT_SIZE_UNIT_BITS; + case IOG_ITEM_UNIT_CALC_LOAD: + return FORMAT_SIZE_UNIT_ERLANGS; + case IOG_ITEM_UNIT_CALC_FIELDS: + return FORMAT_SIZE_UNIT_FIELDS; case IOG_ITEM_UNIT_CALC_SUM: case IOG_ITEM_UNIT_CALC_MAX: case IOG_ITEM_UNIT_CALC_MIN: case IOG_ITEM_UNIT_CALC_AVERAGE: // Unit is not yet known, continue detecting it. - break; - default: - // Unit is Packets, Bytes, Bits, etc. - return; - } - - if (proto_registrar_get_ftype(hf_index_) == FT_RELATIVE_TIME) { - // find maximum absolute value and scale accordingly - double maxValue = 0; - if (graph_) { - maxValue = maxValueFromGraphData(*graph_->data()); - } else if (bars_) { - maxValue = maxValueFromGraphData(*bars_->data()); - } - // If the maximum value is zero, then either we have no data or - // everything is zero, do not scale the unit in this case. - if (maxValue == 0) { - return; - } - - // XXX GTK+ always uses "ms" for log scale, should we do that too? - int value_multiplier; - if (maxValue >= 1.0) { - scaled_value_unit_ = "s"; - value_multiplier = 1; - } else if (maxValue >= 0.001) { - scaled_value_unit_ = "ms"; - value_multiplier = 1000; - } else { - scaled_value_unit_ = "us"; - value_multiplier = 1000000; - } - - if (graph_) { - scaleGraphData(*graph_->data(), value_multiplier); - } else if (bars_) { - scaleGraphData(*bars_->data(), value_multiplier); + if (hf_index_ > 0) { + if (proto_registrar_get_ftype(hf_index_) == FT_RELATIVE_TIME) { + return FORMAT_SIZE_UNIT_SECONDS; + } + // Could we look if it's BASE_UNIT_STRING and use that? + // One complication is that prefixes shouldn't be combined, + // and some unit strings are already prefixed units. } + return FORMAT_SIZE_UNIT_NONE; + case IOG_ITEM_UNIT_CALC_THROUGHPUT: + return FORMAT_SIZE_UNIT_BITS_S; + default: + return FORMAT_SIZE_UNIT_NONE; } } @@ -2211,7 +2736,7 @@ void IOGraph::captureEvent(CaptureEvent e) if ((e.captureContext() == CaptureEvent::File) && (e.eventType() == CaptureEvent::Closing)) { - remove_tap_listener(this); + removeTapListener(); } } @@ -2222,6 +2747,38 @@ void IOGraph::reloadValueUnitField() } } +// returns true if the current plot style shows zero values, +// false if null values are omitted. +bool IOGraph::showsZero() const +{ + switch (val_units_) { + case IOG_ITEM_UNIT_PACKETS: + case IOG_ITEM_UNIT_BYTES: + case IOG_ITEM_UNIT_BITS: + case IOG_ITEM_UNIT_CALC_FRAMES: + case IOG_ITEM_UNIT_CALC_FIELDS: + if (graph_ && graph_->lineStyle() == QCPGraph::lsNone) { + return false; + } + else { + return true; + } + case IOG_ITEM_UNIT_CALC_SUM: + case IOG_ITEM_UNIT_CALC_MAX: + case IOG_ITEM_UNIT_CALC_MIN: + case IOG_ITEM_UNIT_CALC_AVERAGE: + case IOG_ITEM_UNIT_CALC_LOAD: + case IOG_ITEM_UNIT_CALC_THROUGHPUT: + // These are not the same sort of "omitted zeros" as above, + // but changing val_units_ always results in a recalculation + // so it doesn't matter (see modelDataChanged) + return false; + + default: + return true; + } +} + // Check if a packet is available at the given interval (idx). bool IOGraph::hasItemToShow(int idx, double value) const { @@ -2237,7 +2794,7 @@ bool IOGraph::hasItemToShow(int idx, double value) const case IOG_ITEM_UNIT_BITS: case IOG_ITEM_UNIT_CALC_FRAMES: case IOG_ITEM_UNIT_CALC_FIELDS: - if(value == 0.0 && (graph_ && graph_->scatterStyle().shape() != QCPScatterStyle::ssNone)) { + if (value == 0.0 && (graph_ && graph_->lineStyle() == QCPGraph::lsNone)) { result = false; } else { @@ -2250,6 +2807,7 @@ bool IOGraph::hasItemToShow(int idx, double value) const case IOG_ITEM_UNIT_CALC_MIN: case IOG_ITEM_UNIT_CALC_AVERAGE: case IOG_ITEM_UNIT_CALC_LOAD: + case IOG_ITEM_UNIT_CALC_THROUGHPUT: if (item->fields) { result = true; } @@ -2266,6 +2824,9 @@ bool IOGraph::hasItemToShow(int idx, double value) const void IOGraph::setInterval(int interval) { interval_ = interval; + if (bars_) { + bars_->setWidth(interval_ / SCALE_F); + } } // Get the value at the given interval (idx) for the current value unit. @@ -2273,7 +2834,7 @@ double IOGraph::getItemValue(int idx, const capture_file *cap_file) const { ws_assert(idx < max_io_items_); - return get_io_graph_item(items_, val_units_, idx, hf_index_, cap_file, interval_, cur_idx_); + return get_io_graph_item(&items_[0], val_units_, idx, hf_index_, cap_file, interval_, cur_idx_); } // "tap_reset" callback for register_tap_listener @@ -2294,27 +2855,60 @@ tap_packet_status IOGraph::tapPacket(void *iog_ptr, packet_info *pinfo, epan_dis return TAP_PACKET_DONT_REDRAW; } - int idx = get_io_graph_index(pinfo, iog->interval_); + int64_t tmp_idx = get_io_graph_index(pinfo, iog->interval_); bool recalc = false; /* some sanity checks */ - if ((idx < 0) || (idx >= max_io_items_)) { - iog->cur_idx_ = max_io_items_ - 1; + if ((tmp_idx < 0) || (tmp_idx >= max_io_items_)) { + iog->cur_idx_ = (int)iog->items_.size() - 1; return TAP_PACKET_DONT_REDRAW; } + int idx = (int)tmp_idx; + /* If the graph isn't visible, don't do the work or redraw, but mark + * the graph in need of a retap if it is ever enabled. The alternative + * is to do the work, but clear pending retaps when the taps are reset + * (which indicates something else triggered a retap.) The tradeoff would + * be more calculation and memory usage when a graph is disabled in + * exchange for fewer scenarios that involve retaps when toggling the + * enabled/disabled taps. + */ + if (!iog->visible()) { + if (idx > iog->cur_idx_) { + iog->need_retap_ = true; + } + return TAP_PACKET_DONT_REDRAW; + } + + if ((size_t)idx >= iog->items_.size()) { + const size_t old_size = iog->items_.size(); + size_t new_size; + if (old_size == 0) { + new_size = 1024; + } else { + new_size = MIN((old_size * 3) / 2, max_io_items_); + } + new_size = MAX(new_size, (size_t)idx + 1); + try { + iog->items_.resize(new_size); + } catch (std::bad_alloc&) { + // std::vector.resize() has strong exception safety + ws_warning("Failed memory allocation!"); + return TAP_PACKET_DONT_REDRAW; + } + // resize zero-initializes new items, which is what we want + //reset_io_graph_items(&iog->items_[old_size], new_size - old_size); + } + /* update num_items */ if (idx > iog->cur_idx_) { - iog->cur_idx_ = (guint32) idx; + iog->cur_idx_ = idx; recalc = true; } /* set start time */ - if (iog->start_time_ == 0.0) { - nstime_t start_nstime; - nstime_set_zero(&start_nstime); - nstime_delta(&start_nstime, &pinfo->abs_ts, &pinfo->rel_ts); - iog->start_time_ = nstime_to_sec(&start_nstime); + if (nstime_is_zero(&iog->start_time_)) { + nstime_delta(&iog->start_time_, &pinfo->abs_ts, &pinfo->rel_ts); } epan_dissect_t *adv_edt = NULL; @@ -2323,7 +2917,7 @@ tap_packet_status IOGraph::tapPacket(void *iog_ptr, packet_info *pinfo, epan_dis adv_edt = edt; } - if (!update_io_graph_item(iog->items_, idx, pinfo, adv_edt, iog->hf_index_, iog->val_units_, iog->interval_)) { + if (!update_io_graph_item(&iog->items_[0], idx, pinfo, adv_edt, iog->hf_index_, iog->val_units_, iog->interval_)) { return TAP_PACKET_DONT_REDRAW; } |