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