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}