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

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

Search dialog: provide real-time visual feedback on valid/invalid search expressions typed by user

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