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