001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trc; 006 007import java.awt.Cursor; 008import java.awt.Dimension; 009import java.awt.FlowLayout; 010import java.awt.GridBagLayout; 011import java.awt.event.ActionEvent; 012import java.awt.event.ItemEvent; 013import java.awt.event.ItemListener; 014import java.awt.event.MouseAdapter; 015import java.awt.event.MouseEvent; 016import java.util.Arrays; 017import java.util.List; 018 019import javax.swing.BorderFactory; 020import javax.swing.ButtonGroup; 021import javax.swing.JCheckBox; 022import javax.swing.JLabel; 023import javax.swing.JOptionPane; 024import javax.swing.JPanel; 025import javax.swing.JRadioButton; 026import javax.swing.SwingUtilities; 027import javax.swing.text.BadLocationException; 028import javax.swing.text.Document; 029import javax.swing.text.JTextComponent; 030 031import org.openstreetmap.josm.data.osm.Filter; 032import org.openstreetmap.josm.data.osm.search.SearchCompiler; 033import org.openstreetmap.josm.data.osm.search.SearchMode; 034import org.openstreetmap.josm.data.osm.search.SearchParseError; 035import org.openstreetmap.josm.data.osm.search.SearchSetting; 036import org.openstreetmap.josm.gui.ExtendedDialog; 037import org.openstreetmap.josm.gui.MainApplication; 038import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException; 039import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset; 040import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector; 041import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 042import org.openstreetmap.josm.gui.widgets.HistoryComboBox; 043import org.openstreetmap.josm.tools.GBC; 044import org.openstreetmap.josm.tools.JosmRuntimeException; 045import org.openstreetmap.josm.tools.Logging; 046import org.openstreetmap.josm.tools.Utils; 047 048/** 049 * Search dialog to find primitives by a wide range of search criteria. 050 * @since 14927 (extracted from {@code SearchAction}) 051 */ 052public class SearchDialog extends ExtendedDialog { 053 054 private final SearchSetting searchSettings; 055 056 private final HistoryComboBox hcbSearchString = new HistoryComboBox(); 057 058 private JCheckBox addOnToolbar; 059 private JCheckBox caseSensitive; 060 private JCheckBox allElements; 061 062 private JRadioButton standardSearch; 063 private JRadioButton regexSearch; 064 private JRadioButton mapCSSSearch; 065 066 private JRadioButton replace; 067 private JRadioButton add; 068 private JRadioButton remove; 069 private JRadioButton inSelection; 070 071 /** 072 * Constructs a new {@code SearchDialog}. 073 * @param initialValues initial search settings 074 * @param searchExpressionHistory list of all texts that were recently used in the search 075 * @param expertMode expert mode 076 */ 077 public SearchDialog(SearchSetting initialValues, List<String> searchExpressionHistory, boolean expertMode) { 078 super(MainApplication.getMainFrame(), 079 initialValues instanceof Filter ? tr("Filter") : tr("Search"), 080 initialValues instanceof Filter ? tr("Submit filter") : tr("Search"), 081 tr("Cancel")); 082 this.searchSettings = new SearchSetting(initialValues); 083 setButtonIcons("dialogs/search", "cancel"); 084 configureContextsensitiveHelp("/Action/Search", true /* show help button */); 085 setContent(buildPanel(searchExpressionHistory, expertMode)); 086 } 087 088 private JPanel buildPanel(List<String> searchExpressionHistory, boolean expertMode) { 089 090 // prepare the combo box with the search expressions 091 JLabel label = new JLabel(searchSettings instanceof Filter ? tr("Filter string:") : tr("Search string:")); 092 093 String tooltip = tr("Enter the search expression"); 094 hcbSearchString.setText(searchSettings.text); 095 hcbSearchString.setToolTipText(tooltip); 096 097 hcbSearchString.setPossibleItemsTopDown(searchExpressionHistory); 098 hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height)); 099 label.setLabelFor(hcbSearchString); 100 101 replace = new JRadioButton(tr("select"), searchSettings.mode == SearchMode.replace); 102 add = new JRadioButton(tr("add to selection"), searchSettings.mode == SearchMode.add); 103 remove = new JRadioButton(tr("remove from selection"), searchSettings.mode == SearchMode.remove); 104 inSelection = new JRadioButton(tr("find in selection"), searchSettings.mode == SearchMode.in_selection); 105 ButtonGroup bg = new ButtonGroup(); 106 bg.add(replace); 107 bg.add(add); 108 bg.add(remove); 109 bg.add(inSelection); 110 111 caseSensitive = new JCheckBox(tr("case sensitive"), searchSettings.caseSensitive); 112 allElements = new JCheckBox(tr("all objects"), searchSettings.allElements); 113 allElements.setToolTipText(tr("Also include incomplete and deleted objects in search.")); 114 addOnToolbar = new JCheckBox(tr("add toolbar button"), false); 115 addOnToolbar.setToolTipText(tr("Add a button with this search expression to the toolbar.")); 116 117 standardSearch = new JRadioButton(tr("standard"), !searchSettings.regexSearch && !searchSettings.mapCSSSearch); 118 regexSearch = new JRadioButton(tr("regular expression"), searchSettings.regexSearch); 119 mapCSSSearch = new JRadioButton(tr("MapCSS selector"), searchSettings.mapCSSSearch); 120 121 ButtonGroup bg2 = new ButtonGroup(); 122 bg2.add(standardSearch); 123 bg2.add(regexSearch); 124 bg2.add(mapCSSSearch); 125 126 JPanel selectionSettings = new JPanel(new GridBagLayout()); 127 selectionSettings.setBorder(BorderFactory.createTitledBorder(tr("Results"))); 128 selectionSettings.add(replace, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL)); 129 selectionSettings.add(add, GBC.eol()); 130 selectionSettings.add(remove, GBC.eol()); 131 selectionSettings.add(inSelection, GBC.eop()); 132 133 JPanel additionalSettings = new JPanel(new GridBagLayout()); 134 additionalSettings.setBorder(BorderFactory.createTitledBorder(tr("Options"))); 135 additionalSettings.add(caseSensitive, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL)); 136 137 JPanel left = new JPanel(new GridBagLayout()); 138 139 left.add(selectionSettings, GBC.eol().fill(GBC.BOTH)); 140 left.add(additionalSettings, GBC.eol().fill(GBC.BOTH)); 141 142 if (expertMode) { 143 additionalSettings.add(allElements, GBC.eol()); 144 additionalSettings.add(addOnToolbar, GBC.eop()); 145 146 JPanel searchOptions = new JPanel(new GridBagLayout()); 147 searchOptions.setBorder(BorderFactory.createTitledBorder(tr("Search syntax"))); 148 searchOptions.add(standardSearch, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL)); 149 searchOptions.add(regexSearch, GBC.eol()); 150 searchOptions.add(mapCSSSearch, GBC.eol()); 151 152 left.add(searchOptions, GBC.eol().fill(GBC.BOTH)); 153 } 154 155 JPanel right = buildHintsSection(hcbSearchString, expertMode); 156 JPanel top = new JPanel(new GridBagLayout()); 157 top.add(label, GBC.std().insets(0, 0, 5, 0)); 158 top.add(hcbSearchString, GBC.eol().fill(GBC.HORIZONTAL)); 159 160 JTextComponent editorComponent = hcbSearchString.getEditorComponent(); 161 Document document = editorComponent.getDocument(); 162 163 /* 164 * Setup the logic to validate the contents of the search text field which is executed 165 * every time the content of the field has changed. If the query is incorrect, then 166 * the text field is colored red. 167 */ 168 AbstractTextComponentValidator validator = new AbstractTextComponentValidator(editorComponent) { 169 170 @Override 171 public void validate() { 172 if (!isValid()) { 173 feedbackInvalid(tr("Invalid search expression")); 174 } else { 175 feedbackValid(tooltip); 176 } 177 } 178 179 @Override 180 public boolean isValid() { 181 try { 182 SearchSetting ss = new SearchSetting(); 183 ss.text = hcbSearchString.getText(); 184 ss.caseSensitive = caseSensitive.isSelected(); 185 ss.regexSearch = regexSearch.isSelected(); 186 ss.mapCSSSearch = mapCSSSearch.isSelected(); 187 SearchCompiler.compile(ss); 188 return true; 189 } catch (SearchParseError | MapCSSException e) { 190 return false; 191 } 192 } 193 }; 194 document.addDocumentListener(validator); 195 ItemListener validateActionListener = e -> { 196 if (e.getStateChange() == ItemEvent.SELECTED) { 197 validator.validate(); 198 } 199 }; 200 standardSearch.addItemListener(validateActionListener); 201 regexSearch.addItemListener(validateActionListener); 202 mapCSSSearch.addItemListener(validateActionListener); 203 204 /* 205 * Setup the logic to append preset queries to the search text field according to 206 * selected preset by the user. Every query is of the form ' group/sub-group/.../presetName' 207 * if the corresponding group of the preset exists, otherwise it is simply ' presetName'. 208 */ 209 TaggingPresetSelector selector = new TaggingPresetSelector(false, false); 210 selector.setBorder(BorderFactory.createTitledBorder(tr("Search by preset"))); 211 selector.setDblClickListener(ev -> setPresetDblClickListener(selector, editorComponent)); 212 213 JPanel p = new JPanel(new GridBagLayout()); 214 p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0)); 215 p.add(left, GBC.std().anchor(GBC.NORTH).insets(5, 10, 10, 0).fill(GBC.VERTICAL)); 216 p.add(right, GBC.std().fill(GBC.BOTH).insets(0, 10, 0, 0)); 217 p.add(selector, GBC.eol().fill(GBC.BOTH).insets(0, 10, 0, 0)); 218 219 return p; 220 } 221 222 @Override 223 protected void buttonAction(int buttonIndex, ActionEvent evt) { 224 if (buttonIndex == 0) { 225 try { 226 SearchSetting ss = new SearchSetting(); 227 ss.text = hcbSearchString.getText(); 228 ss.caseSensitive = caseSensitive.isSelected(); 229 ss.regexSearch = regexSearch.isSelected(); 230 ss.mapCSSSearch = mapCSSSearch.isSelected(); 231 SearchCompiler.compile(ss); 232 super.buttonAction(buttonIndex, evt); 233 } catch (SearchParseError | MapCSSException e) { 234 Logging.debug(e); 235 JOptionPane.showMessageDialog( 236 MainApplication.getMainFrame(), 237 "<html>" + tr("Search expression is not valid: \n\n {0}", 238 e.getMessage().replace("<html>", "").replace("</html>", "")).replace("\n", "<br>") + 239 "</html>", 240 tr("Invalid search expression"), 241 JOptionPane.ERROR_MESSAGE); 242 } 243 } else { 244 super.buttonAction(buttonIndex, evt); 245 } 246 } 247 248 /** 249 * Returns the search settings chosen by user. 250 * @return the search settings chosen by user 251 */ 252 public SearchSetting getSearchSettings() { 253 searchSettings.text = hcbSearchString.getText(); 254 searchSettings.caseSensitive = caseSensitive.isSelected(); 255 searchSettings.allElements = allElements.isSelected(); 256 searchSettings.regexSearch = regexSearch.isSelected(); 257 searchSettings.mapCSSSearch = mapCSSSearch.isSelected(); 258 259 if (inSelection.isSelected()) { 260 searchSettings.mode = SearchMode.in_selection; 261 } else if (replace.isSelected()) { 262 searchSettings.mode = SearchMode.replace; 263 } else if (add.isSelected()) { 264 searchSettings.mode = SearchMode.add; 265 } else { 266 searchSettings.mode = SearchMode.remove; 267 } 268 return searchSettings; 269 } 270 271 /** 272 * Determines if the "add toolbar button" checkbox is selected. 273 * @return {@code true} if the "add toolbar button" checkbox is selected 274 */ 275 public boolean isAddOnToolbar() { 276 return addOnToolbar.isSelected(); 277 } 278 279 private static JPanel buildHintsSection(HistoryComboBox hcbSearchString, boolean expertMode) { 280 JPanel hintPanel = new JPanel(new GridBagLayout()); 281 hintPanel.setBorder(BorderFactory.createTitledBorder(tr("Hints"))); 282 283 hintPanel.add(new SearchKeywordRow(hcbSearchString) 284 .addTitle(tr("basics")) 285 .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key")) 286 .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key")) 287 .addKeyword("<i>key</i>:<i>valuefragment</i>", null, 288 tr("''valuefragment'' anywhere in ''key''"), 289 trc("search string example", "name:str matches name=Bakerstreet")) 290 .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''")), 291 GBC.eol()); 292 hintPanel.add(new SearchKeywordRow(hcbSearchString) 293 .addKeyword("<i>key</i>", null, tr("matches if ''key'' exists")) 294 .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''")) 295 .addKeyword("<i>key</i>=*", null, tr("''key'' with any value")) 296 .addKeyword("<i>key</i>=", null, tr("''key'' with empty value")) 297 .addKeyword("*=<i>value</i>", null, tr("''value'' in any key")) 298 .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)")) 299 .addKeyword("\"key\"=\"value\"", "\"\"=\"\"", 300 tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " + 301 "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."), 302 trc("search string example", "name=\"Baker Street\""), 303 "\"addr:street\""), 304 GBC.eol().anchor(GBC.CENTER)); 305 hintPanel.add(new SearchKeywordRow(hcbSearchString) 306 .addTitle(tr("combinators")) 307 .addKeyword("<i>expr</i> <i>expr</i>", null, 308 tr("logical and (both expressions have to be satisfied)"), 309 trc("search string example", "Baker Street")) 310 .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)")) 311 .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)")) 312 .addKeyword("-<i>expr</i>", null, tr("logical not")) 313 .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")), 314 GBC.eol()); 315 316 if (expertMode) { 317 hintPanel.add(new SearchKeywordRow(hcbSearchString) 318 .addTitle(tr("objects")) 319 .addKeyword("type:node", "type:node ", tr("all nodes")) 320 .addKeyword("type:way", "type:way ", tr("all ways")) 321 .addKeyword("type:relation", "type:relation ", tr("all relations")) 322 .addKeyword("closed", "closed ", tr("all closed ways")) 323 .addKeyword("untagged", "untagged ", tr("object without useful tags")), 324 GBC.eol()); 325 hintPanel.add(new SearchKeywordRow(hcbSearchString) 326 .addKeyword("preset:\"Annotation/Address\"", "preset:\"Annotation/Address\"", 327 tr("all objects that use the address preset")) 328 .addKeyword("preset:\"Geography/Nature/*\"", "preset:\"Geography/Nature/*\"", 329 tr("all objects that use any preset under the Geography/Nature group")), 330 GBC.eol().anchor(GBC.CENTER)); 331 hintPanel.add(new SearchKeywordRow(hcbSearchString) 332 .addTitle(tr("metadata")) 333 .addKeyword("user:", "user:", tr("objects changed by author"), 334 trc("search string example", "user:<i>OSM username</i> (objects with the author <i>OSM username</i>)"), 335 trc("search string example", "user:anonymous (objects without an assigned author)")) 336 .addKeyword("id:", "id:", tr("objects with given ID"), 337 trc("search string example", "id:0 (new objects)")) 338 .addKeyword("version:", "version:", tr("objects with given version"), 339 trc("search string example", "version:0 (objects without an assigned version)")) 340 .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"), 341 trc("search string example", "changeset:0 (objects without an assigned changeset)")) 342 .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/", 343 "timestamp:2008/2011-02-04T12"), 344 GBC.eol()); 345 hintPanel.add(new SearchKeywordRow(hcbSearchString) 346 .addTitle(tr("properties")) 347 .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes")) 348 .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways")) 349 .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags")) 350 .addKeyword("role:", "role:", tr("objects with given role in a relation")) 351 .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2")) 352 .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")), 353 GBC.eol()); 354 hintPanel.add(new SearchKeywordRow(hcbSearchString) 355 .addTitle(tr("state")) 356 .addKeyword("modified", "modified ", tr("all modified objects")) 357 .addKeyword("new", "new ", tr("all new objects")) 358 .addKeyword("selected", "selected ", tr("all selected objects")) 359 .addKeyword("incomplete", "incomplete ", tr("all incomplete objects")) 360 .addKeyword("deleted", "deleted ", tr("all deleted objects (checkbox <b>{0}</b> must be enabled)", tr("all objects"))), 361 GBC.eol()); 362 hintPanel.add(new SearchKeywordRow(hcbSearchString) 363 .addTitle(tr("related objects")) 364 .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building") 365 .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop") 366 .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>")) 367 .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>")) 368 .addKeyword("nth:<i>7</i>", "nth:", 369 tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1") 370 .addKeyword("nth%:<i>7</i>", "nth%:", 371 tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"), 372 GBC.eol()); 373 hintPanel.add(new SearchKeywordRow(hcbSearchString) 374 .addTitle(tr("view")) 375 .addKeyword("inview", "inview ", tr("objects in current view")) 376 .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view")) 377 .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area")) 378 .addKeyword("allindownloadedarea", "allindownloadedarea ", 379 tr("objects (and all its way nodes / relation members) in downloaded area")), 380 GBC.eol()); 381 } 382 383 return hintPanel; 384 } 385 386 /** 387 * 388 * @param selector Selector component that the user interacts with 389 * @param searchEditor Editor for search queries 390 */ 391 private static void setPresetDblClickListener(TaggingPresetSelector selector, JTextComponent searchEditor) { 392 TaggingPreset selectedPreset = selector.getSelectedPresetAndUpdateClassification(); 393 394 if (selectedPreset == null) { 395 return; 396 } 397 398 // Make sure that the focus is transferred to the search text field from the selector component 399 searchEditor.requestFocusInWindow(); 400 401 // In order to make interaction with the search dialog simpler, we make sure that 402 // if autocompletion triggers and the text field is not in focus, the correct area is selected. 403 // We first request focus and then execute the selection logic. 404 // invokeLater allows us to defer the selection until waiting for focus. 405 SwingUtilities.invokeLater(() -> { 406 int textOffset = searchEditor.getCaretPosition(); 407 String presetSearchQuery = " preset:" + 408 "\"" + selectedPreset.getRawName() + "\""; 409 try { 410 searchEditor.getDocument().insertString(textOffset, presetSearchQuery, null); 411 } catch (BadLocationException e1) { 412 throw new JosmRuntimeException(e1.getMessage(), e1); 413 } 414 }); 415 } 416 417 private static class SearchKeywordRow extends JPanel { 418 419 private final HistoryComboBox hcb; 420 421 SearchKeywordRow(HistoryComboBox hcb) { 422 super(new FlowLayout(FlowLayout.LEFT)); 423 this.hcb = hcb; 424 } 425 426 /** 427 * Adds the title (prefix) label at the beginning of the row. Should be called only once. 428 * @param title English title 429 * @return {@code this} for easy chaining 430 */ 431 public SearchKeywordRow addTitle(String title) { 432 add(new JLabel(tr("{0}: ", title))); 433 return this; 434 } 435 436 /** 437 * Adds an example keyword label at the end of the row. Can be called several times. 438 * @param displayText displayed HTML text 439 * @param insertText optional: if set, makes the label clickable, and {@code insertText} will be inserted in search string 440 * @param description optional: HTML text to be displayed in the tooltip 441 * @param examples optional: examples joined as HTML list in the tooltip 442 * @return {@code this} for easy chaining 443 */ 444 public SearchKeywordRow addKeyword(String displayText, final String insertText, String description, String... examples) { 445 JLabel label = new JLabel("<html>" 446 + "<style>td{border:1px solid gray; font-weight:normal;}</style>" 447 + "<table><tr><td>" + displayText + "</td></tr></table></html>"); 448 add(label); 449 if (description != null || examples.length > 0) { 450 label.setToolTipText("<html>" 451 + description 452 + (examples.length > 0 ? Utils.joinAsHtmlUnorderedList(Arrays.asList(examples)) : "") 453 + "</html>"); 454 } 455 if (insertText != null) { 456 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 457 label.addMouseListener(new MouseAdapter() { 458 459 @Override 460 public void mouseClicked(MouseEvent e) { 461 JTextComponent tf = hcb.getEditorComponent(); 462 463 // Make sure that the focus is transferred to the search text field from the selector component 464 if (!tf.hasFocus()) { 465 tf.requestFocusInWindow(); 466 } 467 468 // In order to make interaction with the search dialog simpler, we make sure that 469 // if autocompletion triggers and the text field is not in focus, the correct area is selected. 470 // We first request focus and then execute the selection logic. 471 // invokeLater allows us to defer the selection until waiting for focus. 472 SwingUtilities.invokeLater(() -> { 473 try { 474 tf.getDocument().insertString(tf.getCaretPosition(), ' ' + insertText, null); 475 } catch (BadLocationException ex) { 476 throw new JosmRuntimeException(ex.getMessage(), ex); 477 } 478 }); 479 } 480 }); 481 } 482 return this; 483 } 484 } 485}