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