source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/SearchDialog.java@ 16306

Last change on this file since 16306 was 16306, checked in by simon04, 4 years ago

SearchDialog: escape HTML characters from SearchParseError

  • Property svn:eol-style set to native
File size: 24.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trc;
6
7import java.awt.Cursor;
8import java.awt.Dimension;
9import java.awt.FlowLayout;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.ItemEvent;
13import java.awt.event.ItemListener;
14import java.awt.event.MouseAdapter;
15import java.awt.event.MouseEvent;
16import java.util.Arrays;
17import java.util.List;
18
19import javax.swing.BorderFactory;
20import javax.swing.ButtonGroup;
21import javax.swing.JCheckBox;
22import javax.swing.JLabel;
23import javax.swing.JOptionPane;
24import javax.swing.JPanel;
25import javax.swing.JRadioButton;
26import javax.swing.SwingUtilities;
27import javax.swing.text.BadLocationException;
28import javax.swing.text.Document;
29import javax.swing.text.JTextComponent;
30
31import org.openstreetmap.josm.data.osm.Filter;
32import org.openstreetmap.josm.data.osm.search.SearchCompiler;
33import org.openstreetmap.josm.data.osm.search.SearchMode;
34import org.openstreetmap.josm.data.osm.search.SearchParseError;
35import org.openstreetmap.josm.data.osm.search.SearchSetting;
36import org.openstreetmap.josm.gui.ExtendedDialog;
37import org.openstreetmap.josm.gui.MainApplication;
38import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
39import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
40import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
41import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
42import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
43import org.openstreetmap.josm.tools.GBC;
44import org.openstreetmap.josm.tools.JosmRuntimeException;
45import org.openstreetmap.josm.tools.Logging;
46import org.openstreetmap.josm.tools.Utils;
47
48/**
49 * Search dialog to find primitives by a wide range of search criteria.
50 * @since 14927 (extracted from {@code SearchAction})
51 */
52public class SearchDialog extends ExtendedDialog {
53
54 private final SearchSetting searchSettings;
55
56 private final HistoryComboBox hcbSearchString = new HistoryComboBox();
57
58 private JCheckBox addOnToolbar;
59 private JCheckBox caseSensitive;
60 private JCheckBox allElements;
61
62 private JRadioButton standardSearch;
63 private JRadioButton regexSearch;
64 private JRadioButton mapCSSSearch;
65
66 private JRadioButton replace;
67 private JRadioButton add;
68 private JRadioButton remove;
69 private JRadioButton inSelection;
70
71 /**
72 * Constructs a new {@code SearchDialog}.
73 * @param initialValues initial search settings
74 * @param searchExpressionHistory list of all texts that were recently used in the search
75 * @param expertMode expert mode
76 */
77 public SearchDialog(SearchSetting initialValues, List<String> searchExpressionHistory, boolean expertMode) {
78 super(MainApplication.getMainFrame(),
79 initialValues instanceof Filter ? tr("Filter") : tr("Search"),
80 initialValues instanceof Filter ? tr("Submit filter") : tr("Search"),
81 tr("Cancel"));
82 this.searchSettings = new SearchSetting(initialValues);
83 setButtonIcons("dialogs/search", "cancel");
84 configureContextsensitiveHelp("/Action/Search", true /* show help button */);
85 setContent(buildPanel(searchExpressionHistory, expertMode));
86 }
87
88 private JPanel buildPanel(List<String> searchExpressionHistory, boolean expertMode) {
89
90 // prepare the combo box with the search expressions
91 JLabel label = new JLabel(searchSettings instanceof Filter ? tr("Filter string:") : tr("Search string:"));
92
93 String tooltip = tr("Enter the search expression");
94 hcbSearchString.setText(searchSettings.text);
95 hcbSearchString.setToolTipText(tooltip);
96
97 hcbSearchString.setPossibleItemsTopDown(searchExpressionHistory);
98 hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height));
99 label.setLabelFor(hcbSearchString);
100
101 replace = new JRadioButton(tr("select"), searchSettings.mode == SearchMode.replace);
102 add = new JRadioButton(tr("add to selection"), searchSettings.mode == SearchMode.add);
103 remove = new JRadioButton(tr("remove from selection"), searchSettings.mode == SearchMode.remove);
104 inSelection = new JRadioButton(tr("find in selection"), searchSettings.mode == SearchMode.in_selection);
105 ButtonGroup bg = new ButtonGroup();
106 bg.add(replace);
107 bg.add(add);
108 bg.add(remove);
109 bg.add(inSelection);
110
111 caseSensitive = new JCheckBox(tr("case sensitive"), searchSettings.caseSensitive);
112 allElements = new JCheckBox(tr("all objects"), searchSettings.allElements);
113 allElements.setToolTipText(tr("Also include incomplete and deleted objects in search."));
114 addOnToolbar = new JCheckBox(tr("add toolbar button"), false);
115 addOnToolbar.setToolTipText(tr("Add a button with this search expression to the toolbar."));
116
117 standardSearch = new JRadioButton(tr("standard"), !searchSettings.regexSearch && !searchSettings.mapCSSSearch);
118 regexSearch = new JRadioButton(tr("regular expression"), searchSettings.regexSearch);
119 mapCSSSearch = new JRadioButton(tr("MapCSS selector"), searchSettings.mapCSSSearch);
120
121 ButtonGroup bg2 = new ButtonGroup();
122 bg2.add(standardSearch);
123 bg2.add(regexSearch);
124 bg2.add(mapCSSSearch);
125
126 JPanel selectionSettings = new JPanel(new GridBagLayout());
127 selectionSettings.setBorder(BorderFactory.createTitledBorder(tr("Results")));
128 selectionSettings.add(replace, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
129 selectionSettings.add(add, GBC.eol());
130 selectionSettings.add(remove, GBC.eol());
131 selectionSettings.add(inSelection, GBC.eop());
132
133 JPanel additionalSettings = new JPanel(new GridBagLayout());
134 additionalSettings.setBorder(BorderFactory.createTitledBorder(tr("Options")));
135 additionalSettings.add(caseSensitive, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
136
137 JPanel left = new JPanel(new GridBagLayout());
138
139 left.add(selectionSettings, GBC.eol().fill(GBC.BOTH));
140 left.add(additionalSettings, GBC.eol().fill(GBC.BOTH));
141
142 if (expertMode) {
143 additionalSettings.add(allElements, GBC.eol());
144 additionalSettings.add(addOnToolbar, GBC.eop());
145
146 JPanel searchOptions = new JPanel(new GridBagLayout());
147 searchOptions.setBorder(BorderFactory.createTitledBorder(tr("Search syntax")));
148 searchOptions.add(standardSearch, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
149 searchOptions.add(regexSearch, GBC.eol());
150 searchOptions.add(mapCSSSearch, GBC.eol());
151
152 left.add(searchOptions, GBC.eol().fill(GBC.BOTH));
153 }
154
155 JPanel right = buildHintsSection(hcbSearchString, expertMode);
156 JPanel top = new JPanel(new GridBagLayout());
157 top.add(label, GBC.std().insets(0, 0, 5, 0));
158 top.add(hcbSearchString, GBC.eol().fill(GBC.HORIZONTAL));
159
160 JTextComponent editorComponent = hcbSearchString.getEditorComponent();
161 Document document = editorComponent.getDocument();
162
163 /*
164 * Setup the logic to validate the contents of the search text field which is executed
165 * every time the content of the field has changed. If the query is incorrect, then
166 * the text field is colored red.
167 */
168 AbstractTextComponentValidator validator = new AbstractTextComponentValidator(editorComponent) {
169
170 @Override
171 public void validate() {
172 if (!isValid()) {
173 feedbackInvalid(tr("Invalid search expression"));
174 } else {
175 feedbackValid(tooltip);
176 }
177 }
178
179 @Override
180 public boolean isValid() {
181 try {
182 SearchSetting ss = new SearchSetting();
183 ss.text = hcbSearchString.getText();
184 ss.caseSensitive = caseSensitive.isSelected();
185 ss.regexSearch = regexSearch.isSelected();
186 ss.mapCSSSearch = mapCSSSearch.isSelected();
187 SearchCompiler.compile(ss);
188 return true;
189 } catch (SearchParseError | MapCSSException e) {
190 Logging.trace(e);
191 return false;
192 }
193 }
194 };
195 document.addDocumentListener(validator);
196 ItemListener validateActionListener = e -> {
197 if (e.getStateChange() == ItemEvent.SELECTED) {
198 validator.validate();
199 }
200 };
201 standardSearch.addItemListener(validateActionListener);
202 regexSearch.addItemListener(validateActionListener);
203 mapCSSSearch.addItemListener(validateActionListener);
204
205 /*
206 * Setup the logic to append preset queries to the search text field according to
207 * selected preset by the user. Every query is of the form ' group/sub-group/.../presetName'
208 * if the corresponding group of the preset exists, otherwise it is simply ' presetName'.
209 */
210 TaggingPresetSelector selector = new TaggingPresetSelector(false, false);
211 selector.setBorder(BorderFactory.createTitledBorder(tr("Search by preset")));
212 selector.setDblClickListener(ev -> setPresetDblClickListener(selector, editorComponent));
213
214 JPanel p = new JPanel(new GridBagLayout());
215 p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0));
216 p.add(left, GBC.std().anchor(GBC.NORTH).insets(5, 10, 10, 0).fill(GBC.VERTICAL));
217 p.add(right, GBC.std().fill(GBC.BOTH).insets(0, 10, 0, 0));
218 p.add(selector, GBC.eol().fill(GBC.BOTH).insets(0, 10, 0, 0));
219
220 return p;
221 }
222
223 @Override
224 protected void buttonAction(int buttonIndex, ActionEvent evt) {
225 if (buttonIndex == 0) {
226 try {
227 SearchSetting ss = new SearchSetting();
228 ss.text = hcbSearchString.getText();
229 ss.caseSensitive = caseSensitive.isSelected();
230 ss.regexSearch = regexSearch.isSelected();
231 ss.mapCSSSearch = mapCSSSearch.isSelected();
232 SearchCompiler.compile(ss);
233 super.buttonAction(buttonIndex, evt);
234 } catch (SearchParseError | MapCSSException e) {
235 Logging.warn(e);
236 String message = Utils.escapeReservedCharactersHTML(e.getMessage()
237 .replace("<html>", "")
238 .replace("</html>", ""));
239 JOptionPane.showMessageDialog(
240 MainApplication.getMainFrame(),
241 "<html>" + tr("Search expression is not valid: \n\n {0}", message).replace("\n", "<br>") + "</html>",
242 tr("Invalid search expression"),
243 JOptionPane.ERROR_MESSAGE);
244 }
245 } else {
246 super.buttonAction(buttonIndex, evt);
247 }
248 }
249
250 /**
251 * Returns the search settings chosen by user.
252 * @return the search settings chosen by user
253 */
254 public SearchSetting getSearchSettings() {
255 searchSettings.text = hcbSearchString.getText();
256 searchSettings.caseSensitive = caseSensitive.isSelected();
257 searchSettings.allElements = allElements.isSelected();
258 searchSettings.regexSearch = regexSearch.isSelected();
259 searchSettings.mapCSSSearch = mapCSSSearch.isSelected();
260
261 if (inSelection.isSelected()) {
262 searchSettings.mode = SearchMode.in_selection;
263 } else if (replace.isSelected()) {
264 searchSettings.mode = SearchMode.replace;
265 } else if (add.isSelected()) {
266 searchSettings.mode = SearchMode.add;
267 } else {
268 searchSettings.mode = SearchMode.remove;
269 }
270 return searchSettings;
271 }
272
273 /**
274 * Determines if the "add toolbar button" checkbox is selected.
275 * @return {@code true} if the "add toolbar button" checkbox is selected
276 */
277 public boolean isAddOnToolbar() {
278 return addOnToolbar.isSelected();
279 }
280
281 private static JPanel buildHintsSection(HistoryComboBox hcbSearchString, boolean expertMode) {
282 JPanel hintPanel = new JPanel(new GridBagLayout());
283 hintPanel.setBorder(BorderFactory.createTitledBorder(tr("Hints")));
284
285 hintPanel.add(new SearchKeywordRow(hcbSearchString)
286 .addTitle(tr("basics"))
287 .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key"))
288 .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key"))
289 .addKeyword("<i>key</i>:<i>valuefragment</i>", null,
290 tr("''valuefragment'' anywhere in ''key''"),
291 trc("search string example", "name:str matches name=Bakerstreet"))
292 .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''")),
293 GBC.eol());
294 hintPanel.add(new SearchKeywordRow(hcbSearchString)
295 .addKeyword("<i>key:</i>", null, tr("matches if ''key'' exists"))
296 .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''"))
297 .addKeyword("<i>key</i>~<i>regexp</i>", null, tr("value of ''key'' matching the regular expression ''regexp''"))
298 .addKeyword("<i>key</i>=*", null, tr("''key'' with any value"))
299 .addKeyword("<i>key</i>=", null, tr("''key'' with empty value"))
300 .addKeyword("*=<i>value</i>", null, tr("''value'' in any key"))
301 .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)"))
302 .addKeyword("\"key\"=\"value\"", "\"\"=\"\"",
303 tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " +
304 "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."),
305 trc("search string example", "name=\"Baker Street\""),
306 "\"addr:street\""),
307 GBC.eol().anchor(GBC.CENTER));
308 hintPanel.add(new SearchKeywordRow(hcbSearchString)
309 .addTitle(tr("combinators"))
310 .addKeyword("<i>expr</i> <i>expr</i>", null,
311 tr("logical and (both expressions have to be satisfied)"),
312 trc("search string example", "Baker Street"))
313 .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)"))
314 .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)"))
315 .addKeyword("-<i>expr</i>", null, tr("logical not"))
316 .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")),
317 GBC.eol());
318
319 if (expertMode) {
320 hintPanel.add(new SearchKeywordRow(hcbSearchString)
321 .addTitle(tr("objects"))
322 .addKeyword("type:node", "type:node ", tr("all nodes"))
323 .addKeyword("type:way", "type:way ", tr("all ways"))
324 .addKeyword("type:relation", "type:relation ", tr("all relations"))
325 .addKeyword("closed", "closed ", tr("all closed ways"))
326 .addKeyword("untagged", "untagged ", tr("object without useful tags")),
327 GBC.eol());
328 hintPanel.add(new SearchKeywordRow(hcbSearchString)
329 .addKeyword("preset:\"Annotation/Address\"", "preset:\"Annotation/Address\"",
330 tr("all objects that use the address preset"))
331 .addKeyword("preset:\"Geography/Nature/*\"", "preset:\"Geography/Nature/*\"",
332 tr("all objects that use any preset under the Geography/Nature group")),
333 GBC.eol().anchor(GBC.CENTER));
334 hintPanel.add(new SearchKeywordRow(hcbSearchString)
335 .addTitle(tr("metadata"))
336 .addKeyword("user:", "user:", tr("objects changed by author"),
337 trc("search string example", "user:<i>OSM username</i> (objects with the author <i>OSM username</i>)"),
338 trc("search string example", "user:anonymous (objects without an assigned author)"))
339 .addKeyword("id:", "id:", tr("objects with given ID"),
340 trc("search string example", "id:0 (new objects)"))
341 .addKeyword("version:", "version:", tr("objects with given version"),
342 trc("search string example", "version:0 (objects without an assigned version)"))
343 .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"),
344 trc("search string example", "changeset:0 (objects without an assigned changeset)"))
345 .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/",
346 "timestamp:2008/2011-02-04T12"),
347 GBC.eol());
348 hintPanel.add(new SearchKeywordRow(hcbSearchString)
349 .addTitle(tr("properties"))
350 .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes"))
351 .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways"))
352 .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags"))
353 .addKeyword("role:", "role:", tr("objects with given role in a relation"))
354 .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2"))
355 .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")),
356 GBC.eol());
357 hintPanel.add(new SearchKeywordRow(hcbSearchString)
358 .addTitle(tr("state"))
359 .addKeyword("modified", "modified ", tr("all modified objects"))
360 .addKeyword("new", "new ", tr("all new objects"))
361 .addKeyword("selected", "selected ", tr("all selected objects"))
362 .addKeyword("incomplete", "incomplete ", tr("all incomplete objects"))
363 .addKeyword("deleted", "deleted ", tr("all deleted objects (checkbox <b>{0}</b> must be enabled)", tr("all objects"))),
364 GBC.eol());
365 hintPanel.add(new SearchKeywordRow(hcbSearchString)
366 .addTitle(tr("related objects"))
367 .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building")
368 .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop")
369 .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>"))
370 .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>"))
371 .addKeyword("nth:<i>7</i>", "nth:",
372 tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1")
373 .addKeyword("nth%:<i>7</i>", "nth%:",
374 tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"),
375 GBC.eol());
376 hintPanel.add(new SearchKeywordRow(hcbSearchString)
377 .addTitle(tr("view"))
378 .addKeyword("inview", "inview ", tr("objects in current view"))
379 .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view"))
380 .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area"))
381 .addKeyword("allindownloadedarea", "allindownloadedarea ",
382 tr("objects (and all its way nodes / relation members) in downloaded area")),
383 GBC.eol());
384 }
385
386 return hintPanel;
387 }
388
389 /**
390 *
391 * @param selector Selector component that the user interacts with
392 * @param searchEditor Editor for search queries
393 */
394 private static void setPresetDblClickListener(TaggingPresetSelector selector, JTextComponent searchEditor) {
395 TaggingPreset selectedPreset = selector.getSelectedPresetAndUpdateClassification();
396
397 if (selectedPreset == null) {
398 return;
399 }
400
401 // Make sure that the focus is transferred to the search text field from the selector component
402 searchEditor.requestFocusInWindow();
403
404 // In order to make interaction with the search dialog simpler, we make sure that
405 // if autocompletion triggers and the text field is not in focus, the correct area is selected.
406 // We first request focus and then execute the selection logic.
407 // invokeLater allows us to defer the selection until waiting for focus.
408 SwingUtilities.invokeLater(() -> {
409 int textOffset = searchEditor.getCaretPosition();
410 String presetSearchQuery = " preset:" +
411 "\"" + selectedPreset.getRawName() + "\"";
412 try {
413 searchEditor.getDocument().insertString(textOffset, presetSearchQuery, null);
414 } catch (BadLocationException e1) {
415 throw new JosmRuntimeException(e1.getMessage(), e1);
416 }
417 });
418 }
419
420 private static class SearchKeywordRow extends JPanel {
421
422 private final HistoryComboBox hcb;
423
424 SearchKeywordRow(HistoryComboBox hcb) {
425 super(new FlowLayout(FlowLayout.LEFT));
426 this.hcb = hcb;
427 }
428
429 /**
430 * Adds the title (prefix) label at the beginning of the row. Should be called only once.
431 * @param title English title
432 * @return {@code this} for easy chaining
433 */
434 public SearchKeywordRow addTitle(String title) {
435 add(new JLabel(tr("{0}: ", title)));
436 return this;
437 }
438
439 /**
440 * Adds an example keyword label at the end of the row. Can be called several times.
441 * @param displayText displayed HTML text
442 * @param insertText optional: if set, makes the label clickable, and {@code insertText} will be inserted in search string
443 * @param description optional: HTML text to be displayed in the tooltip
444 * @param examples optional: examples joined as HTML list in the tooltip
445 * @return {@code this} for easy chaining
446 */
447 public SearchKeywordRow addKeyword(String displayText, final String insertText, String description, String... examples) {
448 JLabel label = new JLabel("<html>"
449 + "<style>td{border:1px solid gray; font-weight:normal;}</style>"
450 + "<table><tr><td>" + displayText + "</td></tr></table></html>");
451 add(label);
452 if (description != null || examples.length > 0) {
453 label.setToolTipText("<html>"
454 + description
455 + (examples.length > 0 ? Utils.joinAsHtmlUnorderedList(Arrays.asList(examples)) : "")
456 + "</html>");
457 }
458 if (insertText != null) {
459 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
460 label.addMouseListener(new MouseAdapter() {
461
462 @Override
463 public void mouseClicked(MouseEvent e) {
464 JTextComponent tf = hcb.getEditorComponent();
465
466 // Make sure that the focus is transferred to the search text field from the selector component
467 if (!tf.hasFocus()) {
468 tf.requestFocusInWindow();
469 }
470
471 // In order to make interaction with the search dialog simpler, we make sure that
472 // if autocompletion triggers and the text field is not in focus, the correct area is selected.
473 // We first request focus and then execute the selection logic.
474 // invokeLater allows us to defer the selection until waiting for focus.
475 SwingUtilities.invokeLater(() -> {
476 try {
477 tf.getDocument().insertString(tf.getCaretPosition(), ' ' + insertText, null);
478 } catch (BadLocationException ex) {
479 throw new JosmRuntimeException(ex.getMessage(), ex);
480 }
481 });
482 }
483 });
484 }
485 return this;
486 }
487 }
488}
Note: See TracBrowser for help on using the repository browser.