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 if (MainApplication.getToolbar() != null) { 282 MainApplication.getToolbar().unregister(this); 283 } 284 } 285 286 private void setHelpId() { 287 String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1); 288 if (helpId.endsWith("Action")) { 289 helpId = helpId.substring(0, helpId.length()-6); 290 } 291 setHelpId(helpId); 292 } 293 294 protected void setHelpId(String helpId) { 295 putValue("help", helpId); 296 } 297 298 /** 299 * Returns the shortcut for this action. 300 * @return the shortcut for this action, or "No shortcut" if none is defined 301 */ 302 public Shortcut getShortcut() { 303 if (sc == null) { 304 sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE); 305 // as this shortcut is shared by all action that don't want to have a shortcut, 306 // we shouldn't allow the user to change it... 307 // this is handled by special name "core:none" 308 } 309 return sc; 310 } 311 312 /** 313 * Sets the tooltip text of this action. 314 * @param tooltip The text to display in tooltip. Can be {@code null} 315 */ 316 public final void setTooltip(String tooltip) { 317 if (tooltip != null && sc != null) { 318 sc.setTooltip(this, tooltip); 319 } else if (tooltip != null) { 320 putValue(SHORT_DESCRIPTION, tooltip); 321 } 322 } 323 324 /** 325 * Gets the layer manager used for this action. Defaults to the main layer manager but you can overwrite this. 326 * <p> 327 * The layer manager must be available when {@link #installAdapters()} is called and must not change. 328 * 329 * @return The layer manager. 330 * @since 10353 331 */ 332 public MainLayerManager getLayerManager() { 333 return MainApplication.getLayerManager(); 334 } 335 336 protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) { 337 MainApplication.worker.submit(() -> { 338 try { 339 future.get(); 340 } catch (InterruptedException | ExecutionException | CancellationException e) { 341 Logging.error(e); 342 return; 343 } 344 monitor.close(); 345 }); 346 } 347 348 /** 349 * Override in subclasses to init the enabled state of an action when it is 350 * created. Default behaviour is to call {@link #updateEnabledState()} 351 * 352 * @see #updateEnabledState() 353 * @see #updateEnabledState(Collection) 354 */ 355 protected void initEnabledState() { 356 updateEnabledState(); 357 } 358 359 /** 360 * Override in subclasses to update the enabled state of the action when 361 * something in the JOSM state changes, i.e. when a layer is removed or added. 362 * 363 * See {@link #updateEnabledState(Collection)} to respond to changes in the collection 364 * of selected primitives. 365 * 366 * Default behavior is empty. 367 * 368 * @see #updateEnabledState(Collection) 369 * @see #initEnabledState() 370 * @see #listenToLayerChange() 371 */ 372 protected void updateEnabledState() { 373 } 374 375 /** 376 * Override in subclasses to update the enabled state of the action if the 377 * collection of selected primitives changes. This method is called with the 378 * new selection. 379 * 380 * @param selection the collection of selected primitives; may be empty, but not null 381 * 382 * @see #updateEnabledState() 383 * @see #initEnabledState() 384 * @see #listenToSelectionChange() 385 */ 386 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 387 } 388 389 /** 390 * Updates enabled state according to primitives currently selected in edit data set, if any. 391 * Can be called in {@link #updateEnabledState()} implementations. 392 * @see #updateEnabledStateOnCurrentSelection(boolean) 393 * @since 10409 394 */ 395 protected final void updateEnabledStateOnCurrentSelection() { 396 updateEnabledStateOnCurrentSelection(false); 397 } 398 399 /** 400 * Updates enabled state according to primitives currently selected in active data set, if any. 401 * Can be called in {@link #updateEnabledState()} implementations. 402 * @param allowReadOnly if {@code true}, read-only data sets are considered 403 * @since 13434 404 */ 405 protected final void updateEnabledStateOnCurrentSelection(boolean allowReadOnly) { 406 DataSet ds = getLayerManager().getActiveDataSet(); 407 if (ds != null && (allowReadOnly || !ds.isLocked())) { 408 updateEnabledState(ds.getSelected()); 409 } else { 410 setEnabled(false); 411 } 412 } 413 414 /** 415 * Updates enabled state according to selected primitives, if any. 416 * Enables action if the collection is not empty and references primitives in a modifiable data layer. 417 * Can be called in {@link #updateEnabledState(Collection)} implementations. 418 * @param selection the collection of selected primitives 419 * @since 13434 420 */ 421 protected final void updateEnabledStateOnModifiableSelection(Collection<? extends OsmPrimitive> selection) { 422 setEnabled(OsmUtils.isOsmCollectionEditable(selection)); 423 } 424 425 /** 426 * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed. 427 */ 428 protected class LayerChangeAdapter implements LayerChangeListener { 429 @Override 430 public void layerAdded(LayerAddEvent e) { 431 updateEnabledState(); 432 } 433 434 @Override 435 public void layerRemoving(LayerRemoveEvent e) { 436 updateEnabledState(); 437 } 438 439 @Override 440 public void layerOrderChanged(LayerOrderChangeEvent e) { 441 updateEnabledState(); 442 } 443 444 @Override 445 public String toString() { 446 return "LayerChangeAdapter [" + JosmAction.this + ']'; 447 } 448 } 449 450 /** 451 * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed. 452 */ 453 protected class ActiveLayerChangeAdapter implements ActiveLayerChangeListener { 454 @Override 455 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 456 updateEnabledState(); 457 } 458 459 @Override 460 public String toString() { 461 return "ActiveLayerChangeAdapter [" + JosmAction.this + ']'; 462 } 463 } 464 465 /** 466 * Adapter for selection change events. Runs updateEnabledState() whenever the selection changed. 467 */ 468 protected class SelectionChangeAdapter implements DataSelectionListener { 469 @Override 470 public void selectionChanged(SelectionChangeEvent event) { 471 updateEnabledState(event.getSelection()); 472 } 473 474 @Override 475 public String toString() { 476 return "SelectionChangeAdapter [" + JosmAction.this + ']'; 477 } 478 } 479 480 /** 481 * Check whether user is about to operate on data outside of the download area. 482 * Request confirmation if he is. 483 * 484 * @param operation the operation name which is used for setting some preferences 485 * @param dialogTitle the title of the dialog being displayed 486 * @param outsideDialogMessage the message text to be displayed when data is outside of the download area 487 * @param incompleteDialogMessage the message text to be displayed when data is incomplete 488 * @param primitives the primitives to operate on 489 * @param ignore {@code null} or a primitive to be ignored 490 * @return true, if operating on outlying primitives is OK; false, otherwise 491 * @since 12749 (moved from Command) 492 */ 493 public static boolean checkAndConfirmOutlyingOperation(String operation, 494 String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage, 495 Collection<? extends OsmPrimitive> primitives, 496 Collection<? extends OsmPrimitive> ignore) { 497 int checkRes = Command.checkOutlyingOrIncompleteOperation(primitives, ignore); 498 if ((checkRes & Command.IS_OUTSIDE) != 0) { 499 JPanel msg = new JPanel(new GridBagLayout()); 500 msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>")); 501 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 502 operation + "_outside_nodes", 503 MainApplication.getMainFrame(), 504 msg, 505 dialogTitle, 506 JOptionPane.YES_NO_OPTION, 507 JOptionPane.QUESTION_MESSAGE, 508 JOptionPane.YES_OPTION); 509 if (!answer) 510 return false; 511 } 512 if ((checkRes & Command.IS_INCOMPLETE) != 0) { 513 JPanel msg = new JPanel(new GridBagLayout()); 514 msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>")); 515 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 516 operation + "_incomplete", 517 MainApplication.getMainFrame(), 518 msg, 519 dialogTitle, 520 JOptionPane.YES_NO_OPTION, 521 JOptionPane.QUESTION_MESSAGE, 522 JOptionPane.YES_OPTION); 523 if (!answer) 524 return false; 525 } 526 return true; 527 } 528}