001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.search; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.Component; 009import java.awt.GraphicsEnvironment; 010import java.awt.event.ActionEvent; 011import java.awt.event.KeyEvent; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.HashSet; 016import java.util.List; 017import java.util.Map; 018import java.util.function.Predicate; 019 020import javax.swing.JOptionPane; 021 022import org.openstreetmap.josm.actions.ActionParameter; 023import org.openstreetmap.josm.actions.ExpertToggleAction; 024import org.openstreetmap.josm.actions.JosmAction; 025import org.openstreetmap.josm.actions.ParameterizedAction; 026import org.openstreetmap.josm.data.osm.IPrimitive; 027import org.openstreetmap.josm.data.osm.OsmData; 028import org.openstreetmap.josm.data.osm.search.PushbackTokenizer; 029import org.openstreetmap.josm.data.osm.search.SearchCompiler; 030import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 031import org.openstreetmap.josm.data.osm.search.SearchCompiler.SimpleMatchFactory; 032import org.openstreetmap.josm.data.osm.search.SearchMode; 033import org.openstreetmap.josm.data.osm.search.SearchParseError; 034import org.openstreetmap.josm.data.osm.search.SearchSetting; 035import org.openstreetmap.josm.gui.MainApplication; 036import org.openstreetmap.josm.gui.MapFrame; 037import org.openstreetmap.josm.gui.Notification; 038import org.openstreetmap.josm.gui.PleaseWaitRunnable; 039import org.openstreetmap.josm.gui.dialogs.SearchDialog; 040import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 041import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser; 042import org.openstreetmap.josm.gui.progress.ProgressMonitor; 043import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel; 044import org.openstreetmap.josm.spi.preferences.Config; 045import org.openstreetmap.josm.tools.Logging; 046import org.openstreetmap.josm.tools.Shortcut; 047import org.openstreetmap.josm.tools.Utils; 048 049/** 050 * The search action allows the user to search the data layer using a complex search string. 051 * 052 * @see SearchCompiler 053 * @see SearchDialog 054 */ 055public class SearchAction extends JosmAction implements ParameterizedAction { 056 057 /** 058 * The default size of the search history 059 */ 060 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15; 061 /** 062 * Maximum number of characters before the search expression is shortened for display purposes. 063 */ 064 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100; 065 066 private static final String SEARCH_EXPRESSION = "searchExpression"; 067 068 private static AutoCompComboBoxModel<SearchSetting> model = new AutoCompComboBoxModel<>(); 069 070 /** preferences reader/writer with automatic transmogrification to and from String */ 071 private static AutoCompComboBoxModel<SearchSetting>.Preferences prefs = model.prefs( 072 SearchSetting::readFromString, SearchSetting::writeToString); 073 074 static { 075 SearchCompiler.addMatchFactory(new SimpleMatchFactory() { 076 @Override 077 public Collection<String> getKeywords() { 078 return Arrays.asList("inview", "allinview"); 079 } 080 081 @Override 082 public Match get(String keyword, boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) throws SearchParseError { 083 switch(keyword) { 084 case "inview": 085 return new InView(false); 086 case "allinview": 087 return new InView(true); 088 default: 089 throw new IllegalStateException("Not expecting keyword " + keyword); 090 } 091 } 092 }); 093 model.setSize(Config.getPref().getInt("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE)); 094 } 095 096 /** 097 * Gets the search history 098 * @return The last searched terms. 099 */ 100 public static Collection<SearchSetting> getSearchHistory() { 101 return model.asCollection(); 102 } 103 104 /** 105 * Saves a search to the search history. 106 * @param s The search to save 107 */ 108 public static void saveToHistory(SearchSetting s) { 109 model.addTopElement(s); 110 prefs.save("search.history"); 111 } 112 113 /** 114 * Gets a list of all texts that were recently used in the search 115 * @return The list of search texts. 116 */ 117 public static List<String> getSearchExpressionHistory() { 118 return prefs.asStringList(); 119 } 120 121 private static volatile SearchSetting lastSearch; 122 123 /** 124 * Constructs a new {@code SearchAction}. 125 */ 126 public SearchAction() { 127 super(tr("Search..."), "dialogs/search", tr("Search for objects"), 128 Shortcut.registerShortcut("system:find", tr("Edit: {0}", tr("Search...")), KeyEvent.VK_F, Shortcut.CTRL), true); 129 setHelpId(ht("/Action/Search")); 130 } 131 132 @Override 133 public void actionPerformed(ActionEvent e) { 134 if (!isEnabled()) 135 return; 136 search(); 137 } 138 139 @Override 140 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) { 141 if (parameters.get(SEARCH_EXPRESSION) == null) { 142 actionPerformed(e); 143 } else { 144 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION)); 145 } 146 } 147 148 /** 149 * Builds and shows the search dialog. 150 * @param initialValues A set of initial values needed in order to initialize the search dialog. 151 * If is {@code null}, then default settings are used. 152 * @return Returns new {@link SearchSetting} object containing parameters of the search. 153 */ 154 public static SearchSetting showSearchDialog(SearchSetting initialValues) { 155 if (initialValues == null) { 156 initialValues = new SearchSetting(); 157 } 158 159 SearchDialog dialog = new SearchDialog( 160 initialValues, model, ExpertToggleAction.isExpert()); 161 162 if (dialog.showDialog().getValue() != 1) return null; 163 164 // User pressed OK - let's perform the search 165 SearchSetting searchSettings = dialog.getSearchSettings(); 166 167 if (dialog.isAddOnToolbar()) { 168 ToolbarPreferences.ActionDefinition aDef = 169 new ToolbarPreferences.ActionDefinition(MainApplication.getMenu().search); 170 aDef.getParameters().put(SEARCH_EXPRESSION, searchSettings); 171 // Display search expression as tooltip instead of generic one 172 aDef.setName(Utils.shortenString(searchSettings.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY)); 173 // parametrized action definition is now composed 174 ActionParser actionParser = new ToolbarPreferences.ActionParser(null); 175 String res = actionParser.saveAction(aDef); 176 177 // add custom search button to toolbar preferences 178 MainApplication.getToolbar().addCustomButton(res, -1, false); 179 } 180 181 return searchSettings; 182 } 183 184 /** 185 * Launches the dialog for specifying search criteria and runs a search 186 */ 187 public static void search() { 188 prefs.load("search.history"); 189 SearchSetting se = showSearchDialog(lastSearch); 190 if (se != null) { 191 searchWithHistory(se); 192 } 193 } 194 195 /** 196 * Adds the search specified by the settings in <code>s</code> to the 197 * search history and performs the search. 198 * 199 * @param s search settings 200 */ 201 public static void searchWithHistory(SearchSetting s) { 202 saveToHistory(s); 203 lastSearch = new SearchSetting(s); 204 searchStateless(s); 205 } 206 207 /** 208 * Performs the search specified by the settings in <code>s</code> without saving it to search history. 209 * 210 * @param s search settings 211 */ 212 public static void searchWithoutHistory(SearchSetting s) { 213 lastSearch = new SearchSetting(s); 214 searchStateless(s); 215 } 216 217 /** 218 * Performs the search specified by the search string {@code search} and the search mode {@code mode}. 219 * 220 * @param search the search string to use 221 * @param mode the search mode to use 222 */ 223 public static void search(String search, SearchMode mode) { 224 final SearchSetting searchSetting = new SearchSetting(); 225 searchSetting.text = search; 226 searchSetting.mode = mode; 227 searchStateless(searchSetting); 228 } 229 230 /** 231 * Performs a stateless search specified by the settings in <code>s</code>. 232 * 233 * @param s search settings 234 * @since 15356 235 */ 236 public static void searchStateless(SearchSetting s) { 237 SearchTask.newSearchTask(s, new SelectSearchReceiver()).run(); 238 } 239 240 /** 241 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search. 242 * 243 * @param search the search string to use 244 * @param mode the search mode to use 245 * @return The result of the search. 246 * @since 10457 247 * @since 13950 (signature) 248 */ 249 public static Collection<IPrimitive> searchAndReturn(String search, SearchMode mode) { 250 final SearchSetting searchSetting = new SearchSetting(); 251 searchSetting.text = search; 252 searchSetting.mode = mode; 253 CapturingSearchReceiver receiver = new CapturingSearchReceiver(); 254 SearchTask.newSearchTask(searchSetting, receiver).run(); 255 return receiver.result; 256 } 257 258 /** 259 * Interfaces implementing this may receive the result of the current search. 260 * @author Michael Zangl 261 * @since 10457 262 * @since 10600 (functional interface) 263 * @since 13950 (signature) 264 */ 265 @FunctionalInterface 266 interface SearchReceiver { 267 /** 268 * Receive the search result 269 * @param ds The data set searched on. 270 * @param result The result collection, including the initial collection. 271 * @param foundMatches The number of matches added to the result. 272 * @param setting The setting used. 273 * @param parent parent component 274 */ 275 void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, 276 int foundMatches, SearchSetting setting, Component parent); 277 } 278 279 /** 280 * Select the search result and display a status text for it. 281 */ 282 private static class SelectSearchReceiver implements SearchReceiver { 283 284 @Override 285 public void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, 286 int foundMatches, SearchSetting setting, Component parent) { 287 ds.setSelected(result); 288 MapFrame map = MainApplication.getMap(); 289 if (foundMatches == 0) { 290 final String msg; 291 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY); 292 if (setting.mode == SearchMode.replace) { 293 msg = tr("No match found for ''{0}''", text); 294 } else if (setting.mode == SearchMode.add) { 295 msg = tr("Nothing added to selection by searching for ''{0}''", text); 296 } else if (setting.mode == SearchMode.remove) { 297 msg = tr("Nothing removed from selection by searching for ''{0}''", text); 298 } else if (setting.mode == SearchMode.in_selection) { 299 msg = tr("Nothing found in selection by searching for ''{0}''", text); 300 } else { 301 msg = null; 302 } 303 if (map != null) { 304 map.statusLine.setHelpText(msg); 305 } 306 if (!GraphicsEnvironment.isHeadless()) { 307 new Notification(msg).show(); 308 } 309 } else { 310 map.statusLine.setHelpText(tr("Found {0} matches", foundMatches)); 311 } 312 } 313 } 314 315 /** 316 * This class stores the result of the search in a local variable. 317 * @author Michael Zangl 318 */ 319 private static final class CapturingSearchReceiver implements SearchReceiver { 320 private Collection<IPrimitive> result; 321 322 @Override 323 public void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, int foundMatches, 324 SearchSetting setting, Component parent) { 325 this.result = result; 326 } 327 } 328 329 static final class SearchTask extends PleaseWaitRunnable { 330 private final OsmData<?, ?, ?, ?> ds; 331 private final SearchSetting setting; 332 private final Collection<IPrimitive> selection; 333 private final Predicate<IPrimitive> predicate; 334 private boolean canceled; 335 private int foundMatches; 336 private final SearchReceiver resultReceiver; 337 338 private SearchTask(OsmData<?, ?, ?, ?> ds, SearchSetting setting, Collection<IPrimitive> selection, 339 Predicate<IPrimitive> predicate, SearchReceiver resultReceiver) { 340 super(tr("Searching")); 341 this.ds = ds; 342 this.setting = setting; 343 this.selection = selection; 344 this.predicate = predicate; 345 this.resultReceiver = resultReceiver; 346 } 347 348 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) { 349 final OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData(); 350 if (ds == null) { 351 throw new IllegalStateException("No active dataset"); 352 } 353 return newSearchTask(setting, ds, resultReceiver); 354 } 355 356 /** 357 * Create a new search task for the given search setting. 358 * @param setting The setting to use 359 * @param ds The data set to search on 360 * @param resultReceiver will receive the search result 361 * @return A new search task. 362 */ 363 private static SearchTask newSearchTask(SearchSetting setting, final OsmData<?, ?, ?, ?> ds, SearchReceiver resultReceiver) { 364 final Collection<IPrimitive> selection = new HashSet<>(ds.getAllSelected()); 365 return new SearchTask(ds, setting, selection, IPrimitive::isSelected, resultReceiver); 366 } 367 368 @Override 369 protected void cancel() { 370 this.canceled = true; 371 } 372 373 @Override 374 protected void realRun() { 375 try { 376 foundMatches = 0; 377 SearchCompiler.Match matcher = SearchCompiler.compile(setting); 378 379 if (setting.mode == SearchMode.replace) { 380 selection.clear(); 381 } else if (setting.mode == SearchMode.in_selection) { 382 foundMatches = selection.size(); 383 } 384 385 Collection<? extends IPrimitive> all; 386 if (setting.allElements) { 387 all = ds.allPrimitives(); 388 } else { 389 all = ds.getPrimitives(p -> p.isSelectable()); // Do not use method reference before Java 11! 390 } 391 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false); 392 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size())); 393 394 for (IPrimitive osm : all) { 395 if (canceled) { 396 return; 397 } 398 if (setting.mode == SearchMode.replace) { 399 if (matcher.match(osm)) { 400 selection.add(osm); 401 ++foundMatches; 402 } 403 } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) { 404 selection.add(osm); 405 ++foundMatches; 406 } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) { 407 selection.remove(osm); 408 ++foundMatches; 409 } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) { 410 selection.remove(osm); 411 --foundMatches; 412 } 413 subMonitor.worked(1); 414 } 415 subMonitor.finishTask(); 416 } catch (SearchParseError e) { 417 Logging.debug(e); 418 JOptionPane.showMessageDialog( 419 MainApplication.getMainFrame(), 420 e.getMessage(), 421 tr("Error"), 422 JOptionPane.ERROR_MESSAGE 423 ); 424 } 425 } 426 427 @Override 428 protected void finish() { 429 if (canceled) { 430 return; 431 } 432 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting, getProgressMonitor().getWindowParent()); 433 } 434 } 435 436 /** 437 * {@link ActionParameter} implementation with {@link SearchSetting} as value type. 438 * @since 12547 (moved from {@link ActionParameter}) 439 */ 440 public static class SearchSettingsActionParameter extends ActionParameter<SearchSetting> { 441 442 /** 443 * Constructs a new {@code SearchSettingsActionParameter}. 444 * @param name parameter name (the key) 445 */ 446 public SearchSettingsActionParameter(String name) { 447 super(name); 448 } 449 450 @Override 451 public Class<SearchSetting> getType() { 452 return SearchSetting.class; 453 } 454 455 @Override 456 public SearchSetting readFromString(String s) { 457 return SearchSetting.readFromString(s); 458 } 459 460 @Override 461 public String writeToString(SearchSetting value) { 462 if (value == null) 463 return ""; 464 return value.writeToString(); 465 } 466 } 467 468 @Override 469 protected boolean listenToSelectionChange() { 470 return false; 471 } 472 473 /** 474 * Refreshes the enabled state 475 */ 476 @Override 477 protected void updateEnabledState() { 478 setEnabled(getLayerManager().getActiveData() != null); 479 } 480 481 @Override 482 public List<ActionParameter<?>> getActionParameters() { 483 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION)); 484 } 485}