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

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

see #15182 - deprecate Main.map and Main.isDisplayingMapView(). Replacements: gui.MainApplication.getMap() / gui.MainApplication.isDisplayingMapView()

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