001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data;
003
004import java.util.Collection;
005import java.util.Iterator;
006import java.util.LinkedList;
007
008import org.openstreetmap.josm.Main;
009import org.openstreetmap.josm.command.Command;
010import org.openstreetmap.josm.data.osm.DataSet;
011import org.openstreetmap.josm.data.osm.OsmPrimitive;
012import org.openstreetmap.josm.gui.layer.Layer;
013import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
014import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
015import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
016import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
017import org.openstreetmap.josm.gui.layer.OsmDataLayer;
018import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
019import org.openstreetmap.josm.tools.CheckParameterUtil;
020
021/**
022 * This is the global undo/redo handler for all {@link OsmDataLayer}s.
023 * <p>
024 * If you want to change a data layer, you can use {@link #add(Command)} to execute a command on it and make that command undoable.
025 */
026public class UndoRedoHandler implements LayerChangeListener {
027
028    /**
029     * All commands that were made on the dataset. Don't write from outside!
030     */
031    public final LinkedList<Command> commands = new LinkedList<>();
032    /**
033     * The stack for redoing commands
034     */
035    public final LinkedList<Command> redoCommands = new LinkedList<>();
036
037    private final LinkedList<CommandQueueListener> listenerCommands = new LinkedList<>();
038
039    /**
040     * Constructs a new {@code UndoRedoHandler}.
041     */
042    public UndoRedoHandler() {
043        Main.getLayerManager().addLayerChangeListener(this);
044    }
045
046    /**
047     * Executes the command and add it to the intern command queue.
048     * @param c The command to execute. Must not be {@code null}.
049     */
050    public void addNoRedraw(final Command c) {
051        CheckParameterUtil.ensureParameterNotNull(c, "c");
052        c.executeCommand();
053        c.invalidateAffectedLayers();
054        commands.add(c);
055        // Limit the number of commands in the undo list.
056        // Currently you have to undo the commands one by one. If
057        // this changes, a higher default value may be reasonable.
058        if (commands.size() > Main.pref.getInteger("undo.max", 1000)) {
059            commands.removeFirst();
060        }
061        redoCommands.clear();
062    }
063
064    /**
065     * Fires a commands change event after adding a command.
066     */
067    public void afterAdd() {
068        fireCommandsChanged();
069    }
070
071    /**
072     * Executes the command and add it to the intern command queue.
073     * @param c The command to execute. Must not be {@code null}.
074     */
075    public synchronized void add(final Command c) {
076        DataSet ds = c.getAffectedDataSet();
077        if (ds == null) {
078            // old, legacy behaviour
079            ds = Main.getLayerManager().getEditDataSet();
080        }
081        Collection<? extends OsmPrimitive> oldSelection = null;
082        if (ds != null) {
083            oldSelection = ds.getSelected();
084        }
085        addNoRedraw(c);
086        afterAdd();
087
088        // the command may have changed the selection so tell the listeners about the current situation
089        if (ds != null) {
090            fireIfSelectionChanged(ds, oldSelection);
091        }
092    }
093
094    /**
095     * Undoes the last added command.
096     */
097    public void undo() {
098        undo(1);
099    }
100
101    /**
102     * Undoes multiple commands.
103     * @param num The number of commands to undo
104     */
105    public synchronized void undo(int num) {
106        if (commands.isEmpty())
107            return;
108        DataSet ds = Main.getLayerManager().getEditDataSet();
109        Collection<? extends OsmPrimitive> oldSelection = null;
110        if (ds != null) {
111            oldSelection = ds.getSelected();
112            ds.beginUpdate();
113        }
114        try {
115            for (int i = 1; i <= num; ++i) {
116                final Command c = commands.removeLast();
117                c.undoCommand();
118                c.invalidateAffectedLayers();
119                redoCommands.addFirst(c);
120                if (commands.isEmpty()) {
121                    break;
122                }
123            }
124        } finally {
125            if (ds != null) {
126                ds.endUpdate();
127            }
128        }
129        fireCommandsChanged();
130        if (ds != null) {
131            fireIfSelectionChanged(ds, oldSelection);
132        }
133    }
134
135    /**
136     * Redoes the last undoed command.
137     */
138    public void redo() {
139        redo(1);
140    }
141
142    /**
143     * Redoes multiple commands.
144     * @param num The number of commands to redo
145     */
146    public void redo(int num) {
147        if (redoCommands.isEmpty())
148            return;
149        DataSet ds = Main.getLayerManager().getEditDataSet();
150        Collection<? extends OsmPrimitive> oldSelection = ds.getSelected();
151        for (int i = 0; i < num; ++i) {
152            final Command c = redoCommands.removeFirst();
153            c.executeCommand();
154            c.invalidateAffectedLayers();
155            commands.add(c);
156            if (redoCommands.isEmpty()) {
157                break;
158            }
159        }
160        fireCommandsChanged();
161        fireIfSelectionChanged(ds, oldSelection);
162    }
163
164    private static void fireIfSelectionChanged(DataSet ds, Collection<? extends OsmPrimitive> oldSelection) {
165        Collection<? extends OsmPrimitive> newSelection = ds.getSelected();
166        if (!oldSelection.equals(newSelection)) {
167            ds.fireSelectionChanged();
168        }
169    }
170
171    /**
172     * Fires a command change to all listeners.
173     */
174    private void fireCommandsChanged() {
175        for (final CommandQueueListener l : listenerCommands) {
176            l.commandChanged(commands.size(), redoCommands.size());
177        }
178    }
179
180    /**
181     * Resets the undo/redo list.
182     */
183    public void clean() {
184        redoCommands.clear();
185        commands.clear();
186        fireCommandsChanged();
187    }
188
189    /**
190     * Resets all commands that affect the given layer.
191     * @param layer The layer that was affected.
192     */
193    public void clean(Layer layer) {
194        if (layer == null)
195            return;
196        boolean changed = false;
197        for (Iterator<Command> it = commands.iterator(); it.hasNext();) {
198            if (it.next().invalidBecauselayerRemoved(layer)) {
199                it.remove();
200                changed = true;
201            }
202        }
203        for (Iterator<Command> it = redoCommands.iterator(); it.hasNext();) {
204            if (it.next().invalidBecauselayerRemoved(layer)) {
205                it.remove();
206                changed = true;
207            }
208        }
209        if (changed) {
210            fireCommandsChanged();
211        }
212    }
213
214    @Override
215    public void layerRemoving(LayerRemoveEvent e) {
216        clean(e.getRemovedLayer());
217    }
218
219    @Override
220    public void layerAdded(LayerAddEvent e) {
221        // Do nothing
222    }
223
224    @Override
225    public void layerOrderChanged(LayerOrderChangeEvent e) {
226        // Do nothing
227    }
228
229    /**
230     * Removes a command queue listener.
231     * @param l The command queue listener to remove
232     */
233    public void removeCommandQueueListener(CommandQueueListener l) {
234        listenerCommands.remove(l);
235    }
236
237    /**
238     * Adds a command queue listener.
239     * @param l The commands queue listener to add
240     * @return {@code true} if the listener has been added, {@code false} otherwise
241     */
242    public boolean addCommandQueueListener(CommandQueueListener l) {
243        return listenerCommands.add(l);
244    }
245}