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

Last change on this file since 12841 was 12841, checked in by bastiK, 7 years ago

see #15229 - fix deprecations caused by [12840]

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