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

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

fix #16106 - disable search button arrow when button itself is disabled. Default behaviour of SideButton is not changed (as select button does not have the same logic)

  • Property svn:eol-style set to native
File size: 39.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.search;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.Cursor;
10import java.awt.Dimension;
11import java.awt.FlowLayout;
12import java.awt.GraphicsEnvironment;
13import java.awt.GridBagLayout;
14import java.awt.event.ActionEvent;
15import java.awt.event.KeyEvent;
16import java.awt.event.MouseAdapter;
17import java.awt.event.MouseEvent;
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.HashSet;
23import java.util.LinkedHashSet;
24import java.util.LinkedList;
25import java.util.List;
26import java.util.Map;
27import java.util.Set;
28import java.util.function.Predicate;
29
30import javax.swing.BorderFactory;
31import javax.swing.ButtonGroup;
32import javax.swing.JCheckBox;
33import javax.swing.JLabel;
34import javax.swing.JOptionPane;
35import javax.swing.JPanel;
36import javax.swing.JRadioButton;
37import javax.swing.SwingUtilities;
38import javax.swing.text.BadLocationException;
39import javax.swing.text.Document;
40import javax.swing.text.JTextComponent;
41
42import org.openstreetmap.josm.Main;
43import org.openstreetmap.josm.actions.ActionParameter;
44import org.openstreetmap.josm.actions.ExpertToggleAction;
45import org.openstreetmap.josm.actions.JosmAction;
46import org.openstreetmap.josm.actions.ParameterizedAction;
47import org.openstreetmap.josm.data.osm.DataSet;
48import org.openstreetmap.josm.data.osm.Filter;
49import org.openstreetmap.josm.data.osm.OsmPrimitive;
50import org.openstreetmap.josm.data.osm.search.PushbackTokenizer;
51import org.openstreetmap.josm.data.osm.search.SearchCompiler;
52import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
53import org.openstreetmap.josm.data.osm.search.SearchCompiler.SimpleMatchFactory;
54import org.openstreetmap.josm.data.osm.search.SearchMode;
55import org.openstreetmap.josm.data.osm.search.SearchParseError;
56import org.openstreetmap.josm.data.osm.search.SearchSetting;
57import org.openstreetmap.josm.gui.ExtendedDialog;
58import org.openstreetmap.josm.gui.MainApplication;
59import org.openstreetmap.josm.gui.MapFrame;
60import org.openstreetmap.josm.gui.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>", null, tr("matches if ''key'' exists"))
487 .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''"))
488 .addKeyword("<i>key</i>=*", null, tr("''key'' with any value"))
489 .addKeyword("<i>key</i>=", null, tr("''key'' with empty value"))
490 .addKeyword("*=<i>value</i>", null, tr("''value'' in any key"))
491 .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)"))
492 .addKeyword("\"key\"=\"value\"", "\"\"=\"\"",
493 tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " +
494 "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."),
495 "\"addr:street\""),
496 GBC.eol().anchor(GBC.CENTER));
497 hintPanel.add(new SearchKeywordRow(hcbSearchString)
498 .addTitle(tr("combinators"))
499 .addKeyword("<i>expr</i> <i>expr</i>", null, tr("logical and (both expressions have to be satisfied)"))
500 .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)"))
501 .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)"))
502 .addKeyword("-<i>expr</i>", null, tr("logical not"))
503 .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")),
504 GBC.eol());
505
506 if (ExpertToggleAction.isExpert()) {
507 hintPanel.add(new SearchKeywordRow(hcbSearchString)
508 .addTitle(tr("objects"))
509 .addKeyword("type:node", "type:node ", tr("all nodes"))
510 .addKeyword("type:way", "type:way ", tr("all ways"))
511 .addKeyword("type:relation", "type:relation ", tr("all relations"))
512 .addKeyword("closed", "closed ", tr("all closed ways"))
513 .addKeyword("untagged", "untagged ", tr("object without useful tags")),
514 GBC.eol());
515 hintPanel.add(new SearchKeywordRow(hcbSearchString)
516 .addKeyword("preset:\"Annotation/Address\"", "preset:\"Annotation/Address\"",
517 tr("all objects that use the address preset"))
518 .addKeyword("preset:\"Geography/Nature/*\"", "preset:\"Geography/Nature/*\"",
519 tr("all objects that use any preset under the Geography/Nature group")),
520 GBC.eol().anchor(GBC.CENTER));
521 hintPanel.add(new SearchKeywordRow(hcbSearchString)
522 .addTitle(tr("metadata"))
523 .addKeyword("user:", "user:", tr("objects changed by user", "user:anonymous"))
524 .addKeyword("id:", "id:", tr("objects with given ID"), "id:0 (new objects)")
525 .addKeyword("version:", "version:", tr("objects with given version"), "version:0 (objects without an assigned version)")
526 .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"),
527 "changeset:0 (objects without an assigned changeset)")
528 .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/",
529 "timestamp:2008/2011-02-04T12"),
530 GBC.eol());
531 hintPanel.add(new SearchKeywordRow(hcbSearchString)
532 .addTitle(tr("properties"))
533 .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes"))
534 .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways"))
535 .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags"))
536 .addKeyword("role:", "role:", tr("objects with given role in a relation"))
537 .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2"))
538 .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")),
539 GBC.eol());
540 hintPanel.add(new SearchKeywordRow(hcbSearchString)
541 .addTitle(tr("state"))
542 .addKeyword("modified", "modified ", tr("all modified objects"))
543 .addKeyword("new", "new ", tr("all new objects"))
544 .addKeyword("selected", "selected ", tr("all selected objects"))
545 .addKeyword("incomplete", "incomplete ", tr("all incomplete objects"))
546 .addKeyword("deleted", "deleted ", tr("all deleted objects (checkbox <b>{0}</b> must be enabled)", tr("all objects"))),
547 GBC.eol());
548 hintPanel.add(new SearchKeywordRow(hcbSearchString)
549 .addTitle(tr("related objects"))
550 .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building")
551 .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop")
552 .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>"))
553 .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>"))
554 .addKeyword("nth:<i>7</i>", "nth:",
555 tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1")
556 .addKeyword("nth%:<i>7</i>", "nth%:",
557 tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"),
558 GBC.eol());
559 hintPanel.add(new SearchKeywordRow(hcbSearchString)
560 .addTitle(tr("view"))
561 .addKeyword("inview", "inview ", tr("objects in current view"))
562 .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view"))
563 .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area"))
564 .addKeyword("allindownloadedarea", "allindownloadedarea ",
565 tr("objects (and all its way nodes / relation members) in downloaded area")),
566 GBC.eol());
567 }
568
569 return hintPanel;
570 }
571
572 /**
573 * Launches the dialog for specifying search criteria and runs a search
574 */
575 public static void search() {
576 SearchSetting se = showSearchDialog(lastSearch);
577 if (se != null) {
578 searchWithHistory(se);
579 }
580 }
581
582 /**
583 * Adds the search specified by the settings in <code>s</code> to the
584 * search history and performs the search.
585 *
586 * @param s search settings
587 */
588 public static void searchWithHistory(SearchSetting s) {
589 saveToHistory(s);
590 lastSearch = new SearchSetting(s);
591 search(s);
592 }
593
594 /**
595 * Performs the search specified by the settings in <code>s</code> without saving it to search history.
596 *
597 * @param s search settings
598 */
599 public static void searchWithoutHistory(SearchSetting s) {
600 lastSearch = new SearchSetting(s);
601 search(s);
602 }
603
604 /**
605 * Performs the search specified by the search string {@code search} and the search mode {@code mode}.
606 *
607 * @param search the search string to use
608 * @param mode the search mode to use
609 */
610 public static void search(String search, SearchMode mode) {
611 final SearchSetting searchSetting = new SearchSetting();
612 searchSetting.text = search;
613 searchSetting.mode = mode;
614 search(searchSetting);
615 }
616
617 static void search(SearchSetting s) {
618 SearchTask.newSearchTask(s, new SelectSearchReceiver()).run();
619 }
620
621 /**
622 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search.
623 *
624 * @param search the search string to use
625 * @param mode the search mode to use
626 * @return The result of the search.
627 * @since 10457
628 */
629 public static Collection<OsmPrimitive> searchAndReturn(String search, SearchMode mode) {
630 final SearchSetting searchSetting = new SearchSetting();
631 searchSetting.text = search;
632 searchSetting.mode = mode;
633 CapturingSearchReceiver receiver = new CapturingSearchReceiver();
634 SearchTask.newSearchTask(searchSetting, receiver).run();
635 return receiver.result;
636 }
637
638 /**
639 *
640 * @param selector Selector component that the user interacts with
641 * @param searchEditor Editor for search queries
642 */
643 private static void setPresetDblClickListener(TaggingPresetSelector selector, JTextComponent searchEditor) {
644 TaggingPreset selectedPreset = selector.getSelectedPresetAndUpdateClassification();
645
646 if (selectedPreset == null) {
647 return;
648 }
649
650 /*
651 * Make sure that the focus is transferred to the search text field
652 * from the selector component.
653 */
654 searchEditor.requestFocusInWindow();
655
656 /*
657 * In order to make interaction with the search dialog simpler,
658 * we make sure that if autocompletion triggers and the text field is
659 * not in focus, the correct area is selected. We first request focus
660 * and then execute the selection logic. invokeLater allows us to
661 * defer the selection until waiting for focus.
662 */
663 SwingUtilities.invokeLater(() -> {
664 int textOffset = searchEditor.getCaretPosition();
665 String presetSearchQuery = " preset:" +
666 "\"" + selectedPreset.getRawName() + "\"";
667 try {
668 searchEditor.getDocument().insertString(textOffset, presetSearchQuery, null);
669 } catch (BadLocationException e1) {
670 throw new JosmRuntimeException(e1.getMessage(), e1);
671 }
672 });
673 }
674
675 /**
676 * Interfaces implementing this may receive the result of the current search.
677 * @author Michael Zangl
678 * @since 10457
679 * @since 10600 (functional interface)
680 */
681 @FunctionalInterface
682 interface SearchReceiver {
683 /**
684 * Receive the search result
685 * @param ds The data set searched on.
686 * @param result The result collection, including the initial collection.
687 * @param foundMatches The number of matches added to the result.
688 * @param setting The setting used.
689 * @param parent parent component
690 */
691 void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting, Component parent);
692 }
693
694 /**
695 * Select the search result and display a status text for it.
696 */
697 private static class SelectSearchReceiver implements SearchReceiver {
698
699 @Override
700 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting, Component parent) {
701 ds.setSelected(result);
702 MapFrame map = MainApplication.getMap();
703 if (foundMatches == 0) {
704 final String msg;
705 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
706 if (setting.mode == SearchMode.replace) {
707 msg = tr("No match found for ''{0}''", text);
708 } else if (setting.mode == SearchMode.add) {
709 msg = tr("Nothing added to selection by searching for ''{0}''", text);
710 } else if (setting.mode == SearchMode.remove) {
711 msg = tr("Nothing removed from selection by searching for ''{0}''", text);
712 } else if (setting.mode == SearchMode.in_selection) {
713 msg = tr("Nothing found in selection by searching for ''{0}''", text);
714 } else {
715 msg = null;
716 }
717 if (map != null) {
718 map.statusLine.setHelpText(msg);
719 }
720 if (!GraphicsEnvironment.isHeadless()) {
721 new Notification(msg).show();
722 }
723 } else {
724 map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
725 }
726 }
727 }
728
729 /**
730 * This class stores the result of the search in a local variable.
731 * @author Michael Zangl
732 */
733 private static final class CapturingSearchReceiver implements SearchReceiver {
734 private Collection<OsmPrimitive> result;
735
736 @Override
737 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches,
738 SearchSetting setting, Component parent) {
739 this.result = result;
740 }
741 }
742
743 static final class SearchTask extends PleaseWaitRunnable {
744 private final DataSet ds;
745 private final SearchSetting setting;
746 private final Collection<OsmPrimitive> selection;
747 private final Predicate<OsmPrimitive> predicate;
748 private boolean canceled;
749 private int foundMatches;
750 private final SearchReceiver resultReceiver;
751
752 private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate,
753 SearchReceiver resultReceiver) {
754 super(tr("Searching"));
755 this.ds = ds;
756 this.setting = setting;
757 this.selection = selection;
758 this.predicate = predicate;
759 this.resultReceiver = resultReceiver;
760 }
761
762 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) {
763 final DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
764 if (ds == null) {
765 throw new IllegalStateException("No active dataset");
766 }
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 (SearchParseError 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, getProgressMonitor().getWindowParent());
847 }
848 }
849
850 /**
851 * {@link ActionParameter} implementation with {@link SearchSetting} as value type.
852 * @since 12547 (moved from {@link ActionParameter})
853 */
854 public static class SearchSettingsActionParameter extends ActionParameter<SearchSetting> {
855
856 /**
857 * Constructs a new {@code SearchSettingsActionParameter}.
858 * @param name parameter name (the key)
859 */
860 public SearchSettingsActionParameter(String name) {
861 super(name);
862 }
863
864 @Override
865 public Class<SearchSetting> getType() {
866 return SearchSetting.class;
867 }
868
869 @Override
870 public SearchSetting readFromString(String s) {
871 return SearchSetting.readFromString(s);
872 }
873
874 @Override
875 public String writeToString(SearchSetting value) {
876 if (value == null)
877 return "";
878 return value.writeToString();
879 }
880 }
881
882 /**
883 * Refreshes the enabled state
884 */
885 @Override
886 protected void updateEnabledState() {
887 setEnabled(getLayerManager().getActiveDataSet() != null);
888 }
889
890 @Override
891 public List<ActionParameter<?>> getActionParameters() {
892 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
893 }
894}
Note: See TracBrowser for help on using the repository browser.