wireshark/ui/qt/io_graph_dialog.cpp

2163 lines
64 KiB
C++

/* io_graph_dialog.cpp
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
* Copyright 1998 Gerald Combs
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "io_graph_dialog.h"
#include <ui_io_graph_dialog.h>
#include "file.h"
#include <epan/stat_tap_ui.h>
#include "epan/stats_tree_priv.h"
#include "epan/uat-int.h"
#include <wsutil/utf8_entities.h>
#include <ui/qt/utils/qt_ui_utils.h>
#include <ui/qt/utils/variant_pointer.h>
#include <ui/qt/utils/color_utils.h>
#include <ui/qt/widgets/qcustomplot.h>
#include "progress_frame.h"
#include "wireshark_application.h"
#include <wsutil/report_message.h>
#include <ui/qt/utils/tango_colors.h> //provides some default colors
#include <ui/qt/widgets/copy_from_profile_menu.h>
#include "ui/qt/widgets/wireshark_file_dialog.h"
#include <QClipboard>
#include <QFontMetrics>
#include <QFrame>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QRubberBand>
#include <QSpacerItem>
#include <QTimer>
#include <QVariant>
// 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:
// https://www.qcustomplot.com/index.php/support/forum/62
// - 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.
// To do:
// - Use scroll bars?
// - Scroll during live captures
// - Set ticks per pixel (e.g. pressing "2" sets 2 tpp).
const qreal graph_line_width_ = 1.0;
const int DEFAULT_MOVING_AVERAGE = 0;
// Don't accidentally zoom into a 1x1 rect if you happen to click on the graph
// in zoom mode.
const int min_zoom_pixels_ = 20;
const int stat_update_interval_ = 200; // ms
// Saved graph settings
typedef struct _io_graph_settings_t {
gboolean enabled;
char* name;
char* dfilter;
guint color;
guint32 style;
guint32 yaxis;
char* yfield;
guint32 sma_period;
} io_graph_settings_t;
static const value_string graph_style_vs[] = {
{ IOGraph::psLine, "Line" },
{ IOGraph::psImpulse, "Impulse" },
{ IOGraph::psBar, "Bar" },
{ IOGraph::psStackedBar, "Stacked Bar" },
{ IOGraph::psDot, "Dot" },
{ IOGraph::psSquare, "Square" },
{ IOGraph::psDiamond, "Diamond" },
{ 0, NULL }
};
static const value_string y_axis_vs[] = {
{ IOG_ITEM_UNIT_PACKETS, "Packets" },
{ IOG_ITEM_UNIT_BYTES, "Bytes" },
{ IOG_ITEM_UNIT_BITS, "Bits" },
{ IOG_ITEM_UNIT_CALC_SUM, "SUM(Y Field)" },
{ IOG_ITEM_UNIT_CALC_FRAMES, "COUNT FRAMES(Y Field)" },
{ IOG_ITEM_UNIT_CALC_FIELDS, "COUNT FIELDS(Y Field)" },
{ 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_LOAD, "LOAD(Y Field)" },
{ 0, NULL }
};
static const value_string moving_avg_vs[] = {
{ 0, "None" },
{ 10, "10 interval SMA" },
{ 20, "20 interval SMA" },
{ 50, "50 interval SMA" },
{ 100, "100 interval SMA" },
{ 200, "200 interval SMA" },
{ 500, "500 interval SMA" },
{ 1000, "1000 interval SMA" },
{ 0, NULL }
};
static io_graph_settings_t *iog_settings_ = NULL;
static guint num_io_graphs_ = 0;
static uat_t *iog_uat_ = NULL;
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"
#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)) {\
char* tmp_str = g_strndup(buf,len); \
if ((g_strcmp0(tmp_str, "Enabled") == 0) || \
(g_strcmp0(tmp_str, "TRUE") == 0)) \
((rec_t*)rec)->field_name = 1; \
else \
((rec_t*)rec)->field_name = 0; \
g_free(tmp_str); } \
static void basename ## _ ## field_name ## _tostr_cb(void* rec, char** out_ptr, unsigned* out_len, const void* UNUSED_PARAMETER(u1), const void* UNUSED_PARAMETER(u2)) {\
*out_ptr = g_strdup_printf("%s",((rec_t*)rec)->field_name ? "Enabled" : "Disabled"); \
*out_len = (unsigned)strlen(*out_ptr); }
static gboolean uat_fld_chk_enable(void* u1 _U_, const char* strptr, guint len, const void* u2 _U_, const void* u3 _U_, char** err)
{
char* str = g_strndup(strptr,len);
if ((g_strcmp0(str, "Enabled") == 0) ||
(g_strcmp0(str, "Disabled") == 0) ||
(g_strcmp0(str, "TRUE") == 0) || //just for UAT functionality
(g_strcmp0(str, "FALSE") == 0)) {
*err = NULL;
g_free(str);
return TRUE;
}
//User should never see this unless they are manually modifying UAT
*err = g_strdup_printf("invalid value: %s (must be Enabled or Disabled)", str);
g_free(str);
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_)
{
guint i;
char* str = g_strndup(buf,len);
const char* cstr;
((io_graph_settings_t*)rec)->sma_period = 0;
//Original UAT had just raw numbers and not enumerated values with "interval SMA"
if (strstr(str, "interval SMA") == NULL) {
if (strcmp(str, "None") == 0) { //Valid enumerated value
} else if (strcmp(str, "0") == 0) {
g_free(str);
str = g_strdup("None");
} else {
char *str2 = g_strdup_printf("%s interval SMA", str);
g_free(str);
str = str2;
}
}
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;
g_free(str);
return;
}
}
g_free(str);
}
//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;
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);
*out_len = (unsigned)strlen(*out_ptr);
return;
}
}
*out_ptr = g_strdup("None");
*out_len = (unsigned)strlen("None");
}
static gboolean sma_period_chk_enum(void* u1 _U_, const char* strptr, guint len, const void* v, const void* u3 _U_, char** err) {
char *str = g_strndup(strptr,len);
guint i;
const value_string* vs = (const value_string *)v;
//Original UAT had just raw numbers and not enumerated values with "interval SMA"
if (strstr(str, "interval SMA") == NULL) {
if (strcmp(str, "None") == 0) { //Valid enumerated value
} else if (strcmp(str, "0") == 0) {
g_free(str);
str = g_strdup("None");
} else {
char *str2 = g_strdup_printf("%s interval SMA", str);
g_free(str);
str = str2;
}
}
for(i=0;vs[i].strptr;i++) {
if (g_strcmp0(vs[i].strptr,str) == 0) {
*err = NULL;
g_free(str);
return TRUE;
}
}
*err = g_strdup_printf("invalid value: %s",str);
g_free(str);
return FALSE;
}
#define UAT_FLD_SMA_PERIOD(basename,field_name,title,enum,desc) \
{#field_name, title, PT_TXTMOD_ENUM,{sma_period_chk_enum,basename ## _ ## field_name ## _set_cb,basename ## _ ## field_name ## _tostr_cb},{&(enum),&(enum),&(enum)},&(enum),desc,FLDFILL}
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_PROTO_FIELD_CB_DEF(io_graph, yfield, io_graph_settings_t)
static uat_field_t io_graph_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_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_END_FIELDS
};
static void* io_graph_copy_cb(void* dst_ptr, const void* src_ptr, size_t) {
io_graph_settings_t* dst = (io_graph_settings_t *)dst_ptr;
const io_graph_settings_t* src = (const io_graph_settings_t *)src_ptr;
dst->enabled = src->enabled;
dst->name = g_strdup(src->name);
dst->dfilter = g_strdup(src->dfilter);
dst->color = src->color;
dst->style = src->style;
dst->yaxis = src->yaxis;
dst->yfield = g_strdup(src->yfield);
dst->sma_period = src->sma_period;
return dst;
}
static void io_graph_free_cb(void* p) {
io_graph_settings_t *iogs = (io_graph_settings_t *)p;
g_free(iogs->name);
g_free(iogs->dfilter);
g_free(iogs->yfield);
}
} // extern "C"
IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf) :
WiresharkDialog(parent, cf),
ui(new Ui::IOGraphDialog),
uat_model_(NULL),
uat_delegate_(NULL),
base_graph_(NULL),
tracer_(NULL),
start_time_(0.0),
mouse_drags_(true),
rubber_band_(NULL),
stat_timer_(NULL),
need_replot_(false),
need_retap_(false),
auto_axes_(true)
{
ui->setupUi(this);
loadGeometry();
setWindowSubtitle(tr("I/O Graphs"));
setAttribute(Qt::WA_DeleteOnClose, true);
QCustomPlot *iop = ui->ioPlot;
ui->newToolButton->setStockIcon("list-add");
ui->deleteToolButton->setStockIcon("list-remove");
ui->copyToolButton->setStockIcon("list-copy");
ui->clearToolButton->setStockIcon("list-clear");
#ifdef Q_OS_MAC
ui->newToolButton->setAttribute(Qt::WA_MacSmallSize, true);
ui->deleteToolButton->setAttribute(Qt::WA_MacSmallSize, true);
ui->copyToolButton->setAttribute(Qt::WA_MacSmallSize, true);
ui->clearToolButton->setAttribute(Qt::WA_MacSmallSize, true);
#endif
QPushButton *save_bt = ui->buttonBox->button(QDialogButtonBox::Save);
save_bt->setText(tr("Save As" UTF8_HORIZONTAL_ELLIPSIS));
QPushButton *copy_bt = ui->buttonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole);
connect (copy_bt, SIGNAL(clicked()), this, SLOT(copyAsCsvClicked()));
QPushButton *copy_from_bt = ui->buttonBox->addButton(tr("Copy from"), QDialogButtonBox::ActionRole);
CopyFromProfileMenu *copy_from_menu = new CopyFromProfileMenu("io_graphs", copy_from_bt);
copy_from_bt->setMenu(copy_from_menu);
copy_from_bt->setToolTip(tr("Copy graphs from another profile."));
copy_from_bt->setEnabled(copy_from_menu->haveProfiles());
connect(copy_from_menu, SIGNAL(triggered(QAction *)), this, SLOT(copyFromProfile(QAction *)));
QPushButton *close_bt = ui->buttonBox->button(QDialogButtonBox::Close);
if (close_bt) {
close_bt->setDefault(true);
}
stat_timer_ = new QTimer(this);
connect(stat_timer_, SIGNAL(timeout()), this, SLOT(updateStatistics()));
stat_timer_->start(stat_update_interval_);
// Intervals (ms)
ui->intervalComboBox->addItem(tr("1 ms"), 1);
ui->intervalComboBox->addItem(tr("5 ms"), 5);
ui->intervalComboBox->addItem(tr("10 ms"), 10);
ui->intervalComboBox->addItem(tr("100 ms"), 100);
ui->intervalComboBox->addItem(tr("1 sec"), 1000);
ui->intervalComboBox->addItem(tr("10 sec"), 10000);
ui->intervalComboBox->addItem(tr("1 min"), 60000);
ui->intervalComboBox->addItem(tr("10 min"), 600000);
ui->intervalComboBox->setCurrentIndex(4);
ui->todCheckBox->setChecked(false);
ui->dragRadioButton->setChecked(mouse_drags_);
ctx_menu_.addAction(ui->actionZoomIn);
ctx_menu_.addAction(ui->actionZoomInX);
ctx_menu_.addAction(ui->actionZoomInY);
ctx_menu_.addAction(ui->actionZoomOut);
ctx_menu_.addAction(ui->actionZoomOutX);
ctx_menu_.addAction(ui->actionZoomOutY);
ctx_menu_.addAction(ui->actionReset);
ctx_menu_.addSeparator();
ctx_menu_.addAction(ui->actionMoveRight10);
ctx_menu_.addAction(ui->actionMoveLeft10);
ctx_menu_.addAction(ui->actionMoveUp10);
ctx_menu_.addAction(ui->actionMoveDown10);
ctx_menu_.addAction(ui->actionMoveRight1);
ctx_menu_.addAction(ui->actionMoveLeft1);
ctx_menu_.addAction(ui->actionMoveUp1);
ctx_menu_.addAction(ui->actionMoveDown1);
ctx_menu_.addSeparator();
ctx_menu_.addAction(ui->actionGoToPacket);
ctx_menu_.addSeparator();
ctx_menu_.addAction(ui->actionDragZoom);
ctx_menu_.addAction(ui->actionToggleTimeOrigin);
ctx_menu_.addAction(ui->actionCrosshairs);
iop->xAxis->setLabel(tr("Time (s)"));
iop->setMouseTracking(true);
iop->setEnabled(true);
QCPPlotTitle *title = new QCPPlotTitle(iop);
iop->plotLayout()->insertRow(0);
iop->plotLayout()->addElement(0, 0, title);
title->setText(tr("Wireshark I/O Graphs: %1").arg(cap_file_.fileDisplayName()));
tracer_ = new QCPItemTracer(iop);
iop->addItem(tracer_);
loadProfileGraphs();
if (num_io_graphs_ > 0) {
for (guint i = 0; i < num_io_graphs_; i++) {
createIOGraph(i);
}
} else {
addDefaultGraph(true, 0);
addDefaultGraph(true, 1);
}
toggleTracerStyle(true);
iop->setFocus();
iop->rescaleAxes();
ui->clearToolButton->setEnabled(uat_model_->rowCount() != 0);
//XXX - resize columns?
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*)));
disconnect(ui->buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
}
IOGraphDialog::~IOGraphDialog()
{
cap_file_.stopLoading();
foreach(IOGraph* iog, ioGraphs_) {
delete iog;
}
delete ui;
ui = NULL;
}
void IOGraphDialog::copyFromProfile(QAction *action)
{
QString filename = action->data().toString();
guint orig_data_len = iog_uat_->raw_data->len;
gchar *err = NULL;
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);
}
} else {
report_failure("Error while loading %s: %s", iog_uat_->name, err);
g_free(err);
}
}
void IOGraphDialog::addGraph(bool checked, QString name, QString dfilter, int color_idx, IOGraph::PlotStyles style, io_graph_item_unit_t value_units, QString yfield, int moving_average)
{
// should not fail, but you never know.
if (!uat_model_->insertRows(uat_model_->rowCount(), 1)) {
qDebug() << "Failed to add a new record";
return;
}
int currentRow = uat_model_->rowCount() - 1;
const QModelIndex &new_index = uat_model_->index(currentRow, 0);
//populate model with data
uat_model_->setData(uat_model_->index(currentRow, colEnabled), checked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
uat_model_->setData(uat_model_->index(currentRow, colName), name);
uat_model_->setData(uat_model_->index(currentRow, colDFilter), dfilter);
uat_model_->setData(uat_model_->index(currentRow, colColor), QColor(color_idx), Qt::DecorationRole);
uat_model_->setData(uat_model_->index(currentRow, colStyle), val_to_str_const(style, graph_style_vs, "None"));
uat_model_->setData(uat_model_->index(currentRow, colYAxis), val_to_str_const(value_units, y_axis_vs, "Packets"));
uat_model_->setData(uat_model_->index(currentRow, colYField), yfield);
uat_model_->setData(uat_model_->index(currentRow, colSMAPeriod), val_to_str_const(moving_average, moving_avg_vs, "None"));
// due to an EditTrigger, this will also start editing.
ui->graphUat->setCurrentIndex(new_index);
createIOGraph(currentRow);
}
void IOGraphDialog::addGraph(bool copy_from_current)
{
const QModelIndex &current = ui->graphUat->currentIndex();
if (copy_from_current && !current.isValid())
return;
if (copy_from_current) {
// should not fail, but you never know.
if (!uat_model_->insertRows(uat_model_->rowCount(), 1)) {
qDebug() << "Failed to add a new record";
return;
}
const QModelIndex &new_index = uat_model_->index(uat_model_->rowCount() - 1, 0);
uat_model_->copyRow(new_index.row(), current.row());
createIOGraph(new_index.row());
ui->graphUat->setCurrentIndex(new_index);
} else {
addDefaultGraph(false);
const QModelIndex &new_index = uat_model_->index(uat_model_->rowCount() - 1, 0);
ui->graphUat->setCurrentIndex(new_index);
}
}
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));
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()));
syncGraphSettings(currentRow);
if (iog->visible()) {
scheduleRetap();
}
}
void IOGraphDialog::addDefaultGraph(bool enabled, int idx)
{
switch (idx % 2) {
case 0:
addGraph(enabled, tr("All packets"), QString(), ColorUtils::graphColor(idx),
IOGraph::psLine, IOG_ITEM_UNIT_PACKETS, QString(), DEFAULT_MOVING_AVERAGE);
break;
default:
addGraph(enabled, tr("TCP errors"), "tcp.analysis.flags", ColorUtils::graphColor(idx),
IOGraph::psBar, IOG_ITEM_UNIT_PACKETS, QString(), DEFAULT_MOVING_AVERAGE);
break;
}
}
// Sync the settings from UAT model to its IOGraph.
// Disables the graph if any errors are found.
//
// NOTE: Setting dfilter, yaxis and yfield here will all end up in setFilter() and this
// has a chicken-and-egg problem because setFilter() depends on previous assigned
// values for filter_, val_units_ and vu_field_. Setting values in wrong order
// may give unpredicted results because setFilter() does not always set filter_
// on errors.
// TODO: The issues in the above note should be fixed and setFilter() should not be
// called so frequently.
void IOGraphDialog::syncGraphSettings(int row)
{
IOGraph *iog = ioGraphs_.value(row, Q_NULLPTR);
if (!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());
iog->setFilter(uat_model_->data(uat_model_->index(row, colDFilter)).toString());
/* 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));
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());
data_str = uat_model_->data(uat_model_->index(row, colStyle)).toString();
iog->setPlotStyle((int) str_to_val(qUtf8Printable(data_str), graph_style_vs, 0));
data_str = uat_model_->data(uat_model_->index(row, colSMAPeriod)).toString();
iog->moving_avg_period_ = str_to_val(qUtf8Printable(data_str), moving_avg_vs, 0);
iog->setInterval(ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt());
if (!iog->configError().isEmpty()) {
hint_err_ = iog->configError();
visible = false;
retap = false;
}
iog->setVisible(visible);
getGraphInfo();
mouseMoved(NULL); // Update hint
updateLegend();
if (visible) {
if (retap) {
scheduleRetap();
} else {
scheduleReplot();
}
}
}
void IOGraphDialog::updateWidgets()
{
WiresharkDialog::updateWidgets();
}
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)
{
need_recalc_ = true;
if (now) updateStatistics();
}
void IOGraphDialog::scheduleRetap(bool now)
{
need_retap_ = true;
if (now) updateStatistics();
}
void IOGraphDialog::reloadFields()
{
emit reloadValueUnitFields();
}
void IOGraphDialog::keyPressEvent(QKeyEvent *event)
{
int pan_pixels = event->modifiers() & Qt::ShiftModifier ? 1 : 10;
switch(event->key()) {
case Qt::Key_Minus:
case Qt::Key_Underscore: // Shifted minus on U.S. keyboards
case Qt::Key_O: // GTK+
case Qt::Key_R:
zoomAxes(false);
break;
case Qt::Key_Plus:
case Qt::Key_Equal: // Unshifted plus on U.S. keyboards
case Qt::Key_I: // GTK+
zoomAxes(true);
break;
case Qt::Key_X: // Zoom X axis only
if(event->modifiers() & Qt::ShiftModifier){
zoomXAxis(false); // upper case X -> Zoom out
} else {
zoomXAxis(true); // lower case x -> Zoom in
}
break;
case Qt::Key_Y: // Zoom Y axis only
if(event->modifiers() & Qt::ShiftModifier){
zoomYAxis(false); // upper case Y -> Zoom out
} else {
zoomYAxis(true); // lower case y -> Zoom in
}
break;
case Qt::Key_Right:
case Qt::Key_L:
panAxes(pan_pixels, 0);
break;
case Qt::Key_Left:
case Qt::Key_H:
panAxes(-1 * pan_pixels, 0);
break;
case Qt::Key_Up:
case Qt::Key_K:
panAxes(0, pan_pixels);
break;
case Qt::Key_Down:
case Qt::Key_J:
panAxes(0, -1 * pan_pixels);
break;
case Qt::Key_Space:
toggleTracerStyle();
break;
case Qt::Key_0:
case Qt::Key_ParenRight: // Shifted 0 on U.S. keyboards
case Qt::Key_Home:
resetAxes();
break;
case Qt::Key_G:
on_actionGoToPacket_triggered();
break;
case Qt::Key_T:
on_actionToggleTimeOrigin_triggered();
break;
case Qt::Key_Z:
on_actionDragZoom_triggered();
break;
}
QDialog::keyPressEvent(event);
}
void IOGraphDialog::reject()
{
if (!uat_model_)
return;
// Changes to the I/O Graph settings are always saved,
// there is no possibility for "rejection".
QString error;
if (uat_model_->applyChanges(error)) {
if (!error.isEmpty()) {
report_failure("%s", qPrintable(error));
}
}
QDialog::reject();
}
void IOGraphDialog::zoomAxes(bool in)
{
QCustomPlot *iop = ui->ioPlot;
double h_factor = iop->axisRect()->rangeZoomFactor(Qt::Horizontal);
double v_factor = iop->axisRect()->rangeZoomFactor(Qt::Vertical);
auto_axes_ = false;
if (!in) {
h_factor = pow(h_factor, -1);
v_factor = pow(v_factor, -1);
}
iop->xAxis->scaleRange(h_factor, iop->xAxis->range().center());
iop->yAxis->scaleRange(v_factor, iop->yAxis->range().center());
iop->replot();
}
void IOGraphDialog::zoomXAxis(bool in)
{
QCustomPlot *iop = ui->ioPlot;
double h_factor = iop->axisRect()->rangeZoomFactor(Qt::Horizontal);
auto_axes_ = false;
if (!in) {
h_factor = pow(h_factor, -1);
}
iop->xAxis->scaleRange(h_factor, iop->xAxis->range().center());
iop->replot();
}
void IOGraphDialog::zoomYAxis(bool in)
{
QCustomPlot *iop = ui->ioPlot;
double v_factor = iop->axisRect()->rangeZoomFactor(Qt::Vertical);
auto_axes_ = false;
if (!in) {
v_factor = pow(v_factor, -1);
}
iop->yAxis->scaleRange(v_factor, iop->yAxis->range().center());
iop->replot();
}
void IOGraphDialog::panAxes(int x_pixels, int y_pixels)
{
QCustomPlot *iop = ui->ioPlot;
double h_pan = 0.0;
double v_pan = 0.0;
auto_axes_ = false;
h_pan = iop->xAxis->range().size() * x_pixels / iop->xAxis->axisRect()->width();
v_pan = iop->yAxis->range().size() * y_pixels / iop->yAxis->axisRect()->height();
// The GTK+ version won't pan unless we're zoomed. Should we do the same here?
if (h_pan) {
iop->xAxis->moveRange(h_pan);
iop->replot();
}
if (v_pan) {
iop->yAxis->moveRange(v_pan);
iop->replot();
}
}
void IOGraphDialog::toggleTracerStyle(bool force_default)
{
if (!tracer_->visible() && !force_default) return;
if (!ui->ioPlot->graph(0)) return;
QPen sp_pen = ui->ioPlot->graph(0)->pen();
QCPItemTracer::TracerStyle tstyle = QCPItemTracer::tsCrosshair;
QPen tr_pen = QPen(tracer_->pen());
QColor tr_color = sp_pen.color();
if (force_default || tracer_->style() != QCPItemTracer::tsCircle) {
tstyle = QCPItemTracer::tsCircle;
tr_color.setAlphaF(1.0);
tr_pen.setWidthF(1.5);
} else {
tr_color.setAlphaF(0.5);
tr_pen.setWidthF(1.0);
}
tracer_->setStyle(tstyle);
tr_pen.setColor(tr_color);
tracer_->setPen(tr_pen);
ui->ioPlot->replot();
}
// Returns the IOGraph which is most likely to be used by the user. This is the
// currently selected, visible graph or the first visible graph otherwise.
IOGraph *IOGraphDialog::currentActiveGraph() const
{
QModelIndex index = ui->graphUat->currentIndex();
if (index.isValid()) {
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++)
{
if (graphIsEnabled(row)) {
return ioGraphs_.value(row, NULL);
}
}
return NULL;
}
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;
}
// Scan through our graphs and gather information.
// QCPItemTracers can only be associated with QCPGraphs. Find the first one
// and associate it with our tracer. Set bar stacking order while we're here.
void IOGraphDialog::getGraphInfo()
{
base_graph_ = NULL;
QCPBars *prev_bars = NULL;
start_time_ = 0.0;
tracer_->setGraph(NULL);
IOGraph *selectedGraph = currentActiveGraph();
if (uat_model_ != NULL) {
//all graphs may not be created yet, so bounds check the graph array
for (int row = 0; row < uat_model_->rowCount(); row++) {
IOGraph* iog = ioGraphs_.value(row, Q_NULLPTR);
if (iog && graphIsEnabled(row)) {
QCPGraph *graph = iog->graph();
QCPBars *bars = iog->bars();
if (graph && (!base_graph_ || iog == selectedGraph)) {
base_graph_ = graph;
} else if (bars &&
(uat_model_->data(uat_model_->index(row, colStyle), Qt::DisplayRole).toString().compare(graph_style_vs[IOGraph::psStackedBar].strptr) == 0) &&
iog->visible()) {
bars->moveBelow(NULL); // Remove from existing stack
bars->moveBelow(prev_bars);
prev_bars = bars;
}
if (iog->visible()) {
double iog_start = iog->startOffset();
if (start_time_ == 0.0 || iog_start < start_time_) {
start_time_ = iog_start;
}
}
}
}
}
if (base_graph_ && base_graph_->data()->size() > 0) {
tracer_->setGraph(base_graph_);
tracer_->setVisible(true);
}
}
void IOGraphDialog::updateLegend()
{
QCustomPlot *iop = ui->ioPlot;
QSet<QString> vu_label_set;
QString intervalText = ui->intervalComboBox->itemText(ui->intervalComboBox->currentIndex());
iop->legend->setVisible(false);
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);
}
}
}
// Nothing.
if (vu_label_set.size() < 1) {
return;
}
// All the same. Use the Y Axis label.
if (vu_label_set.size() == 1) {
iop->yAxis->setLabel(vu_label_set.values()[0] + "/" + intervalText);
return;
}
// Differing labels. Create a legend with a Title label at top.
// Legend Title thanks to: https://www.qcustomplot.com/index.php/support/forum/443
QCPStringLegendItem* legendTitle = qobject_cast<QCPStringLegendItem*>(iop->legend->elementAt(0));
if (legendTitle == NULL) {
legendTitle = new QCPStringLegendItem(iop->legend, 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->setVisible(true);
}
QRectF IOGraphDialog::getZoomRanges(QRect zoom_rect)
{
QRectF zoom_ranges = QRectF();
if (zoom_rect.width() < min_zoom_pixels_ && zoom_rect.height() < min_zoom_pixels_) {
return zoom_ranges;
}
QCustomPlot *iop = ui->ioPlot;
QRect zr = zoom_rect.normalized();
QRect ar = iop->axisRect()->rect();
if (ar.intersects(zr)) {
QRect zsr = ar.intersected(zr);
zoom_ranges.setX(iop->xAxis->range().lower
+ iop->xAxis->range().size() * (zsr.left() - ar.left()) / ar.width());
zoom_ranges.setWidth(iop->xAxis->range().size() * zsr.width() / ar.width());
// QRects grow down
zoom_ranges.setY(iop->yAxis->range().lower
+ iop->yAxis->range().size() * (ar.bottom() - zsr.bottom()) / ar.height());
zoom_ranges.setHeight(iop->yAxis->range().size() * zsr.height() / ar.height());
}
return zoom_ranges;
}
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.
ctx_menu_.exec(event->globalPos());
} else if (mouse_drags_) {
if (iop->axisRect()->rect().contains(event->pos())) {
iop->setCursor(QCursor(Qt::ClosedHandCursor));
}
on_actionGoToPacket_triggered();
} else {
if (!rubber_band_) {
rubber_band_ = new QRubberBand(QRubberBand::Rectangle, iop);
}
rb_origin_ = event->pos();
rubber_band_->setGeometry(QRect(rb_origin_, QSize()));
rubber_band_->show();
}
iop->setFocus();
}
void IOGraphDialog::mouseMoved(QMouseEvent *event)
{
QCustomPlot *iop = ui->ioPlot;
QString hint;
Qt::CursorShape shape = Qt::ArrowCursor;
if (!hint_err_.isEmpty()) {
hint += QString("<b>%1</b> ").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;
}
}
iop->setCursor(QCursor(shape));
}
if (mouse_drags_) {
double ts = 0;
packet_num_ = 0;
int interval_packet = -1;
if (event && tracer_->graph()) {
tracer_->setGraphKey(iop->xAxis->pixelToCoord(event->pos().x()));
ts = tracer_->position->key();
if (IOGraph *iog = currentActiveGraph()) {
interval_packet = iog->packetFromTime(ts);
}
}
if (interval_packet < 0) {
hint += tr("Hover over the graph for details.");
} else {
QString msg = tr("No packets in interval");
QString val;
if (interval_packet > 0) {
packet_num_ = (guint32) interval_packet;
msg = QString("%1 %2")
.arg(!file_closed_ ? tr("Click to select packet") : tr("Packet"))
.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()) {
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.");
}
}
hint.prepend("<small><i>");
hint.append("</i></small>");
ui->hintLabel->setText(hint);
}
void IOGraphDialog::mouseReleased(QMouseEvent *event)
{
QCustomPlot *iop = ui->ioPlot;
auto_axes_ = false;
if (rubber_band_) {
rubber_band_->hide();
if (!mouse_drags_) {
QRectF zoom_ranges = getZoomRanges(QRect(rb_origin_, event->pos()));
if (zoom_ranges.width() > 0.0 && zoom_ranges.height() > 0.0) {
iop->xAxis->setRangeLower(zoom_ranges.x());
iop->xAxis->setRangeUpper(zoom_ranges.x() + zoom_ranges.width());
iop->yAxis->setRangeLower(zoom_ranges.y());
iop->yAxis->setRangeUpper(zoom_ranges.y() + zoom_ranges.height());
iop->replot();
}
}
} else if (iop->cursor().shape() == Qt::ClosedHandCursor) {
iop->setCursor(QCursor(Qt::OpenHandCursor));
}
}
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);
double axis_pixels = iop->xAxis->axisRect()->width();
iop->xAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, x_range.center());
axis_pixels = iop->yAxis->axisRect()->height();
iop->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, iop->yAxis->range().center());
auto_axes_ = true;
iop->replot();
}
void IOGraphDialog::updateStatistics()
{
if (!isVisible()) return;
if (need_retap_ && !file_closed_) {
need_retap_ = false;
cap_file_.retapPackets();
// The user might have closed the window while tapping, which means
// we might no longer exist.
} else {
if (need_recalc_ && !file_closed_) {
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);
if (!tracer_->graph()) {
if (base_graph_ && base_graph_->data()->size() > 0) {
tracer_->setGraph(base_graph_);
tracer_->setVisible(true);
} else {
tracer_->setVisible(false);
}
}
}
if (need_replot_) {
need_replot_ = false;
if (auto_axes_) {
resetAxes();
}
ui->ioPlot->replot();
}
}
}
void IOGraphDialog::loadProfileGraphs()
{
if (iog_uat_ == NULL) {
iog_uat_ = uat_new("I/O Graphs",
sizeof(io_graph_settings_t),
"io_graphs",
TRUE,
&iog_settings_,
&num_io_graphs_,
0, /* doesn't affect anything that requires a GUI update */
"ChStatIOGraphs",
io_graph_copy_cb,
NULL,
io_graph_free_cb,
NULL,
NULL,
io_graph_fields);
char* err = NULL;
if (!uat_load(iog_uat_, NULL, &err)) {
report_failure("Error while loading %s: %s. Default graph values will be used", iog_uat_->name, err);
g_free(err);
uat_clear(iog_uat_);
}
}
uat_model_ = new UatModel(NULL, iog_uat_);
uat_delegate_ = new UatDelegate;
ui->graphUat->setModel(uat_model_);
ui->graphUat->setItemDelegate(uat_delegate_);
connect(uat_model_, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(modelDataChanged(QModelIndex)));
connect(uat_model_, SIGNAL(modelReset()), this, SLOT(modelRowsReset()));
}
// Slots
void IOGraphDialog::on_intervalComboBox_currentIndexChanged(int)
{
int interval = ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt();
bool need_retap = false;
if (uat_model_ != NULL) {
for (int row = 0; row < uat_model_->rowCount(); row++) {
IOGraph *iog = ioGraphs_.value(row, NULL);
if (iog) {
iog->setInterval(interval);
if (iog->visible()) {
need_retap = true;
}
}
}
}
if (need_retap) {
scheduleRetap(true);
}
updateLegend();
}
void IOGraphDialog::on_todCheckBox_toggled(bool checked)
{
double orig_start = start_time_;
bool orig_auto = auto_axes_;
ui->ioPlot->xAxis->setTickLabelType(checked ? QCPAxis::ltDateTime : QCPAxis::ltNumber);
auto_axes_ = false;
scheduleRecalc(true);
auto_axes_ = orig_auto;
getGraphInfo();
ui->ioPlot->xAxis->moveRange(start_time_ - orig_start);
mouseMoved(NULL); // Update hint
}
void IOGraphDialog::modelRowsReset()
{
ui->deleteToolButton->setEnabled(false);
ui->copyToolButton->setEnabled(false);
ui->clearToolButton->setEnabled(uat_model_->rowCount() != 0);
}
void IOGraphDialog::on_graphUat_currentItemChanged(const QModelIndex &current, const QModelIndex&)
{
if (current.isValid()) {
ui->deleteToolButton->setEnabled(true);
ui->copyToolButton->setEnabled(true);
ui->clearToolButton->setEnabled(true);
} else {
ui->deleteToolButton->setEnabled(false);
ui->copyToolButton->setEnabled(false);
ui->clearToolButton->setEnabled(false);
}
}
void IOGraphDialog::modelDataChanged(const QModelIndex &index)
{
bool recalc = false;
switch (index.column())
{
case colYAxis:
case colSMAPeriod:
recalc = true;
}
syncGraphSettings(index.row());
if (recalc) {
scheduleRecalc(true);
} else {
scheduleReplot(true);
}
}
void IOGraphDialog::on_resetButton_clicked()
{
resetAxes();
}
void IOGraphDialog::on_newToolButton_clicked()
{
addGraph();
}
void IOGraphDialog::on_deleteToolButton_clicked()
{
const QModelIndex &current = ui->graphUat->currentIndex();
if (uat_model_ && current.isValid()) {
delete ioGraphs_[current.row()];
ioGraphs_.remove(current.row());
if (!uat_model_->removeRows(current.row(), 1)) {
qDebug() << "Failed to remove row";
}
}
// We should probably be smarter about this.
hint_err_.clear();
mouseMoved(NULL);
}
void IOGraphDialog::on_copyToolButton_clicked()
{
addGraph(true);
}
void IOGraphDialog::on_clearToolButton_clicked()
{
if (uat_model_) {
foreach(IOGraph* iog, ioGraphs_) {
delete iog;
}
ioGraphs_.clear();
uat_model_->clearAll();
}
hint_err_.clear();
mouseMoved(NULL);
}
void IOGraphDialog::on_dragRadioButton_toggled(bool checked)
{
if (checked) mouse_drags_ = true;
ui->ioPlot->setInteractions(
QCP::iRangeDrag |
QCP::iRangeZoom
);
}
void IOGraphDialog::on_zoomRadioButton_toggled(bool checked)
{
if (checked) mouse_drags_ = false;
ui->ioPlot->setInteractions(0);
}
void IOGraphDialog::on_logCheckBox_toggled(bool checked)
{
QCustomPlot *iop = ui->ioPlot;
iop->yAxis->setScaleType(checked ? QCPAxis::stLogarithmic : QCPAxis::stLinear);
iop->replot();
}
void IOGraphDialog::on_actionReset_triggered()
{
on_resetButton_clicked();
}
void IOGraphDialog::on_actionZoomIn_triggered()
{
zoomAxes(true);
}
void IOGraphDialog::on_actionZoomInX_triggered()
{
zoomXAxis(true);
}
void IOGraphDialog::on_actionZoomInY_triggered()
{
zoomYAxis(true);
}
void IOGraphDialog::on_actionZoomOut_triggered()
{
zoomAxes(false);
}
void IOGraphDialog::on_actionZoomOutX_triggered()
{
zoomXAxis(false);
}
void IOGraphDialog::on_actionZoomOutY_triggered()
{
zoomYAxis(false);
}
void IOGraphDialog::on_actionMoveUp10_triggered()
{
panAxes(0, 10);
}
void IOGraphDialog::on_actionMoveLeft10_triggered()
{
panAxes(-10, 0);
}
void IOGraphDialog::on_actionMoveRight10_triggered()
{
panAxes(10, 0);
}
void IOGraphDialog::on_actionMoveDown10_triggered()
{
panAxes(0, -10);
}
void IOGraphDialog::on_actionMoveUp1_triggered()
{
panAxes(0, 1);
}
void IOGraphDialog::on_actionMoveLeft1_triggered()
{
panAxes(-1, 0);
}
void IOGraphDialog::on_actionMoveRight1_triggered()
{
panAxes(1, 0);
}
void IOGraphDialog::on_actionMoveDown1_triggered()
{
panAxes(0, -1);
}
void IOGraphDialog::on_actionGoToPacket_triggered()
{
if (tracer_->visible() && !file_closed_ && packet_num_ > 0) {
emit goToPacket(packet_num_);
}
}
void IOGraphDialog::on_actionDragZoom_triggered()
{
if (mouse_drags_) {
ui->zoomRadioButton->toggle();
} else {
ui->dragRadioButton->toggle();
}
}
void IOGraphDialog::on_actionToggleTimeOrigin_triggered()
{
}
void IOGraphDialog::on_actionCrosshairs_triggered()
{
}
void IOGraphDialog::on_buttonBox_helpRequested()
{
wsApp->helpTopicAction(HELP_STATS_IO_GRAPH_DIALOG);
}
// XXX - Copied from tcp_stream_dialog. This should be common code.
void IOGraphDialog::on_buttonBox_accepted()
{
QString file_name, extension;
QDir path(wsApp->lastOpenDir());
QString pdf_filter = tr("Portable Document Format (*.pdf)");
QString png_filter = tr("Portable Network Graphics (*.png)");
QString bmp_filter = tr("Windows Bitmap (*.bmp)");
// Gaze upon my beautiful graph with lossy artifacts!
QString jpeg_filter = tr("JPEG File Interchange Format (*.jpeg *.jpg)");
QString csv_filter = tr("Comma Separated Values (*.csv)");
QString filter = QString("%1;;%2;;%3;;%4;;%5")
.arg(pdf_filter)
.arg(png_filter)
.arg(bmp_filter)
.arg(jpeg_filter)
.arg(csv_filter);
QString save_file = path.canonicalPath();
if (!file_closed_) {
save_file += QString("/%1").arg(cap_file_.fileBaseName());
}
file_name = WiresharkFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save Graph As" UTF8_HORIZONTAL_ELLIPSIS)),
save_file, filter, &extension);
if (file_name.length() > 0) {
bool save_ok = false;
if (extension.compare(pdf_filter) == 0) {
save_ok = ui->ioPlot->savePdf(file_name);
} else if (extension.compare(png_filter) == 0) {
save_ok = ui->ioPlot->savePng(file_name);
} else if (extension.compare(bmp_filter) == 0) {
save_ok = ui->ioPlot->saveBmp(file_name);
} else if (extension.compare(jpeg_filter) == 0) {
save_ok = ui->ioPlot->saveJpg(file_name);
} else if (extension.compare(csv_filter) == 0) {
save_ok = saveCsv(file_name);
}
// else error dialog?
if (save_ok) {
path = QDir(file_name);
wsApp->setLastOpenDir(path.canonicalPath().toUtf8().constData());
}
}
}
void IOGraphDialog::makeCsv(QTextStream &stream) const
{
QList<IOGraph *> activeGraphs;
int ui_interval = ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt();
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;
}
}
}
stream << endl;
for (int interval = 0; interval <= max_interval; interval++) {
double interval_start = (double)interval * ((double)ui_interval / 1000.0);
stream << interval_start;
foreach (IOGraph *iog, activeGraphs) {
double value = 0.0;
if (interval <= iog->maxInterval()) {
value = iog->getItemValue(interval, cap_file_.capFile());
}
stream << "," << value;
}
stream << endl;
}
}
void IOGraphDialog::copyAsCsvClicked()
{
QString csv;
QTextStream stream(&csv, QIODevice::Text);
makeCsv(stream);
wsApp->clipboard()->setText(stream.readAll());
}
bool IOGraphDialog::saveCsv(const QString &file_name) const
{
QFile save_file(file_name);
save_file.open(QFile::WriteOnly);
QTextStream out(&save_file);
makeCsv(out);
return true;
}
// IOGraph
IOGraph::IOGraph(QCustomPlot *parent) :
parent_(parent),
visible_(false),
graph_(NULL),
bars_(NULL),
val_units_(IOG_ITEM_UNIT_FIRST),
hf_index_(-1),
cur_idx_(-1)
{
Q_ASSERT(parent_ != NULL);
graph_ = parent_->addGraph(parent_->xAxis, parent_->yAxis);
Q_ASSERT(graph_ != NULL);
GString *error_string;
error_string = register_tap_listener("frame",
this,
"",
TL_REQUIRES_PROTO_TREE,
tapReset,
tapPacket,
tapDraw,
NULL);
if (error_string) {
// QMessageBox::critical(this, tr("%1 failed to register tap listener").arg(name_),
// error_string->str);
// config_err_ = error_string->str;
g_string_free(error_string, TRUE);
}
}
IOGraph::~IOGraph() {
remove_tap_listener(this);
if (graph_) {
parent_->removeGraph(graph_);
}
if (bars_) {
parent_->removePlottable(bars_);
}
}
// 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)
{
GString *error_string;
QString full_filter(filter.trimmed());
config_err_.clear();
// Make sure we have a good display filter
if (!full_filter.isEmpty()) {
dfilter_t *dfilter;
bool status;
gchar *err_msg;
status = dfilter_compile(full_filter.toUtf8().constData(), &dfilter, &err_msg);
dfilter_free(dfilter);
if (!status) {
config_err_ = QString::fromUtf8(err_msg);
g_free(err_msg);
filter_ = full_filter;
return;
}
}
// Check our value unit + field combo.
error_string = check_field_unit(vu_field_.toUtf8().constData(), NULL, val_units_);
if (error_string) {
config_err_ = error_string->str;
g_string_free(error_string, TRUE);
return;
}
// Make sure vu_field_ survives edt tree pruning by adding it to our filter
// expression.
if (val_units_ >= IOG_ITEM_UNIT_CALC_SUM && !vu_field_.isEmpty() && hf_index_ >= 0) {
if (full_filter.isEmpty()) {
full_filter = vu_field_;
} else {
full_filter += QString(" && (%1)").arg(vu_field_);
}
}
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();
}
filter_ = filter;
}
}
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_->setBrush(color_);
}
}
void IOGraph::setVisible(bool visible)
{
bool old_visibility = visible_;
visible_ = visible;
if (graph_) {
graph_->setVisible(visible_);
}
if (bars_) {
bars_->setVisible(visible_);
}
if (old_visibility != visible_) {
emit requestReplot();
}
}
void IOGraph::setName(const QString &name)
{
name_ = name;
if (graph_) {
graph_->setName(name_);
}
if (bars_) {
bars_->setName(name_);
}
}
QRgb IOGraph::color()
{
return color_.color().rgb();
}
void IOGraph::setColor(const QRgb color)
{
color_ = QBrush(color);
applyCurrentColor();
}
void IOGraph::setPlotStyle(int style)
{
// Switch plottable if needed
switch (style) {
case psBar:
case psStackedBar:
if (graph_) {
bars_ = new QCPBars(parent_->xAxis, parent_->yAxis);
parent_->addPlottable(bars_);
parent_->removeGraph(graph_);
graph_ = NULL;
}
break;
default:
if (bars_) {
graph_ = parent_->addGraph(parent_->xAxis, parent_->yAxis);
parent_->removePlottable(bars_);
bars_ = NULL;
}
break;
}
setValueUnits(val_units_);
if (graph_) {
graph_->setLineStyle(QCPGraph::lsNone);
graph_->setScatterStyle(QCPScatterStyle::ssNone);
}
switch (style) {
case psLine:
if (graph_) {
graph_->setLineStyle(QCPGraph::lsLine);
}
break;
case psImpulse:
if (graph_) {
graph_->setLineStyle(QCPGraph::lsImpulse);
}
break;
case psDot:
if (graph_) {
graph_->setScatterStyle(QCPScatterStyle::ssDisc);
}
break;
case psSquare:
if (graph_) {
graph_->setScatterStyle(QCPScatterStyle::ssSquare);
}
break;
case psDiamond:
if (graph_) {
graph_->setScatterStyle(QCPScatterStyle::ssDiamond);
}
break;
case psBar:
case IOGraph::psStackedBar:
// Stacking set in scanGraphs
bars_->moveBelow(NULL);
break;
}
setName(name_);
applyCurrentColor();
}
const QString IOGraph::valueUnitLabel()
{
return val_to_str_const(val_units_, y_axis_vs, "Unknown");
}
void IOGraph::setValueUnits(int val_units)
{
if (val_units >= IOG_ITEM_UNIT_FIRST && val_units <= IOG_ITEM_UNIT_LAST) {
int old_val_units = 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();
}
}
}
}
void IOGraph::setValueUnitField(const QString &vu_field)
{
int old_hf_index = hf_index_;
vu_field_ = vu_field.trimmed();
hf_index_ = -1;
header_field_info *hfi = proto_registrar_get_byname(vu_field_.toUtf8().constData());
if (hfi) {
hf_index_ = hfi->id;
}
if (old_hf_index != hf_index_) {
setFilter(filter_); // Check config & prime vu field
}
}
bool IOGraph::addToLegend()
{
if (graph_) {
return graph_->addToLegend();
}
if (bars_) {
return bars_->addToLegend();
}
return false;
}
bool IOGraph::removeFromLegend()
{
if (graph_) {
return graph_->removeFromLegend();
}
if (bars_) {
return bars_->removeFromLegend();
}
return false;
}
double IOGraph::startOffset()
{
if (graph_ && graph_->keyAxis()->tickLabelType() == QCPAxis::ltDateTime && graph_->data()->size() > 0) {
return graph_->data()->keys()[0];
}
if (bars_ && bars_->keyAxis()->tickLabelType() == QCPAxis::ltDateTime && bars_->data()->size() > 0) {
return bars_->data()->keys()[0];
}
return 0.0;
}
int IOGraph::packetFromTime(double ts)
{
int idx = ts * 1000 / interval_;
if (idx >= 0 && idx < (int) cur_idx_) {
switch (val_units_) {
case IOG_ITEM_UNIT_CALC_MAX:
case IOG_ITEM_UNIT_CALC_MIN:
return items_[idx].extreme_frame_in_invl;
default:
return items_[idx].last_frame_in_invl;
}
}
return -1;
}
void IOGraph::clearAllData()
{
cur_idx_ = -1;
reset_io_graph_items(items_, max_io_items_);
if (graph_) {
graph_->clearData();
}
if (bars_) {
bars_->clearData();
}
start_time_ = 0.0;
}
void IOGraph::recalcGraphData(capture_file *cap_file, bool enable_scaling)
{
/* Moving average variables */
unsigned int mavg_in_average_count = 0, mavg_left = 0, mavg_right = 0;
unsigned int mavg_to_remove = 0, mavg_to_add = 0;
double mavg_cumulated = 0;
QCPAxis *x_axis = NULL;
if (graph_) {
graph_->clearData();
x_axis = graph_->keyAxis();
}
if (bars_) {
bars_->clearData();
x_axis = bars_->keyAxis();
}
if (moving_avg_period_ > 0 && cur_idx_ >= 0) {
/* "Warm-up phase" - calculate average on some data not displayed;
* just to make sure average on leftmost and rightmost displayed
* values is as reliable as possible
*/
guint64 warmup_interval = 0;
// for (; warmup_interval < first_interval; warmup_interval += interval_) {
// mavg_cumulated += get_it_value(io, i, (int)warmup_interval/interval_);
// mavg_in_average_count++;
// mavg_left++;
// }
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 += interval_) {
mavg_cumulated += getItemValue((int)warmup_interval / interval_, cap_file);
mavg_in_average_count++;
mavg_right++;
}
mavg_to_add = (unsigned int)warmup_interval;
}
for (int i = 0; i <= cur_idx_; i++) {
double ts = (double) i * interval_ / 1000;
if (x_axis && x_axis->tickLabelType() == QCPAxis::ltDateTime) {
ts += start_time_;
}
double val = getItemValue(i, cap_file);
if (moving_avg_period_ > 0) {
if (i != 0) {
mavg_left++;
if (mavg_left > moving_avg_period_ / 2) {
mavg_left--;
mavg_in_average_count--;
mavg_cumulated -= getItemValue((int)mavg_to_remove / interval_, cap_file);
mavg_to_remove += interval_;
}
if (mavg_to_add <= (unsigned int) cur_idx_ * interval_) {
mavg_in_average_count++;
mavg_cumulated += getItemValue((int)mavg_to_add / interval_, cap_file);
mavg_to_add += interval_;
} else {
mavg_right--;
}
}
if (mavg_in_average_count > 0) {
val = mavg_cumulated / mavg_in_average_count;
}
}
if (graph_) {
graph_->addData(ts, val);
}
if (bars_) {
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()
{
// 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_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);
}
}
}
template<class DataMap>
double IOGraph::maxValueFromGraphData(const DataMap &map)
{
double maxValue = 0;
typename DataMap::const_iterator it = map.constBegin();
while (it != map.constEnd()) {
maxValue = MAX(fabs((*it).value), maxValue);
++it;
}
return maxValue;
}
template<class DataMap>
void IOGraph::scaleGraphData(DataMap &map, int scalar)
{
if (scalar != 1) {
typename DataMap::iterator it = map.begin();
while (it != map.end()) {
(*it).value *= scalar;
++it;
}
}
}
void IOGraph::captureEvent(CaptureEvent e)
{
if ((e.captureContext() == CaptureEvent::File) &&
(e.eventType() == CaptureEvent::Closing))
{
remove_tap_listener(this);
}
}
void IOGraph::reloadValueUnitField()
{
if (vu_field_.length() > 0) {
setValueUnitField(vu_field_);
}
}
void IOGraph::setInterval(int interval)
{
interval_ = interval;
}
// Get the value at the given interval (idx) for the current value unit.
double IOGraph::getItemValue(int idx, const capture_file *cap_file) const
{
g_assert(idx < max_io_items_);
return get_io_graph_item(items_, val_units_, idx, hf_index_, cap_file, interval_, cur_idx_);
}
// "tap_reset" callback for register_tap_listener
void IOGraph::tapReset(void *iog_ptr)
{
IOGraph *iog = static_cast<IOGraph *>(iog_ptr);
if (!iog) return;
// qDebug() << "=tapReset" << iog->name_;
iog->clearAllData();
}
// "tap_packet" callback for register_tap_listener
tap_packet_status IOGraph::tapPacket(void *iog_ptr, packet_info *pinfo, epan_dissect_t *edt, const void *)
{
IOGraph *iog = static_cast<IOGraph *>(iog_ptr);
if (!pinfo || !iog) {
return TAP_PACKET_DONT_REDRAW;
}
int 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;
return TAP_PACKET_DONT_REDRAW;
}
/* update num_items */
if (idx > iog->cur_idx_) {
iog->cur_idx_ = (guint32) 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);
}
epan_dissect_t *adv_edt = NULL;
/* For ADVANCED mode we need to keep track of some more stuff than just frame and byte counts */
if (iog->val_units_ >= IOG_ITEM_UNIT_CALC_SUM) {
adv_edt = edt;
}
if (!update_io_graph_item(iog->items_, idx, pinfo, adv_edt, iog->hf_index_, iog->val_units_, iog->interval_)) {
return TAP_PACKET_DONT_REDRAW;
}
// qDebug() << "=tapPacket" << iog->name_ << idx << iog->hf_index_ << iog->val_units_ << iog->num_items_;
if (recalc) {
emit iog->requestRecalc();
}
return TAP_PACKET_REDRAW;
}
// "tap_draw" callback for register_tap_listener
void IOGraph::tapDraw(void *iog_ptr)
{
IOGraph *iog = static_cast<IOGraph *>(iog_ptr);
if (!iog) return;
emit iog->requestRecalc();
if (iog->graph_) {
// qDebug() << "=tapDraw g" << iog->name_ << iog->graph_->data()->keys().size();
}
if (iog->bars_) {
// qDebug() << "=tapDraw b" << iog->name_ << iog->bars_->data()->keys().size();
}
}
// Stat command + args
static void
io_graph_init(const char *, void*) {
wsApp->emitStatCommandSignal("IOGraph", NULL, NULL);
}
static stat_tap_ui io_stat_ui = {
REGISTER_STAT_GROUP_GENERIC,
NULL,
"io,stat",
io_graph_init,
0,
NULL
};
extern "C" {
void
register_tap_listener_qt_iostat(void)
{
register_stat_tap_ui(&io_stat_ui, NULL);
}
}
/*
* Editor modelines
*
* Local Variables:
* c-basic-offset: 4
* tab-width: 8
* indent-tabs-mode: nil
* End:
*
* ex: set shiftwidth=4 tabstop=8 expandtab:
* :indentSize=4:tabSize=8:noTabs=true:
*/