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

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

fix #15755 - Unprocessed HTML in search error box

  • Property svn:eol-style set to native
File size: 39.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.search;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.Cursor;
10import java.awt.Dimension;
11import java.awt.FlowLayout;
12import java.awt.GraphicsEnvironment;
13import java.awt.GridBagLayout;
14import java.awt.event.ActionEvent;
15import java.awt.event.KeyEvent;
16import java.awt.event.MouseAdapter;
17import java.awt.event.MouseEvent;
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.HashSet;
23import java.util.LinkedHashSet;
24import java.util.LinkedList;
25import java.util.List;
26import java.util.Map;
27import java.util.Set;
28import java.util.function.Predicate;
29
30import javax.swing.BorderFactory;
31import javax.swing.ButtonGroup;
32import javax.swing.JCheckBox;
33import javax.swing.JLabel;
34import javax.swing.JOptionPane;
35import javax.swing.JPanel;
36import javax.swing.JRadioButton;
37import javax.swing.SwingUtilities;
38import javax.swing.text.BadLocationException;
39import javax.swing.text.Document;
40import javax.swing.text.JTextComponent;
41
42import org.openstreetmap.josm.Main;
43import org.openstreetmap.josm.actions.ActionParameter;
44import org.openstreetmap.josm.actions.ExpertToggleAction;
45import org.openstreetmap.josm.actions.JosmAction;
46import org.openstreetmap.josm.actions.ParameterizedAction;
47import org.openstreetmap.josm.data.osm.DataSet;
48import org.openstreetmap.josm.data.osm.Filter;
49import org.openstreetmap.josm.data.osm.OsmPrimitive;
50import org.openstreetmap.josm.data.osm.search.PushbackTokenizer;
51import org.openstreetmap.josm.data.osm.search.SearchCompiler;
52import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
53import org.openstreetmap.josm.data.osm.search.SearchCompiler.SimpleMatchFactory;
54import org.openstreetmap.josm.data.osm.search.SearchMode;
55import org.openstreetmap.josm.data.osm.search.SearchParseError;
56import org.openstreetmap.josm.data.osm.search.SearchSetting;
57import org.openstreetmap.josm.gui.ExtendedDialog;
58import org.openstreetmap.josm.gui.MainApplication;
59import org.openstreetmap.josm.gui.MapFrame;
60import org.openstreetmap.josm.gui.Notification;
61import org.openstreetmap.josm.gui.PleaseWaitRunnable;
62import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
63import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
64import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
65import org.openstreetmap.josm.gui.progress.ProgressMonitor;
66import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
67import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
68import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
69import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
70import org.openstreetmap.josm.spi.preferences.Config;
71import org.openstreetmap.josm.tools.GBC;
72import org.openstreetmap.josm.tools.JosmRuntimeException;
73import org.openstreetmap.josm.tools.Logging;
74import org.openstreetmap.josm.tools.Shortcut;
75import org.openstreetmap.josm.tools.Utils;
76
77/**
78 * The search action allows the user to search the data layer using a complex search string.
79 *
80 * @see SearchCompiler
81 */
82public class SearchAction extends JosmAction implements ParameterizedAction {
83
84 /**
85 * The default size of the search history
86 */
87 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
88 /**
89 * Maximum number of characters before the search expression is shortened for display purposes.
90 */
91 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100;
92
93 private static final String SEARCH_EXPRESSION = "searchExpression";
94
95 private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>();
96 static {
97 SearchCompiler.addMatchFactory(new SimpleMatchFactory() {
98 @Override
99 public Collection<String> getKeywords() {
100 return Arrays.asList("inview", "allinview");
101 }
102
103 @Override
104 public Match get(String keyword, boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) throws SearchParseError {
105 switch(keyword) {
106 case "inview":
107 return new InView(false);
108 case "allinview":
109 return new InView(true);
110 default:
111 throw new IllegalStateException("Not expecting keyword " + keyword);
112 }
113 }
114 });
115
116 for (String s: Config.getPref().getList("search.history", Collections.<String>emptyList())) {
117 SearchSetting ss = SearchSetting.readFromString(s);
118 if (ss != null) {
119 searchHistory.add(ss);
120 }
121 }
122 }
123
124 /**
125 * Gets the search history
126 * @return The last searched terms. Do not modify it.
127 */
128 public static Collection<SearchSetting> getSearchHistory() {
129 return searchHistory;
130 }
131
132 /**
133 * Saves a search to the search history.
134 * @param s The search to save
135 */
136 public static void saveToHistory(SearchSetting s) {
137 if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
138 searchHistory.addFirst(new SearchSetting(s));
139 } else if (searchHistory.contains(s)) {
140 // move existing entry to front, fixes #8032 - search history loses entries when re-using queries
141 searchHistory.remove(s);
142 searchHistory.addFirst(new SearchSetting(s));
143 }
144 int maxsize = Config.getPref().getInt("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
145 while (searchHistory.size() > maxsize) {
146 searchHistory.removeLast();
147 }
148 Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size());
149 for (SearchSetting item: searchHistory) {
150 savedHistory.add(item.writeToString());
151 }
152 Config.getPref().putList("search.history", new ArrayList<>(savedHistory));
153 }
154
155 /**
156 * Gets a list of all texts that were recently used in the search
157 * @return The list of search texts.
158 */
159 public static List<String> getSearchExpressionHistory() {
160 List<String> ret = new ArrayList<>(getSearchHistory().size());
161 for (SearchSetting ss: getSearchHistory()) {
162 ret.add(ss.text);
163 }
164 return ret;
165 }
166
167 private static volatile SearchSetting lastSearch;
168
169 /**
170 * Constructs a new {@code SearchAction}.
171 */
172 public SearchAction() {
173 super(tr("Search..."), "dialogs/search", tr("Search for objects."),
174 Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true);
175 putValue("help", ht("/Action/Search"));
176 }
177
178 @Override
179 public void actionPerformed(ActionEvent e) {
180 if (!isEnabled())
181 return;
182 search();
183 }
184
185 @Override
186 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) {
187 if (parameters.get(SEARCH_EXPRESSION) == null) {
188 actionPerformed(e);
189 } else {
190 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION));
191 }
192 }
193
194 private static class SearchKeywordRow extends JPanel {
195
196 private final HistoryComboBox hcb;
197
198 SearchKeywordRow(HistoryComboBox hcb) {
199 super(new FlowLayout(FlowLayout.LEFT));
200 this.hcb = hcb;
201 }
202
203 /**
204 * Adds the title (prefix) label at the beginning of the row. Should be called only once.
205 * @param title English title
206 * @return {@code this} for easy chaining
207 */
208 public SearchKeywordRow addTitle(String title) {
209 add(new JLabel(tr("{0}: ", title)));
210 return this;
211 }
212
213 /**
214 * Adds an example keyword label at the end of the row. Can be called several times.
215 * @param displayText displayed HTML text
216 * @param insertText optional: if set, makes the label clickable, and {@code insertText} will be inserted in search string
217 * @param description optional: HTML text to be displayed in the tooltip
218 * @param examples optional: examples joined as HTML list in the tooltip
219 * @return {@code this} for easy chaining
220 */
221 public SearchKeywordRow addKeyword(String displayText, final String insertText, String description, String... examples) {
222 JLabel label = new JLabel("<html>"
223 + "<style>td{border:1px solid gray; font-weight:normal;}</style>"
224 + "<table><tr><td>" + displayText + "</td></tr></table></html>");
225 add(label);
226 if (description != null || examples.length > 0) {
227 label.setToolTipText("<html>"
228 + description
229 + (examples.length > 0 ? Utils.joinAsHtmlUnorderedList(Arrays.asList(examples)) : "")
230 + "</html>");
231 }
232 if (insertText != null) {
233 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
234 label.addMouseListener(new MouseAdapter() {
235
236 @Override
237 public void mouseClicked(MouseEvent e) {
238 JTextComponent tf = hcb.getEditorComponent();
239
240 /*
241 * Make sure that the focus is transferred to the search text field
242 * from the selector component.
243 */
244 if (!tf.hasFocus()) {
245 tf.requestFocusInWindow();
246 }
247
248 /*
249 * In order to make interaction with the search dialog simpler,
250 * we make sure that if autocompletion triggers and the text field is
251 * not in focus, the correct area is selected. We first request focus
252 * and then execute the selection logic. invokeLater allows us to
253 * defer the selection until waiting for focus.
254 */
255 SwingUtilities.invokeLater(() -> {
256 try {
257 tf.getDocument().insertString(tf.getCaretPosition(), ' ' + insertText, null);
258 } catch (BadLocationException ex) {
259 throw new JosmRuntimeException(ex.getMessage(), ex);
260 }
261 });
262 }
263 });
264 }
265 return this;
266 }
267 }
268
269 /**
270 * Builds and shows the search dialog.
271 * @param initialValues A set of initial values needed in order to initialize the search dialog.
272 * If is {@code null}, then default settings are used.
273 * @return Returns {@link SearchAction} object containing parameters of the search.
274 */
275 public static SearchSetting showSearchDialog(SearchSetting initialValues) {
276 if (initialValues == null) {
277 initialValues = new SearchSetting();
278 }
279
280 // prepare the combo box with the search expressions
281 JLabel label = new JLabel(initialValues instanceof Filter ? tr("Filter string:") : tr("Search string:"));
282 HistoryComboBox hcbSearchString = new HistoryComboBox();
283 String tooltip = tr("Enter the search expression");
284 hcbSearchString.setText(initialValues.text);
285 hcbSearchString.setToolTipText(tooltip);
286
287 // we have to reverse the history, because ComboBoxHistory will reverse it again in addElement()
288 List<String> searchExpressionHistory = getSearchExpressionHistory();
289 Collections.reverse(searchExpressionHistory);
290 hcbSearchString.setPossibleItems(searchExpressionHistory);
291 hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height));
292 label.setLabelFor(hcbSearchString);
293
294 JRadioButton replace = new JRadioButton(tr("replace selection"), initialValues.mode == SearchMode.replace);
295 JRadioButton add = new JRadioButton(tr("add to selection"), initialValues.mode == SearchMode.add);
296 JRadioButton remove = new JRadioButton(tr("remove from selection"), initialValues.mode == SearchMode.remove);
297 JRadioButton inSelection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection);
298 ButtonGroup bg = new ButtonGroup();
299 bg.add(replace);
300 bg.add(add);
301 bg.add(remove);
302 bg.add(inSelection);
303
304 JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive);
305 JCheckBox allElements = new JCheckBox(tr("all objects"), initialValues.allElements);
306 allElements.setToolTipText(tr("Also include incomplete and deleted objects in search."));
307 JCheckBox addOnToolbar = new JCheckBox(tr("add toolbar button"), false);
308
309 JRadioButton standardSearch = new JRadioButton(tr("standard"), !initialValues.regexSearch && !initialValues.mapCSSSearch);
310 JRadioButton regexSearch = new JRadioButton(tr("regular expression"), initialValues.regexSearch);
311 JRadioButton mapCSSSearch = new JRadioButton(tr("MapCSS selector"), initialValues.mapCSSSearch);
312 ButtonGroup bg2 = new ButtonGroup();
313 bg2.add(standardSearch);
314 bg2.add(regexSearch);
315 bg2.add(mapCSSSearch);
316
317 JPanel selectionSettings = new JPanel(new GridBagLayout());
318 selectionSettings.setBorder(BorderFactory.createTitledBorder(tr("Selection settings")));
319 selectionSettings.add(replace, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
320 selectionSettings.add(add, GBC.eol());
321 selectionSettings.add(remove, GBC.eol());
322 selectionSettings.add(inSelection, GBC.eop());
323
324 JPanel additionalSettings = new JPanel(new GridBagLayout());
325 additionalSettings.setBorder(BorderFactory.createTitledBorder(tr("Additional settings")));
326 additionalSettings.add(caseSensitive, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
327
328 JPanel left = new JPanel(new GridBagLayout());
329
330 left.add(selectionSettings, GBC.eol().fill(GBC.BOTH));
331 left.add(additionalSettings, GBC.eol().fill(GBC.BOTH));
332
333 if (ExpertToggleAction.isExpert()) {
334 additionalSettings.add(allElements, GBC.eol());
335 additionalSettings.add(addOnToolbar, GBC.eop());
336
337 JPanel searchOptions = new JPanel(new GridBagLayout());
338 searchOptions.setBorder(BorderFactory.createTitledBorder(tr("Search syntax")));
339 searchOptions.add(standardSearch, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
340 searchOptions.add(regexSearch, GBC.eol());
341 searchOptions.add(mapCSSSearch, GBC.eol());
342
343 left.add(searchOptions, GBC.eol().fill(GBC.BOTH));
344 }
345
346 JPanel right = SearchAction.buildHintsSection(hcbSearchString);
347 JPanel top = new JPanel(new GridBagLayout());
348 top.add(label, GBC.std().insets(0, 0, 5, 0));
349 top.add(hcbSearchString, GBC.eol().fill(GBC.HORIZONTAL));
350
351 JTextComponent editorComponent = hcbSearchString.getEditorComponent();
352 Document document = editorComponent.getDocument();
353
354 /*
355 * Setup the logic to validate the contents of the search text field which is executed
356 * every time the content of the field has changed. If the query is incorrect, then
357 * the text field is colored red.
358 */
359 document.addDocumentListener(new AbstractTextComponentValidator(editorComponent) {
360
361 @Override
362 public void validate() {
363 if (!isValid()) {
364 feedbackInvalid(tr("Invalid search expression"));
365 } else {
366 feedbackValid(tooltip);
367 }
368 }
369
370 @Override
371 public boolean isValid() {
372 try {
373 SearchSetting ss = new SearchSetting();
374 ss.text = hcbSearchString.getText();
375 ss.caseSensitive = caseSensitive.isSelected();
376 ss.regexSearch = regexSearch.isSelected();
377 ss.mapCSSSearch = mapCSSSearch.isSelected();
378 SearchCompiler.compile(ss);
379 return true;
380 } catch (SearchParseError | MapCSSException e) {
381 return false;
382 }
383 }
384 });
385
386 /*
387 * Setup the logic to append preset queries to the search text field according to
388 * selected preset by the user. Every query is of the form ' group/sub-group/.../presetName'
389 * if the corresponding group of the preset exists, otherwise it is simply ' presetName'.
390 */
391 TaggingPresetSelector selector = new TaggingPresetSelector(false, false);
392 selector.setBorder(BorderFactory.createTitledBorder(tr("Search by preset")));
393 selector.setDblClickListener(ev -> setPresetDblClickListener(selector, editorComponent));
394
395 JPanel p = new JPanel(new GridBagLayout());
396 p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0));
397 p.add(left, GBC.std().anchor(GBC.NORTH).insets(5, 10, 10, 0).fill(GBC.VERTICAL));
398 p.add(right, GBC.std().fill(GBC.BOTH).insets(0, 10, 0, 0));
399 p.add(selector, GBC.eol().fill(GBC.BOTH).insets(0, 10, 0, 0));
400
401 ExtendedDialog dialog = new ExtendedDialog(
402 Main.parent,
403 initialValues instanceof Filter ? tr("Filter") : tr("Search"),
404 initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"),
405 tr("Cancel")
406 ) {
407 @Override
408 protected void buttonAction(int buttonIndex, ActionEvent evt) {
409 if (buttonIndex == 0) {
410 try {
411 SearchSetting ss = new SearchSetting();
412 ss.text = hcbSearchString.getText();
413 ss.caseSensitive = caseSensitive.isSelected();
414 ss.regexSearch = regexSearch.isSelected();
415 ss.mapCSSSearch = mapCSSSearch.isSelected();
416 SearchCompiler.compile(ss);
417 super.buttonAction(buttonIndex, evt);
418 } catch (SearchParseError | MapCSSException e) {
419 Logging.debug(e);
420 JOptionPane.showMessageDialog(
421 Main.parent,
422 "<html>" + tr("Search expression is not valid: \n\n {0}",
423 e.getMessage().replace("<html>", "").replace("</html>", "")).replace("\n", "<br>") +
424 "</html>",
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 = SearchMode.in_selection;
448 } else if (replace.isSelected()) {
449 initialValues.mode = SearchMode.replace;
450 } else if (add.isSelected()) {
451 initialValues.mode = SearchMode.add;
452 } else {
453 initialValues.mode = SearchMode.remove;
454 }
455
456 if (addOnToolbar.isSelected()) {
457 ToolbarPreferences.ActionDefinition aDef =
458 new ToolbarPreferences.ActionDefinition(MainApplication.getMenu().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 MainApplication.getToolbar().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 * @param parent parent component
689 */
690 void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting, Component parent);
691 }
692
693 /**
694 * Select the search result and display a status text for it.
695 */
696 private static class SelectSearchReceiver implements SearchReceiver {
697
698 @Override
699 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting, Component parent) {
700 ds.setSelected(result);
701 MapFrame map = MainApplication.getMap();
702 if (foundMatches == 0) {
703 final String msg;
704 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
705 if (setting.mode == SearchMode.replace) {
706 msg = tr("No match found for ''{0}''", text);
707 } else if (setting.mode == SearchMode.add) {
708 msg = tr("Nothing added to selection by searching for ''{0}''", text);
709 } else if (setting.mode == SearchMode.remove) {
710 msg = tr("Nothing removed from selection by searching for ''{0}''", text);
711 } else if (setting.mode == SearchMode.in_selection) {
712 msg = tr("Nothing found in selection by searching for ''{0}''", text);
713 } else {
714 msg = null;
715 }
716 if (map != null) {
717 map.statusLine.setHelpText(msg);
718 }
719 if (!GraphicsEnvironment.isHeadless()) {
720 new Notification(msg).show();
721 }
722 } else {
723 map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
724 }
725 }
726 }
727
728 /**
729 * This class stores the result of the search in a local variable.
730 * @author Michael Zangl
731 */
732 private static final class CapturingSearchReceiver implements SearchReceiver {
733 private Collection<OsmPrimitive> result;
734
735 @Override
736 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches,
737 SearchSetting setting, Component parent) {
738 this.result = result;
739 }
740 }
741
742 static final class SearchTask extends PleaseWaitRunnable {
743 private final DataSet ds;
744 private final SearchSetting setting;
745 private final Collection<OsmPrimitive> selection;
746 private final Predicate<OsmPrimitive> predicate;
747 private boolean canceled;
748 private int foundMatches;
749 private final SearchReceiver resultReceiver;
750
751 private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate,
752 SearchReceiver resultReceiver) {
753 super(tr("Searching"));
754 this.ds = ds;
755 this.setting = setting;
756 this.selection = selection;
757 this.predicate = predicate;
758 this.resultReceiver = resultReceiver;
759 }
760
761 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) {
762 final DataSet ds = MainApplication.getLayerManager().getEditDataSet();
763 return newSearchTask(setting, ds, resultReceiver);
764 }
765
766 /**
767 * Create a new search task for the given search setting.
768 * @param setting The setting to use
769 * @param ds The data set to search on
770 * @param resultReceiver will receive the search result
771 * @return A new search task.
772 */
773 private static SearchTask newSearchTask(SearchSetting setting, final DataSet ds, SearchReceiver resultReceiver) {
774 final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected());
775 return new SearchTask(ds, setting, selection, ds::isSelected, resultReceiver);
776 }
777
778 @Override
779 protected void cancel() {
780 this.canceled = true;
781 }
782
783 @Override
784 protected void realRun() {
785 try {
786 foundMatches = 0;
787 SearchCompiler.Match matcher = SearchCompiler.compile(setting);
788
789 if (setting.mode == SearchMode.replace) {
790 selection.clear();
791 } else if (setting.mode == SearchMode.in_selection) {
792 foundMatches = selection.size();
793 }
794
795 Collection<OsmPrimitive> all;
796 if (setting.allElements) {
797 all = ds.allPrimitives();
798 } else {
799 all = ds.getPrimitives(OsmPrimitive::isSelectable);
800 }
801 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false);
802 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size()));
803
804 for (OsmPrimitive osm : all) {
805 if (canceled) {
806 return;
807 }
808 if (setting.mode == SearchMode.replace) {
809 if (matcher.match(osm)) {
810 selection.add(osm);
811 ++foundMatches;
812 }
813 } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) {
814 selection.add(osm);
815 ++foundMatches;
816 } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) {
817 selection.remove(osm);
818 ++foundMatches;
819 } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) {
820 selection.remove(osm);
821 --foundMatches;
822 }
823 subMonitor.worked(1);
824 }
825 subMonitor.finishTask();
826 } catch (SearchParseError e) {
827 Logging.debug(e);
828 JOptionPane.showMessageDialog(
829 Main.parent,
830 e.getMessage(),
831 tr("Error"),
832 JOptionPane.ERROR_MESSAGE
833 );
834 }
835 }
836
837 @Override
838 protected void finish() {
839 if (canceled) {
840 return;
841 }
842 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting, getProgressMonitor().getWindowParent());
843 }
844 }
845
846 /**
847 * {@link ActionParameter} implementation with {@link SearchSetting} as value type.
848 * @since 12547 (moved from {@link ActionParameter})
849 */
850 public static class SearchSettingsActionParameter extends ActionParameter<SearchSetting> {
851
852 /**
853 * Constructs a new {@code SearchSettingsActionParameter}.
854 * @param name parameter name (the key)
855 */
856 public SearchSettingsActionParameter(String name) {
857 super(name);
858 }
859
860 @Override
861 public Class<SearchSetting> getType() {
862 return SearchSetting.class;
863 }
864
865 @Override
866 public SearchSetting readFromString(String s) {
867 return SearchSetting.readFromString(s);
868 }
869
870 @Override
871 public String writeToString(SearchSetting value) {
872 if (value == null)
873 return "";
874 return value.writeToString();
875 }
876 }
877
878 /**
879 * Refreshes the enabled state
880 *
881 */
882 @Override
883 protected void updateEnabledState() {
884 setEnabled(getLayerManager().getEditLayer() != null);
885 }
886
887 @Override
888 public List<ActionParameter<?>> getActionParameters() {
889 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
890 }
891}
Note: See TracBrowser for help on using the repository browser.