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