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