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

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

see #15182 - remove dependence on GUI from data.osm.search.SearchCompiler

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