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