wireshark/ui/qt/io_graph_dialog.cpp

2197 lines
66 KiB
C++

/* io_graph_dialog.cpp
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
* Copyright 1998 Gerald Combs
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "io_graph_dialog.h"
#include "ui_io_graph_dialog.h"
#include <epan/stat_tap_ui.h>
#include "epan/stats_tree_priv.h"
#include "epan/uat-int.h"
#include "qt_ui_utils.h"
#include "tango_colors.h"
#include "wireshark_application.h"
#include <QClipboard>
#include <QComboBox>
#include <QFileDialog>
#include <QFontMetrics>
#include <QFrame>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPushButton>
#include <QSpacerItem>
#include <QTreeWidget>
#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:
// http://www.qcustomplot.com/index.php/support/forum/62
// - You can't manually set a graph color other than manually editing the io_graphs
// UAT. We should add a "graph color" preference.
// - We retap and redraw more than we should.
// - Smoothing doesn't seem to match GTK+
// - We don't register a tap listener ("-z io,stat", bottom of gtk/io_stat.c)
// To do:
// - Use scroll bars?
// - Scroll during live captures
// - Set ticks per pixel (e.g. pressing "2" sets 2 tpp).
const int name_col_ = 0;
const int dfilter_col_ = 1;
const int color_col_ = 2;
const int style_col_ = 3;
const int yaxis_col_ = 4;
const int yfield_col_ = 5;
const int sma_period_col_ = 6;
const int num_cols_ = 7;
// Available colors
// XXX - Add custom
QList<QRgb> colors_ = QList<QRgb>()
<< tango_aluminium_6 // Bar outline (use black instead)?
<< tango_sky_blue_5
<< tango_butter_6
<< tango_chameleon_5
<< tango_scarlet_red_5
<< tango_plum_5
<< tango_orange_6
<< tango_aluminium_3
<< tango_sky_blue_3
<< tango_butter_3
<< tango_chameleon_3
<< tango_scarlet_red_3
<< tango_plum_3
<< tango_orange_3;
const qreal graph_line_width_ = 1.0;
// When we drop support for Qt <5 we can initialize these with
// datastreams.
const QMap<io_graph_item_unit_t, QString> value_unit_to_name_ = IOGraph::valueUnitsToNames();
const QMap<IOGraph::PlotStyles, QString> plot_style_to_name_ = IOGraph::plotStylesToNames();
const QMap<int, QString> moving_average_to_name_ = IOGraph::movingAveragesToNames();
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
static const value_string graph_enabled_vs[] = {
{ 0, "Disabled" },
{ 1, "Enabled" },
{ 0, NULL }
};
typedef struct _io_graph_settings_t {
guint32 enabled;
char* name;
char* dfilter;
char* color;
char* style;
char* yaxis;
char* yfield;
int sma_period;
} io_graph_settings_t;
static io_graph_settings_t *iog_settings_ = NULL;
static guint num_io_graphs_ = 0;
static uat_t *iog_uat_ = NULL;
extern "C" {
UAT_VS_DEF(io_graph, enabled, io_graph_settings_t, guint32, 0, "Disabled")
UAT_CSTRING_CB_DEF(io_graph, name, io_graph_settings_t)
UAT_CSTRING_CB_DEF(io_graph, dfilter, io_graph_settings_t)
UAT_CSTRING_CB_DEF(io_graph, color, io_graph_settings_t)
UAT_CSTRING_CB_DEF(io_graph, style, io_graph_settings_t)
UAT_CSTRING_CB_DEF(io_graph, yaxis, io_graph_settings_t)
UAT_CSTRING_CB_DEF(io_graph, yfield, io_graph_settings_t)
UAT_DEC_CB_DEF(io_graph, sma_period, io_graph_settings_t)
static uat_field_t io_graph_fields[] = {
UAT_FLD_VS(io_graph, enabled, "Enabled", graph_enabled_vs, "Graph visibility"),
UAT_FLD_CSTRING(io_graph, name, "Graph Name", "The name of the graph"),
UAT_FLD_CSTRING(io_graph, dfilter, "Display Filter", "Graph packets matching this display filter"),
UAT_FLD_CSTRING(io_graph, color, "Color", "Graph color (#RRGGBB)"),
UAT_FLD_CSTRING(io_graph, style, "Style", "Graph style (Line, Bars, etc.)"),
UAT_FLD_CSTRING(io_graph, yaxis, "Y Axis", "Y Axis units"),
UAT_FLD_CSTRING(io_graph, yfield, "Y Field", "Apply calculations to this field"),
UAT_FLD_DEC(io_graph, sma_period, "SMA Period", "Simple moving average period"),
UAT_END_FIELDS
};
static void* io_graph_copy_cb(void* dst_ptr, const void* src_ptr, size_t len) {
Q_UNUSED(len);
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 = g_strdup(src->color);
dst->style = g_strdup(src->style);
dst->yaxis = g_strdup(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->color);
g_free(iogs->yfield);
}
} // extern "C"
Q_DECLARE_METATYPE(IOGraph *)
IOGraphDialog::IOGraphDialog(QWidget *parent, capture_file *cf) :
QDialog(parent),
ui(new Ui::IOGraphDialog),
cap_file_(cf),
name_line_edit_(NULL),
dfilter_line_edit_(NULL),
yfield_line_edit_(NULL),
color_combo_box_(NULL),
style_combo_box_(NULL),
yaxis_combo_box_(NULL),
sma_combo_box_(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);
setAttribute(Qt::WA_DeleteOnClose, true);
QCustomPlot *iop = ui->ioPlot;
QPushButton *save_bt = ui->buttonBox->button(QDialogButtonBox::Save);
save_bt->setText(tr("Save As..."));
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("0.001 sec"), 1);
ui->intervalComboBox->addItem(tr("0.01 sec"), 10);
ui->intervalComboBox->addItem(tr("0.1 sec"), 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(3);
ui->todCheckBox->setChecked(false);
ui->dragRadioButton->setChecked(mouse_drags_);
ctx_menu_.addAction(ui->actionZoomIn);
ctx_menu_.addAction(ui->actionZoomOut);
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);
QString dlg_title = tr("Wireshark IO Graphs: ");
if (cap_file_) {
dlg_title += cf_get_display_name(cap_file_);
} else {
dlg_title += tr("No Capture Data");
}
setWindowTitle(dlg_title);
QCPPlotTitle *title = new QCPPlotTitle(iop);
iop->plotLayout()->insertRow(0);
iop->plotLayout()->addElement(0, 0, title);
title->setText(dlg_title);
tracer_ = new QCPItemTracer(iop);
iop->addItem(tracer_);
loadProfileGraphs();
if (num_io_graphs_ > 0) {
for (guint i = 0; i < num_io_graphs_; i++) {
io_graph_settings_t *iogs = &iog_settings_[i];
QRgb pcolor = QColor(iogs->color).rgb();
int color_idx;
IOGraph::PlotStyles style = plot_style_to_name_.key(iogs->style, IOGraph::psLine);
io_graph_item_unit_t value_units = value_unit_to_name_.key(iogs->yaxis, IOG_ITEM_UNIT_PACKETS);
for (color_idx = 0; color_idx < colors_.size(); color_idx++) {
if (pcolor == colors_[color_idx]) break;
}
if (color_idx >= colors_.size()) {
colors_ << pcolor;
}
addGraph(iogs->enabled == 1, iogs->name, iogs->dfilter, color_idx, style, value_units, iogs->yfield, iogs->sma_period);
}
} else {
addDefaultGraph(true, 0);
addDefaultGraph(true, 1);
}
on_graphTreeWidget_itemSelectionChanged();
toggleTracerStyle(true);
iop->setFocus();
iop->rescaleAxes();
// Shrink columns down, then expand as needed
QTreeWidget *gtw = ui->graphTreeWidget;
int one_em = fontMetrics().height();
gtw->setRootIsDecorated(false);
gtw->setColumnWidth(name_col_, one_em * 10);
gtw->setColumnWidth(dfilter_col_, one_em * 10);
gtw->setColumnWidth(color_col_, one_em * 2.5);
gtw->setColumnWidth(style_col_, one_em * 5.5);
gtw->setColumnWidth(yaxis_col_, one_em * 6.5);
gtw->setColumnWidth(yfield_col_, one_em * 6);
gtw->setColumnWidth(sma_period_col_, one_em * 6);
connect(wsApp, SIGNAL(focusChanged(QWidget*,QWidget*)), this, SLOT(focusChanged(QWidget*,QWidget*)));
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()
{
for (int i = 0; i < ui->graphTreeWidget->topLevelItemCount(); i++) {
IOGraph *iog = qvariant_cast<IOGraph *>(ui->graphTreeWidget->topLevelItem(i)->data(name_col_, Qt::UserRole));
delete iog;
}
delete ui;
}
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)
{
QTreeWidgetItem *ti = new QTreeWidgetItem();
ui->graphTreeWidget->addTopLevelItem(ti);
IOGraph *iog = new IOGraph(ui->ioPlot);
ti->setData(name_col_, Qt::UserRole, qVariantFromValue(iog));
ti->setCheckState(name_col_, checked ? Qt::Checked : Qt::Unchecked);
ti->setText(name_col_, name);
ti->setText(dfilter_col_, dfilter);
color_idx = color_idx % colors_.size();
ti->setData(color_col_, Qt::UserRole, color_idx);
ti->setIcon(color_col_, graphColorIcon(color_idx));
ti->setText(style_col_, plot_style_to_name_[style]);
ti->setData(style_col_, Qt::UserRole, style);
ti->setText(yaxis_col_, value_unit_to_name_[value_units]);
ti->setData(yaxis_col_, Qt::UserRole, value_units);
ti->setText(yfield_col_, yfield);
ti->setText(sma_period_col_, moving_average_to_name_[moving_average]);
ti->setData(sma_period_col_, Qt::UserRole, moving_average);
connect(this, SIGNAL(recalcGraphData(capture_file *)), iog, SLOT(recalcGraphData(capture_file *)));
connect(iog, SIGNAL(requestRetap()), this, SLOT(scheduleRetap()));
connect(iog, SIGNAL(requestRecalc()), this, SLOT(scheduleRecalc()));
connect(iog, SIGNAL(requestReplot()), this, SLOT(scheduleReplot()));
syncGraphSettings(ti);
if (iog->visible()) {
scheduleRetap();
}
}
void IOGraphDialog::addGraph(bool copy_from_current)
{
QTreeWidgetItem *cur_ti = NULL;
if (copy_from_current) {
cur_ti = ui->graphTreeWidget->currentItem();
}
if (copy_from_current && cur_ti) {
addGraph(cur_ti->checkState(name_col_) == Qt::Checked,
cur_ti->text(name_col_),
cur_ti->text(dfilter_col_),
cur_ti->data(color_col_, Qt::UserRole).toInt(),
(IOGraph::PlotStyles)cur_ti->data(style_col_, Qt::UserRole).toInt(),
(io_graph_item_unit_t)cur_ti->data(yaxis_col_, Qt::UserRole).toInt(),
cur_ti->text(yfield_col_),
cur_ti->data(sma_period_col_, Qt::UserRole).toInt());
} else {
addDefaultGraph(false);
}
}
void IOGraphDialog::addDefaultGraph(bool enabled, int idx)
{
switch (idx % 2) {
case 0:
addGraph(enabled, tr("All packets"), QString(), ui->graphTreeWidget->topLevelItemCount(),
IOGraph::psLine, IOG_ITEM_UNIT_PACKETS, QString(), default_moving_average_);
break;
default:
addGraph(enabled, tr("TCP errors"), "tcp.analysis.flags", ui->graphTreeWidget->topLevelItemCount(),
IOGraph::psBar, IOG_ITEM_UNIT_PACKETS, QString(), default_moving_average_);
break;
}
}
// Sync the settings from a graphTreeWidget item to its IOGraph.
// Disables the graph if any errors are found.
void IOGraphDialog::syncGraphSettings(QTreeWidgetItem *item)
{
if (!item) return;
IOGraph *iog = item->data(name_col_, Qt::UserRole).value<IOGraph *>();
if (!iog) return;
bool visible = item->checkState(name_col_) == Qt::Checked;
bool retap = !iog->visible() && visible;
iog->setName(item->text(name_col_));
iog->setFilter(item->text(dfilter_col_));
iog->setColor(colors_[item->data(color_col_, Qt::UserRole).toInt() % colors_.size()]);
iog->setPlotStyle(item->data(style_col_, Qt::UserRole).toInt());
iog->setValueUnits(item->data(yaxis_col_, Qt::UserRole).toInt());
iog->setValueUnitField(item->text(yfield_col_));
iog->moving_avg_period_ = item->data(sma_period_col_, Qt::UserRole).toUInt();
iog->setInterval(ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt());
ui->graphTreeWidget->blockSignals(true); // setFlags emits itemChanged
if (!iog->configError().isEmpty()) {
hint_err_ = iog->configError();
visible = false;
retap = false;
// On OS X the "not user checkable" checkbox isn't obviously disabled.
// For now show it as partially checked.
item->setCheckState(name_col_, Qt::PartiallyChecked);
item->setFlags(item->flags() & ~Qt::ItemIsUserCheckable);
} else {
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
}
ui->graphTreeWidget->blockSignals(false);
iog->setVisible(visible);
getGraphInfo();
mouseMoved(NULL); // Update hint
updateLegend();
if (visible) {
if (retap) {
scheduleRetap();
} else {
scheduleReplot();
}
}
}
void IOGraphDialog::setCaptureFile(capture_file *cf)
{
if (!cf) { // We only want to know when the file closes.
cap_file_ = NULL;
}
}
void IOGraphDialog::scheduleReplot(bool now)
{
need_replot_ = true;
if (now) updateStatistics();
}
void IOGraphDialog::scheduleRecalc(bool now)
{
need_recalc_ = true;
if (now) updateStatistics();
}
void IOGraphDialog::scheduleRetap(bool now)
{
need_retap_ = true;
if (now) updateStatistics();
}
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_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()
{
// Catch escape keys.
QList<QWidget *>editors = QList<QWidget *>() << name_line_edit_ << dfilter_line_edit_ << yfield_line_edit_;
foreach (QWidget *w, editors) {
if (w && w->hasFocus()) {
ui->graphTreeWidget->setFocus(); // Trigger itemEditingFinished
return;
}
}
QList<QComboBox *>combos = QList<QComboBox *>() << color_combo_box_ << style_combo_box_ <<
yaxis_combo_box_ << sma_combo_box_;
foreach (QComboBox *cb, combos) {
if (cb && (cb->hasFocus() || cb->view()->hasFocus())) {
ui->graphTreeWidget->setFocus(); // Trigger itemEditingFinished
return;
}
}
if (iog_uat_) {
uat_clear(iog_uat_);
for (int i = 0; i < ui->graphTreeWidget->topLevelItemCount(); i++) {
QTreeWidgetItem *item = ui->graphTreeWidget->topLevelItem(i);
IOGraph *iog = NULL;
if (item) {
iog = item->data(name_col_, Qt::UserRole).value<IOGraph *>();
io_graph_settings_t iogs;
QColor color(iog->color());
iogs.enabled = iog->visible() ? 1 : 0;
iogs.name = qstring_strdup(iog->name());
iogs.dfilter = qstring_strdup(iog->filter());
iogs.color = qstring_strdup(color.name());
iogs.style = qstring_strdup(plot_style_to_name_[(IOGraph::PlotStyles)item->data(style_col_, Qt::UserRole).toInt()]);
iogs.yaxis = qstring_strdup(iog->valueUnitLabel());
iogs.yfield = qstring_strdup(iog->valueUnitField());
iogs.sma_period = iog->movingAveragePeriod();
uat_add_record(iog_uat_, &iogs, TRUE);
io_graph_free_cb(&iogs);
}
}
const char* err = NULL;
uat_save(iog_uat_, &err);
}
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::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();
}
}
QIcon IOGraphDialog::graphColorIcon(int color_idx)
{
int h = fontMetrics().height() * 3 / 4;
QPixmap pm(h * 2, h);
pm.fill(colors_[color_idx % colors_.size()]);
return QIcon(pm);
}
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();
}
// 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);
for (int i = 0; i < ui->graphTreeWidget->topLevelItemCount(); i++) {
QTreeWidgetItem *item = ui->graphTreeWidget->topLevelItem(i);
IOGraph *iog = NULL;
if (item) {
iog = item->data(name_col_, Qt::UserRole).value<IOGraph *>();
QCPGraph *graph = iog->graph();
QCPBars *bars = iog->bars();
int style = item->data(style_col_, Qt::UserRole).toInt();
if (graph && !base_graph_) {
base_graph_ = graph;
} else if (bars && style == IOGraph::psStackedBar && 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;
iop->legend->setVisible(false);
iop->yAxis->setLabel(QString());
// Find unique labels
for (int i = 0; i < ui->graphTreeWidget->topLevelItemCount(); i++) {
QTreeWidgetItem *ti = ui->graphTreeWidget->topLevelItem(i);
IOGraph *iog = NULL;
if (ti) {
iog = ti->data(name_col_, Qt::UserRole).value<IOGraph *>();
vu_label_set.insert(iog->valueUnitLabel());
}
}
// 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]);
return;
}
// Differing labels. Create a legend.
for (int i = 0; i < ui->graphTreeWidget->topLevelItemCount(); i++) {
QTreeWidgetItem *ti = ui->graphTreeWidget->topLevelItem(i);
IOGraph *iog = NULL;
if (ti) {
iog = ti->data(name_col_, Qt::UserRole).value<IOGraph *>();
iog->addToLegend();
}
}
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();
QTreeWidgetItem *ti = ui->graphTreeWidget->topLevelItem(0);
IOGraph *iog = NULL;
if (ti) {
iog = ti->data(name_col_, Qt::UserRole).value<IOGraph *>();
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 = tr("%1 %2")
.arg(cap_file_ ? 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::focusChanged(QWidget *previous, QWidget *current)
{
Q_UNUSED(previous);
QTreeWidgetItem *item = ui->graphTreeWidget->currentItem();
if (!item) {
return;
}
// If we navigated away from an editing session, clear it.
QList<QWidget *>editors = QList<QWidget *>() << name_line_edit_ << dfilter_line_edit_ <<
color_combo_box_ << style_combo_box_ <<
yaxis_combo_box_ << yfield_line_edit_ <<
sma_combo_box_;
bool edit_active = false;
foreach (QWidget *w, editors) {
if (w) {
edit_active = true;
}
}
if (!edit_active) {
return;
}
editors.append(color_combo_box_->view());
editors.append(style_combo_box_->view());
editors.append(yaxis_combo_box_->view());
editors.append(sma_combo_box_->view());
if (! editors.contains(current)) {
itemEditingFinished(item);
}
}
void IOGraphDialog::activateLastItem()
{
int last_idx = ui->graphTreeWidget->topLevelItemCount() - 1;
if (last_idx < 0) return;
QTreeWidgetItem *last_item = ui->graphTreeWidget->invisibleRootItem()->child(last_idx);
if (!last_item) return;
ui->graphTreeWidget->setCurrentItem(last_item);
on_graphTreeWidget_itemActivated(last_item, name_col_);
}
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_) {
need_retap_ = false;
cf_retap_packets(cap_file_);
ui->ioPlot->setFocus();
} else {
if (need_recalc_) {
need_recalc_ = false;
need_replot_ = true;
emit recalcGraphData(cap_file_);
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();
}
}
}
// We're done editing a treewidgetitem. Set its values based on its
// widgets, remove each widget, then sync with our associated graph.
void IOGraphDialog::itemEditingFinished(QTreeWidgetItem *item)
{
if (item) {
bool recalc = false;
// Don't force a retap here. Disable the graph instead.
Qt::CheckState check_state = item->checkState(name_col_);
hint_err_.clear();
io_graph_item_unit_t item_unit = IOG_ITEM_UNIT_PACKETS;
QString field_name;
if (name_line_edit_) {
item->setText(name_col_, name_line_edit_->text());
}
if (dfilter_line_edit_) {
QString df = dfilter_line_edit_->text();
if (item->text(dfilter_col_).compare(df)) {
check_state = Qt::Unchecked;
}
item->setText(dfilter_col_, df);
}
if (color_combo_box_) {
int index = color_combo_box_->currentIndex();
item->setData(color_col_, Qt::UserRole, index);
item->setIcon(color_col_, graphColorIcon(index));
}
if (style_combo_box_) {
IOGraph::PlotStyles ps = IOGraph::psLine;
int index = style_combo_box_->currentIndex();
if (index < plot_style_to_name_.size()) {
ps = plot_style_to_name_.keys()[index];
}
item->setText(style_col_, plot_style_to_name_[ps]);
item->setData(style_col_, Qt::UserRole, ps);
}
if (yaxis_combo_box_) {
int index = yaxis_combo_box_->currentIndex();
if (index != item->data(yaxis_col_, Qt::UserRole).toInt()) {
if (index <= IOG_ITEM_UNIT_CALC_SUM) {
recalc = true;
} else {
check_state = Qt::Unchecked;
}
}
if (index < value_unit_to_name_.size()) {
item_unit = value_unit_to_name_.keys()[index];
}
item->setText(yaxis_col_, value_unit_to_name_[item_unit]);
item->setData(yaxis_col_, Qt::UserRole, item_unit);
}
if (yfield_line_edit_) {
if (item->text(yfield_col_).compare(yfield_line_edit_->text())) {
check_state = Qt::Unchecked;
}
item->setText(yfield_col_, yfield_line_edit_->text());
field_name = yfield_line_edit_->text();
}
if (sma_combo_box_) {
int index = sma_combo_box_->currentIndex();
if (index != item->data(sma_period_col_, Qt::UserRole).toInt()) {
recalc = true;
}
QString text = sma_combo_box_->itemText(index);
int sma = sma_combo_box_->itemData(index, Qt::UserRole).toInt();
item->setText(sma_period_col_, text);
item->setData(sma_period_col_, Qt::UserRole, sma);
}
for (int col = 0; col < num_cols_; col++) {
QWidget *w = ui->graphTreeWidget->itemWidget(item, col);
if (w) {
ui->graphTreeWidget->removeItemWidget(item, col);
delete w;
}
}
item->setCheckState(name_col_, check_state);
syncGraphSettings(item);
if (recalc) {
scheduleRecalc(true);
} else {
scheduleReplot(true);
}
}
}
void IOGraphDialog::loadProfileGraphs()
{
if (iog_uat_) return;
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,
io_graph_fields);
const char* err = NULL;
uat_load(iog_uat_, &err);
}
// Slots
void IOGraphDialog::lineEditDestroyed()
{
if (QObject::sender() == name_line_edit_) {
name_line_edit_ = NULL;
} else if (QObject::sender() == dfilter_line_edit_) {
dfilter_line_edit_ = NULL;
} else if (QObject::sender() == yfield_line_edit_) {
yfield_line_edit_ = NULL;
}
}
void IOGraphDialog::comboDestroyed()
{
if (QObject::sender() == color_combo_box_) {
color_combo_box_ = NULL;
} else if (QObject::sender() == style_combo_box_) {
style_combo_box_ = NULL;
} else if (QObject::sender() == yaxis_combo_box_) {
yaxis_combo_box_ = NULL;
} else if (QObject::sender() == sma_combo_box_) {
sma_combo_box_ = NULL;
}
}
void IOGraphDialog::on_intervalComboBox_currentIndexChanged(int index)
{
Q_UNUSED(index);
int interval = ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt();
bool need_retap = false;
for (int i = 0; i < ui->graphTreeWidget->topLevelItemCount(); i++) {
QTreeWidgetItem *item = ui->graphTreeWidget->topLevelItem(i);
IOGraph *iog = NULL;
if (item) {
iog = item->data(name_col_, Qt::UserRole).value<IOGraph *>();
if (iog) {
iog->setInterval(interval);
if (iog->visible()) {
need_retap = true;
}
}
}
}
if (need_retap) {
scheduleRetap(true);
}
}
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);
}
void IOGraphDialog::on_graphTreeWidget_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous)
{
Q_UNUSED(current);
if (previous && ui->graphTreeWidget->itemWidget(previous, name_col_)) {
itemEditingFinished(previous);
}
}
void IOGraphDialog::on_graphTreeWidget_itemActivated(QTreeWidgetItem *item, int column)
{
if (!item || name_line_edit_) return;
QWidget *editor = NULL;
int cur_idx;
name_line_edit_ = new QLineEdit();
name_line_edit_->setText(item->text(name_col_));
connect(name_line_edit_, SIGNAL(destroyed()), this, SLOT(lineEditDestroyed()));
dfilter_line_edit_ = new SyntaxLineEdit();
connect(dfilter_line_edit_, SIGNAL(textChanged(QString)),
dfilter_line_edit_, SLOT(checkDisplayFilter(QString)));
connect(dfilter_line_edit_, SIGNAL(destroyed()), this, SLOT(lineEditDestroyed()));
dfilter_line_edit_->setText(item->text(dfilter_col_));
color_combo_box_ = new QComboBox();
cur_idx = item->data(color_col_, Qt::UserRole).toInt();
for (int i = 0; i < colors_.size(); i++) {
color_combo_box_->addItem(QString());
color_combo_box_->setItemIcon(i, graphColorIcon(i));
if (i == cur_idx) {
color_combo_box_->setCurrentIndex(i);
}
}
item->setIcon(color_col_, QIcon());
color_combo_box_->setFocusPolicy(Qt::StrongFocus);
connect(color_combo_box_, SIGNAL(destroyed()), this, SLOT(comboDestroyed()));
style_combo_box_ = new QComboBox();
cur_idx = item->data(style_col_, Qt::UserRole).toInt();
for (int i = 0; i < plot_style_to_name_.size(); i++) {
IOGraph::PlotStyles ps = plot_style_to_name_.keys()[i];
style_combo_box_->addItem(plot_style_to_name_[ps], ps);
if (ps == cur_idx) {
style_combo_box_->setCurrentIndex(i);
}
}
style_combo_box_->setFocusPolicy(Qt::StrongFocus);
connect(style_combo_box_, SIGNAL(destroyed()), this, SLOT(comboDestroyed()));
yaxis_combo_box_ = new QComboBox();
cur_idx = item->data(yaxis_col_, Qt::UserRole).toInt();
for (int i = 0; i < value_unit_to_name_.size(); i++) {
io_graph_item_unit_t vu = value_unit_to_name_.keys()[i];
yaxis_combo_box_->addItem(value_unit_to_name_[vu], vu);
if (vu == cur_idx) {
yaxis_combo_box_->setCurrentIndex(i);
}
}
yaxis_combo_box_->setFocusPolicy(Qt::StrongFocus);
connect(yaxis_combo_box_, SIGNAL(destroyed()), this, SLOT(comboDestroyed()));
yfield_line_edit_ = new SyntaxLineEdit();
connect(yfield_line_edit_, SIGNAL(textChanged(QString)),
yfield_line_edit_, SLOT(checkFieldName(QString)));
connect(yfield_line_edit_, SIGNAL(destroyed()), this, SLOT(lineEditDestroyed()));
yfield_line_edit_->setText(item->text(yfield_col_));
sma_combo_box_ = new QComboBox();
cur_idx = item->data(sma_period_col_, Qt::UserRole).toInt();
for (int i = 0; i < moving_average_to_name_.size(); i++) {
int sma = moving_average_to_name_.keys()[i];
sma_combo_box_->addItem(moving_average_to_name_[sma], sma);
if (sma == cur_idx) {
sma_combo_box_->setCurrentIndex(i);
}
}
sma_combo_box_->setFocusPolicy(Qt::StrongFocus);
connect(sma_combo_box_, SIGNAL(destroyed()), this, SLOT(comboDestroyed()));
switch (column) {
case name_col_:
editor = name_line_edit_;
name_line_edit_->selectAll();
break;
case dfilter_col_:
editor = dfilter_line_edit_;
dfilter_line_edit_->selectAll();
break;
case color_col_:
{
editor = color_combo_box_;
break;
}
case style_col_:
{
editor = style_combo_box_;
break;
}
case yaxis_col_:
{
editor = yaxis_combo_box_;
break;
}
case yfield_col_:
editor = yfield_line_edit_;
yfield_line_edit_->selectAll();
break;
case sma_period_col_:
{
editor = sma_combo_box_;
break;
}
default:
return;
}
QList<QWidget *>editors = QList<QWidget *>() << name_line_edit_ << dfilter_line_edit_ <<
color_combo_box_ << style_combo_box_ <<
yaxis_combo_box_ << yfield_line_edit_ <<
sma_combo_box_;
int cur_col = name_col_;
QWidget *prev_widget = ui->graphTreeWidget;
foreach (QWidget *editor, editors) {
QFrame *edit_frame = new QFrame();
QHBoxLayout *hb = new QHBoxLayout();
QSpacerItem *spacer = new QSpacerItem(5, 10);
hb->addWidget(editor, 0);
hb->addSpacerItem(spacer);
hb->setStretch(1, 1);
hb->setContentsMargins(0, 0, 0, 0);
edit_frame->setLineWidth(0);
edit_frame->setFrameStyle(QFrame::NoFrame);
edit_frame->setLayout(hb);
ui->graphTreeWidget->setItemWidget(item, cur_col, edit_frame);
setTabOrder(prev_widget, editor);
prev_widget = editor;
cur_col++;
}
// setTabOrder(prev_widget, ui->graphTreeWidget);
editor->setFocus();
}
void IOGraphDialog::on_graphTreeWidget_itemSelectionChanged()
{
if (ui->graphTreeWidget->selectedItems().length() > 0) {
ui->deleteToolButton->setEnabled(true);
ui->copyToolButton->setEnabled(true);
} else {
ui->deleteToolButton->setEnabled(false);
ui->copyToolButton->setEnabled(false);
}
}
void IOGraphDialog::on_graphTreeWidget_itemChanged(QTreeWidgetItem *item, int column)
{
if (!item) {
return;
}
if (column == name_col_ && !name_line_edit_) {
syncGraphSettings(item);
}
}
void IOGraphDialog::on_resetButton_clicked()
{
resetAxes();
}
void IOGraphDialog::on_newToolButton_clicked()
{
addGraph();
}
void IOGraphDialog::on_deleteToolButton_clicked()
{
QTreeWidgetItem *item = ui->graphTreeWidget->currentItem();
if (!item) return;
IOGraph *iog = qvariant_cast<IOGraph *>(item->data(name_col_, Qt::UserRole));
delete iog;
delete item;
// We should probably be smarter about this.
hint_err_.clear();
mouseMoved(NULL);
}
void IOGraphDialog::on_copyToolButton_clicked()
{
addGraph(true);
}
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_actionZoomOut_triggered()
{
zoomAxes(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() && cap_file_ && 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 filter = QString("%1;;%2;;%3;;%4")
.arg(pdf_filter)
.arg(png_filter)
.arg(bmp_filter)
.arg(jpeg_filter);
QString save_file = path.canonicalPath();
if (cap_file_) {
save_file += QString("/%1").arg(cf_get_display_name(cap_file_));
}
file_name = QFileDialog::getSaveFileName(this, tr("Wireshark: Save Graph As..."),
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 error dialog?
if (save_ok) {
path = QDir(file_name);
wsApp->setLastOpenDir(path.canonicalPath().toUtf8().constData());
}
}
}
// IOGraph
IOGraph::IOGraph(QCustomPlot *parent) :
parent_(parent),
visible_(false),
graph_(NULL),
bars_(NULL),
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);
if (error_string) {
// QMessageBox::critical(this, tr("%1 failed to register tap listener").arg(name_),
// error_string->str);
g_string_free(error_string, TRUE);
}
setFilter(QString());
}
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;
status = dfilter_compile(full_filter.toUtf8().constData(), &dfilter);
dfilter_free(dfilter);
if (!status) {
config_err_ = dfilter_error_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(colors_[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()
{
if (val_units_ >= IOG_ITEM_UNIT_FIRST && val_units_ <= IOG_ITEM_UNIT_LAST) {
return value_unit_to_name_[val_units_];
}
return tr("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;
}
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_) {
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;
}
QMap<io_graph_item_unit_t, QString> IOGraph::valueUnitsToNames()
{
QMap<io_graph_item_unit_t, QString> vuton;
vuton[IOG_ITEM_UNIT_PACKETS] = QObject::tr("Packets/s");
vuton[IOG_ITEM_UNIT_BYTES] = QObject::tr("Bytes/s");
vuton[IOG_ITEM_UNIT_BITS] = QObject::tr("Bits/s");
vuton[IOG_ITEM_UNIT_CALC_SUM] = QObject::tr("SUM(Y Field)");
vuton[IOG_ITEM_UNIT_CALC_FRAMES] = QObject::tr("COUNT FRAMES(Y Field)");
vuton[IOG_ITEM_UNIT_CALC_FIELDS] = QObject::tr("COUNT FIELDS(Y Field)");
vuton[IOG_ITEM_UNIT_CALC_MAX] = QObject::tr("MAX(Y Field)");
vuton[IOG_ITEM_UNIT_CALC_MIN] = QObject::tr("MIN(Y Field)");
vuton[IOG_ITEM_UNIT_CALC_AVERAGE] = QObject::tr("AVG(Y Field)");
vuton[IOG_ITEM_UNIT_CALC_LOAD] = QObject::tr("LOAD(Y Field)");
return vuton;
}
QMap<IOGraph::PlotStyles, QString> IOGraph::plotStylesToNames()
{
QMap<IOGraph::PlotStyles, QString> pston;
pston[psLine] = QObject::tr("Line");
pston[psImpulse] = QObject::tr("Impulse");
pston[psBar] = QObject::tr("Bar");
pston[psStackedBar] = QObject::tr("Stacked Bar");
pston[psDot] = QObject::tr("Dot");
pston[psSquare] = QObject::tr("Square");
pston[psDiamond] = QObject::tr("Diamond");
return pston;
}
QMap<int, QString> IOGraph::movingAveragesToNames()
{
QMap<int, QString> maton;
QList<int> averages = QList<int>()
/* << 8 */ << 10 /* << 16 */ << 20 << 50 << 100 << 200 << 500 << 1000; // Arbitrarily chosen
maton[0] = QObject::tr("None");
foreach (int avg, averages) {
maton[avg] = QString(QObject::tr("%1 interval SMA")).arg(avg);
}
return maton;
}
void IOGraph::recalcGraphData(capture_file *cap_file)
{
/* 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 = 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;
}
// qDebug() << "=rgd" << num_items_ << hf_index_;
emit requestReplot();
}
void IOGraph::setInterval(int interval)
{
interval_ = interval;
}
// Get the value at the given interval (idx) for the current value unit.
// Adapted from get_it_value in gtk/io_stat.c.
double IOGraph::getItemValue(int idx, capture_file *cap_file)
{
double value = 0; /* FIXME: loss of precision, visible on the graph for small values */
int adv_type;
io_graph_item_t *item;
guint32 interval;
g_assert(idx < max_io_items_);
item = &items_[idx];
// Basic units
switch (val_units_) {
case IOG_ITEM_UNIT_PACKETS:
return item->frames;
case IOG_ITEM_UNIT_BYTES:
return item->bytes;
case IOG_ITEM_UNIT_BITS:
return (item->bytes * 8);
case IOG_ITEM_UNIT_CALC_FRAMES:
return item->frames;
case IOG_ITEM_UNIT_CALC_FIELDS:
return item->fields;
default:
/* If it's COUNT_TYPE_ADVANCED but not one of the
* generic ones we'll get it when we switch on the
* adv_type below. */
break;
}
if (hf_index_ < 0) {
return 0;
}
// Advanced units
adv_type = proto_registrar_get_ftype(hf_index_);
switch (adv_type) {
case FT_UINT8:
case FT_UINT16:
case FT_UINT24:
case FT_UINT32:
case FT_UINT64:
case FT_INT8:
case FT_INT16:
case FT_INT24:
case FT_INT32:
case FT_INT64:
switch (val_units_) {
case IOG_ITEM_UNIT_CALC_SUM:
value = item->int_tot;
break;
case IOG_ITEM_UNIT_CALC_MAX:
value = item->int_max;
break;
case IOG_ITEM_UNIT_CALC_MIN:
value = item->int_min;
break;
case IOG_ITEM_UNIT_CALC_AVERAGE:
if (item->fields) {
value = (double)item->int_tot / item->fields;
} else {
value = 0;
}
break;
default:
break;
}
break;
case FT_FLOAT:
switch (val_units_) {
case IOG_ITEM_UNIT_CALC_SUM:
value = (guint64)item->float_tot;
break;
case IOG_ITEM_UNIT_CALC_MAX:
value = (guint64)item->float_max;
break;
case IOG_ITEM_UNIT_CALC_MIN:
value = (guint64)item->float_min;
break;
case IOG_ITEM_UNIT_CALC_AVERAGE:
if (item->fields) {
value = (guint64)item->float_tot / item->fields;
} else {
value = 0;
}
break;
default:
break;
}
break;
case FT_DOUBLE:
switch (val_units_) {
case IOG_ITEM_UNIT_CALC_SUM:
value = (guint64)item->double_tot;
break;
case IOG_ITEM_UNIT_CALC_MAX:
value = (guint64)item->double_max;
break;
case IOG_ITEM_UNIT_CALC_MIN:
value = (guint64)item->double_min;
break;
case IOG_ITEM_UNIT_CALC_AVERAGE:
if (item->fields) {
value = (guint64)item->double_tot / item->fields;
} else {
value = 0;
}
break;
default:
break;
}
break;
case FT_RELATIVE_TIME:
switch (val_units_) {
case IOG_ITEM_UNIT_CALC_MAX:
value = (guint64) (item->time_max.secs*1000000 + item->time_max.nsecs/1000);
break;
case IOG_ITEM_UNIT_CALC_MIN:
value = (guint64) (item->time_min.secs*1000000 + item->time_min.nsecs/1000);
break;
case IOG_ITEM_UNIT_CALC_SUM:
value = (guint64) (item->time_tot.secs*1000000 + item->time_tot.nsecs/1000);
break;
case IOG_ITEM_UNIT_CALC_AVERAGE:
if (item->fields) {
guint64 t; /* time in us */
t = item->time_tot.secs;
t = t*1000000+item->time_tot.nsecs/1000;
value = (guint64) (t/item->fields);
} else {
value = 0;
}
break;
case IOG_ITEM_UNIT_CALC_LOAD:
if (idx == (int)cur_idx_ && cap_file) {
interval = (guint32)((cap_file->elapsed_time.secs*1000) +
((cap_file->elapsed_time.nsecs+500000)/1000000));
interval -= (interval_ * idx);
} else {
interval = interval_;
}
value = (guint64) ((item->time_tot.secs*1000000 + item->time_tot.nsecs/1000) / interval);
break;
default:
break;
}
break;
default:
break;
}
return value;
}
// "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
gboolean IOGraph::tapPacket(void *iog_ptr, packet_info *pinfo, epan_dissect_t *edt, const void *data)
{
Q_UNUSED(data);
IOGraph *iog = static_cast<IOGraph *>(iog_ptr);
if (!pinfo || !iog) {
return FALSE;
}
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 FALSE;
}
/* 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->fd->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 FALSE;
}
// qDebug() << "=tapPacket" << iog->name_ << idx << iog->hf_index_ << iog->val_units_ << iog->num_items_;
if (recalc) {
emit iog->requestRecalc();
}
return TRUE;
}
// "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,
-1,
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:
*/