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