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 058 059public class SearchAction extends JosmAction implements ParameterizedAction { 060 061 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15; 062 /** Maximum number of characters before the search expression is shortened for display purposes. */ 063 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100; 064 065 private static final String SEARCH_EXPRESSION = "searchExpression"; 066 067 public enum SearchMode { 068 replace('R'), add('A'), remove('D'), in_selection('S'); 069 070 private final char code; 071 072 SearchMode(char code) { 073 this.code = code; 074 } 075 076 public char getCode() { 077 return code; 078 } 079 080 public static SearchMode fromCode(char code) { 081 for (SearchMode mode: values()) { 082 if (mode.getCode() == code) 083 return mode; 084 } 085 return null; 086 } 087 } 088 089 private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>(); 090 static { 091 for (String s: Main.pref.getCollection("search.history", Collections.<String>emptyList())) { 092 SearchSetting ss = SearchSetting.readFromString(s); 093 if (ss != null) { 094 searchHistory.add(ss); 095 } 096 } 097 } 098 099 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}