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 = ds.getSelected();
110        ds.beginUpdate();
111        try {
112            for (int i = 1; i <= num; ++i) {
113                final Command c = commands.removeLast();
114                c.undoCommand();
115                c.invalidateAffectedLayers();
116                redoCommands.addFirst(c);
117                if (commands.isEmpty()) {
118                    break;
119                }
120            }
121        } finally {
122            ds.endUpdate();
123        }
124        fireCommandsChanged();
125        fireIfSelectionChanged(ds, oldSelection);
126    }
127
128    /**
129     * Redoes the last undoed command.
130     */
131    public void redo() {
132        redo(1);
133    }
134
135    /**
136     * Redoes multiple commands.
137     * @param num The number of commands to redo
138     */
139    public void redo(int num) {
140        if (redoCommands.isEmpty())
141            return;
142        DataSet ds = Main.getLayerManager().getEditDataSet();
143        Collection<? extends OsmPrimitive> oldSelection = ds.getSelected();
144        for (int i = 0; i < num; ++i) {
145            final Command c = redoCommands.removeFirst();
146            c.executeCommand();
147            c.invalidateAffectedLayers();
148            commands.add(c);
149            if (redoCommands.isEmpty()) {
150                break;
151            }
152        }
153        fireCommandsChanged();
154        fireIfSelectionChanged(ds, oldSelection);
155    }
156
157    private static void fireIfSelectionChanged(DataSet ds, Collection<? extends OsmPrimitive> oldSelection) {
158        Collection<? extends OsmPrimitive> newSelection = ds.getSelected();
159        if (!oldSelection.equals(newSelection)) {
160            ds.fireSelectionChanged();
161        }
162    }
163
164    /**
165     * Fires a command change to all listeners.
166     */
167    private void fireCommandsChanged() {
168        for (final CommandQueueListener l : listenerCommands) {
169            l.commandChanged(commands.size(), redoCommands.size());
170        }
171    }
172
173    /**
174     * Resets the undo/redo list.
175     */
176    public void clean() {
177        redoCommands.clear();
178        commands.clear();
179        fireCommandsChanged();
180    }
181
182    /**
183     * Resets all commands that affect the given layer.
184     * @param layer The layer that was affected.
185     */
186    public void clean(Layer layer) {
187        if (layer == null)
188            return;
189        boolean changed = false;
190        for (Iterator<Command> it = commands.iterator(); it.hasNext();) {
191            if (it.next().invalidBecauselayerRemoved(layer)) {
192                it.remove();
193                changed = true;
194            }
195        }
196        for (Iterator<Command> it = redoCommands.iterator(); it.hasNext();) {
197            if (it.next().invalidBecauselayerRemoved(layer)) {
198                it.remove();
199                changed = true;
200            }
201        }
202        if (changed) {
203            fireCommandsChanged();
204        }
205    }
206
207    @Override
208    public void layerRemoving(LayerRemoveEvent e) {
209        clean(e.getRemovedLayer());
210    }
211
212    @Override
213    public void layerAdded(LayerAddEvent e) {
214        // Do nothing
215    }
216
217    @Override
218    public void layerOrderChanged(LayerOrderChangeEvent e) {
219        // Do nothing
220    }
221
222    /**
223     * Removes a command queue listener.
224     * @param l The command queue listener to remove
225     */
226    public void removeCommandQueueListener(CommandQueueListener l) {
227        listenerCommands.remove(l);
228    }
229
230    /**
231     * Adds a command queue listener.
232     * @param l The commands queue listener to add
233     * @return {@code true} if the listener has been added, {@code false} otherwise
234     */
235    public boolean addCommandQueueListener(CommandQueueListener l) {
236        return listenerCommands.add(l);
237    }
238}