source: josm/trunk/src/org/openstreetmap/josm/actions/JosmAction.java@ 13731

Last change on this file since 13731 was 13647, checked in by Don-vip, 6 years ago

see #16204 - Allow to start and close JOSM in WebStart sandbox mode (where every external access is denied). This was very useful to reproduce some very tricky bugs that occured in real life but were almost impossible to diagnose.

  • Property svn:eol-style set to native
File size: 20.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GridBagLayout;
7import java.awt.event.KeyEvent;
8import java.util.Collection;
9import java.util.concurrent.CancellationException;
10import java.util.concurrent.ExecutionException;
11import java.util.concurrent.Future;
12
13import javax.swing.AbstractAction;
14import javax.swing.JOptionPane;
15import javax.swing.JPanel;
16
17import org.openstreetmap.josm.Main;
18import org.openstreetmap.josm.command.Command;
19import org.openstreetmap.josm.data.SelectionChangedListener;
20import org.openstreetmap.josm.data.osm.DataSet;
21import org.openstreetmap.josm.data.osm.OsmPrimitive;
22import org.openstreetmap.josm.data.osm.OsmUtils;
23import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
24import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
25import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
26import org.openstreetmap.josm.gui.MainApplication;
27import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
28import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
29import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
30import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
31import org.openstreetmap.josm.gui.layer.MainLayerManager;
32import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
33import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
34import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
35import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
36import org.openstreetmap.josm.tools.Destroyable;
37import org.openstreetmap.josm.tools.ImageProvider;
38import org.openstreetmap.josm.tools.ImageResource;
39import org.openstreetmap.josm.tools.Logging;
40import org.openstreetmap.josm.tools.Shortcut;
41
42/**
43 * Base class helper for all Actions in JOSM. Just to make the life easier.
44 *
45 * 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
46 * layer/selection listeners that call {@link #updateEnabledState()} whenever the global context is changed.
47 *
48 * A JosmAction can register a {@link LayerChangeListener} and a {@link SelectionChangedListener}. Upon
49 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}.
50 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state
51 * of a JosmAction depending on the {@link #getLayerManager()} state.
52 *
53 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has
54 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never
55 * be called (currently).
56 *
57 * @author imi
58 */
59public abstract class JosmAction extends AbstractAction implements Destroyable {
60
61 protected transient Shortcut sc;
62 private transient LayerChangeAdapter layerChangeAdapter;
63 private transient ActiveLayerChangeAdapter activeLayerChangeAdapter;
64 private transient SelectionChangeAdapter selectionChangeAdapter;
65
66 /**
67 * Constructs a {@code JosmAction}.
68 *
69 * @param name the action's text as displayed on the menu (if it is added to a menu)
70 * @param icon the icon to use
71 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note
72 * that html is not supported for menu actions on some platforms.
73 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
74 * do want a shortcut, remember you can always register it with group=none, so you
75 * won't be assigned a shortcut unless the user configures one. If you pass null here,
76 * the user CANNOT configure a shortcut for your action.
77 * @param registerInToolbar register this action for the toolbar preferences?
78 * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
79 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
80 */
81 public JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar,
82 String toolbarId, boolean installAdapters) {
83 super(name);
84 if (icon != null) {
85 ImageResource resource = icon.getResource();
86 if (resource != null) {
87 resource.attachImageIcon(this, true);
88 }
89 }
90 setHelpId();
91 sc = shortcut;
92 if (sc != null && !sc.isAutomatic()) {
93 MainApplication.registerActionShortcut(this, sc);
94 }
95 setTooltip(tooltip);
96 if (getValue("toolbar") == null) {
97 putValue("toolbar", toolbarId);
98 }
99 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}
Note: See TracBrowser for help on using the repository browser.