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