001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data;
003
004import java.util.EventObject;
005import java.util.Iterator;
006import java.util.LinkedList;
007import java.util.Objects;
008
009import org.openstreetmap.josm.Main;
010import org.openstreetmap.josm.command.Command;
011import org.openstreetmap.josm.data.osm.DataSet;
012import org.openstreetmap.josm.spi.preferences.Config;
013import org.openstreetmap.josm.tools.CheckParameterUtil;
014
015/**
016 * This is the global undo/redo handler for all {@link DataSet}s.
017 * <p>
018 * 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.
019 */
020public class UndoRedoHandler {
021
022    /**
023     * All commands that were made on the dataset. Don't write from outside!
024     *
025     * @see #getLastCommand()
026     */
027    public final LinkedList<Command> commands = new LinkedList<>();
028    /**
029     * The stack for redoing commands
030     */
031    public final LinkedList<Command> redoCommands = new LinkedList<>();
032
033    private final LinkedList<CommandQueueListener> listenerCommands = new LinkedList<>();
034    private final LinkedList<CommandQueuePreciseListener> preciseListenerCommands = new LinkedList<>();
035
036    /**
037     * Constructs a new {@code UndoRedoHandler}.
038     */
039    public UndoRedoHandler() {
040        // Do nothing
041    }
042
043    /**
044     * A simple listener that gets notified of command queue (undo/redo) size changes.
045     * @see CommandQueuePreciseListener
046     * @since 12718 (moved from {@code OsmDataLayer}
047     */
048    @FunctionalInterface
049    public interface CommandQueueListener {
050        /**
051         * Notifies the listener about the new queue size
052         * @param queueSize Undo stack size
053         * @param redoSize Redo stack size
054         */
055        void commandChanged(int queueSize, int redoSize);
056    }
057
058    /**
059     * A listener that gets notified of command queue (undo/redo) operations individually.
060     * @see CommandQueueListener
061     * @since 13729
062     */
063    public interface CommandQueuePreciseListener {
064
065        /**
066         * Notifies the listener about a new command added to the queue.
067         * @param e event
068         */
069        void commandAdded(CommandAddedEvent e);
070
071        /**
072         * Notifies the listener about commands being cleaned.
073         * @param e event
074         */
075        void cleaned(CommandQueueCleanedEvent e);
076
077        /**
078         * Notifies the listener about a command that has been undone.
079         * @param e event
080         */
081        void commandUndone(CommandUndoneEvent e);
082
083        /**
084         * Notifies the listener about a command that has been redone.
085         * @param e event
086         */
087        void commandRedone(CommandRedoneEvent e);
088    }
089
090    abstract static class CommandQueueEvent extends EventObject {
091        protected CommandQueueEvent(UndoRedoHandler source) {
092            super(Objects.requireNonNull(source));
093        }
094
095        /**
096         * Calls the appropriate method of the listener for this event.
097         * @param listener dataset listener to notify about this event
098         */
099        abstract void fire(CommandQueuePreciseListener listener);
100
101        @Override
102        public final UndoRedoHandler getSource() {
103            return (UndoRedoHandler) super.getSource();
104        }
105    }
106
107    /**
108     * Event fired after a command has been added to the command queue.
109     * @since 13729
110     */
111    public static final class CommandAddedEvent extends CommandQueueEvent {
112
113        private static final long serialVersionUID = 1L;
114        private final Command cmd;
115
116        private CommandAddedEvent(UndoRedoHandler source, Command cmd) {
117            super(source);
118            this.cmd = Objects.requireNonNull(cmd);
119        }
120
121        /**
122         * Returns the added command.
123         * @return the added command
124         */
125        public Command getCommand() {
126            return cmd;
127        }
128
129        @Override
130        void fire(CommandQueuePreciseListener listener) {
131            listener.commandAdded(this);
132        }
133    }
134
135    /**
136     * Event fired after the command queue has been cleaned.
137     * @since 13729
138     */
139    public static final class CommandQueueCleanedEvent extends CommandQueueEvent {
140
141        private static final long serialVersionUID = 1L;
142        private final DataSet ds;
143
144        private CommandQueueCleanedEvent(UndoRedoHandler source, DataSet ds) {
145            super(source);
146            this.ds = ds;
147        }
148
149        /**
150         * Returns the affected dataset.
151         * @return the affected dataset, or null if the queue has been globally emptied
152         */
153        public DataSet getDataSet() {
154            return ds;
155        }
156
157        @Override
158        void fire(CommandQueuePreciseListener listener) {
159            listener.cleaned(this);
160        }
161    }
162
163    /**
164     * Event fired after a command has been undone.
165     * @since 13729
166     */
167    public static final class CommandUndoneEvent extends CommandQueueEvent {
168
169        private static final long serialVersionUID = 1L;
170        private final Command cmd;
171
172        private CommandUndoneEvent(UndoRedoHandler source, Command cmd) {
173            super(source);
174            this.cmd = Objects.requireNonNull(cmd);
175        }
176
177        /**
178         * Returns the undone command.
179         * @return the undone command
180         */
181        public Command getCommand() {
182            return cmd;
183        }
184
185        @Override
186        void fire(CommandQueuePreciseListener listener) {
187            listener.commandUndone(this);
188        }
189    }
190
191    /**
192     * Event fired after a command has been redone.
193     * @since 13729
194     */
195    public static final class CommandRedoneEvent extends CommandQueueEvent {
196
197        private static final long serialVersionUID = 1L;
198        private final Command cmd;
199
200        private CommandRedoneEvent(UndoRedoHandler source, Command cmd) {
201            super(source);
202            this.cmd = Objects.requireNonNull(cmd);
203        }
204
205        /**
206         * Returns the redone command.
207         * @return the redone command
208         */
209        public Command getCommand() {
210            return cmd;
211        }
212
213        @Override
214        void fire(CommandQueuePreciseListener listener) {
215            listener.commandRedone(this);
216        }
217    }
218
219    /**
220     * Gets the last command that was executed on the command stack.
221     * @return That command or <code>null</code> if there is no such command.
222     * @since #12316
223     */
224    public Command getLastCommand() {
225        return commands.peekLast();
226    }
227
228    /**
229     * Executes the command and add it to the intern command queue.
230     * @param c The command to execute. Must not be {@code null}.
231     */
232    public void addNoRedraw(final Command c) {
233        CheckParameterUtil.ensureParameterNotNull(c, "c");
234        c.executeCommand();
235        commands.add(c);
236        // Limit the number of commands in the undo list.
237        // Currently you have to undo the commands one by one. If
238        // this changes, a higher default value may be reasonable.
239        if (commands.size() > Config.getPref().getInt("undo.max", 1000)) {
240            commands.removeFirst();
241        }
242        redoCommands.clear();
243    }
244
245    /**
246     * Fires a commands change event after adding a command.
247     * @param cmd command added
248     * @since 13729
249     */
250    public void afterAdd(Command cmd) {
251        if (cmd != null) {
252            fireEvent(new CommandAddedEvent(this, cmd));
253        }
254        fireCommandsChanged();
255    }
256
257    /**
258     * Executes the command and add it to the intern command queue.
259     * @param c The command to execute. Must not be {@code null}.
260     */
261    public synchronized void add(final Command c) {
262        addNoRedraw(c);
263        afterAdd(c);
264    }
265
266    /**
267     * Undoes the last added command.
268     */
269    public void undo() {
270        undo(1);
271    }
272
273    /**
274     * Undoes multiple commands.
275     * @param num The number of commands to undo
276     */
277    public synchronized void undo(int num) {
278        if (commands.isEmpty())
279            return;
280        DataSet ds = Main.main.getEditDataSet();
281        if (ds != null) {
282            ds.beginUpdate();
283        }
284        try {
285            for (int i = 1; i <= num; ++i) {
286                final Command c = commands.removeLast();
287                c.undoCommand();
288                redoCommands.addFirst(c);
289                fireEvent(new CommandUndoneEvent(this, c));
290                if (commands.isEmpty()) {
291                    break;
292                }
293            }
294        } finally {
295            if (ds != null) {
296                ds.endUpdate();
297            }
298        }
299        fireCommandsChanged();
300    }
301
302    /**
303     * Redoes the last undoed command.
304     */
305    public void redo() {
306        redo(1);
307    }
308
309    /**
310     * Redoes multiple commands.
311     * @param num The number of commands to redo
312     */
313    public void redo(int num) {
314        if (redoCommands.isEmpty())
315            return;
316        for (int i = 0; i < num; ++i) {
317            final Command c = redoCommands.removeFirst();
318            c.executeCommand();
319            commands.add(c);
320            fireEvent(new CommandRedoneEvent(this, c));
321            if (redoCommands.isEmpty()) {
322                break;
323            }
324        }
325        fireCommandsChanged();
326    }
327
328    /**
329     * Fires a command change to all listeners.
330     */
331    private void fireCommandsChanged() {
332        for (final CommandQueueListener l : listenerCommands) {
333            l.commandChanged(commands.size(), redoCommands.size());
334        }
335    }
336
337    private void fireEvent(CommandQueueEvent e) {
338        preciseListenerCommands.forEach(e::fire);
339    }
340
341    /**
342     * Resets the undo/redo list.
343     */
344    public void clean() {
345        redoCommands.clear();
346        commands.clear();
347        fireEvent(new CommandQueueCleanedEvent(this, null));
348        fireCommandsChanged();
349    }
350
351    /**
352     * Resets all commands that affect the given dataset.
353     * @param dataSet The data set that was affected.
354     * @since 12718
355     */
356    public void clean(DataSet dataSet) {
357        if (dataSet == null)
358            return;
359        boolean changed = false;
360        for (Iterator<Command> it = commands.iterator(); it.hasNext();) {
361            if (it.next().getAffectedDataSet() == dataSet) {
362                it.remove();
363                changed = true;
364            }
365        }
366        for (Iterator<Command> it = redoCommands.iterator(); it.hasNext();) {
367            if (it.next().getAffectedDataSet() == dataSet) {
368                it.remove();
369                changed = true;
370            }
371        }
372        if (changed) {
373            fireEvent(new CommandQueueCleanedEvent(this, dataSet));
374            fireCommandsChanged();
375        }
376    }
377
378    /**
379     * Removes a command queue listener.
380     * @param l The command queue listener to remove
381     */
382    public void removeCommandQueueListener(CommandQueueListener l) {
383        listenerCommands.remove(l);
384    }
385
386    /**
387     * Adds a command queue listener.
388     * @param l The command queue listener to add
389     * @return {@code true} if the listener has been added, {@code false} otherwise
390     */
391    public boolean addCommandQueueListener(CommandQueueListener l) {
392        return listenerCommands.add(l);
393    }
394
395    /**
396     * Removes a precise command queue listener.
397     * @param l The precise command queue listener to remove
398     * @since 13729
399     */
400    public void removeCommandQueuePreciseListener(CommandQueuePreciseListener l) {
401        preciseListenerCommands.remove(l);
402    }
403
404    /**
405     * Adds a precise command queue listener.
406     * @param l The precise command queue listener to add
407     * @return {@code true} if the listener has been added, {@code false} otherwise
408     * @since 13729
409     */
410    public boolean addCommandQueuePreciseListener(CommandQueuePreciseListener l) {
411        return preciseListenerCommands.add(l);
412    }
413}