source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/SearchDialog.java@ 18069

Last change on this file since 18069 was 17055, checked in by GerdP, 4 years ago

see #19793: Memory leaks: Bad programming pattern reg. listeners
SearchDialog did not remove TaggingPresetListener

  • Property svn:eol-style set to native
File size: 27.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trc;
6
7import java.awt.Component;
8import java.awt.Cursor;
9import java.awt.Dimension;
10import java.awt.FlowLayout;
11import java.awt.GridBagLayout;
12import java.awt.event.ActionEvent;
13import java.awt.event.ItemEvent;
14import java.awt.event.ItemListener;
15import java.awt.event.MouseAdapter;
16import java.awt.event.MouseEvent;
17import java.util.Arrays;
18import java.util.List;
19
20import javax.swing.BorderFactory;
21import javax.swing.ButtonGroup;
22import javax.swing.JCheckBox;
23import javax.swing.JLabel;
24import javax.swing.JOptionPane;
25import javax.swing.JPanel;
26import javax.swing.JRadioButton;
27import javax.swing.SwingUtilities;
28import javax.swing.text.BadLocationException;
29import javax.swing.text.Document;
30import javax.swing.text.JTextComponent;
31
32import org.openstreetmap.josm.data.osm.Filter;
33import org.openstreetmap.josm.data.osm.search.SearchCompiler;
34import org.openstreetmap.josm.data.osm.search.SearchMode;
35import org.openstreetmap.josm.data.osm.search.SearchParseError;
36import org.openstreetmap.josm.data.osm.search.SearchSetting;
37import org.openstreetmap.josm.gui.ExtendedDialog;
38import org.openstreetmap.josm.gui.MainApplication;
39import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
40import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
41import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
42import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
43import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
44import org.openstreetmap.josm.tools.GBC;
45import org.openstreetmap.josm.tools.JosmRuntimeException;
46import org.openstreetmap.josm.tools.Logging;
47import org.openstreetmap.josm.tools.Utils;
48
49/**
50 * Search dialog to find primitives by a wide range of search criteria.
51 * @since 14927 (extracted from {@code SearchAction})
52 */
53public class SearchDialog extends ExtendedDialog {
54
55 private final SearchSetting searchSettings;
56
57 protected final HistoryComboBox hcbSearchString = new HistoryComboBox();
58
59 private JCheckBox addOnToolbar;
60 private JCheckBox caseSensitive;
61 private JCheckBox allElements;
62
63 private JRadioButton standardSearch;
64 private JRadioButton regexSearch;
65 private JRadioButton mapCSSSearch;
66
67 private JRadioButton replace;
68 private JRadioButton add;
69 private JRadioButton remove;
70 private JRadioButton inSelection;
71 private TaggingPresetSelector selector;
72 /**
73 * Constructs a new {@code SearchDialog}.
74 * @param initialValues initial search settings
75 * @param searchExpressionHistory list of all texts that were recently used in the search
76 * @param expertMode expert mode
77 */
78 public SearchDialog(SearchSetting initialValues, List<String> searchExpressionHistory, boolean expertMode) {
79 this(initialValues, searchExpressionHistory, new PanelOptions(expertMode, false), MainApplication.getMainFrame(),
80 initialValues instanceof Filter ? tr("Filter") : tr("Search"),
81 initialValues instanceof Filter ? tr("Submit filter") : tr("Search"),
82 tr("Cancel"));
83 setButtonIcons("dialogs/search", "cancel");
84 configureContextsensitiveHelp("/Action/Search", true /* show help button */);
85 }
86
87 protected SearchDialog(SearchSetting initialValues, List<String> searchExpressionHistory, PanelOptions options,
88 Component mainFrame, String title, String... buttonTexts) {
89 super(mainFrame, title, buttonTexts);
90 this.searchSettings = new SearchSetting(initialValues);
91 setContent(buildPanel(searchExpressionHistory, options));
92 }
93
94 /**
95 * Determines which parts of the search dialog will be shown
96 */
97 protected static class PanelOptions {
98 private final boolean expertMode;
99 private final boolean overpassQuery;
100
101 /**
102 * Constructs new options which determine which parts of the search dialog will be shown
103 * @param expertMode whether export mode is enabled
104 * @param overpassQuery whether the panel shall be adapted for Overpass query
105 */
106 public PanelOptions(boolean expertMode, boolean overpassQuery) {
107 this.expertMode = expertMode;
108 this.overpassQuery = overpassQuery;
109 }
110 }
111
112 private JPanel buildPanel(List<String> searchExpressionHistory, PanelOptions options) {
113
114 // prepare the combo box with the search expressions
115 JLabel label = new JLabel(searchSettings instanceof Filter ? tr("Filter string:") : tr("Search string:"));
116
117 String tooltip = tr("Enter the search expression");
118 hcbSearchString.setText(searchSettings.text);
119 hcbSearchString.setToolTipText(tooltip);
120
121 hcbSearchString.setPossibleItemsTopDown(searchExpressionHistory);
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(HistoryComboBox 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 HistoryComboBox hcb;
469
470 SearchKeywordRow(HistoryComboBox 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}
Note: See TracBrowser for help on using the repository browser.