From e4ba6dbc3f1e76890b22773807ea37fe8fa2b1bc Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 22:34:10 +0200 Subject: Adding upstream version 4.2.2. Signed-off-by: Daniel Baumann --- ui/qt/packet_list.cpp | 2164 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2164 insertions(+) create mode 100644 ui/qt/packet_list.cpp (limited to 'ui/qt/packet_list.cpp') diff --git a/ui/qt/packet_list.cpp b/ui/qt/packet_list.cpp new file mode 100644 index 0000000..e74f151 --- /dev/null +++ b/ui/qt/packet_list.cpp @@ -0,0 +1,2164 @@ +/* packet_list.cpp + * + * Wireshark - Network traffic analyzer + * By Gerald Combs + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include + +#include "config.h" + +#include + +#include "file.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#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 "ui/simple_dialog.h" +#include +#include "ui/util.h" + +#include "wiretap/wtap_opttypes.h" +#include "wsutil/str_util.h" +#include + +#include +#include "frame_tvbuff.h" + +#include +#include +#include "proto_tree.h" +#include +#include "main_application.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include "wsutil/file_util.h" +#include +#include +#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 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 overlay_update_interval_ = 100; // 250; // Milliseconds. + + +/* + * 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(gbl_cur_packet_list->model()); + + if (! model) + return FALSE; + + model->flushVisibleRows(); + int row = -1; + if (!fdata_needle) + row = 0; + else + 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); + gbl_cur_packet_list->scrollTo(gbl_cur_packet_list->currentIndex(), PacketList::PositionAtCenter); + return TRUE; + } + + return FALSE; +} + +void +packet_list_clear(void) +{ + if (gbl_cur_packet_list) { + gbl_cur_packet_list->clear(); + } +} + +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(); +} + +/* 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_at_end_(0), + columns_changed_(false), + set_column_visibility_(false), + frozen_current_row_(QModelIndex()), + frozen_selected_rows_(QModelIndexList()), + cur_history_(-1), + in_history_(false), + finfo_array(NULL) +{ + setItemsExpandable(false); + setRootIsDecorated(false); + setSortingEnabled(prefs.gui_packet_list_sortable); + setUniformRowHeights(true); + setAccessibleName("Packet list"); + + proto_prefs_menus_.setTitle(tr("Protocol Preferences")); + + packet_list_header_ = new PacketListHeader(header()->orientation()); + 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(mainApp, SIGNAL(addressResolutionChanged()), this, SLOT(redrawVisiblePacketsDontSelectCurrent())); + connect(mainApp, SIGNAL(columnDataChanged()), this, SLOT(redrawVisiblePacketsDontSelectCurrent())); + connect(mainApp, &MainApplication::preferencesChanged, this, [=]() { + /* The pref is a uint but QCache maxCost is a signed int (/ + * qsizetype in Qt 6). Note that QAbstractItemModel row numbers + * are ints, not unsigned ints, so we're limited to INT_MAX + * rows anyway. + */ + PacketListRecord::setMaxCache(prefs.gui_packet_list_cached_rows_max > INT_MAX ? INT_MAX : prefs.gui_packet_list_cached_rows_max); + if ((bool) (prefs.gui_packet_list_sortable) != isSortingEnabled()) { + setSortingEnabled(prefs.gui_packet_list_sortable); + } + }); + + 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))); +} + +PacketList::~PacketList() +{ + if (finfo_array) + { + g_ptr_array_free(finfo_array, TRUE); + } +} + +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_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_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::uniqueSelectActive() +{ + return selectionModel()->selectedRows(0).count() == 1 ? true : false; +} + +bool PacketList::multiSelectActive() +{ + return selectionModel()->selectedRows(0).count() > 1 ? true : false; +} + +QList PacketList::selectedRows(bool useFrameNum) +{ + QList 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 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 || !packet_list_model_) + cf_unselect_packet(cap_file_); + else { + frame_data * fdata = packet_list_model_->getRowFdata(row); + cf_select_packet(cap_file_, fdata); + } + + 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() << 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) { + 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. + fi = cf_find_string_protocol_tree(cap_file_, cap_file_->edt->tree); + } 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 (finfo_array) + { + g_ptr_array_free(finfo_array, TRUE); + finfo_array = NULL; + } + if (cap_file_ && cap_file_->edt && cap_file_->edt->tree) { + finfo_array = proto_all_finfos(cap_file_->edt->tree); + QList 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; + } + } + } + + 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); + ctx_menu->setAttribute(Qt::WA_DeleteOnClose); + // XXX We might want to reimplement setParent() and fill in the context + // menu there. + ctx_menu->addAction(window()->findChild("actionEditMarkPacket")); + ctx_menu->addAction(window()->findChild("actionEditIgnorePacket")); + ctx_menu->addAction(window()->findChild("actionEditSetTimeReference")); + ctx_menu->addAction(window()->findChild("actionEditTimeShift")); + ctx_menu->addMenu(window()->findChild("menuPacketComment")); + + ctx_menu->addSeparator(); + + // Code for custom context menus from Lua's register_packet_menu() + MainWindow * mainWindow = qobject_cast(mainApp->mainWindow()); + // N.B., Only want to call for a single frame selection. + // Testing finfo_array as a proxy, because mainWindow->hasUniqueSelection() doesn't detect multiple selections... + if (cap_file_ && cap_file_->edt && cap_file_->edt->tree && mainWindow && finfo_array) { + bool insertedPacketMenu = mainWindow->addPacketMenus(ctx_menu, finfo_array); + if (insertedPacketMenu) { + ctx_menu->addSeparator(); + } + } + + ctx_menu->addAction(window()->findChild("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(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(colorize_menu_name); + colorize_menu_.setTitle(main_menu_item->title()); + colorize_menu_.setObjectName(colorize_menu_name); + ctx_menu->addMenu(&colorize_menu_); + + QMenu * submenu; + main_menu_item = window()->findChild("menuSCTP"); + if (main_menu_item) { + submenu = new QMenu(main_menu_item->title(), ctx_menu); + ctx_menu->addMenu(submenu); + submenu->addAction(window()->findChild("actionSCTPAnalyseThisAssociation")); + submenu->addAction(window()->findChild("actionSCTPShowAllAssociations")); + submenu->addAction(window()->findChild("actionSCTPFilterThisAssociation")); + } + + main_menu_item = window()->findChild("menuFollow"); + if (main_menu_item) { + submenu = new QMenu(main_menu_item->title(), ctx_menu); + ctx_menu->addMenu(submenu); + foreach (FollowStreamAction *follow_action, main_menu_item->findChildren()) { + /* XXX: We could, like the prefs above, walk the protocols/layers + * and add the follow actions in the order they appear in the packet. + */ + if (follow_action->isEnabled()) { + submenu->addAction(follow_action); + } + } + } + + ctx_menu->addSeparator(); + + main_menu_item = window()->findChild("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("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("actionViewShowPacketInNewWindow"); + ctx_menu->addAction(action); + + + // Set menu sensitivity for the current column and set action data. + if (frameData) + emit framesSelected(QList() << frameData->frameNum()); + else + emit framesSelected(QList()); + + ctx_menu->popup(event->globalPos()); +} + +void PacketList::ctxDecodeAsDialog() +{ + QAction *da_action = qobject_cast(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*)), mainApp, SLOT(flushAppSignals())); + da_dialog->setWindowModality(Qt::ApplicationModal); + da_dialog->setAttribute(Qt::WA_DeleteOnClose); + da_dialog->show(); +} + +void PacketList::timerEvent(QTimerEvent *event) +{ + 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 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) +{ + bool handled = false; + // If scrolling up/down, want to preserve horizontal scroll extent. + if (event->key() == Qt::Key_Down || event->key() == Qt::Key_Up || + event->key() == Qt::Key_PageDown || event->key() == Qt::Key_PageUp || + event->key() == Qt::Key_End || event->key() == Qt::Key_Home ) + { + // XXX: Why allow jumping to the left if the first column is current? + if (currentIndex().isValid() && currentIndex().column() > 0) { + int pos = horizontalScrollBar()->value(); + QTreeView::keyPressEvent(event); + horizontalScrollBar()->setValue(pos); + handled = true; + } + } + + if (!handled) + QTreeView::keyPressEvent(event); + + if (event->matches(QKeySequence::Copy)) + { + QStringList content; + if (model() && selectionModel() && selectionModel()->hasSelection()) + { + QList 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) + mainApp->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(mainApp->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()->clearSelection(); + 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; + column_register_fields(); + mainApp->emitAppSignal(MainApplication::FieldsChanged); + 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. (XXX: We +// still save the stretched value to recent, sectionResized doesn't +// distinguish between a resize from being stretched and a manual change.) +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. + + 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_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::freezePacketList(bool changing_profile) +{ + changing_profile_ = changing_profile; + freeze(true); +} + +void PacketList::recolorPackets() +{ + packet_list_model_->resetColorized(); + redrawVisiblePackets(); +} + +// Enable autoscroll. +void PacketList::setVerticalAutoScroll(bool enabled) +{ + tail_at_end_ = enabled; + if (enabled && capture_in_progress_) { + scrollToBottom(); + } +} + +// 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(); + // Sort *after* invalidating the column strings + if (isSortingEnabled()) { + sortByColumn(header()->sortIndicatorSection(), header()->sortIndicatorOrder()); + } +} + +bool PacketList::freeze(bool keep_current_frame) +{ + if (!cap_file_ || model() == Q_NULLPTR) { + // No capture file or already frozen + return false; + } + + frame_data *current_frame = cap_file_->current_frame; + column_state_ = header()->saveState(); + setHeaderHidden(true); + frozen_current_row_ = currentIndex(); + frozen_selected_rows_ = selectionModel()->selectedRows(); + 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(); + + if (keep_current_frame) { + cap_file_->current_frame = current_frame; + } + + /* Clears packet list as well as byteview */ + emit framesSelected(QList()); + + return true; +} + +bool PacketList::thaw(bool restore_selection) +{ + if (!cap_file_ || model() != Q_NULLPTR) { + // No capture file or not frozen + return false; + } + + setHeaderHidden(false); + // Note that if we have a current sort status set in the header, + // this will automatically try to sort the model (we don't want + // that to happen if we're in the middle of reading the file). + setModel(packet_list_model_); + + if (changing_profile_) { + // When changing profile the new recent settings must be applied to the columns. + applyRecentColumnWidths(); + setColumnVisibility(); + changing_profile_ = false; + } else { + // 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_selected_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(); + setCurrentIndex(frozen_current_row_); + foreach (QModelIndex idx, frozen_selected_rows_) { + selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); + } + scrollTo(currentIndex(), PositionAtCenter); + } + frozen_current_row_ = QModelIndex(); + frozen_selected_rows_ = QModelIndexList(); + + return true; +} + +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); + + if (cap_file_->cinfo.columns[column].col_fmt == COL_CUSTOM) { + filter.append(gchar_free_to_qstring(col_custom_get_filter(&edt, &cap_file_->cinfo, column))); + } else { + /* We don't need to fill in the custom columns, as we get their + * filters above. + */ + col_fill_in(&edt.pi, TRUE, TRUE); + 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; + 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) +{ + if (!cap_file_ || !packet_list_model_) return; + if (new_comment.isEmpty()) return; + + QByteArray ba = new_comment.toUtf8(); + + /* + * Make sure this would fit in a pcapng option. + * + * XXX - 65535 is the maximum size for an option in pcapng; + * what if another capture file format supports larger + * comments? + */ + if (ba.size() > 65535) { + simple_dialog(ESD_TYPE_ERROR, ESD_BTN_OK, + "That comment is too large to save in a capture file."); + return; + } + + if (selectionModel() && selectionModel()->hasSelection()) { + packet_list_model_->addFrameComment(selectionModel()->selectedRows(), ba); + drawCurrentPacket(); + } +} + +void PacketList::setPacketComment(guint c_number, QString new_comment) +{ + QModelIndex curIndex = currentIndex(); + + if (!cap_file_ || !packet_list_model_) return; + + QByteArray ba = new_comment.toUtf8(); + /* + * Make sure this would fit in a pcapng option. + * + * XXX - 65535 is the maximum size for an option in pcapng; + * what if another capture file format supports larger + * comments? + */ + if (ba.size() > 65535) { + simple_dialog(ESD_TYPE_ERROR, ESD_BTN_OK, + "That comment is too large to save in a capture file."); + return; + } + + packet_list_model_->setFrameComment(curIndex, ba, c_number); + drawCurrentPacket(); +} + +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() +{ + if (!cap_file_ || !packet_list_model_) return; + + if (selectionModel() && selectionModel()->hasSelection()) { + packet_list_model_->deleteFrameComments(selectionModel()->selectedRows()); + drawCurrentPacket(); + } +} + +void PacketList::deleteAllPacketComments() +{ + if (!cap_file_ || !packet_list_model_) return; + + packet_list_model_->deleteAllFrameComments(); + drawCurrentPacket(); +} + + +// Slots + +void PacketList::setCaptureFile(capture_file *cf) +{ + cap_file_ = cf; + packet_list_model_->setCaptureFile(cf); + if (cf) { + if (columns_changed_) { + columnsChanged(); + } else { + // Restore columns widths and visibility. + header()->restoreState(column_state_); + setColumnVisibility(); + } + } + create_near_overlay_ = true; + changing_profile_ = false; + sortByColumn(-1, Qt::AscendingOrder); +} + +void PacketList::setMonospaceFont(const QFont &mono_font) +{ + setFont(mono_font); + header()->setFont(mainApp->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(void) { + if (packet_list_model_->rowCount() < 1) return; + selectionModel()->setCurrentIndex(packet_list_model_->index(0, 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + scrollTo(currentIndex()); + + 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); + scrollTo(currentIndex(), PositionAtCenter); + 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(); + emit packetDissectionChanged(); +} + +void PacketList::updatePackets(bool redraw) +{ + if (redraw) { + packet_list_model_->resetColumns(); + redrawVisiblePackets(); + } else { + update(); + } +} + +void PacketList::columnVisibilityTriggered() +{ + QAction *ha = qobject_cast(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 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(); + + mainApp->emitAppSignal(MainApplication::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(sender()); + if (!ca) return; + + QVariant type = ca->data(); + if (! type.canConvert()) + return; + SummaryCopyType copy_type = type.value(); + + QString copy_text = createSummaryText(currentIndex(), copy_type); + + mainApp->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_) { + // We want to start auto scrolling when the user scrolls to (or past) + // the end only if recent.capture_auto_scroll is set. + // We want to stop autoscrolling if the user scrolls up or uses + // Go to Packet regardless of the preference setting. + if (recent.capture_auto_scroll || !at_end) { + 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); + QFontMetricsF fmf(mainApp->font()); + int o_width = ((static_cast(fmf.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 + 1) * 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 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, (o_height / o_rows)); + } 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.3f); + 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); + } +} + +// Auto scroll if: +// - We are capturing +// - actionGoAutoScroll in the main UI is checked. + +// actionGoAutoScroll in the main UI: +// - Is set to the value of recent.capture_auto_scroll when beginning a capture +// - Can be triggered manually by the user +// - Is turned on if the last user-set vertical scrollbar position is at the +// end and recent.capture_auto_scroll is enabled +// - Is turned off if the last user-set vertical scrollbar is not at the end, +// or if one of the Go to Packet actions is used (XXX: Should keyboard +// navigation in keyPressEvent turn it off for similar reasons?) +void PacketList::rowsInserted(const QModelIndex &parent, int start, int end) +{ + QTreeView::rowsInserted(parent, start, end); + if (capture_in_progress_ && tail_at_end_) { + scrollToBottom(); + } +} + +void PacketList::resizeAllColumns(bool onlyTimeFormatted) +{ + if (!cap_file_ || cap_file_->state == FILE_CLOSED || cap_file_->state == FILE_READ_PENDING) + return; + + for (int col = 0; col < cap_file_->cinfo.num_cols; col++) { + if (! onlyTimeFormatted || col_has_time_fmt(&cap_file_->cinfo, col)) { + resizeColumnToContents(col); + } + } +} -- cgit v1.2.3