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

Last change on this file since 14693 was 14690, checked in by simon04, 5 years ago

see #17202 - Use Shortcut#setTooltip to fix deprecations

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