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

Last change on this file since 12333 was 12333, checked in by michael2402, 7 years ago

Fix #14840: Add separation borders to search dialog. Patch by bafonins

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