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
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[626]2package org.openstreetmap.josm.actions.search;
3
[2467]4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
[626]5import static org.openstreetmap.josm.tools.I18n.tr;
[2842]6import static org.openstreetmap.josm.tools.I18n.trc;
[8883]7import static org.openstreetmap.josm.tools.I18n.trn;
[626]8
[5063]9import java.awt.Cursor;
[4085]10import java.awt.Dimension;
[4832]11import java.awt.FlowLayout;
[11986]12import java.awt.GraphicsEnvironment;
[626]13import java.awt.GridBagLayout;
14import java.awt.event.ActionEvent;
15import java.awt.event.KeyEvent;
[4832]16import java.awt.event.MouseAdapter;
17import java.awt.event.MouseEvent;
[2451]18import java.util.ArrayList;
[4832]19import java.util.Arrays;
[626]20import java.util.Collection;
[2451]21import java.util.Collections;
[2790]22import java.util.HashSet;
[6744]23import java.util.LinkedHashSet;
[967]24import java.util.LinkedList;
[2451]25import java.util.List;
[3175]26import java.util.Map;
[9371]27import java.util.Objects;
[8338]28import java.util.Set;
[10658]29import java.util.function.Predicate;
[626]30
[12334]31import javax.swing.BorderFactory;
32import javax.swing.ButtonGroup;
[626]33import javax.swing.JCheckBox;
34import javax.swing.JLabel;
35import javax.swing.JOptionPane;
36import javax.swing.JPanel;
37import javax.swing.JRadioButton;
[4832]38import javax.swing.text.BadLocationException;
[5948]39import javax.swing.text.JTextComponent;
[626]40
41import org.openstreetmap.josm.Main;
[3175]42import org.openstreetmap.josm.actions.ActionParameter;
[5948]43import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter;
[12346]44import org.openstreetmap.josm.actions.ExpertToggleAction;
[626]45import org.openstreetmap.josm.actions.JosmAction;
[3175]46import org.openstreetmap.josm.actions.ParameterizedAction;
[3355]47import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
[2451]48import org.openstreetmap.josm.data.osm.DataSet;
49import org.openstreetmap.josm.data.osm.Filter;
[626]50import org.openstreetmap.josm.data.osm.OsmPrimitive;
[1397]51import org.openstreetmap.josm.gui.ExtendedDialog;
[8883]52import org.openstreetmap.josm.gui.PleaseWaitRunnable;
[9696]53import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
[4587]54import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
[4948]55import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
[8883]56import org.openstreetmap.josm.gui.progress.ProgressMonitor;
[8971]57import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
[2451]58import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
[626]59import org.openstreetmap.josm.tools.GBC;
[11374]60import org.openstreetmap.josm.tools.JosmRuntimeException;
[1084]61import org.openstreetmap.josm.tools.Shortcut;
[4832]62import org.openstreetmap.josm.tools.Utils;
[626]63
[12334]64/**
65 * The search action allows the user to search the data layer using a complex search string.
66 *
67 * @see SearchCompiler
68 */
[3175]69public class SearchAction extends JosmAction implements ParameterizedAction {
[626]70
[12334]71    /**
72     * The default size of the search history
73     */
[4018]74    public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
[12334]75    /**
76     * Maximum number of characters before the search expression is shortened for display purposes.
77     */
[6742]78    public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100;
[626]79
[3175]80    private static final String SEARCH_EXPRESSION = "searchExpression";
81
[11978]82    /**
83     * Search mode.
84     */
[8836]85    public enum SearchMode {
[9980]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');
[3175]94
95        private final char code;
96
97        SearchMode(char code) {
98            this.code = code;
99        }
100
[9980]101        /**
102         * Returns the unique character code of this mode.
103         * @return the unique character code of this mode
104         */
[3175]105        public char getCode() {
106            return code;
107        }
108
[9980]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         */
[3175]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        }
[967]121    }
122
[7005]123    private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>();
[5889]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);
[3176]129            }
130        }
[5889]131    }
[6069]132
[12334]133    /**
134     * Gets the search history
135     * @return The last searched terms. Do not modify it.
136     */
[5889]137    public static Collection<SearchSetting> getSearchHistory() {
[3176]138        return searchHistory;
139    }
140
[12334]141    /**
142     * Saves a search to the search history.
143     * @param s The search to save
144     */
[3176]145    public static void saveToHistory(SearchSetting s) {
[8510]146        if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
[3176]147            searchHistory.addFirst(new SearchSetting(s));
[6744]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));
[3176]152        }
153        int maxsize = Main.pref.getInteger("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
154        while (searchHistory.size() > maxsize) {
155            searchHistory.removeLast();
156        }
[8338]157        Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size());
[3176]158        for (SearchSetting item: searchHistory) {
159            savedHistory.add(item.writeToString());
160        }
161        Main.pref.putCollection("search.history", savedHistory);
162    }
163
[12334]164    /**
165     * Gets a list of all texts that were recently used in the search
166     * @return The list of search texts.
167     */
[3176]168    public static List<String> getSearchExpressionHistory() {
[7005]169        List<String> ret = new ArrayList<>(getSearchHistory().size());
[3176]170        for (SearchSetting ss: getSearchHistory()) {
171            ret.add(ss.text);
172        }
173        return ret;
174    }
175
[8840]176    private static volatile SearchSetting lastSearch;
[967]177
[6316]178    /**
179     * Constructs a new {@code SearchAction}.
180     */
[626]181    public SearchAction() {
[1212]182        super(tr("Search..."), "dialogs/search", tr("Search for objects."),
[4982]183                Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true);
[2467]184        putValue("help", ht("/Action/Search"));
[626]185    }
186
[6084]187    @Override
[626]188    public void actionPerformed(ActionEvent e) {
[1808]189        if (!isEnabled())
190            return;
[3102]191        search();
[626]192    }
193
[6084]194    @Override
[3175]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
[4832]203    private static class SearchKeywordRow extends JPanel {
204
205        private final HistoryComboBox hcb;
206
[8836]207        SearchKeywordRow(HistoryComboBox hcb) {
[4832]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) {
[5063]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>");
[4832]221            add(label);
222            if (description != null || examples.length > 0) {
223                label.setToolTipText("<html>"
224                        + description
[6524]225                        + (examples.length > 0 ? Utils.joinAsHtmlUnorderedList(Arrays.asList(examples)) : "")
[4832]226                        + "</html>");
227            }
228            if (insertText != null) {
[5063]229                label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
[4832]230                label.addMouseListener(new MouseAdapter() {
231
232                    @Override
233                    public void mouseClicked(MouseEvent e) {
234                        try {
[9484]235                            JTextComponent tf = hcb.getEditorComponent();
[8846]236                            tf.getDocument().insertString(tf.getCaretPosition(), ' ' + insertText, null);
[4832]237                        } catch (BadLocationException ex) {
[11374]238                            throw new JosmRuntimeException(ex.getMessage(), ex);
[4832]239                        }
240                    }
241                });
242            }
243            return this;
244        }
245    }
246
[2123]247    public static SearchSetting showSearchDialog(SearchSetting initialValues) {
[3102]248        if (initialValues == null) {
249            initialValues = new SearchSetting();
250        }
[2451]251        // -- prepare the combo box with the search expressions
252        //
[8426]253        JLabel label = new JLabel(initialValues instanceof Filter ? tr("Filter string:") : tr("Search string:"));
[2451]254        final HistoryComboBox hcbSearchString = new HistoryComboBox();
[8971]255        final String tooltip = tr("Enter the search expression");
[2451]256        hcbSearchString.setText(initialValues.text);
[8971]257        hcbSearchString.setToolTipText(tooltip);
[8426]258        // we have to reverse the history, because ComboBoxHistory will reverse it again in addElement()
[2451]259        //
260        List<String> searchExpressionHistory = getSearchExpressionHistory();
261        Collections.reverse(searchExpressionHistory);
262        hcbSearchString.setPossibleItems(searchExpressionHistory);
[3684]263        hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height));
[8426]264        label.setLabelFor(hcbSearchString);
[2451]265
[967]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);
[10001]269        JRadioButton inSelection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection);
[967]270        ButtonGroup bg = new ButtonGroup();
271        bg.add(replace);
272        bg.add(add);
273        bg.add(remove);
[10001]274        bg.add(inSelection);
[967]275
[3355]276        final JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive);
[3317]277        JCheckBox allElements = new JCheckBox(tr("all objects"), initialValues.allElements);
278        allElements.setToolTipText(tr("Also include incomplete and deleted objects in search."));
[12333]279        JCheckBox addOnToolbar = new JCheckBox(tr("add toolbar button"), false);
280
[8821]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);
[8812]284        final ButtonGroup bg2 = new ButtonGroup();
[8821]285        bg2.add(standardSearch);
[8812]286        bg2.add(regexSearch);
287        bg2.add(mapCSSSearch);
[967]288
[1291]289        JPanel left = new JPanel(new GridBagLayout());
[12333]290
291        JPanel selectionSettings = new JPanel(new GridBagLayout());
292        selectionSettings.setBorder(BorderFactory.createTitledBorder(tr("Selection settings")));
[12345]293        selectionSettings.add(replace, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
[12333]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")));
[12345]300        additionalSettings.add(caseSensitive, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
[12333]301
[12346]302        left.add(selectionSettings, GBC.eol().fill(GBC.BOTH));
303        left.add(additionalSettings, GBC.eol().fill(GBC.BOTH));
304
305        if (ExpertToggleAction.isExpert()) {
[12333]306            additionalSettings.add(allElements, GBC.eol());
307            additionalSettings.add(addOnToolbar, GBC.eop());
308
309            JPanel searchOptions = new JPanel(new GridBagLayout());
[12344]310            searchOptions.setBorder(BorderFactory.createTitledBorder(tr("Search syntax")));
[12345]311            searchOptions.add(standardSearch, GBC.eol().anchor(GBC.WEST).fill(GBC.HORIZONTAL));
[12333]312            searchOptions.add(regexSearch, GBC.eol());
313            searchOptions.add(mapCSSSearch, GBC.eol());
314
315            left.add(searchOptions, GBC.eol().fill(GBC.BOTH));
[4543]316        }
[1291]317
[12333]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));
[1291]322
[9484]323        final JTextComponent editorComponent = hcbSearchString.getEditorComponent();
[8971]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;
[9696]345                } catch (ParseError | MapCSSException e) {
[8971]346                    return false;
347                }
348            }
349        });
350
[4018]351        final JPanel p = new JPanel(new GridBagLayout());
352        p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0));
[12333]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
[2070]356        ExtendedDialog dialog = new ExtendedDialog(
357                Main.parent,
[2123]358                initialValues instanceof Filter ? tr("Filter") : tr("Search"),
[12279]359                initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"),
360                tr("Cancel")
[3355]361        ) {
362            @Override
363            protected void buttonAction(int buttonIndex, ActionEvent evt) {
364                if (buttonIndex == 0) {
365                    try {
[8811]366                        SearchSetting ss = new SearchSetting();
367                        ss.text = hcbSearchString.getText();
368                        ss.caseSensitive = caseSensitive.isSelected();
369                        ss.regexSearch = regexSearch.isSelected();
[8812]370                        ss.mapCSSSearch = mapCSSSearch.isSelected();
[8811]371                        SearchCompiler.compile(ss);
[3355]372                        super.buttonAction(buttonIndex, evt);
373                    } catch (ParseError e) {
[10463]374                        Main.debug(e);
[3355]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        };
[12279]386        dialog.setButtonIcons("dialogs/search", "cancel");
[2467]387        dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */);
[2070]388        dialog.setContent(p);
[1677]389
[12279]390        if (dialog.showDialog().getValue() != 1) return null;
[1291]391
[967]392        // User pressed OK - let's perform the search
393        SearchMode mode = replace.isSelected() ? SearchAction.SearchMode.replace
[2123]394                : (add.isSelected() ? SearchAction.SearchMode.add
[2212]395                        : (remove.isSelected() ? SearchAction.SearchMode.remove : SearchAction.SearchMode.in_selection));
[2451]396        initialValues.text = hcbSearchString.getText();
[2145]397        initialValues.mode = mode;
398        initialValues.caseSensitive = caseSensitive.isSelected();
[3317]399        initialValues.allElements = allElements.isSelected();
[2145]400        initialValues.regexSearch = regexSearch.isSelected();
[8812]401        initialValues.mapCSSSearch = mapCSSSearch.isSelected();
[4948]402
[4587]403        if (addOnToolbar.isSelected()) {
[4948]404            ToolbarPreferences.ActionDefinition aDef =
[4587]405                    new ToolbarPreferences.ActionDefinition(Main.main.menu.search);
[6586]406            aDef.getParameters().put(SEARCH_EXPRESSION, initialValues);
[8509]407            // Display search expression as tooltip instead of generic one
408            aDef.setName(Utils.shortenString(initialValues.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY));
[4587]409            // parametrized action definition is now composed
410            ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
411            String res = actionParser.saveAction(aDef);
[4948]412
[4613]413            // add custom search button to toolbar preferences
[6844]414            Main.toolbar.addCustomButton(res, -1, false);
[4587]415        }
[2145]416        return initialValues;
[626]417    }
[967]418
[12333]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"))
[5063]425                .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key"))
[12333]426                .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key"))
[8510]427                .addKeyword("<i>key</i>:<i>valuefragment</i>", null,
428                        tr("''valuefragment'' anywhere in ''key''"), "name:str matches name=Bakerstreet")
[12333]429                .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''")),
430                GBC.eol());
431        hintPanel.add(new SearchKeywordRow(hcbSearchString)
[4832]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"))
[6429]436                .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)"))
[8509]437                .addKeyword("\"key\"=\"value\"", "\"\"=\"\"",
[8510]438                        tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " +
[12333]439                                "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."),
[8836]440                        "\"addr:street\""),
[12333]441                GBC.eol().anchor(GBC.CENTER));
442        hintPanel.add(new SearchKeywordRow(hcbSearchString)
[4832]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"))
[8836]448                .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")),
449                GBC.eol());
[4832]450
[12346]451        if (ExpertToggleAction.isExpert()) {
[12333]452            hintPanel.add(new SearchKeywordRow(hcbSearchString)
[4832]453                .addTitle(tr("objects"))
[11464]454                .addKeyword("type:node", "type:node ", tr("all nodes"))
[4832]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"))
[8836]458                .addKeyword("untagged", "untagged ", tr("object without useful tags")),
459                GBC.eol());
[12333]460            hintPanel.add(new SearchKeywordRow(hcbSearchString)
[4832]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)")
[8509]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/",
[8836]468                        "timestamp:2008/2011-02-04T12"),
469                GBC.eol());
[12333]470            hintPanel.add(new SearchKeywordRow(hcbSearchString)
[4832]471                .addTitle(tr("properties"))
[8220]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"))
[4832]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"))
[8836]477                .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")),
478                GBC.eol());
[12333]479            hintPanel.add(new SearchKeywordRow(hcbSearchString)
[4832]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"))
[11446]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"))),
[8836]486                GBC.eol());
[12333]487            hintPanel.add(new SearchKeywordRow(hcbSearchString)
[5578]488                .addTitle(tr("related objects"))
[4832]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")
[8884]491                .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>"))
[8896]492                .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>"))
[8884]493                .addKeyword("nth:<i>7</i>", "nth:",
[8540]494                        tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1")
[8884]495                .addKeyword("nth%:<i>7</i>", "nth%:",
[8836]496                        tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"),
497                GBC.eol());
[12333]498            hintPanel.add(new SearchKeywordRow(hcbSearchString)
[4832]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"))
[8509]503                .addKeyword("allindownloadedarea", "allindownloadedarea ",
[8836]504                        tr("objects (and all its way nodes / relation members) in downloaded area")),
505                GBC.eol());
[4832]506        }
[12333]507
508        return hintPanel;
[4832]509    }
510
[967]511    /**
[8470]512     * Launches the dialog for specifying search criteria and runs a search
[3102]513     */
514    public static void search() {
515        SearchSetting se = showSearchDialog(lastSearch);
[8510]516        if (se != null) {
[3102]517            searchWithHistory(se);
518        }
519    }
520
521    /**
[1023]522     * Adds the search specified by the settings in <code>s</code> to the
[967]523     * search history and performs the search.
[1023]524     *
[8470]525     * @param s search settings
[967]526     */
527    public static void searchWithHistory(SearchSetting s) {
[3176]528        saveToHistory(s);
[2262]529        lastSearch = new SearchSetting(s);
[2123]530        search(s);
[967]531    }
532
[8470]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     */
[967]538    public static void searchWithoutHistory(SearchSetting s) {
[2262]539        lastSearch = new SearchSetting(s);
[2123]540        search(s);
[967]541    }
542
[8883]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    }
[2790]555
[8883]556    static void search(SearchSetting s) {
[10457]557        SearchTask.newSearchTask(s, new SelectSearchReceiver()).run();
[8883]558    }
[2790]559
[10457]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
[10600]581     * @since 10600 (functional interface)
[10457]582     */
[10600]583    @FunctionalInterface
[10457]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                }
[11986]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                }
[12333]628            } else {
[10457]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
[8902]648    static final class SearchTask extends PleaseWaitRunnable {
[8883]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;
[10598]655        private final SearchReceiver resultReceiver;
[8131]656
[10457]657        private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate,
658                SearchReceiver resultReceiver) {
[8883]659            super(tr("Searching"));
660            this.ds = ds;
661            this.setting = setting;
662            this.selection = selection;
663            this.predicate = predicate;
[10457]664            this.resultReceiver = resultReceiver;
[8883]665        }
666
[10457]667        static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) {
[10446]668            final DataSet ds = Main.getLayerManager().getEditDataSet();
[10457]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
[10459]676         * @param resultReceiver will receive the search result
[10457]677         * @return A new search task.
678         */
679        private static SearchTask newSearchTask(SearchSetting setting, final DataSet ds, SearchReceiver resultReceiver) {
[8883]680            final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected());
[10601]681            return new SearchTask(ds, setting, selection, ds::isSelected, resultReceiver);
[8883]682        }
[1847]683
[8883]684        @Override
685        protected void cancel() {
686            this.canceled = true;
[967]687        }
[2212]688
[8883]689        @Override
690        protected void realRun() {
691            try {
692                foundMatches = 0;
693                SearchCompiler.Match matcher = SearchCompiler.compile(setting);
[3300]694
[8883]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) {
[10711]703                    all = ds.allPrimitives();
[8883]704                } else {
[10711]705                    all = ds.getPrimitives(OsmPrimitive::isSelectable);
[8883]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;
[3300]713                    }
[8883]714                    if (setting.mode == SearchMode.replace) {
715                        if (matcher.match(osm)) {
716                            selection.add(osm);
717                            ++foundMatches;
718                        }
[10658]719                    } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) {
[8883]720                        selection.add(osm);
721                        ++foundMatches;
[10658]722                    } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) {
[8883]723                        selection.remove(osm);
724                        ++foundMatches;
[10658]725                    } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) {
[8883]726                        selection.remove(osm);
727                        --foundMatches;
728                    }
729                    subMonitor.worked(1);
[3300]730                }
[8883]731                subMonitor.finishTask();
[10463]732            } catch (ParseError e) {
733                Main.debug(e);
[8883]734                JOptionPane.showMessageDialog(
735                        Main.parent,
736                        e.getMessage(),
737                        tr("Error"),
738                        JOptionPane.ERROR_MESSAGE
739                );
[3300]740            }
741        }
742
[8883]743        @Override
744        protected void finish() {
745            if (canceled) {
746                return;
[2212]747            }
[10457]748            resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting);
[2212]749        }
[2123]750    }
751
[967]752    public static class SearchSetting {
[9989]753        public String text;
754        public SearchMode mode;
[2123]755        public boolean caseSensitive;
756        public boolean regexSearch;
[8812]757        public boolean mapCSSSearch;
[3317]758        public boolean allElements;
[967]759
[8332]760        /**
761         * Constructs a new {@code SearchSetting}.
762         */
[2912]763        public SearchSetting() {
[9989]764            text = "";
765            mode = SearchMode.replace;
[2912]766        }
767
[8971]768        /**
769         * Constructs a new {@code SearchSetting} from an existing one.
770         * @param original original search settings
771         */
[2226]772        public SearchSetting(SearchSetting original) {
[8811]773            text = original.text;
774            mode = original.mode;
775            caseSensitive = original.caseSensitive;
776            regexSearch = original.regexSearch;
[8812]777            mapCSSSearch = original.mapCSSSearch;
[8811]778            allElements = original.allElements;
[2226]779        }
780
[967]781        @Override
782        public String toString() {
[2854]783            String cs = caseSensitive ?
[2863]784                    /*case sensitive*/  trc("search", "CS") :
[3355]785                        /*case insensitive*/  trc("search", "CI");
[8811]786            String rx = regexSearch ? ", " +
[8345]787                            /*regex search*/ trc("search", "RX") : "";
[8812]788            String css = mapCSSSearch ? ", " +
789                            /*MapCSS search*/ trc("search", "CSS") : "";
[8811]790            String all = allElements ? ", " +
[8345]791                            /*all elements*/ trc("search", "A") : "";
[8846]792            return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')';
[967]793        }
794
[1808]795        @Override
[1451]796        public boolean equals(Object other) {
[9371]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 &&
[11452]804                    mode == that.mode &&
805                    Objects.equals(text, that.text);
[1451]806        }
[2676]807
808        @Override
809        public int hashCode() {
[9371]810            return Objects.hash(text, mode, caseSensitive, regexSearch, mapCSSSearch, allElements);
[2676]811        }
[3175]812
813        public static SearchSetting readFromString(String s) {
[8394]814            if (s.isEmpty())
[3175]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;
[3317]832                } else if (s.charAt(index) == 'A') {
833                    result.allElements = true;
[8812]834                } else if (s.charAt(index) == 'M') {
835                    result.mapCSSSearch = true;
[3175]836                } else if (s.charAt(index) == ' ') {
837                    break;
838                } else {
[6248]839                    Main.warn("Unknown char in SearchSettings: " + s);
[3175]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() {
[8394]855            if (text == null || text.isEmpty())
[3175]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            }
[8812]866            if (mapCSSSearch) {
867                result.append('M');
868            }
[3317]869            if (allElements) {
870                result.append('A');
871            }
[8379]872            result.append(' ')
873                  .append(text);
[3175]874            return result.toString();
875        }
[967]876    }
[1808]877
878    /**
879     * Refreshes the enabled state
[2512]880     *
[1808]881     */
[1820]882    @Override
883    protected void updateEnabledState() {
[10382]884        setEnabled(getLayerManager().getEditLayer() != null);
[1808]885    }
[3175]886
[6084]887    @Override
[3175]888    public List<ActionParameter<?>> getActionParameters() {
889        return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
890    }
[626]891}
Note: See TracBrowser for help on using the repository browser.