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