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

Last change on this file since 12839 was 12839, checked in by bastiK, 5 weeks ago

fixed #15198 - JOSM freezes after showing popup for unsuccesfull search

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