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

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

Apply #14840: Fix alignment in search dialog. Patch by bafonins

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