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.trn; 007 008import java.awt.Component; 009import java.awt.Cursor; 010import java.awt.Dimension; 011import java.awt.FlowLayout; 012import java.awt.GraphicsEnvironment; 013import java.awt.GridBagLayout; 014import java.awt.event.ActionEvent; 015import java.awt.event.KeyEvent; 016import java.awt.event.MouseAdapter; 017import java.awt.event.MouseEvent; 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashSet; 023import java.util.LinkedHashSet; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.function.Predicate; 029 030import javax.swing.BorderFactory; 031import javax.swing.ButtonGroup; 032import javax.swing.JCheckBox; 033import javax.swing.JLabel; 034import javax.swing.JOptionPane; 035import javax.swing.JPanel; 036import javax.swing.JRadioButton; 037import javax.swing.SwingUtilities; 038import javax.swing.text.BadLocationException; 039import javax.swing.text.Document; 040import javax.swing.text.JTextComponent; 041 042import org.openstreetmap.josm.actions.ActionParameter; 043import org.openstreetmap.josm.actions.ExpertToggleAction; 044import org.openstreetmap.josm.actions.JosmAction; 045import org.openstreetmap.josm.actions.ParameterizedAction; 046import org.openstreetmap.josm.data.osm.Filter; 047import org.openstreetmap.josm.data.osm.IPrimitive; 048import org.openstreetmap.josm.data.osm.OsmData; 049import org.openstreetmap.josm.data.osm.search.PushbackTokenizer; 050import org.openstreetmap.josm.data.osm.search.SearchCompiler; 051import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 052import org.openstreetmap.josm.data.osm.search.SearchCompiler.SimpleMatchFactory; 053import org.openstreetmap.josm.data.osm.search.SearchMode; 054import org.openstreetmap.josm.data.osm.search.SearchParseError; 055import org.openstreetmap.josm.data.osm.search.SearchSetting; 056import org.openstreetmap.josm.gui.ExtendedDialog; 057import org.openstreetmap.josm.gui.MainApplication; 058import org.openstreetmap.josm.gui.MapFrame; 059import org.openstreetmap.josm.gui.Notification; 060import org.openstreetmap.josm.gui.PleaseWaitRunnable; 061import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException; 062import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 063import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser; 064import org.openstreetmap.josm.gui.progress.ProgressMonitor; 065import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset; 066import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector; 067import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 068import org.openstreetmap.josm.gui.widgets.HistoryComboBox; 069import org.openstreetmap.josm.spi.preferences.Config; 070import org.openstreetmap.josm.tools.GBC; 071import org.openstreetmap.josm.tools.JosmRuntimeException; 072import org.openstreetmap.josm.tools.Logging; 073import org.openstreetmap.josm.tools.Shortcut; 074import org.openstreetmap.josm.tools.Utils; 075 076/** 077 * The search action allows the user to search the data layer using a complex search string. 078 * 079 * @see SearchCompiler 080 */ 081public class SearchAction extends JosmAction implements ParameterizedAction { 082 083 /** 084 * The default size of the search history 085 */ 086 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15; 087 /** 088 * Maximum number of characters before the search expression is shortened for display purposes. 089 */ 090 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100; 091 092 private static final String SEARCH_EXPRESSION = "searchExpression"; 093 094 private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>(); 095 static { 096 SearchCompiler.addMatchFactory(new SimpleMatchFactory() { 097 @Override 098 public Collection<String> getKeywords() { 099 return Arrays.asList("inview", "allinview"); 100 } 101 102 @Override 103 public Match get(String keyword, boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) throws SearchParseError { 104 switch(keyword) { 105 case "inview": 106 return new InView(false); 107 case "allinview": 108 return new InView(true); 109 default: 110 throw new IllegalStateException("Not expecting keyword " + keyword); 111 } 112 } 113 }); 114 115 for (String s: Config.getPref().getList("search.history", Collections.<String>emptyList())) { 116 SearchSetting ss = SearchSetting.readFromString(s); 117 if (ss != null) { 118 searchHistory.add(ss); 119 } 120 } 121 } 122 123 /** 124 * Gets the search history 125 * @return The last searched terms. Do not modify it. 126 */ 127 public static Collection<SearchSetting> getSearchHistory() { 128 return searchHistory; 129 } 130 131 /** 132 * Saves a search to the search history. 133 * @param s The search to save 134 */ 135 public static void saveToHistory(SearchSetting s) { 136 if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) { 137 searchHistory.addFirst(new SearchSetting(s)); 138 } else if (searchHistory.contains(s)) { 139 // move existing entry to front, fixes #8032 - search history loses entries when re-using queries 140 searchHistory.remove(s); 141 searchHistory.addFirst(new SearchSetting(s)); 142 } 143 int maxsize = Config.getPref().getInt("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE); 144 while (searchHistory.size() > maxsize) { 145 searchHistory.removeLast(); 146 } 147 Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size()); 148 for (SearchSetting item: searchHistory) { 149 savedHistory.add(item.writeToString()); 150 } 151 Config.getPref().putList("search.history", new ArrayList<>(savedHistory)); 152 } 153 154 /** 155 * Gets a list of all texts that were recently used in the search 156 * @return The list of search texts. 157 */ 158 public static List<String> getSearchExpressionHistory() { 159 List<String> ret = new ArrayList<>(getSearchHistory().size()); 160 for (SearchSetting ss: getSearchHistory()) { 161 ret.add(ss.text); 162 } 163 return ret; 164 } 165 166 private static volatile SearchSetting lastSearch; 167 168 /** 169 * Constructs a new {@code SearchAction}. 170 */ 171 public SearchAction() { 172 super(tr("Search..."), "dialogs/search", tr("Search for objects"), 173 Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true); 174 setHelpId(ht("/Action/Search")); 175 } 176 177 @Override 178 public void actionPerformed(ActionEvent e) { 179 if (!isEnabled()) 180 return; 181 search(); 182 } 183 184 @Override 185 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) { 186 if (parameters.get(SEARCH_EXPRESSION) == null) { 187 actionPerformed(e); 188 } else { 189 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION)); 190 } 191 } 192 193 private static class SearchKeywordRow extends JPanel { 194 195 private final HistoryComboBox hcb; 196 197 SearchKeywordRow(HistoryComboBox hcb) { 198 super(new FlowLayout(FlowLayout.LEFT)); 199 this.hcb = hcb; 200 } 201 202 /** 203 * Adds the title (prefix) label at the beginning of the row. Should be called only once. 204 * @param title English title 205 * @return {@code this} for easy chaining 206 */ 207 public SearchKeywordRow addTitle(String title) { 208 add(new JLabel(tr("{0}: ", title))); 209 return this; 210 } 211 212 /** 213 * Adds an example keyword label at the end of the row. Can be called several times. 214 * @param displayText displayed HTML text 215 * @param insertText optional: if set, makes the label clickable, and {@code insertText} will be inserted in search string 216 * @param description optional: HTML text to be displayed in the tooltip 217 * @param examples optional: examples joined as HTML list in the tooltip 218 * @return {@code this} for easy chaining 219 */ 220 public SearchKeywordRow addKeyword(String displayText, final String insertText, String description, String... examples) { 221 JLabel label = new JLabel("<html>" 222 + "<style>td{border:1px solid gray; font-weight:normal;}</style>" 223 + "<table><tr><td>" + displayText + "</td></tr></table></html>"); 224 add(label); 225 if (description != null || examples.length > 0) { 226 label.setToolTipText("<html>" 227 + description 228 + (examples.length > 0 ? Utils.joinAsHtmlUnorderedList(Arrays.asList(examples)) : "") 229 + "</html>"); 230 } 231 if (insertText != null) { 232 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 233 label.addMouseListener(new MouseAdapter() { 234 235 @Override 236 public void mouseClicked(MouseEvent e) { 237 JTextComponent tf = hcb.getEditorComponent(); 238 239 /* 240 * Make sure that the focus is transferred to the search text field 241 * from the selector component. 242 */ 243 if (!tf.hasFocus()) { 244 tf.requestFocusInWindow(); 245 } 246 247 /* 248 * In order to make interaction with the search dialog simpler, 249 * we make sure that if autocompletion triggers and the text field is 250 * not in focus, the correct area is selected. We first request focus 251 * and then execute the selection logic. invokeLater allows us to 252 * defer the selection until waiting for focus. 253 */ 254 SwingUtilities.invokeLater(() -> { 255 try { 256 tf.getDocument().insertString(tf.getCaretPosition(), ' ' + insertText, null); 257 } catch (BadLocationException ex) { 258 throw new JosmRuntimeException(ex.getMessage(), ex); 259 } 260 }); 261 } 262 }); 263 } 264 return this; 265 } 266 } 267 268 /** 269 * Builds and shows the search dialog. 270 * @param initialValues A set of initial values needed in order to initialize the search dialog. 271 * If is {@code null}, then default settings are used. 272 * @return Returns {@link SearchAction} object containing parameters of the search. 273 */ 274 public static SearchSetting showSearchDialog(SearchSetting initialValues) { 275 if (initialValues == null) { 276 initialValues = new SearchSetting(); 277 } 278 279 // prepare the combo box with the search expressions 280 JLabel label = new JLabel(initialValues instanceof Filter ? tr("Filter string:") : tr("Search string:")); 281 HistoryComboBox hcbSearchString = new HistoryComboBox(); 282 String tooltip = tr("Enter the search expression"); 283 hcbSearchString.setText(initialValues.text); 284 hcbSearchString.setToolTipText(tooltip); 285 286 // we have to reverse the history, because ComboBoxHistory will reverse it again in addElement() 287 List<String> searchExpressionHistory = getSearchExpressionHistory(); 288 Collections.reverse(searchExpressionHistory); 289 hcbSearchString.setPossibleItems(searchExpressionHistory); 290 hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height)); 291 label.setLabelFor(hcbSearchString); 292 293 JRadioButton replace = new JRadioButton(tr("replace selection"), initialValues.mode == SearchMode.replace); 294 JRadioButton add = new JRadioButton(tr("add to selection"), initialValues.mode == SearchMode.add); 295 JRadioButton remove = new JRadioButton(tr("remove from selection"), initialValues.mode == SearchMode.remove); 296 JRadioButton inSelection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection); 297 ButtonGroup bg = new ButtonGroup(); 298 bg.add(replace); 299 bg.add(add); 300 bg.add(remove); 301 bg.add(inSelection); 302 303 JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive); 304 JCheckBox allElements = new JCheckBox(tr("all objects"), initialValues.allElements); 305 allElements.setToolTipText(tr("Also include incomplete and deleted objects in search.")); 306 JCheckBox addOnToolbar = new JCheckBox(tr("add toolbar button"), false); 307 308 JRadioButton standardSearch = new JRadioButton(tr("standard"), !initialValues.regexSearch && !initialValues.mapCSSSearch); 309 JRadioButton regexSearch = new JRadioButton(tr("regular expression"), initialValues.regexSearch); 310 JRadioButton mapCSSSearch = new JRadioButton(tr("MapCSS selector"), initialValues.mapCSSSearch); 311 ButtonGroup bg2 = new ButtonGroup(); 312 bg2.add(standardSearch); 313 bg2.add(regexSearch); 314 bg2.add(mapCSSSearch); 315 316 JPanel selectionSettings = new JPanel(new GridBagLayout()); 317 selectionSettings.setBorder(BorderFactory.createTitledBorder(tr("Selection settings"))); 318 selectionSettings.add(replace, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL)); 319 selectionSettings.add(add, GBC.eol()); 320 selectionSettings.add(remove, GBC.eol()); 321 selectionSettings.add(inSelection, GBC.eop()); 322 323 JPanel additionalSettings = new JPanel(new GridBagLayout()); 324 additionalSettings.setBorder(BorderFactory.createTitledBorder(tr("Additional settings"))); 325 additionalSettings.add(caseSensitive, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL)); 326 327 JPanel left = new JPanel(new GridBagLayout()); 328 329 left.add(selectionSettings, GBC.eol().fill(GBC.BOTH)); 330 left.add(additionalSettings, GBC.eol().fill(GBC.BOTH)); 331 332 if (ExpertToggleAction.isExpert()) { 333 additionalSettings.add(allElements, GBC.eol()); 334 additionalSettings.add(addOnToolbar, GBC.eop()); 335 336 JPanel searchOptions = new JPanel(new GridBagLayout()); 337 searchOptions.setBorder(BorderFactory.createTitledBorder(tr("Search syntax"))); 338 searchOptions.add(standardSearch, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL)); 339 searchOptions.add(regexSearch, GBC.eol()); 340 searchOptions.add(mapCSSSearch, GBC.eol()); 341 342 left.add(searchOptions, GBC.eol().fill(GBC.BOTH)); 343 } 344 345 JPanel right = SearchAction.buildHintsSection(hcbSearchString); 346 JPanel top = new JPanel(new GridBagLayout()); 347 top.add(label, GBC.std().insets(0, 0, 5, 0)); 348 top.add(hcbSearchString, GBC.eol().fill(GBC.HORIZONTAL)); 349 350 JTextComponent editorComponent = hcbSearchString.getEditorComponent(); 351 Document document = editorComponent.getDocument(); 352 353 /* 354 * Setup the logic to validate the contents of the search text field which is executed 355 * every time the content of the field has changed. If the query is incorrect, then 356 * the text field is colored red. 357 */ 358 document.addDocumentListener(new AbstractTextComponentValidator(editorComponent) { 359 360 @Override 361 public void validate() { 362 if (!isValid()) { 363 feedbackInvalid(tr("Invalid search expression")); 364 } else { 365 feedbackValid(tooltip); 366 } 367 } 368 369 @Override 370 public boolean isValid() { 371 try { 372 SearchSetting ss = new SearchSetting(); 373 ss.text = hcbSearchString.getText(); 374 ss.caseSensitive = caseSensitive.isSelected(); 375 ss.regexSearch = regexSearch.isSelected(); 376 ss.mapCSSSearch = mapCSSSearch.isSelected(); 377 SearchCompiler.compile(ss); 378 return true; 379 } catch (SearchParseError | MapCSSException e) { 380 return false; 381 } 382 } 383 }); 384 385 /* 386 * Setup the logic to append preset queries to the search text field according to 387 * selected preset by the user. Every query is of the form ' group/sub-group/.../presetName' 388 * if the corresponding group of the preset exists, otherwise it is simply ' presetName'. 389 */ 390 TaggingPresetSelector selector = new TaggingPresetSelector(false, false); 391 selector.setBorder(BorderFactory.createTitledBorder(tr("Search by preset"))); 392 selector.setDblClickListener(ev -> setPresetDblClickListener(selector, editorComponent)); 393 394 JPanel p = new JPanel(new GridBagLayout()); 395 p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0)); 396 p.add(left, GBC.std().anchor(GBC.NORTH).insets(5, 10, 10, 0).fill(GBC.VERTICAL)); 397 p.add(right, GBC.std().fill(GBC.BOTH).insets(0, 10, 0, 0)); 398 p.add(selector, GBC.eol().fill(GBC.BOTH).insets(0, 10, 0, 0)); 399 400 ExtendedDialog dialog = new ExtendedDialog( 401 MainApplication.getMainFrame(), 402 initialValues instanceof Filter ? tr("Filter") : tr("Search"), 403 initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"), 404 tr("Cancel") 405 ) { 406 @Override 407 protected void buttonAction(int buttonIndex, ActionEvent evt) { 408 if (buttonIndex == 0) { 409 try { 410 SearchSetting ss = new SearchSetting(); 411 ss.text = hcbSearchString.getText(); 412 ss.caseSensitive = caseSensitive.isSelected(); 413 ss.regexSearch = regexSearch.isSelected(); 414 ss.mapCSSSearch = mapCSSSearch.isSelected(); 415 SearchCompiler.compile(ss); 416 super.buttonAction(buttonIndex, evt); 417 } catch (SearchParseError | MapCSSException e) { 418 Logging.debug(e); 419 JOptionPane.showMessageDialog( 420 MainApplication.getMainFrame(), 421 "<html>" + tr("Search expression is not valid: \n\n {0}", 422 e.getMessage().replace("<html>", "").replace("</html>", "")).replace("\n", "<br>") + 423 "</html>", 424 tr("Invalid search expression"), 425 JOptionPane.ERROR_MESSAGE); 426 } 427 } else { 428 super.buttonAction(buttonIndex, evt); 429 } 430 } 431 }; 432 dialog.setButtonIcons("dialogs/search", "cancel"); 433 dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */); 434 dialog.setContent(p); 435 436 if (dialog.showDialog().getValue() != 1) return null; 437 438 // User pressed OK - let's perform the search 439 initialValues.text = hcbSearchString.getText(); 440 initialValues.caseSensitive = caseSensitive.isSelected(); 441 initialValues.allElements = allElements.isSelected(); 442 initialValues.regexSearch = regexSearch.isSelected(); 443 initialValues.mapCSSSearch = mapCSSSearch.isSelected(); 444 445 if (inSelection.isSelected()) { 446 initialValues.mode = SearchMode.in_selection; 447 } else if (replace.isSelected()) { 448 initialValues.mode = SearchMode.replace; 449 } else if (add.isSelected()) { 450 initialValues.mode = SearchMode.add; 451 } else { 452 initialValues.mode = SearchMode.remove; 453 } 454 455 if (addOnToolbar.isSelected()) { 456 ToolbarPreferences.ActionDefinition aDef = 457 new ToolbarPreferences.ActionDefinition(MainApplication.getMenu().search); 458 aDef.getParameters().put(SEARCH_EXPRESSION, initialValues); 459 // Display search expression as tooltip instead of generic one 460 aDef.setName(Utils.shortenString(initialValues.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY)); 461 // parametrized action definition is now composed 462 ActionParser actionParser = new ToolbarPreferences.ActionParser(null); 463 String res = actionParser.saveAction(aDef); 464 465 // add custom search button to toolbar preferences 466 MainApplication.getToolbar().addCustomButton(res, -1, false); 467 } 468 469 return initialValues; 470 } 471 472 private static JPanel buildHintsSection(HistoryComboBox hcbSearchString) { 473 JPanel hintPanel = new JPanel(new GridBagLayout()); 474 hintPanel.setBorder(BorderFactory.createTitledBorder(tr("Search hints"))); 475 476 hintPanel.add(new SearchKeywordRow(hcbSearchString) 477 .addTitle(tr("basics")) 478 .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key")) 479 .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key")) 480 .addKeyword("<i>key</i>:<i>valuefragment</i>", null, 481 tr("''valuefragment'' anywhere in ''key''"), "name:str matches name=Bakerstreet") 482 .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''")), 483 GBC.eol()); 484 hintPanel.add(new SearchKeywordRow(hcbSearchString) 485 .addKeyword("<i>key</i>", null, tr("matches if ''key'' exists")) 486 .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''")) 487 .addKeyword("<i>key</i>=*", null, tr("''key'' with any value")) 488 .addKeyword("<i>key</i>=", null, tr("''key'' with empty value")) 489 .addKeyword("*=<i>value</i>", null, tr("''value'' in any key")) 490 .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)")) 491 .addKeyword("\"key\"=\"value\"", "\"\"=\"\"", 492 tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " + 493 "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."), 494 "\"addr:street\""), 495 GBC.eol().anchor(GBC.CENTER)); 496 hintPanel.add(new SearchKeywordRow(hcbSearchString) 497 .addTitle(tr("combinators")) 498 .addKeyword("<i>expr</i> <i>expr</i>", null, tr("logical and (both expressions have to be satisfied)")) 499 .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)")) 500 .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)")) 501 .addKeyword("-<i>expr</i>", null, tr("logical not")) 502 .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")), 503 GBC.eol()); 504 505 if (ExpertToggleAction.isExpert()) { 506 hintPanel.add(new SearchKeywordRow(hcbSearchString) 507 .addTitle(tr("objects")) 508 .addKeyword("type:node", "type:node ", tr("all nodes")) 509 .addKeyword("type:way", "type:way ", tr("all ways")) 510 .addKeyword("type:relation", "type:relation ", tr("all relations")) 511 .addKeyword("closed", "closed ", tr("all closed ways")) 512 .addKeyword("untagged", "untagged ", tr("object without useful tags")), 513 GBC.eol()); 514 hintPanel.add(new SearchKeywordRow(hcbSearchString) 515 .addKeyword("preset:\"Annotation/Address\"", "preset:\"Annotation/Address\"", 516 tr("all objects that use the address preset")) 517 .addKeyword("preset:\"Geography/Nature/*\"", "preset:\"Geography/Nature/*\"", 518 tr("all objects that use any preset under the Geography/Nature group")), 519 GBC.eol().anchor(GBC.CENTER)); 520 hintPanel.add(new SearchKeywordRow(hcbSearchString) 521 .addTitle(tr("metadata")) 522 .addKeyword("user:", "user:", tr("objects changed by user", "user:anonymous")) 523 .addKeyword("id:", "id:", tr("objects with given ID"), "id:0 (new objects)") 524 .addKeyword("version:", "version:", tr("objects with given version"), "version:0 (objects without an assigned version)") 525 .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"), 526 "changeset:0 (objects without an assigned changeset)") 527 .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/", 528 "timestamp:2008/2011-02-04T12"), 529 GBC.eol()); 530 hintPanel.add(new SearchKeywordRow(hcbSearchString) 531 .addTitle(tr("properties")) 532 .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes")) 533 .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways")) 534 .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags")) 535 .addKeyword("role:", "role:", tr("objects with given role in a relation")) 536 .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2")) 537 .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")), 538 GBC.eol()); 539 hintPanel.add(new SearchKeywordRow(hcbSearchString) 540 .addTitle(tr("state")) 541 .addKeyword("modified", "modified ", tr("all modified objects")) 542 .addKeyword("new", "new ", tr("all new objects")) 543 .addKeyword("selected", "selected ", tr("all selected objects")) 544 .addKeyword("incomplete", "incomplete ", tr("all incomplete objects")) 545 .addKeyword("deleted", "deleted ", tr("all deleted objects (checkbox <b>{0}</b> must be enabled)", tr("all objects"))), 546 GBC.eol()); 547 hintPanel.add(new SearchKeywordRow(hcbSearchString) 548 .addTitle(tr("related objects")) 549 .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building") 550 .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop") 551 .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>")) 552 .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>")) 553 .addKeyword("nth:<i>7</i>", "nth:", 554 tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1") 555 .addKeyword("nth%:<i>7</i>", "nth%:", 556 tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"), 557 GBC.eol()); 558 hintPanel.add(new SearchKeywordRow(hcbSearchString) 559 .addTitle(tr("view")) 560 .addKeyword("inview", "inview ", tr("objects in current view")) 561 .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view")) 562 .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area")) 563 .addKeyword("allindownloadedarea", "allindownloadedarea ", 564 tr("objects (and all its way nodes / relation members) in downloaded area")), 565 GBC.eol()); 566 } 567 568 return hintPanel; 569 } 570 571 /** 572 * Launches the dialog for specifying search criteria and runs a search 573 */ 574 public static void search() { 575 SearchSetting se = showSearchDialog(lastSearch); 576 if (se != null) { 577 searchWithHistory(se); 578 } 579 } 580 581 /** 582 * Adds the search specified by the settings in <code>s</code> to the 583 * search history and performs the search. 584 * 585 * @param s search settings 586 */ 587 public static void searchWithHistory(SearchSetting s) { 588 saveToHistory(s); 589 lastSearch = new SearchSetting(s); 590 search(s); 591 } 592 593 /** 594 * Performs the search specified by the settings in <code>s</code> without saving it to search history. 595 * 596 * @param s search settings 597 */ 598 public static void searchWithoutHistory(SearchSetting s) { 599 lastSearch = new SearchSetting(s); 600 search(s); 601 } 602 603 /** 604 * Performs the search specified by the search string {@code search} and the search mode {@code mode}. 605 * 606 * @param search the search string to use 607 * @param mode the search mode to use 608 */ 609 public static void search(String search, SearchMode mode) { 610 final SearchSetting searchSetting = new SearchSetting(); 611 searchSetting.text = search; 612 searchSetting.mode = mode; 613 search(searchSetting); 614 } 615 616 static void search(SearchSetting s) { 617 SearchTask.newSearchTask(s, new SelectSearchReceiver()).run(); 618 } 619 620 /** 621 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search. 622 * 623 * @param search the search string to use 624 * @param mode the search mode to use 625 * @return The result of the search. 626 * @since 10457 627 * @since 13950 (signature) 628 */ 629 public static Collection<IPrimitive> searchAndReturn(String search, SearchMode mode) { 630 final SearchSetting searchSetting = new SearchSetting(); 631 searchSetting.text = search; 632 searchSetting.mode = mode; 633 CapturingSearchReceiver receiver = new CapturingSearchReceiver(); 634 SearchTask.newSearchTask(searchSetting, receiver).run(); 635 return receiver.result; 636 } 637 638 /** 639 * 640 * @param selector Selector component that the user interacts with 641 * @param searchEditor Editor for search queries 642 */ 643 private static void setPresetDblClickListener(TaggingPresetSelector selector, JTextComponent searchEditor) { 644 TaggingPreset selectedPreset = selector.getSelectedPresetAndUpdateClassification(); 645 646 if (selectedPreset == null) { 647 return; 648 } 649 650 /* 651 * Make sure that the focus is transferred to the search text field 652 * from the selector component. 653 */ 654 searchEditor.requestFocusInWindow(); 655 656 /* 657 * In order to make interaction with the search dialog simpler, 658 * we make sure that if autocompletion triggers and the text field is 659 * not in focus, the correct area is selected. We first request focus 660 * and then execute the selection logic. invokeLater allows us to 661 * defer the selection until waiting for focus. 662 */ 663 SwingUtilities.invokeLater(() -> { 664 int textOffset = searchEditor.getCaretPosition(); 665 String presetSearchQuery = " preset:" + 666 "\"" + selectedPreset.getRawName() + "\""; 667 try { 668 searchEditor.getDocument().insertString(textOffset, presetSearchQuery, null); 669 } catch (BadLocationException e1) { 670 throw new JosmRuntimeException(e1.getMessage(), e1); 671 } 672 }); 673 } 674 675 /** 676 * Interfaces implementing this may receive the result of the current search. 677 * @author Michael Zangl 678 * @since 10457 679 * @since 10600 (functional interface) 680 * @since 13950 (signature) 681 */ 682 @FunctionalInterface 683 interface SearchReceiver { 684 /** 685 * Receive the search result 686 * @param ds The data set searched on. 687 * @param result The result collection, including the initial collection. 688 * @param foundMatches The number of matches added to the result. 689 * @param setting The setting used. 690 * @param parent parent component 691 */ 692 void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, 693 int foundMatches, SearchSetting setting, Component parent); 694 } 695 696 /** 697 * Select the search result and display a status text for it. 698 */ 699 private static class SelectSearchReceiver implements SearchReceiver { 700 701 @Override 702 public void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, 703 int foundMatches, SearchSetting setting, Component parent) { 704 ds.setSelected(result); 705 MapFrame map = MainApplication.getMap(); 706 if (foundMatches == 0) { 707 final String msg; 708 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY); 709 if (setting.mode == SearchMode.replace) { 710 msg = tr("No match found for ''{0}''", text); 711 } else if (setting.mode == SearchMode.add) { 712 msg = tr("Nothing added to selection by searching for ''{0}''", text); 713 } else if (setting.mode == SearchMode.remove) { 714 msg = tr("Nothing removed from selection by searching for ''{0}''", text); 715 } else if (setting.mode == SearchMode.in_selection) { 716 msg = tr("Nothing found in selection by searching for ''{0}''", text); 717 } else { 718 msg = null; 719 } 720 if (map != null) { 721 map.statusLine.setHelpText(msg); 722 } 723 if (!GraphicsEnvironment.isHeadless()) { 724 new Notification(msg).show(); 725 } 726 } else { 727 map.statusLine.setHelpText(tr("Found {0} matches", foundMatches)); 728 } 729 } 730 } 731 732 /** 733 * This class stores the result of the search in a local variable. 734 * @author Michael Zangl 735 */ 736 private static final class CapturingSearchReceiver implements SearchReceiver { 737 private Collection<IPrimitive> result; 738 739 @Override 740 public void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, int foundMatches, 741 SearchSetting setting, Component parent) { 742 this.result = result; 743 } 744 } 745 746 static final class SearchTask extends PleaseWaitRunnable { 747 private final OsmData<?, ?, ?, ?> ds; 748 private final SearchSetting setting; 749 private final Collection<IPrimitive> selection; 750 private final Predicate<IPrimitive> predicate; 751 private boolean canceled; 752 private int foundMatches; 753 private final SearchReceiver resultReceiver; 754 755 private SearchTask(OsmData<?, ?, ?, ?> ds, SearchSetting setting, Collection<IPrimitive> selection, 756 Predicate<IPrimitive> predicate, SearchReceiver resultReceiver) { 757 super(tr("Searching")); 758 this.ds = ds; 759 this.setting = setting; 760 this.selection = selection; 761 this.predicate = predicate; 762 this.resultReceiver = resultReceiver; 763 } 764 765 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) { 766 final OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData(); 767 if (ds == null) { 768 throw new IllegalStateException("No active dataset"); 769 } 770 return newSearchTask(setting, ds, resultReceiver); 771 } 772 773 /** 774 * Create a new search task for the given search setting. 775 * @param setting The setting to use 776 * @param ds The data set to search on 777 * @param resultReceiver will receive the search result 778 * @return A new search task. 779 */ 780 private static SearchTask newSearchTask(SearchSetting setting, final OsmData<?, ?, ?, ?> ds, SearchReceiver resultReceiver) { 781 final Collection<IPrimitive> selection = new HashSet<>(ds.getAllSelected()); 782 return new SearchTask(ds, setting, selection, IPrimitive::isSelected, resultReceiver); 783 } 784 785 @Override 786 protected void cancel() { 787 this.canceled = true; 788 } 789 790 @Override 791 protected void realRun() { 792 try { 793 foundMatches = 0; 794 SearchCompiler.Match matcher = SearchCompiler.compile(setting); 795 796 if (setting.mode == SearchMode.replace) { 797 selection.clear(); 798 } else if (setting.mode == SearchMode.in_selection) { 799 foundMatches = selection.size(); 800 } 801 802 Collection<? extends IPrimitive> all; 803 if (setting.allElements) { 804 all = ds.allPrimitives(); 805 } else { 806 all = ds.getPrimitives(p -> p.isSelectable()); // Do not use method reference before Java 11! 807 } 808 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false); 809 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size())); 810 811 for (IPrimitive osm : all) { 812 if (canceled) { 813 return; 814 } 815 if (setting.mode == SearchMode.replace) { 816 if (matcher.match(osm)) { 817 selection.add(osm); 818 ++foundMatches; 819 } 820 } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) { 821 selection.add(osm); 822 ++foundMatches; 823 } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) { 824 selection.remove(osm); 825 ++foundMatches; 826 } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) { 827 selection.remove(osm); 828 --foundMatches; 829 } 830 subMonitor.worked(1); 831 } 832 subMonitor.finishTask(); 833 } catch (SearchParseError e) { 834 Logging.debug(e); 835 JOptionPane.showMessageDialog( 836 MainApplication.getMainFrame(), 837 e.getMessage(), 838 tr("Error"), 839 JOptionPane.ERROR_MESSAGE 840 ); 841 } 842 } 843 844 @Override 845 protected void finish() { 846 if (canceled) { 847 return; 848 } 849 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting, getProgressMonitor().getWindowParent()); 850 } 851 } 852 853 /** 854 * {@link ActionParameter} implementation with {@link SearchSetting} as value type. 855 * @since 12547 (moved from {@link ActionParameter}) 856 */ 857 public static class SearchSettingsActionParameter extends ActionParameter<SearchSetting> { 858 859 /** 860 * Constructs a new {@code SearchSettingsActionParameter}. 861 * @param name parameter name (the key) 862 */ 863 public SearchSettingsActionParameter(String name) { 864 super(name); 865 } 866 867 @Override 868 public Class<SearchSetting> getType() { 869 return SearchSetting.class; 870 } 871 872 @Override 873 public SearchSetting readFromString(String s) { 874 return SearchSetting.readFromString(s); 875 } 876 877 @Override 878 public String writeToString(SearchSetting value) { 879 if (value == null) 880 return ""; 881 return value.writeToString(); 882 } 883 } 884 885 /** 886 * Refreshes the enabled state 887 */ 888 @Override 889 protected void updateEnabledState() { 890 setEnabled(getLayerManager().getActiveData() != null); 891 } 892 893 @Override 894 public List<ActionParameter<?>> getActionParameters() { 895 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION)); 896 } 897}