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

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

see #15229 - use Config.getPref() wherever possible

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