001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.search;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trc;
007import static org.openstreetmap.josm.tools.I18n.trn;
008
009import java.awt.Cursor;
010import java.awt.Dimension;
011import java.awt.FlowLayout;
012import java.awt.GridBagLayout;
013import java.awt.event.ActionEvent;
014import java.awt.event.KeyEvent;
015import java.awt.event.MouseAdapter;
016import java.awt.event.MouseEvent;
017import java.util.ArrayList;
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashSet;
022import java.util.LinkedHashSet;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import javax.swing.ButtonGroup;
029import javax.swing.JCheckBox;
030import javax.swing.JLabel;
031import javax.swing.JOptionPane;
032import javax.swing.JPanel;
033import javax.swing.JRadioButton;
034import javax.swing.text.BadLocationException;
035import javax.swing.text.JTextComponent;
036
037import org.openstreetmap.josm.Main;
038import org.openstreetmap.josm.actions.ActionParameter;
039import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter;
040import org.openstreetmap.josm.actions.JosmAction;
041import org.openstreetmap.josm.actions.ParameterizedAction;
042import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
043import org.openstreetmap.josm.data.osm.DataSet;
044import org.openstreetmap.josm.data.osm.Filter;
045import org.openstreetmap.josm.data.osm.OsmPrimitive;
046import org.openstreetmap.josm.gui.ExtendedDialog;
047import org.openstreetmap.josm.gui.PleaseWaitRunnable;
048import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
049import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
050import org.openstreetmap.josm.gui.progress.ProgressMonitor;
051import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
052import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
053import org.openstreetmap.josm.tools.GBC;
054import org.openstreetmap.josm.tools.Predicate;
055import org.openstreetmap.josm.tools.Shortcut;
056import org.openstreetmap.josm.tools.Utils;
057
058
059public class SearchAction extends JosmAction implements ParameterizedAction {
060
061    public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
062    /** Maximum number of characters before the search expression is shortened for display purposes. */
063    public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100;
064
065    private static final String SEARCH_EXPRESSION = "searchExpression";
066
067    public enum SearchMode {
068        replace('R'), add('A'), remove('D'), in_selection('S');
069
070        private final char code;
071
072        SearchMode(char code) {
073            this.code = code;
074        }
075
076        public char getCode() {
077            return code;
078        }
079
080        public static SearchMode fromCode(char code) {
081            for (SearchMode mode: values()) {
082                if (mode.getCode() == code)
083                    return mode;
084            }
085            return null;
086        }
087    }
088
089    private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>();
090    static {
091        for (String s: Main.pref.getCollection("search.history", Collections.<String>emptyList())) {
092            SearchSetting ss = SearchSetting.readFromString(s);
093            if (ss != null) {
094                searchHistory.add(ss);
095            }
096        }
097    }
098
099    public static Collection<SearchSetting> getSearchHistory() {
100        return searchHistory;
101    }
102
103    public static void saveToHistory(SearchSetting s) {
104        if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
105            searchHistory.addFirst(new SearchSetting(s));
106        } else if (searchHistory.contains(s)) {
107            // move existing entry to front, fixes #8032 - search history loses entries when re-using queries
108            searchHistory.remove(s);
109            searchHistory.addFirst(new SearchSetting(s));
110        }
111        int maxsize = Main.pref.getInteger("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
112        while (searchHistory.size() > maxsize) {
113            searchHistory.removeLast();
114        }
115        Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size());
116        for (SearchSetting item: searchHistory) {
117            savedHistory.add(item.writeToString());
118        }
119        Main.pref.putCollection("search.history", savedHistory);
120    }
121
122    public static List<String> getSearchExpressionHistory() {
123        List<String> ret = new ArrayList<>(getSearchHistory().size());
124        for (SearchSetting ss: getSearchHistory()) {
125            ret.add(ss.text);
126        }
127        return ret;
128    }
129
130    private static volatile SearchSetting lastSearch;
131
132    /**
133     * Constructs a new {@code SearchAction}.
134     */
135    public SearchAction() {
136        super(tr("Search..."), "dialogs/search", tr("Search for objects."),
137                Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true);
138        putValue("help", ht("/Action/Search"));
139    }
140
141    @Override
142    public void actionPerformed(ActionEvent e) {
143        if (!isEnabled())
144            return;
145        search();
146    }
147
148    @Override
149    public void actionPerformed(ActionEvent e, Map<String, Object> parameters) {
150        if (parameters.get(SEARCH_EXPRESSION) == null) {
151            actionPerformed(e);
152        } else {
153            searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION));
154        }
155    }
156
157    private static class DescriptionTextBuilder {
158
159        private final StringBuilder s = new StringBuilder(4096);
160
161        public StringBuilder append(String string) {
162            return s.append(string);
163        }
164
165        StringBuilder appendItem(String item) {
166            return append("<li>").append(item).append("</li>\n");
167        }
168
169        StringBuilder appendItemHeader(String itemHeader) {
170            return append("<li class=\"header\">").append(itemHeader).append("</li>\n");
171        }
172
173        @Override
174        public String toString() {
175            return s.toString();
176        }
177    }
178
179    private static class SearchKeywordRow extends JPanel {
180
181        private final HistoryComboBox hcb;
182
183        SearchKeywordRow(HistoryComboBox hcb) {
184            super(new FlowLayout(FlowLayout.LEFT));
185            this.hcb = hcb;
186        }
187
188        public SearchKeywordRow addTitle(String title) {
189            add(new JLabel(tr("{0}: ", title)));
190            return this;
191        }
192
193        public SearchKeywordRow addKeyword(String displayText, final String insertText, String description, String... examples) {
194            JLabel label = new JLabel("<html>"
195                    + "<style>td{border:1px solid gray; font-weight:normal;}</style>"
196                    + "<table><tr><td>" + displayText + "</td></tr></table></html>");
197            add(label);
198            if (description != null || examples.length > 0) {
199                label.setToolTipText("<html>"
200                        + description
201                        + (examples.length > 0 ? Utils.joinAsHtmlUnorderedList(Arrays.asList(examples)) : "")
202                        + "</html>");
203            }
204            if (insertText != null) {
205                label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
206                label.addMouseListener(new MouseAdapter() {
207
208                    @Override
209                    public void mouseClicked(MouseEvent e) {
210                        try {
211                            JTextComponent tf = (JTextComponent) hcb.getEditor().getEditorComponent();
212                            tf.getDocument().insertString(tf.getCaretPosition(), ' ' + insertText, null);
213                        } catch (BadLocationException ex) {
214                            throw new RuntimeException(ex.getMessage(), ex);
215                        }
216                    }
217                });
218            }
219            return this;
220        }
221    }
222
223    public static SearchSetting showSearchDialog(SearchSetting initialValues) {
224        if (initialValues == null) {
225            initialValues = new SearchSetting();
226        }
227        // -- prepare the combo box with the search expressions
228        //
229        JLabel label = new JLabel(initialValues instanceof Filter ? tr("Filter string:") : tr("Search string:"));
230        final HistoryComboBox hcbSearchString = new HistoryComboBox();
231        final String tooltip = tr("Enter the search expression");
232        hcbSearchString.setText(initialValues.text);
233        hcbSearchString.setToolTipText(tooltip);
234        // we have to reverse the history, because ComboBoxHistory will reverse it again in addElement()
235        //
236        List<String> searchExpressionHistory = getSearchExpressionHistory();
237        Collections.reverse(searchExpressionHistory);
238        hcbSearchString.setPossibleItems(searchExpressionHistory);
239        hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height));
240        label.setLabelFor(hcbSearchString);
241
242        JRadioButton replace = new JRadioButton(tr("replace selection"), initialValues.mode == SearchMode.replace);
243        JRadioButton add = new JRadioButton(tr("add to selection"), initialValues.mode == SearchMode.add);
244        JRadioButton remove = new JRadioButton(tr("remove from selection"), initialValues.mode == SearchMode.remove);
245        JRadioButton in_selection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection);
246        ButtonGroup bg = new ButtonGroup();
247        bg.add(replace);
248        bg.add(add);
249        bg.add(remove);
250        bg.add(in_selection);
251
252        final JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive);
253        JCheckBox allElements = new JCheckBox(tr("all objects"), initialValues.allElements);
254        allElements.setToolTipText(tr("Also include incomplete and deleted objects in search."));
255        final JRadioButton standardSearch = new JRadioButton(tr("standard"), !initialValues.regexSearch && !initialValues.mapCSSSearch);
256        final JRadioButton regexSearch = new JRadioButton(tr("regular expression"), initialValues.regexSearch);
257        final JRadioButton mapCSSSearch = new JRadioButton(tr("MapCSS selector"), initialValues.mapCSSSearch);
258        final JCheckBox addOnToolbar = new JCheckBox(tr("add toolbar button"), false);
259        final ButtonGroup bg2 = new ButtonGroup();
260        bg2.add(standardSearch);
261        bg2.add(regexSearch);
262        bg2.add(mapCSSSearch);
263
264        JPanel top = new JPanel(new GridBagLayout());
265        top.add(label, GBC.std().insets(0, 0, 5, 0));
266        top.add(hcbSearchString, GBC.eol().fill(GBC.HORIZONTAL));
267        JPanel left = new JPanel(new GridBagLayout());
268        left.add(replace, GBC.eol());
269        left.add(add, GBC.eol());
270        left.add(remove, GBC.eol());
271        left.add(in_selection, GBC.eop());
272        left.add(caseSensitive, GBC.eol());
273        if (Main.pref.getBoolean("expert", false)) {
274            left.add(allElements, GBC.eol());
275            left.add(addOnToolbar, GBC.eop());
276            left.add(standardSearch, GBC.eol());
277            left.add(regexSearch, GBC.eol());
278            left.add(mapCSSSearch, GBC.eol());
279        }
280
281        final JPanel right;
282        right = new JPanel(new GridBagLayout());
283        buildHints(right, hcbSearchString);
284
285        final JTextComponent editorComponent = (JTextComponent) hcbSearchString.getEditor().getEditorComponent();
286        editorComponent.getDocument().addDocumentListener(new AbstractTextComponentValidator(editorComponent) {
287
288            @Override
289            public void validate() {
290                if (!isValid()) {
291                    feedbackInvalid(tr("Invalid search expression"));
292                } else {
293                    feedbackValid(tooltip);
294                }
295            }
296
297            @Override
298            public boolean isValid() {
299                try {
300                    SearchSetting ss = new SearchSetting();
301                    ss.text = hcbSearchString.getText();
302                    ss.caseSensitive = caseSensitive.isSelected();
303                    ss.regexSearch = regexSearch.isSelected();
304                    ss.mapCSSSearch = mapCSSSearch.isSelected();
305                    SearchCompiler.compile(ss);
306                    return true;
307                } catch (ParseError e) {
308                    return false;
309                }
310            }
311        });
312
313        final JPanel p = new JPanel(new GridBagLayout());
314        p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0));
315        p.add(left, GBC.std().anchor(GBC.NORTH).insets(5, 10, 10, 0));
316        p.add(right, GBC.eol());
317        ExtendedDialog dialog = new ExtendedDialog(
318                Main.parent,
319                initialValues instanceof Filter ? tr("Filter") : tr("Search"),
320                        new String[] {
321                    initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"),
322                            tr("Cancel")}
323        ) {
324            @Override
325            protected void buttonAction(int buttonIndex, ActionEvent evt) {
326                if (buttonIndex == 0) {
327                    try {
328                        SearchSetting ss = new SearchSetting();
329                        ss.text = hcbSearchString.getText();
330                        ss.caseSensitive = caseSensitive.isSelected();
331                        ss.regexSearch = regexSearch.isSelected();
332                        ss.mapCSSSearch = mapCSSSearch.isSelected();
333                        SearchCompiler.compile(ss);
334                        super.buttonAction(buttonIndex, evt);
335                    } catch (ParseError e) {
336                        JOptionPane.showMessageDialog(
337                                Main.parent,
338                                tr("Search expression is not valid: \n\n {0}", e.getMessage()),
339                                tr("Invalid search expression"),
340                                JOptionPane.ERROR_MESSAGE);
341                    }
342                } else {
343                    super.buttonAction(buttonIndex, evt);
344                }
345            }
346        };
347        dialog.setButtonIcons(new String[] {"dialogs/search", "cancel"});
348        dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */);
349        dialog.setContent(p);
350        dialog.showDialog();
351        int result = dialog.getValue();
352
353        if (result != 1) return null;
354
355        // User pressed OK - let's perform the search
356        SearchMode mode = replace.isSelected() ? SearchAction.SearchMode.replace
357                : (add.isSelected() ? SearchAction.SearchMode.add
358                        : (remove.isSelected() ? SearchAction.SearchMode.remove : SearchAction.SearchMode.in_selection));
359        initialValues.text = hcbSearchString.getText();
360        initialValues.mode = mode;
361        initialValues.caseSensitive = caseSensitive.isSelected();
362        initialValues.allElements = allElements.isSelected();
363        initialValues.regexSearch = regexSearch.isSelected();
364        initialValues.mapCSSSearch = mapCSSSearch.isSelected();
365
366        if (addOnToolbar.isSelected()) {
367            ToolbarPreferences.ActionDefinition aDef =
368                    new ToolbarPreferences.ActionDefinition(Main.main.menu.search);
369            aDef.getParameters().put(SEARCH_EXPRESSION, initialValues);
370            // Display search expression as tooltip instead of generic one
371            aDef.setName(Utils.shortenString(initialValues.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY));
372            // parametrized action definition is now composed
373            ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
374            String res = actionParser.saveAction(aDef);
375
376            // add custom search button to toolbar preferences
377            Main.toolbar.addCustomButton(res, -1, false);
378        }
379        return initialValues;
380    }
381
382    private static void buildHints(JPanel right, HistoryComboBox hcbSearchString) {
383        right.add(new SearchKeywordRow(hcbSearchString)
384                .addTitle(tr("basic examples"))
385                .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key"))
386                .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key")),
387                GBC.eol());
388        right.add(new SearchKeywordRow(hcbSearchString)
389                .addTitle(tr("basics"))
390                .addKeyword("<i>key</i>:<i>valuefragment</i>", null,
391                        tr("''valuefragment'' anywhere in ''key''"), "name:str matches name=Bakerstreet")
392                .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''"))
393                .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''"))
394                .addKeyword("<i>key</i>=*", null, tr("''key'' with any value"))
395                .addKeyword("*=<i>value</i>", null, tr("''value'' in any key"))
396                .addKeyword("<i>key</i>=", null, tr("matches if ''key'' exists"))
397                .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)"))
398                .addKeyword("\"key\"=\"value\"", "\"\"=\"\"",
399                        tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " +
400                           "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."),
401                        "\"addr:street\""),
402                GBC.eol());
403        right.add(new SearchKeywordRow(hcbSearchString)
404                .addTitle(tr("combinators"))
405                .addKeyword("<i>expr</i> <i>expr</i>", null, tr("logical and (both expressions have to be satisfied)"))
406                .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)"))
407                .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)"))
408                .addKeyword("-<i>expr</i>", null, tr("logical not"))
409                .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")),
410                GBC.eol());
411
412        if (Main.pref.getBoolean("expert", false)) {
413            right.add(new SearchKeywordRow(hcbSearchString)
414                .addTitle(tr("objects"))
415                .addKeyword("type:node", "type:node ", tr("all ways"))
416                .addKeyword("type:way", "type:way ", tr("all ways"))
417                .addKeyword("type:relation", "type:relation ", tr("all relations"))
418                .addKeyword("closed", "closed ", tr("all closed ways"))
419                .addKeyword("untagged", "untagged ", tr("object without useful tags")),
420                GBC.eol());
421            right.add(new SearchKeywordRow(hcbSearchString)
422                .addTitle(tr("metadata"))
423                .addKeyword("user:", "user:", tr("objects changed by user", "user:anonymous"))
424                .addKeyword("id:", "id:", tr("objects with given ID"), "id:0 (new objects)")
425                .addKeyword("version:", "version:", tr("objects with given version"), "version:0 (objects without an assigned version)")
426                .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"),
427                        "changeset:0 (objects without an assigned changeset)")
428                .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/",
429                        "timestamp:2008/2011-02-04T12"),
430                GBC.eol());
431            right.add(new SearchKeywordRow(hcbSearchString)
432                .addTitle(tr("properties"))
433                .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes"))
434                .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways"))
435                .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags"))
436                .addKeyword("role:", "role:", tr("objects with given role in a relation"))
437                .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2"))
438                .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")),
439                GBC.eol());
440            right.add(new SearchKeywordRow(hcbSearchString)
441                .addTitle(tr("state"))
442                .addKeyword("modified", "modified ", tr("all modified objects"))
443                .addKeyword("new", "new ", tr("all new objects"))
444                .addKeyword("selected", "selected ", tr("all selected objects"))
445                .addKeyword("incomplete", "incomplete ", tr("all incomplete objects")),
446                GBC.eol());
447            right.add(new SearchKeywordRow(hcbSearchString)
448                .addTitle(tr("related objects"))
449                .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building")
450                .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop")
451                .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>"))
452                .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>"))
453                .addKeyword("nth:<i>7</i>", "nth:",
454                        tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1")
455                .addKeyword("nth%:<i>7</i>", "nth%:",
456                        tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"),
457                GBC.eol());
458            right.add(new SearchKeywordRow(hcbSearchString)
459                .addTitle(tr("view"))
460                .addKeyword("inview", "inview ", tr("objects in current view"))
461                .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view"))
462                .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area"))
463                .addKeyword("allindownloadedarea", "allindownloadedarea ",
464                        tr("objects (and all its way nodes / relation members) in downloaded area")),
465                GBC.eol());
466        }
467    }
468
469    /**
470     * Launches the dialog for specifying search criteria and runs a search
471     */
472    public static void search() {
473        SearchSetting se = showSearchDialog(lastSearch);
474        if (se != null) {
475            searchWithHistory(se);
476        }
477    }
478
479    /**
480     * Adds the search specified by the settings in <code>s</code> to the
481     * search history and performs the search.
482     *
483     * @param s search settings
484     */
485    public static void searchWithHistory(SearchSetting s) {
486        saveToHistory(s);
487        lastSearch = new SearchSetting(s);
488        search(s);
489    }
490
491    /**
492     * Performs the search specified by the settings in <code>s</code> without saving it to search history.
493     *
494     * @param s search settings
495     */
496    public static void searchWithoutHistory(SearchSetting s) {
497        lastSearch = new SearchSetting(s);
498        search(s);
499    }
500
501    /**
502     * Performs the search specified by the search string {@code search} and the search mode {@code mode}.
503     *
504     * @param search the search string to use
505     * @param mode the search mode to use
506     */
507    public static void search(String search, SearchMode mode) {
508        final SearchSetting searchSetting = new SearchSetting();
509        searchSetting.text = search;
510        searchSetting.mode = mode;
511        search(searchSetting);
512    }
513
514    static void search(SearchSetting s) {
515        SearchTask.newSearchTask(s).run();
516    }
517
518    static final class SearchTask extends PleaseWaitRunnable {
519        private final DataSet ds;
520        private final SearchSetting setting;
521        private final Collection<OsmPrimitive> selection;
522        private final Predicate<OsmPrimitive> predicate;
523        private boolean canceled;
524        private int foundMatches;
525
526        private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate) {
527            super(tr("Searching"));
528            this.ds = ds;
529            this.setting = setting;
530            this.selection = selection;
531            this.predicate = predicate;
532        }
533
534        static SearchTask newSearchTask(SearchSetting setting) {
535            final DataSet ds = Main.main.getCurrentDataSet();
536            final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected());
537            return new SearchTask(ds, setting, selection, new Predicate<OsmPrimitive>() {
538                @Override
539                public boolean evaluate(OsmPrimitive o) {
540                    return ds.isSelected(o);
541                }
542            });
543        }
544
545        @Override
546        protected void cancel() {
547            this.canceled = true;
548        }
549
550        @Override
551        protected void realRun() {
552            try {
553                foundMatches = 0;
554                SearchCompiler.Match matcher = SearchCompiler.compile(setting);
555
556                if (setting.mode == SearchMode.replace) {
557                    selection.clear();
558                } else if (setting.mode == SearchMode.in_selection) {
559                    foundMatches = selection.size();
560                }
561
562                Collection<OsmPrimitive> all;
563                if (setting.allElements) {
564                    all = Main.main.getCurrentDataSet().allPrimitives();
565                } else {
566                    all = Main.main.getCurrentDataSet().allNonDeletedCompletePrimitives();
567                }
568                final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false);
569                subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size()));
570
571                for (OsmPrimitive osm : all) {
572                    if (canceled) {
573                        return;
574                    }
575                    if (setting.mode == SearchMode.replace) {
576                        if (matcher.match(osm)) {
577                            selection.add(osm);
578                            ++foundMatches;
579                        }
580                    } else if (setting.mode == SearchMode.add && !predicate.evaluate(osm) && matcher.match(osm)) {
581                        selection.add(osm);
582                        ++foundMatches;
583                    } else if (setting.mode == SearchMode.remove && predicate.evaluate(osm) && matcher.match(osm)) {
584                        selection.remove(osm);
585                        ++foundMatches;
586                    } else if (setting.mode == SearchMode.in_selection && predicate.evaluate(osm) && !matcher.match(osm)) {
587                        selection.remove(osm);
588                        --foundMatches;
589                    }
590                    subMonitor.worked(1);
591                }
592                subMonitor.finishTask();
593            } catch (SearchCompiler.ParseError e) {
594                JOptionPane.showMessageDialog(
595                        Main.parent,
596                        e.getMessage(),
597                        tr("Error"),
598                        JOptionPane.ERROR_MESSAGE
599
600                );
601            }
602        }
603
604        @Override
605        protected void finish() {
606            if (canceled) {
607                return;
608            }
609            ds.setSelected(selection);
610            if (foundMatches == 0) {
611                final String msg;
612                final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
613                if (setting.mode == SearchMode.replace) {
614                    msg = tr("No match found for ''{0}''", text);
615                } else if (setting.mode == SearchMode.add) {
616                    msg = tr("Nothing added to selection by searching for ''{0}''", text);
617                } else if (setting.mode == SearchMode.remove) {
618                    msg = tr("Nothing removed from selection by searching for ''{0}''", text);
619                } else if (setting.mode == SearchMode.in_selection) {
620                    msg = tr("Nothing found in selection by searching for ''{0}''", text);
621                } else {
622                    msg = null;
623                }
624                Main.map.statusLine.setHelpText(msg);
625                JOptionPane.showMessageDialog(
626                        Main.parent,
627                        msg,
628                        tr("Warning"),
629                        JOptionPane.WARNING_MESSAGE
630                );
631            } else {
632                Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
633            }
634        }
635    }
636
637    public static class SearchSetting {
638        public String text = "";
639        public SearchMode mode = SearchMode.replace;
640        public boolean caseSensitive;
641        public boolean regexSearch;
642        public boolean mapCSSSearch;
643        public boolean allElements;
644
645        /**
646         * Constructs a new {@code SearchSetting}.
647         */
648        public SearchSetting() {
649        }
650
651        /**
652         * Constructs a new {@code SearchSetting} from an existing one.
653         * @param original original search settings
654         */
655        public SearchSetting(SearchSetting original) {
656            text = original.text;
657            mode = original.mode;
658            caseSensitive = original.caseSensitive;
659            regexSearch = original.regexSearch;
660            mapCSSSearch = original.mapCSSSearch;
661            allElements = original.allElements;
662        }
663
664        @Override
665        public String toString() {
666            String cs = caseSensitive ?
667                    /*case sensitive*/  trc("search", "CS") :
668                        /*case insensitive*/  trc("search", "CI");
669            String rx = regexSearch ? ", " +
670                            /*regex search*/ trc("search", "RX") : "";
671            String css = mapCSSSearch ? ", " +
672                            /*MapCSS search*/ trc("search", "CSS") : "";
673            String all = allElements ? ", " +
674                            /*all elements*/ trc("search", "A") : "";
675            return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')';
676        }
677
678        @Override
679        public boolean equals(Object other) {
680            if (!(other instanceof SearchSetting))
681                return false;
682            SearchSetting o = (SearchSetting) other;
683            return o.caseSensitive == this.caseSensitive
684                    && o.regexSearch == this.regexSearch
685                    && o.mapCSSSearch == this.mapCSSSearch
686                    && o.allElements == this.allElements
687                    && o.mode.equals(this.mode)
688                    && o.text.equals(this.text);
689        }
690
691        @Override
692        public int hashCode() {
693            return text.hashCode();
694        }
695
696        public static SearchSetting readFromString(String s) {
697            if (s.isEmpty())
698                return null;
699
700            SearchSetting result = new SearchSetting();
701
702            int index = 1;
703
704            result.mode = SearchMode.fromCode(s.charAt(0));
705            if (result.mode == null) {
706                result.mode = SearchMode.replace;
707                index = 0;
708            }
709
710            while (index < s.length()) {
711                if (s.charAt(index) == 'C') {
712                    result.caseSensitive = true;
713                } else if (s.charAt(index) == 'R') {
714                    result.regexSearch = true;
715                } else if (s.charAt(index) == 'A') {
716                    result.allElements = true;
717                } else if (s.charAt(index) == 'M') {
718                    result.mapCSSSearch = true;
719                } else if (s.charAt(index) == ' ') {
720                    break;
721                } else {
722                    Main.warn("Unknown char in SearchSettings: " + s);
723                    break;
724                }
725                index++;
726            }
727
728            if (index < s.length() && s.charAt(index) == ' ') {
729                index++;
730            }
731
732            result.text = s.substring(index);
733
734            return result;
735        }
736
737        public String writeToString() {
738            if (text == null || text.isEmpty())
739                return "";
740
741            StringBuilder result = new StringBuilder();
742            result.append(mode.getCode());
743            if (caseSensitive) {
744                result.append('C');
745            }
746            if (regexSearch) {
747                result.append('R');
748            }
749            if (mapCSSSearch) {
750                result.append('M');
751            }
752            if (allElements) {
753                result.append('A');
754            }
755            result.append(' ')
756                  .append(text);
757            return result.toString();
758        }
759    }
760
761    /**
762     * Refreshes the enabled state
763     *
764     */
765    @Override
766    protected void updateEnabledState() {
767        setEnabled(getEditLayer() != null);
768    }
769
770    @Override
771    public List<ActionParameter<?>> getActionParameters() {
772        return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
773    }
774
775    public static String escapeStringForSearch(String s) {
776        return s.replace("\\", "\\\\").replace("\"", "\\\"");
777    }
778}