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

Last change on this file since 12333 was 12333, checked in by michael2402, 7 years ago

Fix #14840: Add separation borders to search dialog. Patch by bafonins

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