wireshark/ui/qt/packet_list.cpp
John Thacker 52955b9c43 Qt: highlight when search result is in the current packet.
Calling setCurrentIndex with QItemSelectionModel::ClearAndSelect clears
the currentIndex, but not the selection, so it doesn't trigger
selectionChanged. However, highlighting depends on the selectionChanged
signal, not currentChanged(), and it's not emitted if we're still on the
same packet/row as the current selection (which can occur if searching
for something that occurs in exactly one packet in a capture.)

Since packet_list_select_row_from_data() cares about where the data
is within the packet, it needs to clear the selection.  Fix #8269
2022-02-11 18:21:35 +00:00

2135 lines
68 KiB
C++

/* packet_list.cpp
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
* Copyright 1998 Gerald Combs
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <ui/qt/packet_list.h>
#include "config.h"
#include <glib.h>
#include "file.h"
#include <epan/epan.h>
#include <epan/epan_dissect.h>
#include <epan/column-info.h>
#include <epan/column.h>
#include <epan/expert.h>
#include <epan/ipproto.h>
#include <epan/packet.h>
#include <epan/prefs.h>
#include <epan/proto.h>
#include "ui/main_statusbar.h"
#include "ui/packet_list_utils.h"
#include "ui/preference_utils.h"
#include "ui/recent.h"
#include "ui/recent_utils.h"
#include "ui/ws_ui_util.h"
#include <wsutil/utf8_entities.h>
#include "ui/util.h"
#include "wiretap/wtap_opttypes.h"
#include "wsutil/str_util.h"
#include <wsutil/wslog.h>
#include <epan/color_filters.h>
#include "frame_tvbuff.h"
#include <ui/qt/utils/color_utils.h>
#include <ui/qt/widgets/overlay_scroll_bar.h>
#include "proto_tree.h"
#include <ui/qt/utils/qt_ui_utils.h>
#include "wireshark_application.h"
#include <ui/qt/utils/data_printer.h>
#include <ui/qt/utils/frame_information.h>
#include <ui/qt/utils/variant_pointer.h>
#include <ui/qt/models/pref_models.h>
#include <ui/qt/widgets/packet_list_header.h>
#include <ui/qt/utils/wireshark_mime_data.h>
#include <ui/qt/widgets/drag_label.h>
#include <ui/qt/filter_action.h>
#include <ui/qt/decode_as_dialog.h>
#include <QAction>
#include <QActionGroup>
#include <QClipboard>
#include <QContextMenuEvent>
#include <QtCore/qmath.h>
#include <QElapsedTimer>
#include <QFontMetrics>
#include <QHeaderView>
#include <QMessageBox>
#include <QPainter>
#include <QScreen>
#include <QScrollBar>
#include <QTabWidget>
#include <QTextEdit>
#include <QTimerEvent>
#include <QTreeWidget>
#include <QWindow>
#include <QJsonObject>
#include <QJsonDocument>
#ifdef Q_OS_WIN
#include "wsutil/file_util.h"
#include <QSysInfo>
#include <Uxtheme.h>
#endif
// To do:
// - Fix "apply as filter" behavior.
// - Add colorize conversation.
// - Use a timer to trigger automatic scrolling.
// If we ever add the ability to open multiple capture files we might be
// able to use something like QMap<capture_file *, PacketList *> to match
// capture files against packet lists and models.
static PacketList *gbl_cur_packet_list = NULL;
const int max_comments_to_fetch_ = 20000000; // Arbitrary
const int tail_update_interval_ = 100; // Milliseconds.
const int overlay_update_interval_ = 100; // 250; // Milliseconds.
// Copied from ui/gtk/packet_list.c
void packet_list_resize_column(gint col)
{
if (!gbl_cur_packet_list) return;
gbl_cur_packet_list->resizeColumnToContents(col);
}
void
packet_list_select_first_row(void)
{
if (!gbl_cur_packet_list)
return;
gbl_cur_packet_list->goFirstPacket(false);
}
/*
* Given a frame_data structure, scroll to and select the row in the
* packet list corresponding to that frame. If there is no such
* row, return FALSE, otherwise return TRUE.
*/
gboolean
packet_list_select_row_from_data(frame_data *fdata_needle)
{
if (! gbl_cur_packet_list || ! gbl_cur_packet_list->model())
return FALSE;
PacketListModel * model = qobject_cast<PacketListModel *>(gbl_cur_packet_list->model());
if (! model)
return FALSE;
model->flushVisibleRows();
int row = model->visibleIndexOf(fdata_needle);
if (row >= 0) {
/* Calling ClearAndSelect with setCurrentIndex clears the "current"
* item, but doesn't clear the "selected" item. We want to clear
* the "selected" item as well so that selectionChanged() will be
* emitted in order to force an update of the packet details and
* packet bytes after a search.
*/
gbl_cur_packet_list->selectionModel()->clearSelection();
gbl_cur_packet_list->selectionModel()->setCurrentIndex(model->index(row, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
return TRUE;
}
return FALSE;
}
void
packet_list_clear(void)
{
if (gbl_cur_packet_list) {
gbl_cur_packet_list->clear();
}
}
void
packet_list_recolor_packets(void)
{
if (gbl_cur_packet_list) {
gbl_cur_packet_list->recolorPackets();
}
}
void
packet_list_freeze(void)
{
if (gbl_cur_packet_list) {
gbl_cur_packet_list->freeze();
}
}
void
packet_list_thaw(void)
{
if (gbl_cur_packet_list) {
gbl_cur_packet_list->thaw();
}
packets_bar_update();
}
frame_data *
packet_list_get_row_data(gint row)
{
if (gbl_cur_packet_list) {
return gbl_cur_packet_list->getFDataForRow(row);
}
return NULL;
}
// Called from cf_continue_tail and cf_finish_tail when auto_scroll_live
// is enabled.
void
packet_list_moveto_end(void)
{
// gbl_cur_packet_list->scrollToBottom();
}
/* Redraw the packet list *and* currently-selected detail */
void
packet_list_queue_draw(void)
{
if (gbl_cur_packet_list)
gbl_cur_packet_list->redrawVisiblePackets();
}
void
packet_list_recent_write_all(FILE *rf) {
if (!gbl_cur_packet_list)
return;
gbl_cur_packet_list->writeRecent(rf);
}
gboolean
packet_list_multi_select_active(void)
{
if (gbl_cur_packet_list) {
return gbl_cur_packet_list->multiSelectActive();
}
return FALSE;
}
#define MIN_COL_WIDTH_STR "MMMMMM"
PacketList::PacketList(QWidget *parent) :
QTreeView(parent),
proto_tree_(NULL),
cap_file_(NULL),
ctx_column_(-1),
overlay_timer_id_(0),
create_near_overlay_(true),
create_far_overlay_(true),
mouse_pressed_at_(QModelIndex()),
capture_in_progress_(false),
tail_timer_id_(0),
tail_at_end_(0),
rows_inserted_(false),
columns_changed_(false),
set_column_visibility_(false),
frozen_rows_(QModelIndexList()),
cur_history_(-1),
in_history_(false)
{
setItemsExpandable(false);
setRootIsDecorated(false);
setSortingEnabled(true);
setUniformRowHeights(true);
setAccessibleName("Packet list");
proto_prefs_menus_.setTitle(tr("Protocol Preferences"));
packet_list_header_ = new PacketListHeader(header()->orientation(), cap_file_);
connect(packet_list_header_, &PacketListHeader::resetColumnWidth, this, &PacketList::setRecentColumnWidth);
connect(packet_list_header_, &PacketListHeader::updatePackets, this, &PacketList::updatePackets);
connect(packet_list_header_, &PacketListHeader::showColumnPreferences, this, &PacketList::showProtocolPreferences);
connect(packet_list_header_, &PacketListHeader::editColumn, this, &PacketList::editColumn);
connect(packet_list_header_, &PacketListHeader::columnsChanged, this, &PacketList::columnsChanged);
setHeader(packet_list_header_);
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
header()->setFirstSectionMovable(true);
#endif
setSelectionMode(QAbstractItemView::ExtendedSelection);
// Shrink down to a small but nonzero size in the main splitter.
int one_em = fontMetrics().height();
setMinimumSize(one_em, one_em);
overlay_sb_ = new OverlayScrollBar(Qt::Vertical, this);
setVerticalScrollBar(overlay_sb_);
header()->setSortIndicator(-1, Qt::AscendingOrder);
packet_list_model_ = new PacketListModel(this, cap_file_);
setModel(packet_list_model_);
Q_ASSERT(gbl_cur_packet_list == Q_NULLPTR);
gbl_cur_packet_list = this;
connect(packet_list_model_, SIGNAL(goToPacket(int)), this, SLOT(goToPacket(int)));
connect(packet_list_model_, SIGNAL(itemHeightChanged(const QModelIndex&)), this, SLOT(updateRowHeights(const QModelIndex&)));
connect(wsApp, SIGNAL(addressResolutionChanged()), this, SLOT(redrawVisiblePacketsDontSelectCurrent()));
connect(wsApp, SIGNAL(columnDataChanged()), this, SLOT(redrawVisiblePacketsDontSelectCurrent()));
connect(header(), SIGNAL(sectionResized(int,int,int)),
this, SLOT(sectionResized(int,int,int)));
connect(header(), SIGNAL(sectionMoved(int,int,int)),
this, SLOT(sectionMoved(int,int,int)));
connect(verticalScrollBar(), SIGNAL(actionTriggered(int)), this, SLOT(vScrollBarActionTriggered(int)));
}
void PacketList::colorsChanged()
{
const QString c_active = "active";
const QString c_inactive = "!active";
QString flat_style_format =
"QTreeView::item:selected:%1 {"
" color: %2;"
" background-color: %3;"
"}";
QString gradient_style_format =
"QTreeView::item:selected:%1 {"
" color: %2;"
" background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1 stop: 0 %4, stop: 0.5 %3, stop: 1 %4);"
"}";
QString hover_style;
#if !defined(Q_OS_WIN)
hover_style = QString(
"QTreeView:item:hover {"
" background-color: %1;"
" color: palette(text);"
"}").arg(ColorUtils::hoverBackground().name(QColor::HexArgb));
#endif
QString active_style = QString();
QString inactive_style = QString();
if (prefs.gui_active_style == COLOR_STYLE_DEFAULT) {
// ACTIVE = Default
} else if (prefs.gui_active_style == COLOR_STYLE_FLAT) {
// ACTIVE = Flat
QColor foreground = ColorUtils::fromColorT(prefs.gui_active_fg);
QColor background = ColorUtils::fromColorT(prefs.gui_active_bg);
active_style = flat_style_format.arg(
c_active,
foreground.name(),
background.name());
} else if (prefs.gui_active_style == COLOR_STYLE_GRADIENT) {
// ACTIVE = Gradient
QColor foreground = ColorUtils::fromColorT(prefs.gui_active_fg);
QColor background1 = ColorUtils::fromColorT(prefs.gui_active_bg);
QColor background2 = QColor::fromRgb(ColorUtils::alphaBlend(foreground, background1, COLOR_STYLE_ALPHA));
active_style = gradient_style_format.arg(
c_active,
foreground.name(),
background1.name(),
background2.name());
}
// INACTIVE style sheet settings
if (prefs.gui_inactive_style == COLOR_STYLE_DEFAULT) {
// INACTIVE = Default
} else if (prefs.gui_inactive_style == COLOR_STYLE_FLAT) {
// INACTIVE = Flat
QColor foreground = ColorUtils::fromColorT(prefs.gui_inactive_fg);
QColor background = ColorUtils::fromColorT(prefs.gui_inactive_bg);
inactive_style = flat_style_format.arg(
c_inactive,
foreground.name(),
background.name());
} else if (prefs.gui_inactive_style == COLOR_STYLE_GRADIENT) {
// INACTIVE = Gradient
QColor foreground = ColorUtils::fromColorT(prefs.gui_inactive_fg);
QColor background1 = ColorUtils::fromColorT(prefs.gui_inactive_bg);
QColor background2 = QColor::fromRgb(ColorUtils::alphaBlend(foreground, background1, COLOR_STYLE_ALPHA));
inactive_style = gradient_style_format.arg(
c_inactive,
foreground.name(),
background1.name(),
background2.name());
}
// Set the style sheet
if(prefs.gui_qt_packet_list_hover_style) {
setStyleSheet(active_style + inactive_style + hover_style);
} else {
setStyleSheet(active_style + inactive_style);
}
}
QString PacketList::joinSummaryRow(QStringList col_parts, int row, SummaryCopyType type)
{
QString copy_text;
switch (type) {
case CopyAsCSV:
copy_text = "\"";
copy_text += col_parts.join("\",\"");
copy_text += "\"";
break;
case CopyAsYAML:
copy_text = "----\n";
copy_text += QString("# Packet %1 from %2\n").arg(row).arg(cap_file_->filename);
copy_text += "- ";
copy_text += col_parts.join("\n- ");
copy_text += "\n";
break;
case CopyAsText:
default:
copy_text = col_parts.join("\t");
}
return copy_text;
}
void PacketList::drawRow (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QTreeView::drawRow(painter, option, index);
if (prefs.gui_qt_packet_list_separator) {
QRect rect = visualRect(index);
painter->setPen(QColor(Qt::white));
painter->drawLine(0, rect.y() + rect.height() - 1, width(), rect.y() + rect.height() - 1);
}
}
void PacketList::setProtoTree (ProtoTree *proto_tree) {
proto_tree_ = proto_tree;
connect(proto_tree_, SIGNAL(goToPacket(int)), this, SLOT(goToPacket(int)));
connect(proto_tree_, SIGNAL(relatedFrame(int,ft_framenum_type_t)),
&related_packet_delegate_, SLOT(addRelatedFrame(int,ft_framenum_type_t)));
}
bool PacketList::multiSelectActive()
{
return selectionModel()->selectedRows(0).count() > 1 ? true : false;
}
QList<int> PacketList::selectedRows(bool useFrameNum)
{
QList<int> rows;
if (selectionModel() && selectionModel()->hasSelection())
{
foreach (QModelIndex idx, selectionModel()->selectedRows(0))
{
if (idx.isValid())
{
if (! useFrameNum)
rows << idx.row();
else if (useFrameNum)
{
frame_data * frame = getFDataForRow(idx.row());
if (frame)
rows << frame->num;
}
}
}
}
else if (currentIndex().isValid())
{
//
// XXX - will we ever have a current index but not a selection
// model?
//
if (! useFrameNum)
rows << currentIndex().row();
else
{
frame_data *frame = getFDataForRow(currentIndex().row());
if (frame)
rows << frame->num;
}
}
return rows;
}
void PacketList::selectionChanged (const QItemSelection & selected, const QItemSelection & deselected)
{
QTreeView::selectionChanged(selected, deselected);
if (!cap_file_) return;
int row = -1;
static bool multiSelect = false;
if (selectionModel())
{
QModelIndexList selRows = selectionModel()->selectedRows(0);
if (selRows.count() > 1)
{
QList<int> rows;
foreach (QModelIndex idx, selRows)
{
if (idx.isValid())
rows << idx.row();
}
emit framesSelected(rows);
emit fieldSelected(0);
cf_unselect_packet(cap_file_);
/* We have to repaint the content while changing state, as some delegates react to multi-select */
if (! multiSelect)
{
related_packet_delegate_.clear();
viewport()->update();
}
multiSelect = true;
return;
}
else if (selRows.count() > 0 && selRows.at(0).isValid())
{
multiSelect = false;
row = selRows.at(0).row();
}
/* Handling empty selection */
if (selRows.count() <= 0)
{
/* Nothing selected, but multiSelect is still active */
if (multiSelect)
{
multiSelect = false;
if (currentIndex().isValid())
{
selectionModel()->select(currentIndex(), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows );
return;
}
}
/* Nothing selected, so in WS <= 3.0 nothing was indicated as well */
else if (currentIndex().isValid())
{
setCurrentIndex(QModelIndex());
}
}
}
if (row < 0)
cf_unselect_packet(cap_file_);
else
cf_select_packet(cap_file_, row);
if (!in_history_ && cap_file_->current_frame) {
cur_history_++;
selection_history_.resize(cur_history_);
selection_history_.append(cap_file_->current_frame->num);
}
in_history_ = false;
related_packet_delegate_.clear();
// The previous dissection state has been invalidated by cf_select_packet
// above, receivers must clear the previous state and apply the updated one.
emit framesSelected(QList<int>() << row);
if (!cap_file_->edt) {
viewport()->update();
emit fieldSelected(0);
return;
}
if (cap_file_->edt->tree) {
packet_info *pi = &cap_file_->edt->pi;
related_packet_delegate_.setCurrentFrame(pi->num);
conversation_t *conv = find_conversation_pinfo(pi, 0);
if (conv) {
related_packet_delegate_.setConversation(conv);
}
viewport()->update();
}
if (cap_file_->search_in_progress) {
match_data mdata;
field_info *fi = NULL;
if (cap_file_->string && cap_file_->decode_data) {
// The tree where the target string matched one of the labels was discarded in
// match_protocol_tree() so we have to search again in the latest tree.
if (cf_find_string_protocol_tree(cap_file_, cap_file_->edt->tree, &mdata)) {
fi = mdata.finfo;
}
} else if (cap_file_->search_pos != 0) {
// Find the finfo that corresponds to our byte.
fi = proto_find_field_from_offset(cap_file_->edt->tree, cap_file_->search_pos,
cap_file_->edt->tvb);
}
if (fi) {
FieldInformation finfo(fi, this);
emit fieldSelected(&finfo);
} else {
emit fieldSelected(0);
}
} else if (proto_tree_) {
proto_tree_->restoreSelectedField();
}
}
void PacketList::contextMenuEvent(QContextMenuEvent *event)
{
const char *module_name = NULL;
proto_prefs_menus_.clear();
if (cap_file_ && cap_file_->edt && cap_file_->edt->tree) {
GPtrArray *finfo_array = proto_all_finfos(cap_file_->edt->tree);
QList<QString> added_proto_prefs;
for (guint i = 0; i < finfo_array->len; i++) {
field_info *fi = (field_info *)g_ptr_array_index (finfo_array, i);
header_field_info *hfinfo = fi->hfinfo;
if (prefs_is_registered_protocol(hfinfo->abbrev)) {
if (hfinfo->parent == -1) {
module_name = hfinfo->abbrev;
} else {
module_name = proto_registrar_get_abbrev(hfinfo->parent);
}
if (added_proto_prefs.contains(module_name)) {
continue;
}
ProtocolPreferencesMenu *proto_prefs_menu = new ProtocolPreferencesMenu(hfinfo->name, module_name, &proto_prefs_menus_);
connect(proto_prefs_menu, SIGNAL(showProtocolPreferences(QString)),
this, SIGNAL(showProtocolPreferences(QString)));
connect(proto_prefs_menu, SIGNAL(editProtocolPreference(preference*,pref_module*)),
this, SIGNAL(editProtocolPreference(preference*,pref_module*)));
proto_prefs_menus_.addMenu(proto_prefs_menu);
added_proto_prefs << module_name;
}
}
g_ptr_array_free(finfo_array, TRUE);
}
QModelIndex ctxIndex = indexAt(event->pos());
if (selectionModel() && selectionModel()->selectedRows(0).count() > 1)
selectionModel()->select(ctxIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
// frameData will be owned by one of the submenus, see below.
FrameInformation * frameData =
new FrameInformation(new CaptureFile(this, cap_file_), packet_list_model_->getRowFdata(ctxIndex.row()));
QMenu * ctx_menu = new QMenu(this);
// XXX We might want to reimplement setParent() and fill in the context
// menu there.
ctx_menu->addAction(window()->findChild<QAction *>("actionEditMarkPacket"));
ctx_menu->addAction(window()->findChild<QAction *>("actionEditIgnorePacket"));
ctx_menu->addAction(window()->findChild<QAction *>("actionEditSetTimeReference"));
ctx_menu->addAction(window()->findChild<QAction *>("actionEditTimeShift"));
ctx_menu->addMenu(window()->findChild<QMenu *>("menuPacketComment"));
ctx_menu->addSeparator();
ctx_menu->addAction(window()->findChild<QAction *>("actionViewEditResolvedName"));
ctx_menu->addSeparator();
QString selectedfilter = getFilterFromRowAndColumn(currentIndex());
if (! hasFocus() && cap_file_ && cap_file_->finfo_selected) {
char *tmp_field = proto_construct_match_selected_string(cap_file_->finfo_selected, cap_file_->edt);
selectedfilter = QString(tmp_field);
wmem_free(NULL, tmp_field);
}
bool have_filter_expr = !selectedfilter.isEmpty();
ctx_menu->addMenu(FilterAction::createFilterMenu(FilterAction::ActionApply, selectedfilter, have_filter_expr, ctx_menu));
ctx_menu->addMenu(FilterAction::createFilterMenu(FilterAction::ActionPrepare, selectedfilter, have_filter_expr, ctx_menu));
const char *conv_menu_name = "menuConversationFilter";
QMenu * main_menu_item = window()->findChild<QMenu *>(conv_menu_name);
conv_menu_.setTitle(main_menu_item->title());
conv_menu_.setObjectName(conv_menu_name);
ctx_menu->addMenu(&conv_menu_);
const char *colorize_menu_name = "menuColorizeConversation";
main_menu_item = window()->findChild<QMenu *>(colorize_menu_name);
colorize_menu_.setTitle(main_menu_item->title());
colorize_menu_.setObjectName(colorize_menu_name);
ctx_menu->addMenu(&colorize_menu_);
main_menu_item = window()->findChild<QMenu *>("menuSCTP");
QMenu * submenu = new QMenu(main_menu_item->title(), ctx_menu);
ctx_menu->addMenu(submenu);
submenu->addAction(window()->findChild<QAction *>("actionSCTPAnalyseThisAssociation"));
submenu->addAction(window()->findChild<QAction *>("actionSCTPShowAllAssociations"));
submenu->addAction(window()->findChild<QAction *>("actionSCTPFilterThisAssociation"));
main_menu_item = window()->findChild<QMenu *>("menuFollow");
submenu = new QMenu(main_menu_item->title(), ctx_menu);
ctx_menu->addMenu(submenu);
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowTCPStream"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowUDPStream"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowDCCPStream"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowTLSStream"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowHTTPStream"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowHTTP2Stream"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowQUICStream"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowSIPCall"));
ctx_menu->addSeparator();
main_menu_item = window()->findChild<QMenu *>("menuEditCopy");
submenu = new QMenu(main_menu_item->title(), ctx_menu);
ctx_menu->addMenu(submenu);
QAction * action = submenu->addAction(tr("Summary as Text"));
action->setData(CopyAsText);
connect(action, SIGNAL(triggered()), this, SLOT(copySummary()));
action = submenu->addAction(tr("…as CSV"));
action->setData(CopyAsCSV);
connect(action, SIGNAL(triggered()), this, SLOT(copySummary()));
action = submenu->addAction(tr("…as YAML"));
action->setData(CopyAsYAML);
connect(action, SIGNAL(triggered()), this, SLOT(copySummary()));
submenu->addSeparator();
submenu->addAction(window()->findChild<QAction *>("actionEditCopyAsFilter"));
submenu->addSeparator();
QActionGroup * copyEntries = DataPrinter::copyActions(this, frameData);
submenu->addActions(copyEntries->actions());
copyEntries->setParent(submenu);
frameData->setParent(submenu);
ctx_menu->addSeparator();
ctx_menu->addMenu(&proto_prefs_menus_);
action = ctx_menu->addAction(tr("Decode As…"));
action->setProperty("create_new", QVariant(true));
connect(action, &QAction::triggered, this, &PacketList::ctxDecodeAsDialog);
// "Print" not ported intentionally
action = window()->findChild<QAction *>("actionViewShowPacketInNewWindow");
ctx_menu->addAction(action);
// Set menu sensitivity for the current column and set action data.
if (frameData)
emit framesSelected(QList<int>() << frameData->frameNum());
else
emit framesSelected(QList<int>());
ctx_menu->exec(event->globalPos());
}
void PacketList::ctxDecodeAsDialog()
{
QAction *da_action = qobject_cast<QAction*>(sender());
if (! da_action)
return;
bool create_new = da_action->property("create_new").toBool();
DecodeAsDialog *da_dialog = new DecodeAsDialog(this, cap_file_, create_new);
connect(da_dialog, SIGNAL(destroyed(QObject*)), wsApp, SLOT(flushAppSignals()));
da_dialog->setWindowModality(Qt::ApplicationModal);
da_dialog->setAttribute(Qt::WA_DeleteOnClose);
da_dialog->show();
}
// Auto scroll if:
// - We're not at the end
// - We are capturing
// - actionGoAutoScroll in the main UI is checked.
// - It's been more than tail_update_interval_ ms since we last scrolled
// - The last user-set vertical scrollbar position was at the end.
// Using a timer assumes that we can save CPU overhead by updating
// periodically. If that's not the case we can dispense with it and call
// scrollToBottom() from rowsInserted().
void PacketList::timerEvent(QTimerEvent *event)
{
if (event->timerId() == tail_timer_id_) {
if (rows_inserted_ && capture_in_progress_ && tail_at_end_) {
scrollToBottom();
rows_inserted_ = false;
}
} else if (event->timerId() == overlay_timer_id_) {
if (!capture_in_progress_) {
if (create_near_overlay_) drawNearOverlay();
if (create_far_overlay_) drawFarOverlay();
}
} else {
QTreeView::timerEvent(event);
}
}
void PacketList::paintEvent(QPaintEvent *event)
{
// XXX This is overkill, but there are quite a few events that
// require a new overlay, e.g. page up/down, scrolling, column
// resizing, etc.
create_near_overlay_ = true;
QTreeView::paintEvent(event);
}
void PacketList::mousePressEvent (QMouseEvent *event)
{
setAutoScroll(false);
QTreeView::mousePressEvent(event);
setAutoScroll(true);
QModelIndex curIndex = indexAt(event->pos());
mouse_pressed_at_ = curIndex;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
bool midButton = (event->buttons() & Qt::MiddleButton) == Qt::MiddleButton;
#else
bool midButton = (event->buttons() & Qt::MidButton) == Qt::MidButton;
#endif
if (midButton && cap_file_ && packet_list_model_)
{
packet_list_model_->toggleFrameMark(QModelIndexList() << curIndex);
// Make sure the packet list's frame.marked related field text is updated.
redrawVisiblePackets();
create_far_overlay_ = true;
packets_bar_update();
}
}
void PacketList::mouseReleaseEvent(QMouseEvent *event) {
QTreeView::mouseReleaseEvent(event);
mouse_pressed_at_ = QModelIndex();
}
void PacketList::mouseMoveEvent (QMouseEvent *event)
{
QModelIndex curIndex = indexAt(event->pos());
if (event->buttons() & Qt::LeftButton && curIndex.isValid() && curIndex == mouse_pressed_at_)
{
ctx_column_ = curIndex.column();
QMimeData * mimeData = new QMimeData();
QWidget * content = nullptr;
QString filter = getFilterFromRowAndColumn(curIndex);
QList<int> rows = selectedRows();
if (rows.count() > 1)
{
QStringList content;
foreach (int row, rows)
{
QModelIndex idx = model()->index(row, 0);
if (! idx.isValid())
continue;
QString entry = createSummaryText(idx, CopyAsText);
content << entry;
}
if (content.count() > 0)
mimeData->setText(content.join("\n"));
}
else if (! filter.isEmpty())
{
QString abbrev;
QString name = model()->headerData(curIndex.column(), header()->orientation()).toString();
if (! filter.isEmpty())
{
abbrev = filter.left(filter.indexOf(' '));
}
else
{
filter = model()->data(curIndex).toString().toLower();
abbrev = filter;
}
mimeData->setText(filter);
QJsonObject filterData;
filterData["filter"] = filter;
filterData["name"] = abbrev;
filterData["description"] = name;
mimeData->setData(WiresharkMimeData::DisplayFilterMimeType, QJsonDocument(filterData).toJson());
content = new DragLabel(QString("%1\n%2").arg(name, abbrev), this);
}
else
{
QString text = model()->data(curIndex).toString();
if (! text.isEmpty())
mimeData->setText(text);
}
if (mimeData->hasText() || mimeData->hasFormat(WiresharkMimeData::DisplayFilterMimeType))
{
QDrag * drag = new QDrag(this);
drag->setMimeData(mimeData);
if (content)
{
qreal dpr = window()->windowHandle()->devicePixelRatio();
QPixmap pixmap= QPixmap(content->size() * dpr);
pixmap.setDevicePixelRatio(dpr);
content->render(&pixmap);
drag->setPixmap(pixmap);
}
drag->exec(Qt::CopyAction);
}
else
{
delete mimeData;
}
}
}
void PacketList::keyPressEvent(QKeyEvent *event)
{
QTreeView::keyPressEvent(event);
if (event->matches(QKeySequence::Copy))
{
QStringList content;
if (model() && selectionModel() && selectionModel()->hasSelection())
{
QList<int> rows;
QModelIndexList selRows = selectionModel()->selectedRows(0);
foreach(QModelIndex row, selRows)
rows.append(row.row());
foreach(int row, rows)
{
QModelIndex idx = model()->index(row, 0);
if (! idx.isValid())
continue;
QString entry = createSummaryText(idx, CopyAsText);
content << entry;
}
}
if (content.count() > 0)
wsApp->clipboard()->setText(content.join('\n'), QClipboard::Clipboard);
}
}
void PacketList::resizeEvent(QResizeEvent *event)
{
create_near_overlay_ = true;
create_far_overlay_ = true;
QTreeView::resizeEvent(event);
}
void PacketList::setColumnVisibility()
{
set_column_visibility_ = true;
for (int i = 0; i < prefs.num_cols; i++) {
setColumnHidden(i, get_column_visible(i) ? false : true);
}
set_column_visibility_ = false;
}
int PacketList::sizeHintForColumn(int column) const
{
int size_hint = 0;
// This is a bit hacky but Qt does a fine job of column sizing and
// reimplementing QTreeView::sizeHintForColumn seems like a worse idea.
if (itemDelegateForColumn(column)) {
QStyleOptionViewItem option;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
initViewItemOption(&option);
#else
option = viewOptions();
#endif
// In my (gcc) testing this results in correct behavior on Windows but adds extra space
// on macOS and Linux. We might want to add Q_OS_... #ifdefs accordingly.
size_hint = itemDelegateForColumn(column)->sizeHint(option, QModelIndex()).width();
}
size_hint += QTreeView::sizeHintForColumn(column); // Decoration padding
return size_hint;
}
void PacketList::setRecentColumnWidth(int col)
{
int col_width = recent_get_column_width(col);
if (col_width < 1) {
int fmt = get_column_format(col);
const char *long_str = get_column_width_string(fmt, col);
QFontMetrics fm = QFontMetrics(wsApp->monospaceFont());
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
if (long_str) {
col_width = fm.horizontalAdvance(long_str);
} else {
col_width = fm.horizontalAdvance(MIN_COL_WIDTH_STR);
}
#else
if (long_str) {
col_width = fm.width(long_str);
} else {
col_width = fm.width(MIN_COL_WIDTH_STR);
}
#endif
// Custom delegate padding
if (itemDelegateForColumn(col)) {
QStyleOptionViewItem option;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
initViewItemOption(&option);
#else
option = viewOptions();
#endif
col_width += itemDelegateForColumn(col)->sizeHint(option, QModelIndex()).width();
}
}
setColumnWidth(col, col_width);
}
void PacketList::drawCurrentPacket()
{
QModelIndex current_index = currentIndex();
if (selectionModel() && current_index.isValid()) {
selectionModel()->clearCurrentIndex();
selectionModel()->setCurrentIndex(current_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
}
}
// Redraw the packet list and detail. Re-selects the current packet (causes
// the UI to scroll to that packet).
// Called from many places.
void PacketList::redrawVisiblePackets() {
redrawVisiblePacketsDontSelectCurrent();
drawCurrentPacket();
}
// Redraw the packet list and detail.
// Does not scroll back to the selected packet.
void PacketList::redrawVisiblePacketsDontSelectCurrent() {
packet_list_model_->invalidateAllColumnStrings();
}
void PacketList::resetColumns()
{
packet_list_model_->resetColumns();
}
// Return true if we have a visible packet further along in the history.
bool PacketList::haveNextHistory(bool update_cur)
{
if (selection_history_.size() < 1 || cur_history_ >= selection_history_.size() - 1) {
return false;
}
for (int i = cur_history_ + 1; i < selection_history_.size(); i++) {
if (packet_list_model_->packetNumberToRow(selection_history_.at(i)) >= 0) {
if (update_cur) {
cur_history_ = i;
}
return true;
}
}
return false;
}
// Return true if we have a visible packet back in the history.
bool PacketList::havePreviousHistory(bool update_cur)
{
if (selection_history_.size() < 1 || cur_history_ < 1) {
return false;
}
for (int i = cur_history_ - 1; i >= 0; i--) {
if (packet_list_model_->packetNumberToRow(selection_history_.at(i)) >= 0) {
if (update_cur) {
cur_history_ = i;
}
return true;
}
}
return false;
}
frame_data *PacketList::getFDataForRow(int row) const
{
return packet_list_model_->getRowFdata(row);
}
// prefs.col_list has changed.
void PacketList::columnsChanged()
{
columns_changed_ = true;
if (!cap_file_) {
// Keep columns_changed_ = true until we load a capture file.
return;
}
prefs.num_cols = g_list_length(prefs.col_list);
col_cleanup(&cap_file_->cinfo);
build_column_format_array(&cap_file_->cinfo, prefs.num_cols, FALSE);
create_far_overlay_ = true;
resetColumns();
applyRecentColumnWidths();
setColumnVisibility();
columns_changed_ = false;
}
// Fields have changed, update custom columns
void PacketList::fieldsChanged(capture_file *cf)
{
prefs.num_cols = g_list_length(prefs.col_list);
col_cleanup(&cf->cinfo);
build_column_format_array(&cf->cinfo, prefs.num_cols, FALSE);
resetColumns();
}
// Column widths should
// - Load from recent when we load a new profile (including at starting up).
// - Reapply when changing columns.
// - Persist across freezes and thaws.
// - Persist across file closing and opening.
// - Save to recent when we save our profile (including shutting down).
// - Not be affected by the behavior of stretchLastSection.
void PacketList::applyRecentColumnWidths()
{
// Either we've just started up or a profile has changed. Read
// the recent settings, apply them, and save the header state.
int column_width = 0;
for (int col = 0; col < prefs.num_cols; col++) {
// The column must be shown before setting column width.
// Visibility will be updated in setColumnVisibility().
setColumnHidden(col, false);
setRecentColumnWidth(col);
column_width += columnWidth(col);
}
if (column_width > width()) {
resize(column_width, height());
}
column_state_ = header()->saveState();
}
void PacketList::preferencesChanged()
{
// Update color style changes
colorsChanged();
// Related packet delegate
if (prefs.gui_packet_list_show_related) {
setItemDelegateForColumn(0, &related_packet_delegate_);
} else {
setItemDelegateForColumn(0, 0);
}
// Intelligent scroll bar (minimap)
if (prefs.gui_packet_list_show_minimap) {
if (overlay_timer_id_ == 0) {
overlay_timer_id_ = startTimer(overlay_update_interval_);
}
} else {
if (overlay_timer_id_ != 0) {
killTimer(overlay_timer_id_);
overlay_timer_id_ = 0;
}
}
// Elide mode.
// This sets the mode for the entire view. If we want to make this setting
// per-column we'll either have to generalize RelatedPacketDelegate so that
// we can set it for entire rows or create another delegate.
Qt::TextElideMode elide_mode = Qt::ElideRight;
switch (prefs.gui_packet_list_elide_mode) {
case ELIDE_LEFT:
elide_mode = Qt::ElideLeft;
break;
case ELIDE_MIDDLE:
elide_mode = Qt::ElideMiddle;
break;
case ELIDE_NONE:
elide_mode = Qt::ElideNone;
break;
default:
break;
}
setTextElideMode(elide_mode);
}
void PacketList::recolorPackets()
{
packet_list_model_->resetColorized();
redrawVisiblePackets();
}
/* Enable autoscroll timer. Note: must be called after the capture is started,
* otherwise the timer will not be executed. */
void PacketList::setVerticalAutoScroll(bool enabled)
{
tail_at_end_ = enabled;
if (enabled && capture_in_progress_) {
scrollToBottom();
if (tail_timer_id_ == 0) tail_timer_id_ = startTimer(tail_update_interval_);
} else if (tail_timer_id_ != 0) {
killTimer(tail_timer_id_);
tail_timer_id_ = 0;
}
}
// Called when we finish reading, reloading, rescanning, and retapping
// packets.
void PacketList::captureFileReadFinished()
{
packet_list_model_->flushVisibleRows();
packet_list_model_->dissectIdle(true);
// Invalidating the column strings picks up and request/response
// tracking changes. We might just want to call it from flushVisibleRows.
packet_list_model_->invalidateAllColumnStrings();
}
void PacketList::freeze()
{
column_state_ = header()->saveState();
setHeaderHidden(true);
frozen_rows_ = selectedIndexes();
selectionModel()->clear();
setModel(Q_NULLPTR);
// It looks like GTK+ sends a cursor-changed signal at this point but Qt doesn't
// call selectionChanged.
related_packet_delegate_.clear();
/* Clears packet list as well as byteview */
emit framesSelected(QList<int>());
}
void PacketList::thaw(bool restore_selection)
{
setHeaderHidden(false);
setModel(packet_list_model_);
// Resetting the model resets our column widths so we restore them here.
// We don't reapply the recent settings because the user could have
// resized the columns manually since they were initially loaded.
header()->restoreState(column_state_);
if (restore_selection && frozen_rows_.length() > 0 && selectionModel()) {
/* This updates our selection, which redissects the current packet,
* which is needed when we're called from MainWindow::layoutPanes.
* Also, this resets all ProtoTree and ByteView data */
clearSelection();
foreach (QModelIndex idx, frozen_rows_) {
selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows);
}
}
frozen_rows_ = QModelIndexList();
}
void PacketList::clear() {
related_packet_delegate_.clear();
selectionModel()->clear();
packet_list_model_->clear();
proto_tree_->clear();
selection_history_.clear();
cur_history_ = -1;
in_history_ = false;
QImage overlay;
overlay_sb_->setNearOverlayImage(overlay);
overlay_sb_->setMarkedPacketImage(overlay);
create_near_overlay_ = true;
create_far_overlay_ = true;
}
void PacketList::writeRecent(FILE *rf) {
gint col, width, col_fmt;
gchar xalign;
fprintf (rf, "%s:\n", RECENT_KEY_COL_WIDTH);
for (col = 0; col < prefs.num_cols; col++) {
if (col > 0) {
fprintf (rf, ",\n");
}
col_fmt = get_column_format(col);
if (col_fmt == COL_CUSTOM) {
fprintf (rf, " \"%%Cus:%s\",", get_column_custom_fields(col));
} else {
fprintf (rf, " %s,", col_format_to_string(col_fmt));
}
width = recent_get_column_width (col);
xalign = recent_get_column_xalign (col);
fprintf (rf, " %d", width);
if (xalign != COLUMN_XALIGN_DEFAULT) {
fprintf (rf, ":%c", xalign);
}
}
fprintf (rf, "\n");
}
bool PacketList::contextMenuActive()
{
return ctx_column_ >= 0 ? true : false;
}
QString PacketList::getFilterFromRowAndColumn(QModelIndex idx)
{
frame_data *fdata;
QString filter;
if (! idx.isValid())
return filter;
int row = idx.row();
int column = idx.column();
if (!cap_file_ || !packet_list_model_ || column < 0 || column >= cap_file_->cinfo.num_cols)
return filter;
fdata = packet_list_model_->getRowFdata(row);
if (fdata != NULL) {
epan_dissect_t edt;
wtap_rec rec; /* Record metadata */
Buffer buf; /* Record data */
wtap_rec_init(&rec);
ws_buffer_init(&buf, 1514);
if (!cf_read_record(cap_file_, fdata, &rec, &buf)) {
wtap_rec_cleanup(&rec);
ws_buffer_free(&buf);
return filter; /* error reading the record */
}
/* proto tree, visible. We need a proto tree if there's custom columns */
epan_dissect_init(&edt, cap_file_->epan, have_custom_cols(&cap_file_->cinfo), FALSE);
col_custom_prime_edt(&edt, &cap_file_->cinfo);
epan_dissect_run(&edt, cap_file_->cd_t, &rec,
frame_tvbuff_new_buffer(&cap_file_->provider, fdata, &buf),
fdata, &cap_file_->cinfo);
epan_dissect_fill_in_columns(&edt, TRUE, TRUE);
if ((cap_file_->cinfo.columns[column].col_custom_occurrence) ||
(strchr (cap_file_->cinfo.col_expr.col_expr_val[column], ',') == NULL))
{
/* Only construct the filter when a single occurrence is displayed
* otherwise we might end up with a filter like "ip.proto==1,6".
*
* Or do we want to be able to filter on multiple occurrences so that
* the filter might be calculated as "ip.proto==1 && ip.proto==6"
* instead?
*/
if (strlen(cap_file_->cinfo.col_expr.col_expr[column]) != 0 &&
strlen(cap_file_->cinfo.col_expr.col_expr_val[column]) != 0) {
gboolean is_string_value = FALSE;
if (cap_file_->cinfo.columns[column].col_fmt == COL_CUSTOM) {
header_field_info *hfi = proto_registrar_get_byname(cap_file_->cinfo.columns[column].col_custom_fields);
if (hfi && hfi->parent == -1) {
/* Protocol only */
filter.append(cap_file_->cinfo.col_expr.col_expr[column]);
} else if (hfi && hfi->type == FT_STRING) {
/* Custom string, add quotes */
is_string_value = TRUE;
}
} else {
header_field_info *hfi = proto_registrar_get_byname(cap_file_->cinfo.col_expr.col_expr[column]);
if (hfi && hfi->type == FT_STRING) {
/* Could be an address type such as usb.src which must be quoted. */
is_string_value = TRUE;
}
}
if (filter.isEmpty()) {
if (is_string_value) {
filter.append(QString("%1 == \"%2\"")
.arg(cap_file_->cinfo.col_expr.col_expr[column])
.arg(cap_file_->cinfo.col_expr.col_expr_val[column]));
} else {
filter.append(QString("%1 == %2")
.arg(cap_file_->cinfo.col_expr.col_expr[column])
.arg(cap_file_->cinfo.col_expr.col_expr_val[column]));
}
}
}
}
epan_dissect_cleanup(&edt);
wtap_rec_cleanup(&rec);
ws_buffer_free(&buf);
}
return filter;
}
void PacketList::resetColorized()
{
packet_list_model_->resetColorized();
update();
}
QString PacketList::getPacketComment(guint c_number)
{
int row = currentIndex().row();
const frame_data *fdata;
char *pkt_comment;
wtap_opttype_return_val result;
QString ret_val = NULL;
if (!cap_file_ || !packet_list_model_) return NULL;
fdata = packet_list_model_->getRowFdata(row);
if (!fdata) return NULL;
wtap_block_t pkt_block = cf_get_packet_block(cap_file_, fdata);
result = wtap_block_get_nth_string_option_value(pkt_block, OPT_COMMENT, c_number, &pkt_comment);
if (result == WTAP_OPTTYPE_SUCCESS) {
ret_val = QString(pkt_comment);
}
wtap_block_unref(pkt_block);
return ret_val;
}
void PacketList::addPacketComment(QString new_comment)
{
frame_data *fdata;
if (!cap_file_ || !packet_list_model_) return;
if (new_comment.isEmpty()) return;
QByteArray ba = new_comment.toLocal8Bit();
for (int i = 0; i < selectedRows().size(); i++) {
int row = selectedRows().at(i);
fdata = packet_list_model_->getRowFdata(row);
if (!fdata) continue;
wtap_block_t pkt_block = cf_get_packet_block(cap_file_, fdata);
wtap_block_add_string_option(pkt_block, OPT_COMMENT, ba.data(), ba.size());
cf_set_modified_block(cap_file_, fdata, pkt_block);
}
redrawVisiblePackets();
}
void PacketList::setPacketComment(guint c_number, QString new_comment)
{
int row = currentIndex().row();
frame_data *fdata;
if (!cap_file_ || !packet_list_model_) return;
fdata = packet_list_model_->getRowFdata(row);
if (!fdata) return;
wtap_block_t pkt_block = cf_get_packet_block(cap_file_, fdata);
/* Check if we are clearing the comment */
if (new_comment.isEmpty()) {
wtap_block_remove_nth_option_instance(pkt_block, OPT_COMMENT, c_number);
} else {
QByteArray ba = new_comment.toLocal8Bit();
wtap_block_set_nth_string_option_value(pkt_block, OPT_COMMENT, c_number, ba.data(), ba.size());
}
cf_set_modified_block(cap_file_, fdata, pkt_block);
redrawVisiblePackets();
}
QString PacketList::allPacketComments()
{
guint32 framenum;
frame_data *fdata;
QString buf_str;
if (!cap_file_) return buf_str;
for (framenum = 1; framenum <= cap_file_->count ; framenum++) {
fdata = frame_data_sequence_find(cap_file_->provider.frames, framenum);
wtap_block_t pkt_block = cf_get_packet_block(cap_file_, fdata);
if (pkt_block) {
guint n_comments = wtap_block_count_option(pkt_block, OPT_COMMENT);
for (guint i = 0; i < n_comments; i++) {
char *comment_text;
if (WTAP_OPTTYPE_SUCCESS == wtap_block_get_nth_string_option_value(pkt_block, OPT_COMMENT, i, &comment_text)) {
buf_str.append(QString(tr("Frame %1: %2\n\n")).arg(framenum).arg(comment_text));
if (buf_str.length() > max_comments_to_fetch_) {
buf_str.append(QString(tr("[ Comment text exceeds %1. Stopping. ]"))
.arg(format_size(max_comments_to_fetch_, FORMAT_SIZE_UNIT_BYTES, FORMAT_SIZE_PREFIX_SI)));
return buf_str;
}
}
}
}
}
return buf_str;
}
void PacketList::deleteCommentsFromPackets()
{
frame_data *fdata;
if (!cap_file_ || !packet_list_model_) return;
for (int i = 0; i < selectedRows().size(); i++) {
int row = selectedRows().at(i);
fdata = packet_list_model_->getRowFdata(row);
if (!fdata) continue;
wtap_block_t pkt_block = cf_get_packet_block(cap_file_, fdata);
guint n_comments = wtap_block_count_option(pkt_block, OPT_COMMENT);
for (guint j = 0; j < n_comments; j++) {
wtap_block_remove_nth_option_instance(pkt_block, OPT_COMMENT, 0);
}
cf_set_modified_block(cap_file_, fdata, pkt_block);
}
redrawVisiblePackets();
}
void PacketList::deleteAllPacketComments()
{
guint32 framenum;
frame_data *fdata;
QString buf_str;
guint i;
if (!cap_file_)
return;
for (framenum = 1; framenum <= cap_file_->count ; framenum++) {
fdata = frame_data_sequence_find(cap_file_->provider.frames, framenum);
wtap_block_t pkt_block = cf_get_packet_block(cap_file_, fdata);
guint n_comments = wtap_block_count_option(pkt_block, OPT_COMMENT);
for (i = 0; i < n_comments; i++) {
wtap_block_remove_nth_option_instance(pkt_block, OPT_COMMENT, 0);
}
cf_set_modified_block(cap_file_, fdata, pkt_block);
}
cap_file_->packet_comment_count = 0;
expert_update_comment_count(cap_file_->packet_comment_count);
redrawVisiblePackets();
}
// Slots
void PacketList::setCaptureFile(capture_file *cf)
{
cap_file_ = cf;
packet_list_model_->setCaptureFile(cf);
packet_list_header_->setCaptureFile(cf);
if (cf) {
if (columns_changed_) {
columnsChanged();
} else {
// Restore columns widths and visibility.
header()->restoreState(column_state_);
setColumnVisibility();
}
}
create_near_overlay_ = true;
sortByColumn(-1, Qt::AscendingOrder);
}
void PacketList::setMonospaceFont(const QFont &mono_font)
{
setFont(mono_font);
header()->setFont(wsApp->font());
}
void PacketList::goNextPacket(void)
{
if (QApplication::keyboardModifiers() & Qt::AltModifier) {
// Alt+toolbar
goNextHistoryPacket();
return;
}
if (selectionModel()->hasSelection()) {
selectionModel()->setCurrentIndex(moveCursor(MoveDown, Qt::NoModifier), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
} else {
// First visible packet.
selectionModel()->setCurrentIndex(indexAt(viewport()->rect().topLeft()), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
}
scrollViewChanged(false);
}
void PacketList::goPreviousPacket(void)
{
if (QApplication::keyboardModifiers() & Qt::AltModifier) {
// Alt+toolbar
goPreviousHistoryPacket();
return;
}
if (selectionModel()->hasSelection()) {
selectionModel()->setCurrentIndex(moveCursor(MoveUp, Qt::NoModifier), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
} else {
// Last visible packet.
QModelIndex last_idx = indexAt(viewport()->rect().bottomLeft());
if (last_idx.isValid()) {
selectionModel()->setCurrentIndex(last_idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
} else {
goLastPacket();
}
}
scrollViewChanged(false);
}
void PacketList::goFirstPacket(bool user_selected) {
if (packet_list_model_->rowCount() < 1) return;
selectionModel()->setCurrentIndex(packet_list_model_->index(0, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
scrollTo(currentIndex());
if (user_selected) {
scrollViewChanged(false);
}
}
void PacketList::goLastPacket(void) {
if (packet_list_model_->rowCount() < 1) return;
selectionModel()->setCurrentIndex(packet_list_model_->index(packet_list_model_->rowCount() - 1, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
scrollTo(currentIndex());
scrollViewChanged(false);
}
// XXX We can jump to the wrong packet if a display filter is applied
void PacketList::goToPacket(int packet, int hf_id)
{
if (!cf_goto_frame(cap_file_, packet))
return;
int row = packet_list_model_->packetNumberToRow(packet);
if (row >= 0) {
selectionModel()->setCurrentIndex(packet_list_model_->index(row, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
proto_tree_->goToHfid(hf_id);
}
scrollViewChanged(false);
}
void PacketList::goNextHistoryPacket()
{
if (haveNextHistory(true)) {
in_history_ = true;
goToPacket(selection_history_.at(cur_history_));
in_history_ = false;
}
}
void PacketList::goPreviousHistoryPacket()
{
if (havePreviousHistory(true)) {
in_history_ = true;
goToPacket(selection_history_.at(cur_history_));
in_history_ = false;
}
}
void PacketList::markFrame()
{
if (!cap_file_ || !packet_list_model_) return;
QModelIndexList frames;
if (selectionModel() && selectionModel()->hasSelection())
{
QModelIndexList selRows = selectionModel()->selectedRows(0);
foreach (QModelIndex idx, selRows)
{
if (idx.isValid())
{
frames << idx;
}
}
}
else
frames << currentIndex();
packet_list_model_->toggleFrameMark(frames);
// Make sure the packet list's frame.marked related field text is updated.
redrawVisiblePackets();
create_far_overlay_ = true;
packets_bar_update();
}
void PacketList::markAllDisplayedFrames(bool set)
{
if (!cap_file_ || !packet_list_model_) return;
packet_list_model_->setDisplayedFrameMark(set);
// Make sure the packet list's frame.marked related field text is updated.
redrawVisiblePackets();
create_far_overlay_ = true;
packets_bar_update();
}
void PacketList::ignoreFrame()
{
if (!cap_file_ || !packet_list_model_) return;
QModelIndexList frames;
if (selectionModel() && selectionModel()->hasSelection())
{
foreach (QModelIndex idx, selectionModel()->selectedRows(0))
{
if (idx.isValid())
{
frames << idx;
}
}
}
else
frames << currentIndex();
packet_list_model_->toggleFrameIgnore(frames);
create_far_overlay_ = true;
int sb_val = verticalScrollBar()->value(); // Surely there's a better way to keep our position?
setUpdatesEnabled(false);
emit packetDissectionChanged();
setUpdatesEnabled(true);
verticalScrollBar()->setValue(sb_val);
}
void PacketList::ignoreAllDisplayedFrames(bool set)
{
if (!cap_file_ || !packet_list_model_) return;
packet_list_model_->setDisplayedFrameIgnore(set);
create_far_overlay_ = true;
emit packetDissectionChanged();
}
void PacketList::setTimeReference()
{
if (!cap_file_ || !packet_list_model_) return;
packet_list_model_->toggleFrameRefTime(currentIndex());
create_far_overlay_ = true;
}
void PacketList::unsetAllTimeReferences()
{
if (!cap_file_ || !packet_list_model_) return;
packet_list_model_->unsetAllFrameRefTime();
create_far_overlay_ = true;
}
void PacketList::applyTimeShift()
{
packet_list_model_->resetColumns();
redrawVisiblePackets();
// XXX emit packetDissectionChanged(); ?
}
void PacketList::updatePackets(bool redraw)
{
if (redraw) {
redrawVisiblePackets();
} else {
update();
}
}
void PacketList::columnVisibilityTriggered()
{
QAction *ha = qobject_cast<QAction*>(sender());
if (!ha) return;
int col = ha->data().toInt();
set_column_visible(col, ha->isChecked());
setColumnVisibility();
if (ha->isChecked()) {
setRecentColumnWidth(col);
}
prefs_main_write();
}
void PacketList::sectionResized(int col, int, int new_width)
{
if (isVisible() && !columns_changed_ && !set_column_visibility_ && new_width > 0) {
// Column 1 gets an invalid value (32 on macOS) when we're not yet
// visible.
//
// Don't set column width when columns changed or setting column
// visibility because we may get a sectionReized() from QTreeView
// with values from a old columns layout.
//
// Don't set column width when hiding a column.
recent_set_column_width(col, new_width);
}
}
// The user moved a column. Make sure prefs.col_list, the column format
// array, and the header's visual and logical indices all agree.
// gtk/packet_list.c:column_dnd_changed_cb
void PacketList::sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
{
GList *new_col_list = NULL;
QList<int> saved_sizes;
int sort_idx;
// Since we undo the move below, these should always stay in sync.
// Otherwise the order of columns can be unexpected after drag and drop.
if (logicalIndex != oldVisualIndex) {
ws_warning("Column moved from an unexpected state (%d, %d, %d)",
logicalIndex, oldVisualIndex, newVisualIndex);
}
// Remember which column should be sorted. Use the visual index since this
// points to the current GUI state rather than the outdated column order
// (indicated by the logical index).
sort_idx = header()->sortIndicatorSection();
if (sort_idx != -1) {
sort_idx = header()->visualIndex(sort_idx);
}
// Build a new column list based on the header's logical order.
for (int vis_idx = 0; vis_idx < header()->count(); vis_idx++) {
int log_idx = header()->logicalIndex(vis_idx);
saved_sizes << header()->sectionSize(log_idx);
void *pref_data = g_list_nth_data(prefs.col_list, log_idx);
if (!pref_data) continue;
new_col_list = g_list_append(new_col_list, pref_data);
}
// Undo move to ensure that the logical indices map to the visual indices,
// otherwise the column order is changed twice (once via the modified
// col_list, once because of the visual/logical index mismatch).
disconnect(header(), SIGNAL(sectionMoved(int,int,int)),
this, SLOT(sectionMoved(int,int,int)));
header()->moveSection(newVisualIndex, oldVisualIndex);
connect(header(), SIGNAL(sectionMoved(int,int,int)),
this, SLOT(sectionMoved(int,int,int)));
// Clear and rebuild our (and the header's) model. There doesn't appear
// to be another way to reset the logical index.
freeze();
g_list_free(prefs.col_list);
prefs.col_list = new_col_list;
thaw(true);
for (int i = 0; i < saved_sizes.length(); i++) {
if (saved_sizes[i] < 1) continue;
header()->resizeSection(i, saved_sizes[i]);
}
prefs_main_write();
wsApp->emitAppSignal(WiresharkApplication::ColumnsChanged);
// If the column with the sort indicator got shifted, mark the new column
// after updating the columns contents (via ColumnsChanged) to ensure that
// the columns are sorted using the intended column contents.
int left_col = MIN(oldVisualIndex, newVisualIndex);
int right_col = MAX(oldVisualIndex, newVisualIndex);
if (left_col <= sort_idx && sort_idx <= right_col) {
header()->setSortIndicator(sort_idx, header()->sortIndicatorOrder());
}
}
void PacketList::updateRowHeights(const QModelIndex &ih_index)
{
QStyleOptionViewItem option;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
initViewItemOption(&option);
#else
option = viewOptions();
#endif
int max_height = 0;
// One of our columns increased the maximum row height. Find out which one.
for (int col = 0; col < packet_list_model_->columnCount(); col++) {
QSize size_hint = itemDelegate()->sizeHint(option, packet_list_model_->index(ih_index.row(), col));
max_height = qMax(max_height, size_hint.height());
}
if (max_height > 0) {
packet_list_model_->setMaximumRowHeight(max_height);
}
}
QString PacketList::createSummaryText(QModelIndex idx, SummaryCopyType type)
{
if (! idx.isValid())
return "";
QStringList col_parts;
int row = idx.row();
for (int col = 0; col < packet_list_model_->columnCount(); col++) {
if (get_column_visible(col)) {
col_parts << packet_list_model_->data(packet_list_model_->index(row, col), Qt::DisplayRole).toString();
}
}
return joinSummaryRow(col_parts, row, type);
}
QString PacketList::createHeaderSummaryText(SummaryCopyType type)
{
QStringList col_parts;
for (int col = 0; col < packet_list_model_->columnCount(); ++col)
{
if (get_column_visible(col)) {
col_parts << packet_list_model_->headerData(col, Qt::Orientation::Horizontal, Qt::DisplayRole).toString();
}
}
return joinSummaryRow(col_parts, 0, type);
}
void PacketList::copySummary()
{
if (!currentIndex().isValid()) return;
QAction *ca = qobject_cast<QAction*>(sender());
if (!ca) return;
QVariant type = ca->data();
if (! type.canConvert<SummaryCopyType>())
return;
SummaryCopyType copy_type = type.value<SummaryCopyType>();
QString copy_text = createSummaryText(currentIndex(), copy_type);
wsApp->clipboard()->setText(copy_text);
}
// We need to tell when the user has scrolled the packet list, either to
// the end or anywhere other than the end.
void PacketList::vScrollBarActionTriggered(int)
{
// If we're scrolling with a mouse wheel or trackpad sliderPosition can end up
// past the end.
tail_at_end_ = (overlay_sb_->sliderPosition() >= overlay_sb_->maximum());
scrollViewChanged(tail_at_end_);
}
void PacketList::scrollViewChanged(bool at_end)
{
if (capture_in_progress_ && prefs.capture_auto_scroll) {
emit packetListScrolled(at_end);
}
}
// Goal: Overlay the packet list scroll bar with the colors of all of the
// packets.
// Try 1: Average packet colors in each scroll bar raster line. This has
// two problems: It's easy to wash out colors and we dissect every packet.
// Try 2: Color across a 5000 or 10000 packet window. We still end up washing
// out colors.
// Try 3: One packet per vertical scroll bar pixel. This seems to work best
// but has the smallest window.
// Try 4: Use a multiple of the scroll bar heigh and scale the image down
// using Qt::SmoothTransformation. This gives us more packets per raster
// line.
// Odd (prime?) numbers resulted in fewer scaling artifacts. A multiplier
// of 9 washed out colors a little too much.
//const int height_multiplier_ = 7;
void PacketList::drawNearOverlay()
{
if (create_near_overlay_) {
create_near_overlay_ = false;
}
if (!cap_file_ || cap_file_->state != FILE_READ_DONE) return;
if (!prefs.gui_packet_list_show_minimap) return;
qreal dp_ratio = overlay_sb_->devicePixelRatio();
int o_height = overlay_sb_->height() * dp_ratio;
int o_rows = qMin(packet_list_model_->rowCount(), o_height);
int o_width = (wsApp->fontMetrics().height() * 2 * dp_ratio) + 2; // 2ems + 1-pixel border on either side.
if (recent.packet_list_colorize && o_rows > 0) {
QImage overlay(o_width, o_height, QImage::Format_ARGB32_Premultiplied);
QPainter painter(&overlay);
overlay.fill(Qt::transparent);
int cur_line = 0;
int start = 0;
if (packet_list_model_->rowCount() > o_height && overlay_sb_->maximum() > 0) {
start += ((double) overlay_sb_->value() / overlay_sb_->maximum()) * (packet_list_model_->rowCount() - o_rows);
}
int end = start + o_rows;
for (int row = start; row < end; row++) {
packet_list_model_->ensureRowColorized(row);
frame_data *fdata = packet_list_model_->getRowFdata(row);
const color_t *bgcolor = NULL;
if (fdata->color_filter) {
const color_filter_t *color_filter = (const color_filter_t *) fdata->color_filter;
bgcolor = &color_filter->bg_color;
}
int next_line = (row - start) * o_height / o_rows;
if (bgcolor) {
QColor color(ColorUtils::fromColorT(bgcolor));
painter.fillRect(0, cur_line, o_width, next_line - cur_line, color);
}
cur_line = next_line;
}
// If the selected packet is in the overlay set selected_pos
// accordingly. Otherwise, pin it to either the top or bottom.
QList<int> positions;
if (selectionModel()->hasSelection()) {
QModelIndexList selRows = selectionModel()->selectedRows(0);
int last_row = -1;
int last_pos = -1;
foreach (QModelIndex idx, selRows)
{
int selected_pos = -1;
int sel_row = idx.row();
if (sel_row < start) {
selected_pos = 0;
} else if (sel_row >= end) {
selected_pos = overlay.height() - 1;
} else {
selected_pos = (sel_row - start) * o_height / o_rows;
}
/* Due to the difference in the display height, we sometimes get empty positions
* inbetween consecutive valid rows. If those are detected, they are signaled as
* being selected as well */
if (last_pos >= 0 && selected_pos > (last_pos + 1) && (last_row + 1) == sel_row)
{
for (int pos = (last_pos + 1); pos < selected_pos; pos++)
{
if (! positions.contains(pos))
positions << pos;
}
}
else if (selected_pos != -1 && ! positions.contains(selected_pos))
positions << selected_pos;
last_row = sel_row;
last_pos = selected_pos;
}
}
overlay_sb_->setNearOverlayImage(overlay, packet_list_model_->rowCount(), start, end, positions);
} else {
QImage overlay;
overlay_sb_->setNearOverlayImage(overlay);
}
}
void PacketList::drawFarOverlay()
{
if (create_far_overlay_) {
create_far_overlay_ = false;
}
if (!cap_file_ || cap_file_->state != FILE_READ_DONE) return;
if (!prefs.gui_packet_list_show_minimap) return;
QSize groove_size = overlay_sb_->grooveRect().size();
qreal dp_ratio = overlay_sb_->devicePixelRatio();
groove_size *= dp_ratio;
int o_width = groove_size.width();
int o_height = groove_size.height();
int pl_rows = packet_list_model_->rowCount();
QImage overlay(o_width, o_height, QImage::Format_ARGB32_Premultiplied);
bool have_marked_image = false;
// If only there were references from popular culture about getting into
// some sort of groove.
if (!overlay.isNull() && recent.packet_list_colorize && pl_rows > 0) {
QPainter painter(&overlay);
// Draw text-colored tick marks on a transparent background.
// Hopefully no themes use the text color for the groove color.
overlay.fill(Qt::transparent);
QColor tick_color = palette().text().color();
tick_color.setAlphaF(0.3);
painter.setPen(tick_color);
for (int row = 0; row < pl_rows; row++) {
frame_data *fdata = packet_list_model_->getRowFdata(row);
if (fdata->marked || fdata->ref_time || fdata->ignored) {
int new_line = row * o_height / pl_rows;
int tick_width = o_width / 3;
// Marked or ignored: left side, time refs: right side.
// XXX Draw ignored ticks in the middle?
int x1 = fdata->ref_time ? o_width - tick_width : 1;
int x2 = fdata->ref_time ? o_width - 1 : tick_width;
painter.drawLine(x1, new_line, x2, new_line);
have_marked_image = true;
}
}
if (have_marked_image) {
overlay_sb_->setMarkedPacketImage(overlay);
return;
}
}
if (!have_marked_image) {
QImage null_overlay;
overlay_sb_->setMarkedPacketImage(null_overlay);
}
}
void PacketList::rowsInserted(const QModelIndex &parent, int start, int end)
{
QTreeView::rowsInserted(parent, start, end);
rows_inserted_ = true;
}