wireshark/ui/qt/proto_tree.cpp

609 lines
21 KiB
C++

/* proto_tree.cpp
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
* Copyright 1998 Gerald Combs
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <stdio.h>
#include "proto_tree.h"
#include <ui/qt/models/proto_tree_model.h>
#include <epan/ftypes/ftypes.h>
#include <epan/prefs.h>
#include <ui/qt/utils/variant_pointer.h>
#include <ui/qt/utils/wireshark_mime_data.h>
#include <ui/qt/widgets/drag_label.h>
#include <QApplication>
#include <QContextMenuEvent>
#include <QDesktopServices>
#include <QHeaderView>
#include <QItemSelectionModel>
#include <QScrollBar>
#include <QStack>
#include <QUrl>
#include <QWindow>
// To do:
// - Fix "apply as filter" behavior.
ProtoTree::ProtoTree(QWidget *parent) :
QTreeView(parent),
proto_tree_model_(new ProtoTreeModel(this)),
decode_as_(NULL),
column_resize_timer_(0),
cap_file_(NULL)
{
setAccessibleName(tr("Packet details"));
// Leave the uniformRowHeights property as-is (false) since items might
// have multiple lines (e.g. packet comments). If this slows things down
// too much we should add a custom delegate which handles SizeHintRole
// similar to PacketListModel::data.
setHeaderHidden(true);
// Shrink down to a small but nonzero size in the main splitter.
int one_em = fontMetrics().height();
setMinimumSize(one_em, one_em);
setModel(proto_tree_model_);
connect(this, SIGNAL(expanded(QModelIndex)), this, SLOT(syncExpanded(QModelIndex)));
connect(this, SIGNAL(collapsed(QModelIndex)), this, SLOT(syncCollapsed(QModelIndex)));
connect(this, SIGNAL(doubleClicked(QModelIndex)),
this, SLOT(itemDoubleClicked(QModelIndex)));
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*)));
// resizeColumnToContents checks 1000 items by default. The user might
// have scrolled to an area with a different width at this point.
connect(verticalScrollBar(), SIGNAL(sliderReleased()),
this, SLOT(updateContentWidth()));
viewport()->installEventFilter(this);
}
void ProtoTree::clear() {
proto_tree_model_->setRootNode(NULL);
updateContentWidth();
}
void ProtoTree::closeContextMenu()
{
ctx_menu_.close();
}
void ProtoTree::contextMenuEvent(QContextMenuEvent *event)
{
// We're in a PacketDialog
if (! window()->findChild<QAction *>("actionViewExpandSubtrees"))
return;
ctx_menu_.clear();
QMenu *main_menu_item, *submenu;
QAction *action;
ctx_menu_.addAction(window()->findChild<QAction *>("actionViewExpandSubtrees"));
ctx_menu_.addAction(window()->findChild<QAction *>("actionViewCollapseSubtrees"));
ctx_menu_.addAction(window()->findChild<QAction *>("actionViewExpandAll"));
ctx_menu_.addAction(window()->findChild<QAction *>("actionViewCollapseAll"));
ctx_menu_.addSeparator();
action = window()->findChild<QAction *>("actionAnalyzeCreateAColumn");
ctx_menu_.addAction(action);
ctx_menu_.addSeparator();
main_menu_item = window()->findChild<QMenu *>("menuApplyAsFilter");
submenu = new QMenu(main_menu_item->title(), &ctx_menu_);
ctx_menu_.addMenu(submenu);
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeAAFSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeAAFNotSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeAAFAndSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeAAFOrSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeAAFAndNotSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeAAFOrNotSelected"));
main_menu_item = window()->findChild<QMenu *>("menuPrepareAFilter");
submenu = new QMenu(main_menu_item->title(), &ctx_menu_);
ctx_menu_.addMenu(submenu);
submenu->addAction(window()->findChild<QAction *>("actionAnalyzePAFSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzePAFNotSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzePAFAndSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzePAFOrSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzePAFAndNotSelected"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzePAFOrNotSelected"));
QMenu *main_conv_menu = window()->findChild<QMenu *>("menuConversationFilter");
conv_menu_.setTitle(main_conv_menu->title());
conv_menu_.clear();
foreach (QAction *action, main_conv_menu->actions()) {
conv_menu_.addAction(action);
}
ctx_menu_.addMenu(&conv_menu_);
colorize_menu_.setTitle(tr("Colorize with Filter"));
ctx_menu_.addMenu(&colorize_menu_);
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 *>("actionAnalyzeFollowSSLStream"));
submenu->addAction(window()->findChild<QAction *>("actionAnalyzeFollowHTTPStream"));
ctx_menu_.addSeparator();
main_menu_item = window()->findChild<QMenu *>("menuEditCopy");
submenu = new QMenu(main_menu_item->title(), &ctx_menu_);
ctx_menu_.addMenu(submenu);
submenu->addAction(window()->findChild<QAction *>("actionCopyAllVisibleItems"));
submenu->addAction(window()->findChild<QAction *>("actionCopyAllVisibleSelectedTreeItems"));
submenu->addAction(window()->findChild<QAction *>("actionEditCopyDescription"));
submenu->addAction(window()->findChild<QAction *>("actionEditCopyFieldName"));
submenu->addAction(window()->findChild<QAction *>("actionEditCopyValue"));
submenu->addSeparator();
submenu->addAction(window()->findChild<QAction *>("actionEditCopyAsFilter"));
submenu->addSeparator();
QModelIndex index = indexAt(event->pos());
FieldInformation finfo(proto_tree_model_->protoNodeFromIndex(index).protoNode());
QActionGroup * copyEntries = DataPrinter::copyActions(this, &finfo);
submenu->addActions(copyEntries->actions());
action = window()->findChild<QAction *>("actionAnalyzeShowPacketBytes");
ctx_menu_.addAction(action);
action = window()->findChild<QAction *>("actionFileExportPacketBytes");
ctx_menu_.addAction(action);
ctx_menu_.addSeparator();
action = window()->findChild<QAction *>("actionContextWikiProtocolPage");
ctx_menu_.addAction(action);
action = window()->findChild<QAction *>("actionContextFilterFieldReference");
ctx_menu_.addAction(action);
ctx_menu_.addMenu(&proto_prefs_menu_);
ctx_menu_.addSeparator();
decode_as_ = window()->findChild<QAction *>("actionAnalyzeDecodeAs");
ctx_menu_.addAction(decode_as_);
ctx_menu_.addAction(window()->findChild<QAction *>("actionGoGoToLinkedPacket"));
ctx_menu_.addAction(window()->findChild<QAction *>("actionContextShowLinkedPacketInNewWindow"));
proto_prefs_menu_.setModule(finfo.moduleName());
decode_as_->setData(QVariant::fromValue(true));
ctx_menu_.exec(event->globalPos());
decode_as_->setData(QVariant());
}
void ProtoTree::timerEvent(QTimerEvent *event)
{
if (event->timerId() == column_resize_timer_) {
killTimer(column_resize_timer_);
column_resize_timer_ = 0;
resizeColumnToContents(0);
} else {
QTreeView::timerEvent(event);
}
}
// resizeColumnToContents checks 1000 items by default. The user might
// have scrolled to an area with a different width at this point.
void ProtoTree::keyReleaseEvent(QKeyEvent *event)
{
if (event->isAutoRepeat()) return;
switch(event->key()) {
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
case Qt::Key_Home:
case Qt::Key_End:
updateContentWidth();
break;
default:
break;
}
}
void ProtoTree::updateContentWidth()
{
if (column_resize_timer_ == 0) {
column_resize_timer_ = startTimer(0);
}
}
void ProtoTree::setMonospaceFont(const QFont &mono_font)
{
mono_font_ = mono_font;
setFont(mono_font_);
update();
}
void ProtoTree::foreachTreeNode(proto_node *node, gpointer proto_tree_ptr)
{
ProtoTree *tree_view = static_cast<ProtoTree *>(proto_tree_ptr);
ProtoTreeModel *model = qobject_cast<ProtoTreeModel *>(tree_view->model());
if (!tree_view || !model) {
return;
}
// Expanded state
if (tree_expanded(node->finfo->tree_type)) {
ProtoNode expand_node = ProtoNode(node);
tree_view->expand(model->indexFromProtoNode(expand_node));
}
// Related frames
if (node->finfo->hfinfo->type == FT_FRAMENUM) {
ft_framenum_type_t framenum_type = (ft_framenum_type_t)GPOINTER_TO_INT(node->finfo->hfinfo->strings);
tree_view->emitRelatedFrame(node->finfo->value.value.uinteger, framenum_type);
}
proto_tree_children_foreach(node, foreachTreeNode, proto_tree_ptr);
}
// We track item expansion using proto.c:tree_is_expanded. QTreeView
// tracks it using QTreeViewPrivate::expandedIndexes. When we're handed
// a new tree, clear expandedIndexes and repopulate it by walking the
// tree and calling QTreeView::expand above.
void ProtoTree::setRootNode(proto_node *root_node) {
setFont(mono_font_);
reset(); // clears expandedIndexes.
proto_tree_model_->setRootNode(root_node);
disconnect(this, SIGNAL(expanded(QModelIndex)), this, SLOT(syncExpanded(QModelIndex)));
proto_tree_children_foreach(root_node, foreachTreeNode, this);
connect(this, SIGNAL(expanded(QModelIndex)), this, SLOT(syncExpanded(QModelIndex)));
updateContentWidth();
}
void ProtoTree::emitRelatedFrame(int related_frame, ft_framenum_type_t framenum_type)
{
emit relatedFrame(related_frame, framenum_type);
}
void ProtoTree::autoScrollTo(const QModelIndex &index)
{
selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
if (!index.isValid()) {
return;
}
// ensure item is visible (expanding its parents as needed).
scrollTo(index);
}
// XXX We select the first match, which might not be the desired item.
void ProtoTree::goToHfid(int hfid)
{
QModelIndex index = proto_tree_model_->findFirstHfid(hfid);
autoScrollTo(index);
}
void ProtoTree::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
QTreeView::selectionChanged(selected, deselected);
if (selected.isEmpty()) {
emit fieldSelected(0);
return;
}
QModelIndex index = selected.indexes().first();
saveSelectedField(index);
// Find and highlight the protocol bytes. select above won't call
// selectionChanged if the current and selected indexes are the same
// so we do this here.
FieldInformation finfo(proto_tree_model_->protoNodeFromIndex(index).protoNode(), this);
if (finfo.isValid()) {
QModelIndex parent = index;
while (parent.isValid() && parent.parent().isValid()) {
parent = parent.parent();
}
if (parent.isValid()) {
FieldInformation parent_finfo(proto_tree_model_->protoNodeFromIndex(parent).protoNode());
finfo.setParentField(parent_finfo.fieldInfo());
}
emit fieldSelected(&finfo);
}
}
void ProtoTree::syncExpanded(const QModelIndex &index) {
FieldInformation finfo(proto_tree_model_->protoNodeFromIndex(index).protoNode());
if (!finfo.isValid()) return;
/*
* Nodes with "finfo->tree_type" of -1 have no ett_ value, and
* are thus presumably leaf nodes and cannot be expanded.
*/
if (finfo.treeType() != -1) {
tree_expanded_set(finfo.treeType(), TRUE);
}
}
void ProtoTree::syncCollapsed(const QModelIndex &index) {
FieldInformation finfo(proto_tree_model_->protoNodeFromIndex(index).protoNode());
if (!finfo.isValid()) return;
/*
* Nodes with "finfo->tree_type" of -1 have no ett_ value, and
* are thus presumably leaf nodes and cannot be collapsed.
*/
if (finfo.treeType() != -1) {
tree_expanded_set(finfo.treeType(), FALSE);
}
}
void ProtoTree::expandSubtrees()
{
if (!selectionModel()->hasSelection()) return;
QStack<QModelIndex> index_stack;
index_stack.push(selectionModel()->selectedIndexes().first());
while (!index_stack.isEmpty()) {
QModelIndex index = index_stack.pop();
expand(index);
int row_count = proto_tree_model_->rowCount(index);
for (int row = row_count - 1; row >= 0; row--) {
QModelIndex child = proto_tree_model_->index(row, 0, index);
if (proto_tree_model_->hasChildren(child)) {
index_stack.push(child);
}
}
}
updateContentWidth();
}
void ProtoTree::collapseSubtrees()
{
if (!selectionModel()->hasSelection()) return;
QStack<QModelIndex> index_stack;
index_stack.push(selectionModel()->selectedIndexes().first());
while (!index_stack.isEmpty()) {
QModelIndex index = index_stack.pop();
collapse(index);
int row_count = proto_tree_model_->rowCount(index);
for (int row = row_count - 1; row >= 0; row--) {
QModelIndex child = proto_tree_model_->index(row, 0, index);
if (proto_tree_model_->hasChildren(child)) {
index_stack.push(child);
}
}
}
updateContentWidth();
}
void ProtoTree::expandAll()
{
for(int i = 0; i < num_tree_types; i++) {
tree_expanded_set(i, TRUE);
}
QTreeView::expandAll();
updateContentWidth();
}
void ProtoTree::collapseAll()
{
for(int i = 0; i < num_tree_types; i++) {
tree_expanded_set(i, FALSE);
}
QTreeView::collapseAll();
updateContentWidth();
}
void ProtoTree::itemDoubleClicked(const QModelIndex &index) {
FieldInformation finfo(proto_tree_model_->protoNodeFromIndex(index).protoNode());
if (!finfo.isValid()) return;
if (finfo.headerInfo().type == FT_FRAMENUM) {
if (QApplication::queryKeyboardModifiers() & Qt::ShiftModifier) {
emit openPacketInNewWindow(true);
} else {
emit goToPacket(finfo.fieldInfo()->value.value.uinteger);
}
} else {
QString url = finfo.url();
if (!url.isEmpty()) {
QDesktopServices::openUrl(QUrl(url));
}
}
}
// Select a field and bring it into view. Intended to be called by external
// components (such as the byte view).
void ProtoTree::selectedFieldChanged(FieldInformation *finfo)
{
if (finfo && finfo->parent() == this) {
// We only want inbound signals.
return;
}
QModelIndex index = proto_tree_model_->findFieldInformation(finfo);
setUpdatesEnabled(false);
// The new finfo might match the current index. Clear our selection
// so that we force a fresh item selection, so that fieldSelected
// will in turn be emitted.
selectionModel()->clearSelection();
autoScrollTo(index);
setUpdatesEnabled(true);
}
// Remember the currently focussed field based on:
// - current hf_id (obviously)
// - parent items (to avoid selecting a text item in a different tree)
// - the row of each item
void ProtoTree::saveSelectedField(QModelIndex &index)
{
selected_hfid_path_.clear();
QModelIndex save_index = index;
while (save_index.isValid()) {
FieldInformation finfo(proto_tree_model_->protoNodeFromIndex(save_index).protoNode());
if (!finfo.isValid()) break;
selected_hfid_path_.prepend(QPair<int,int>(save_index.row(), finfo.headerInfo().id));
save_index = save_index.parent();
}
}
// Try to focus a tree item which was previously also visible
void ProtoTree::restoreSelectedField()
{
if (selected_hfid_path_.isEmpty()) return;
QModelIndex cur_index = QModelIndex();
QPair<int,int> path_entry;
foreach (path_entry, selected_hfid_path_) {
int row = path_entry.first;
int hf_id = path_entry.second;
cur_index = proto_tree_model_->index(row, 0, cur_index);
FieldInformation finfo(proto_tree_model_->protoNodeFromIndex(cur_index).protoNode());
if (!finfo.isValid() || finfo.headerInfo().id != hf_id) {
cur_index = QModelIndex();
break;
}
}
autoScrollTo(cur_index);
}
const QString ProtoTree::toString(const QModelIndex &start_idx) const
{
QModelIndex cur_idx = start_idx.isValid() ? start_idx : proto_tree_model_->index(0, 0);
QModelIndex stop_idx = proto_tree_model_->index(cur_idx.row() + 1, 0, cur_idx.parent());
QString tree_string;
int indent_level = 0;
do {
tree_string.append(QString(" ").repeated(indent_level));
tree_string.append(cur_idx.data().toString());
tree_string.append("\n");
// Next child
if (isExpanded(cur_idx)) {
cur_idx = proto_tree_model_->index(0, 0, cur_idx);
indent_level++;
continue;
}
// Next sibling
QModelIndex sibling = proto_tree_model_->index(cur_idx.row() + 1, 0, cur_idx.parent());
if (sibling.isValid()) {
cur_idx = sibling;
continue;
}
// Next parent
cur_idx = proto_tree_model_->index(cur_idx.parent().row() + 1, 0, cur_idx.parent().parent());
indent_level--;
} while (cur_idx.isValid() && cur_idx.internalPointer() != stop_idx.internalPointer() && indent_level >= 0);
return tree_string;
}
void ProtoTree::setCaptureFile(capture_file *cf)
{
cap_file_ = cf;
}
bool ProtoTree::eventFilter(QObject * obj, QEvent * event)
{
if ( cap_file_ && event->type() != QEvent::MouseButtonPress && event->type() != QEvent::MouseMove )
return QTreeView::eventFilter(obj, event);
/* Mouse was over scrollbar, ignoring */
if ( qobject_cast<QScrollBar *>(obj) )
return QTreeView::eventFilter(obj, event);
if ( event->type() == QEvent::MouseButtonPress )
{
QMouseEvent * ev = (QMouseEvent *)event;
if ( ev->buttons() & Qt::LeftButton )
drag_start_position_ = ev->pos();
}
else if ( event->type() == QEvent::MouseMove )
{
QMouseEvent * ev = (QMouseEvent *)event;
if ( ( ev->buttons() & Qt::LeftButton ) && (ev->pos() - drag_start_position_).manhattanLength()
> QApplication::startDragDistance())
{
QModelIndex idx = indexAt(drag_start_position_);
FieldInformation finfo(proto_tree_model_->protoNodeFromIndex(idx).protoNode());
if ( finfo.isValid() )
{
/* Hack to prevent QItemSelection taking the item which has been dragged over at start
* of drag-drop operation. selectionModel()->blockSignals could have done the trick, but
* it does not take in a QTreeWidget (maybe View) */
emit fieldSelected(&finfo);
selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect);
QString filter = QString(proto_construct_match_selected_string(finfo.fieldInfo(), cap_file_->edt));
if ( filter.length() > 0 )
{
DisplayFilterMimeData * dfmd =
new DisplayFilterMimeData(QString(finfo.headerInfo().name), QString(finfo.headerInfo().abbreviation), filter);
QDrag * drag = new QDrag(this);
drag->setMimeData(dfmd);
DragLabel * content = new DragLabel(dfmd->labelText(), this);
qreal dpr = window()->windowHandle()->devicePixelRatio();
QPixmap pixmap(content->size() * dpr);
pixmap.setDevicePixelRatio(dpr);
content->render(&pixmap);
drag->setPixmap(pixmap);
drag->exec(Qt::CopyAction);
return true;
}
}
}
}
return QTreeView::eventFilter(obj, event);
}
QModelIndex ProtoTree::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
if (cursorAction == MoveLeft && selectionModel()->hasSelection()) {
QModelIndex cur_idx = selectionModel()->selectedIndexes().first();
QModelIndex parent = cur_idx.parent();
if (!isExpanded(cur_idx) && parent.isValid() && parent != rootIndex()) {
return parent;
}
}
return QTreeView::moveCursor(cursorAction, modifiers);
}
/*
* Editor modelines
*
* Local Variables:
* c-basic-offset: 4
* tab-width: 8
* indent-tabs-mode: nil
* End:
*
* ex: set shiftwidth=4 tabstop=8 expandtab:
* :indentSize=4:tabSize=8:noTabs=true:
*/