source: josm/trunk/src/org/openstreetmap/josm/actions/search/SearchAction.java@ 12578

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

fix some Sonar issues

  • Property svn:eol-style set to native
File size: 44.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.search;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trc;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.Cursor;
10import java.awt.Dimension;
11import java.awt.FlowLayout;
12import java.awt.GraphicsEnvironment;
13import java.awt.GridBagLayout;
14import java.awt.event.ActionEvent;
15import java.awt.event.KeyEvent;
16import java.awt.event.MouseAdapter;
17import java.awt.event.MouseEvent;
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.HashSet;
23import java.util.LinkedHashSet;
24import java.util.LinkedList;
25import java.util.List;
26import java.util.Map;
27import java.util.Objects;
28import java.util.Set;
29import java.util.function.Predicate;
30
31import javax.swing.BorderFactory;
32import javax.swing.ButtonGroup;
33import javax.swing.JCheckBox;
34import javax.swing.JLabel;
35import javax.swing.JOptionPane;
36import javax.swing.JPanel;
37import javax.swing.JRadioButton;
38import javax.swing.SwingUtilities;
39import javax.swing.text.BadLocationException;
40import javax.swing.text.Document;
41import javax.swing.text.JTextComponent;
42
43import org.openstreetmap.josm.Main;
44import org.openstreetmap.josm.actions.ActionParameter;
45import org.openstreetmap.josm.actions.ExpertToggleAction;
46import org.openstreetmap.josm.actions.JosmAction;
47import org.openstreetmap.josm.actions.ParameterizedAction;
48import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
49import org.openstreetmap.josm.data.osm.DataSet;
50import org.openstreetmap.josm.data.osm.Filter;
51import org.openstreetmap.josm.data.osm.OsmPrimitive;
52import org.openstreetmap.josm.gui.ExtendedDialog;
53import org.openstreetmap.josm.gui.PleaseWaitRunnable;
54import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
55import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
56import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
57import org.openstreetmap.josm.gui.progress.ProgressMonitor;
58import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
59import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
60import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
61import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
62import org.openstreetmap.josm.tools.GBC;
63import org.openstreetmap.josm.tools.JosmRuntimeException;
64import org.openstreetmap.josm.tools.Shortcut;
65import org.openstreetmap.josm.tools.Utils;
66
67/**
68 * The search action allows the user to search the data layer using a complex search string.
69 *
70 * @see SearchCompiler
71 */
72public class SearchAction extends JosmAction implements ParameterizedAction {
73
74 /**
75 * The default size of the search history
76 */
77 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
78 /**
79 * Maximum number of characters before the search expression is shortened for display purposes.
80 */
81 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100;
82
83 private static final String SEARCH_EXPRESSION = "searchExpression";
84
85 /**
86 * Search mode.
87 */
88 public enum SearchMode {
89 /** replace selection */
90 replace('R'),
91 /** add to selection */
92 add('A'),
93 /** remove from selection */
94 remove('D'),
95 /** find in selection */
96 in_selection('S');
97
98 private final char code;
99
100 SearchMode(char code) {
101 this.code = code;
102 }
103
104 /**
105 * Returns the unique character code of this mode.
106 * @return the unique character code of this mode
107 */
108 public char getCode() {
109 return code;
110 }
111
112 /**
113 * Returns the search mode matching the given character code.
114 * @param code character code
115 * @return search mode matching the given character code
116 */
117 public static SearchMode fromCode(char code) {
118 for (SearchMode mode: values()) {
119 if (mode.getCode() == code)
120 return mode;
121 }
122 return null;
123 }
124 }
125
126 private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>();
127 static {
128 for (String s: Main.pref.getCollection("search.history", Collections.<String>emptyList())) {
129 SearchSetting ss = SearchSetting.readFromString(s);
130 if (ss != null) {
131 searchHistory.add(ss);
132 }
133 }
134 }
135
136 /**
137 * Gets the search history
138 * @return The last searched terms. Do not modify it.
139 */
140 public static Collection<SearchSetting> getSearchHistory() {
141 return searchHistory;
142 }
143
144 /**
145 * Saves a search to the search history.
146 * @param s The search to save
147 */
148 public static void saveToHistory(SearchSetting s) {
149 if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
150 searchHistory.addFirst(new SearchSetting(s));
151 } else if (searchHistory.contains(s)) {
152 // move existing entry to front, fixes #8032 - search history loses entries when re-using queries
153 searchHistory.remove(s);
154 searchHistory.addFirst(new SearchSetting(s));
155 }
156 int maxsize = Main.pref.getInteger("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
157 while (searchHistory.size() > maxsize) {
158 searchHistory.removeLast();
159 }
160 Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size());
161 for (SearchSetting item: searchHistory) {
162 savedHistory.add(item.writeToString());
163 }
164 Main.pref.putCollection("search.history", savedHistory);
165 }
166
167 /**
168 * Gets a list of all texts that were recently used in the search
169 * @return The list of search texts.
170 */
171 public static List<String> getSearchExpressionHistory() {
172 List<String> ret = new ArrayList<>(getSearchHistory().size());
173 for (SearchSetting ss: getSearchHistory()) {
174 ret.add(ss.text);
175 }
176 return ret;
177 }
178
179 private static volatile SearchSetting lastSearch;
180
181 /**
182 * Constructs a new {@code SearchAction}.
183 */
184 public SearchAction() {
185 super(tr("Search..."), "dialogs/search", tr("Search for objects."),
186 Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true);
187 putValue("help", ht("/Action/Search"));
188 }
189
190 @Override
191 public void actionPerformed(ActionEvent e) {
192 if (!isEnabled())
193 return;
194 search();
195 }
196
197 @Override
198 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) {
199 if (parameters.get(SEARCH_EXPRESSION) == null) {
200 actionPerformed(e);
201 } else {
202 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION));
203 }
204 }
205
206 private static class SearchKeywordRow extends JPanel {
207
208 private final HistoryComboBox hcb;
209
210 SearchKeywordRow(HistoryComboBox hcb) {
211 super(new FlowLayout(FlowLayout.LEFT));
212 this.hcb = hcb;
213 }
214
215 public SearchKeywordRow addTitle(String title) {
216 add(new JLabel(tr("{0}: ", title)));
217 return this;
218 }
219
220 public SearchKeywordRow addKeyword(String displayText, final String insertText, String description, String... examples) {
221 JLabel label = new JLabel("<html>"
222 + "<style>td{border:1px solid gray; font-weight:normal;}</style>"
223 + "<table><tr><td>" + displayText + "</td></tr></table></html>");
224 add(label);
225 if (description != null || examples.length > 0) {
226 label.setToolTipText("<html>"
227 + description
228 + (examples.length > 0 ? Utils.joinAsHtmlUnorderedList(Arrays.asList(examples)) : "")
229 + "</html>");
230 }
231 if (insertText != null) {
232 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
233 label.addMouseListener(new MouseAdapter() {
234
235 @Override
236 public void mouseClicked(MouseEvent e) {
237 JTextComponent tf = hcb.getEditorComponent();
238
239 /*
240 * Make sure that the focus is transferred to the search text field
241 * from the selector component.
242 */
243 if (!tf.hasFocus()) {
244 tf.requestFocusInWindow();
245 }
246
247 /*
248 * In order to make interaction with the search dialog simpler,
249 * we make sure that if autocompletion triggers and the text field is
250 * not in focus, the correct area is selected. We first request focus
251 * and then execute the selection logic. invokeLater allows us to
252 * defer the selection until waiting for focus.
253 */
254 SwingUtilities.invokeLater(() -> {
255 try {
256 tf.getDocument().insertString(tf.getCaretPosition(), ' ' + insertText, null);
257 } catch (BadLocationException ex) {
258 throw new JosmRuntimeException(ex.getMessage(), ex);
259 }
260 });
261 }
262 });
263 }
264 return this;
265 }
266 }
267
268 /**
269 * Builds and shows the search dialog.
270 * @param initialValues A set of initial values needed in order to initialize the search dialog.
271 * If is {@code null}, then default settings are used.
272 * @return Returns {@link SearchAction} object containing parameters of the search.
273 */
274 public static SearchSetting showSearchDialog(SearchSetting initialValues) {
275 if (initialValues == null) {
276 initialValues = new SearchSetting();
277 }
278
279 // prepare the combo box with the search expressions
280 JLabel label = new JLabel(initialValues instanceof Filter ? tr("Filter string:") : tr("Search string:"));
281 HistoryComboBox hcbSearchString = new HistoryComboBox();
282 String tooltip = tr("Enter the search expression");
283 hcbSearchString.setText(initialValues.text);
284 hcbSearchString.setToolTipText(tooltip);
285
286 // we have to reverse the history, because ComboBoxHistory will reverse it again in addElement()
287 List<String> searchExpressionHistory = getSearchExpressionHistory();
288 Collections.reverse(searchExpressionHistory);
289 hcbSearchString.setPossibleItems(searchExpressionHistory);
290 hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height));
291 label.setLabelFor(hcbSearchString);
292
293 JRadioButton replace = new JRadioButton(tr("replace selection"), initialValues.mode == SearchMode.replace);
294 JRadioButton add = new JRadioButton(tr("add to selection"), initialValues.mode == SearchMode.add);
295 JRadioButton remove = new JRadioButton(tr("remove from selection"), initialValues.mode == SearchMode.remove);
296 JRadioButton inSelection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection);
297 ButtonGroup bg = new ButtonGroup();
298 bg.add(replace);
299 bg.add(add);
300 bg.add(remove);
301 bg.add(inSelection);
302
303 JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive);
304 JCheckBox allElements = new JCheckBox(tr("all objects"), initialValues.allElements);
305 allElements.setToolTipText(tr("Also include incomplete and deleted objects in search."));
306 JCheckBox addOnToolbar = new JCheckBox(tr("add toolbar button"), false);
307
308 JRadioButton standardSearch = new JRadioButton(tr("standard"), !initialValues.regexSearch && !initialValues.mapCSSSearch);
309 JRadioButton regexSearch = new JRadioButton(tr("regular expression"), initialValues.regexSearch);
310 JRadioButton mapCSSSearch = new JRadioButton(tr("MapCSS selector"), initialValues.mapCSSSearch);
311 ButtonGroup bg2 = new ButtonGroup();
312 bg2.add(standardSearch);
313 bg2.add(regexSearch);
314 bg2.add(mapCSSSearch);
315
316 JPanel selectionSettings = new JPanel(new GridBagLayout());
317 selectionSettings.setBorder(BorderFactory.createTitledBorder(tr("Selection settings")));
318 selectionSettings.add(replace, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
319 selectionSettings.add(add, GBC.eol());
320 selectionSettings.add(remove, GBC.eol());
321 selectionSettings.add(inSelection, GBC.eop());
322
323 JPanel additionalSettings = new JPanel(new GridBagLayout());
324 additionalSettings.setBorder(BorderFactory.createTitledBorder(tr("Additional settings")));
325 additionalSettings.add(caseSensitive, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
326
327 JPanel left = new JPanel(new GridBagLayout());
328
329 left.add(selectionSettings, GBC.eol().fill(GBC.BOTH));
330 left.add(additionalSettings, GBC.eol().fill(GBC.BOTH));
331
332 if (ExpertToggleAction.isExpert()) {
333 additionalSettings.add(allElements, GBC.eol());
334 additionalSettings.add(addOnToolbar, GBC.eop());
335
336 JPanel searchOptions = new JPanel(new GridBagLayout());
337 searchOptions.setBorder(BorderFactory.createTitledBorder(tr("Search syntax")));
338 searchOptions.add(standardSearch, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
339 searchOptions.add(regexSearch, GBC.eol());
340 searchOptions.add(mapCSSSearch, GBC.eol());
341
342 left.add(searchOptions, GBC.eol().fill(GBC.BOTH));
343 }
344
345 JPanel right = SearchAction.buildHintsSection(hcbSearchString);
346 JPanel top = new JPanel(new GridBagLayout());
347 top.add(label, GBC.std().insets(0, 0, 5, 0));
348 top.add(hcbSearchString, GBC.eol().fill(GBC.HORIZONTAL));
349
350 JTextComponent editorComponent = hcbSearchString.getEditorComponent();
351 Document document = editorComponent.getDocument();
352
353 /*
354 * Setup the logic to validate the contents of the search text field which is executed
355 * every time the content of the field has changed. If the query is incorrect, then
356 * the text field is colored red.
357 */
358 document.addDocumentListener(new AbstractTextComponentValidator(editorComponent) {
359
360 @Override
361 public void validate() {
362 if (!isValid()) {
363 feedbackInvalid(tr("Invalid search expression"));
364 } else {
365 feedbackValid(tooltip);
366 }
367 }
368
369 @Override
370 public boolean isValid() {
371 try {
372 SearchSetting ss = new SearchSetting();
373 ss.text = hcbSearchString.getText();
374 ss.caseSensitive = caseSensitive.isSelected();
375 ss.regexSearch = regexSearch.isSelected();
376 ss.mapCSSSearch = mapCSSSearch.isSelected();
377 SearchCompiler.compile(ss);
378 return true;
379 } catch (ParseError | MapCSSException e) {
380 return false;
381 }
382 }
383 });
384
385 /*
386 * Setup the logic to append preset queries to the search text field according to
387 * selected preset by the user. Every query is of the form ' group/sub-group/.../presetName'
388 * if the corresponding group of the preset exists, otherwise it is simply ' presetName'.
389 */
390 TaggingPresetSelector selector = new TaggingPresetSelector(false, false);
391 selector.setBorder(BorderFactory.createTitledBorder(tr("Search by preset")));
392 selector.setDblClickListener(ev -> setPresetDblClickListener(selector, editorComponent));
393
394 JPanel p = new JPanel(new GridBagLayout());
395 p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0));
396 p.add(left, GBC.std().anchor(GBC.NORTH).insets(5, 10, 10, 0).fill(GBC.VERTICAL));
397 p.add(right, GBC.std().fill(GBC.BOTH).insets(0, 10, 0, 0));
398 p.add(selector, GBC.eol().fill(GBC.BOTH).insets(0, 10, 0, 0));
399
400 ExtendedDialog dialog = new ExtendedDialog(
401 Main.parent,
402 initialValues instanceof Filter ? tr("Filter") : tr("Search"),
403 initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"),
404 tr("Cancel")
405 ) {
406 @Override
407 protected void buttonAction(int buttonIndex, ActionEvent evt) {
408 if (buttonIndex == 0) {
409 try {
410 SearchSetting ss = new SearchSetting();
411 ss.text = hcbSearchString.getText();
412 ss.caseSensitive = caseSensitive.isSelected();
413 ss.regexSearch = regexSearch.isSelected();
414 ss.mapCSSSearch = mapCSSSearch.isSelected();
415 SearchCompiler.compile(ss);
416 super.buttonAction(buttonIndex, evt);
417 } catch (ParseError e) {
418 Main.debug(e);
419 JOptionPane.showMessageDialog(
420 Main.parent,
421 tr("Search expression is not valid: \n\n {0}", e.getMessage()),
422 tr("Invalid search expression"),
423 JOptionPane.ERROR_MESSAGE);
424 }
425 } else {
426 super.buttonAction(buttonIndex, evt);
427 }
428 }
429 };
430 dialog.setButtonIcons("dialogs/search", "cancel");
431 dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */);
432 dialog.setContent(p);
433
434 if (dialog.showDialog().getValue() != 1) return null;
435
436 // User pressed OK - let's perform the search
437 initialValues.text = hcbSearchString.getText();
438 initialValues.caseSensitive = caseSensitive.isSelected();
439 initialValues.allElements = allElements.isSelected();
440 initialValues.regexSearch = regexSearch.isSelected();
441 initialValues.mapCSSSearch = mapCSSSearch.isSelected();
442
443 if (inSelection.isSelected()) {
444 initialValues.mode = SearchAction.SearchMode.in_selection;
445 } else if (replace.isSelected()) {
446 initialValues.mode = SearchAction.SearchMode.replace;
447 } else if (add.isSelected()) {
448 initialValues.mode = SearchAction.SearchMode.add;
449 } else {
450 initialValues.mode = SearchAction.SearchMode.remove;
451 }
452
453 if (addOnToolbar.isSelected()) {
454 ToolbarPreferences.ActionDefinition aDef =
455 new ToolbarPreferences.ActionDefinition(Main.main.menu.search);
456 aDef.getParameters().put(SEARCH_EXPRESSION, initialValues);
457 // Display search expression as tooltip instead of generic one
458 aDef.setName(Utils.shortenString(initialValues.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY));
459 // parametrized action definition is now composed
460 ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
461 String res = actionParser.saveAction(aDef);
462
463 // add custom search button to toolbar preferences
464 Main.toolbar.addCustomButton(res, -1, false);
465 }
466
467 return initialValues;
468 }
469
470 private static JPanel buildHintsSection(HistoryComboBox hcbSearchString) {
471 JPanel hintPanel = new JPanel(new GridBagLayout());
472 hintPanel.setBorder(BorderFactory.createTitledBorder(tr("Search hints")));
473
474 hintPanel.add(new SearchKeywordRow(hcbSearchString)
475 .addTitle(tr("basics"))
476 .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key"))
477 .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key"))
478 .addKeyword("<i>key</i>:<i>valuefragment</i>", null,
479 tr("''valuefragment'' anywhere in ''key''"), "name:str matches name=Bakerstreet")
480 .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''")),
481 GBC.eol());
482 hintPanel.add(new SearchKeywordRow(hcbSearchString)
483 .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''"))
484 .addKeyword("<i>key</i>=*", null, tr("''key'' with any value"))
485 .addKeyword("*=<i>value</i>", null, tr("''value'' in any key"))
486 .addKeyword("<i>key</i>=", null, tr("matches if ''key'' exists"))
487 .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)"))
488 .addKeyword("\"key\"=\"value\"", "\"\"=\"\"",
489 tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " +
490 "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."),
491 "\"addr:street\""),
492 GBC.eol().anchor(GBC.CENTER));
493 hintPanel.add(new SearchKeywordRow(hcbSearchString)
494 .addTitle(tr("combinators"))
495 .addKeyword("<i>expr</i> <i>expr</i>", null, tr("logical and (both expressions have to be satisfied)"))
496 .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)"))
497 .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)"))
498 .addKeyword("-<i>expr</i>", null, tr("logical not"))
499 .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")),
500 GBC.eol());
501
502 if (ExpertToggleAction.isExpert()) {
503 hintPanel.add(new SearchKeywordRow(hcbSearchString)
504 .addTitle(tr("objects"))
505 .addKeyword("type:node", "type:node ", tr("all nodes"))
506 .addKeyword("type:way", "type:way ", tr("all ways"))
507 .addKeyword("type:relation", "type:relation ", tr("all relations"))
508 .addKeyword("closed", "closed ", tr("all closed ways"))
509 .addKeyword("untagged", "untagged ", tr("object without useful tags")),
510 GBC.eol());
511 hintPanel.add(new SearchKeywordRow(hcbSearchString)
512 .addKeyword("preset:\"Annotation/Address\"", "preset:\"Annotation/Address\"",
513 tr("all objects that use the address preset"))
514 .addKeyword("preset:\"Geography/Nature/*\"", "preset:\"Geography/Nature/*\"",
515 tr("all objects that use any preset under the Geography/Nature group")),
516 GBC.eol().anchor(GBC.CENTER));
517 hintPanel.add(new SearchKeywordRow(hcbSearchString)
518 .addTitle(tr("metadata"))
519 .addKeyword("user:", "user:", tr("objects changed by user", "user:anonymous"))
520 .addKeyword("id:", "id:", tr("objects with given ID"), "id:0 (new objects)")
521 .addKeyword("version:", "version:", tr("objects with given version"), "version:0 (objects without an assigned version)")
522 .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"),
523 "changeset:0 (objects without an assigned changeset)")
524 .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/",
525 "timestamp:2008/2011-02-04T12"),
526 GBC.eol());
527 hintPanel.add(new SearchKeywordRow(hcbSearchString)
528 .addTitle(tr("properties"))
529 .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes"))
530 .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways"))
531 .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags"))
532 .addKeyword("role:", "role:", tr("objects with given role in a relation"))
533 .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2"))
534 .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")),
535 GBC.eol());
536 hintPanel.add(new SearchKeywordRow(hcbSearchString)
537 .addTitle(tr("state"))
538 .addKeyword("modified", "modified ", tr("all modified objects"))
539 .addKeyword("new", "new ", tr("all new objects"))
540 .addKeyword("selected", "selected ", tr("all selected objects"))
541 .addKeyword("incomplete", "incomplete ", tr("all incomplete objects"))
542 .addKeyword("deleted", "deleted ", tr("all deleted objects (checkbox <b>{0}</b> must be enabled)", tr("all objects"))),
543 GBC.eol());
544 hintPanel.add(new SearchKeywordRow(hcbSearchString)
545 .addTitle(tr("related objects"))
546 .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building")
547 .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop")
548 .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>"))
549 .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>"))
550 .addKeyword("nth:<i>7</i>", "nth:",
551 tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1")
552 .addKeyword("nth%:<i>7</i>", "nth%:",
553 tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"),
554 GBC.eol());
555 hintPanel.add(new SearchKeywordRow(hcbSearchString)
556 .addTitle(tr("view"))
557 .addKeyword("inview", "inview ", tr("objects in current view"))
558 .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view"))
559 .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area"))
560 .addKeyword("allindownloadedarea", "allindownloadedarea ",
561 tr("objects (and all its way nodes / relation members) in downloaded area")),
562 GBC.eol());
563 }
564
565 return hintPanel;
566 }
567
568 /**
569 * Launches the dialog for specifying search criteria and runs a search
570 */
571 public static void search() {
572 SearchSetting se = showSearchDialog(lastSearch);
573 if (se != null) {
574 searchWithHistory(se);
575 }
576 }
577
578 /**
579 * Adds the search specified by the settings in <code>s</code> to the
580 * search history and performs the search.
581 *
582 * @param s search settings
583 */
584 public static void searchWithHistory(SearchSetting s) {
585 saveToHistory(s);
586 lastSearch = new SearchSetting(s);
587 search(s);
588 }
589
590 /**
591 * Performs the search specified by the settings in <code>s</code> without saving it to search history.
592 *
593 * @param s search settings
594 */
595 public static void searchWithoutHistory(SearchSetting s) {
596 lastSearch = new SearchSetting(s);
597 search(s);
598 }
599
600 /**
601 * Performs the search specified by the search string {@code search} and the search mode {@code mode}.
602 *
603 * @param search the search string to use
604 * @param mode the search mode to use
605 */
606 public static void search(String search, SearchMode mode) {
607 final SearchSetting searchSetting = new SearchSetting();
608 searchSetting.text = search;
609 searchSetting.mode = mode;
610 search(searchSetting);
611 }
612
613 static void search(SearchSetting s) {
614 SearchTask.newSearchTask(s, new SelectSearchReceiver()).run();
615 }
616
617 /**
618 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search.
619 *
620 * @param search the search string to use
621 * @param mode the search mode to use
622 * @return The result of the search.
623 * @since 10457
624 */
625 public static Collection<OsmPrimitive> searchAndReturn(String search, SearchMode mode) {
626 final SearchSetting searchSetting = new SearchSetting();
627 searchSetting.text = search;
628 searchSetting.mode = mode;
629 CapturingSearchReceiver receiver = new CapturingSearchReceiver();
630 SearchTask.newSearchTask(searchSetting, receiver).run();
631 return receiver.result;
632 }
633
634 /**
635 *
636 * @param selector Selector component that the user interacts with
637 * @param searchEditor Editor for search queries
638 */
639 private static void setPresetDblClickListener(TaggingPresetSelector selector, JTextComponent searchEditor) {
640 TaggingPreset selectedPreset = selector.getSelectedPresetAndUpdateClassification();
641
642 if (selectedPreset == null) {
643 return;
644 }
645
646 /*
647 * Make sure that the focus is transferred to the search text field
648 * from the selector component.
649 */
650 searchEditor.requestFocusInWindow();
651
652 /*
653 * In order to make interaction with the search dialog simpler,
654 * we make sure that if autocompletion triggers and the text field is
655 * not in focus, the correct area is selected. We first request focus
656 * and then execute the selection logic. invokeLater allows us to
657 * defer the selection until waiting for focus.
658 */
659 SwingUtilities.invokeLater(() -> {
660 int textOffset = searchEditor.getCaretPosition();
661 String presetSearchQuery = " preset:" +
662 "\"" + selectedPreset.getRawName() + "\"";
663 try {
664 searchEditor.getDocument().insertString(textOffset, presetSearchQuery, null);
665 } catch (BadLocationException e1) {
666 throw new JosmRuntimeException(e1.getMessage(), e1);
667 }
668 });
669 }
670
671 /**
672 * Interfaces implementing this may receive the result of the current search.
673 * @author Michael Zangl
674 * @since 10457
675 * @since 10600 (functional interface)
676 */
677 @FunctionalInterface
678 interface SearchReceiver {
679 /**
680 * Receive the search result
681 * @param ds The data set searched on.
682 * @param result The result collection, including the initial collection.
683 * @param foundMatches The number of matches added to the result.
684 * @param setting The setting used.
685 */
686 void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting);
687 }
688
689 /**
690 * Select the search result and display a status text for it.
691 */
692 private static class SelectSearchReceiver implements SearchReceiver {
693
694 @Override
695 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting) {
696 ds.setSelected(result);
697 if (foundMatches == 0) {
698 final String msg;
699 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
700 if (setting.mode == SearchMode.replace) {
701 msg = tr("No match found for ''{0}''", text);
702 } else if (setting.mode == SearchMode.add) {
703 msg = tr("Nothing added to selection by searching for ''{0}''", text);
704 } else if (setting.mode == SearchMode.remove) {
705 msg = tr("Nothing removed from selection by searching for ''{0}''", text);
706 } else if (setting.mode == SearchMode.in_selection) {
707 msg = tr("Nothing found in selection by searching for ''{0}''", text);
708 } else {
709 msg = null;
710 }
711 if (Main.map != null) {
712 Main.map.statusLine.setHelpText(msg);
713 }
714 if (!GraphicsEnvironment.isHeadless()) {
715 JOptionPane.showMessageDialog(
716 Main.parent,
717 msg,
718 tr("Warning"),
719 JOptionPane.WARNING_MESSAGE
720 );
721 }
722 } else {
723 Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
724 }
725 }
726 }
727
728 /**
729 * This class stores the result of the search in a local variable.
730 * @author Michael Zangl
731 */
732 private static final class CapturingSearchReceiver implements SearchReceiver {
733 private Collection<OsmPrimitive> result;
734
735 @Override
736 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches,
737 SearchSetting setting) {
738 this.result = result;
739 }
740 }
741
742 static final class SearchTask extends PleaseWaitRunnable {
743 private final DataSet ds;
744 private final SearchSetting setting;
745 private final Collection<OsmPrimitive> selection;
746 private final Predicate<OsmPrimitive> predicate;
747 private boolean canceled;
748 private int foundMatches;
749 private final SearchReceiver resultReceiver;
750
751 private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate,
752 SearchReceiver resultReceiver) {
753 super(tr("Searching"));
754 this.ds = ds;
755 this.setting = setting;
756 this.selection = selection;
757 this.predicate = predicate;
758 this.resultReceiver = resultReceiver;
759 }
760
761 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) {
762 final DataSet ds = Main.getLayerManager().getEditDataSet();
763 return newSearchTask(setting, ds, resultReceiver);
764 }
765
766 /**
767 * Create a new search task for the given search setting.
768 * @param setting The setting to use
769 * @param ds The data set to search on
770 * @param resultReceiver will receive the search result
771 * @return A new search task.
772 */
773 private static SearchTask newSearchTask(SearchSetting setting, final DataSet ds, SearchReceiver resultReceiver) {
774 final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected());
775 return new SearchTask(ds, setting, selection, ds::isSelected, resultReceiver);
776 }
777
778 @Override
779 protected void cancel() {
780 this.canceled = true;
781 }
782
783 @Override
784 protected void realRun() {
785 try {
786 foundMatches = 0;
787 SearchCompiler.Match matcher = SearchCompiler.compile(setting);
788
789 if (setting.mode == SearchMode.replace) {
790 selection.clear();
791 } else if (setting.mode == SearchMode.in_selection) {
792 foundMatches = selection.size();
793 }
794
795 Collection<OsmPrimitive> all;
796 if (setting.allElements) {
797 all = ds.allPrimitives();
798 } else {
799 all = ds.getPrimitives(OsmPrimitive::isSelectable);
800 }
801 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false);
802 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size()));
803
804 for (OsmPrimitive osm : all) {
805 if (canceled) {
806 return;
807 }
808 if (setting.mode == SearchMode.replace) {
809 if (matcher.match(osm)) {
810 selection.add(osm);
811 ++foundMatches;
812 }
813 } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) {
814 selection.add(osm);
815 ++foundMatches;
816 } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) {
817 selection.remove(osm);
818 ++foundMatches;
819 } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) {
820 selection.remove(osm);
821 --foundMatches;
822 }
823 subMonitor.worked(1);
824 }
825 subMonitor.finishTask();
826 } catch (ParseError e) {
827 Main.debug(e);
828 JOptionPane.showMessageDialog(
829 Main.parent,
830 e.getMessage(),
831 tr("Error"),
832 JOptionPane.ERROR_MESSAGE
833 );
834 }
835 }
836
837 @Override
838 protected void finish() {
839 if (canceled) {
840 return;
841 }
842 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting);
843 }
844 }
845
846 /**
847 * This class defines a set of parameters that is used to
848 * perform search within the search dialog.
849 */
850 public static class SearchSetting {
851 public String text;
852 public SearchMode mode;
853 public boolean caseSensitive;
854 public boolean regexSearch;
855 public boolean mapCSSSearch;
856 public boolean allElements;
857
858 /**
859 * Constructs a new {@code SearchSetting}.
860 */
861 public SearchSetting() {
862 text = "";
863 mode = SearchMode.replace;
864 }
865
866 /**
867 * Constructs a new {@code SearchSetting} from an existing one.
868 * @param original original search settings
869 */
870 public SearchSetting(SearchSetting original) {
871 text = original.text;
872 mode = original.mode;
873 caseSensitive = original.caseSensitive;
874 regexSearch = original.regexSearch;
875 mapCSSSearch = original.mapCSSSearch;
876 allElements = original.allElements;
877 }
878
879 @Override
880 public String toString() {
881 String cs = caseSensitive ?
882 /*case sensitive*/ trc("search", "CS") :
883 /*case insensitive*/ trc("search", "CI");
884 String rx = regexSearch ? ", " +
885 /*regex search*/ trc("search", "RX") : "";
886 String css = mapCSSSearch ? ", " +
887 /*MapCSS search*/ trc("search", "CSS") : "";
888 String all = allElements ? ", " +
889 /*all elements*/ trc("search", "A") : "";
890 return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')';
891 }
892
893 @Override
894 public boolean equals(Object other) {
895 if (this == other) return true;
896 if (other == null || getClass() != other.getClass()) return false;
897 SearchSetting that = (SearchSetting) other;
898 return caseSensitive == that.caseSensitive &&
899 regexSearch == that.regexSearch &&
900 mapCSSSearch == that.mapCSSSearch &&
901 allElements == that.allElements &&
902 mode == that.mode &&
903 Objects.equals(text, that.text);
904 }
905
906 @Override
907 public int hashCode() {
908 return Objects.hash(text, mode, caseSensitive, regexSearch, mapCSSSearch, allElements);
909 }
910
911 /**
912 * <p>Transforms a string following a certain format, namely "[R | A | D | S][C?,R?,A?,M?] [a-zA-Z]"
913 * where the first part defines the mode of the search, see {@link SearchMode}, the second defines
914 * a set of attributes within the {@code SearchSetting} class and the second is the search query.
915 * <p>
916 * Attributes are as follows:
917 * <ul>
918 * <li>C - if search is case sensitive
919 * <li>R - if the regex syntax is used
920 * <li>A - if all objects are considered
921 * <li>M - if the mapCSS syntax is used
922 * </ul>
923 * <p>For example, "RC type:node" is a valid string representation of an object that replaces the
924 * current selection, is case sensitive and searches for all objects of type node.
925 * @param s A string representation of a {@code SearchSetting} object
926 * from which the object must be built.
927 * @return A {@code SearchSetting} defined by the input string.
928 */
929 public static SearchSetting readFromString(String s) {
930 if (s.isEmpty())
931 return null;
932
933 SearchSetting result = new SearchSetting();
934
935 int index = 1;
936
937 result.mode = SearchMode.fromCode(s.charAt(0));
938 if (result.mode == null) {
939 result.mode = SearchMode.replace;
940 index = 0;
941 }
942
943 while (index < s.length()) {
944 if (s.charAt(index) == 'C') {
945 result.caseSensitive = true;
946 } else if (s.charAt(index) == 'R') {
947 result.regexSearch = true;
948 } else if (s.charAt(index) == 'A') {
949 result.allElements = true;
950 } else if (s.charAt(index) == 'M') {
951 result.mapCSSSearch = true;
952 } else if (s.charAt(index) == ' ') {
953 break;
954 } else {
955 Main.warn("Unknown char in SearchSettings: " + s);
956 break;
957 }
958 index++;
959 }
960
961 if (index < s.length() && s.charAt(index) == ' ') {
962 index++;
963 }
964
965 result.text = s.substring(index);
966
967 return result;
968 }
969
970 /**
971 * Builds a string representation of the {@code SearchSetting} object,
972 * see {@link #readFromString(String)} for more details.
973 * @return A string representation of the {@code SearchSetting} object.
974 */
975 public String writeToString() {
976 if (text == null || text.isEmpty())
977 return "";
978
979 StringBuilder result = new StringBuilder();
980 result.append(mode.getCode());
981 if (caseSensitive) {
982 result.append('C');
983 }
984 if (regexSearch) {
985 result.append('R');
986 }
987 if (mapCSSSearch) {
988 result.append('M');
989 }
990 if (allElements) {
991 result.append('A');
992 }
993 result.append(' ')
994 .append(text);
995 return result.toString();
996 }
997 }
998
999 /**
1000 * {@link ActionParameter} implementation with {@link SearchSetting} as value type.
1001 * @since 12547 (moved from {@link ActionParameter})
1002 */
1003 public static class SearchSettingsActionParameter extends ActionParameter<SearchSetting> {
1004
1005 /**
1006 * Constructs a new {@code SearchSettingsActionParameter}.
1007 * @param name parameter name (the key)
1008 */
1009 public SearchSettingsActionParameter(String name) {
1010 super(name);
1011 }
1012
1013 @Override
1014 public Class<SearchSetting> getType() {
1015 return SearchSetting.class;
1016 }
1017
1018 @Override
1019 public SearchSetting readFromString(String s) {
1020 return SearchSetting.readFromString(s);
1021 }
1022
1023 @Override
1024 public String writeToString(SearchSetting value) {
1025 if (value == null)
1026 return "";
1027 return value.writeToString();
1028 }
1029 }
1030
1031 /**
1032 * Refreshes the enabled state
1033 *
1034 */
1035 @Override
1036 protected void updateEnabledState() {
1037 setEnabled(getLayerManager().getEditLayer() != null);
1038 }
1039
1040 @Override
1041 public List<ActionParameter<?>> getActionParameters() {
1042 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
1043 }
1044}
Note: See TracBrowser for help on using the repository browser.