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

Last change on this file since 4018 was 4018, checked in by bastiK, 13 years ago

applied #6187 - Search GUI improvements (patch by bilbo)

  • Property svn:eol-style set to native
File size: 23.1 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions.search;
3
4import java.awt.Dimension;
5import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trc;
8
9import java.awt.Font;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.KeyEvent;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.HashSet;
17import java.util.LinkedList;
18import java.util.List;
19import java.util.Map;
20
21import javax.swing.ButtonGroup;
22import javax.swing.JCheckBox;
23import javax.swing.JLabel;
24import javax.swing.JOptionPane;
25import javax.swing.JPanel;
26import javax.swing.JRadioButton;
27
28import org.openstreetmap.josm.Main;
29import org.openstreetmap.josm.actions.ActionParameter;
30import org.openstreetmap.josm.actions.JosmAction;
31import org.openstreetmap.josm.actions.ParameterizedAction;
32import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter;
33import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
34import org.openstreetmap.josm.data.osm.DataSet;
35import org.openstreetmap.josm.data.osm.Filter;
36import org.openstreetmap.josm.data.osm.OsmPrimitive;
37import org.openstreetmap.josm.gui.ExtendedDialog;
38import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
39import org.openstreetmap.josm.tools.GBC;
40import org.openstreetmap.josm.tools.Property;
41import org.openstreetmap.josm.tools.Shortcut;
42
43public class SearchAction extends JosmAction implements ParameterizedAction {
44
45 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
46
47 private static final String SEARCH_EXPRESSION = "searchExpression";
48
49 public static enum SearchMode {
50 replace('R'), add('A'), remove('D'), in_selection('S');
51
52 private final char code;
53
54 SearchMode(char code) {
55 this.code = code;
56 }
57
58 public char getCode() {
59 return code;
60 }
61
62 public static SearchMode fromCode(char code) {
63 for (SearchMode mode: values()) {
64 if (mode.getCode() == code)
65 return mode;
66 }
67 return null;
68 }
69 }
70
71 private static LinkedList<SearchSetting> searchHistory = null;
72
73 public static Collection<SearchSetting> getSearchHistory() {
74 if (searchHistory == null) {
75 searchHistory = new LinkedList<SearchSetting>();
76 for (String s: Main.pref.getCollection("search.history", Collections.<String>emptyList())) {
77 SearchSetting ss = SearchSetting.readFromString(s);
78 if (ss != null) {
79 searchHistory.add(ss);
80 }
81 }
82 }
83
84 return searchHistory;
85 }
86
87 public static void saveToHistory(SearchSetting s) {
88 if(searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
89 searchHistory.addFirst(new SearchSetting(s));
90 }
91 int maxsize = Main.pref.getInteger("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
92 while (searchHistory.size() > maxsize) {
93 searchHistory.removeLast();
94 }
95 List<String> savedHistory = new ArrayList<String>();
96 for (SearchSetting item: searchHistory) {
97 savedHistory.add(item.writeToString());
98 }
99 Main.pref.putCollection("search.history", savedHistory);
100 }
101
102 public static List<String> getSearchExpressionHistory() {
103 ArrayList<String> ret = new ArrayList<String>(getSearchHistory().size());
104 for (SearchSetting ss: getSearchHistory()) {
105 ret.add(ss.text);
106 }
107 return ret;
108 }
109
110 private static SearchSetting lastSearch = null;
111
112 public SearchAction() {
113 super(tr("Search..."), "dialogs/search", tr("Search for objects."),
114 Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.GROUP_HOTKEY), true);
115 putValue("help", ht("/Action/Search"));
116 }
117
118 public void actionPerformed(ActionEvent e) {
119 if (!isEnabled())
120 return;
121 search();
122 }
123
124 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) {
125 if (parameters.get(SEARCH_EXPRESSION) == null) {
126 actionPerformed(e);
127 } else {
128 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION));
129 }
130 }
131
132 public static SearchSetting showSearchDialog(SearchSetting initialValues) {
133 if (initialValues == null) {
134 initialValues = new SearchSetting();
135 }
136 // -- prepare the combo box with the search expressions
137 //
138 JLabel label = new JLabel( initialValues instanceof Filter ? tr("Filter string:") : tr("Search string:"));
139 final HistoryComboBox hcbSearchString = new HistoryComboBox();
140 hcbSearchString.setText(initialValues.text);
141 hcbSearchString.getEditor().selectAll();
142 hcbSearchString.getEditor().getEditorComponent().requestFocusInWindow();
143 hcbSearchString.setToolTipText(tr("Enter the search expression"));
144 // we have to reverse the history, because ComboBoxHistory will reverse it again
145 // in addElement()
146 //
147 List<String> searchExpressionHistory = getSearchExpressionHistory();
148 Collections.reverse(searchExpressionHistory);
149 hcbSearchString.setPossibleItems(searchExpressionHistory);
150 hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height));
151
152 JRadioButton replace = new JRadioButton(tr("replace selection"), initialValues.mode == SearchMode.replace);
153 JRadioButton add = new JRadioButton(tr("add to selection"), initialValues.mode == SearchMode.add);
154 JRadioButton remove = new JRadioButton(tr("remove from selection"), initialValues.mode == SearchMode.remove);
155 JRadioButton in_selection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection);
156 ButtonGroup bg = new ButtonGroup();
157 bg.add(replace);
158 bg.add(add);
159 bg.add(remove);
160 bg.add(in_selection);
161
162 final JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive);
163 JCheckBox allElements = new JCheckBox(tr("all objects"), initialValues.allElements);
164 allElements.setToolTipText(tr("Also include incomplete and deleted objects in search."));
165 final JCheckBox regexSearch = new JCheckBox(tr("regular expression"), initialValues.regexSearch);
166
167 JPanel top = new JPanel(new GridBagLayout());
168 top.add(label, GBC.std().insets(0, 0, 5, 0));
169 top.add(hcbSearchString, GBC.eol().fill(GBC.HORIZONTAL));
170 JPanel left = new JPanel(new GridBagLayout());
171 left.add(replace, GBC.eol());
172 left.add(add, GBC.eol());
173 left.add(remove, GBC.eol());
174 left.add(in_selection, GBC.eop());
175 left.add(caseSensitive, GBC.eol());
176 left.add(allElements, GBC.eol());
177 left.add(regexSearch, GBC.eol());
178
179 JPanel right = new JPanel();
180 JLabel description =
181 new JLabel("<html><ul>"
182 + "<li>"+tr("<b>Baker Street</b> - ''Baker'' and ''Street'' in any key or name.")+"</li>"
183 + "<li>"+tr("<b>\"Baker Street\"</b> - ''Baker Street'' in any key or name.")+"</li>"
184 + "<li>"+tr("<b>name:Bak</b> - ''Bak'' anywhere in the name.")+"</li>"
185 + "<li>"+tr("<b>type=route</b> - key ''type'' with value exactly ''route''.") + "</li>"
186 + "<li>"+tr("<b>type=*</b> - key ''type'' with any value. Try also <b>*=value</b>, <b>type=</b>, <b>*=*</b>, <b>*=</b>") + "</li>"
187 + "<li>"+tr("<b>-name:Bak</b> - not ''Bak'' in the name.")+"</li>"
188 + "<li>"+tr("<b>oneway?</b> - oneway=yes, true, 1 or on")+"</li>"
189 + "<li>"+tr("<b>foot:</b> - key=foot set to any value.")+"</li>"
190 + "<li>"+tr("<u>Special targets:</u>")+"</li>"
191 + "<li>"+tr("<b>type:</b> - type of the object (<b>node</b>, <b>way</b>, <b>relation</b>)")+"</li>"
192 + "<li>"+tr("<b>user:</b>... - all objects changed by user")+"</li>"
193 + "<li>"+tr("<b>user:anonymous</b> - all objects changed by anonymous users")+"</li>"
194 + "<li>"+tr("<b>id:</b>... - object with given ID (0 for new objects)")+"</li>"
195 + "<li>"+tr("<b>version:</b>... - object with given version (0 objects without an assigned version)")+"</li>"
196 + "<li>"+tr("<b>changeset:</b>... - object with given changeset id (0 objects without assigned changeset)")+"</li>"
197 + "<li>"+tr("<b>nodes:</b>... - object with given number of nodes (nodes:count or nodes:min-max)")+"</li>"
198 + "<li>"+tr("<b>tags:</b>... - object with given number of tags (tags:count or tags:min-max)")+"</li>"
199 + "<li>"+tr("<b>role:</b>... - object with given role in a relation")+"</li>"
200 + "<li>"+tr("<b>timestamp:</b>... - objects with this timestamp (<b>2009-11-12T14:51:09Z</b>, <b>2009-11-12</b> or <b>T14:51</b> ...)")+"</li>"
201 + "<li>"+tr("<b>modified</b> - all changed objects")+"</li>"
202 + "<li>"+tr("<b>selected</b> - all selected objects")+"</li>"
203 + "<li>"+tr("<b>incomplete</b> - all incomplete objects")+"</li>"
204 + "<li>"+tr("<b>untagged</b> - all untagged objects")+"</li>"
205 + "<li>"+tr("<b>child <i>expr</i></b> - all children of objects matching the expression")+"</li>"
206 + "<li>"+tr("<b>parent <i>expr</i></b> - all parents of objects matching the expression")+"</li>"
207 + "<li>"+tr("Use <b>|</b> or <b>OR</b> to combine with logical or")+"</li>"
208 + "<li>"+tr("Use <b>\"</b> to quote operators (e.g. if key contains <b>:</b>)") + "<br/>"
209 + tr("Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>).")+"</li>"
210 + "<li>"+tr("Use <b>(</b> and <b>)</b> to group expressions")+"</li>"
211 + "</ul></html>");
212 description.setFont(description.getFont().deriveFont(Font.PLAIN));
213 right.add(description);
214
215 final JPanel p = new JPanel(new GridBagLayout());
216 p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0));
217 p.add(left, GBC.std().anchor(GBC.NORTH).insets(5, 10, 10, 0));
218 p.add(right, GBC.eol());
219 ExtendedDialog dialog = new ExtendedDialog(
220 Main.parent,
221 initialValues instanceof Filter ? tr("Filter") : tr("Search"),
222 new String[] {
223 initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"),
224 tr("Cancel")}
225 ) {
226 @Override
227 protected void buttonAction(int buttonIndex, ActionEvent evt) {
228 if (buttonIndex == 0) {
229 try {
230 SearchCompiler.compile(hcbSearchString.getText(), caseSensitive.isSelected(), regexSearch.isSelected());
231 super.buttonAction(buttonIndex, evt);
232 } catch (ParseError e) {
233 JOptionPane.showMessageDialog(
234 Main.parent,
235 tr("Search expression is not valid: \n\n {0}", e.getMessage()),
236 tr("Invalid search expression"),
237 JOptionPane.ERROR_MESSAGE);
238 }
239 } else {
240 super.buttonAction(buttonIndex, evt);
241 }
242 }
243 };
244 dialog.setButtonIcons(new String[] {"dialogs/search.png", "cancel.png"});
245 dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */);
246 dialog.setContent(p);
247 dialog.showDialog();
248 int result = dialog.getValue();
249
250 if(result != 1) return null;
251
252 // User pressed OK - let's perform the search
253 SearchMode mode = replace.isSelected() ? SearchAction.SearchMode.replace
254 : (add.isSelected() ? SearchAction.SearchMode.add
255 : (remove.isSelected() ? SearchAction.SearchMode.remove : SearchAction.SearchMode.in_selection));
256 initialValues.text = hcbSearchString.getText();
257 initialValues.mode = mode;
258 initialValues.caseSensitive = caseSensitive.isSelected();
259 initialValues.allElements = allElements.isSelected();
260 initialValues.regexSearch = regexSearch.isSelected();
261 return initialValues;
262 }
263
264 /**
265 * Launches the dialog for specifying search criteria and runs
266 * a search
267 */
268 public static void search() {
269 SearchSetting se = showSearchDialog(lastSearch);
270 if(se != null) {
271 searchWithHistory(se);
272 }
273 }
274
275 /**
276 * Adds the search specified by the settings in <code>s</code> to the
277 * search history and performs the search.
278 *
279 * @param s
280 */
281 public static void searchWithHistory(SearchSetting s) {
282 saveToHistory(s);
283 lastSearch = new SearchSetting(s);
284 search(s);
285 }
286
287 public static void searchWithoutHistory(SearchSetting s) {
288 lastSearch = new SearchSetting(s);
289 search(s);
290 }
291
292 public interface Function{
293 public Boolean isSomething(OsmPrimitive o);
294 }
295
296 public static int getSelection(SearchSetting s, Collection<OsmPrimitive> sel, Function f) {
297 int foundMatches = 0;
298 try {
299 String searchText = s.text;
300 SearchCompiler.Match matcher = SearchCompiler.compile(searchText, s.caseSensitive, s.regexSearch);
301
302 if (s.mode == SearchMode.replace) {
303 sel.clear();
304 }
305
306 Collection<OsmPrimitive> all;
307 if(s.allElements) {
308 all = Main.main.getCurrentDataSet().allPrimitives();
309 } else {
310 all = Main.main.getCurrentDataSet().allNonDeletedCompletePrimitives();
311 }
312 for (OsmPrimitive osm : all) {
313 if (s.mode == SearchMode.replace) {
314 if (matcher.match(osm)) {
315 sel.add(osm);
316 ++foundMatches;
317 }
318 } else if (s.mode == SearchMode.add && !f.isSomething(osm) && matcher.match(osm)) {
319 sel.add(osm);
320 ++foundMatches;
321 } else if (s.mode == SearchMode.remove && f.isSomething(osm) && matcher.match(osm)) {
322 sel.remove(osm);
323 ++foundMatches;
324 } else if (s.mode == SearchMode.in_selection && f.isSomething(osm)&& !matcher.match(osm)) {
325 sel.remove(osm);
326 ++foundMatches;
327 }
328 }
329 } catch (SearchCompiler.ParseError e) {
330 JOptionPane.showMessageDialog(
331 Main.parent,
332 e.getMessage(),
333 tr("Error"),
334 JOptionPane.ERROR_MESSAGE
335
336 );
337 }
338 return foundMatches;
339 }
340
341 /**
342 * Version of getSelection that is customized for filter, but should
343 * also work in other context.
344 *
345 * @param s the search settings
346 * @param all the collection of all the primitives that should be considered
347 * @param p the property that should be set/unset if something is found
348 */
349 public static void getSelection(SearchSetting s, Collection<OsmPrimitive> all, Property<OsmPrimitive, Boolean> p) {
350 try {
351 String searchText = s.text;
352 if (s instanceof Filter && ((Filter)s).inverted) {
353 searchText = String.format("-(%s)", searchText);
354 }
355 SearchCompiler.Match matcher = SearchCompiler.compile(searchText, s.caseSensitive, s.regexSearch);
356
357 for (OsmPrimitive osm : all) {
358 if (s.mode == SearchMode.replace) {
359 if (matcher.match(osm)) {
360 p.set(osm, true);
361 } else {
362 p.set(osm, false);
363 }
364 } else if (s.mode == SearchMode.add && !p.get(osm) && matcher.match(osm)) {
365 p.set(osm, true);
366 } else if (s.mode == SearchMode.remove && p.get(osm) && matcher.match(osm)) {
367 p.set(osm, false);
368 } else if (s.mode == SearchMode.in_selection && p.get(osm) && !matcher.match(osm)) {
369 p.set(osm, false);
370 }
371 }
372 } catch (SearchCompiler.ParseError e) {
373 JOptionPane.showMessageDialog(
374 Main.parent,
375 e.getMessage(),
376 tr("Error"),
377 JOptionPane.ERROR_MESSAGE
378
379 );
380 }
381 }
382
383 public static void search(String search, SearchMode mode) {
384 search(new SearchSetting(search, mode, false, false, false));
385 }
386
387 public static void search(SearchSetting s) {
388 // FIXME: This is confusing. The GUI says nothing about loading primitives from an URL. We'd like to *search*
389 // for URLs in the current data set.
390 // Disabling until a better solution is in place
391 //
392 // if (search.startsWith("http://") || search.startsWith("ftp://") || search.startsWith("https://")
393 // || search.startsWith("file:/")) {
394 // SelectionWebsiteLoader loader = new SelectionWebsiteLoader(search, mode);
395 // if (loader.url != null && loader.url.getHost() != null) {
396 // Main.worker.execute(loader);
397 // return;
398 // }
399 // }
400
401 final DataSet ds = Main.main.getCurrentDataSet();
402 Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>(ds.getSelected());
403 int foundMatches = getSelection(s, sel, new Function(){
404 public Boolean isSomething(OsmPrimitive o){
405 return ds.isSelected(o);
406 }
407 });
408 ds.setSelected(sel);
409 if (foundMatches == 0) {
410 String msg = null;
411 if (s.mode == SearchMode.replace) {
412 msg = tr("No match found for ''{0}''", s.text);
413 } else if (s.mode == SearchMode.add) {
414 msg = tr("Nothing added to selection by searching for ''{0}''", s.text);
415 } else if (s.mode == SearchMode.remove) {
416 msg = tr("Nothing removed from selection by searching for ''{0}''", s.text);
417 } else if (s.mode == SearchMode.in_selection) {
418 msg = tr("Nothing found in selection by searching for ''{0}''", s.text);
419 }
420 Main.map.statusLine.setHelpText(msg);
421 JOptionPane.showMessageDialog(
422 Main.parent,
423 msg,
424 tr("Warning"),
425 JOptionPane.WARNING_MESSAGE
426 );
427 } else {
428 Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
429 }
430 }
431
432 public static class SearchSetting {
433 public String text;
434 public SearchMode mode;
435 public boolean caseSensitive;
436 public boolean regexSearch;
437 public boolean allElements;
438
439 public SearchSetting() {
440 this("", SearchMode.replace, false /* case insensitive */,
441 false /* no regexp */, false /* only useful primitives */);
442 }
443
444 public SearchSetting(String text, SearchMode mode, boolean caseSensitive,
445 boolean regexSearch, boolean allElements) {
446 this.caseSensitive = caseSensitive;
447 this.regexSearch = regexSearch;
448 this.allElements = allElements;
449 this.mode = mode;
450 this.text = text;
451 }
452
453 public SearchSetting(SearchSetting original) {
454 this(original.text, original.mode, original.caseSensitive,
455 original.regexSearch, original.allElements);
456 }
457
458 @Override
459 public String toString() {
460 String cs = caseSensitive ?
461 /*case sensitive*/ trc("search", "CS") :
462 /*case insensitive*/ trc("search", "CI");
463 String rx = regexSearch ? (", " +
464 /*regex search*/ trc("search", "RX")) : "";
465 String all = allElements ? (", " +
466 /*all elements*/ trc("search", "A")) : "";
467 return "\"" + text + "\" (" + cs + rx + all + ", " + mode + ")";
468 }
469
470 @Override
471 public boolean equals(Object other) {
472 if(!(other instanceof SearchSetting))
473 return false;
474 SearchSetting o = (SearchSetting) other;
475 return (o.caseSensitive == this.caseSensitive
476 && o.regexSearch == this.regexSearch
477 && o.allElements == this.allElements
478 && o.mode.equals(this.mode)
479 && o.text.equals(this.text));
480 }
481
482 @Override
483 public int hashCode() {
484 return text.hashCode();
485 }
486
487 public static SearchSetting readFromString(String s) {
488 if (s.length() == 0)
489 return null;
490
491 SearchSetting result = new SearchSetting();
492
493 int index = 1;
494
495 result.mode = SearchMode.fromCode(s.charAt(0));
496 if (result.mode == null) {
497 result.mode = SearchMode.replace;
498 index = 0;
499 }
500
501 while (index < s.length()) {
502 if (s.charAt(index) == 'C') {
503 result.caseSensitive = true;
504 } else if (s.charAt(index) == 'R') {
505 result.regexSearch = true;
506 } else if (s.charAt(index) == 'A') {
507 result.allElements = true;
508 } else if (s.charAt(index) == ' ') {
509 break;
510 } else {
511 System.out.println("Unknown char in SearchSettings: " + s);
512 break;
513 }
514 index++;
515 }
516
517 if (index < s.length() && s.charAt(index) == ' ') {
518 index++;
519 }
520
521 result.text = s.substring(index);
522
523 return result;
524 }
525
526 public String writeToString() {
527 if (text == null || text.length() == 0)
528 return "";
529
530 StringBuilder result = new StringBuilder();
531 result.append(mode.getCode());
532 if (caseSensitive) {
533 result.append('C');
534 }
535 if (regexSearch) {
536 result.append('R');
537 }
538 if (allElements) {
539 result.append('A');
540 }
541 result.append(' ');
542 result.append(text);
543 return result.toString();
544 }
545 }
546
547 /**
548 * Refreshes the enabled state
549 *
550 */
551 @Override
552 protected void updateEnabledState() {
553 setEnabled(getEditLayer() != null);
554 }
555
556 public List<ActionParameter<?>> getActionParameters() {
557 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
558 }
559}
Note: See TracBrowser for help on using the repository browser.