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

Last change on this file since 14927 was 14927, checked in by Don-vip, 5 years ago

see #15051 - refactoring: extract SearchDialog from SearchAction

  • Property svn:eol-style set to native
File size: 18.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.search;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.GraphicsEnvironment;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.HashSet;
17import java.util.LinkedHashSet;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Map;
21import java.util.Set;
22import java.util.function.Predicate;
23
24import javax.swing.JOptionPane;
25
26import org.openstreetmap.josm.actions.ActionParameter;
27import org.openstreetmap.josm.actions.ExpertToggleAction;
28import org.openstreetmap.josm.actions.JosmAction;
29import org.openstreetmap.josm.actions.ParameterizedAction;
30import org.openstreetmap.josm.data.osm.IPrimitive;
31import org.openstreetmap.josm.data.osm.OsmData;
32import org.openstreetmap.josm.data.osm.search.PushbackTokenizer;
33import org.openstreetmap.josm.data.osm.search.SearchCompiler;
34import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
35import org.openstreetmap.josm.data.osm.search.SearchCompiler.SimpleMatchFactory;
36import org.openstreetmap.josm.data.osm.search.SearchMode;
37import org.openstreetmap.josm.data.osm.search.SearchParseError;
38import org.openstreetmap.josm.data.osm.search.SearchSetting;
39import org.openstreetmap.josm.gui.MainApplication;
40import org.openstreetmap.josm.gui.MapFrame;
41import org.openstreetmap.josm.gui.Notification;
42import org.openstreetmap.josm.gui.PleaseWaitRunnable;
43import org.openstreetmap.josm.gui.dialogs.SearchDialog;
44import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
45import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
46import org.openstreetmap.josm.gui.progress.ProgressMonitor;
47import org.openstreetmap.josm.spi.preferences.Config;
48import org.openstreetmap.josm.tools.Logging;
49import org.openstreetmap.josm.tools.Shortcut;
50import org.openstreetmap.josm.tools.Utils;
51
52/**
53 * The search action allows the user to search the data layer using a complex search string.
54 *
55 * @see SearchCompiler
56 * @see SearchDialog
57 */
58public class SearchAction extends JosmAction implements ParameterizedAction {
59
60 /**
61 * The default size of the search history
62 */
63 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
64 /**
65 * Maximum number of characters before the search expression is shortened for display purposes.
66 */
67 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100;
68
69 private static final String SEARCH_EXPRESSION = "searchExpression";
70
71 private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>();
72 static {
73 SearchCompiler.addMatchFactory(new SimpleMatchFactory() {
74 @Override
75 public Collection<String> getKeywords() {
76 return Arrays.asList("inview", "allinview");
77 }
78
79 @Override
80 public Match get(String keyword, boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) throws SearchParseError {
81 switch(keyword) {
82 case "inview":
83 return new InView(false);
84 case "allinview":
85 return new InView(true);
86 default:
87 throw new IllegalStateException("Not expecting keyword " + keyword);
88 }
89 }
90 });
91
92 for (String s: Config.getPref().getList("search.history", Collections.<String>emptyList())) {
93 SearchSetting ss = SearchSetting.readFromString(s);
94 if (ss != null) {
95 searchHistory.add(ss);
96 }
97 }
98 }
99
100 /**
101 * Gets the search history
102 * @return The last searched terms. Do not modify it.
103 */
104 public static Collection<SearchSetting> getSearchHistory() {
105 return searchHistory;
106 }
107
108 /**
109 * Saves a search to the search history.
110 * @param s The search to save
111 */
112 public static void saveToHistory(SearchSetting s) {
113 if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
114 searchHistory.addFirst(new SearchSetting(s));
115 } else if (searchHistory.contains(s)) {
116 // move existing entry to front, fixes #8032 - search history loses entries when re-using queries
117 searchHistory.remove(s);
118 searchHistory.addFirst(new SearchSetting(s));
119 }
120 int maxsize = Config.getPref().getInt("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
121 while (searchHistory.size() > maxsize) {
122 searchHistory.removeLast();
123 }
124 Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size());
125 for (SearchSetting item: searchHistory) {
126 savedHistory.add(item.writeToString());
127 }
128 Config.getPref().putList("search.history", new ArrayList<>(savedHistory));
129 }
130
131 /**
132 * Gets a list of all texts that were recently used in the search
133 * @return The list of search texts.
134 */
135 public static List<String> getSearchExpressionHistory() {
136 List<String> ret = new ArrayList<>(getSearchHistory().size());
137 for (SearchSetting ss: getSearchHistory()) {
138 ret.add(ss.text);
139 }
140 return ret;
141 }
142
143 private static volatile SearchSetting lastSearch;
144
145 /**
146 * Constructs a new {@code SearchAction}.
147 */
148 public SearchAction() {
149 super(tr("Search..."), "dialogs/search", tr("Search for objects"),
150 Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true);
151 setHelpId(ht("/Action/Search"));
152 }
153
154 @Override
155 public void actionPerformed(ActionEvent e) {
156 if (!isEnabled())
157 return;
158 search();
159 }
160
161 @Override
162 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) {
163 if (parameters.get(SEARCH_EXPRESSION) == null) {
164 actionPerformed(e);
165 } else {
166 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION));
167 }
168 }
169
170
171 /**
172 * Builds and shows the search dialog.
173 * @param initialValues A set of initial values needed in order to initialize the search dialog.
174 * If is {@code null}, then default settings are used.
175 * @return Returns {@link SearchAction} object containing parameters of the search.
176 */
177 public static SearchSetting showSearchDialog(SearchSetting initialValues) {
178 if (initialValues == null) {
179 initialValues = new SearchSetting();
180 }
181
182 SearchDialog dialog = new SearchDialog(
183 initialValues, getSearchExpressionHistory(), ExpertToggleAction.isExpert());
184
185 if (dialog.showDialog().getValue() != 1) return null;
186
187 // User pressed OK - let's perform the search
188 SearchSetting searchSettings = dialog.getSearchSettings();
189
190 if (dialog.isAddOnToolbar()) {
191 ToolbarPreferences.ActionDefinition aDef =
192 new ToolbarPreferences.ActionDefinition(MainApplication.getMenu().search);
193 aDef.getParameters().put(SEARCH_EXPRESSION, searchSettings);
194 // Display search expression as tooltip instead of generic one
195 aDef.setName(Utils.shortenString(searchSettings.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY));
196 // parametrized action definition is now composed
197 ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
198 String res = actionParser.saveAction(aDef);
199
200 // add custom search button to toolbar preferences
201 MainApplication.getToolbar().addCustomButton(res, -1, false);
202 }
203
204 return searchSettings;
205 }
206
207 /**
208 * Launches the dialog for specifying search criteria and runs a search
209 */
210 public static void search() {
211 SearchSetting se = showSearchDialog(lastSearch);
212 if (se != null) {
213 searchWithHistory(se);
214 }
215 }
216
217 /**
218 * Adds the search specified by the settings in <code>s</code> to the
219 * search history and performs the search.
220 *
221 * @param s search settings
222 */
223 public static void searchWithHistory(SearchSetting s) {
224 saveToHistory(s);
225 lastSearch = new SearchSetting(s);
226 search(s);
227 }
228
229 /**
230 * Performs the search specified by the settings in <code>s</code> without saving it to search history.
231 *
232 * @param s search settings
233 */
234 public static void searchWithoutHistory(SearchSetting s) {
235 lastSearch = new SearchSetting(s);
236 search(s);
237 }
238
239 /**
240 * Performs the search specified by the search string {@code search} and the search mode {@code mode}.
241 *
242 * @param search the search string to use
243 * @param mode the search mode to use
244 */
245 public static void search(String search, SearchMode mode) {
246 final SearchSetting searchSetting = new SearchSetting();
247 searchSetting.text = search;
248 searchSetting.mode = mode;
249 search(searchSetting);
250 }
251
252 static void search(SearchSetting s) {
253 SearchTask.newSearchTask(s, new SelectSearchReceiver()).run();
254 }
255
256 /**
257 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search.
258 *
259 * @param search the search string to use
260 * @param mode the search mode to use
261 * @return The result of the search.
262 * @since 10457
263 * @since 13950 (signature)
264 */
265 public static Collection<IPrimitive> searchAndReturn(String search, SearchMode mode) {
266 final SearchSetting searchSetting = new SearchSetting();
267 searchSetting.text = search;
268 searchSetting.mode = mode;
269 CapturingSearchReceiver receiver = new CapturingSearchReceiver();
270 SearchTask.newSearchTask(searchSetting, receiver).run();
271 return receiver.result;
272 }
273
274 /**
275 * Interfaces implementing this may receive the result of the current search.
276 * @author Michael Zangl
277 * @since 10457
278 * @since 10600 (functional interface)
279 * @since 13950 (signature)
280 */
281 @FunctionalInterface
282 interface SearchReceiver {
283 /**
284 * Receive the search result
285 * @param ds The data set searched on.
286 * @param result The result collection, including the initial collection.
287 * @param foundMatches The number of matches added to the result.
288 * @param setting The setting used.
289 * @param parent parent component
290 */
291 void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result,
292 int foundMatches, SearchSetting setting, Component parent);
293 }
294
295 /**
296 * Select the search result and display a status text for it.
297 */
298 private static class SelectSearchReceiver implements SearchReceiver {
299
300 @Override
301 public void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result,
302 int foundMatches, SearchSetting setting, Component parent) {
303 ds.setSelected(result);
304 MapFrame map = MainApplication.getMap();
305 if (foundMatches == 0) {
306 final String msg;
307 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
308 if (setting.mode == SearchMode.replace) {
309 msg = tr("No match found for ''{0}''", text);
310 } else if (setting.mode == SearchMode.add) {
311 msg = tr("Nothing added to selection by searching for ''{0}''", text);
312 } else if (setting.mode == SearchMode.remove) {
313 msg = tr("Nothing removed from selection by searching for ''{0}''", text);
314 } else if (setting.mode == SearchMode.in_selection) {
315 msg = tr("Nothing found in selection by searching for ''{0}''", text);
316 } else {
317 msg = null;
318 }
319 if (map != null) {
320 map.statusLine.setHelpText(msg);
321 }
322 if (!GraphicsEnvironment.isHeadless()) {
323 new Notification(msg).show();
324 }
325 } else {
326 map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
327 }
328 }
329 }
330
331 /**
332 * This class stores the result of the search in a local variable.
333 * @author Michael Zangl
334 */
335 private static final class CapturingSearchReceiver implements SearchReceiver {
336 private Collection<IPrimitive> result;
337
338 @Override
339 public void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, int foundMatches,
340 SearchSetting setting, Component parent) {
341 this.result = result;
342 }
343 }
344
345 static final class SearchTask extends PleaseWaitRunnable {
346 private final OsmData<?, ?, ?, ?> ds;
347 private final SearchSetting setting;
348 private final Collection<IPrimitive> selection;
349 private final Predicate<IPrimitive> predicate;
350 private boolean canceled;
351 private int foundMatches;
352 private final SearchReceiver resultReceiver;
353
354 private SearchTask(OsmData<?, ?, ?, ?> ds, SearchSetting setting, Collection<IPrimitive> selection,
355 Predicate<IPrimitive> predicate, SearchReceiver resultReceiver) {
356 super(tr("Searching"));
357 this.ds = ds;
358 this.setting = setting;
359 this.selection = selection;
360 this.predicate = predicate;
361 this.resultReceiver = resultReceiver;
362 }
363
364 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) {
365 final OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
366 if (ds == null) {
367 throw new IllegalStateException("No active dataset");
368 }
369 return newSearchTask(setting, ds, resultReceiver);
370 }
371
372 /**
373 * Create a new search task for the given search setting.
374 * @param setting The setting to use
375 * @param ds The data set to search on
376 * @param resultReceiver will receive the search result
377 * @return A new search task.
378 */
379 private static SearchTask newSearchTask(SearchSetting setting, final OsmData<?, ?, ?, ?> ds, SearchReceiver resultReceiver) {
380 final Collection<IPrimitive> selection = new HashSet<>(ds.getAllSelected());
381 return new SearchTask(ds, setting, selection, IPrimitive::isSelected, resultReceiver);
382 }
383
384 @Override
385 protected void cancel() {
386 this.canceled = true;
387 }
388
389 @Override
390 protected void realRun() {
391 try {
392 foundMatches = 0;
393 SearchCompiler.Match matcher = SearchCompiler.compile(setting);
394
395 if (setting.mode == SearchMode.replace) {
396 selection.clear();
397 } else if (setting.mode == SearchMode.in_selection) {
398 foundMatches = selection.size();
399 }
400
401 Collection<? extends IPrimitive> all;
402 if (setting.allElements) {
403 all = ds.allPrimitives();
404 } else {
405 all = ds.getPrimitives(p -> p.isSelectable()); // Do not use method reference before Java 11!
406 }
407 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false);
408 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size()));
409
410 for (IPrimitive osm : all) {
411 if (canceled) {
412 return;
413 }
414 if (setting.mode == SearchMode.replace) {
415 if (matcher.match(osm)) {
416 selection.add(osm);
417 ++foundMatches;
418 }
419 } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) {
420 selection.add(osm);
421 ++foundMatches;
422 } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) {
423 selection.remove(osm);
424 ++foundMatches;
425 } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) {
426 selection.remove(osm);
427 --foundMatches;
428 }
429 subMonitor.worked(1);
430 }
431 subMonitor.finishTask();
432 } catch (SearchParseError e) {
433 Logging.debug(e);
434 JOptionPane.showMessageDialog(
435 MainApplication.getMainFrame(),
436 e.getMessage(),
437 tr("Error"),
438 JOptionPane.ERROR_MESSAGE
439 );
440 }
441 }
442
443 @Override
444 protected void finish() {
445 if (canceled) {
446 return;
447 }
448 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting, getProgressMonitor().getWindowParent());
449 }
450 }
451
452 /**
453 * {@link ActionParameter} implementation with {@link SearchSetting} as value type.
454 * @since 12547 (moved from {@link ActionParameter})
455 */
456 public static class SearchSettingsActionParameter extends ActionParameter<SearchSetting> {
457
458 /**
459 * Constructs a new {@code SearchSettingsActionParameter}.
460 * @param name parameter name (the key)
461 */
462 public SearchSettingsActionParameter(String name) {
463 super(name);
464 }
465
466 @Override
467 public Class<SearchSetting> getType() {
468 return SearchSetting.class;
469 }
470
471 @Override
472 public SearchSetting readFromString(String s) {
473 return SearchSetting.readFromString(s);
474 }
475
476 @Override
477 public String writeToString(SearchSetting value) {
478 if (value == null)
479 return "";
480 return value.writeToString();
481 }
482 }
483
484 /**
485 * Refreshes the enabled state
486 */
487 @Override
488 protected void updateEnabledState() {
489 setEnabled(getLayerManager().getActiveData() != null);
490 }
491
492 @Override
493 public List<ActionParameter<?>> getActionParameters() {
494 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
495 }
496}
Note: See TracBrowser for help on using the repository browser.