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

Last change on this file since 17188 was 17188, checked in by Klumbumbus, 4 years ago

fix #19851 - Fix shortcut names

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