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

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

see #15560 - fix javadoc warnings with recent JDKs

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