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