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

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

use IPrimitive in SearchAction

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