source: josm/trunk/src/org/openstreetmap/josm/actions/search/SearchAction.java

Last change on this file was 14687, checked in by GerdP, 2 months ago

revert accidential commit

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