diff options
Diffstat (limited to 'ui/qt/widgets')
47 files changed, 1513 insertions, 271 deletions
diff --git a/ui/qt/widgets/additional_toolbar.cpp b/ui/qt/widgets/additional_toolbar.cpp index c023d149..43eab273 100644 --- a/ui/qt/widgets/additional_toolbar.cpp +++ b/ui/qt/widgets/additional_toolbar.cpp @@ -9,8 +9,6 @@ #include <config.h> -#include <glib.h> - #include <ui/qt/widgets/additional_toolbar.h> #include <ui/qt/widgets/apply_line_edit.h> #include <ui/qt/utils/qt_ui_utils.h> @@ -159,7 +157,7 @@ QWidget * AdditionalToolbarWidgetAction::createWidget(QWidget * parent) } static void -toolbar_button_cb(gpointer item, gpointer item_data, gpointer user_data) +toolbar_button_cb(void *item, void *item_data, void *user_data) { if (! item || ! item_data || ! user_data) return; @@ -170,7 +168,7 @@ toolbar_button_cb(gpointer item, gpointer item_data, gpointer user_data) if (widget) { if (update_entry->type == EXT_TOOLBAR_UPDATE_VALUE) - widget->setText((gchar *)update_entry->user_data); + widget->setText((char *)update_entry->user_data); else if (update_entry->type == EXT_TOOLBAR_SET_ACTIVE) { bool enableState = GPOINTER_TO_INT(update_entry->user_data) == 1; @@ -195,7 +193,7 @@ QWidget * AdditionalToolbarWidgetAction::createButton(ext_toolbar_t * item, QWid } static void -toolbar_boolean_cb(gpointer item, gpointer item_data, gpointer user_data) +toolbar_boolean_cb(void *item, void *item_data, void *user_data) { if (! item || ! item_data || ! user_data) return; @@ -267,7 +265,7 @@ QWidget * AdditionalToolbarWidgetAction::createLabelFrame(ext_toolbar_t * item, } static void -toolbar_string_cb(gpointer item, gpointer item_data, gpointer user_data) +toolbar_string_cb(void *item, void *item_data, void *user_data) { if (! item || ! item_data || ! user_data) return; @@ -282,7 +280,7 @@ toolbar_string_cb(gpointer item, gpointer item_data, gpointer user_data) if (update_entry->silent) oldState = edit->blockSignals(true); - edit->setText((gchar *)update_entry->user_data); + edit->setText((char *)update_entry->user_data); if (update_entry->silent) edit->blockSignals(oldState); @@ -321,7 +319,7 @@ QWidget * AdditionalToolbarWidgetAction::createTextEditor(ext_toolbar_t * item, } static void -toolbar_selector_cb(gpointer item, gpointer item_data, gpointer user_data) +toolbar_selector_cb(void *item, void *item_data, void *user_data) { if (! item || ! item_data || ! user_data) return; @@ -346,7 +344,7 @@ toolbar_selector_cb(gpointer item, gpointer item_data, gpointer user_data) if (update_entry->type == EXT_TOOLBAR_UPDATE_VALUE) { - QString data = QString((gchar *)update_entry->user_data); + QString data = QString((char *)update_entry->user_data); for (int i = 0; i < sourceModel->rowCount(); i++) { @@ -385,8 +383,8 @@ toolbar_selector_cb(gpointer item, gpointer item_data, gpointer user_data) if (! update_entry->data_index) return; - gchar * idx = (gchar *)update_entry->data_index; - gchar * display = (gchar *)update_entry->user_data; + char * idx = (char *)update_entry->data_index; + char * display = (char *)update_entry->user_data; if (update_entry->type == EXT_TOOLBAR_UPDATE_DATABYINDEX) { @@ -522,7 +520,7 @@ void AdditionalToolbarWidgetAction::onCheckBoxChecked(int checkState) if (! item) return; - gboolean value = checkState == Qt::Checked ? true : false; + bool value = checkState == Qt::Checked ? true : false; item->callback(item, &value, item->user_data); } @@ -539,7 +537,7 @@ void AdditionalToolbarWidgetAction::sendTextToCallback() ApplyLineEdit * editor = dynamic_cast<ApplyLineEdit *>(sender()); if (! editor) { - /* Called from button, searching for acompanying line edit */ + /* Called from button, searching for accompanying line edit */ QWidget * parent = dynamic_cast<QWidget *>(sender()->parent()); if (parent) { diff --git a/ui/qt/widgets/byte_view_text.cpp b/ui/qt/widgets/byte_view_text.cpp index e81dc0ac..ee1e0195 100644 --- a/ui/qt/widgets/byte_view_text.cpp +++ b/ui/qt/widgets/byte_view_text.cpp @@ -70,6 +70,7 @@ ByteViewText::ByteViewText(const QByteArray &data, packet_char_enc encoding, QWi offset_normal_fg_ = ColorUtils::alphaBlend(palette().windowText(), palette().window(), 0.35); offset_field_fg_ = ColorUtils::alphaBlend(palette().windowText(), palette().window(), 0.65); + ctx_menu_.setToolTipsVisible(true); window()->winId(); // Required for screenChanged? https://phabricator.kde.org/D20171 connect(window()->windowHandle(), &QWindow::screenChanged, viewport(), [=](const QScreen *) { viewport()->update(); }); @@ -407,11 +408,7 @@ void ByteViewText::updateLayoutMetrics() int ByteViewText::stringWidth(const QString &line) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) return viewport()->fontMetrics().horizontalAdvance(line); -#else - return viewport()->fontMetrics().boundingRect(line).width(); -#endif } // Draw a line of byte view text for a given offset. diff --git a/ui/qt/widgets/capture_filter_combo.cpp b/ui/qt/widgets/capture_filter_combo.cpp index 70635cef..95766310 100644 --- a/ui/qt/widgets/capture_filter_combo.cpp +++ b/ui/qt/widgets/capture_filter_combo.cpp @@ -126,7 +126,7 @@ void CaptureFilterCombo::rebuildFilterList() QString cur_filter = currentText(); clear(); for (GList *li = g_list_first(cfilter_list); li != NULL; li = gxx_list_next(li)) { - addItem(gxx_list_data(const gchar *, li)); + addItem(gxx_list_data(const char *, li)); } lineEdit()->setText(cur_filter); lineEdit()->blockSignals(false); diff --git a/ui/qt/widgets/capture_filter_edit.cpp b/ui/qt/widgets/capture_filter_edit.cpp index 3c72fe41..50793143 100644 --- a/ui/qt/widgets/capture_filter_edit.cpp +++ b/ui/qt/widgets/capture_filter_edit.cpp @@ -9,8 +9,6 @@ #include "config.h" -#include <glib.h> - #include <epan/proto.h> #include "capture_opts.h" @@ -308,7 +306,7 @@ QPair<const QString, bool> CaptureFilterEdit::getSelectedFilter() #ifdef HAVE_LIBPCAP int selected_devices = 0; - for (guint i = 0; i < global_capture_opts.all_ifaces->len; i++) { + for (unsigned i = 0; i < global_capture_opts.all_ifaces->len; i++) { interface_t *device = &g_array_index(global_capture_opts.all_ifaces, interface_t, i); if (device->selected) { selected_devices++; diff --git a/ui/qt/widgets/compression_group_box.cpp b/ui/qt/widgets/compression_group_box.cpp new file mode 100644 index 00000000..08a74dae --- /dev/null +++ b/ui/qt/widgets/compression_group_box.cpp @@ -0,0 +1,69 @@ +/* @file + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "compression_group_box.h" + +#include <QRadioButton> +#include <QButtonGroup> +#include <QVBoxLayout> + +CompressionGroupBox::CompressionGroupBox(QWidget *parent) : + QGroupBox(parent) +{ + setTitle(tr("Compression options")); + setFlat(true); + + + bg_ = new QButtonGroup(this); + QVBoxLayout *vbox = new QVBoxLayout(); + + QRadioButton *radio1 = new QRadioButton(tr("&Uncompressed")); + bg_->addButton(radio1, WTAP_UNCOMPRESSED); + vbox->addWidget(radio1); + +#if defined (HAVE_ZLIB) || defined (HAVE_ZLIBNG) + QRadioButton *radio2 = new QRadioButton(tr("Compress with g&zip")); + bg_->addButton(radio2, WTAP_GZIP_COMPRESSED); + vbox->addWidget(radio2); +#endif +#ifdef HAVE_LZ4FRAME_H + QRadioButton *radio3 = new QRadioButton(tr("Compress with &LZ4")); + bg_->addButton(radio3, WTAP_LZ4_COMPRESSED); + vbox->addWidget(radio3); +#endif + + radio1->setChecked(true); + + setLayout(vbox); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + connect(bg_, &QButtonGroup::idToggled, [=] { emit stateChanged(); }); +#else + connect(bg_, QOverload<int, bool>::of(&QButtonGroup::buttonToggled), [=] { emit stateChanged(); }); +#endif + +} + +CompressionGroupBox::~CompressionGroupBox() +{ +} + +wtap_compression_type CompressionGroupBox::compressionType() const +{ + return static_cast<wtap_compression_type>(bg_->checkedId()); +} + +void CompressionGroupBox::setCompressionType(wtap_compression_type type) +{ + QAbstractButton *button = bg_->button(type); + if (button != nullptr) { + button->setChecked(true); + } +} + diff --git a/ui/qt/widgets/compression_group_box.h b/ui/qt/widgets/compression_group_box.h new file mode 100644 index 00000000..4e70ca2d --- /dev/null +++ b/ui/qt/widgets/compression_group_box.h @@ -0,0 +1,41 @@ +/** @file + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef COMPRESSION_GROUP_BOX_H +#define COMPRESSION_GROUP_BOX_H + +#include <config.h> + +#include <QGroupBox> + +#include <wiretap/wtap.h> + +class QButtonGroup; + +/** + * UI element for selecting compression type from among those supported. + */ +class CompressionGroupBox : public QGroupBox +{ + Q_OBJECT + +public: + explicit CompressionGroupBox(QWidget *parent = 0); + ~CompressionGroupBox(); + wtap_compression_type compressionType() const; + void setCompressionType(wtap_compression_type type); + +signals: + void stateChanged(); + +private: + QButtonGroup *bg_; +}; + +#endif // COMPRESSION_GROUP_BOX_H diff --git a/ui/qt/widgets/copy_from_profile_button.h b/ui/qt/widgets/copy_from_profile_button.h index f074bdb8..ea6df816 100644 --- a/ui/qt/widgets/copy_from_profile_button.h +++ b/ui/qt/widgets/copy_from_profile_button.h @@ -11,7 +11,6 @@ #define COPY_FROM_PROFILE_BUTTON_H #include <config.h> -#include <glib.h> #include <QMenu> #include <QPushButton> diff --git a/ui/qt/widgets/display_filter_combo.cpp b/ui/qt/widgets/display_filter_combo.cpp index c28afb91..674da765 100644 --- a/ui/qt/widgets/display_filter_combo.cpp +++ b/ui/qt/widgets/display_filter_combo.cpp @@ -25,8 +25,31 @@ #include <ui/qt/utils/color_utils.h> #include "main_application.h" -// If we ever add support for multiple windows this will need to be replaced. -static DisplayFilterCombo *cur_display_filter_combo = NULL; +static QStandardItemModel *cur_model; + +extern "C" void dfilter_recent_combo_write_all(FILE *rf) { + if (cur_model == nullptr) + return; + + for (int i = 0; i < cur_model->rowCount(); i++ ) { + const QByteArray& filter = cur_model->item(i)->text().toUtf8(); + if (!filter.isEmpty()) { + fprintf(rf, RECENT_KEY_DISPLAY_FILTER ": %s\n", filter.constData()); + } + } +} + +extern "C" bool dfilter_combo_add_recent(const char *filter) { + if (cur_model == nullptr) { + cur_model = new QStandardItemModel(); + cur_model->setSortRole(Qt::UserRole); + } + + QStandardItem *new_item = new QStandardItem(filter); + new_item->setData(QVariant(QDateTime::currentMSecsSinceEpoch()), Qt::UserRole); + cur_model->appendRow(new_item); + return true; +} DisplayFilterCombo::DisplayFilterCombo(QWidget *parent) : QComboBox(parent) @@ -46,36 +69,51 @@ DisplayFilterCombo::DisplayFilterCombo(QWidget *parent) : // Default is Preferred. setSizePolicy(QSizePolicy::MinimumExpanding, sizePolicy().verticalPolicy()); setAccessibleName(tr("Display filter selector")); - cur_display_filter_combo = this; updateStyleSheet(); setToolTip(tr("Select from previously used filters.")); - QStandardItemModel *model = qobject_cast<QStandardItemModel*>(this->model()); - model->setSortRole(Qt::UserRole); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + // Setting the placeholderText keeps newly added items from being the + // current item. It only works for the placeholderText of the QComboBox, + // not the lineEdit (even though the lineEdit's placeholderText is shown + // instead.) This only matters for any combobox created before the recent + // display filter list is read (i.e., the main window one.) + setPlaceholderText(lineEdit()->placeholderText()); +#endif + + if (cur_model == nullptr) { + cur_model = new QStandardItemModel(); + cur_model->setSortRole(Qt::UserRole); + } + setModel(cur_model); connect(mainApp, &MainApplication::preferencesChanged, this, &DisplayFilterCombo::updateMaxCount); // Ugly cast required (?) // https://stackoverflow.com/questions/16794695/connecting-overloaded-signals-and-slots-in-qt-5 connect(this, static_cast<void (DisplayFilterCombo::*)(int)>(&DisplayFilterCombo::activated), this, &DisplayFilterCombo::onActivated); -} - -extern "C" void dfilter_recent_combo_write_all(FILE *rf) { - if (!cur_display_filter_combo) - return; - cur_display_filter_combo->writeRecent(rf); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + connect(cur_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &DisplayFilterCombo::rowsAboutToBeInserted); + connect(cur_model, &QAbstractItemModel::rowsInserted, this, &DisplayFilterCombo::rowsInserted); +#endif } -void DisplayFilterCombo::writeRecent(FILE *rf) { - int i; +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) +void DisplayFilterCombo::rowsAboutToBeInserted(const QModelIndex&, int, int) +{ + // If the current text is blank but we're inserting a row, that means + // it is being added programmatically from the model, and we want to + // clear it afterwards and show the placeholder text instead. + clear_state_ = (currentText() == QString()); +} - for (i = 0; i < count(); i++) { - const QByteArray& filter = itemText(i).toUtf8(); - if (!filter.isEmpty()) { - fprintf(rf, RECENT_KEY_DISPLAY_FILTER ": %s\n", filter.constData()); - } +void DisplayFilterCombo::rowsInserted(const QModelIndex&, int, int) +{ + if (clear_state_) { + clearEditText(); } } +#endif void DisplayFilterCombo::onActivated(int row) { @@ -172,18 +210,3 @@ void DisplayFilterCombo::updateMaxCount() { setMaxCount(prefs.gui_recent_df_entries_max); } - -extern "C" gboolean dfilter_combo_add_recent(const gchar *filter) { - if (!cur_display_filter_combo) - return FALSE; - - // Adding an item to a QComboBox also sets its lineEdit. In our case - // that means we might trigger a temporary status message so we block - // the lineEdit's signals. - // Another approach would be to update QComboBox->model directly. - bool block_state = cur_display_filter_combo->lineEdit()->blockSignals(true); - cur_display_filter_combo->addItem(filter, QVariant(QDateTime::currentMSecsSinceEpoch())); - cur_display_filter_combo->clearEditText(); - cur_display_filter_combo->lineEdit()->blockSignals(block_state); - return TRUE; -} diff --git a/ui/qt/widgets/display_filter_combo.h b/ui/qt/widgets/display_filter_combo.h index 81fa11ba..d53bfca4 100644 --- a/ui/qt/widgets/display_filter_combo.h +++ b/ui/qt/widgets/display_filter_combo.h @@ -23,9 +23,16 @@ public: void updateStyleSheet(); protected: +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + void rowsAboutToBeInserted(const QModelIndex&, int, int); + void rowsInserted(const QModelIndex&, int, int); +#endif virtual bool event(QEvent *event); private: +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + bool clear_state_; +#endif public slots: bool checkDisplayFilter(); diff --git a/ui/qt/widgets/display_filter_edit.cpp b/ui/qt/widgets/display_filter_edit.cpp index 839b14fd..09113da7 100644 --- a/ui/qt/widgets/display_filter_edit.cpp +++ b/ui/qt/widgets/display_filter_edit.cpp @@ -9,9 +9,8 @@ #include "config.h" -#include <glib.h> - #include <epan/dfilter/dfilter.h> +#include <epan/dfilter/dfunctions.h> #include <ui/recent.h> @@ -58,8 +57,16 @@ #define DEFAULT_MODIFIER "Ctrl-" #endif -// proto.c:fld_abbrev_chars -static const QString fld_abbrev_chars_ = "-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; +// ':' is not a legal field character, but it appears inside literals and +// having it as a token character will keep field completion from being +// offered in a place where it is syntactically impossible. +// +// The other place ':' is used in the grammar is to separate display filter +// macros from their argument lists in the ${macro:arg;arg} format. Adding +// ':' here means that the first argument of the list won't have a completion +// pop-up. (We don't do completion for the macro names, maybe we should?) +// ${macro;arg;arg} is allowed now, though. +static const QString fld_abbrev_chars_ = ":-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; DisplayFilterEdit::DisplayFilterEdit(QWidget *parent, DisplayFilterEditType type) : SyntaxLineEdit(parent), @@ -121,6 +128,8 @@ DisplayFilterEdit::DisplayFilterEdit(QWidget *parent, DisplayFilterEditType type connect(this, &DisplayFilterEdit::returnPressed, this, &DisplayFilterEdit::applyDisplayFilter); } + setDefaultPlaceholderText(); + connect(this, &DisplayFilterEdit::textChanged, this, static_cast<void (DisplayFilterEdit::*)(const QString &)>(&DisplayFilterEdit::checkFilter)); @@ -578,16 +587,24 @@ void DisplayFilterEdit::buildCompletionList(const QString &field_word, const QSt void *field_cookie; const QByteArray fw_ba = field_word.toUtf8(); // or toLatin1 or toStdString? const char *fw_utf8 = fw_ba.constData(); - gsize fw_len = (gsize) strlen(fw_utf8); + size_t fw_len = (size_t) strlen(fw_utf8); for (header_field_info *hfinfo = proto_get_first_protocol_field(proto_id, &field_cookie); hfinfo; hfinfo = proto_get_next_protocol_field(proto_id, &field_cookie)) { if (hfinfo->same_name_prev_id != -1) continue; // Ignore duplicate names. if (!g_ascii_strncasecmp(fw_utf8, hfinfo->abbrev, fw_len)) { - if ((gsize) strlen(hfinfo->abbrev) != fw_len) field_list << hfinfo->abbrev; + if ((size_t) strlen(hfinfo->abbrev) != fw_len) field_list << hfinfo->abbrev; } } } } + + // Add display filter functions to the completion list + GPtrArray *func_list = df_func_name_list(); + for (unsigned i = 0; i < func_list->len; i++) { + field_list << QString::fromUtf8(static_cast<const char *>(func_list->pdata[i])).append("("); + } + g_ptr_array_unref(func_list); + field_list.sort(); } @@ -614,19 +631,42 @@ void DisplayFilterEdit::clearFilter() void DisplayFilterEdit::applyDisplayFilter() { if (completer()->popup()->currentIndex().isValid()) { - // If the popup (not the QCompleter itself) has a currently - // valid QModelIndex, that means that the popup's - // QAbstractItemView::activated() signal has not yet - // been handled, which means that text() has the old value, - // not the one from the completer. - return; + // If the popup (not the QCompleter itself) has a currently valid + // QModelIndex, check to see if text() matches the text from the popup. + // If it does, then all is well, go ahead and filter (this happens + // if the popup entry is selected via mouse.) + // + // If it doesn't match, then it has the old value. There are two + // possibilities: + // 1) The user clicked away from the popup *without* selecting + // anything (making the popup disappear), and then hit Enter, in + // which case the user wants to filter with text() and doesn't care + // about what's in the popup. However, the QModelIndex for the popup + // is still valid until some time after this signal is handled. + // + // 2) The user pressed Return on an entry in the popup, in which + // case the user wants to filter with the new value in the popup, + // not the value in text(), but for some reason the popup's + // activated() signal gets handled *after* returnPressed on the + // LineEdit, unfortunately (#19323). + // + // We haven't figured out how to distinguish case 1 from case 2 yet, + // so ignore this force the user to press Enter again, and which + // point everything will have reconciled. + // + // Note that the currentCompletion() / currentIndex.data() of + // the completer() itself is "what would be the first completion + // of the text currently displayed in the line edit" and has naught + // to do with what was selected in the popup. + if (text() != completer()->popup()->currentIndex().data()) { + return; + } } if (syntaxState() == Invalid) return; - if (text().length() > 0) - last_applied_ = text(); + last_applied_ = text(); updateClearButton(); @@ -712,9 +752,7 @@ void DisplayFilterEdit::applyOrPrepareFilter() if (! pa || pa->property("display_filter").toString().isEmpty()) return; - QString filterText = pa->property("display_filter").toString(); - last_applied_ = filterText; - setText(filterText); + setText(pa->property("display_filter").toString()); // Holding down the Shift key will only prepare filter. if (!(QApplication::keyboardModifiers() & Qt::ShiftModifier)) { @@ -802,7 +840,6 @@ void DisplayFilterEdit::dropEvent(QDropEvent *event) return; } - last_applied_ = filterText; setText(filterText); // Holding down the Shift key will only prepare filter. diff --git a/ui/qt/widgets/dissector_syntax_line_edit.cpp b/ui/qt/widgets/dissector_syntax_line_edit.cpp index ae8a2247..4f228ce4 100644 --- a/ui/qt/widgets/dissector_syntax_line_edit.cpp +++ b/ui/qt/widgets/dissector_syntax_line_edit.cpp @@ -9,8 +9,6 @@ #include "config.h" -#include <glib.h> - #include <epan/packet.h> #include <wsutil/utf8_entities.h> @@ -38,6 +36,15 @@ DissectorSyntaxLineEdit::DissectorSyntaxLineEdit(QWidget *parent) : setCompleter(new QCompleter(completion_model_, this)); setCompletionTokenChars(fld_abbrev_chars_); + updateDissectorNames(); + setDefaultPlaceholderText(); + + connect(this, &DissectorSyntaxLineEdit::textChanged, this, + static_cast<void (DissectorSyntaxLineEdit::*)(const QString &)>(&DissectorSyntaxLineEdit::checkDissectorName)); +} + +void DissectorSyntaxLineEdit::updateDissectorNames() +{ GList *dissector_names = get_dissector_names(); QStringList dissector_list; for (GList* l = dissector_names; l != NULL; l = l->next) { @@ -46,10 +53,6 @@ DissectorSyntaxLineEdit::DissectorSyntaxLineEdit(QWidget *parent) : g_list_free(dissector_names); dissector_list.sort(); completion_model_->setStringList(dissector_list); - setDefaultPlaceholderText(); - - connect(this, &DissectorSyntaxLineEdit::textChanged, this, - static_cast<void (DissectorSyntaxLineEdit::*)(const QString &)>(&DissectorSyntaxLineEdit::checkDissectorName)); } void DissectorSyntaxLineEdit::setDefaultPlaceholderText() diff --git a/ui/qt/widgets/dissector_syntax_line_edit.h b/ui/qt/widgets/dissector_syntax_line_edit.h index adcee8c7..a99442b7 100644 --- a/ui/qt/widgets/dissector_syntax_line_edit.h +++ b/ui/qt/widgets/dissector_syntax_line_edit.h @@ -20,19 +20,22 @@ class DissectorSyntaxLineEdit : public SyntaxLineEdit Q_OBJECT public: explicit DissectorSyntaxLineEdit(QWidget *parent = 0); + void updateDissectorNames(); + void setDefaultPlaceholderText(); protected: void keyPressEvent(QKeyEvent *event) { completionKeyPressEvent(event); } void focusInEvent(QFocusEvent *event) { completionFocusInEvent(event); } -private slots: +public slots: void checkDissectorName(const QString &dissector); + +private slots: void changeEvent(QEvent* event); private: QString placeholder_text_; - void setDefaultPlaceholderText(); void buildCompletionList(const QString &field_word, const QString &preamble); }; diff --git a/ui/qt/widgets/field_filter_edit.cpp b/ui/qt/widgets/field_filter_edit.cpp index 7aebf051..b2452c30 100644 --- a/ui/qt/widgets/field_filter_edit.cpp +++ b/ui/qt/widgets/field_filter_edit.cpp @@ -9,8 +9,6 @@ #include "config.h" -#include <glib.h> - #include <epan/dfilter/dfilter.h> #include <wsutil/filter_files.h> @@ -160,12 +158,12 @@ void FieldFilterEdit::buildCompletionList(const QString &field_word, const QStri void *field_cookie; const QByteArray fw_ba = field_word.toUtf8(); // or toLatin1 or toStdString? const char *fw_utf8 = fw_ba.constData(); - gsize fw_len = (gsize) strlen(fw_utf8); + size_t fw_len = (size_t) strlen(fw_utf8); for (header_field_info *hfinfo = proto_get_first_protocol_field(proto_id, &field_cookie); hfinfo; hfinfo = proto_get_next_protocol_field(proto_id, &field_cookie)) { if (hfinfo->same_name_prev_id != -1) continue; // Ignore duplicate names. if (!g_ascii_strncasecmp(fw_utf8, hfinfo->abbrev, fw_len)) { - if ((gsize) strlen(hfinfo->abbrev) != fw_len) field_list << hfinfo->abbrev; + if ((size_t) strlen(hfinfo->abbrev) != fw_len) field_list << hfinfo->abbrev; } } } diff --git a/ui/qt/widgets/filter_expression_toolbar.cpp b/ui/qt/widgets/filter_expression_toolbar.cpp index 84dabc47..c17f7aee 100644 --- a/ui/qt/widgets/filter_expression_toolbar.cpp +++ b/ui/qt/widgets/filter_expression_toolbar.cpp @@ -181,7 +181,7 @@ WiresharkMimeData * FilterExpressionToolBar::createMimeData(QString name, int po void FilterExpressionToolBar::onActionMoved(QAction* action, int oldPos, int newPos) { - gchar* err = NULL; + char* err = NULL; if (oldPos == newPos) return; @@ -237,7 +237,7 @@ void FilterExpressionToolBar::onFilterDropped(QString description, QString filte return; filter_expression_new(qUtf8Printable(description), - qUtf8Printable(filter), qUtf8Printable(description), TRUE); + qUtf8Printable(filter), qUtf8Printable(description), true); save_migrated_uat("Display expressions", &prefs.filter_expressions_old); filterExpressionsChanged(); @@ -356,16 +356,14 @@ QMenu * FilterExpressionToolBar::findParentMenu(const QStringList tree, void *fe /* Searching existing main menus */ foreach(QAction * entry, data->toolbar->actions()) { - QWidget * widget = data->toolbar->widgetForAction(entry); - QToolButton * tb = qobject_cast<QToolButton *>(widget); - if (tb && tb->menu() && tb->text().compare(tree.at(0).trimmed()) == 0) - return findParentMenu(tree.mid(1), fed_data, tb->menu()); + if (entry->text().compare(tree.at(0).trimmed()) == 0) + return findParentMenu(tree.mid(1), fed_data, entry->menu()); } } else if (parent) { QString menuName = tree.at(0).trimmed(); - /* Iterate to see, if we next have to jump into another submenu */ + /* Iterate to see if we next have to jump into another submenu */ foreach(QAction *entry, parent->actions()) { if (entry->menu() && entry->text().compare(menuName) == 0) @@ -382,16 +380,18 @@ QMenu * FilterExpressionToolBar::findParentMenu(const QStringList tree, void *fe /* No menu has been found, create one */ QString parentName = tree.at(0).trimmed(); - QToolButton * menuButton = new QToolButton(); - menuButton->setText(parentName); - menuButton->setPopupMode(QToolButton::MenuButtonPopup); - QMenu * parentMenu = new QMenu(menuButton); + QMenu * parentMenu = new QMenu(data->toolbar); parentMenu->installEventFilter(data->toolbar); parentMenu->setProperty(dfe_menu_, QVariant::fromValue(true)); - menuButton->setMenu(parentMenu); - // Required for QToolButton::MenuButtonPopup. - connect(menuButton, &QToolButton::pressed, menuButton, &QToolButton::showMenu); - data->toolbar->addWidget(menuButton); + QAction *menuAction = new QAction(data->toolbar); + menuAction->setText(parentName); + menuAction->setMenu(parentMenu); + // QToolButton::MenuButtonPopup means that pressing the button text + // itself doesn't open the menu, only pressing the downwards pointing + // triangle does. This is difficult to change for the auto created + // QToolButton inside the QToolBar. But only auto created tool buttons + // will show up in the extension menu at narrow widths (#19887.) + data->toolbar->addAction(menuAction); return findParentMenu(tree.mid(1), fed_data, parentMenu); } @@ -407,7 +407,7 @@ bool FilterExpressionToolBar::filter_expression_add_action(const void *key _U_, struct filter_expression_data* data = (filter_expression_data*)user_data; if (!fe->enabled) - return FALSE; + return false; QString label = QString(fe->label); @@ -449,7 +449,7 @@ bool FilterExpressionToolBar::filter_expression_add_action(const void *key _U_, connect(dfb_action, &QAction::triggered, data->toolbar, &FilterExpressionToolBar::filterClicked); data->actions_added = true; - return FALSE; + return false; } void FilterExpressionToolBar::filterClicked() diff --git a/ui/qt/widgets/filter_expression_toolbar.h b/ui/qt/widgets/filter_expression_toolbar.h index 80724a27..174d927b 100644 --- a/ui/qt/widgets/filter_expression_toolbar.h +++ b/ui/qt/widgets/filter_expression_toolbar.h @@ -9,8 +9,6 @@ #include <ui/qt/widgets/drag_drop_toolbar.h> -#include <glib.h> - #include <QMenu> #ifndef FILTER_EXPRESSION_TOOLBAR_H diff --git a/ui/qt/widgets/follow_stream_text.cpp b/ui/qt/widgets/follow_stream_text.cpp index a8e0f2b9..b2492c0b 100644 --- a/ui/qt/widgets/follow_stream_text.cpp +++ b/ui/qt/widgets/follow_stream_text.cpp @@ -9,10 +9,16 @@ #include <ui/qt/widgets/follow_stream_text.h> +#include "epan/prefs.h" + +#include <ui/qt/utils/color_utils.h> + #include <main_application.h> +#include <QMap> #include <QMouseEvent> #include <QTextCursor> +#include <QScrollBar> // To do: // - Draw text by hand similar to ByteViewText. This would let us add @@ -20,17 +26,96 @@ // max_document_length_ in FollowStreamDialog. FollowStreamText::FollowStreamText(QWidget *parent) : - QPlainTextEdit(parent) + QPlainTextEdit(parent), truncated_(false) { setMouseTracking(true); // setMaximumBlockCount(1); QTextDocument *text_doc = document(); text_doc->setDefaultFont(mainApp->monospaceFont()); + + metainfo_fg_ = ColorUtils::alphaBlend(palette().windowText(), palette().window(), 0.35); +} + +const int FollowStreamText::max_document_length_ = 500 * 1000 * 1000; // Just a guess + +void FollowStreamText::addTruncated(int cur_pos) +{ + if (truncated_) { + QTextCharFormat tcf = currentCharFormat(); + tcf.setBackground(palette().base().color()); + tcf.setForeground(metainfo_fg_); + insertPlainText("\n" + tr("[Stream output truncated]")); + moveCursor(QTextCursor::End); + } else { + verticalScrollBar()->setValue(cur_pos); + } +} + +void FollowStreamText::addText(QString text, bool is_from_server, uint32_t packet_num, bool colorize) +{ + if (truncated_) { + return; + } + + int char_count = document()->characterCount(); + if (char_count + text.length() > max_document_length_) { + text.truncate(max_document_length_ - char_count); + truncated_ = true; + } + + setUpdatesEnabled(false); + int cur_pos = verticalScrollBar()->value(); + moveCursor(QTextCursor::End); + + QTextCharFormat tcf = currentCharFormat(); + if (!colorize) { + tcf.setBackground(palette().base().color()); + tcf.setForeground(palette().text().color()); + } else if (is_from_server) { + tcf.setForeground(ColorUtils::fromColorT(prefs.st_server_fg)); + tcf.setBackground(ColorUtils::fromColorT(prefs.st_server_bg)); + } else { + tcf.setForeground(ColorUtils::fromColorT(prefs.st_client_fg)); + tcf.setBackground(ColorUtils::fromColorT(prefs.st_client_bg)); + } + setCurrentCharFormat(tcf); + + insertPlainText(text); + text_pos_to_packet_[textCursor().anchor()] = packet_num; + + addTruncated(cur_pos); + setUpdatesEnabled(true); +} + +void FollowStreamText::addDeltaTime(double delta) +{ + QString delta_str = QString("\n%1s").arg(QString::number(delta, 'f', 6)); + if (truncated_) { + return; + } + + if (document()->characterCount() + delta_str.length() > max_document_length_) { + truncated_ = true; + } + + setUpdatesEnabled(false); + int cur_pos = verticalScrollBar()->value(); + moveCursor(QTextCursor::End); + + QTextCharFormat tcf = currentCharFormat(); + tcf.setBackground(palette().base().color()); + tcf.setForeground(metainfo_fg_); + setCurrentCharFormat(tcf); + + insertPlainText(delta_str); + + addTruncated(cur_pos); + setUpdatesEnabled(true); } void FollowStreamText::mouseMoveEvent(QMouseEvent *event) { - emit mouseMovedToTextCursorPosition(cursorForPosition(event->pos()).position()); + emit mouseMovedToPacket(textPosToPacket(cursorForPosition(event->pos()).position())); // Don't send the mouseMoveEvents with no buttons pushed to the base // class, effectively turning off mouse tracking for the base class. // It causes a lot of useless calculations that hurt scroll performance. @@ -41,12 +126,37 @@ void FollowStreamText::mouseMoveEvent(QMouseEvent *event) void FollowStreamText::mousePressEvent(QMouseEvent *event) { - emit mouseClickedOnTextCursorPosition(cursorForPosition(event->pos()).position()); + emit mouseClickedOnPacket(textPosToPacket(cursorForPosition(event->pos()).position())); QPlainTextEdit::mousePressEvent(event); } void FollowStreamText::leaveEvent(QEvent *event) { - emit mouseMovedToTextCursorPosition(-1); + emit mouseMovedToPacket(0); QPlainTextEdit::leaveEvent(event); } + +void FollowStreamText::clear() +{ + truncated_ = false; + text_pos_to_packet_.clear(); + QPlainTextEdit::clear(); +} + +int FollowStreamText::currentPacket() const +{ + return textPosToPacket(textCursor().position()); +} + +int FollowStreamText::textPosToPacket(int text_pos) const +{ + int pkt = 0; + if (text_pos >= 0) { + QMap<int, uint32_t>::const_iterator it = text_pos_to_packet_.upperBound(text_pos); + if (it != text_pos_to_packet_.end()) { + pkt = it.value(); + } + } + + return pkt; +} diff --git a/ui/qt/widgets/follow_stream_text.h b/ui/qt/widgets/follow_stream_text.h index 0dd8dca5..0276c872 100644 --- a/ui/qt/widgets/follow_stream_text.h +++ b/ui/qt/widgets/follow_stream_text.h @@ -17,6 +17,10 @@ class FollowStreamText : public QPlainTextEdit Q_OBJECT public: explicit FollowStreamText(QWidget *parent = 0); + bool isTruncated() const { return truncated_; } + void addText(QString text, bool is_from_server, uint32_t packet_num, bool colorize); + void addDeltaTime(double delta); + int currentPacket() const; protected: void mouseMoveEvent(QMouseEvent *event); @@ -24,12 +28,20 @@ protected: void leaveEvent(QEvent *event); signals: - // Perhaps this is not descriptive enough. We should add more words. - void mouseMovedToTextCursorPosition(int); - void mouseClickedOnTextCursorPosition(int); + void mouseMovedToPacket(int); + void mouseClickedOnPacket(int); public slots: + void clear(); +private: + int textPosToPacket(int text_pos) const; + void addTruncated(int cur_pos); + + static const int max_document_length_; + bool truncated_; + QMap<int, uint32_t> text_pos_to_packet_; + QColor metainfo_fg_; }; #endif // FOLLOW_STREAM_TEXT_H diff --git a/ui/qt/widgets/label_stack.cpp b/ui/qt/widgets/label_stack.cpp index 4cac15b7..8deedaec 100644 --- a/ui/qt/widgets/label_stack.cpp +++ b/ui/qt/widgets/label_stack.cpp @@ -68,9 +68,10 @@ void LabelStack::fillLabel() { setStyleSheet(style_sheet); } setText(si.text); + setToolTip(si.tooltip); } -void LabelStack::pushText(const QString &text, int ctx) { +void LabelStack::pushText(const QString &text, int ctx, const QString &tooltip) { popText(ctx); if (ctx == temporary_ctx_) { @@ -83,6 +84,7 @@ void LabelStack::pushText(const QString &text, int ctx) { StackItem si; si.text = text; + si.tooltip = tooltip; si.ctx = ctx; labels_.prepend(si); fillLabel(); diff --git a/ui/qt/widgets/label_stack.h b/ui/qt/widgets/label_stack.h index 63657b74..27e41be1 100644 --- a/ui/qt/widgets/label_stack.h +++ b/ui/qt/widgets/label_stack.h @@ -21,7 +21,7 @@ class LabelStack : public QLabel public: explicit LabelStack(QWidget *parent = 0); void setTemporaryContext(const int ctx); - void pushText(const QString &text, int ctx); + void pushText(const QString &text, int ctx, const QString &tooltip = QString()); void setShrinkable(bool shrinkable = true); protected: @@ -35,6 +35,7 @@ protected: private: typedef struct _StackItem { QString text; + QString tooltip; int ctx; } StackItem; diff --git a/ui/qt/widgets/path_selection_edit.cpp b/ui/qt/widgets/path_selection_edit.cpp index eb30f1d0..660a6f53 100644 --- a/ui/qt/widgets/path_selection_edit.cpp +++ b/ui/qt/widgets/path_selection_edit.cpp @@ -10,18 +10,18 @@ #include "config.h" -#include "epan/prefs.h" #include "ui/util.h" #include <ui/qt/widgets/path_selection_edit.h> #include "ui/qt/widgets/wireshark_file_dialog.h" +#include "ui/qt/utils/qt_ui_utils.h" #include <QHBoxLayout> #include <QToolButton> #include <QWidget> #include <QLineEdit> -PathSelectionEdit::PathSelectionEdit(QString title, QString path, bool selectFile, QWidget *parent) : +PathSelectionEdit::PathSelectionEdit(QString title, QString path, bool selectFile, QWidget *parent) : QWidget(parent) { _title = title; @@ -49,7 +49,7 @@ PathSelectionEdit::PathSelectionEdit(QString title, QString path, bool selectFil setFocusPolicy(_edit->focusPolicy()); } -PathSelectionEdit::PathSelectionEdit(QWidget *parent) : +PathSelectionEdit::PathSelectionEdit(QWidget *parent) : PathSelectionEdit(tr("Select a path"), QString(), true, parent) {} @@ -75,11 +75,7 @@ void PathSelectionEdit::browseForPath() QString openDir = _path; if (openDir.isEmpty()) { - if (prefs.gui_fileopen_style == FO_STYLE_LAST_OPENED) { - openDir = QString(get_open_dialog_initial_dir()); - } else if (prefs.gui_fileopen_style == FO_STYLE_SPECIFIED) { - openDir = QString(prefs.gui_fileopen_dir); - } + openDir = openDialogInitialDir(); } QString newPath; diff --git a/ui/qt/widgets/profile_tree_view.cpp b/ui/qt/widgets/profile_tree_view.cpp index afa86d71..ebac67c7 100644 --- a/ui/qt/widgets/profile_tree_view.cpp +++ b/ui/qt/widgets/profile_tree_view.cpp @@ -7,31 +7,28 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -#include <ui/qt/models/url_link_delegate.h> #include <ui/qt/models/profile_model.h> #include <ui/qt/utils/qt_ui_utils.h> +#include <ui/qt/widgets/display_filter_edit.h> #include <ui/qt/widgets/profile_tree_view.h> #include <QDesktopServices> #include <QDir> +#include <QHeaderView> #include <QItemDelegate> #include <QLineEdit> -#include <QUrl> -ProfileUrlLinkDelegate::ProfileUrlLinkDelegate(QObject *parent) : UrlLinkDelegate (parent) {} +ProfileTreeEditDelegate::ProfileTreeEditDelegate(QWidget *parent) : QItemDelegate(parent), editor_(Q_NULLPTR) {} -void ProfileUrlLinkDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +QWidget *ProfileTreeEditDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const { - /* Only paint links for valid paths */ - if (index.data(ProfileModel::DATA_PATH_IS_NOT_DESCRIPTION).toBool()) - UrlLinkDelegate::paint(painter, option, index); - else - QStyledItemDelegate::paint(painter, option, index); - + if (index.column() == ProfileModel::COL_AUTO_SWITCH_FILTER) { + return new DisplayFilterEdit(parent); + } + return QItemDelegate::createEditor(parent, option, index); } -ProfileTreeEditDelegate::ProfileTreeEditDelegate(QWidget *parent) : QItemDelegate(parent), editor_(Q_NULLPTR) {} - void ProfileTreeEditDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (qobject_cast<QLineEdit *>(editor)) @@ -46,8 +43,8 @@ ProfileTreeView::ProfileTreeView(QWidget *parent) : { delegate_ = new ProfileTreeEditDelegate(); setItemDelegateForColumn(ProfileModel::COL_NAME, delegate_); + setItemDelegateForColumn(ProfileModel::COL_AUTO_SWITCH_FILTER, delegate_); - connect(this, &QAbstractItemView::clicked, this, &ProfileTreeView::clicked); connect(delegate_, &ProfileTreeEditDelegate::commitData, this, &ProfileTreeView::itemUpdated); } @@ -80,19 +77,6 @@ void ProfileTreeView::selectionChanged(const QItemSelection &selected, const QIt } } -void ProfileTreeView::clicked(const QModelIndex &index) -{ - if (!index.isValid()) - return; - - /* Only paint links for valid paths */ - if (index.data(ProfileModel::DATA_INDEX_VALUE_IS_URL).toBool()) - { - QString path = QDir::toNativeSeparators(index.data().toString()); - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); - } -} - void ProfileTreeView::selectRow(int row) { if (row < 0) @@ -117,3 +101,30 @@ bool ProfileTreeView::activeEdit() { return (state() == QAbstractItemView::EditingState); } + +// If our auto switch filters are shorter than the filter column title, +// stretch the name column. +void ProfileTreeView::showEvent(QShowEvent *) +{ + bool have_wide_filter = false; + int auto_switch_title_width = fontMetrics().horizontalAdvance(model()->headerData(ProfileModel::COL_AUTO_SWITCH_FILTER, Qt::Horizontal).toString()); + for (int row = 0; row < model()->rowCount(); row++) { + QString filter = model()->data(model()->index(row, ProfileModel::COL_AUTO_SWITCH_FILTER)).toString(); + if (fontMetrics().horizontalAdvance(filter) > auto_switch_title_width) { + have_wide_filter = true; + break; + } + } + + if (have_wide_filter) { + return; + } + + int col_name_size_hint = sizeHintForColumn(ProfileModel::COL_NAME); + int col_asf_size_hint = qMax(header()->sectionSizeHint(ProfileModel::COL_AUTO_SWITCH_FILTER), sizeHintForColumn(ProfileModel::COL_AUTO_SWITCH_FILTER)); + int extra = columnWidth(ProfileModel::COL_AUTO_SWITCH_FILTER) - col_asf_size_hint; + if (extra > 0) { + setColumnWidth(ProfileModel::COL_NAME, col_name_size_hint + extra); + } + +} diff --git a/ui/qt/widgets/profile_tree_view.h b/ui/qt/widgets/profile_tree_view.h index 9684811e..02492e4b 100644 --- a/ui/qt/widgets/profile_tree_view.h +++ b/ui/qt/widgets/profile_tree_view.h @@ -15,16 +15,6 @@ #include <QTreeView> #include <QItemDelegate> -class ProfileUrlLinkDelegate : public UrlLinkDelegate -{ - Q_OBJECT - -public: - explicit ProfileUrlLinkDelegate(QObject *parent = Q_NULLPTR); - - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; -}; - class ProfileTreeEditDelegate : public QItemDelegate { Q_OBJECT @@ -32,6 +22,7 @@ public: ProfileTreeEditDelegate(QWidget *parent = Q_NULLPTR); // QAbstractItemDelegate interface + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; private: @@ -54,12 +45,12 @@ signals: // QWidget interface protected: + virtual void showEvent(QShowEvent *); virtual void mouseDoubleClickEvent(QMouseEvent *event); // QAbstractItemView interface protected slots: virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); - virtual void clicked(const QModelIndex &index); private: ProfileTreeEditDelegate *delegate_; diff --git a/ui/qt/widgets/qcp_axis_ticker_elided.cpp b/ui/qt/widgets/qcp_axis_ticker_elided.cpp new file mode 100644 index 00000000..154a750a --- /dev/null +++ b/ui/qt/widgets/qcp_axis_ticker_elided.cpp @@ -0,0 +1,74 @@ +/** @file + * + * QCustomPlot QCPAxisTickerText subclass that elides labels to the + * width of the parent QCPAxis's QCPAxisRect's margin for the appropriate + * side, for use when the margin is fixed. + * + * Copyright 2024 John Thacker <johnthacker@gmail.com> + * + * 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/widgets/qcp_axis_ticker_elided.h> + +#include <QFontMetrics> + +QCPAxisTickerElided::QCPAxisTickerElided(QCPAxis *parent) : + mParent(parent) +{ +} + +QCP::MarginSide QCPAxisTickerElided::axisTypeToMarginSide(const QCPAxis::AxisType axis) +{ + switch (axis) { + case QCPAxis::atLeft: + return QCP::msLeft; + case QCPAxis::atRight: + return QCP::msRight; + case QCPAxis::atTop: + return QCP::msTop; + case QCPAxis::atBottom: + return QCP::msBottom; + } + return QCP::msNone; +} + +QString QCPAxisTickerElided::elidedText(const QString& text) +{ + QCP::MarginSides autoMargins = mParent->axisRect()->autoMargins(); + if (autoMargins & axisTypeToMarginSide(mParent->axisType())) { + return text; + } + int elide_w; + QMargins margins = mParent->axisRect()->margins(); + switch (mParent->axisType()) { + case QCPAxis::atLeft: + elide_w = margins.left(); + break; + case QCPAxis::atRight: + elide_w = margins.right(); + break; + case QCPAxis::atTop: + elide_w = margins.top(); + break; + case QCPAxis::atBottom: + elide_w = margins.bottom(); + break; + default: + // ?? + elide_w = margins.left(); + } + + return QFontMetrics(mParent->tickLabelFont()).elidedText(text, + Qt::ElideRight, + elide_w); +} + +QString QCPAxisTickerElided::getTickLabel(double tick, const QLocale& , QChar , int) +{ + return elidedText(mTicks.value(tick)); +} diff --git a/ui/qt/widgets/qcp_axis_ticker_elided.h b/ui/qt/widgets/qcp_axis_ticker_elided.h new file mode 100644 index 00000000..b542d9a9 --- /dev/null +++ b/ui/qt/widgets/qcp_axis_ticker_elided.h @@ -0,0 +1,38 @@ +/** @file + * + * QCustomPlot QCPAxisTickerText subclass that elides labels to the + * width of the parent QCPAxis's QCPAxisRect's margin for the appropriate + * side, for use when the margin is fixed. + * + * Copyright 2024 John Thacker <johnthacker@gmail.com> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef QCP_AXIS_TICKER_ELIDED_H +#define QCP_AXIS_TICKER_ELIDED_H + +#include <ui/qt/widgets/qcustomplot.h> + +class QCPAxisTickerElided : public QCPAxisTickerText +{ +public: + explicit QCPAxisTickerElided(QCPAxis *parent); + + // QCP has marginSideToAxisType but not the inverse + static QCP::MarginSide axisTypeToMarginSide(const QCPAxis::AxisType); + + QString elidedText(const QString& text); + +protected: + virtual QString getTickLabel(double tick, const QLocale &locale, QChar formatChar, int precision) override; + +private: + QCPAxis *mParent; +}; + +#endif diff --git a/ui/qt/widgets/qcp_axis_ticker_si.cpp b/ui/qt/widgets/qcp_axis_ticker_si.cpp new file mode 100644 index 00000000..be6d35a6 --- /dev/null +++ b/ui/qt/widgets/qcp_axis_ticker_si.cpp @@ -0,0 +1,74 @@ +/** @file + * + * QCustomPlot QCPAxisTicker subclass that creates human-readable + * SI unit labels, optionally supporting log scale. + * + * Copyright 2024 John Thacker <johnthacker@gmail.com> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include <cmath> + +#include <ui/qt/widgets/qcp_axis_ticker_si.h> +#include <ui/qt/utils/qt_ui_utils.h> + +#include <wsutil/str_util.h> + +QCPAxisTickerSi::QCPAxisTickerSi(format_size_units_e unit, QString customUnit, bool log) : + mUnit(unit), mCustomUnit(customUnit), mLog(log) +{ +} + +QString QCPAxisTickerSi::getTickLabel(double tick, const QLocale& , QChar , int precision) +{ + QString label = gchar_free_to_qstring(format_units(nullptr, tick, mUnit, FORMAT_SIZE_PREFIX_SI, precision)); + + // XXX - format_units isn't consistent about whether we need to + // add a space or not + if (mUnit == FORMAT_SIZE_UNIT_NONE && !mCustomUnit.isEmpty()) { + label += mCustomUnit; + } + // XXX - "Beautiful typeset powers" for exponentials is handled by QCPAxis, + // not QCPAxisTicker and its subclasses, and its detection of exponentials + // doesn't handle having a unit or other suffix, so that won't work. + // In practical use we'll be within our prefix range, though. + return label; +} + +int QCPAxisTickerSi::getSubTickCount(double tickStep) +{ + if (mLog) { + return QCPAxisTickerLog::getSubTickCount(tickStep); + } else { + return QCPAxisTicker::getSubTickCount(tickStep); + } +} + +QVector<double> QCPAxisTickerSi::createTickVector(double tickStep, const QCPRange &range) +{ + if (mLog) { + return QCPAxisTickerLog::createTickVector(tickStep, range); + } else { + return QCPAxisTicker::createTickVector(tickStep, range); + } +} + +void QCPAxisTickerSi::setUnit(format_size_units_e unit) +{ + mUnit = unit; +} + +void QCPAxisTickerSi::setCustomUnit(QString unit) +{ + mCustomUnit = unit; +} + +void QCPAxisTickerSi::setLog(bool log) +{ + mLog = log; +} diff --git a/ui/qt/widgets/qcp_axis_ticker_si.h b/ui/qt/widgets/qcp_axis_ticker_si.h new file mode 100644 index 00000000..a9fa51cc --- /dev/null +++ b/ui/qt/widgets/qcp_axis_ticker_si.h @@ -0,0 +1,42 @@ +/** @file + * + * QCustomPlot QCPAxisTicker subclass that creates human-readable + * SI unit labels, optionally supporting log scale. + * + * Copyright 2024 John Thacker <johnthacker@gmail.com> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef QCP_AXIS_TICKER_SI_H +#define QCP_AXIS_TICKER_SI_H + +#include <ui/qt/widgets/qcustomplot.h> + +#include <wsutil/str_util.h> + +class QCPAxisTickerSi : public QCPAxisTickerLog +{ +public: + explicit QCPAxisTickerSi(format_size_units_e unit = FORMAT_SIZE_UNIT_PACKETS, QString customUnit = QString(), bool log = false); + + format_size_units_e getUnit() const { return mUnit; } + void setUnit(format_size_units_e unit); + void setCustomUnit(QString unit); + void setLog(bool log); + +protected: + virtual QString getTickLabel(double tick, const QLocale &locale, QChar formatChar, int precision) override; + virtual int getSubTickCount(double tickStep) override; + virtual QVector<double> createTickVector(double tickStep, const QCPRange &range) override; + + format_size_units_e mUnit; + QString mCustomUnit; + bool mLog; +}; + +#endif diff --git a/ui/qt/widgets/qcp_string_legend_item.cpp b/ui/qt/widgets/qcp_string_legend_item.cpp new file mode 100644 index 00000000..62596895 --- /dev/null +++ b/ui/qt/widgets/qcp_string_legend_item.cpp @@ -0,0 +1,46 @@ +/** @file + * + * QCustomPlot QCPAbstractLegendItem subclass containing a string. + * This is used to add a title to a QCPLegend. + * + * This file is from https://www.qcustomplot.com/index.php/support/forum/443 + * where the author David said "I thought I would share in case any one else + * is needing the same functionality." Accordingly, this file is in the + * public domain. + */ + +#include <ui/qt/widgets/qcp_string_legend_item.h> + +QCPStringLegendItem::QCPStringLegendItem(QCPLegend *pParent, const QString& strText) + : QCPAbstractLegendItem(pParent) + , m_strText(strText) +{ +} + +QString QCPStringLegendItem::text() const +{ + return m_strText; +} + +void QCPStringLegendItem::setText(const QString& strText) +{ + m_strText = strText; +} + +void QCPStringLegendItem::draw(QCPPainter *pPainter) +{ + pPainter->setFont(mFont); + pPainter->setPen(QPen(mTextColor)); + QRectF textRect = pPainter->fontMetrics().boundingRect(0, 0, 0, 0, Qt::TextDontClip, m_strText); + pPainter->drawText(mRect.x() + mMargins.left(), mRect.y(), textRect.width(), textRect.height(), Qt::TextDontClip | Qt::AlignHCenter, m_strText); +} + +QSize QCPStringLegendItem::minimumOuterSizeHint() const +{ + QSize cSize(0, 0); + QFontMetrics fontMetrics(mFont); + QRect textRect = fontMetrics.boundingRect(0, 0, 0, 0, Qt::TextDontClip, m_strText); + cSize.setWidth(textRect.width() + mMargins.left() + mMargins.right()); + cSize.setHeight(textRect.height() + mMargins.top() + mMargins.bottom()); + return cSize; +} diff --git a/ui/qt/widgets/qcp_string_legend_item.h b/ui/qt/widgets/qcp_string_legend_item.h new file mode 100644 index 00000000..d32631ad --- /dev/null +++ b/ui/qt/widgets/qcp_string_legend_item.h @@ -0,0 +1,35 @@ +/** @file + * + * QCustomPlot QCPAbstractLegendItem subclass containing a string. + * This is used to add a title to a QCPLegend. + * + * This file is from https://www.qcustomplot.com/index.php/support/forum/443 + * where the author David said "I thought I would share in case any one else + * is needing the same functionality." Accordingly, this file is in the + * public domain. + */ + +#ifndef QCP_STRING_LEGEND_ITEM_H +#define QCP_STRING_LEGEND_ITEM_H + +#include <ui/qt/widgets/qcustomplot.h> + +class QCPStringLegendItem : public QCPAbstractLegendItem +{ + Q_OBJECT + +public: + explicit QCPStringLegendItem(QCPLegend *pParent, const QString& strText); + + QString text() const; + void setText(const QString& strText); + +protected: + virtual void draw(QCPPainter *painter) override; + virtual QSize minimumOuterSizeHint() const override; + +private: + QString m_strText; +}; + +#endif diff --git a/ui/qt/widgets/resize_header_view.cpp b/ui/qt/widgets/resize_header_view.cpp new file mode 100644 index 00000000..7335265f --- /dev/null +++ b/ui/qt/widgets/resize_header_view.cpp @@ -0,0 +1,45 @@ +/** @file + * + * Header view with a context menu to resize all sections to contents + * + * Copyright 2024 John Thacker <johnthacker@gmail.com> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "resize_header_view.h" + +#include <QMenu> +#include <QContextMenuEvent> + +ResizeHeaderView::ResizeHeaderView(Qt::Orientation orientation, QWidget *parent) : QHeaderView(orientation, parent) +{ + setStretchLastSection(true); + setSectionsMovable(true); + // setFirstSectionMovable(true) ? +} + +/*! + \fn void ResizeHeaderView::contextMenuEvent(QContextMenuEvent *e) + + Shows a context menu which resizes all sections to their contents. + */ + +void ResizeHeaderView::contextMenuEvent(QContextMenuEvent *e) +{ + if (e == nullptr) + return; + + QMenu *ctxMenu = new QMenu(this); + ctxMenu->setAttribute(Qt::WA_DeleteOnClose); + + QString text = tr("Resize all %1 to contents").arg((orientation() == Qt::Horizontal) ? "columns" : "rows"); + QAction *act = ctxMenu->addAction(std::move(text)); + connect(act, &QAction::triggered, this, [&]() { resizeSections(QHeaderView::ResizeToContents); }); + + ctxMenu->popup(e->globalPos()); +} diff --git a/ui/qt/widgets/resize_header_view.h b/ui/qt/widgets/resize_header_view.h new file mode 100644 index 00000000..a8aa57f3 --- /dev/null +++ b/ui/qt/widgets/resize_header_view.h @@ -0,0 +1,31 @@ +/** @file + * + * Header view with a context menu to resize all sections to contents + * + * Copyright 2024 John Thacker <johnthacker@gmail.com> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef RESIZE_HEADER_VIEW_H +#define RESIZE_HEADER_VIEW_H + +#include <config.h> +#include <QHeaderView> + +class ResizeHeaderView : public QHeaderView +{ + Q_OBJECT + +public: + ResizeHeaderView(Qt::Orientation orientation, QWidget *parent = nullptr); + +protected: + void contextMenuEvent(QContextMenuEvent *e) override; + +}; +#endif // RESIZE_HEADER_VIEW_H diff --git a/ui/qt/widgets/resolved_addresses_view.cpp b/ui/qt/widgets/resolved_addresses_view.cpp new file mode 100644 index 00000000..82b2a299 --- /dev/null +++ b/ui/qt/widgets/resolved_addresses_view.cpp @@ -0,0 +1,261 @@ +/** @file + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include <config.h> +#define WS_LOG_DOMAIN LOG_DOMAIN_QTUI + +#include <ui/qt/widgets/resolved_addresses_view.h> +#include <ui/qt/models/resolved_addresses_models.h> +#include <ui/qt/widgets/wireshark_file_dialog.h> + +#include <QHeaderView> +#include <QMessageBox> +#include <QClipboard> +#include <QTextStream> +#include <QJsonArray> +#include <QJsonObject> +#include <QJsonDocument> +#include <QContextMenuEvent> + +#include "main_application.h" + +#include <wsutil/wslog.h> + +ResolvedAddressesView::ResolvedAddressesView(QWidget *parent) : QTableView(parent) +{ + setEditTriggers(QAbstractItemView::NoEditTriggers); + setSortingEnabled(true); + setSelectionBehavior(QAbstractItemView::SelectRows); + horizontalHeader()->setStretchLastSection(true); + verticalHeader()->setVisible(false); + + // creating this action is mostly to override the default Ctrl-C handling + // (which could also be done by overriding KeyPressEvent) and to make the + // keyboard shortcut show up in the context menu. +#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0) + clip_action_ = new QAction(tr("as Plain Text"), this); + clip_action_->setShortcut(QKeySequence(QKeySequence::Copy)); + connect(clip_action_, &QAction::triggered, this, &ResolvedAddressesView::clipboardAction); + addAction(clip_action_); +#else + clip_action_ = addAction(tr("as Plain Text"), QKeySequence(QKeySequence::Copy), this, &ResolvedAddressesView::clipboardAction); +#endif + clip_action_->setProperty("copy_as", ResolvedAddressesView::EXPORT_TEXT); + clip_action_->setProperty("selected", true); +} + +QMenu* ResolvedAddressesView::createCopyMenu(bool selected, QWidget *parent) +{ + QMenu *copy_menu; + if (selected) { + copy_menu = new QMenu(tr("Copy selected rows"), parent); + } else { + copy_menu = new QMenu(tr("Copy table"), parent); + } + copy_menu->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); + QAction *ca; + if (selected) { + copy_menu->addAction(clip_action_); + } else { + ca = copy_menu->addAction(tr("as Plain Text"), this, &ResolvedAddressesView::clipboardAction); + ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_TEXT); + ca->setProperty("selected", selected); + } + ca = copy_menu->addAction(tr("as CSV"), this, &ResolvedAddressesView::clipboardAction); + ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_CSV); + ca->setProperty("selected", selected); + ca = copy_menu->addAction(tr("as JSON"), this, &ResolvedAddressesView::clipboardAction); + ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_JSON); + ca->setProperty("selected", selected); + + return copy_menu; +} + +void ResolvedAddressesView::contextMenuEvent(QContextMenuEvent *e) +{ + if (!e) + return; + + QMenu *ctxMenu = new QMenu(this); + ctxMenu->setAttribute(Qt::WA_DeleteOnClose); + ctxMenu->addMenu(createCopyMenu(true, ctxMenu)); + QAction *act = ctxMenu->addAction(tr("Save selected rows as…")); + act->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); + act->setProperty("selected", true); + connect(act, &QAction::triggered, this, &ResolvedAddressesView::saveAs); + ctxMenu->addSeparator(); + ctxMenu->addMenu(createCopyMenu(false, ctxMenu)); + act = ctxMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), tr("Save table as…"), this, &ResolvedAddressesView::saveAs); + act->setProperty("selected", false); + + ctxMenu->popup(e->globalPos()); +} + +AStringListListModel* ResolvedAddressesView::dataModel() const +{ + QSortFilterProxyModel *proxy = qobject_cast<QSortFilterProxyModel *>(model()); + + if (proxy) { + QAbstractItemModel *source = proxy->sourceModel(); + while (qobject_cast<QSortFilterProxyModel *>(source) != nullptr) { + proxy = qobject_cast<QSortFilterProxyModel *>(source); + source = proxy->sourceModel(); + } + return qobject_cast<AStringListListModel *>(source); + } + return nullptr; +} + +void ResolvedAddressesView::clipboardAction() +{ + QAction *ca = qobject_cast<QAction *>(sender()); + if (ca && ca->property("copy_as").isValid()) { + copyToClipboard(static_cast<eResolvedAddressesExport>(ca->property("copy_as").toInt()), + ca->property("selected").toBool()); + } +} + +void ResolvedAddressesView::copyToClipboard(eResolvedAddressesExport format, bool selected) +{ + QString clipText; + QTextStream stream(&clipText, QIODevice::Text); + toTextStream(stream, format, selected); + mainApp->clipboard()->setText(stream.readAll()); +} + +void ResolvedAddressesView::saveAs() +{ + bool selected = false; + QAction *ca = qobject_cast<QAction *>(sender()); + if (ca && ca->property("selected").isValid()) { + selected = true; + } + QString caption(mainApp->windowTitleString(tr("Save Resolved Addresses As…"))); + QString txtFilter = tr("Plain text (*.txt)"); + QString csvFilter = tr("CSV Document (*.csv)"); + QString jsonFilter = tr("JSON Document (*.json)"); + QString selectedFilter; + QString fileName = WiresharkFileDialog::getSaveFileName(this, caption, + mainApp->openDialogInitialDir().canonicalPath(), + QString("%1;;%2;;%3").arg(txtFilter).arg(csvFilter).arg(jsonFilter), + &selectedFilter); + if (fileName.isEmpty()) { + return; + } + + eResolvedAddressesExport format(EXPORT_TEXT); + if (selectedFilter.compare(csvFilter) == 0) { + format = EXPORT_CSV; + } else if (selectedFilter.compare(jsonFilter) == 0) { + format = EXPORT_JSON; + } + + // macOS and Windows use the native file dialog, which enforces file + // extensions. UN*X dialogs generally don't. That's ok here, at + // least for the text format, because hosts and ethers and services + // files don't have an extension. + QFile saveFile(fileName); + if (saveFile.open(QFile::WriteOnly | QFile::Text)) { + QTextStream stream(&saveFile); + toTextStream(stream, format, selected); + saveFile.close(); + } else { + QMessageBox::warning(this, tr("Warning").arg(saveFile.fileName()), + tr("Unable to save %1: %2").arg(saveFile.fileName().arg(saveFile.errorString()))); + } +} + + +void ResolvedAddressesView::toTextStream(QTextStream& stream, + eResolvedAddressesExport format, bool selected) const +{ + if (model() == nullptr) { + return; + } + + // XXX: TrafficTree and TapParameterDialog have similar + // "export a QAbstractItemModel to a QTextStream in TEXT, CSV or JSON" + // functions that could be made into common code. + QStringList rowText; + if (format == EXPORT_TEXT) { + if (qobject_cast<PortsModel*>(dataModel()) != nullptr) { + // Format of services(5) + if (!selected) { + stream << "# service-name\tport/protocol\n"; + } + for (int row = 0; row < model()->rowCount(); row++) { + if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue; + rowText.clear(); + rowText << model()->data(model()->index(row, PORTS_COL_NAME)).toString(); + rowText << QString("%1/%2") + .arg(model()->data(model()->index(row, PORTS_COL_PORT)).toString()) + .arg(model()->data(model()->index(row, PORTS_COL_PROTOCOL)).toString()); + stream << rowText.join("\t") << "\n"; + } + } else { + // Format as hosts(5) and ethers(5) + if (!selected) { + for (int col = 0; col < model()->columnCount(); col++) { + rowText << model()->headerData(col, Qt::Horizontal).toString(); + } + stream << "# " << rowText.join("\t") << "\n"; + } + for (int row = 0; row < model()->rowCount(); row++) { + if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue; + rowText.clear(); + for (int col = 0; col < model()->columnCount(); col++) { + rowText << model()->data(model()->index(row, col)).toString(); + } + stream << rowText.join("\t") << "\n"; + } + } + } else if (format == EXPORT_CSV) { + for (int col = 0; col < model()->columnCount(); col++) { + rowText << model()->headerData(col, Qt::Horizontal).toString(); + } + if (!selected) { + stream << rowText.join(",") << "\n"; + } + for (int row = 0; row < model()->rowCount(); row++) { + if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue; + rowText.clear(); + for (int col = 0; col < model()->columnCount(); col++) { + QVariant v = model()->data(model()->index(row, col)); + if (!v.isValid()) { + rowText << QStringLiteral("\"\""); + } else if (v.userType() == QMetaType::QString) { + rowText << QString("\"%1\"").arg(v.toString().replace('\"', "\"\"")); + } else { + rowText << v.toString(); + } + } + stream << rowText.join(",") << "\n"; + } + } else if (format == EXPORT_JSON) { + QMap<int, QString> headers; + for (int col = 0; col < model()->columnCount(); col++) + headers.insert(col, model()->headerData(col, Qt::Horizontal, Qt::DisplayRole).toString()); + + QJsonArray records; + + for (int row = 0; row < model()->rowCount(); row++) { + if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue; + QJsonObject rowData; + foreach(int col, headers.keys()) { + QModelIndex idx = model()->index(row, col); + rowData.insert(headers[col], model()->data(idx).toString()); + } + records.push_back(rowData); + } + + QJsonDocument json; + json.setArray(records); + stream << json.toJson(); + } +} diff --git a/ui/qt/widgets/resolved_addresses_view.h b/ui/qt/widgets/resolved_addresses_view.h new file mode 100644 index 00000000..23fb707f --- /dev/null +++ b/ui/qt/widgets/resolved_addresses_view.h @@ -0,0 +1,50 @@ +/** @file + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef RESOLVED_ADDRESSES_VIEW_H +#define RESOLVED_ADDRESSES_VIEW_H + +#include <ui/qt/models/resolved_addresses_models.h> + +#include <QTableView> +#include <QMenu> + +class ResolvedAddressesView : public QTableView +{ + Q_OBJECT + +public: + typedef enum { + EXPORT_TEXT, + EXPORT_CSV, + EXPORT_JSON + } eResolvedAddressesExport; + + ResolvedAddressesView(QWidget *parent = nullptr); + + QMenu* createCopyMenu(bool selected = false, QWidget *parent = nullptr); + +public slots: + void saveAs(); + +protected: + void contextMenuEvent(QContextMenuEvent *e) override; + +private: + QAction *clip_action_; + + AStringListListModel* dataModel() const; + void copyToClipboard(eResolvedAddressesExport format, bool selected); + +private slots: + void clipboardAction(); + void toTextStream(QTextStream &stream, eResolvedAddressesExport format, bool selected = false) const; +}; + +#endif // RESOLVED_ADDRESSES_VIEW_H diff --git a/ui/qt/widgets/rowmove_tree_view.cpp b/ui/qt/widgets/rowmove_tree_view.cpp new file mode 100644 index 00000000..fa91b28e --- /dev/null +++ b/ui/qt/widgets/rowmove_tree_view.cpp @@ -0,0 +1,98 @@ +/* @file + * Tree view that uses the model's moveRows(), if implemented, to + * support internalMoves. The model must also have Qt::MoveAction + * among the supportedDropActions, and its item flags must allow drag + * and drop. + * + * The normal Qt Drag and Drop approach for moves involves inserting a + * new row and removing the original row. That has greater generality, + * but works poorly for views like the I/O Graphs Dialog where a newly + * inserted row would require an expensive retap. + * + * Copyright 2024 John Thacker <johnthacker@gmail.com> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "rowmove_tree_view.h" + +#include <QDropEvent> + +RowMoveTreeView::RowMoveTreeView(QWidget *parent) : TabnavTreeView(parent) +{ + // QTreeViews default to row selection. + // setSelectionMode(QAbstractItemView::ContiguousSelection); + // ContiguousSelection works, but we probably want to make sure + // that the models we use this for can handle removing multiple + // rows (and that the dialogs support doing that.) + setDropIndicatorShown(true); + // We could override dragMoveEvent to have the dropIndicator cover + // the entire row. + setDragDropMode(QAbstractItemView::InternalMove); + // Classes can change this if they also support other drag and drop + // modes. +} + +void RowMoveTreeView::dropEvent(QDropEvent *event) +{ + if (event->source() == this && (event->possibleActions() & Qt::MoveAction) && !event->isAccepted()) { + + const QModelIndexList sourceIndices = selectionModel()->selectedRows(); + + if (sourceIndices.empty()) { + TabnavTreeView::dropEvent(event); + return; + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QModelIndex destIndex = indexAt(event->position().toPoint()); +#else + QModelIndex destIndex = indexAt(event->pos()); +#endif + if (!destIndex.isValid() || destIndex.row() == -1) { + destIndex = model()->index(model()->rowCount() - 1, 0); + } + // dropIndicatorPosition() can be used to determine if we're slightly + // above the item, slightly below the item, on top, or elsewhere in + // the viewPort. We will just use the row number, table-like. + // Note that if we setDragDropOverwriteMode(true) then there wouldn't + // be graphical hints in between rows, but that could cause issues + // if we added non internalMove handling; overriding dragMoveEvent + // could also change it. + + const auto minmaxIndex = std::minmax_element(sourceIndices.begin(), sourceIndices.end(), + [](const QModelIndex &a, const QModelIndex &b) + { return a.row() < b.row(); } + ); + + // Only allow a contiguous selection. (This check is unnecessary + // if the selectionMode is SingleSelection or ContiguousSelection.) + // We could handle multiple ranges with multiple moveRows() calls + // and QPersistentModelIndexes in place of the QModelIndexes, but + // it gets a little confusing, especially if some indices are above + // the target row and some are below (the default behavior would be + // to move all the indices above to immediately below, and vice versa.) + // Microsoft Excel doesn't allow row moves unless the selected + // rows are contiguous, and has an alert. + // + // Note that selectionModel()->selection()->size() is *not* + // guaranteed to be the minimal merged number of possible ranges + // if the selection order was unusual, so we can't just use it. + if ((minmaxIndex.second->row() - minmaxIndex.first->row() + 1) == sourceIndices.size()) { + if (model()->moveRows(QModelIndex(), minmaxIndex.first->row(), static_cast<int>(sourceIndices.size()), QModelIndex(), destIndex.row())) { + // Prevent QAbstractItemView from removing the sourceIndices + // There's an element in the private class (dropEventMoved) + // that QTreeWidget and QTableWidget use via the d-pointer, + // but as long as the action is no longer a MoveAction when + // it returns it won't get removed. + event->setDropAction(Qt::IgnoreAction); + event->accept(); + } + } + } + TabnavTreeView::dropEvent(event); +} diff --git a/ui/qt/widgets/rowmove_tree_view.h b/ui/qt/widgets/rowmove_tree_view.h new file mode 100644 index 00000000..ac415c41 --- /dev/null +++ b/ui/qt/widgets/rowmove_tree_view.h @@ -0,0 +1,35 @@ +/** @file + * + * Tree view that uses a model's moveRows(), if implemented, to support + * internalMoves. + * + * Copyright 2024 John Thacker <johnthacker@gmail.com> + * + * Wireshark - Network traffic analyzer + * By Gerald Combs <gerald@wireshark.org> + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef ROWMOVE_TREE_VIEW_H +#define ROWMOVE_TREE_VIEW_H + +#include <config.h> +#include <ui/qt/widgets/tabnav_tree_view.h> + +/** + * Like QTreeView, but instead of changing to the next row (same column) when + * pressing Tab while editing, change to the next column (same row). + */ +class RowMoveTreeView : public TabnavTreeView +{ + Q_OBJECT + +public: + RowMoveTreeView(QWidget *parent = nullptr); + +protected: + void dropEvent(QDropEvent *event) override; +}; +#endif // ROWMOVE_TREE_VIEW_H diff --git a/ui/qt/widgets/rtp_audio_graph.cpp b/ui/qt/widgets/rtp_audio_graph.cpp index 1340658b..58ff2f1f 100644 --- a/ui/qt/widgets/rtp_audio_graph.cpp +++ b/ui/qt/widgets/rtp_audio_graph.cpp @@ -9,8 +9,6 @@ #include "rtp_audio_graph.h" -#include <glib.h> - #include <epan/prefs.h> #include <ui/qt/utils/color_utils.h> diff --git a/ui/qt/widgets/splash_overlay.h b/ui/qt/widgets/splash_overlay.h index b15d98c4..3f5625fc 100644 --- a/ui/qt/widgets/splash_overlay.h +++ b/ui/qt/widgets/splash_overlay.h @@ -12,8 +12,6 @@ #include <config.h> -#include <glib.h> - #include "epan/register.h" #include <QWidget> diff --git a/ui/qt/widgets/syntax_line_edit.cpp b/ui/qt/widgets/syntax_line_edit.cpp index f2c32142..3e1abe96 100644 --- a/ui/qt/widgets/syntax_line_edit.cpp +++ b/ui/qt/widgets/syntax_line_edit.cpp @@ -9,8 +9,6 @@ #include "config.h" -#include <glib.h> - #include <epan/prefs.h> #include <epan/proto.h> #include <epan/dfilter/dfilter.h> @@ -86,7 +84,7 @@ void SyntaxLineEdit::setSyntaxState(SyntaxState state) { QColor deprecated_bg = ColorUtils::fromColorT(&prefs.gui_text_deprecated); QColor deprecated_fg = ColorUtils::contrastingTextColor(deprecated_bg); - // Try to matche QLineEdit's placeholder text color (which sets the + // Try to match QLineEdit's placeholder text color (which sets the // alpha channel to 50%, which doesn't work in style sheets). // Setting the foreground color lets us avoid yet another background // color preference and should hopefully make things easier to @@ -212,7 +210,7 @@ bool SyntaxLineEdit::checkDisplayFilter(QString filter) * We're being lazy and only printing the first warning. * Would it be better to print all of them? */ - syntax_error_message_ = QString(static_cast<gchar *>(warn->data)); + syntax_error_message_ = QString(static_cast<char *>(warn->data)); } else if (dfp != NULL && (depr = dfilter_deprecated_tokens(dfp)) != NULL) { // You keep using that word. I do not think it means what you think it means. // Possible alternatives: ::Troubled, or ::Problematic maybe? @@ -222,7 +220,7 @@ bool SyntaxLineEdit::checkDisplayFilter(QString filter) * Would it be better to print all of them? */ QString token((const char *)g_ptr_array_index(depr, 0)); - gchar *token_str = qstring_strdup(token.section('.', 0, 0)); + char *token_str = qstring_strdup(token.section('.', 0, 0)); header_field_info *hfi = proto_registrar_get_byalias(token_str); if (hfi) syntax_error_message_ = tr("\"%1\" is deprecated in favour of \"%2\". " @@ -267,10 +265,23 @@ void SyntaxLineEdit::checkCustomColumn(QString fields) return; } - gchar **splitted_fields = g_regex_split_simple(COL_CUSTOM_PRIME_REGEX, - fields.toUtf8().constData(), G_REGEX_ANCHORED, G_REGEX_MATCH_ANCHORED); - - for (guint i = 0; i < g_strv_length(splitted_fields); i++) { +#if 0 + // XXX - Eventually, if the operator we split on is something not supported + // in the filter expression syntax (so that we can distinguish multifield + // concatenation of column strings from a logical OR), we would split and + // then check each split result as a valid display filter. + // For now, any expression that is a valid display filter should work. + // + // We also, for the custom columns, want some of the extra completion + // information from DisplayFilterEdit (like the display filter functions), + // without all of its integration into the main app, but not every user + // of FieldFilterEdit wants that, so perhaps we eventually should have + // another class. + char **splitted_fields = g_regex_split_simple(COL_CUSTOM_PRIME_REGEX, + fields.toUtf8().constData(), (GRegexCompileFlags) G_REGEX_RAW, + (GRegexMatchFlags) 0); + + for (unsigned i = 0; i < g_strv_length(splitted_fields); i++) { if (splitted_fields[i] && *splitted_fields[i]) { if (proto_check_field_name(splitted_fields[i]) != 0) { setSyntaxState(SyntaxLineEdit::Invalid); @@ -280,6 +291,7 @@ void SyntaxLineEdit::checkCustomColumn(QString fields) } } g_strfreev(splitted_fields); +#endif checkDisplayFilter(fields); } diff --git a/ui/qt/widgets/traffic_tab.cpp b/ui/qt/widgets/traffic_tab.cpp index 9913acaa..6233d60d 100644 --- a/ui/qt/widgets/traffic_tab.cpp +++ b/ui/qt/widgets/traffic_tab.cpp @@ -78,7 +78,7 @@ TrafficTab::TrafficTab(QWidget * parent) : TrafficTab::~TrafficTab() {} -void TrafficTab::setProtocolInfo(QString tableName, TrafficTypesList * trafficList, GList ** recentColumnList, ATapModelCallback createModel) +void TrafficTab::setProtocolInfo(QString tableName, TrafficTypesList * trafficList, GList ** recentList, GList ** recentColumnList, ATapModelCallback createModel) { setTabBasename(tableName); @@ -86,6 +86,7 @@ void TrafficTab::setProtocolInfo(QString tableName, TrafficTypesList * trafficLi if (createModel) _createModel = createModel; + _recentList = recentList; _recentColumnList = recentColumnList; setOpenTabs(trafficList->protocols(true)); @@ -159,7 +160,7 @@ void TrafficTab::useAbsoluteTime(bool absolute) { for(int idx = 0; idx < count(); idx++) { - ATapDataModel * atdm = modelForTabIndex(idx); + ATapDataModel * atdm = dataModelForTabIndex(idx); if (atdm) atdm->useAbsoluteTime(absolute); } @@ -169,7 +170,7 @@ void TrafficTab::useNanosecondTimestamps(bool nanoseconds) { for(int idx = 0; idx < count(); idx++) { - ATapDataModel * atdm = modelForTabIndex(idx); + ATapDataModel * atdm = dataModelForTabIndex(idx); if (atdm) atdm->useNanosecondTimestamps(nanoseconds); } @@ -179,7 +180,7 @@ void TrafficTab::disableTap() { for(int idx = 0; idx < count(); idx++) { - ATapDataModel * atdm = modelForTabIndex(idx); + ATapDataModel * atdm = dataModelForTabIndex(idx); if (atdm) atdm->disableTap(); } @@ -260,6 +261,12 @@ void TrafficTab::insertProtoTab(int protoId, bool emitSignals) if (tabId >= 0) tabBar()->setTabData(tabId, storage); + // Identify the last known opened tab + int lastOpened_protoId = -1; + GList *selected_tab = g_list_first(*_recentList); + if (selected_tab != nullptr) { + lastOpened_protoId = proto_get_id_by_short_name((const char *)selected_tab->data); + } /* We reset the correct tab idxs. That operations is costly, but it is only * called during this operation and ensures, that other operations do not @@ -270,6 +277,11 @@ void TrafficTab::insertProtoTab(int protoId, bool emitSignals) _tabs.insert(tabData.protoId(), idx); } + // Restore the last known opened tab + if(lastOpened_protoId == protoId) { + setCurrentIndex(tabId); + } + if (emitSignals) { emit tabsChanged(_tabs.keys()); emit retapRequired(); @@ -329,7 +341,7 @@ QVariant TrafficTab::currentItemData(int role) * to ensure proper handling. Especially ConversationDialog depends on this * method always returning data */ if (!idx.isValid()) { - ATapDataModel * model = modelForTabIndex(currentIndex()); + TrafficDataFilterProxy * model = modelForTabIndex(currentIndex()); idx = model->index(0, 0); } return idx.data(role); @@ -365,7 +377,7 @@ void TrafficTab::modelReset() emit tabDataChanged(tabIdx); } -ATapDataModel * TrafficTab::modelForTabIndex(int tabIdx) +TrafficDataFilterProxy * TrafficTab::modelForTabIndex(int tabIdx) { if (tabIdx == -1) tabIdx = currentIndex(); @@ -373,26 +385,40 @@ ATapDataModel * TrafficTab::modelForTabIndex(int tabIdx) return modelForWidget(widget(tabIdx)); } -ATapDataModel * TrafficTab::modelForWidget(QWidget * searchWidget) +TrafficDataFilterProxy * TrafficTab::modelForWidget(QWidget * searchWidget) { if (qobject_cast<QTreeView *>(searchWidget)) { QTreeView * tree = qobject_cast<QTreeView *>(searchWidget); if (qobject_cast<TrafficDataFilterProxy *>(tree->model())) { - TrafficDataFilterProxy * qsfpm = qobject_cast<TrafficDataFilterProxy *>(tree->model()); - if (qsfpm && qobject_cast<ATapDataModel *>(qsfpm->sourceModel())) { - return qobject_cast<ATapDataModel *>(qsfpm->sourceModel()); - } + return qobject_cast<TrafficDataFilterProxy *>(tree->model()); } } return nullptr; } +ATapDataModel * TrafficTab::dataModelForTabIndex(int tabIdx) +{ + if (tabIdx == -1) + tabIdx = currentIndex(); + + return dataModelForWidget(widget(tabIdx)); +} + +ATapDataModel * TrafficTab::dataModelForWidget(QWidget * searchWidget) +{ + TrafficDataFilterProxy * qsfpm = modelForWidget(searchWidget); + if (qsfpm && qobject_cast<ATapDataModel *>(qsfpm->sourceModel())) { + return qobject_cast<ATapDataModel *>(qsfpm->sourceModel()); + } + return nullptr; +} + void TrafficTab::setFilter(QString filter) { for (int idx = 0; idx < count(); idx++ ) { - ATapDataModel * atdm = modelForTabIndex(idx); + ATapDataModel * atdm = dataModelForTabIndex(idx); if (! atdm) continue; atdm->setFilter(filter); @@ -406,7 +432,7 @@ void TrafficTab::setNameResolution(bool checked) for (int idx = 0; idx < count(); idx++ ) { - ATapDataModel * atdm = modelForTabIndex(idx); + ATapDataModel * atdm = dataModelForTabIndex(idx); if (! atdm) continue; atdm->setResolveNames(checked); @@ -422,7 +448,7 @@ void TrafficTab::setNameResolution(bool checked) bool TrafficTab::hasNameResolution(int tabIdx) { int tab = tabIdx == -1 || tabIdx >= count() ? currentIndex() : tabIdx; - ATapDataModel * dataModel = modelForTabIndex(tab); + ATapDataModel * dataModel = dataModelForTabIndex(tab); if (! dataModel) return false; @@ -443,12 +469,12 @@ bool TrafficTab::hasGeoIPData(int tabIdx) { int tab = tabIdx == -1 || tabIdx >= count() ? currentIndex() : tabIdx; - ATapDataModel * dataModel = modelForTabIndex(tab); + ATapDataModel * dataModel = dataModelForTabIndex(tab); return dataModel->hasGeoIPData(); } bool -TrafficTab::writeGeoIPMapFile(QFile * fp, bool json_only, ATapDataModel * dataModel) +TrafficTab::writeGeoIPMapFile(QFile * fp, bool json_only, TrafficDataFilterProxy * model) { QTextStream out(fp); @@ -508,14 +534,15 @@ TrafficTab::writeGeoIPMapFile(QFile * fp, bool json_only, ATapDataModel * dataMo QJsonArray features; /* Append map data. */ - for(int row = 0; row < dataModel->rowCount(QModelIndex()); row++) + for(int row = 0; row < model->rowCount(QModelIndex()); row++) { - QModelIndex index = dataModel->index(row, 0); + QModelIndex index = model->mapToSource(model->index(row, 0)); + ATapDataModel *dataModel = qobject_cast<ATapDataModel *>(model->sourceModel()); const mmdb_lookup_t * result = VariantPointer<const mmdb_lookup_t>::asPtr(dataModel->data(index, ATapDataModel::GEODATA_LOOKUPTABLE)); if (!maxmind_db_has_coords(result)) { // result could be NULL if the caller did not trigger a lookup - // before. result->found could be FALSE if no MMDB entry exists. + // before. result->found could be false if no MMDB entry exists. continue; } @@ -545,8 +572,8 @@ TrafficTab::writeGeoIPMapFile(QFile * fp, bool json_only, ATapDataModel * dataMo if (qobject_cast<EndpointDataModel *>(dataModel)) { EndpointDataModel * endpointModel = qobject_cast<EndpointDataModel *>(dataModel); - property["packets"] = endpointModel->data(endpointModel->index(row, EndpointDataModel::ENDP_COLUMN_PACKETS)).toString(); - property["bytes"] = endpointModel->data(endpointModel->index(row, EndpointDataModel::ENDP_COLUMN_BYTES)).toString(); + property["packets"] = endpointModel->data(index.siblingAtColumn(EndpointDataModel::ENDP_COLUMN_PACKETS)).toString(); + property["bytes"] = endpointModel->data(index.siblingAtColumn(EndpointDataModel::ENDP_COLUMN_BYTES)).toString(); } arrEntry["properties"] = property; features.append(arrEntry); @@ -568,7 +595,7 @@ TrafficTab::writeGeoIPMapFile(QFile * fp, bool json_only, ATapDataModel * dataMo QUrl TrafficTab::createGeoIPMap(bool json_only, int tabIdx) { int tab = tabIdx == -1 || tabIdx >= count() ? currentIndex() : tabIdx; - ATapDataModel * dataModel = modelForTabIndex(tab); + ATapDataModel * dataModel = dataModelForTabIndex(tab); if (! (dataModel && dataModel->hasGeoIPData())) { QMessageBox::warning(this, tr("Map file error"), tr("No endpoints available to map")); return QUrl(); @@ -581,7 +608,7 @@ QUrl TrafficTab::createGeoIPMap(bool json_only, int tabIdx) return QUrl(); } - if (!writeGeoIPMapFile(&tf, json_only, dataModel)) { + if (!writeGeoIPMapFile(&tf, json_only, modelForTabIndex(tab))) { tf.close(); return QUrl(); } @@ -592,7 +619,7 @@ QUrl TrafficTab::createGeoIPMap(bool json_only, int tabIdx) #endif void TrafficTab::detachTab(int tabIdx, QPoint pos) { - ATapDataModel * model = modelForTabIndex(tabIdx); + ATapDataModel * model = dataModelForTabIndex(tabIdx); if (!model) return; @@ -608,7 +635,7 @@ void TrafficTab::detachTab(int tabIdx, QPoint pos) { void TrafficTab::attachTab(QWidget * content, QString name) { - ATapDataModel * model = modelForWidget(content); + ATapDataModel * model = dataModelForWidget(content); if (!model) { attachTab(content, name); return; diff --git a/ui/qt/widgets/traffic_tab.h b/ui/qt/widgets/traffic_tab.h index 71c48ae5..ff0bacc5 100644 --- a/ui/qt/widgets/traffic_tab.h +++ b/ui/qt/widgets/traffic_tab.h @@ -12,10 +12,9 @@ #include "config.h" -#include <glib.h> - #include <ui/qt/models/atap_data_model.h> #include <ui/qt/filter_action.h> +#include <ui/qt/widgets/traffic_tree.h> #include <ui/qt/widgets/detachable_tabwidget.h> #include <ui/qt/widgets/traffic_types_list.h> @@ -94,7 +93,7 @@ public: * * @see ATapModelCallback */ - void setProtocolInfo(QString tableName, TrafficTypesList * trafficList, GList ** recentColumnList, ATapModelCallback createModel); + void setProtocolInfo(QString tableName, TrafficTypesList * trafficList, GList ** recentList, GList ** recentColumnList, ATapModelCallback createModel); /** * @brief Set the Delegate object for the tab. It will apply for all @@ -221,20 +220,23 @@ private: QMap<int, int> _tabs; ATapModelCallback _createModel; ATapCreateDelegate _createDelegate; + GList ** _recentList; GList ** _recentColumnList; bool _disableTaps; bool _nameResolution; QTreeView * createTree(int protoId); - ATapDataModel * modelForTabIndex(int tabIdx = -1); - ATapDataModel * modelForWidget(QWidget * widget); + TrafficDataFilterProxy * modelForTabIndex(int tabIdx = -1); + TrafficDataFilterProxy * modelForWidget(QWidget * widget); + ATapDataModel * dataModelForTabIndex(int tabIdx = -1); + ATapDataModel * dataModelForWidget(QWidget * widget); void insertProtoTab(int protoId, bool emitSignals = true); void removeProtoTab(int protoId, bool emitSignals = true); #ifdef HAVE_MAXMINDDB - bool writeGeoIPMapFile(QFile * fp, bool json_only, ATapDataModel * dataModel); + bool writeGeoIPMapFile(QFile * fp, bool json_only, TrafficDataFilterProxy * model); #endif private slots: diff --git a/ui/qt/widgets/traffic_tree.cpp b/ui/qt/widgets/traffic_tree.cpp index be154d7d..4325e556 100644 --- a/ui/qt/widgets/traffic_tree.cpp +++ b/ui/qt/widgets/traffic_tree.cpp @@ -204,7 +204,7 @@ void TrafficTreeHeaderView::columnTriggered(bool checked) for (int col = 0; col < tree->dataModel()->columnCount(); col++) { if (proxy->columnVisible(col)) { visible << col; - gchar *nr = qstring_strdup(QString::number(col)); + char *nr = qstring_strdup(QString::number(col)); *_recentColumnList = g_list_append(*_recentColumnList, nr); } } @@ -473,7 +473,35 @@ bool TrafficDataFilterProxy::lessThan(const QModelIndex &source_left, const QMod int addressTypeA = model->data(source_left, ATapDataModel::DATA_ADDRESS_TYPE).toInt(); int addressTypeB = model->data(source_right, ATapDataModel::DATA_ADDRESS_TYPE).toInt(); if (addressTypeA != 0 && addressTypeB != 0 && addressTypeA != addressTypeB) { - result = addressTypeA < addressTypeB; + + /* Handle subnets when they are compared to IP addresses */ + if ( (addressTypeA == AT_STRINGZ) && (addressTypeB == AT_IPv4) ) { + QString subnet = datA.toString(); + qint64 lpart = subnet.indexOf("/"); + ws_in4_addr ip4addr; + + if(ws_inet_pton4(subnet.left(lpart).toUtf8().data(), &ip4addr)) { + quint32 valA = g_ntohl(ip4addr); + quint32 valB = model->data(source_right, ATapDataModel::DATA_IPV4_INTEGER).value<quint32>(); + result = valA < valB; + identical = valA == valB; + } + // else: never supposed to happen + } else if ( (addressTypeA == AT_IPv4) && (addressTypeB == AT_STRINGZ) ) { + QString subnet = datB.toString(); + qint64 lpart = subnet.indexOf("/"); + ws_in4_addr ip4addr; + if(ws_inet_pton4(subnet.left(lpart).toUtf8().data(), &ip4addr)) { + quint32 valA = model->data(source_left, ATapDataModel::DATA_IPV4_INTEGER).value<quint32>(); + quint32 valB = g_ntohl(ip4addr); + result = valA < valB; + identical = valA == valB; + } + // else: never supposed to happen + } else { + result = addressTypeA < addressTypeB; + } + } else if (addressTypeA != 0 && addressTypeA == addressTypeB) { if (addressTypeA == AT_IPv4) { @@ -693,8 +721,28 @@ QMenu * TrafficTree::createActionSubMenu(FilterAction::Action cur_action, QModel foreach (FilterAction::ActionType at, FilterAction::actionTypes()) { if (isConversation && conv_item) { QMenu *subsubmenu = subMenu->addMenu(FilterAction::actionTypeName(at)); - if (hasConvId && (cur_action == FilterAction::ActionApply || cur_action == FilterAction::ActionPrepare)) { - QString filter = QString("%1.stream eq %2").arg(conv_item->ctype == CONVERSATION_TCP ? "tcp" : "udp").arg(conv_item->conv_id); + + /* For IP, ensure subnets-like conversations won't enable Stream ID filters (!CONV_ID_UNSET) */ + if (hasConvId && (conv_item->conv_id!=CONV_ID_UNSET) && (cur_action == FilterAction::ActionApply || cur_action == FilterAction::ActionPrepare)) { + QString filter; + switch (conv_item->ctype) { + case CONVERSATION_TCP: + filter = QString("%1.stream eq %2").arg("tcp").arg(conv_item->conv_id); + break; + case CONVERSATION_UDP: + filter = QString("%1.stream eq %2").arg("udp").arg(conv_item->conv_id); + break; + case CONVERSATION_IP: + filter = QString("%1.stream eq %2").arg("ip").arg(conv_item->conv_id); + break; + case CONVERSATION_IPV6: + filter = QString("%1.stream eq %2").arg("ipv6").arg(conv_item->conv_id); + break; + case CONVERSATION_ETH: + default: + filter = QString("%1.stream eq %2").arg("eth").arg(conv_item->conv_id); + break; + } FilterAction * act = new FilterAction(subsubmenu, cur_action, at, tr("Filter on stream id")); act->setProperty("filter", filter); subsubmenu->addAction(act); diff --git a/ui/qt/widgets/traffic_tree.h b/ui/qt/widgets/traffic_tree.h index 5bc87e91..00d91783 100644 --- a/ui/qt/widgets/traffic_tree.h +++ b/ui/qt/widgets/traffic_tree.h @@ -12,8 +12,6 @@ #include "config.h" -#include <glib.h> - #include <ui/recent.h> #include <ui/qt/models/atap_data_model.h> diff --git a/ui/qt/widgets/traffic_types_list.cpp b/ui/qt/widgets/traffic_types_list.cpp index 30126ef9..ddbe34a3 100644 --- a/ui/qt/widgets/traffic_types_list.cpp +++ b/ui/qt/widgets/traffic_types_list.cpp @@ -9,9 +9,8 @@ #include "config.h" -#include <glib.h> - #include <epan/conversation_table.h> +#include <epan/prefs.h> #include <ui/qt/widgets/traffic_types_list.h> @@ -48,12 +47,12 @@ static bool iterateProtocols(const void *key, void *value, void *userdata) QList<TrafficTypesRowData> * protocols = (QList<TrafficTypesRowData> *)userdata; register_ct_t* ct = (register_ct_t*)value; - const QString title = (const gchar*)key; + const QString title = (const char*)key; int proto_id = get_conversation_proto_id(ct); TrafficTypesRowData entry(proto_id, title); protocols->append(entry); - return FALSE; + return false; } TrafficTypesModel::TrafficTypesModel(GList ** recentList, QObject *parent) : @@ -152,6 +151,26 @@ bool TrafficTypesModel::setData(const QModelIndex &idx, const QVariant &value, i if (_allTaps.count() <= idx.row()) return false; + // When updating the tabs, save the current selection, it will be restored below + GList *selected_tab = g_list_first(*_recentList); + int rct_protoId = -1; + if (selected_tab != nullptr) { + rct_protoId = proto_get_id_by_short_name((const char *)selected_tab->data); + + // Did the user just uncheck the current selection? + if (_allTaps[idx.row()].protocol() == rct_protoId && value.toInt() == Qt::Unchecked) { + // Yes. The code below will restore it. Rather than removing it, + // resetting the model, and then adding it back, just return. + // The user might want to uncheck the current selection, in which + // case the code needs to changed to handle that. + // + // Note that not allowing the current first tab to be unselected does + // have the advantage of preventing a crash from having no tabs + // selected in the Endpoint dialog (#18250). + return false; + } + } + _allTaps[idx.row()].setChecked(value.toInt() == Qt::Checked); QList<int> selected; @@ -161,12 +180,21 @@ bool TrafficTypesModel::setData(const QModelIndex &idx, const QVariant &value, i for (int cnt = 0; cnt < _allTaps.count(); cnt++) { if (_allTaps[cnt].checked()) { int protoId = _allTaps[cnt].protocol(); - selected.append(protoId); - char *title = g_strdup(proto_get_protocol_short_name(find_protocol_by_id(protoId))); - *_recentList = g_list_append(*_recentList, title); + if(protoId != rct_protoId) { + selected.append(protoId); + char *title = g_strdup(proto_get_protocol_short_name(find_protocol_by_id(protoId))); + *_recentList = g_list_append(*_recentList, title); + } } } + if (rct_protoId != -1) { + // restore the selection by prepending it to the recent list + char *rct_title = g_strdup(proto_get_protocol_short_name(find_protocol_by_id(rct_protoId))); + selected.prepend(rct_protoId); + *_recentList = g_list_prepend(*_recentList, rct_title); + } + emit protocolsChanged(selected); emit dataChanged(idx, idx); diff --git a/ui/qt/widgets/traffic_types_list.h b/ui/qt/widgets/traffic_types_list.h index 00798c50..2666eb78 100644 --- a/ui/qt/widgets/traffic_types_list.h +++ b/ui/qt/widgets/traffic_types_list.h @@ -12,8 +12,6 @@ #include "config.h" -#include <glib.h> - #include <QTreeView> #include <QAbstractListModel> #include <QMap> @@ -122,4 +120,4 @@ private: TrafficListSortModel * _sortModel; }; -#endif // TRAFFIC_TYPES_LIST_H
\ No newline at end of file +#endif // TRAFFIC_TYPES_LIST_H diff --git a/ui/qt/widgets/wireless_timeline.cpp b/ui/qt/widgets/wireless_timeline.cpp index dcdcd4f8..87eb1079 100644 --- a/ui/qt/widgets/wireless_timeline.cpp +++ b/ui/qt/widgets/wireless_timeline.cpp @@ -75,7 +75,7 @@ static void reset_rgb(float rgb[TIMELINE_HEIGHT][3]) rgb[i][0] = rgb[i][1] = rgb[i][2] = 1.0; } -static void render_pixels(QPainter &p, gint x, gint width, float rgb[TIMELINE_HEIGHT][3], float ratio) +static void render_pixels(QPainter &p, int x, int width, float rgb[TIMELINE_HEIGHT][3], float ratio) { int previous = 0, i; for (i = 1; i <= TIMELINE_HEIGHT; i++) { @@ -92,7 +92,7 @@ static void render_pixels(QPainter &p, gint x, gint width, float rgb[TIMELINE_HE reset_rgb(rgb); } -static void render_rectangle(QPainter &p, gint x, gint width, guint height, int dfilter, float r, float g, float b, float ratio) +static void render_rectangle(QPainter &p, int x, int width, unsigned height, int dfilter, float r, float g, float b, float ratio) { p.fillRect(QRectF(x/ratio, TIMELINE_HEIGHT/2-height, width/ratio, dfilter ? height * 2 : height), pcolor(r,g,b)); } @@ -157,29 +157,25 @@ void WirelessTimeline::mouseReleaseEvent(QMouseEvent *event) return; /* this was a click */ - guint num = find_packet(localPos.x()); + unsigned num = find_packet(localPos.x()); if (num == 0) return; - frame_data *fdata = frame_data_sequence_find(cfile.provider.frames, num); - if (!fdata->passed_dfilter && fdata->prev_dis_num > 0) - num = fdata->prev_dis_num; - - cf_goto_frame(&cfile, num); + cf_goto_frame(&cfile, num, false); } void WirelessTimeline::clip_tsf() { // did we go past the start of the file? - if (((gint64) start_tsf) < ((gint64) first->start_tsf)) { + if (((int64_t) start_tsf) < ((int64_t) first->start_tsf)) { // align the start of the file at the left edge - guint64 shift = first->start_tsf - start_tsf; + uint64_t shift = first->start_tsf - start_tsf; start_tsf += shift; end_tsf += shift; } if (end_tsf > last->end_tsf) { - guint64 shift = end_tsf - last->end_tsf; + uint64_t shift = end_tsf - last->end_tsf; start_tsf -= shift; end_tsf -= shift; } @@ -194,32 +190,32 @@ void WirelessTimeline::selectedFrameChanged(QList<int>) if (cfile.current_frame) { struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num); - guint left_margin = 0.9 * start_tsf + 0.1 * end_tsf; - guint right_margin = 0.1 * start_tsf + 0.9 * end_tsf; - guint64 half_window = (end_tsf - start_tsf)/2; + unsigned left_margin = 0.9 * start_tsf + 0.1 * end_tsf; + unsigned right_margin = 0.1 * start_tsf + 0.9 * end_tsf; + uint64_t half_window = (end_tsf - start_tsf)/2; if (wr) { // are we to the left of the left margin? if (wr->start_tsf < left_margin) { // scroll the left edge back to the left margin - guint64 offset = left_margin - wr->start_tsf; + uint64_t offset = left_margin - wr->start_tsf; if (offset < half_window) { // small movement; keep packet to margin start_tsf -= offset; end_tsf -= offset; } else { // large movement; move packet to center of window - guint64 center = (wr->start_tsf + wr->end_tsf)/2; + uint64_t center = (wr->start_tsf + wr->end_tsf)/2; start_tsf = center - half_window; end_tsf = center + half_window; } } else if (wr->end_tsf > right_margin) { - guint64 offset = wr->end_tsf - right_margin; + uint64_t offset = wr->end_tsf - right_margin; if (offset < half_window) { start_tsf += offset; end_tsf += offset; } else { - guint64 center = (wr->start_tsf + wr->end_tsf)/2; + uint64_t center = (wr->start_tsf + wr->end_tsf)/2; start_tsf = center - half_window; end_tsf = center + half_window; } @@ -234,10 +230,10 @@ void WirelessTimeline::selectedFrameChanged(QList<int>) /* given an x position find which packet that corresponds to. * if it's inter frame space the subsequent packet is returned */ -guint +unsigned WirelessTimeline::find_packet(qreal x_position) { - guint64 x_time = start_tsf + (x_position/width() * (end_tsf - start_tsf)); + uint64_t x_time = start_tsf + (x_position/width() * (end_tsf - start_tsf)); return find_packet_tsf(x_time); } @@ -267,7 +263,7 @@ void WirelessTimeline::captureFileReadFinished() */ /* TODO: update GUI to handle captures with occasional frames missing TSF data */ /* TODO: indicate error message to the user */ - for (guint32 n = 1; n < cfile.count; n++) { + for (uint32_t n = 1; n < cfile.count; n++) { struct wlan_radio *w = get_wlan_radio(n); if (w->start_tsf == 0 || w->end_tsf == 0) { QString err = tr("Packet number %1 does not include TSF timestamp, not showing timeline.").arg(n); @@ -315,11 +311,11 @@ void WirelessTimeline::resizeEvent(QResizeEvent*) // Calculate the x position on the GUI from the timestamp -int WirelessTimeline::position(guint64 tsf, float ratio) +int WirelessTimeline::position(uint64_t tsf, float ratio) { int position = -100; - if (tsf != G_MAXUINT64) { + if (tsf != UINT64_MAX) { position = ((double) tsf - start_tsf)*width()*ratio/(end_tsf-start_tsf); } return position; @@ -378,11 +374,11 @@ tap_packet_status WirelessTimeline::tap_timeline_packet(void *tapdata, packet_in const struct wlan_radio *wlan_radio_info = (const struct wlan_radio *)data; /* Save the radio information in our own (GUI) hashtable */ - g_hash_table_insert(timeline->radio_packet_list, GUINT_TO_POINTER(pinfo->num), (gpointer)wlan_radio_info); + g_hash_table_insert(timeline->radio_packet_list, GUINT_TO_POINTER(pinfo->num), (void *)wlan_radio_info); return TAP_PACKET_DONT_REDRAW; } -struct wlan_radio* WirelessTimeline::get_wlan_radio(guint32 packet_num) +struct wlan_radio* WirelessTimeline::get_wlan_radio(uint32_t packet_num) { return (struct wlan_radio*)g_hash_table_lookup(radio_packet_list, GUINT_TO_POINTER(packet_num)); } @@ -402,7 +398,7 @@ bool WirelessTimeline::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event); - guint packet = find_packet(helpEvent->pos().x()); + unsigned packet = find_packet(helpEvent->pos().x()); if (packet) { doToolTip(get_wlan_radio(packet), helpEvent->globalPos(), helpEvent->x()); } else { @@ -452,16 +448,16 @@ void WirelessTimeline::bgColorizationProgress(int first, int last) void WirelessTimeline::zoom(double x_fraction) { /* adjust the zoom around the selected packet */ - guint64 file_range = last->end_tsf - first->start_tsf; - guint64 center = start_tsf + x_fraction * (end_tsf - start_tsf); - guint64 span = pow(file_range, 1.0 - zoom_level / TIMELINE_MAX_ZOOM); + uint64_t file_range = last->end_tsf - first->start_tsf; + uint64_t center = start_tsf + x_fraction * (end_tsf - start_tsf); + uint64_t span = pow(file_range, 1.0 - zoom_level / TIMELINE_MAX_ZOOM); start_tsf = center - span * x_fraction; end_tsf = center + span * (1.0 - x_fraction); clip_tsf(); update(); } -int WirelessTimeline::find_packet_tsf(guint64 tsf) +int WirelessTimeline::find_packet_tsf(uint64_t tsf) { if (cfile.count < 1) return 0; @@ -469,11 +465,11 @@ int WirelessTimeline::find_packet_tsf(guint64 tsf) if (cfile.count < 2) return 1; - guint32 min_count = 1; - guint32 max_count = cfile.count-1; + uint32_t min_count = 1; + uint32_t max_count = cfile.count-1; - guint64 min_tsf = get_wlan_radio(min_count)->end_tsf; - guint64 max_tsf = get_wlan_radio(max_count)->end_tsf; + uint64_t min_tsf = get_wlan_radio(min_count)->end_tsf; + uint64_t max_tsf = get_wlan_radio(max_count)->end_tsf; for (;;) { if (tsf >= max_tsf) @@ -482,11 +478,11 @@ int WirelessTimeline::find_packet_tsf(guint64 tsf) if (tsf < min_tsf) return min_count; - guint32 middle = (min_count + max_count)/2; + uint32_t middle = (min_count + max_count)/2; if (middle == min_count) return middle+1; - guint64 middle_tsf = get_wlan_radio(middle)->end_tsf; + uint64_t middle_tsf = get_wlan_radio(middle)->end_tsf; if (tsf >= middle_tsf) { min_count = middle; @@ -550,9 +546,9 @@ WirelessTimeline::paintEvent(QPaintEvent *qpe) if (ri == NULL) continue; - gint8 rssi = ri->aggregate ? ri->aggregate->rssi : ri->rssi; - guint height = (rssi+100)/2; - gint end_nav; + int8_t rssi = ri->aggregate ? ri->aggregate->rssi : ri->rssi; + unsigned height = (rssi+100)/2; + int end_nav; /* leave a margin above the packets so the selected packet can be seen */ if (height > TIMELINE_HEIGHT/2-6) @@ -567,7 +563,7 @@ WirelessTimeline::paintEvent(QPaintEvent *qpe) if (ri->start_tsf == 0 || ri->end_tsf == 0) continue; - x = ((gint64) (ri->start_tsf - start_tsf))*zoom; + x = ((int64_t) (ri->start_tsf - start_tsf))*zoom; /* is there a previous anti-aliased pixel to output */ if (last_x >= 0 && ((int) x) != last_x) { /* write it out now */ @@ -606,7 +602,7 @@ WirelessTimeline::paintEvent(QPaintEvent *qpe) /* record NAV field at higher magnifications */ end_nav = x + width + ri->nav*zoom; if (zoom >= 0.01 && ri->nav && end_nav > 0) { - gint y = 2*(packet % (TIMELINE_HEIGHT/2)); + int y = 2*(packet % (TIMELINE_HEIGHT/2)); qs.addLine(QLineF((x+width)/ratio, y, end_nav/ratio, y), QPen(pcolor(red,green,blue))); } diff --git a/ui/qt/widgets/wireless_timeline.h b/ui/qt/widgets/wireless_timeline.h index de43d123..cf67ff4e 100644 --- a/ui/qt/widgets/wireless_timeline.h +++ b/ui/qt/widgets/wireless_timeline.h @@ -21,8 +21,6 @@ #include <config.h> -#include <glib.h> - #include "file.h" #include "ui/ws_ui_util.h" @@ -75,21 +73,21 @@ protected: static void tap_timeline_reset(void* tapdata); static tap_packet_status tap_timeline_packet(void *tapdata, packet_info* pinfo, epan_dissect_t* edt, const void *data, tap_flags_t flags); - struct wlan_radio* get_wlan_radio(guint32 packet_num); + struct wlan_radio* get_wlan_radio(uint32_t packet_num); void clip_tsf(); - int position(guint64 tsf, float ratio); - int find_packet_tsf(guint64 tsf); + int position(uint64_t tsf, float ratio); + int find_packet_tsf(uint64_t tsf); void doToolTip(struct wlan_radio *wr, QPoint pos, int x); void zoom(double x_fraction); double zoom_level; qreal start_x, last_x; PacketList *packet_list; - guint find_packet(qreal x); + unsigned find_packet(qreal x); float rgb[TIMELINE_HEIGHT][3]; - guint64 start_tsf; - guint64 end_tsf; + uint64_t start_tsf; + uint64_t end_tsf; int first_packet; /* first packet displayed */ struct wlan_radio *first, *last; capture_file *capfile; diff --git a/ui/qt/widgets/wireshark_file_dialog.cpp b/ui/qt/widgets/wireshark_file_dialog.cpp index acf9c7db..3c5b8d3e 100644 --- a/ui/qt/widgets/wireshark_file_dialog.cpp +++ b/ui/qt/widgets/wireshark_file_dialog.cpp @@ -43,6 +43,15 @@ WiresharkFileDialog::WiresharkFileDialog(QWidget *parent, const QString &caption #endif } +QString WiresharkFileDialog::selectedNativePath() const +{ + if (selectedFiles().isEmpty()) { + // The API implies this can't happen + return QString(); + } + return QDir::toNativeSeparators(selectedFiles().at(0)); +} + QString WiresharkFileDialog::getExistingDirectory(QWidget *parent, const QString &caption, const QString &dir, Options options) { #ifdef Q_OS_WIN @@ -52,7 +61,7 @@ QString WiresharkFileDialog::getExistingDirectory(QWidget *parent, const QString #ifdef Q_OS_WIN revert_thread_per_monitor_v2_awareness(da_ctx); #endif - return ed; + return QDir::toNativeSeparators(ed); } QString WiresharkFileDialog::getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options) @@ -64,7 +73,7 @@ QString WiresharkFileDialog::getOpenFileName(QWidget *parent, const QString &cap #ifdef Q_OS_WIN revert_thread_per_monitor_v2_awareness(da_ctx); #endif - return ofn; + return QDir::toNativeSeparators(ofn); } QString WiresharkFileDialog::getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options) @@ -76,5 +85,5 @@ QString WiresharkFileDialog::getSaveFileName(QWidget *parent, const QString &cap #ifdef Q_OS_WIN revert_thread_per_monitor_v2_awareness(da_ctx); #endif - return sfn; + return QDir::toNativeSeparators(sfn); } diff --git a/ui/qt/widgets/wireshark_file_dialog.h b/ui/qt/widgets/wireshark_file_dialog.h index 43ac67a6..7d452b3d 100644 --- a/ui/qt/widgets/wireshark_file_dialog.h +++ b/ui/qt/widgets/wireshark_file_dialog.h @@ -15,6 +15,11 @@ /** * @brief The WiresharkFileDialog class * + * Qt uses '/' as a universal path separator and converts to native path + * separators, i.e., '\' on Windows, only immediately before displaying a + * path to a user. This class can return the path with native path + * separators. + * * Qt <= 5.9 supports setting old (Windows 8.1) per-monitor DPI awareness * via Qt:AA_EnableHighDpiScaling. We do this in main.cpp. In order for * native dialogs to be rendered correctly we need to set per-monitor @@ -23,13 +28,15 @@ * we need to revert our thread context when we're done. * The class functions below are simple wrappers around their QFileDialog * equivalents that set PMv2 awareness before showing native dialogs on - * Windows and resets it afterward. + * Windows and resets it afterward. They also return the result with native + * directory separators on Windows. */ class WiresharkFileDialog : public QFileDialog { public: WiresharkFileDialog(QWidget *parent = nullptr, const QString &caption = QString(), const QString &directory = QString(), const QString &filter = QString()); + QString selectedNativePath() const; static QString getExistingDirectory(QWidget *parent = Q_NULLPTR, const QString &caption = QString(), const QString &dir = QString(), Options options = ShowDirsOnly); static QString getOpenFileName(QWidget *parent = Q_NULLPTR, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = Q_NULLPTR, Options options = Options()); static QString getSaveFileName(QWidget *parent = Q_NULLPTR, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = Q_NULLPTR, Options options = Options()); |