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