1 /* syntax_line_edit.cpp
2  *
3  * Wireshark - Network traffic analyzer
4  * By Gerald Combs <gerald@wireshark.org>
5  * Copyright 1998 Gerald Combs
6  *
7  * SPDX-License-Identifier: GPL-2.0-or-later
8  */
9 
10 #include "config.h"
11 
12 #include <glib.h>
13 
14 #include <epan/prefs.h>
15 #include <epan/proto.h>
16 #include <epan/dfilter/dfilter.h>
17 #include <epan/column-info.h>
18 
19 #include <wsutil/utf8_entities.h>
20 
21 #include <ui/qt/widgets/syntax_line_edit.h>
22 
23 #include <ui/qt/utils/qt_ui_utils.h>
24 #include <ui/qt/utils/color_utils.h>
25 #include <ui/qt/utils/stock_icon.h>
26 
27 #include <QAbstractItemView>
28 #include <QApplication>
29 #include <QCompleter>
30 #include <QKeyEvent>
31 #include <QPainter>
32 #include <QScrollBar>
33 #include <QStringListModel>
34 #include <QStyleOptionFrame>
35 #include <limits>
36 
37 const int max_completion_items_ = 20;
38 
SyntaxLineEdit(QWidget * parent)39 SyntaxLineEdit::SyntaxLineEdit(QWidget *parent) :
40     QLineEdit(parent),
41     completer_(NULL),
42     completion_model_(NULL),
43     completion_enabled_(false)
44 {
45     setSyntaxState();
46     setMaxLength(std::numeric_limits<quint32>::max());
47 }
48 
49 // Override setCompleter so that we don't clobber the filter text on activate.
setCompleter(QCompleter * c)50 void SyntaxLineEdit::setCompleter(QCompleter *c)
51 {
52     if (completer_)
53         QObject::disconnect(completer_, 0, this, 0);
54 
55     completer_ = c;
56 
57     if (!completer_)
58         return;
59 
60     completer_->setWidget(this);
61     completer_->setCompletionMode(QCompleter::PopupCompletion);
62     completer_->setCaseSensitivity(Qt::CaseInsensitive);
63     // Completion items are not guaranteed to be sorted (recent filters +
64     // fields), so no setModelSorting.
65     completer_->setMaxVisibleItems(max_completion_items_);
66     QObject::connect(completer_, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated),
67                      this, &SyntaxLineEdit::insertFieldCompletion);
68 
69     // Auto-completion is turned on.
70     completion_enabled_ = true;
71 }
72 
allowCompletion(bool enabled)73 void SyntaxLineEdit::allowCompletion(bool enabled)
74 {
75     completion_enabled_ = enabled;
76 }
77 
setSyntaxState(SyntaxState state)78 void SyntaxLineEdit::setSyntaxState(SyntaxState state) {
79     syntax_state_ = state;
80 
81     QColor valid_bg = ColorUtils::fromColorT(&prefs.gui_text_valid);
82     QColor valid_fg = ColorUtils::contrastingTextColor(valid_bg);
83     QColor invalid_bg = ColorUtils::fromColorT(&prefs.gui_text_invalid);
84     QColor invalid_fg = ColorUtils::contrastingTextColor(invalid_bg);
85     QColor deprecated_bg = ColorUtils::fromColorT(&prefs.gui_text_deprecated);
86     QColor deprecated_fg = ColorUtils::contrastingTextColor(deprecated_bg);
87 
88     // Try to matche QLineEdit's placeholder text color (which sets the
89     // alpha channel to 50%, which doesn't work in style sheets).
90     // Setting the foreground color lets us avoid yet another background
91     // color preference and should hopefully make things easier to
92     // distinguish for color blind folk.
93     QColor busy_fg = ColorUtils::alphaBlend(QApplication::palette().text(), QApplication::palette().base(), 0.5);
94 
95     state_style_sheet_ = QString(
96             "SyntaxLineEdit[syntaxState=\"%1\"] {"
97             "  color: %2;"
98             "  background-color: %3;"
99             "}"
100 
101             "SyntaxLineEdit[syntaxState=\"%4\"] {"
102             "  color: %5;"
103             "  background-color: %6;"
104             "}"
105 
106             "SyntaxLineEdit[syntaxState=\"%7\"] {"
107             "  color: %8;"
108             "  background-color: %9;"
109             "}"
110 
111             "SyntaxLineEdit[syntaxState=\"%10\"] {"
112             "  color: %11;"
113             "  background-color: %12;"
114             "}"
115             )
116 
117             // CSS selector, foreground, background
118             .arg(Valid)
119             .arg(valid_fg.name())
120             .arg(valid_bg.name())
121 
122             .arg(Invalid)
123             .arg(invalid_fg.name())
124             .arg(invalid_bg.name())
125 
126             .arg(Deprecated)
127             .arg(deprecated_fg.name())
128             .arg(deprecated_bg.name())
129 
130             .arg(Busy)
131             .arg(busy_fg.name())
132             .arg(palette().base().color().name())
133             ;
134     setStyleSheet(style_sheet_);
135 }
136 
syntaxErrorMessage()137 QString SyntaxLineEdit::syntaxErrorMessage() {
138     return syntax_error_message_;
139 }
140 
styleSheet() const141 QString SyntaxLineEdit::styleSheet() const {
142     return style_sheet_;
143 }
144 
setStyleSheet(const QString & style_sheet)145 void SyntaxLineEdit::setStyleSheet(const QString &style_sheet) {
146     style_sheet_ = style_sheet;
147     QLineEdit::setStyleSheet(style_sheet_ + state_style_sheet_);
148 }
149 
insertFilter(const QString & filter)150 void SyntaxLineEdit::insertFilter(const QString &filter)
151 {
152     QString padded_filter = filter;
153 
154     if (hasSelectedText()) {
155         backspace();
156     }
157 
158     int pos = cursorPosition();
159     if (pos > 0 && !text().at(pos - 1).isSpace()) {
160         padded_filter.prepend(" ");
161     }
162     if (pos < text().length() - 1 && !text().at(pos + 1).isSpace()) {
163         padded_filter.append(" ");
164     }
165     insert(padded_filter);
166 }
167 
checkDisplayFilter(QString filter)168 bool SyntaxLineEdit::checkDisplayFilter(QString filter)
169 {
170     if (!completion_enabled_) {
171         return false;
172     }
173 
174     if (filter.isEmpty()) {
175         setSyntaxState(SyntaxLineEdit::Empty);
176         return true;
177     }
178 
179     dfilter_t *dfp = NULL;
180     gchar *err_msg;
181     if (dfilter_compile(filter.toUtf8().constData(), &dfp, &err_msg)) {
182         GPtrArray *depr = NULL;
183         if (dfp) {
184             depr = dfilter_deprecated_tokens(dfp);
185         }
186         if (depr) {
187             // You keep using that word. I do not think it means what you think it means.
188             // Possible alternatives: ::Troubled, or ::Problematic maybe?
189             setSyntaxState(SyntaxLineEdit::Deprecated);
190             /*
191              * We're being lazy and only printing the first "problem" token.
192              * Would it be better to print all of them?
193              */
194             QString token((const char *)g_ptr_array_index(depr, 0));
195             gchar *token_str = qstring_strdup(token.section('.', 0, 0));
196             header_field_info *hfi = proto_registrar_get_byalias(token_str);
197             if (hfi)
198                 syntax_error_message_ = tr("\"%1\" is deprecated in favour of \"%2\". "
199                                            "See the User's Guide.").arg(token_str).arg(hfi->abbrev);
200             else
201                 // The token_str is the message.
202                 syntax_error_message_ = tr("%1").arg(token_str);
203             g_free(token_str);
204         } else {
205             setSyntaxState(SyntaxLineEdit::Valid);
206         }
207     } else {
208         setSyntaxState(SyntaxLineEdit::Invalid);
209         syntax_error_message_ = QString::fromUtf8(err_msg);
210         g_free(err_msg);
211     }
212     dfilter_free(dfp);
213 
214     return true;
215 }
216 
checkFieldName(QString field)217 void SyntaxLineEdit::checkFieldName(QString field)
218 {
219     if (field.isEmpty()) {
220         setSyntaxState(SyntaxLineEdit::Empty);
221         return;
222     }
223 
224     char invalid_char = proto_check_field_name(field.toUtf8().constData());
225     if (invalid_char) {
226         setSyntaxState(SyntaxLineEdit::Invalid);
227     } else {
228         checkDisplayFilter(field);
229     }
230 }
231 
checkCustomColumn(QString fields)232 void SyntaxLineEdit::checkCustomColumn(QString fields)
233 {
234     if (fields.isEmpty()) {
235         setSyntaxState(SyntaxLineEdit::Empty);
236         return;
237     }
238 
239     gchar **splitted_fields = g_regex_split_simple(COL_CUSTOM_PRIME_REGEX,
240                 fields.toUtf8().constData(), G_REGEX_ANCHORED, G_REGEX_MATCH_ANCHORED);
241 
242     for (guint i = 0; i < g_strv_length(splitted_fields); i++) {
243         if (splitted_fields[i] && *splitted_fields[i]) {
244             if (proto_check_field_name(splitted_fields[i]) != 0) {
245                 setSyntaxState(SyntaxLineEdit::Invalid);
246                 g_strfreev(splitted_fields);
247                 return;
248             }
249         }
250     }
251     g_strfreev(splitted_fields);
252 
253     checkDisplayFilter(fields);
254 }
255 
checkInteger(QString number)256 void SyntaxLineEdit::checkInteger(QString number)
257 {
258     if (number.isEmpty()) {
259         setSyntaxState(SyntaxLineEdit::Empty);
260         return;
261     }
262 
263     bool ok;
264     text().toInt(&ok);
265     if (ok) {
266         setSyntaxState(SyntaxLineEdit::Valid);
267     } else {
268         setSyntaxState(SyntaxLineEdit::Invalid);
269     }
270 }
271 
isComplexFilter(const QString & filter)272 bool SyntaxLineEdit::isComplexFilter(const QString &filter)
273 {
274     bool is_complex = false;
275     for (int i = 0; i < filter.length(); i++) {
276         if (!token_chars_.contains(filter.at(i))) {
277             is_complex = true;
278             break;
279         }
280     }
281     // Don't complete the current filter.
282     if (is_complex && filter.startsWith(text()) && filter.compare(text())) {
283         return true;
284     }
285     return false;
286 }
287 
event(QEvent * event)288 bool SyntaxLineEdit::event(QEvent *event)
289 {
290     if (event->type() == QEvent::ShortcutOverride) {
291         // You can't set time display formats while the display filter edit
292         // has focus.
293 
294         // Keep shortcuts in the main window from stealing keyPressEvents
295         // with Ctrl+Alt modifiers from us. This is a problem for many AltGr
296         // combinations since they are delivered with Ctrl+Alt modifiers
297         // instead of Qt::Key_AltGr and they tend to match the time display
298         // format shortcuts.
299 
300         // Uncommenting the qDebug line below prints the following here:
301         //
302         // US Keyboard:
303         // Ctrl+o: 79 QFlags<Qt::KeyboardModifiers>(ControlModifier) "\u000F"
304         // Ctrl+Alt+2: 50 QFlags<Qt::KeyboardModifiers>(ControlModifier|AltModifier) "2"
305         //
306         // Swedish (Sweden) Keyboard:
307         // Ctrl+o: 79 QFlags<Qt::KeyboardModifiers>(ControlModifier) "\u000F"
308         // Ctrl+Alt+2: 64 QFlags<Qt::KeyboardModifiers>(ControlModifier|AltModifier) "@"
309         // AltGr+{: 123 QFlags<Qt::KeyboardModifiers>(ControlModifier|AltModifier) "{"
310 
311         QKeyEvent* key_event = static_cast<QKeyEvent*>(event);
312         // qDebug() << "=so" << key_event->key() << key_event->modifiers() << key_event->text();
313 
314         if (key_event->modifiers() == Qt::KeyboardModifiers(Qt::ControlModifier|Qt::AltModifier)) {
315             event->accept();
316             return true;
317         }
318     }
319     return QLineEdit::event(event);
320 }
321 
completionKeyPressEvent(QKeyEvent * event)322 void SyntaxLineEdit::completionKeyPressEvent(QKeyEvent *event)
323 {
324     // Forward to the completer if needed...
325     if (completer_ && completer_->popup()->isVisible()) {
326         switch (event->key()) {
327         case Qt::Key_Enter:
328         case Qt::Key_Return:
329             break;
330         case Qt::Key_Tab:
331             focusNextChild();
332             break;
333         case Qt::Key_Escape:
334         case Qt::Key_Backtab:
335             event->ignore();
336             return;
337         default:
338             break;
339         }
340     }
341 
342     // ...otherwise process the key ourselves.
343     SyntaxLineEdit::keyPressEvent(event);
344 
345     if (!completion_enabled_ || !completer_ || !completion_model_ || !prefs.gui_autocomplete_filter) return;
346 
347     // Do nothing on bare shift.
348     if ((event->modifiers() & Qt::ShiftModifier) && event->text().isEmpty()) return;
349 
350     if (event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier)) {
351         completer_->popup()->hide();
352         return;
353     }
354 
355     QPoint token_coords(getTokenUnderCursor());
356 
357     QString token_word = text().mid(token_coords.x(), token_coords.y());
358     buildCompletionList(token_word);
359 
360     if (completion_model_->stringList().length() < 1) {
361         completer_->popup()->hide();
362         return;
363     }
364 
365     QRect cr = cursorRect();
366     cr.setWidth(completer_->popup()->sizeHintForColumn(0)
367                 + completer_->popup()->verticalScrollBar()->sizeHint().width());
368     completer_->complete(cr);
369 }
370 
completionFocusInEvent(QFocusEvent * event)371 void SyntaxLineEdit::completionFocusInEvent(QFocusEvent *event)
372 {
373     if (completer_)
374         completer_->setWidget(this);
375     SyntaxLineEdit::focusInEvent(event);
376 }
377 
focusOutEvent(QFocusEvent * event)378 void SyntaxLineEdit::focusOutEvent(QFocusEvent *event)
379 {
380     if (completer_ && completer_->popup()->isVisible() && event->reason() == Qt::PopupFocusReason) {
381         // Pretend we still have focus so that we'll draw our cursor.
382         // If cursorRect() were more precise we could just draw the cursor
383         // during a paintEvent.
384         return;
385     }
386     QLineEdit::focusOutEvent(event);
387 }
388 
389 // Add indicator icons for syntax states in order to make things more clear for
390 // color blind people.
paintEvent(QPaintEvent * event)391 void SyntaxLineEdit::paintEvent(QPaintEvent *event)
392 {
393     QLineEdit::paintEvent(event);
394 
395     QString si_name;
396 
397     switch (syntax_state_) {
398     case Invalid:
399         si_name = "x-filter-invalid";
400         break;
401     case Deprecated:
402         si_name = "x-filter-deprecated";
403         break;
404     default:
405         return;
406     }
407 
408     QStyleOptionFrame opt;
409     initStyleOption(&opt);
410     QRect cr = style()->subElementRect(QStyle::SE_LineEditContents, &opt, this);
411     QRect sir = QRect(0, 0, 14, 14); // QIcon::paint scales, which is not what we want.
412     int textWidth = fontMetrics().boundingRect(text()).width();
413     // Qt always adds a margin of 6px between the border and text, see
414     // QLineEditPrivate::effectiveLeftTextMargin and
415     // QLineEditPrivate::sideWidgetParameters.
416     int margin = 2 * 6 + 1;
417 
418     if (cr.width() - margin - textWidth < sir.width() || cr.height() < sir.height()) {
419         // No space to draw
420         return;
421     }
422 
423     QIcon state_icon = StockIcon(si_name);
424     if (state_icon.isNull()) {
425         return;
426     }
427 
428     int si_off = (cr.height() - sir.height()) / 2;
429     sir.moveTop(si_off);
430     sir.moveRight(cr.right() - si_off);
431     QPainter painter(this);
432     painter.setOpacity(0.25);
433     state_icon.paint(&painter, sir);
434 }
435 
insertFieldCompletion(const QString & completion_text)436 void SyntaxLineEdit::insertFieldCompletion(const QString &completion_text)
437 {
438     if (!completer_) return;
439 
440     QPoint field_coords(getTokenUnderCursor());
441 
442     // Insert only if we have a matching field or if the entry is empty
443     if (field_coords.y() < 1 && !text().isEmpty()) {
444         completer_->popup()->hide();
445         return;
446     }
447 
448     QString new_text = text().replace(field_coords.x(), field_coords.y(), completion_text);
449     setText(new_text);
450     setCursorPosition(field_coords.x() + completion_text.length());
451     emit textEdited(new_text);
452 }
453 
getTokenUnderCursor()454 QPoint SyntaxLineEdit::getTokenUnderCursor()
455 {
456     if (selectionStart() >= 0) return (QPoint(0,0));
457 
458     int pos = cursorPosition();
459     int start = pos;
460     int len = 0;
461 
462     while (start > 0 && token_chars_.contains(text().at(start -1))) {
463         start--;
464         len++;
465     }
466     while (pos < text().length() && token_chars_.contains(text().at(pos))) {
467         pos++;
468         len++;
469     }
470 
471     return QPoint(start, len);
472 }
473