/* syntax_line_edit.cpp * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include const int max_completion_items_ = 20; SyntaxLineEdit::SyntaxLineEdit(QWidget *parent) : QLineEdit(parent), completer_(NULL), completion_model_(NULL), completion_enabled_(false) { setSyntaxState(); setMaxLength(std::numeric_limits::max()); } // 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_, static_cast(&QCompleter::activated), this, &SyntaxLineEdit::insertFieldCompletion); // Auto-completion is turned on. completion_enabled_ = true; } void SyntaxLineEdit::allowCompletion(bool enabled) { completion_enabled_ = enabled; } void SyntaxLineEdit::setSyntaxState(SyntaxState state) { syntax_state_ = state; // XXX Should we drop the background colors here in favor of ::paintEvent below? QColor valid_bg = ColorUtils::fromColorT(&prefs.gui_text_valid); QColor valid_fg = ColorUtils::contrastingTextColor(valid_bg); QColor invalid_bg = ColorUtils::fromColorT(&prefs.gui_text_invalid); QColor invalid_fg = ColorUtils::contrastingTextColor(invalid_bg); QColor deprecated_bg = ColorUtils::fromColorT(&prefs.gui_text_deprecated); QColor deprecated_fg = ColorUtils::contrastingTextColor(deprecated_bg); // 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 // distinguish for color blind folk. QColor busy_fg = ColorUtils::alphaBlend(QApplication::palette().text(), QApplication::palette().base(), 0.5); state_style_sheet_ = QString( "SyntaxLineEdit[syntaxState=\"%1\"] {" " color: %2;" " background-color: %3;" "}" "SyntaxLineEdit[syntaxState=\"%4\"] {" " color: %5;" " background-color: %6;" "}" "SyntaxLineEdit[syntaxState=\"%7\"] {" " color: %8;" " background-color: %9;" "}" "SyntaxLineEdit[syntaxState=\"%10\"] {" " color: %11;" " background-color: %12;" "}" ) // CSS selector, foreground, background .arg(Valid) .arg(valid_fg.name()) .arg(valid_bg.name()) .arg(Invalid) .arg(invalid_fg.name()) .arg(invalid_bg.name()) .arg(Deprecated) .arg(deprecated_fg.name()) .arg(deprecated_bg.name()) .arg(Busy) .arg(busy_fg.name()) .arg(palette().base().color().name()) ; setStyleSheet(style_sheet_); } QString SyntaxLineEdit::syntaxErrorMessage() { return syntax_error_message_; } QString SyntaxLineEdit::syntaxErrorMessageFull() { return syntax_error_message_full_; } QString SyntaxLineEdit::createSyntaxErrorMessageFull( const QString &filter, const QString &err_msg, qsizetype loc_start, size_t loc_length) { QString msg = tr("Invalid filter: %1").arg(err_msg); if (loc_start >= 0 && loc_length >= 1) { // Add underlined location msg = QString("

%1

  %2\n  %3^%4

") .arg(msg) .arg(filter) .arg(QString(' ').repeated(static_cast(loc_start))) .arg(QString('~').repeated(static_cast(loc_length) - 1)); } return msg; } QString SyntaxLineEdit::styleSheet() const { return style_sheet_; } void SyntaxLineEdit::setStyleSheet(const QString &style_sheet) { style_sheet_ = style_sheet; QLineEdit::setStyleSheet(style_sheet_ + state_style_sheet_); } void SyntaxLineEdit::insertFilter(const QString &filter) { QString padded_filter = filter; if (hasSelectedText()) { backspace(); } int pos = cursorPosition(); if (pos > 0 && !text().at(pos - 1).isSpace()) { padded_filter.prepend(" "); } if (pos < text().length() - 1 && !text().at(pos + 1).isSpace()) { padded_filter.append(" "); } insert(padded_filter); } bool SyntaxLineEdit::checkDisplayFilter(QString filter) { if (!completion_enabled_) { return false; } if (filter.isEmpty()) { setSyntaxState(SyntaxLineEdit::Empty); return true; } dfilter_t *dfp = NULL; df_error_t *df_err = NULL; if (dfilter_compile(filter.toUtf8().constData(), &dfp, &df_err)) { GSList *warn; GPtrArray *depr = NULL; if (dfp != NULL && (warn = dfilter_get_warnings(dfp)) != NULL) { // FIXME Need to use a different state or rename ::Deprecated setSyntaxState(SyntaxLineEdit::Deprecated); /* * 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(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? setSyntaxState(SyntaxLineEdit::Deprecated); /* * We're being lazy and only printing the first "problem" token. * Would it be better to print all of them? */ QString token((const char *)g_ptr_array_index(depr, 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\". " "See Help section 6.4.8 for details.").arg(token_str).arg(hfi->abbrev); else // The token_str is the message. syntax_error_message_ = tr("%1").arg(token_str); g_free(token_str); } else { setSyntaxState(SyntaxLineEdit::Valid); } } else { setSyntaxState(SyntaxLineEdit::Invalid); syntax_error_message_ = QString::fromUtf8(df_err->msg); syntax_error_message_full_ = createSyntaxErrorMessageFull(filter, syntax_error_message_, df_err->loc.col_start, df_err->loc.col_len); df_error_free(&df_err); } dfilter_free(dfp); return true; } void SyntaxLineEdit::checkFieldName(QString field) { if (field.isEmpty()) { setSyntaxState(SyntaxLineEdit::Empty); return; } char invalid_char = proto_check_field_name(field.toUtf8().constData()); if (invalid_char) { setSyntaxState(SyntaxLineEdit::Invalid); } else { checkDisplayFilter(field); } } void SyntaxLineEdit::checkCustomColumn(QString fields) { if (fields.isEmpty()) { setSyntaxState(SyntaxLineEdit::Empty); return; } #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); g_strfreev(splitted_fields); return; } } } g_strfreev(splitted_fields); #endif checkDisplayFilter(fields); } void SyntaxLineEdit::checkInteger(QString number) { if (number.isEmpty()) { setSyntaxState(SyntaxLineEdit::Empty); return; } bool ok; text().toInt(&ok); if (ok) { setSyntaxState(SyntaxLineEdit::Valid); } else { 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; } bool SyntaxLineEdit::event(QEvent *event) { if (event->type() == QEvent::ShortcutOverride) { // You can't set time display formats while the display filter edit // has focus. // Keep shortcuts in the main window from stealing keyPressEvents // with Ctrl+Alt modifiers from us. This is a problem for many AltGr // combinations since they are delivered with Ctrl+Alt modifiers // instead of Qt::Key_AltGr and they tend to match the time display // format shortcuts. // Uncommenting the qDebug line below prints the following here: // // US Keyboard: // Ctrl+o: 79 QFlags(ControlModifier) "\u000F" // Ctrl+Alt+2: 50 QFlags(ControlModifier|AltModifier) "2" // // Swedish (Sweden) Keyboard: // Ctrl+o: 79 QFlags(ControlModifier) "\u000F" // Ctrl+Alt+2: 64 QFlags(ControlModifier|AltModifier) "@" // AltGr+{: 123 QFlags(ControlModifier|AltModifier) "{" QKeyEvent* key_event = static_cast(event); // qDebug() << "=so" << key_event->key() << key_event->modifiers() << key_event->text(); if (key_event->modifiers() == Qt::KeyboardModifiers(Qt::ControlModifier|Qt::AltModifier)) { event->accept(); return true; } } return QLineEdit::event(event); } 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: break; case Qt::Key_Tab: focusNextChild(); break; case Qt::Key_Escape: case Qt::Key_Backtab: event->ignore(); return; default: break; } } // ...otherwise process the key ourselves. SyntaxLineEdit::keyPressEvent(event); if (!completion_enabled_ || !completer_ || !completion_model_ || !prefs.gui_autocomplete_filter) 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; } QStringList sentence(splitLineUnderCursor()); Q_ASSERT(sentence.size() == 2); // (preamble, token) buildCompletionList(sentence[1] /* token */, sentence[0] /* preamble */); 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::focusOutEvent(QFocusEvent *event) { if (completer_ && completer_->popup()->isVisible() && event->reason() == Qt::PopupFocusReason) { // Pretend we still have focus so that we'll draw our cursor. // If cursorRect() were more precise we could just draw the cursor // during a paintEvent. return; } QLineEdit::focusOutEvent(event); } // Add indicator icons for syntax states in order to make things more clear for // color blind people. void SyntaxLineEdit::paintEvent(QPaintEvent *event) { QStyleOptionFrame opt; initStyleOption(&opt); QRect cr = style()->subElementRect(QStyle::SE_LineEditContents, &opt, this); QPainter painter(this); // In the attempt to fix https://bugreports.qt.io/browse/QTBUG-81533 // the following commit was added to Qt 6.0.0 and later 5.15.3: // https://code.qt.io/cgit/qt/qtbase.git/commit/src/widgets/widgets/qcombobox.cpp?h=5.15&id=6e470764a98434a120eba4fcc6035137cf9c92cf // // It causes a similar problem to the one it was trying to fix, viz. if I // add "background: yellow;" to the DisplayFilterCombo stylesheet, when // building with Qt 5.15.2 the combobox background is yellow and the text // entry area (between the bookmark and apply button) is drawn in the correct // base color (white for light mode and black for dark mode), and the correct // syntax color otherwise. When building with Qt 5.15.3 and 6.2.4 and 6.3.1, // the combobox background is yellow and the text entry area is always yellow, // i.e. QLineEdit isn't painting its background because the palette from // the combobox is used instead. // // It's not clear if this is a bug or just how things work under Qt6. // Either way, it's easy to work around. #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 3) // Must match CaptureFilterEdit and DisplayFilterEdit stylesheets. int pad = style()->pixelMetric(QStyle::PM_DefaultFrameWidth) + 1; QRect full_cr = cr.adjusted(-pad, 0, -1, 0); QBrush bg; switch (syntax_state_) { case Valid: bg = ColorUtils::fromColorT(&prefs.gui_text_valid); break; case Invalid: bg = ColorUtils::fromColorT(&prefs.gui_text_invalid); break; case Deprecated: bg = ColorUtils::fromColorT(&prefs.gui_text_deprecated); break; default: bg = palette().base(); break; } painter.fillRect(full_cr, bg); #endif QLineEdit::paintEvent(event); QString si_name; switch (syntax_state_) { case Invalid: si_name = "x-filter-invalid"; break; case Deprecated: si_name = "x-filter-deprecated"; break; default: return; } QRect sir = QRect(0, 0, 14, 14); // QIcon::paint scales, which is not what we want. int textWidth = fontMetrics().boundingRect(text()).width(); // Qt always adds a margin of 6px between the border and text, see // QLineEditPrivate::effectiveLeftTextMargin and // QLineEditPrivate::sideWidgetParameters. int margin = 2 * 6 + 1; if (cr.width() - margin - textWidth < sir.width() || cr.height() < sir.height()) { // No space to draw return; } QIcon state_icon = StockIcon(si_name); if (state_icon.isNull()) { return; } int si_off = (cr.height() - sir.height()) / 2; sir.moveTop(cr.top() + si_off); sir.moveRight(cr.right() - si_off); painter.save(); painter.setOpacity(0.25); state_icon.paint(&painter, sir); painter.restore(); } 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() + static_cast(completion_text.length())); emit textEdited(new_text); } 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); } QStringList SyntaxLineEdit::splitLineUnderCursor() { QPoint token_coords(getTokenUnderCursor()); // Split line into preamble and word under cursor. #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) QString preamble = text().first(token_coords.x()).trimmed(); #else QString preamble = text().mid(0, token_coords.x()).trimmed(); #endif // This should be trimmed already QString token_word = text().mid(token_coords.x(), token_coords.y()); return QStringList{ preamble, token_word }; }