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

Last change on this file since 15080 was 15080, checked in by Don-vip, 5 years ago

fix #17526 - Search action dialog: New tooltip and more translations (patch by Hb---)

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