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