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

Last change on this file since 12464 was 12464, checked in by michael2402, 21 months ago

Apply #14923: Adjust the search dialog to allow to search for primitives that use a preset. Patch by bafonins

  • Property svn:eol-style set to native
File size: 43.2 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.trc;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
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.Objects;
28import java.util.Set;
29import java.util.function.Predicate;
30
31import javax.swing.BorderFactory;
32import javax.swing.ButtonGroup;
33import javax.swing.JCheckBox;
34import javax.swing.JLabel;
35import javax.swing.JOptionPane;
36import javax.swing.JPanel;
37import javax.swing.JRadioButton;
38import javax.swing.SwingUtilities;
39import javax.swing.text.BadLocationException;
40import javax.swing.text.Document;
41import javax.swing.text.JTextComponent;
42
43import org.openstreetmap.josm.Main;
44import org.openstreetmap.josm.actions.ActionParameter;
45import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter;
46import org.openstreetmap.josm.actions.ExpertToggleAction;
47import org.openstreetmap.josm.actions.JosmAction;
48import org.openstreetmap.josm.actions.ParameterizedAction;
49import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
50import org.openstreetmap.josm.data.osm.DataSet;
51import org.openstreetmap.josm.data.osm.Filter;
52import org.openstreetmap.josm.data.osm.OsmPrimitive;
53import org.openstreetmap.josm.gui.ExtendedDialog;
54import org.openstreetmap.josm.gui.PleaseWaitRunnable;
55import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
56import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
57import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
58import org.openstreetmap.josm.gui.progress.ProgressMonitor;
59import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
60import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
61import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
62import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
63import org.openstreetmap.josm.tools.GBC;
64import org.openstreetmap.josm.tools.JosmRuntimeException;
65import org.openstreetmap.josm.tools.Shortcut;
66import org.openstreetmap.josm.tools.Utils;
67
68/**
69 * The search action allows the user to search the data layer using a complex search string.
70 *
71 * @see SearchCompiler
72 */
73public class SearchAction extends JosmAction implements ParameterizedAction {
74
75    /**
76     * The default size of the search history
77     */
78    public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
79    /**
80     * Maximum number of characters before the search expression is shortened for display purposes.
81     */
82    public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100;
83
84    private static final String SEARCH_EXPRESSION = "searchExpression";
85
86    /**
87     * Search mode.
88     */
89    public enum SearchMode {
90        /** replace selection */
91        replace('R'),
92        /** add to selection */
93        add('A'),
94        /** remove from selection */
95        remove('D'),
96        /** find in selection */
97        in_selection('S');
98
99        private final char code;
100
101        SearchMode(char code) {
102            this.code = code;
103        }
104
105        /**
106         * Returns the unique character code of this mode.
107         * @return the unique character code of this mode
108         */
109        public char getCode() {
110            return code;
111        }
112
113        /**
114         * Returns the search mode matching the given character code.
115         * @param code character code
116         * @return search mode matching the given character code
117         */
118        public static SearchMode fromCode(char code) {
119            for (SearchMode mode: values()) {
120                if (mode.getCode() == code)
121                    return mode;
122            }
123            return null;
124        }
125    }
126
127    private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>();
128    static {
129        for (String s: Main.pref.getCollection("search.history", Collections.<String>emptyList())) {
130            SearchSetting ss = SearchSetting.readFromString(s);
131            if (ss != null) {
132                searchHistory.add(ss);
133            }
134        }
135    }
136
137    /**
138     * Gets the search history
139     * @return The last searched terms. Do not modify it.
140     */
141    public static Collection<SearchSetting> getSearchHistory() {
142        return searchHistory;
143    }
144
145    /**
146     * Saves a search to the search history.
147     * @param s The search to save
148     */
149    public static void saveToHistory(SearchSetting s) {
150        if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
151            searchHistory.addFirst(new SearchSetting(s));
152        } else if (searchHistory.contains(s)) {
153            // move existing entry to front, fixes #8032 - search history loses entries when re-using queries
154            searchHistory.remove(s);
155            searchHistory.addFirst(new SearchSetting(s));
156        }
157        int maxsize = Main.pref.getInteger("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
158        while (searchHistory.size() > maxsize) {
159            searchHistory.removeLast();
160        }
161        Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size());
162        for (SearchSetting item: searchHistory) {
163            savedHistory.add(item.writeToString());
164        }
165        Main.pref.putCollection("search.history", savedHistory);
166    }
167
168    /**
169     * Gets a list of all texts that were recently used in the search
170     * @return The list of search texts.
171     */
172    public static List<String> getSearchExpressionHistory() {
173        List<String> ret = new ArrayList<>(getSearchHistory().size());
174        for (SearchSetting ss: getSearchHistory()) {
175            ret.add(ss.text);
176        }
177        return ret;
178    }
179
180    private static volatile SearchSetting lastSearch;
181
182    /**
183     * Constructs a new {@code SearchAction}.
184     */
185    public SearchAction() {
186        super(tr("Search..."), "dialogs/search", tr("Search for objects."),
187                Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true);
188        putValue("help", ht("/Action/Search"));
189    }
190
191    @Override
192    public void actionPerformed(ActionEvent e) {
193        if (!isEnabled())
194            return;
195        search();
196    }
197
198    @Override
199    public void actionPerformed(ActionEvent e, Map<String, Object> parameters) {
200        if (parameters.get(SEARCH_EXPRESSION) == null) {
201            actionPerformed(e);
202        } else {
203            searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION));
204        }
205    }
206
207    private static class SearchKeywordRow extends JPanel {
208
209        private final HistoryComboBox hcb;
210
211        SearchKeywordRow(HistoryComboBox hcb) {
212            super(new FlowLayout(FlowLayout.LEFT));
213            this.hcb = hcb;
214        }
215
216        public SearchKeywordRow addTitle(String title) {
217            add(new JLabel(tr("{0}: ", title)));
218            return this;
219        }
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 (ParseError | 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 (ParseError e) {
419                        Main.debug(e);
420                        JOptionPane.showMessageDialog(
421                                Main.parent,
422                                tr("Search expression is not valid: \n\n {0}", e.getMessage()),
423                                tr("Invalid search expression"),
424                                JOptionPane.ERROR_MESSAGE);
425                    }
426                } else {
427                    super.buttonAction(buttonIndex, evt);
428                }
429            }
430        };
431        dialog.setButtonIcons("dialogs/search", "cancel");
432        dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */);
433        dialog.setContent(p);
434
435        if (dialog.showDialog().getValue() != 1) return null;
436
437        // User pressed OK - let's perform the search
438        initialValues.text = hcbSearchString.getText();
439        initialValues.caseSensitive = caseSensitive.isSelected();
440        initialValues.allElements = allElements.isSelected();
441        initialValues.regexSearch = regexSearch.isSelected();
442        initialValues.mapCSSSearch = mapCSSSearch.isSelected();
443
444        if (inSelection.isSelected()) {
445            initialValues.mode = SearchAction.SearchMode.in_selection;
446        } else if (replace.isSelected()) {
447            initialValues.mode = SearchAction.SearchMode.replace;
448        } else if (add.isSelected()) {
449            initialValues.mode = SearchAction.SearchMode.add;
450        } else {
451            initialValues.mode = SearchAction.SearchMode.remove;
452        }
453
454        if (addOnToolbar.isSelected()) {
455            ToolbarPreferences.ActionDefinition aDef =
456                    new ToolbarPreferences.ActionDefinition(Main.main.menu.search);
457            aDef.getParameters().put(SEARCH_EXPRESSION, initialValues);
458            // Display search expression as tooltip instead of generic one
459            aDef.setName(Utils.shortenString(initialValues.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY));
460            // parametrized action definition is now composed
461            ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
462            String res = actionParser.saveAction(aDef);
463
464            // add custom search button to toolbar preferences
465            Main.toolbar.addCustomButton(res, -1, false);
466        }
467
468        return initialValues;
469    }
470
471    private static JPanel buildHintsSection(HistoryComboBox hcbSearchString) {
472        JPanel hintPanel = new JPanel(new GridBagLayout());
473        hintPanel.setBorder(BorderFactory.createTitledBorder(tr("Search hints")));
474
475        hintPanel.add(new SearchKeywordRow(hcbSearchString)
476                .addTitle(tr("basics"))
477                .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key"))
478                .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key"))
479                .addKeyword("<i>key</i>:<i>valuefragment</i>", null,
480                        tr("''valuefragment'' anywhere in ''key''"), "name:str matches name=Bakerstreet")
481                .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''")),
482                GBC.eol());
483        hintPanel.add(new SearchKeywordRow(hcbSearchString)
484                .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''"))
485                .addKeyword("<i>key</i>=*", null, tr("''key'' with any value"))
486                .addKeyword("*=<i>value</i>", null, tr("''value'' in any key"))
487                .addKeyword("<i>key</i>=", null, tr("matches if ''key'' exists"))
488                .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)"))
489                .addKeyword("\"key\"=\"value\"", "\"\"=\"\"",
490                        tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " +
491                                "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."),
492                        "\"addr:street\""),
493                GBC.eol().anchor(GBC.CENTER));
494        hintPanel.add(new SearchKeywordRow(hcbSearchString)
495                .addTitle(tr("combinators"))
496                .addKeyword("<i>expr</i> <i>expr</i>", null, tr("logical and (both expressions have to be satisfied)"))
497                .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)"))
498                .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)"))
499                .addKeyword("-<i>expr</i>", null, tr("logical not"))
500                .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")),
501                GBC.eol());
502
503        if (ExpertToggleAction.isExpert()) {
504            hintPanel.add(new SearchKeywordRow(hcbSearchString)
505                .addTitle(tr("objects"))
506                .addKeyword("type:node", "type:node ", tr("all nodes"))
507                .addKeyword("type:way", "type:way ", tr("all ways"))
508                .addKeyword("type:relation", "type:relation ", tr("all relations"))
509                .addKeyword("closed", "closed ", tr("all closed ways"))
510                .addKeyword("untagged", "untagged ", tr("object without useful tags")),
511                GBC.eol());
512            hintPanel.add(new SearchKeywordRow(hcbSearchString)
513                    .addKeyword("preset:\"Annotation/Address\"", "preset:\"Annotation/Address\"",
514                            tr("all objects that use the address preset"))
515                    .addKeyword("preset:\"Geography/Nature/*\"", "preset:\"Geography/Nature/*\"",
516                            tr("all objects that use any preset under the Geography/Nature group")),
517                    GBC.eol().anchor(GBC.CENTER));
518            hintPanel.add(new SearchKeywordRow(hcbSearchString)
519                .addTitle(tr("metadata"))
520                .addKeyword("user:", "user:", tr("objects changed by user", "user:anonymous"))
521                .addKeyword("id:", "id:", tr("objects with given ID"), "id:0 (new objects)")
522                .addKeyword("version:", "version:", tr("objects with given version"), "version:0 (objects without an assigned version)")
523                .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"),
524                        "changeset:0 (objects without an assigned changeset)")
525                .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/",
526                        "timestamp:2008/2011-02-04T12"),
527                GBC.eol());
528            hintPanel.add(new SearchKeywordRow(hcbSearchString)
529                .addTitle(tr("properties"))
530                .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes"))
531                .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways"))
532                .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags"))
533                .addKeyword("role:", "role:", tr("objects with given role in a relation"))
534                .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2"))
535                .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")),
536                GBC.eol());
537            hintPanel.add(new SearchKeywordRow(hcbSearchString)
538                .addTitle(tr("state"))
539                .addKeyword("modified", "modified ", tr("all modified objects"))
540                .addKeyword("new", "new ", tr("all new objects"))
541                .addKeyword("selected", "selected ", tr("all selected objects"))
542                .addKeyword("incomplete", "incomplete ", tr("all incomplete objects"))
543                .addKeyword("deleted", "deleted ", tr("all deleted objects (checkbox <b>{0}</b> must be enabled)", tr("all objects"))),
544                GBC.eol());
545            hintPanel.add(new SearchKeywordRow(hcbSearchString)
546                .addTitle(tr("related objects"))
547                .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building")
548                .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop")
549                .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>"))
550                .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>"))
551                .addKeyword("nth:<i>7</i>", "nth:",
552                        tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1")
553                .addKeyword("nth%:<i>7</i>", "nth%:",
554                        tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"),
555                GBC.eol());
556            hintPanel.add(new SearchKeywordRow(hcbSearchString)
557                .addTitle(tr("view"))
558                .addKeyword("inview", "inview ", tr("objects in current view"))
559                .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view"))
560                .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area"))
561                .addKeyword("allindownloadedarea", "allindownloadedarea ",
562                        tr("objects (and all its way nodes / relation members) in downloaded area")),
563                GBC.eol());
564        }
565
566        return hintPanel;
567    }
568
569    /**
570     * Launches the dialog for specifying search criteria and runs a search
571     */
572    public static void search() {
573        SearchSetting se = showSearchDialog(lastSearch);
574        if (se != null) {
575            searchWithHistory(se);
576        }
577    }
578
579    /**
580     * Adds the search specified by the settings in <code>s</code> to the
581     * search history and performs the search.
582     *
583     * @param s search settings
584     */
585    public static void searchWithHistory(SearchSetting s) {
586        saveToHistory(s);
587        lastSearch = new SearchSetting(s);
588        search(s);
589    }
590
591    /**
592     * Performs the search specified by the settings in <code>s</code> without saving it to search history.
593     *
594     * @param s search settings
595     */
596    public static void searchWithoutHistory(SearchSetting s) {
597        lastSearch = new SearchSetting(s);
598        search(s);
599    }
600
601    /**
602     * Performs the search specified by the search string {@code search} and the search mode {@code mode}.
603     *
604     * @param search the search string to use
605     * @param mode the search mode to use
606     */
607    public static void search(String search, SearchMode mode) {
608        final SearchSetting searchSetting = new SearchSetting();
609        searchSetting.text = search;
610        searchSetting.mode = mode;
611        search(searchSetting);
612    }
613
614    static void search(SearchSetting s) {
615        SearchTask.newSearchTask(s, new SelectSearchReceiver()).run();
616    }
617
618    /**
619     * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search.
620     *
621     * @param search the search string to use
622     * @param mode the search mode to use
623     * @return The result of the search.
624     * @since 10457
625     */
626    public static Collection<OsmPrimitive> searchAndReturn(String search, SearchMode mode) {
627        final SearchSetting searchSetting = new SearchSetting();
628        searchSetting.text = search;
629        searchSetting.mode = mode;
630        CapturingSearchReceiver receiver = new CapturingSearchReceiver();
631        SearchTask.newSearchTask(searchSetting, receiver).run();
632        return receiver.result;
633    }
634
635    /**
636     *
637     * @param selector Selector component that the user interacts with
638     * @param searchEditor Editor for search queries
639     */
640    private static void setPresetDblClickListener(TaggingPresetSelector selector, JTextComponent searchEditor) {
641        TaggingPreset selectedPreset = selector.getSelectedPresetAndUpdateClassification();
642
643        if (selectedPreset == null) {
644            return;
645        }
646
647        /*
648         * Make sure that the focus is transferred to the search text field
649         * from the selector component.
650         */
651        searchEditor.requestFocusInWindow();
652
653        /*
654         * In order to make interaction with the search dialog simpler,
655         * we make sure that if autocompletion triggers and the text field is
656         * not in focus, the correct area is selected. We first request focus
657         * and then execute the selection logic. invokeLater allows us to
658         * defer the selection until waiting for focus.
659         */
660        SwingUtilities.invokeLater(() -> {
661            int textOffset = searchEditor.getCaretPosition();
662            String presetSearchQuery = " preset:" +
663                    "\"" + selectedPreset.getRawName() + "\"";
664            try {
665                searchEditor.getDocument().insertString(textOffset, presetSearchQuery, null);
666            } catch (BadLocationException e1) {
667                throw new JosmRuntimeException(e1.getMessage(), e1);
668            }
669        });
670    }
671
672    /**
673     * Interfaces implementing this may receive the result of the current search.
674     * @author Michael Zangl
675     * @since 10457
676     * @since 10600 (functional interface)
677     */
678    @FunctionalInterface
679    interface SearchReceiver {
680        /**
681         * Receive the search result
682         * @param ds The data set searched on.
683         * @param result The result collection, including the initial collection.
684         * @param foundMatches The number of matches added to the result.
685         * @param setting The setting used.
686         */
687        void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting);
688    }
689
690    /**
691     * Select the search result and display a status text for it.
692     */
693    private static class SelectSearchReceiver implements SearchReceiver {
694
695        @Override
696        public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting) {
697            ds.setSelected(result);
698            if (foundMatches == 0) {
699                final String msg;
700                final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
701                if (setting.mode == SearchMode.replace) {
702                    msg = tr("No match found for ''{0}''", text);
703                } else if (setting.mode == SearchMode.add) {
704                    msg = tr("Nothing added to selection by searching for ''{0}''", text);
705                } else if (setting.mode == SearchMode.remove) {
706                    msg = tr("Nothing removed from selection by searching for ''{0}''", text);
707                } else if (setting.mode == SearchMode.in_selection) {
708                    msg = tr("Nothing found in selection by searching for ''{0}''", text);
709                } else {
710                    msg = null;
711                }
712                if (Main.map != null) {
713                    Main.map.statusLine.setHelpText(msg);
714                }
715                if (!GraphicsEnvironment.isHeadless()) {
716                    JOptionPane.showMessageDialog(
717                            Main.parent,
718                            msg,
719                            tr("Warning"),
720                            JOptionPane.WARNING_MESSAGE
721                    );
722                }
723            } else {
724                Main.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) {
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 = Main.getLayerManager().getEditDataSet();
764            return newSearchTask(setting, ds, resultReceiver);
765        }
766
767        /**
768         * Create a new search task for the given search setting.
769         * @param setting The setting to use
770         * @param ds The data set to search on
771         * @param resultReceiver will receive the search result
772         * @return A new search task.
773         */
774        private static SearchTask newSearchTask(SearchSetting setting, final DataSet ds, SearchReceiver resultReceiver) {
775            final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected());
776            return new SearchTask(ds, setting, selection, ds::isSelected, resultReceiver);
777        }
778
779        @Override
780        protected void cancel() {
781            this.canceled = true;
782        }
783
784        @Override
785        protected void realRun() {
786            try {
787                foundMatches = 0;
788                SearchCompiler.Match matcher = SearchCompiler.compile(setting);
789
790                if (setting.mode == SearchMode.replace) {
791                    selection.clear();
792                } else if (setting.mode == SearchMode.in_selection) {
793                    foundMatches = selection.size();
794                }
795
796                Collection<OsmPrimitive> all;
797                if (setting.allElements) {
798                    all = ds.allPrimitives();
799                } else {
800                    all = ds.getPrimitives(OsmPrimitive::isSelectable);
801                }
802                final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false);
803                subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size()));
804
805                for (OsmPrimitive osm : all) {
806                    if (canceled) {
807                        return;
808                    }
809                    if (setting.mode == SearchMode.replace) {
810                        if (matcher.match(osm)) {
811                            selection.add(osm);
812                            ++foundMatches;
813                        }
814                    } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) {
815                        selection.add(osm);
816                        ++foundMatches;
817                    } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) {
818                        selection.remove(osm);
819                        ++foundMatches;
820                    } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) {
821                        selection.remove(osm);
822                        --foundMatches;
823                    }
824                    subMonitor.worked(1);
825                }
826                subMonitor.finishTask();
827            } catch (ParseError e) {
828                Main.debug(e);
829                JOptionPane.showMessageDialog(
830                        Main.parent,
831                        e.getMessage(),
832                        tr("Error"),
833                        JOptionPane.ERROR_MESSAGE
834                );
835            }
836        }
837
838        @Override
839        protected void finish() {
840            if (canceled) {
841                return;
842            }
843            resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting);
844        }
845    }
846
847    /**
848     * This class defines a set of parameters that is used to
849     * perform search within the search dialog.
850     */
851    public static class SearchSetting {
852        public String text;
853        public SearchMode mode;
854        public boolean caseSensitive;
855        public boolean regexSearch;
856        public boolean mapCSSSearch;
857        public boolean allElements;
858
859        /**
860         * Constructs a new {@code SearchSetting}.
861         */
862        public SearchSetting() {
863            text = "";
864            mode = SearchMode.replace;
865        }
866
867        /**
868         * Constructs a new {@code SearchSetting} from an existing one.
869         * @param original original search settings
870         */
871        public SearchSetting(SearchSetting original) {
872            text = original.text;
873            mode = original.mode;
874            caseSensitive = original.caseSensitive;
875            regexSearch = original.regexSearch;
876            mapCSSSearch = original.mapCSSSearch;
877            allElements = original.allElements;
878        }
879
880        @Override
881        public String toString() {
882            String cs = caseSensitive ?
883                    /*case sensitive*/  trc("search", "CS") :
884                        /*case insensitive*/  trc("search", "CI");
885            String rx = regexSearch ? ", " +
886                            /*regex search*/ trc("search", "RX") : "";
887            String css = mapCSSSearch ? ", " +
888                            /*MapCSS search*/ trc("search", "CSS") : "";
889            String all = allElements ? ", " +
890                            /*all elements*/ trc("search", "A") : "";
891            return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')';
892        }
893
894        @Override
895        public boolean equals(Object other) {
896            if (this == other) return true;
897            if (other == null || getClass() != other.getClass()) return false;
898            SearchSetting that = (SearchSetting) other;
899            return caseSensitive == that.caseSensitive &&
900                    regexSearch == that.regexSearch &&
901                    mapCSSSearch == that.mapCSSSearch &&
902                    allElements == that.allElements &&
903                    mode == that.mode &&
904                    Objects.equals(text, that.text);
905        }
906
907        @Override
908        public int hashCode() {
909            return Objects.hash(text, mode, caseSensitive, regexSearch, mapCSSSearch, allElements);
910        }
911
912        /**
913         * <p>Transforms a string following a certain format, namely "[R | A | D | S][C?,R?,A?,M?] [a-zA-Z]"
914         * where the first part defines the mode of the search, see {@link SearchMode}, the second defines
915         * a set of attributes within the {@code SearchSetting} class and the second is the search query.
916         * <p>
917         * Attributes are as follows:
918         * <ul>
919         *     <li>C - if search is case sensitive
920         *     <li>R - if the regex syntax is used
921         *     <li>A - if all objects are considered
922         *     <li>M - if the mapCSS syntax is used
923         * </ul>
924         * <p>For example, "RC type:node" is a valid string representation of an object that replaces the
925         * current selection, is case sensitive and searches for all objects of type node.
926         * @param s A string representation of a {@code SearchSetting} object
927         *          from which the object must be built.
928         * @return A {@code SearchSetting} defined by the input string.
929         */
930        public static SearchSetting readFromString(String s) {
931            if (s.isEmpty())
932                return null;
933
934            SearchSetting result = new SearchSetting();
935
936            int index = 1;
937
938            result.mode = SearchMode.fromCode(s.charAt(0));
939            if (result.mode == null) {
940                result.mode = SearchMode.replace;
941                index = 0;
942            }
943
944            while (index < s.length()) {
945                if (s.charAt(index) == 'C') {
946                    result.caseSensitive = true;
947                } else if (s.charAt(index) == 'R') {
948                    result.regexSearch = true;
949                } else if (s.charAt(index) == 'A') {
950                    result.allElements = true;
951                } else if (s.charAt(index) == 'M') {
952                    result.mapCSSSearch = true;
953                } else if (s.charAt(index) == ' ') {
954                    break;
955                } else {
956                    Main.warn("Unknown char in SearchSettings: " + s);
957                    break;
958                }
959                index++;
960            }
961
962            if (index < s.length() && s.charAt(index) == ' ') {
963                index++;
964            }
965
966            result.text = s.substring(index);
967
968            return result;
969        }
970
971        /**
972         * Builds a string representation of the {@code SearchSetting} object,
973         * see {@link #readFromString(String)} for more details.
974         * @return A string representation of the {@code SearchSetting} object.
975         */
976        public String writeToString() {
977            if (text == null || text.isEmpty())
978                return "";
979
980            StringBuilder result = new StringBuilder();
981            result.append(mode.getCode());
982            if (caseSensitive) {
983                result.append('C');
984            }
985            if (regexSearch) {
986                result.append('R');
987            }
988            if (mapCSSSearch) {
989                result.append('M');
990            }
991            if (allElements) {
992                result.append('A');
993            }
994            result.append(' ')
995                  .append(text);
996            return result.toString();
997        }
998    }
999
1000    /**
1001     * Refreshes the enabled state
1002     *
1003     */
1004    @Override
1005    protected void updateEnabledState() {
1006        setEnabled(getLayerManager().getEditLayer() != null);
1007    }
1008
1009    @Override
1010    public List<ActionParameter<?>> getActionParameters() {
1011        return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
1012    }
1013}
Note: See TracBrowser for help on using the repository browser.