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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 4 weeks ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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