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