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