From af054591c31fdccd8661c87bb3d28cdeb3077db3 Mon Sep 17 00:00:00 2001 From: Gerald Combs Date: Tue, 19 May 2015 10:18:54 -0700 Subject: [PATCH] Add capture filter autocompletion. Autocomplete on recent and saved capture filters along with the primitives in gencode.l in the libpcap sources. Move common autocomplete code to SyntaxLineEdit. Change-Id: I0931a6775bacf9c917c294befbbdaade51d19b93 Reviewed-on: https://code.wireshark.org/review/8542 Petri-Dish: Gerald Combs Tested-by: Petri Dish Buildbot Reviewed-by: Gerald Combs --- ui/qt/capture_filter_combo.cpp | 7 +- ui/qt/capture_filter_edit.cpp | 73 ++++++++++++++++- ui/qt/capture_filter_edit.h | 6 +- ui/qt/display_filter_edit.cpp | 143 ++------------------------------- ui/qt/display_filter_edit.h | 17 +--- ui/qt/syntax_line_edit.cpp | 139 +++++++++++++++++++++++++++++++- ui/qt/syntax_line_edit.h | 26 ++++++ 7 files changed, 250 insertions(+), 161 deletions(-) diff --git a/ui/qt/capture_filter_combo.cpp b/ui/qt/capture_filter_combo.cpp index b2040b3d7c..9bc21a9471 100644 --- a/ui/qt/capture_filter_combo.cpp +++ b/ui/qt/capture_filter_combo.cpp @@ -30,8 +30,6 @@ #include "capture_filter_combo.h" #include "wireshark_application.h" -#include - CaptureFilterCombo::CaptureFilterCombo(QWidget *parent) : QComboBox(parent), cf_edit_(NULL) @@ -39,6 +37,10 @@ CaptureFilterCombo::CaptureFilterCombo(QWidget *parent) : cf_edit_ = new CaptureFilterEdit(this, true); setEditable(true); + // Enabling autocompletion here gives us two simultaneous completions: + // Inline (highlighted text) for entire filters, handled here and popup + // completion for fields handled by CaptureFilterEdit. + setAutoCompletion(false); setLineEdit(cf_edit_); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); setInsertPolicy(QComboBox::NoInsert); @@ -72,7 +74,6 @@ CaptureFilterCombo::CaptureFilterCombo(QWidget *parent) : " left: 1px;" "}" ); - completer()->setCompletionMode(QCompleter::PopupCompletion); connect(this, SIGNAL(interfacesChanged()), cf_edit_, SLOT(checkFilter())); connect(cf_edit_, SIGNAL(pushFilterSyntaxStatus(const QString&)), diff --git a/ui/qt/capture_filter_edit.cpp b/ui/qt/capture_filter_edit.cpp index df54093646..ce84b9fc66 100644 --- a/ui/qt/capture_filter_edit.cpp +++ b/ui/qt/capture_filter_edit.cpp @@ -26,17 +26,27 @@ #include #include "capture_opts.h" -#include "ui/capture_globals.h" + +#include +#include +#include #include "capture_filter_edit.h" #include "wireshark_application.h" +#include +#include #include +#include #include -#include "ui/utf8_entities.h" #include "qt_ui_utils.h" +// To do: +// - This duplicates some DisplayFilterEdit code. +// - We need simplified (button- and dropdown-free) versions for use in dialogs and field-only checking. + + #if defined(Q_OS_MAC) && 0 // http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSImage_Class/Reference/Reference.html // http://www.virtualbox.org/svn/vbox/trunk/src/VBox/Frontends/VirtualBox/src/platform/darwin/UICocoaSpecialControls.mm @@ -72,8 +82,26 @@ UIMiniCancelButton::UIMiniCancelButton(QWidget *pParent /* = 0 */) #endif +static const QString libpcap_primitive_chars_ = "-0123456789abcdefghijklmnopqrstuvwxyz"; -// XXX - We need simplified (button- and dropdown-free) versions for use in dialogs and field-only checking. +// grep '^[a-z].*return [A-Z].*;$' scanner.l | awk '{gsub(/\|/, "\n") ; print " << \"" $1 "\""}' | sort +static const QStringList libpcap_primitives_ = QStringList() + << "aarp" << "action" << "address1" << "address2" << "address3" << "address4" + << "ah" << "and" << "arp" << "atalk" << "bcc" << "broadcast" << "byte" << "carp" + << "clnp" << "connectmsg" << "csnp" << "decnet" << "direction" << "dpc" + << "dst" << "es-is" << "esis" << "esp" << "fddi" << "fisu" << "gateway" + << "greater" << "hdpc" << "hfisu" << "hlssu" << "hmsu" << "hopc" << "host" + << "hsio" << "hsls" << "icmp" << "icmp6" << "igmp" << "igrp" << "iih" << "ilmic" + << "inbound" << "ip" << "ip6" << "ipx" << "is-is" << "isis" << "iso" << "l1" + << "l2" << "lane" << "lat" << "len" << "less" << "link" << "llc" << "lsp" + << "lssu" << "lsu" << "mask" << "metac" << "metaconnect" << "mopdl" << "moprc" + << "mpls" << "msu" << "multicast" << "net" << "netbeui" << "oam" << "oamf4" + << "oamf4ec" << "oamf4sc" << "on" << "opc" << "or" << "outbound" << "pim" + << "port" << "portrange" << "pppoed" << "pppoes" << "proto" << "psnp" << "ra" + << "radio" << "rarp" << "reason" << "rnr" << "rset" << "sc" << "sca" << "sctp" + << "sio" << "sls" << "snp" << "src" << "srnr" << "stp" << "subtype" << "ta" + << "tcp" << "type" << "udp" << "vci" << "vlan" << "vpi" << "vrrp" + ; CaptureFilterEdit::CaptureFilterEdit(QWidget *parent, bool plain) : SyntaxLineEdit(parent), @@ -83,6 +111,10 @@ CaptureFilterEdit::CaptureFilterEdit(QWidget *parent, bool plain) : { setAccessibleName(tr("Capture filter entry")); + completion_model_ = new QStringListModel(this); + setCompleter(new QCompleter(completion_model_, this)); + setCompletionTokenChars(libpcap_primitive_chars_); + placeholder_text_ = QString(tr("Enter a capture filter %1")).arg(UTF8_HORIZONTAL_ELLIPSIS); #if QT_VERSION >= QT_VERSION_CHECK(4, 7, 0) setPlaceholderText(placeholder_text_); @@ -343,6 +375,41 @@ void CaptureFilterEdit::bookmarkClicked() emit addBookmark(text()); } +void CaptureFilterEdit::buildCompletionList(const QString &primitive_word) +{ + if (primitive_word.length() < 1) { + completion_model_->setStringList(QStringList()); + return; + } + + // Grab matching capture filters from our parent combo and from the + // saved capture filters file. Skip ones that look like single fields + // and assume they will be added below. + QStringList complex_list; + QComboBox *cf_combo = qobject_cast(parent()); + if (cf_combo) { + for (int i = 0; i < cf_combo->count() ; i++) { + QString recent_filter = cf_combo->itemText(i); + + if (isComplexFilter(recent_filter)) { + complex_list << recent_filter; + } + } + } + for (const GList *cf_item = get_filter_list_first(CFILTER_LIST); cf_item; cf_item = g_list_next(cf_item)) { + const filter_def *cf_def = (filter_def *) cf_item->data; + if (!cf_def || !cf_def->strval) continue; + QString saved_filter = cf_def->strval; + + if (isComplexFilter(saved_filter) && !complex_list.contains(saved_filter)) { + complex_list << saved_filter; + } + } + + completion_model_->setStringList(complex_list + libpcap_primitives_); + completer()->setCompletionPrefix(primitive_word); +} + void CaptureFilterEdit::applyCaptureFilter() { if (syntaxState() == Invalid) { diff --git a/ui/qt/capture_filter_edit.h b/ui/qt/capture_filter_edit.h index 020799622f..a4d61076e4 100644 --- a/ui/qt/capture_filter_edit.h +++ b/ui/qt/capture_filter_edit.h @@ -38,8 +38,8 @@ protected: void paintEvent(QPaintEvent *evt); #endif void resizeEvent(QResizeEvent *); -// void focusInEvent(QFocusEvent *evt); -// void focusOutEvent(QFocusEvent *evt); + void keyPressEvent(QKeyEvent *event) { completionKeyPressEvent(event); } + void focusInEvent(QFocusEvent *event) { completionFocusInEvent(event); } public slots: void checkFilter(); @@ -60,6 +60,8 @@ private: QToolButton *apply_button_; CaptureFilterSyntaxWorker *syntax_worker_; + void buildCompletionList(const QString& primitive_word); + signals: void pushFilterSyntaxStatus(const QString&); void popFilterSyntaxStatus(); diff --git a/ui/qt/display_filter_edit.cpp b/ui/qt/display_filter_edit.cpp index b20b88963a..09fc1fb8d3 100644 --- a/ui/qt/display_filter_edit.cpp +++ b/ui/qt/display_filter_edit.cpp @@ -36,9 +36,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -95,22 +93,19 @@ UIMiniCancelButton::UIMiniCancelButton(QWidget *pParent /* = 0 */) #define DEFAULT_MODIFIER "Ctrl-" #endif -const int max_completion_items_ = 20; - // proto.c:fld_abbrev_chars static const QString fld_abbrev_chars_ = "-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; DisplayFilterEdit::DisplayFilterEdit(QWidget *parent, bool plain) : SyntaxLineEdit(parent), plain_(plain), - apply_button_(NULL), - completer_(NULL) + apply_button_(NULL) { setAccessibleName(tr("Display filter entry")); completion_model_ = new QStringListModel(this); - QCompleter *completer_ = new QCompleter(completion_model_, this); - setCompleter(completer_); + setCompleter(new QCompleter(completion_model_, this)); + setCompletionTokenChars(fld_abbrev_chars_); if (plain_) { placeholder_text_ = QString(tr("Enter a display filter %1")).arg(UTF8_HORIZONTAL_ELLIPSIS); @@ -238,27 +233,6 @@ DisplayFilterEdit::DisplayFilterEdit(QWidget *parent, bool plain) : ); } -// Override setCompleter so that we don't clobber the filter text on activate. -void DisplayFilterEdit::setCompleter(QCompleter *c) -{ - if (completer_) - QObject::disconnect(completer_, 0, this, 0); - - completer_ = c; - - if (!completer_) - return; - - completer_->setWidget(this); - completer_->setCompletionMode(QCompleter::PopupCompletion); - completer_->setCaseSensitivity(Qt::CaseInsensitive); - // Completion items are not guaranteed to be sorted (recent filters + - // fields), so no setModelSorting. - completer_->setMaxVisibleItems(max_completion_items_); - QObject::connect(completer_, SIGNAL(activated(QString)), - this, SLOT(insertFieldCompletion(QString))); -} - #if QT_VERSION < QT_VERSION_CHECK(4, 7, 0) void DisplayFilterEdit::paintEvent(QPaintEvent *evt) { SyntaxLineEdit::paintEvent(evt); @@ -308,64 +282,11 @@ void DisplayFilterEdit::resizeEvent(QResizeEvent *) bookmark_button_->setMaximumHeight(contentsRect().height()); } -void DisplayFilterEdit::keyPressEvent(QKeyEvent *event) -{ - // Forward to the completer if needed... - if (completer_ && completer_->popup()->isVisible()) { - switch (event->key()) { - case Qt::Key_Enter: - case Qt::Key_Return: - case Qt::Key_Escape: - case Qt::Key_Tab: - case Qt::Key_Backtab: - event->ignore(); - return; - default: - break; - } - } - - // ...otherwise process the key ourselves. - QLineEdit::keyPressEvent(event); - - if (!completer_) return; - - // Do nothing on bare shift. - if ((event->modifiers() & Qt::ShiftModifier) && event->text().isEmpty()) return; - - if (event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier)) { - completer_->popup()->hide(); - return; - } - - QPoint field_coords(getFieldUnderCursor()); - - QString field_word = text().mid(field_coords.x(), field_coords.y()); - buildCompletionList(field_word); - - if (completion_model_->stringList().length() < 1) { - completer_->popup()->hide(); - return; - } - - QRect cr = cursorRect(); - cr.setWidth(completer_->popup()->sizeHintForColumn(0) - + completer_->popup()->verticalScrollBar()->sizeHint().width()); - completer_->complete(cr); -} - -void DisplayFilterEdit::focusInEvent(QFocusEvent *evt) -{ - if (completer_) - completer_->setWidget(this); - SyntaxLineEdit::focusInEvent(evt); -} - -void DisplayFilterEdit::focusOutEvent(QFocusEvent *evt) +void DisplayFilterEdit::focusOutEvent(QFocusEvent *event) { if (syntaxState() == Valid) emit popFilterSyntaxStatus(); - SyntaxLineEdit::focusOutEvent(evt); + SyntaxLineEdit::focusOutEvent(event); } void DisplayFilterEdit::checkFilter(const QString& text) @@ -404,22 +325,6 @@ void DisplayFilterEdit::checkFilter(const QString& text) } } -bool DisplayFilterEdit::isComplexFilter(const QString &dfilter) -{ - bool is_complex = false; - for (int i = 0; i < dfilter.length(); i++) { - if (!fld_abbrev_chars_.contains(dfilter.at(i))) { - is_complex = true; - break; - } - } - // Don't complete the current filter. - if (is_complex && dfilter.startsWith(text()) && dfilter.compare(text())) { - return true; - } - return false; -} - // GTK+ behavior: // - Operates on words (proto.c:fld_abbrev_chars). // - Popup appears when you enter or remove text. @@ -563,44 +468,6 @@ void DisplayFilterEdit::changeEvent(QEvent* event) SyntaxLineEdit::changeEvent(event); } -void DisplayFilterEdit::insertFieldCompletion(const QString &completion_text) -{ - QCompleter *completer_ = completer(); - if (!completer_) return; - - QPoint field_coords(getFieldUnderCursor()); - - // Insert only if we have a matching field or if the entry is empty - if (field_coords.y() < 1 && !text().isEmpty()) { - completer_->popup()->hide(); - return; - } - - QString new_text = text().replace(field_coords.x(), field_coords.y(), completion_text); - setText(new_text); - setCursorPosition(field_coords.x() + completion_text.length()); -} - -QPoint DisplayFilterEdit::getFieldUnderCursor() -{ - if (selectionStart() >= 0) return (QPoint(0,0)); - - int pos = cursorPosition(); - int start = pos; - int len = 0; - - while (start > 0 && fld_abbrev_chars_.contains(text().at(start -1))) { - start--; - len++; - } - while (pos < text().length() && fld_abbrev_chars_.contains(text().at(pos))) { - pos++; - len++; - } - - return QPoint(start, len); -} - /* * Editor modelines * diff --git a/ui/qt/display_filter_edit.h b/ui/qt/display_filter_edit.h index 70fdb1b75e..f26389a6cf 100644 --- a/ui/qt/display_filter_edit.h +++ b/ui/qt/display_filter_edit.h @@ -24,9 +24,7 @@ #include "syntax_line_edit.h" -class QCompleter; class QEvent; -class QStringListModel; class QToolButton; class DisplayFilterEdit : public SyntaxLineEdit @@ -35,17 +33,14 @@ class DisplayFilterEdit : public SyntaxLineEdit public: explicit DisplayFilterEdit(QWidget *parent = 0, bool plain = true); - void setCompleter(QCompleter *c); - QCompleter *completer() const { return completer_; } - protected: #if QT_VERSION < QT_VERSION_CHECK(4, 7, 0) void paintEvent(QPaintEvent *evt); #endif void resizeEvent(QResizeEvent *); - void keyPressEvent(QKeyEvent *event); - void focusInEvent(QFocusEvent *evt); - void focusOutEvent(QFocusEvent *evt); + void keyPressEvent(QKeyEvent *event) { completionKeyPressEvent(event); } + void focusInEvent(QFocusEvent *event) { completionFocusInEvent(event); } + void focusOutEvent(QFocusEvent *event); public slots: void applyDisplayFilter(); @@ -56,7 +51,6 @@ private slots: void bookmarkClicked(); void clearFilter(); void changeEvent(QEvent* event); - void insertFieldCompletion(const QString & completion_text); private: bool plain_; @@ -64,13 +58,8 @@ private: QToolButton *bookmark_button_; QToolButton *clear_button_; QToolButton *apply_button_; - QCompleter *completer_; - QStringListModel *completion_model_; - bool isComplexFilter(const QString &dfilter); void buildCompletionList(const QString& field_word); - // x = Start position, y = length - QPoint getFieldUnderCursor(); signals: void pushFilterSyntaxStatus(const QString&); diff --git a/ui/qt/syntax_line_edit.cpp b/ui/qt/syntax_line_edit.cpp index 867c01d38a..ac1b029a9f 100644 --- a/ui/qt/syntax_line_edit.cpp +++ b/ui/qt/syntax_line_edit.cpp @@ -31,12 +31,43 @@ #include "color_utils.h" +#include +#include +#include +#include +#include + +const int max_completion_items_ = 20; + SyntaxLineEdit::SyntaxLineEdit(QWidget *parent) : - QLineEdit(parent) + QLineEdit(parent), + completer_(NULL), + completion_model_(NULL) { setSyntaxState(); } +// Override setCompleter so that we don't clobber the filter text on activate. +void SyntaxLineEdit::setCompleter(QCompleter *c) +{ + if (completer_) + QObject::disconnect(completer_, 0, this, 0); + + completer_ = c; + + if (!completer_) + return; + + completer_->setWidget(this); + completer_->setCompletionMode(QCompleter::PopupCompletion); + completer_->setCaseSensitivity(Qt::CaseInsensitive); + // Completion items are not guaranteed to be sorted (recent filters + + // fields), so no setModelSorting. + completer_->setMaxVisibleItems(max_completion_items_); + QObject::connect(completer_, SIGNAL(activated(QString)), + this, SLOT(insertFieldCompletion(QString))); +} + void SyntaxLineEdit::setSyntaxState(SyntaxState state) { syntax_state_ = state; state_style_sheet_ = QString( @@ -144,3 +175,109 @@ void SyntaxLineEdit::checkInteger(QString number) setSyntaxState(SyntaxLineEdit::Invalid); } } + +bool SyntaxLineEdit::isComplexFilter(const QString &filter) +{ + bool is_complex = false; + for (int i = 0; i < filter.length(); i++) { + if (!token_chars_.contains(filter.at(i))) { + is_complex = true; + break; + } + } + // Don't complete the current filter. + if (is_complex && filter.startsWith(text()) && filter.compare(text())) { + return true; + } + return false; +} + +void SyntaxLineEdit::completionKeyPressEvent(QKeyEvent *event) +{ + // Forward to the completer if needed... + if (completer_ && completer_->popup()->isVisible()) { + switch (event->key()) { + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Escape: + case Qt::Key_Tab: + case Qt::Key_Backtab: + event->ignore(); + return; + default: + break; + } + } + + // ...otherwise process the key ourselves. + SyntaxLineEdit::keyPressEvent(event); + + if (!completer_ || !completion_model_) return; + + // Do nothing on bare shift. + if ((event->modifiers() & Qt::ShiftModifier) && event->text().isEmpty()) return; + + if (event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier)) { + completer_->popup()->hide(); + return; + } + + QPoint token_coords(getTokenUnderCursor()); + + QString token_word = text().mid(token_coords.x(), token_coords.y()); + buildCompletionList(token_word); + + if (completion_model_->stringList().length() < 1) { + completer_->popup()->hide(); + return; + } + + QRect cr = cursorRect(); + cr.setWidth(completer_->popup()->sizeHintForColumn(0) + + completer_->popup()->verticalScrollBar()->sizeHint().width()); + completer_->complete(cr); +} + +void SyntaxLineEdit::completionFocusInEvent(QFocusEvent *event) +{ + if (completer_) + completer_->setWidget(this); + SyntaxLineEdit::focusInEvent(event); +} + +void SyntaxLineEdit::insertFieldCompletion(const QString &completion_text) +{ + if (!completer_) return; + + QPoint field_coords(getTokenUnderCursor()); + + // Insert only if we have a matching field or if the entry is empty + if (field_coords.y() < 1 && !text().isEmpty()) { + completer_->popup()->hide(); + return; + } + + QString new_text = text().replace(field_coords.x(), field_coords.y(), completion_text); + setText(new_text); + setCursorPosition(field_coords.x() + completion_text.length()); +} + +QPoint SyntaxLineEdit::getTokenUnderCursor() +{ + if (selectionStart() >= 0) return (QPoint(0,0)); + + int pos = cursorPosition(); + int start = pos; + int len = 0; + + while (start > 0 && token_chars_.contains(text().at(start -1))) { + start--; + len++; + } + while (pos < text().length() && token_chars_.contains(text().at(pos))) { + pos++; + len++; + } + + return QPoint(start, len); +} diff --git a/ui/qt/syntax_line_edit.h b/ui/qt/syntax_line_edit.h index 77edf92e84..585e3b59bb 100644 --- a/ui/qt/syntax_line_edit.h +++ b/ui/qt/syntax_line_edit.h @@ -24,6 +24,13 @@ #include +class QCompleter; +class QStringListModel; + +// Autocompletion is partially implemented. Subclasses must: +// - Provide buildCompletionList +// - Call setCompletionTokenChars + class SyntaxLineEdit : public QLineEdit { Q_OBJECT @@ -39,6 +46,9 @@ public: QString styleSheet() const; QString deprecatedToken(); + void setCompleter(QCompleter *c); + QCompleter *completer() const { return completer_; } + public slots: void setStyleSheet(const QString &style_sheet); @@ -47,12 +57,28 @@ public slots: void checkFieldName(QString field); void checkInteger(QString number); +protected: + QCompleter *completer_; + QStringListModel *completion_model_; + void setCompletionTokenChars(const QString &token_chars) { token_chars_ = token_chars; } + bool isComplexFilter(const QString &filter); + virtual void buildCompletionList(const QString&) { } + // x = Start position, y = length + QPoint getTokenUnderCursor(); + + void completionKeyPressEvent(QKeyEvent *event); + void completionFocusInEvent(QFocusEvent *event); + private: SyntaxState syntax_state_; QString style_sheet_; QString state_style_sheet_; QString deprecated_token_; QString syntax_error_message_; + QString token_chars_; + +private slots: + void insertFieldCompletion(const QString &completion_text); signals: