1 /* filter_expression_toolbar.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 <ui/qt/widgets/filter_expression_toolbar.h>
11 #include <ui/qt/utils/color_utils.h>
12 #include <ui/qt/utils/qt_ui_utils.h>
13 #include <ui/qt/utils/wireshark_mime_data.h>
14 #include <ui/qt/models/uat_model.h>
15 #include <ui/qt/filter_action.h>
16 #include <ui/qt/wireshark_application.h>
17 
18 #include <epan/filter_expressions.h>
19 #include <ui/preference_utils.h>
20 
21 #include <QApplication>
22 #include <QFrame>
23 #include <QMenu>
24 #include <QEvent>
25 #include <QContextMenuEvent>
26 #include <QToolButton>
27 #include <QToolTip>
28 
29 static const char *dfe_property_ = "display filter expression"; //TODO : Fix Translate
30 static const char *dfe_property_label_ = "display_filter_expression_label";
31 static const char *dfe_property_expression_ = "display_filter_expression_expr";
32 static const char *dfe_property_comment_ = "display_filter_expression_comment";
33 static const char *dfe_menu_ = "filter_menu";
34 
35 #define PARENT_SEPARATOR "//"
36 
37 struct filter_expression_data
38 {
39     FilterExpressionToolBar* toolbar;
40     bool actions_added;
41 };
42 
FilterExpressionToolBar(QWidget * parent)43 FilterExpressionToolBar::FilterExpressionToolBar(QWidget * parent) :
44     DragDropToolBar(parent)
45 {
46     updateStyleSheet();
47 
48     setContextMenuPolicy(Qt::CustomContextMenu);
49     /* Give minimum space to the bar, so that drops on an empty bar will work */
50     setMinimumWidth(10);
51 
52     connect (this, &QWidget::customContextMenuRequested, this, &FilterExpressionToolBar::onCustomMenuHandler);
53     connect(this, &DragDropToolBar::actionMoved, this, &FilterExpressionToolBar::onActionMoved);
54     connect(this, &DragDropToolBar::newFilterDropped, this, &FilterExpressionToolBar::onFilterDropped);
55 
56     connect(wsApp, &WiresharkApplication::appInitialized,
57             this, &FilterExpressionToolBar::filterExpressionsChanged);
58     connect(wsApp, &WiresharkApplication::filterExpressionsChanged,
59             this, &FilterExpressionToolBar::filterExpressionsChanged);
60 
61 }
62 
event(QEvent * event)63 bool FilterExpressionToolBar::event(QEvent *event)
64 {
65     switch (event->type()) {
66     case QEvent::ApplicationPaletteChange:
67         updateStyleSheet();
68         break;
69     default:
70         break;
71 
72     }
73     return DragDropToolBar::event(event);
74 }
75 
onCustomMenuHandler(const QPoint & pos)76 void FilterExpressionToolBar::onCustomMenuHandler(const QPoint& pos)
77 {
78     QAction * filterAction = actionAt(pos);
79     if (! filterAction)
80         return;
81 
82     customMenu(this, filterAction, pos);
83 }
84 
customMenu(FilterExpressionToolBar * target,QAction * filterAction,const QPoint & pos)85 void FilterExpressionToolBar::customMenu(FilterExpressionToolBar * target, QAction * filterAction, const QPoint& pos)
86 {
87     QMenu * filterMenu = new QMenu(target);
88 
89     /* Only display context menu for actual filter actions */
90     QString filterText = filterAction->property(dfe_property_expression_).toString().trimmed();
91 
92     if (!filterText.isEmpty())
93     {
94         filterMenu->addMenu(FilterAction::createFilterMenu(FilterAction::ActionApply, filterText, true, target));
95         filterMenu->addMenu(FilterAction::createFilterMenu(FilterAction::ActionPrepare, filterText, true, target));
96         filterMenu->addSeparator();
97         filterMenu->addAction(FilterAction::copyFilterAction(filterText, target));
98         filterMenu->addSeparator();
99         QAction * actEdit = filterMenu->addAction(tr("Edit"));
100         connect(actEdit, &QAction::triggered, target, &FilterExpressionToolBar::editFilter);
101         actEdit->setProperty(dfe_property_label_, filterAction->property(dfe_property_label_));
102         actEdit->setProperty(dfe_property_expression_, filterAction->property(dfe_property_expression_));
103         actEdit->setData(filterAction->data());
104         QAction * actDisable = filterMenu->addAction(tr("Disable"));
105         connect(actDisable, &QAction::triggered, target, &FilterExpressionToolBar::disableFilter);
106         actDisable->setProperty(dfe_property_label_, filterAction->property(dfe_property_label_));
107         actDisable->setProperty(dfe_property_expression_, filterAction->property(dfe_property_expression_));
108         actDisable->setData(filterAction->data());
109         QAction * actRemove = filterMenu->addAction(tr("Remove"));
110         connect(actRemove, &QAction::triggered, target, &FilterExpressionToolBar::removeFilter);
111         actRemove->setProperty(dfe_property_label_, filterAction->property(dfe_property_label_));
112         actRemove->setProperty(dfe_property_expression_, filterAction->property(dfe_property_expression_));
113         actRemove->setData(filterAction->data());
114         filterMenu->addSeparator();
115     }
116     QAction *actFilter = filterMenu->addAction(tr("Filter Button Preferences..."));
117     connect(actFilter, &QAction::triggered, target, &FilterExpressionToolBar::toolBarShowPreferences);
118 
119     /* Forcing the menus to get closed, no matter which action has been triggered */
120     connect(filterMenu, &QMenu::triggered, this, &FilterExpressionToolBar::closeMenu);
121 
122     filterMenu->exec(mapToGlobal(pos));
123 }
124 
filterExpressionsChanged()125 void FilterExpressionToolBar::filterExpressionsChanged()
126 {
127     struct filter_expression_data data;
128 
129     data.toolbar = this;
130     data.actions_added = false;
131 
132     // Hiding and showing seems to be the only way to get the layout to
133     // work correctly in some cases. See bug 14121 for details.
134     clear();
135     setUpdatesEnabled(false);
136     hide();
137 
138     // XXX Add a context menu for removing and changing buttons.
139     filter_expression_iterate_expressions(filter_expression_add_action, &data);
140 
141     show();
142     setUpdatesEnabled(true);
143 }
144 
removeFilter()145 void FilterExpressionToolBar::removeFilter()
146 {
147     UatModel * uatModel = new UatModel(this, "Display expressions");
148 
149     QString label = ((QAction *)sender())->property(dfe_property_label_).toString();
150     QString expr = ((QAction *)sender())->property(dfe_property_expression_).toString();
151 
152     int idx = uatRowIndexForFilter(label, expr);
153 
154     QModelIndex rowIndex = uatModel->index(idx, 0);
155     if (rowIndex.isValid()) {
156         uatModel->removeRow(rowIndex.row());
157 
158         save_migrated_uat("Display expressions", &prefs.filter_expressions_old);
159         filterExpressionsChanged();
160     }
161 }
162 
createMimeData(QString name,int position)163 WiresharkMimeData * FilterExpressionToolBar::createMimeData(QString name, int position)
164 {
165     ToolbarEntryMimeData * element = new ToolbarEntryMimeData(name, position);
166     UatModel * uatModel = new UatModel(this, "Display expressions");
167 
168     QModelIndex rowIndex;
169     for (int cnt = 0; cnt < uatModel->rowCount() && ! rowIndex.isValid(); cnt++)
170     {
171         if (uatModel->data(uatModel->index(cnt, 1), Qt::DisplayRole).toString().compare(name) == 0)
172         {
173             rowIndex = uatModel->index(cnt, 2);
174             element->setFilter(rowIndex.data().toString());
175         }
176     }
177 
178     return element;
179 }
180 
onActionMoved(QAction * action,int oldPos,int newPos)181 void FilterExpressionToolBar::onActionMoved(QAction* action, int oldPos, int newPos)
182 {
183     gchar* err = NULL;
184     if (oldPos == newPos)
185         return;
186 
187     QString label = action->property(dfe_property_label_).toString();
188     QString expr = action->property(dfe_property_expression_).toString();
189 
190     int idx = uatRowIndexForFilter(label, expr);
191 
192     if (idx > -1 && oldPos > -1 && newPos > -1)
193     {
194         uat_t * table = uat_get_table_by_name("Display expressions");
195         uat_move_index(table, oldPos, newPos);
196         uat_save(table, &err);
197 
198         g_free(err);
199     }
200 }
201 
disableFilter()202 void FilterExpressionToolBar::disableFilter()
203 {
204     QString label = ((QAction *)sender())->property(dfe_property_label_).toString();
205     QString expr = ((QAction *)sender())->property(dfe_property_expression_).toString();
206 
207     int idx = uatRowIndexForFilter(label, expr);
208     UatModel * uatModel = new UatModel(this, "Display expressions");
209 
210     QModelIndex rowIndex = uatModel->index(idx, 0);
211     if (rowIndex.isValid()) {
212         uatModel->setData(rowIndex, QVariant::fromValue(false));
213 
214         save_migrated_uat("Display expressions", &prefs.filter_expressions_old);
215         filterExpressionsChanged();
216     }
217 }
218 
editFilter()219 void FilterExpressionToolBar::editFilter()
220 {
221     if (! sender())
222         return;
223 
224     QString label = ((QAction *)sender())->property(dfe_property_label_).toString();
225     QString expr = ((QAction *)sender())->property(dfe_property_expression_).toString();
226 
227     int idx = uatRowIndexForFilter(label, expr);
228 
229     if (idx > -1)
230         emit filterEdit(idx);
231 }
232 
onFilterDropped(QString description,QString filter)233 void FilterExpressionToolBar::onFilterDropped(QString description, QString filter)
234 {
235     if (filter.length() == 0)
236         return;
237 
238     filter_expression_new(qUtf8Printable(description),
239             qUtf8Printable(filter), qUtf8Printable(description), TRUE);
240 
241     save_migrated_uat("Display expressions", &prefs.filter_expressions_old);
242     filterExpressionsChanged();
243 }
244 
toolBarShowPreferences()245 void FilterExpressionToolBar::toolBarShowPreferences()
246 {
247     emit filterPreferences();
248 }
249 
updateStyleSheet()250 void FilterExpressionToolBar::updateStyleSheet()
251 {
252     // Try to draw 1-pixel-wide separator lines from the button label
253     // ascent to its baseline.
254     setStyleSheet(QString(
255                 "QToolBar { background: none; border: none; spacing: 1px; }"
256                 "QFrame { background: none; min-width: 1px; max-width: 1px; }"
257                 ));
258 }
259 
uatRowIndexForFilter(QString label,QString expression)260 int FilterExpressionToolBar::uatRowIndexForFilter(QString label, QString expression)
261 {
262     int result = -1;
263 
264     if (expression.length() == 0)
265         return result;
266 
267     UatModel * uatModel = new UatModel(this, "Display expressions");
268 
269     QModelIndex rowIndex;
270 
271     if (label.length() > 0)
272     {
273         for (int cnt = 0; cnt < uatModel->rowCount() && ! rowIndex.isValid(); cnt++)
274         {
275             if (uatModel->data(uatModel->index(cnt, 1), Qt::DisplayRole).toString().compare(label) == 0 &&
276                     uatModel->data(uatModel->index(cnt, 2), Qt::DisplayRole).toString().compare(expression) == 0)
277             {
278                 rowIndex = uatModel->index(cnt, 2);
279             }
280         }
281     }
282     else
283     {
284         rowIndex = uatModel->findRowForColumnContent(((QAction *)sender())->data(), 2);
285     }
286 
287     if (rowIndex.isValid())
288         result = rowIndex.row();
289 
290     delete uatModel;
291 
292     return result;
293 }
294 
eventFilter(QObject * obj,QEvent * event)295 bool FilterExpressionToolBar::eventFilter(QObject *obj, QEvent *event)
296 {
297     QMenu * qm = qobject_cast<QMenu *>(obj);
298 
299     if (qm && qm->property(dfe_menu_).toBool())
300     {
301 
302         if (event->type() == QEvent::ContextMenu)
303         {
304             QContextMenuEvent *ctx = static_cast<QContextMenuEvent *>(event);
305             QAction * filterAction = qm->actionAt(ctx->pos());
306 
307             if (filterAction)
308                 customMenu(this, filterAction, ctx->pos());
309             return true;
310         }
311         else if (event->type() == QEvent::ToolTip)
312         {
313             QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
314             QAction * filterAction = qm->actionAt(helpEvent->pos());
315             if (filterAction) {
316                 QToolTip::showText(helpEvent->globalPos(), filterAction->property(dfe_property_comment_).toString().trimmed());
317             } else {
318                 QToolTip::hideText();
319                 event->ignore();
320             }
321 
322             return true;
323         }
324     }
325 
326     return QToolBar::eventFilter(obj, event);
327 }
328 
closeMenu(QAction *)329 void FilterExpressionToolBar::closeMenu(QAction * /*sender*/)
330 {
331     foreach(QAction * entry, actions())
332     {
333         QWidget * widget = widgetForAction(entry);
334         QToolButton * tb = qobject_cast<QToolButton *>(widget);
335         if (tb && tb->menu())
336             tb->menu()->close();
337     }
338 }
339 
findParentMenu(const QStringList tree,void * fed_data,QMenu * parent)340 QMenu * FilterExpressionToolBar::findParentMenu(const QStringList tree, void *fed_data, QMenu *parent )
341 {
342     if (!fed_data)
343         return Q_NULLPTR;
344 
345     struct filter_expression_data* data = (filter_expression_data*)fed_data;
346     if (!data->toolbar)
347         return Q_NULLPTR;
348 
349     if (! tree.isEmpty())
350     {
351         if (!parent)
352         {
353             /* Searching existing main menus */
354             foreach(QAction * entry, data->toolbar->actions())
355             {
356                 QWidget * widget = data->toolbar->widgetForAction(entry);
357                 QToolButton * tb = qobject_cast<QToolButton *>(widget);
358                 if (tb && tb->menu() && tb->text().compare(tree.at(0).trimmed()) == 0)
359                     return findParentMenu(tree.mid(1), fed_data, tb->menu());
360             }
361         }
362         else if (parent)
363         {
364             QString menuName = tree.at(0).trimmed();
365             /* Iterate to see, if we next have to jump into another submenu */
366             foreach(QAction *entry, parent->actions())
367             {
368                 if (entry->menu() && entry->text().compare(menuName) == 0)
369                     return findParentMenu(tree.mid(1), fed_data, entry->menu());
370             }
371 
372             /* Submenu not found, creating */
373             QMenu * subMenu = new QMenu(menuName);
374             subMenu->installEventFilter(data->toolbar);
375             subMenu->setProperty(dfe_menu_, QVariant::fromValue(true));
376             parent->addMenu(subMenu);
377             return findParentMenu(tree.mid(1), fed_data, subMenu);
378         }
379 
380         /* No menu has been found, create one */
381         QString parentName = tree.at(0).trimmed();
382         QToolButton * menuButton = new QToolButton();
383         menuButton->setText(parentName);
384         menuButton->setPopupMode(QToolButton::MenuButtonPopup);
385         QMenu * parentMenu = new QMenu(menuButton);
386         parentMenu->installEventFilter(data->toolbar);
387         parentMenu->setProperty(dfe_menu_, QVariant::fromValue(true));
388         menuButton->setMenu(parentMenu);
389         // Required for QToolButton::MenuButtonPopup.
390         connect(menuButton, &QToolButton::pressed, menuButton, &QToolButton::showMenu);
391         data->toolbar->addWidget(menuButton);
392 
393         return findParentMenu(tree.mid(1), fed_data, parentMenu);
394     }
395     else if (parent)
396         return parent;
397 
398     return Q_NULLPTR;
399 }
400 
filter_expression_add_action(const void * key _U_,void * value,void * user_data)401 gboolean FilterExpressionToolBar::filter_expression_add_action(const void *key _U_, void *value, void *user_data)
402 {
403     filter_expression_t* fe = (filter_expression_t*)value;
404     struct filter_expression_data* data = (filter_expression_data*)user_data;
405 
406     if (!fe->enabled)
407         return FALSE;
408 
409     QString label = QString(fe->label);
410 
411     /* Search for parent menu and create if not found */
412     QStringList tree = label.split(PARENT_SEPARATOR);
413     if (!tree.isEmpty())
414         tree.removeLast();
415     QMenu * parentMenu = findParentMenu(tree, data);
416     if (parentMenu)
417         label = label.mid(label.lastIndexOf(PARENT_SEPARATOR) + QString(PARENT_SEPARATOR).length()).trimmed();
418 
419     QAction *dfb_action = new QAction(label, data->toolbar);
420     if (strlen(fe->comment) > 0)
421     {
422         QString tooltip = QString("%1\n%2").arg(fe->comment).arg(fe->expression);
423         dfb_action->setToolTip(tooltip);
424         dfb_action->setProperty(dfe_property_comment_, tooltip);
425     }
426     else
427     {
428         dfb_action->setToolTip(fe->expression);
429         dfb_action->setProperty(dfe_property_comment_, QString(fe->expression));
430     }
431     dfb_action->setData(fe->expression);
432     dfb_action->setProperty(dfe_property_, true);
433     dfb_action->setProperty(dfe_property_label_, QString(fe->label));
434     dfb_action->setProperty(dfe_property_expression_, QString(fe->expression));
435 
436     if (data->actions_added) {
437         QFrame *sep = new QFrame();
438         sep->setEnabled(false);
439         data->toolbar->addWidget(sep);
440     }
441 
442     if (parentMenu)
443         parentMenu->addAction(dfb_action);
444     else
445         data->toolbar->addAction(dfb_action);
446 
447     connect(dfb_action, &QAction::triggered, data->toolbar, &FilterExpressionToolBar::filterClicked);
448     data->actions_added = true;
449     return FALSE;
450 }
451 
filterClicked()452 void FilterExpressionToolBar::filterClicked()
453 {
454     bool prepare = false;
455     QAction *dfb_action = qobject_cast<QAction*>(sender());
456 
457     if (!dfb_action)
458         return;
459 
460     QString filterText = dfb_action->data().toString();
461     prepare = (QApplication::keyboardModifiers() & Qt::ShiftModifier);
462 
463     emit filterSelected(filterText, prepare);
464 }
465