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

Last change on this file since 18755 was 18181, checked in by Don-vip, 3 years ago

fix #4504 - display notification when an outlying operation is ignored due to saved preference

  • Property svn:eol-style set to native
File size: 22.8 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.Notification;
27import org.openstreetmap.josm.gui.help.HelpUtil;
28import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
29import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
30import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
31import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
32import org.openstreetmap.josm.gui.layer.MainLayerManager;
33import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
34import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
35import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
36import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
37import org.openstreetmap.josm.tools.Destroyable;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.ImageResource;
40import org.openstreetmap.josm.tools.Logging;
41import org.openstreetmap.josm.tools.Shortcut;
42
43/**
44 * Base class helper for all Actions in JOSM. Just to make the life easier.
45 *
46 * 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
47 * layer/selection listeners that call {@link #updateEnabledState()} whenever the global context is changed.
48 *
49 * A JosmAction can register a {@link LayerChangeListener} and a {@link DataSelectionListener}. Upon
50 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}.
51 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state
52 * of a JosmAction depending on the {@link #getLayerManager()} state.
53 *
54 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has
55 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never
56 * be called (currently).
57 *
58 * @author imi
59 */
60public abstract class JosmAction extends AbstractAction implements Destroyable {
61
62 protected transient Shortcut sc;
63 private transient LayerChangeAdapter layerChangeAdapter;
64 private transient ActiveLayerChangeAdapter activeLayerChangeAdapter;
65 private transient SelectionChangeAdapter selectionChangeAdapter;
66
67 /**
68 * Constructs a {@code JosmAction}.
69 *
70 * @param name the action's text as displayed on the menu (if it is added to a menu)
71 * @param icon the icon to use
72 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note
73 * that html is not supported for menu actions on some platforms.
74 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
75 * do want a shortcut, remember you can always register it with group=none, so you
76 * won't be assigned a shortcut unless the user configures one. If you pass null here,
77 * the user CANNOT configure a shortcut for your action.
78 * @param registerInToolbar register this action for the toolbar preferences?
79 * @param toolbarId identifier for the toolbar preferences
80 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
81 */
82 protected JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar,
83 String toolbarId, boolean installAdapters) {
84 super(name);
85 if (icon != null) {
86 ImageResource resource = icon.getResource();
87 if (resource != null) {
88 try {
89 resource.attachImageIcon(this, true);
90 } catch (RuntimeException e) {
91 Logging.warn("Unable to attach image icon {0} for action {1}", icon, name);
92 Logging.error(e);
93 }
94 }
95 }
96 setHelpId();
97 sc = shortcut;
98 if (sc != null && !sc.isAutomatic()) {
99 MainApplication.registerActionShortcut(this, sc);
100 }
101 setTooltip(tooltip);
102 if (getValue("toolbar") == null) {
103 setToolbarId(toolbarId);
104 }
105 if (registerInToolbar && MainApplication.getToolbar() != null) {
106 MainApplication.getToolbar().register(this);
107 }
108 if (installAdapters) {
109 installAdapters();
110 }
111 }
112
113 /**
114 * The new super for all actions.
115 *
116 * Use this super constructor to setup your action.
117 *
118 * @param name the action's text as displayed on the menu (if it is added to a menu)
119 * @param iconName the filename of the icon to use
120 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note
121 * that html is not supported for menu actions on some platforms.
122 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
123 * do want a shortcut, remember you can always register it with group=none, so you
124 * won't be assigned a shortcut unless the user configures one. If you pass null here,
125 * the user CANNOT configure a shortcut for your action.
126 * @param registerInToolbar register this action for the toolbar preferences?
127 * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
128 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
129 */
130 protected JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar,
131 String toolbarId, boolean installAdapters) {
132 this(name, iconName == null ? null : new ImageProvider(iconName).setOptional(true), tooltip, shortcut, registerInToolbar,
133 toolbarId == null ? iconName : toolbarId, installAdapters);
134 }
135
136 /**
137 * Constructs a new {@code JosmAction}.
138 *
139 * Use this super constructor to setup your action.
140 *
141 * @param name the action's text as displayed on the menu (if it is added to a menu)
142 * @param iconName the filename of the icon to use
143 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note
144 * that html is not supported for menu actions on some platforms.
145 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
146 * do want a shortcut, remember you can always register it with group=none, so you
147 * won't be assigned a shortcut unless the user configures one. If you pass null here,
148 * the user CANNOT configure a shortcut for your action.
149 * @param registerInToolbar register this action for the toolbar preferences?
150 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
151 */
152 protected JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, boolean installAdapters) {
153 this(name, iconName, tooltip, shortcut, registerInToolbar, null, installAdapters);
154 }
155
156 /**
157 * Constructs a new {@code JosmAction} and installs layer changed and selection changed adapters.
158 *
159 * Use this super constructor to setup your action.
160 *
161 * @param name the action's text as displayed on the menu (if it is added to a menu)
162 * @param iconName the filename of the icon to use
163 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note
164 * that html is not supported for menu actions on some platforms.
165 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
166 * do want a shortcut, remember you can always register it with group=none, so you
167 * won't be assigned a shortcut unless the user configures one. If you pass null here,
168 * the user CANNOT configure a shortcut for your action.
169 * @param registerInToolbar register this action for the toolbar preferences?
170 */
171 protected JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) {
172 this(name, iconName, tooltip, shortcut, registerInToolbar, null, true);
173 }
174
175 /**
176 * Constructs a new {@code JosmAction}.
177 */
178 protected JosmAction() {
179 this(true);
180 }
181
182 /**
183 * Constructs a new {@code JosmAction}.
184 *
185 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
186 */
187 protected JosmAction(boolean installAdapters) {
188 setHelpId();
189 if (installAdapters) {
190 installAdapters();
191 }
192 }
193
194 /**
195 * Constructs a new {@code JosmAction}.
196 *
197 * Use this super constructor to setup your action.
198 *
199 * @param name the action's text as displayed on the menu (if it is added to a menu)
200 * @param iconName the filename of the icon to use
201 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note
202 * that html is not supported for menu actions on some platforms.
203 * @param shortcuts ready-created shortcut objects
204 * @since 14012
205 */
206 protected JosmAction(String name, String iconName, String tooltip, List<Shortcut> shortcuts) {
207 this(name, iconName, tooltip, shortcuts.get(0), true, null, true);
208 for (int i = 1; i < shortcuts.size(); i++) {
209 MainApplication.registerActionShortcut(this, shortcuts.get(i));
210 }
211 }
212
213 /**
214 * Installs the listeners to this action.
215 * <p>
216 * This should either never be called or only called in the constructor of this action.
217 * <p>
218 * All registered adapters should be removed in {@link #destroy()}
219 */
220 protected void installAdapters() {
221 // make this action listen to layer change and selection change events
222 if (listenToLayerChange()) {
223 layerChangeAdapter = buildLayerChangeAdapter();
224 activeLayerChangeAdapter = buildActiveLayerChangeAdapter();
225 getLayerManager().addLayerChangeListener(layerChangeAdapter);
226 getLayerManager().addActiveLayerChangeListener(activeLayerChangeAdapter);
227 }
228 if (listenToSelectionChange()) {
229 selectionChangeAdapter = new SelectionChangeAdapter();
230 SelectionEventManager.getInstance().addSelectionListenerForEdt(selectionChangeAdapter);
231 }
232 initEnabledState();
233 }
234
235 /**
236 * Override this if calling {@link #updateEnabledState()} on layer change events is not enough.
237 * @return the {@link LayerChangeAdapter} that will be called on layer change events
238 * @since 15404
239 */
240 protected LayerChangeAdapter buildLayerChangeAdapter() {
241 return new LayerChangeAdapter();
242 }
243
244 /**
245 * Override this if calling {@link #updateEnabledState()} on active layer change event is not enough.
246 * @return the {@link LayerChangeAdapter} that will be called on active layer change event
247 * @since 15404
248 */
249 protected ActiveLayerChangeAdapter buildActiveLayerChangeAdapter() {
250 return new ActiveLayerChangeAdapter();
251 }
252
253 /**
254 * Overwrite this if {@link #updateEnabledState()} should be called when the active / available layers change. Default is true.
255 * @return <code>true</code> if a {@link LayerChangeListener} and a {@link ActiveLayerChangeListener} should be registered.
256 * @since 10353
257 */
258 protected boolean listenToLayerChange() {
259 return true;
260 }
261
262 /**
263 * Overwrite this if {@link #updateEnabledState()} should be called when the selection changed. Default is true.
264 * @return <code>true</code> if a {@link DataSelectionListener} should be registered.
265 * @since 10353
266 */
267 protected boolean listenToSelectionChange() {
268 return true;
269 }
270
271 @Override
272 public void destroy() {
273 if (sc != null && !sc.isAutomatic()) {
274 MainApplication.unregisterActionShortcut(this);
275 }
276 if (layerChangeAdapter != null) {
277 getLayerManager().removeLayerChangeListener(layerChangeAdapter);
278 getLayerManager().removeActiveLayerChangeListener(activeLayerChangeAdapter);
279 }
280 if (selectionChangeAdapter != null) {
281 SelectionEventManager.getInstance().removeSelectionListener(selectionChangeAdapter);
282 }
283 if (MainApplication.getToolbar() != null) {
284 MainApplication.getToolbar().unregister(this);
285 }
286 }
287
288 private void setHelpId() {
289 String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1);
290 if (helpId.endsWith("Action")) {
291 helpId = helpId.substring(0, helpId.length()-6);
292 }
293 setHelpId(helpId);
294 }
295
296 /**
297 * Sets the help topic id.
298 * @param helpId help topic id (result of {@link HelpUtil#ht})
299 * @since 14397
300 */
301 protected void setHelpId(String helpId) {
302 putValue("help", helpId);
303 }
304
305 /**
306 * Sets the toolbar id.
307 * @param toolbarId toolbar id
308 * @since 16138
309 */
310 protected void setToolbarId(String toolbarId) {
311 putValue("toolbar", toolbarId);
312 }
313
314 /**
315 * Returns the shortcut for this action.
316 * @return the shortcut for this action, or "No shortcut" if none is defined
317 */
318 public Shortcut getShortcut() {
319 if (sc == null) {
320 sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
321 // as this shortcut is shared by all action that don't want to have a shortcut,
322 // we shouldn't allow the user to change it...
323 // this is handled by special name "core:none"
324 }
325 return sc;
326 }
327
328 /**
329 * Sets the tooltip text of this action.
330 * @param tooltip The text to display in tooltip. Can be {@code null}
331 */
332 public final void setTooltip(String tooltip) {
333 if (tooltip != null && sc != null) {
334 sc.setTooltip(this, tooltip);
335 } else if (tooltip != null) {
336 putValue(SHORT_DESCRIPTION, tooltip);
337 }
338 }
339
340 /**
341 * Gets the layer manager used for this action. Defaults to the main layer manager but you can overwrite this.
342 * <p>
343 * The layer manager must be available when {@link #installAdapters()} is called and must not change.
344 *
345 * @return The layer manager.
346 * @since 10353
347 */
348 public MainLayerManager getLayerManager() {
349 return MainApplication.getLayerManager();
350 }
351
352 protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) {
353 MainApplication.worker.submit(() -> {
354 try {
355 future.get();
356 } catch (InterruptedException | ExecutionException | CancellationException e) {
357 Logging.error(e);
358 return;
359 }
360 monitor.close();
361 });
362 }
363
364 /**
365 * Override in subclasses to init the enabled state of an action when it is
366 * created. Default behaviour is to call {@link #updateEnabledState()}
367 *
368 * @see #updateEnabledState()
369 * @see #updateEnabledState(Collection)
370 */
371 protected void initEnabledState() {
372 updateEnabledState();
373 }
374
375 /**
376 * Override in subclasses to update the enabled state of the action when
377 * something in the JOSM state changes, i.e. when a layer is removed or added.
378 *
379 * See {@link #updateEnabledState(Collection)} to respond to changes in the collection
380 * of selected primitives.
381 *
382 * Default behavior is empty.
383 *
384 * @see #updateEnabledState(Collection)
385 * @see #initEnabledState()
386 * @see #listenToLayerChange()
387 */
388 protected void updateEnabledState() {
389 }
390
391 /**
392 * Override in subclasses to update the enabled state of the action if the
393 * collection of selected primitives changes. This method is called with the
394 * new selection.
395 *
396 * @param selection the collection of selected primitives; may be empty, but not null
397 *
398 * @see #updateEnabledState()
399 * @see #initEnabledState()
400 * @see #listenToSelectionChange()
401 */
402 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
403 }
404
405 /**
406 * Updates enabled state according to primitives currently selected in edit data set, if any.
407 * Can be called in {@link #updateEnabledState()} implementations.
408 * @see #updateEnabledStateOnCurrentSelection(boolean)
409 * @since 10409
410 */
411 protected final void updateEnabledStateOnCurrentSelection() {
412 updateEnabledStateOnCurrentSelection(false);
413 }
414
415 /**
416 * Updates enabled state according to primitives currently selected in active data set, if any.
417 * Can be called in {@link #updateEnabledState()} implementations.
418 * @param allowReadOnly if {@code true}, read-only data sets are considered
419 * @since 13434
420 */
421 protected final void updateEnabledStateOnCurrentSelection(boolean allowReadOnly) {
422 DataSet ds = getLayerManager().getActiveDataSet();
423 if (ds != null && (allowReadOnly || !ds.isLocked())) {
424 updateEnabledState(ds.getSelected());
425 } else {
426 setEnabled(false);
427 }
428 }
429
430 /**
431 * Updates enabled state according to selected primitives, if any.
432 * Enables action if the collection is not empty and references primitives in a modifiable data layer.
433 * Can be called in {@link #updateEnabledState(Collection)} implementations.
434 * @param selection the collection of selected primitives
435 * @since 13434
436 */
437 protected final void updateEnabledStateOnModifiableSelection(Collection<? extends OsmPrimitive> selection) {
438 setEnabled(OsmUtils.isOsmCollectionEditable(selection));
439 }
440
441 /**
442 * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed.
443 */
444 protected class LayerChangeAdapter implements LayerChangeListener {
445 @Override
446 public void layerAdded(LayerAddEvent e) {
447 updateEnabledState();
448 }
449
450 @Override
451 public void layerRemoving(LayerRemoveEvent e) {
452 updateEnabledState();
453 }
454
455 @Override
456 public void layerOrderChanged(LayerOrderChangeEvent e) {
457 updateEnabledState();
458 }
459
460 @Override
461 public String toString() {
462 return "LayerChangeAdapter [" + JosmAction.this + ']';
463 }
464 }
465
466 /**
467 * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed.
468 */
469 protected class ActiveLayerChangeAdapter implements ActiveLayerChangeListener {
470 @Override
471 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
472 updateEnabledState();
473 }
474
475 @Override
476 public String toString() {
477 return "ActiveLayerChangeAdapter [" + JosmAction.this + ']';
478 }
479 }
480
481 /**
482 * Adapter for selection change events. Runs updateEnabledState() whenever the selection changed.
483 */
484 protected class SelectionChangeAdapter implements DataSelectionListener {
485 @Override
486 public void selectionChanged(SelectionChangeEvent event) {
487 updateEnabledState(event.getSelection());
488 }
489
490 @Override
491 public String toString() {
492 return "SelectionChangeAdapter [" + JosmAction.this + ']';
493 }
494 }
495
496 /**
497 * Check whether user is about to operate on data outside of the download area.
498 * Request confirmation if he is.
499 * Also handles the case that there is no download area.
500 *
501 * @param operation the operation name which is used for setting some preferences
502 * @param dialogTitle the title of the dialog being displayed
503 * @param outsideDialogMessage the message text to be displayed when data is outside of the download area or no download area exists
504 * @param incompleteDialogMessage the message text to be displayed when data is incomplete
505 * @param primitives the primitives to operate on
506 * @param ignore {@code null} or a primitive to be ignored
507 * @return true, if operating on outlying primitives is OK; false, otherwise
508 * @since 12749 (moved from Command)
509 */
510 public static boolean checkAndConfirmOutlyingOperation(String operation,
511 String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage,
512 Collection<? extends OsmPrimitive> primitives,
513 Collection<? extends OsmPrimitive> ignore) {
514 int checkRes = Command.checkOutlyingOrIncompleteOperation(primitives, ignore);
515 if ((checkRes & Command.IS_OUTSIDE) != 0) {
516 boolean answer = showConfirmOutlyingOperationDialog(operation + "_outside_nodes", outsideDialogMessage, dialogTitle);
517 if (!answer)
518 return false;
519 }
520 if ((checkRes & Command.IS_INCOMPLETE) != 0) {
521 boolean answer = showConfirmOutlyingOperationDialog(operation + "_incomplete", incompleteDialogMessage, dialogTitle);
522 if (!answer)
523 return false;
524 }
525 return true;
526 }
527
528 private static boolean showConfirmOutlyingOperationDialog(String preferenceKey, String dialogMessage, String dialogTitle) {
529 JPanel msg = new JPanel(new GridBagLayout());
530 msg.add(new JMultilineLabel("<html>" + dialogMessage + "</html>"));
531 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
532 preferenceKey,
533 MainApplication.getMainFrame(),
534 msg,
535 dialogTitle,
536 JOptionPane.YES_NO_OPTION,
537 JOptionPane.QUESTION_MESSAGE,
538 JOptionPane.YES_OPTION);
539 if (!answer && JOptionPane.NO_OPTION == ConditionalOptionPaneUtil.getDialogReturnValue(preferenceKey)) {
540 String message = tr("Operation was not performed, as per {0} preference", preferenceKey);
541 new Notification(message).show();
542 Logging.info(message);
543 }
544 return answer;
545 }
546}
Note: See TracBrowser for help on using the repository browser.