001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagLayout; 007import java.awt.event.KeyEvent; 008import java.util.Collection; 009import java.util.List; 010import java.util.concurrent.CancellationException; 011import java.util.concurrent.ExecutionException; 012import java.util.concurrent.Future; 013 014import javax.swing.AbstractAction; 015import javax.swing.JOptionPane; 016import javax.swing.JPanel; 017 018import org.openstreetmap.josm.command.Command; 019import org.openstreetmap.josm.data.osm.DataSelectionListener; 020import org.openstreetmap.josm.data.osm.DataSet; 021import org.openstreetmap.josm.data.osm.OsmPrimitive; 022import org.openstreetmap.josm.data.osm.OsmUtils; 023import org.openstreetmap.josm.data.osm.event.SelectionEventManager; 024import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 025import org.openstreetmap.josm.gui.MainApplication; 026import org.openstreetmap.josm.gui.Notification; 027import org.openstreetmap.josm.gui.help.HelpUtil; 028import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 029import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 030import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 031import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 032import org.openstreetmap.josm.gui.layer.MainLayerManager; 033import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 034import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 035import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor; 036import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 037import org.openstreetmap.josm.tools.Destroyable; 038import org.openstreetmap.josm.tools.ImageProvider; 039import org.openstreetmap.josm.tools.ImageResource; 040import org.openstreetmap.josm.tools.Logging; 041import org.openstreetmap.josm.tools.Shortcut; 042 043/** 044 * Base class helper for all Actions in JOSM. Just to make the life easier. 045 * 046 * This action allows you to set up an icon, a tooltip text, a globally registered shortcut, register it in the main toolbar and set up 047 * layer/selection listeners that call {@link #updateEnabledState()} whenever the global context is changed. 048 * 049 * A JosmAction can register a {@link LayerChangeListener} and a {@link DataSelectionListener}. Upon 050 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}. 051 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state 052 * of a JosmAction depending on the {@link #getLayerManager()} state. 053 * 054 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has 055 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never 056 * be called (currently). 057 * 058 * @author imi 059 */ 060public abstract class JosmAction extends AbstractAction implements Destroyable { 061 062 protected transient Shortcut sc; 063 private transient LayerChangeAdapter layerChangeAdapter; 064 private transient ActiveLayerChangeAdapter activeLayerChangeAdapter; 065 private transient SelectionChangeAdapter selectionChangeAdapter; 066 067 /** 068 * Constructs a {@code JosmAction}. 069 * 070 * @param name the action's text as displayed on the menu (if it is added to a menu) 071 * @param icon the icon to use 072 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 073 * that html is not supported for menu actions on some platforms. 074 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 075 * do want a shortcut, remember you can always register it with group=none, so you 076 * won't be assigned a shortcut unless the user configures one. If you pass null here, 077 * the user CANNOT configure a shortcut for your action. 078 * @param registerInToolbar register this action for the toolbar preferences? 079 * @param toolbarId identifier for the toolbar preferences 080 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 081 */ 082 protected JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar, 083 String toolbarId, boolean installAdapters) { 084 super(name); 085 if (icon != null) { 086 ImageResource resource = icon.getResource(); 087 if (resource != null) { 088 try { 089 resource.attachImageIcon(this, true); 090 } catch (RuntimeException e) { 091 Logging.warn("Unable to attach image icon {0} for action {1}", icon, name); 092 Logging.error(e); 093 } 094 } 095 } 096 setHelpId(); 097 sc = shortcut; 098 if (sc != null && !sc.isAutomatic()) { 099 MainApplication.registerActionShortcut(this, sc); 100 } 101 setTooltip(tooltip); 102 if (getValue("toolbar") == null) { 103 setToolbarId(toolbarId); 104 } 105 if (registerInToolbar && MainApplication.getToolbar() != null) { 106 MainApplication.getToolbar().register(this); 107 } 108 if (installAdapters) { 109 installAdapters(); 110 } 111 } 112 113 /** 114 * The new super for all actions. 115 * 116 * Use this super constructor to setup your action. 117 * 118 * @param name the action's text as displayed on the menu (if it is added to a menu) 119 * @param iconName the filename of the icon to use 120 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 121 * that html is not supported for menu actions on some platforms. 122 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 123 * do want a shortcut, remember you can always register it with group=none, so you 124 * won't be assigned a shortcut unless the user configures one. If you pass null here, 125 * the user CANNOT configure a shortcut for your action. 126 * @param registerInToolbar register this action for the toolbar preferences? 127 * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null 128 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 129 */ 130 protected JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, 131 String toolbarId, boolean installAdapters) { 132 this(name, iconName == null ? null : new ImageProvider(iconName).setOptional(true), tooltip, shortcut, registerInToolbar, 133 toolbarId == null ? iconName : toolbarId, installAdapters); 134 } 135 136 /** 137 * Constructs a new {@code JosmAction}. 138 * 139 * Use this super constructor to setup your action. 140 * 141 * @param name the action's text as displayed on the menu (if it is added to a menu) 142 * @param iconName the filename of the icon to use 143 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 144 * that html is not supported for menu actions on some platforms. 145 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 146 * do want a shortcut, remember you can always register it with group=none, so you 147 * won't be assigned a shortcut unless the user configures one. If you pass null here, 148 * the user CANNOT configure a shortcut for your action. 149 * @param registerInToolbar register this action for the toolbar preferences? 150 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 151 */ 152 protected JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, boolean installAdapters) { 153 this(name, iconName, tooltip, shortcut, registerInToolbar, null, installAdapters); 154 } 155 156 /** 157 * Constructs a new {@code JosmAction} and installs layer changed and selection changed adapters. 158 * 159 * Use this super constructor to setup your action. 160 * 161 * @param name the action's text as displayed on the menu (if it is added to a menu) 162 * @param iconName the filename of the icon to use 163 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 164 * that html is not supported for menu actions on some platforms. 165 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 166 * do want a shortcut, remember you can always register it with group=none, so you 167 * won't be assigned a shortcut unless the user configures one. If you pass null here, 168 * the user CANNOT configure a shortcut for your action. 169 * @param registerInToolbar register this action for the toolbar preferences? 170 */ 171 protected JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) { 172 this(name, iconName, tooltip, shortcut, registerInToolbar, null, true); 173 } 174 175 /** 176 * Constructs a new {@code JosmAction}. 177 */ 178 protected JosmAction() { 179 this(true); 180 } 181 182 /** 183 * Constructs a new {@code JosmAction}. 184 * 185 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 186 */ 187 protected JosmAction(boolean installAdapters) { 188 setHelpId(); 189 if (installAdapters) { 190 installAdapters(); 191 } 192 } 193 194 /** 195 * Constructs a new {@code JosmAction}. 196 * 197 * Use this super constructor to setup your action. 198 * 199 * @param name the action's text as displayed on the menu (if it is added to a menu) 200 * @param iconName the filename of the icon to use 201 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 202 * that html is not supported for menu actions on some platforms. 203 * @param shortcuts ready-created shortcut objects 204 * @since 14012 205 */ 206 protected JosmAction(String name, String iconName, String tooltip, List<Shortcut> shortcuts) { 207 this(name, iconName, tooltip, shortcuts.get(0), true, null, true); 208 for (int i = 1; i < shortcuts.size(); i++) { 209 MainApplication.registerActionShortcut(this, shortcuts.get(i)); 210 } 211 } 212 213 /** 214 * Installs the listeners to this action. 215 * <p> 216 * This should either never be called or only called in the constructor of this action. 217 * <p> 218 * All registered adapters should be removed in {@link #destroy()} 219 */ 220 protected void installAdapters() { 221 // make this action listen to layer change and selection change events 222 if (listenToLayerChange()) { 223 layerChangeAdapter = buildLayerChangeAdapter(); 224 activeLayerChangeAdapter = buildActiveLayerChangeAdapter(); 225 getLayerManager().addLayerChangeListener(layerChangeAdapter); 226 getLayerManager().addActiveLayerChangeListener(activeLayerChangeAdapter); 227 } 228 if (listenToSelectionChange()) { 229 selectionChangeAdapter = new SelectionChangeAdapter(); 230 SelectionEventManager.getInstance().addSelectionListenerForEdt(selectionChangeAdapter); 231 } 232 initEnabledState(); 233 } 234 235 /** 236 * Override this if calling {@link #updateEnabledState()} on layer change events is not enough. 237 * @return the {@link LayerChangeAdapter} that will be called on layer change events 238 * @since 15404 239 */ 240 protected LayerChangeAdapter buildLayerChangeAdapter() { 241 return new LayerChangeAdapter(); 242 } 243 244 /** 245 * Override this if calling {@link #updateEnabledState()} on active layer change event is not enough. 246 * @return the {@link LayerChangeAdapter} that will be called on active layer change event 247 * @since 15404 248 */ 249 protected ActiveLayerChangeAdapter buildActiveLayerChangeAdapter() { 250 return new ActiveLayerChangeAdapter(); 251 } 252 253 /** 254 * Overwrite this if {@link #updateEnabledState()} should be called when the active / available layers change. Default is true. 255 * @return <code>true</code> if a {@link LayerChangeListener} and a {@link ActiveLayerChangeListener} should be registered. 256 * @since 10353 257 */ 258 protected boolean listenToLayerChange() { 259 return true; 260 } 261 262 /** 263 * Overwrite this if {@link #updateEnabledState()} should be called when the selection changed. Default is true. 264 * @return <code>true</code> if a {@link DataSelectionListener} should be registered. 265 * @since 10353 266 */ 267 protected boolean listenToSelectionChange() { 268 return true; 269 } 270 271 @Override 272 public void destroy() { 273 if (sc != null && !sc.isAutomatic()) { 274 MainApplication.unregisterActionShortcut(this); 275 } 276 if (layerChangeAdapter != null) { 277 getLayerManager().removeLayerChangeListener(layerChangeAdapter); 278 getLayerManager().removeActiveLayerChangeListener(activeLayerChangeAdapter); 279 } 280 if (selectionChangeAdapter != null) { 281 SelectionEventManager.getInstance().removeSelectionListener(selectionChangeAdapter); 282 } 283 if (MainApplication.getToolbar() != null) { 284 MainApplication.getToolbar().unregister(this); 285 } 286 } 287 288 private void setHelpId() { 289 String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1); 290 if (helpId.endsWith("Action")) { 291 helpId = helpId.substring(0, helpId.length()-6); 292 } 293 setHelpId(helpId); 294 } 295 296 /** 297 * Sets the help topic id. 298 * @param helpId help topic id (result of {@link HelpUtil#ht}) 299 * @since 14397 300 */ 301 protected void setHelpId(String helpId) { 302 putValue("help", helpId); 303 } 304 305 /** 306 * Sets the toolbar id. 307 * @param toolbarId toolbar id 308 * @since 16138 309 */ 310 protected void setToolbarId(String toolbarId) { 311 putValue("toolbar", toolbarId); 312 } 313 314 /** 315 * Returns the shortcut for this action. 316 * @return the shortcut for this action, or "No shortcut" if none is defined 317 */ 318 public Shortcut getShortcut() { 319 if (sc == null) { 320 sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE); 321 // as this shortcut is shared by all action that don't want to have a shortcut, 322 // we shouldn't allow the user to change it... 323 // this is handled by special name "core:none" 324 } 325 return sc; 326 } 327 328 /** 329 * Sets the tooltip text of this action. 330 * @param tooltip The text to display in tooltip. Can be {@code null} 331 */ 332 public final void setTooltip(String tooltip) { 333 if (tooltip != null && sc != null) { 334 sc.setTooltip(this, tooltip); 335 } else if (tooltip != null) { 336 putValue(SHORT_DESCRIPTION, tooltip); 337 } 338 } 339 340 /** 341 * Gets the layer manager used for this action. Defaults to the main layer manager but you can overwrite this. 342 * <p> 343 * The layer manager must be available when {@link #installAdapters()} is called and must not change. 344 * 345 * @return The layer manager. 346 * @since 10353 347 */ 348 public MainLayerManager getLayerManager() { 349 return MainApplication.getLayerManager(); 350 } 351 352 protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) { 353 MainApplication.worker.submit(() -> { 354 try { 355 future.get(); 356 } catch (InterruptedException | ExecutionException | CancellationException e) { 357 Logging.error(e); 358 return; 359 } 360 monitor.close(); 361 }); 362 } 363 364 /** 365 * Override in subclasses to init the enabled state of an action when it is 366 * created. Default behaviour is to call {@link #updateEnabledState()} 367 * 368 * @see #updateEnabledState() 369 * @see #updateEnabledState(Collection) 370 */ 371 protected void initEnabledState() { 372 updateEnabledState(); 373 } 374 375 /** 376 * Override in subclasses to update the enabled state of the action when 377 * something in the JOSM state changes, i.e. when a layer is removed or added. 378 * 379 * See {@link #updateEnabledState(Collection)} to respond to changes in the collection 380 * of selected primitives. 381 * 382 * Default behavior is empty. 383 * 384 * @see #updateEnabledState(Collection) 385 * @see #initEnabledState() 386 * @see #listenToLayerChange() 387 */ 388 protected void updateEnabledState() { 389 } 390 391 /** 392 * Override in subclasses to update the enabled state of the action if the 393 * collection of selected primitives changes. This method is called with the 394 * new selection. 395 * 396 * @param selection the collection of selected primitives; may be empty, but not null 397 * 398 * @see #updateEnabledState() 399 * @see #initEnabledState() 400 * @see #listenToSelectionChange() 401 */ 402 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 403 } 404 405 /** 406 * Updates enabled state according to primitives currently selected in edit data set, if any. 407 * Can be called in {@link #updateEnabledState()} implementations. 408 * @see #updateEnabledStateOnCurrentSelection(boolean) 409 * @since 10409 410 */ 411 protected final void updateEnabledStateOnCurrentSelection() { 412 updateEnabledStateOnCurrentSelection(false); 413 } 414 415 /** 416 * Updates enabled state according to primitives currently selected in active data set, if any. 417 * Can be called in {@link #updateEnabledState()} implementations. 418 * @param allowReadOnly if {@code true}, read-only data sets are considered 419 * @since 13434 420 */ 421 protected final void updateEnabledStateOnCurrentSelection(boolean allowReadOnly) { 422 DataSet ds = getLayerManager().getActiveDataSet(); 423 if (ds != null && (allowReadOnly || !ds.isLocked())) { 424 updateEnabledState(ds.getSelected()); 425 } else { 426 setEnabled(false); 427 } 428 } 429 430 /** 431 * Updates enabled state according to selected primitives, if any. 432 * Enables action if the collection is not empty and references primitives in a modifiable data layer. 433 * Can be called in {@link #updateEnabledState(Collection)} implementations. 434 * @param selection the collection of selected primitives 435 * @since 13434 436 */ 437 protected final void updateEnabledStateOnModifiableSelection(Collection<? extends OsmPrimitive> selection) { 438 setEnabled(OsmUtils.isOsmCollectionEditable(selection)); 439 } 440 441 /** 442 * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed. 443 */ 444 protected class LayerChangeAdapter implements LayerChangeListener { 445 @Override 446 public void layerAdded(LayerAddEvent e) { 447 updateEnabledState(); 448 } 449 450 @Override 451 public void layerRemoving(LayerRemoveEvent e) { 452 updateEnabledState(); 453 } 454 455 @Override 456 public void layerOrderChanged(LayerOrderChangeEvent e) { 457 updateEnabledState(); 458 } 459 460 @Override 461 public String toString() { 462 return "LayerChangeAdapter [" + JosmAction.this + ']'; 463 } 464 } 465 466 /** 467 * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed. 468 */ 469 protected class ActiveLayerChangeAdapter implements ActiveLayerChangeListener { 470 @Override 471 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 472 updateEnabledState(); 473 } 474 475 @Override 476 public String toString() { 477 return "ActiveLayerChangeAdapter [" + JosmAction.this + ']'; 478 } 479 } 480 481 /** 482 * Adapter for selection change events. Runs updateEnabledState() whenever the selection changed. 483 */ 484 protected class SelectionChangeAdapter implements DataSelectionListener { 485 @Override 486 public void selectionChanged(SelectionChangeEvent event) { 487 updateEnabledState(event.getSelection()); 488 } 489 490 @Override 491 public String toString() { 492 return "SelectionChangeAdapter [" + JosmAction.this + ']'; 493 } 494 } 495 496 /** 497 * Check whether user is about to operate on data outside of the download area. 498 * Request confirmation if he is. 499 * Also handles the case that there is no download area. 500 * 501 * @param operation the operation name which is used for setting some preferences 502 * @param dialogTitle the title of the dialog being displayed 503 * @param outsideDialogMessage the message text to be displayed when data is outside of the download area or no download area exists 504 * @param incompleteDialogMessage the message text to be displayed when data is incomplete 505 * @param primitives the primitives to operate on 506 * @param ignore {@code null} or a primitive to be ignored 507 * @return true, if operating on outlying primitives is OK; false, otherwise 508 * @since 12749 (moved from Command) 509 */ 510 public static boolean checkAndConfirmOutlyingOperation(String operation, 511 String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage, 512 Collection<? extends OsmPrimitive> primitives, 513 Collection<? extends OsmPrimitive> ignore) { 514 int checkRes = Command.checkOutlyingOrIncompleteOperation(primitives, ignore); 515 if ((checkRes & Command.IS_OUTSIDE) != 0) { 516 boolean answer = showConfirmOutlyingOperationDialog(operation + "_outside_nodes", outsideDialogMessage, dialogTitle); 517 if (!answer) 518 return false; 519 } 520 if ((checkRes & Command.IS_INCOMPLETE) != 0) { 521 boolean answer = showConfirmOutlyingOperationDialog(operation + "_incomplete", incompleteDialogMessage, dialogTitle); 522 if (!answer) 523 return false; 524 } 525 return true; 526 } 527 528 private static boolean showConfirmOutlyingOperationDialog(String preferenceKey, String dialogMessage, String dialogTitle) { 529 JPanel msg = new JPanel(new GridBagLayout()); 530 msg.add(new JMultilineLabel("<html>" + dialogMessage + "</html>")); 531 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 532 preferenceKey, 533 MainApplication.getMainFrame(), 534 msg, 535 dialogTitle, 536 JOptionPane.YES_NO_OPTION, 537 JOptionPane.QUESTION_MESSAGE, 538 JOptionPane.YES_OPTION); 539 if (!answer && JOptionPane.NO_OPTION == ConditionalOptionPaneUtil.getDialogReturnValue(preferenceKey)) { 540 String message = tr("Operation was not performed, as per {0} preference", preferenceKey); 541 new Notification(message).show(); 542 Logging.info(message); 543 } 544 return answer; 545 } 546}