/* sequence_dialog.cpp * * Wireshark - Network traffic analyzer * By Gerald Combs * 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 "sequence_dialog.h" #include #include "epan/addr_resolv.h" #include "file.h" #include "wsutil/nstime.h" #include "wsutil/utf8_entities.h" #include "wsutil/file_util.h" #include #include "progress_frame.h" #include #include "sequence_diagram.h" #include "wireshark_application.h" #include #include #include #include #include #include // To do: // - Resize or show + hide the Time and Comment axes, possibly via one of // the following: // - Split the time, diagram, and comment sections into three separate // widgets inside a QSplitter. This would resemble the GTK+ UI, but we'd // have to coordinate between the three and we'd lose time and comment // values in PDF and PNG exports. // - Add separate controls for the width and/or visibility of the Time and // Comment columns. // - Fake a splitter widget by catching mouse events in the plot area. // Drawing a QCPItemLine or QCPItemPixmap over each Y axis might make // this easier. // - For general flows, let the user show columns other than COL_INFO. // - Add UTF8 to text dump // - Save to XMI? http://www.umlgraph.org/ // - Time: abs vs delta // - Hide nodes // - Clickable time + comments? // - Incorporate packet comments? // - Change line_style to seq_type (i.e. draw ACKs dashed) // - Create WSGraph subclasses with common behavior. // - Help button and text static const double min_top_ = -1.0; static const double min_left_ = -0.5; typedef struct { int curr_index; QComboBox *flow; SequenceInfo *info; } sequence_items_t; SequenceDialog::SequenceDialog(QWidget &parent, CaptureFile &cf, SequenceInfo *info) : WiresharkDialog(parent, cf), ui(new Ui::SequenceDialog), info_(info), num_items_(0), packet_num_(0), sequence_w_(1) { ui->setupUi(this); QCustomPlot *sp = ui->sequencePlot; setWindowSubtitle(info_ ? tr("Call Flow") : tr("Flow")); if (!info_) { info_ = new SequenceInfo(sequence_analysis_info_new()); info_->sainfo()->name = "any"; } else { info_->ref(); sequence_analysis_free_nodes(info_->sainfo()); num_items_ = sequence_analysis_get_nodes(info_->sainfo()); } seq_diagram_ = new SequenceDiagram(sp->yAxis, sp->xAxis2, sp->yAxis2); sp->addPlottable(seq_diagram_); // When dragging is enabled it's easy to drag past the lower and upper // bounds of each axis. Disable it for now. //sp->axisRect()->setRangeDragAxes(sp->xAxis2, sp->yAxis); //sp->setInteractions(QCP::iRangeDrag); sp->xAxis->setVisible(false); sp->xAxis->setPadding(0); sp->xAxis->setLabelPadding(0); sp->xAxis->setTickLabelPadding(0); QPen base_pen(ColorUtils::alphaBlend(palette().text(), palette().base(), 0.25)); base_pen.setWidthF(0.5); sp->xAxis2->setBasePen(base_pen); sp->yAxis->setBasePen(base_pen); sp->yAxis2->setBasePen(base_pen); sp->xAxis2->setVisible(true); sp->yAxis2->setVisible(true); key_text_ = new QCPItemText(sp); key_text_->setText(tr("Time")); sp->addItem(key_text_); key_text_->setPositionAlignment(Qt::AlignRight | Qt::AlignVCenter); key_text_->position->setType(QCPItemPosition::ptAbsolute); key_text_->setClipToAxisRect(false); comment_text_ = new QCPItemText(sp); comment_text_->setText(tr("Comment")); sp->addItem(comment_text_); comment_text_->setPositionAlignment(Qt::AlignLeft | Qt::AlignVCenter); comment_text_->position->setType(QCPItemPosition::ptAbsolute); comment_text_->setClipToAxisRect(false); one_em_ = QFontMetrics(sp->yAxis->labelFont()).height(); ui->horizontalScrollBar->setSingleStep(100 / one_em_); ui->verticalScrollBar->setSingleStep(100 / one_em_); ui->gridLayout->setSpacing(0); connect(sp->yAxis, SIGNAL(rangeChanged(QCPRange)), sp->yAxis2, SLOT(setRange(QCPRange))); 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_.addAction(ui->actionGoToNextPacket); ctx_menu_.addAction(ui->actionGoToPreviousPacket); ui->addressComboBox->setCurrentIndex(0); sequence_items_t item_data; item_data.curr_index = 0; item_data.flow = ui->flowComboBox; item_data.info = info_; //Add all registered analysis to combo box sequence_analysis_table_iterate_tables(addFlowSequenceItem, &item_data); if (strcmp(info_->sainfo()->name, "voip") == 0) { ui->flowComboBox->blockSignals(true); ui->controlFrame->hide(); } QPushButton *save_bt = ui->buttonBox->button(QDialogButtonBox::Save); save_bt->setText(tr("Save As" UTF8_HORIZONTAL_ELLIPSIS)); QPushButton *close_bt = ui->buttonBox->button(QDialogButtonBox::Close); if (close_bt) { close_bt->setDefault(true); } ProgressFrame::addToButtonBox(ui->buttonBox, &parent); loadGeometry(parent.width(), parent.height() * 4 / 5); connect(ui->horizontalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(hScrollBarChanged(int))); connect(ui->verticalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(vScrollBarChanged(int))); connect(sp->xAxis2, SIGNAL(rangeChanged(QCPRange)), this, SLOT(xAxisChanged(QCPRange))); connect(sp->yAxis, SIGNAL(rangeChanged(QCPRange)), this, SLOT(yAxisChanged(QCPRange))); connect(sp, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(diagramClicked(QMouseEvent*))); connect(sp, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(mouseMoved(QMouseEvent*))); connect(sp, SIGNAL(mouseWheel(QWheelEvent*)), this, SLOT(mouseWheeled(QWheelEvent*))); disconnect(ui->buttonBox, SIGNAL(accepted()), this, SLOT(accept())); } SequenceDialog::~SequenceDialog() { info_->unref(); delete ui; } void SequenceDialog::updateWidgets() { WiresharkDialog::updateWidgets(); } void SequenceDialog::showEvent(QShowEvent *) { QTimer::singleShot(0, this, SLOT(fillDiagram())); } void SequenceDialog::resizeEvent(QResizeEvent *) { if (!info_) return; resetAxes(true); } void SequenceDialog::keyPressEvent(QKeyEvent *event) { int pan_pixels = event->modifiers() & Qt::ShiftModifier ? 1 : 10; // XXX - Copy some shortcuts from tcp_stream_dialog.cpp switch(event->key()) { case Qt::Key_Minus: case Qt::Key_Underscore: // Shifted minus on U.S. keyboards on_actionZoomOut_triggered(); break; case Qt::Key_Plus: case Qt::Key_Equal: // Unshifted plus on U.S. keyboards on_actionZoomIn_triggered(); 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_PageDown: case Qt::Key_Space: ui->verticalScrollBar->setValue(ui->verticalScrollBar->value() + ui->verticalScrollBar->pageStep()); break; case Qt::Key_PageUp: ui->verticalScrollBar->setValue(ui->verticalScrollBar->value() - ui->verticalScrollBar->pageStep()); break; case Qt::Key_0: case Qt::Key_ParenRight: // Shifted 0 on U.S. keyboards case Qt::Key_R: case Qt::Key_Home: resetAxes(); break; case Qt::Key_G: on_actionGoToPacket_triggered(); break; case Qt::Key_N: on_actionGoToNextPacket_triggered(); break; case Qt::Key_P: on_actionGoToPreviousPacket_triggered(); break; } QDialog::keyPressEvent(event); } void SequenceDialog::hScrollBarChanged(int value) { if (qAbs(ui->sequencePlot->xAxis2->range().center()-value/100.0) > 0.01) { ui->sequencePlot->xAxis2->setRange(value/100.0, ui->sequencePlot->xAxis2->range().size(), Qt::AlignCenter); ui->sequencePlot->replot(); } } void SequenceDialog::vScrollBarChanged(int value) { if (qAbs(ui->sequencePlot->yAxis->range().center()-value/100.0) > 0.01) { ui->sequencePlot->yAxis->setRange(value/100.0, ui->sequencePlot->yAxis->range().size(), Qt::AlignCenter); ui->sequencePlot->replot(); } } void SequenceDialog::xAxisChanged(QCPRange range) { ui->horizontalScrollBar->setValue(qRound(qreal(range.center()*100.0))); ui->horizontalScrollBar->setPageStep(qRound(qreal(range.size()*100.0))); } void SequenceDialog::yAxisChanged(QCPRange range) { ui->verticalScrollBar->setValue(qRound(qreal(range.center()*100.0))); ui->verticalScrollBar->setPageStep(qRound(qreal(range.size()*100.0))); } void SequenceDialog::diagramClicked(QMouseEvent *event) { switch (event->button()) { case Qt::LeftButton: on_actionGoToPacket_triggered(); break; case Qt::RightButton: // XXX We should find some way to get sequenceDiagram to handle a // contextMenuEvent instead. ctx_menu_.exec(event->globalPos()); break; default: break; } } void SequenceDialog::mouseMoved(QMouseEvent *event) { packet_num_ = 0; QString hint; if (event) { seq_analysis_item_t *sai = seq_diagram_->itemForPosY(event->pos().y()); if (sai) { packet_num_ = sai->frame_number; QString raw_comment = html_escape(sai->comment); hint = QString("Packet %1: %2").arg(packet_num_).arg(raw_comment); } } if (hint.isEmpty()) { if (!info_->sainfo()) { hint += tr("No data"); } else { hint += tr("%Ln node(s)", "", info_->sainfo()->num_nodes) + QString(", ") + tr("%Ln item(s)", "", num_items_); } } hint.prepend(""); hint.append(""); ui->hintLabel->setText(hint); } void SequenceDialog::mouseWheeled(QWheelEvent *event) { int scroll_delta = event->delta() * -1 / 15; if (event->orientation() == Qt::Vertical) { scroll_delta *= ui->verticalScrollBar->singleStep(); ui->verticalScrollBar->setValue(ui->verticalScrollBar->value() + scroll_delta); } else { scroll_delta *= ui->horizontalScrollBar->singleStep(); ui->horizontalScrollBar->setValue(ui->horizontalScrollBar->value() + scroll_delta); } event->accept(); } void SequenceDialog::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 ascii_filter = tr("ASCII (*.txt)"); QString filter = QString("%1;;%2;;%3;;%4") .arg(pdf_filter) .arg(png_filter) .arg(bmp_filter) .arg(jpeg_filter); if (!file_closed_) { filter.append(QString(";;%5").arg(ascii_filter)); } file_name = QFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save Graph As" UTF8_HORIZONTAL_ELLIPSIS)), path.canonicalPath(), filter, &extension); if (file_name.length() > 0) { bool save_ok = false; if (extension.compare(pdf_filter) == 0) { save_ok = ui->sequencePlot->savePdf(file_name); } else if (extension.compare(png_filter) == 0) { save_ok = ui->sequencePlot->savePng(file_name); } else if (extension.compare(bmp_filter) == 0) { save_ok = ui->sequencePlot->saveBmp(file_name); } else if (extension.compare(jpeg_filter) == 0) { save_ok = ui->sequencePlot->saveJpg(file_name); } else if (extension.compare(ascii_filter) == 0 && !file_closed_ && info_->sainfo()) { FILE *outfile = ws_fopen(file_name.toUtf8().constData(), "w"); if (outfile != NULL) { sequence_analysis_dump_to_file(outfile, info_->sainfo(), 0); save_ok = true; } else { save_ok = false; } } // else error dialog? if (save_ok) { path = QDir(file_name); wsApp->setLastOpenDir(path.canonicalPath().toUtf8().constData()); } else { open_failure_alert_box(file_name.toUtf8().constData(), errno, TRUE); } } } void SequenceDialog::fillDiagram() { if (!info_->sainfo() || file_closed_) return; QCustomPlot *sp = ui->sequencePlot; if (strcmp(info_->sainfo()->name, "voip") == 0) { seq_diagram_->setData(info_->sainfo()); } else { seq_diagram_->clearData(); sequence_analysis_list_free(info_->sainfo()); register_analysis_t* analysis = sequence_analysis_find_by_name(info_->sainfo()->name); if (analysis != NULL) { const char *filter = NULL; if (ui->displayFilterCheckBox->checkState() == Qt::Checked) filter = cap_file_.capFile()->dfilter; register_tap_listener(sequence_analysis_get_tap_listener_name(analysis), info_->sainfo(), filter, sequence_analysis_get_tap_flags(analysis), NULL, sequence_analysis_get_packet_func(analysis), NULL); cf_retap_packets(cap_file_.capFile()); remove_tap_listener(info_->sainfo()); num_items_ = sequence_analysis_get_nodes(info_->sainfo()); seq_diagram_->setData(info_->sainfo()); } } sequence_w_ = one_em_ * 15; // Arbitrary mouseMoved(NULL); resetAxes(); // XXX QCustomPlot doesn't seem to draw any sort of focus indicator. sp->setFocus(); } void SequenceDialog::panAxes(int x_pixels, int y_pixels) { // We could simplify this quite a bit if we set the scroll bar values instead. if (!info_->sainfo()) return; QCustomPlot *sp = ui->sequencePlot; double h_pan = 0.0; double v_pan = 0.0; h_pan = sp->xAxis2->range().size() * x_pixels / sp->xAxis2->axisRect()->width(); if (h_pan < 0) { h_pan = qMax(h_pan, min_left_ - sp->xAxis2->range().lower); } else { h_pan = qMin(h_pan, info_->sainfo()->num_nodes - sp->xAxis2->range().upper); } v_pan = sp->yAxis->range().size() * y_pixels / sp->yAxis->axisRect()->height(); if (v_pan < 0) { v_pan = qMax(v_pan, min_top_ - sp->yAxis->range().lower); } else { v_pan = qMin(v_pan, num_items_ - sp->yAxis->range().upper); } if (h_pan && !(sp->xAxis2->range().contains(min_left_) && sp->xAxis2->range().contains(info_->sainfo()->num_nodes))) { sp->xAxis2->moveRange(h_pan); sp->replot(); } if (v_pan && !(sp->yAxis->range().contains(min_top_) && sp->yAxis->range().contains(num_items_))) { sp->yAxis->moveRange(v_pan); sp->replot(); } } void SequenceDialog::resetAxes(bool keep_lower) { if (!info_->sainfo()) return; QCustomPlot *sp = ui->sequencePlot; // Allow space for labels on the top and port numbers on the left. double top_pos = min_top_, left_pos = min_left_; if (keep_lower) { top_pos = sp->yAxis->range().lower; left_pos = sp->xAxis2->range().lower; } double range_span = sp->viewport().width() / sequence_w_ * sp->axisRect()->rangeZoomFactor(Qt::Horizontal); sp->xAxis2->setRange(left_pos, range_span + left_pos); range_span = sp->axisRect()->height() / (one_em_ * 1.5); sp->yAxis->setRange(top_pos, range_span + top_pos); double rmin = sp->xAxis2->range().size() / 2; ui->horizontalScrollBar->setRange((rmin - 0.5) * 100, (info_->sainfo()->num_nodes - 0.5 - rmin) * 100); xAxisChanged(sp->xAxis2->range()); ui->horizontalScrollBar->setValue(ui->horizontalScrollBar->minimum()); // Shouldn't be needed. rmin = (sp->yAxis->range().size() / 2); ui->verticalScrollBar->setRange((rmin - 1.0) * 100, (num_items_ - 0.5 - rmin) * 100); yAxisChanged(sp->yAxis->range()); // It would be exceedingly handy if we could do one or both of the // following: // - Position an axis label above its axis inline with the tick labels. // - Anchor a QCPItemText to one of the corners of a QCPAxis. // Neither of those appear to be possible, so we first call replot in // order to lay out our X axes, place our labels, the call replot again. sp->replot(QCustomPlot::rpQueued); QRect axis_rect = sp->axisRect()->rect(); key_text_->position->setCoords(axis_rect.left() - sp->yAxis->padding() - sp->yAxis->tickLabelPadding() - sp->yAxis->offset(), axis_rect.top() / 2); comment_text_->position->setCoords(axis_rect.right() + sp->yAxis2->padding() + sp->yAxis2->tickLabelPadding() + sp->yAxis2->offset(), axis_rect.top() / 2); sp->replot(QCustomPlot::rpHint); } void SequenceDialog::on_resetButton_clicked() { resetAxes(); } void SequenceDialog::on_actionGoToPacket_triggered() { if (!file_closed_ && packet_num_ > 0) { cf_goto_frame(cap_file_.capFile(), packet_num_); seq_diagram_->setSelectedPacket(packet_num_); } } void SequenceDialog::goToAdjacentPacket(bool next) { if (file_closed_) return; int old_key = seq_diagram_->selectedKey(); int adjacent_packet = seq_diagram_->adjacentPacket(next); int new_key = seq_diagram_->selectedKey(); if (adjacent_packet > 0) { if (new_key >= 0) { QCustomPlot *sp = ui->sequencePlot; double range_offset = 0.0; // Scroll if we're at our scroll margin and we haven't reached // the end of our range. double scroll_margin = 3.0; // Lines if (old_key >= 0) { range_offset = new_key - old_key; } if (new_key < sp->yAxis->range().lower) { // Out of range, top range_offset = qRound(new_key - sp->yAxis->range().lower - scroll_margin - 0.5); } else if (new_key > sp->yAxis->range().upper) { // Out of range, bottom range_offset = qRound(new_key - sp->yAxis->range().upper + scroll_margin + 0.5); } else if (next) { // In range, next if (new_key + scroll_margin < sp->yAxis->range().upper) { range_offset = 0.0; } } else { // In range, previous if (new_key - scroll_margin > sp->yAxis->range().lower) { range_offset = 0.0; } } // Clamp to our upper & lower bounds. if (range_offset > 0) { range_offset = qMin(range_offset, num_items_ - sp->yAxis->range().upper); } else if (range_offset < 0) { range_offset = qMax(range_offset, min_top_ - sp->yAxis->range().lower); } sp->yAxis->moveRange(range_offset); } cf_goto_frame(cap_file_.capFile(), adjacent_packet); seq_diagram_->setSelectedPacket(adjacent_packet); } } void SequenceDialog::on_displayFilterCheckBox_toggled(bool) { fillDiagram(); } void SequenceDialog::on_flowComboBox_activated(int index) { if (!info_->sainfo() || (strcmp(info_->sainfo()->name, "voip") == 0) || index < 0) return; register_analysis_t* analysis = VariantPointer::asPtr(ui->flowComboBox->itemData(index)); info_->sainfo()->name = sequence_analysis_get_name(analysis); fillDiagram(); } void SequenceDialog::on_addressComboBox_activated(int index) { if (!info_->sainfo()) return; if (index == 0) { info_->sainfo()->any_addr = TRUE; } else { info_->sainfo()->any_addr = FALSE; } fillDiagram(); } void SequenceDialog::on_actionReset_triggered() { on_resetButton_clicked(); } void SequenceDialog::on_actionMoveRight10_triggered() { panAxes(10, 0); } void SequenceDialog::on_actionMoveLeft10_triggered() { panAxes(-10, 0); } void SequenceDialog::on_actionMoveUp10_triggered() { panAxes(0, 10); } void SequenceDialog::on_actionMoveDown10_triggered() { panAxes(0, -10); } void SequenceDialog::on_actionMoveRight1_triggered() { panAxes(1, 0); } void SequenceDialog::on_actionMoveLeft1_triggered() { panAxes(-1, 0); } void SequenceDialog::on_actionMoveUp1_triggered() { panAxes(0, 1); } void SequenceDialog::on_actionMoveDown1_triggered() { panAxes(0, -1); } void SequenceDialog::on_actionZoomIn_triggered() { zoomXAxis(true); } void SequenceDialog::on_actionZoomOut_triggered() { zoomXAxis(false); } void SequenceDialog::zoomXAxis(bool in) { QCustomPlot *sp = ui->sequencePlot; double h_factor = sp->axisRect()->rangeZoomFactor(Qt::Horizontal); if (!in) { h_factor = pow(h_factor, -1); } sp->xAxis2->scaleRange(h_factor, sp->xAxis->range().lower); sp->replot(); } gboolean SequenceDialog::addFlowSequenceItem(const void* key, void *value, void *userdata) { const char* name = (const char*)key; register_analysis_t* analysis = (register_analysis_t*)value; sequence_items_t* item_data = (sequence_items_t*)userdata; /* XXX - Although "voip" isn't a registered name yet, it appears to have special handling that will be done outside of registered data */ if (strcmp(name, "voip") == 0) return FALSE; item_data->flow->addItem(sequence_analysis_get_ui_name(analysis), VariantPointer::asQVariant(analysis)); if (item_data->flow->itemData(item_data->curr_index).toString().compare(item_data->info->sainfo()->name) == 0) item_data->flow->setCurrentIndex(item_data->curr_index); item_data->curr_index++; return FALSE; } SequenceInfo::SequenceInfo(seq_analysis_info_t *sainfo) : sainfo_(sainfo), count_(1) { } SequenceInfo::~SequenceInfo() { sequence_analysis_info_free(sainfo_); } /* * 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: */