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

Last change on this file since 13453 was 13453, checked in by Don-vip, 10 months ago

fix #8039, fix #10456: final fixes for the read-only/locked layers:

  • rename "read-only" to "locked" (in XML and Java classes/interfaces)
  • add a new download policy (true/never) to allow private layers forbidding only to download data, but allowing everything else

This leads to:

  • normal layers: download allowed, modifications allowed, upload allowed
  • private layers: download allowed or not (download=true/never), modifications allowed, upload allowed or not (upload=true/discouraged/never)
  • locked layers: nothing allowed, the data cannot be modified in any way
  • Property svn:eol-style set to native
File size: 20.1 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.event.DatasetEventManager.FireMode;
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.Logging;
38import org.openstreetmap.josm.tools.Shortcut;
39
40/**
41 * Base class helper for all Actions in JOSM. Just to make the life easier.
42 *
43 * 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
44 * layer/selection listeners that call {@link #updateEnabledState()} whenever the global context is changed.
45 *
46 * A JosmAction can register a {@link LayerChangeListener} and a {@link SelectionChangedListener}. Upon
47 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}.
48 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state
49 * of a JosmAction depending on the {@link #getLayerManager()} state.
50 *
51 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has
52 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never
53 * be called (currently).
54 *
55 * @author imi
56 */
57public abstract class JosmAction extends AbstractAction implements Destroyable {
58
59    protected transient Shortcut sc;
60    private transient LayerChangeAdapter layerChangeAdapter;
61    private transient ActiveLayerChangeAdapter activeLayerChangeAdapter;
62    private transient SelectionChangeAdapter selectionChangeAdapter;
63
64    /**
65     * Constructs a {@code JosmAction}.
66     *
67     * @param name the action's text as displayed on the menu (if it is added to a menu)
68     * @param icon the icon to use
69     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
70     *           that html is not supported for menu actions on some platforms.
71     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
72     *            do want a shortcut, remember you can always register it with group=none, so you
73     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
74     *            the user CANNOT configure a shortcut for your action.
75     * @param registerInToolbar register this action for the toolbar preferences?
76     * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
77     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
78     */
79    public JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar,
80            String toolbarId, boolean installAdapters) {
81        super(name);
82        if (icon != null)
83            icon.getResource().attachImageIcon(this, true);
84        setHelpId();
85        sc = shortcut;
86        if (sc != null && !sc.isAutomatic()) {
87            MainApplication.registerActionShortcut(this, sc);
88        }
89        setTooltip(tooltip);
90        if (getValue("toolbar") == null) {
91            putValue("toolbar", toolbarId);
92        }
93        if (registerInToolbar && MainApplication.getToolbar() != null) {
94            MainApplication.getToolbar().register(this);
95        }
96        if (installAdapters) {
97            installAdapters();
98        }
99    }
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 colleciton 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}
Note: See TracBrowser for help on using the repository browser.