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

Last change on this file since 12346 was 12346, checked in by michael2402, 22 months ago

SearchAction: Use ExpertToggleAction to determine if expert mode is active.

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