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    /**
285     * Executes the command and add it to the intern command queue.
286     * @param c The command to execute. Must not be {@code null}.
287     */
288    public void addNoRedraw(final Command c) {
289        addNoRedraw(c, true);
290    }
291
292    /**
293     * Executes the command and add it to the intern command queue.
294     * @param c The command to execute. Must not be {@code null}.
295     * @param execute true: Execute, else it is assumed that the command was already executed
296     * @since 14845
297     */
298    public void addNoRedraw(final Command c, boolean execute) {
299        CheckParameterUtil.ensureParameterNotNull(c, "c");
300        if (execute) {
301            c.executeCommand();
302        }
303        commands.add(c);
304        // Limit the number of commands in the undo list.
305        // Currently you have to undo the commands one by one. If
306        // this changes, a higher default value may be reasonable.
307        if (commands.size() > Config.getPref().getInt("undo.max", 1000)) {
308            commands.removeFirst();
309        }
310        redoCommands.clear();
311    }
312
313    /**
314     * Fires a commands change event after adding a command.
315     * @param cmd command added
316     * @since 13729
317     */
318    public void afterAdd(Command cmd) {
319        if (cmd != null) {
320            fireEvent(new CommandAddedEvent(this, cmd));
321        }
322        fireCommandsChanged();
323    }
324
325    /**
326     * Fires a commands change event after adding a list of commands.
327     * @param cmds commands added
328     * @since 14381
329     */
330    public void afterAdd(List<? extends Command> cmds) {
331        if (cmds != null) {
332            for (Command cmd : cmds) {
333                fireEvent(new CommandAddedEvent(this, cmd));
334            }
335        }
336        fireCommandsChanged();
337    }
338
339    /**
340     * Executes the command only if wanted and add it to the intern command queue.
341     * @param c The command to execute. Must not be {@code null}.
342     * @param execute true: Execute, else it is assumed that the command was already executed
343     */
344    public void add(final Command c, boolean execute) {
345        addNoRedraw(c, execute);
346        afterAdd(c);
347
348    }
349
350    /**
351     * Executes the command and add it to the intern command queue.
352     * @param c The command to execute. Must not be {@code null}.
353     */
354    public synchronized void add(final Command c) {
355        addNoRedraw(c, true);
356        afterAdd(c);
357    }
358
359    /**
360     * Undoes the last added command.
361     */
362    public void undo() {
363        undo(1);
364    }
365
366    /**
367     * Undoes multiple commands.
368     * @param num The number of commands to undo
369     */
370    public synchronized void undo(int num) {
371        if (commands.isEmpty())
372            return;
373        GuiHelper.runInEDTAndWait(() -> {
374            DataSet ds = OsmDataManager.getInstance().getEditDataSet();
375            if (ds != null) {
376                ds.beginUpdate();
377            }
378            try {
379                for (int i = 1; i <= num; ++i) {
380                    final Command c = commands.removeLast();
381                    c.undoCommand();
382                    redoCommands.addFirst(c);
383                    fireEvent(new CommandUndoneEvent(this, c));
384                    if (commands.isEmpty()) {
385                        break;
386                    }
387                }
388            } finally {
389                if (ds != null) {
390                    ds.endUpdate();
391                }
392            }
393            fireCommandsChanged();
394        });
395    }
396
397    /**
398     * Redoes the last undoed command.
399     */
400    public void redo() {
401        redo(1);
402    }
403
404    /**
405     * Redoes multiple commands.
406     * @param num The number of commands to redo
407     */
408    public synchronized void redo(int num) {
409        if (redoCommands.isEmpty())
410            return;
411        for (int i = 0; i < num; ++i) {
412            final Command c = redoCommands.removeFirst();
413            c.executeCommand();
414            commands.add(c);
415            fireEvent(new CommandRedoneEvent(this, c));
416            if (redoCommands.isEmpty()) {
417                break;
418            }
419        }
420        fireCommandsChanged();
421    }
422
423    /**
424     * Fires a command change to all listeners.
425     */
426    private void fireCommandsChanged() {
427        for (final CommandQueueListener l : listenerCommands) {
428            l.commandChanged(commands.size(), redoCommands.size());
429        }
430    }
431
432    private void fireEvent(CommandQueueEvent e) {
433        preciseListenerCommands.forEach(e::fire);
434    }
435
436    /**
437     * Resets the undo/redo list.
438     */
439    public void clean() {
440        redoCommands.clear();
441        commands.clear();
442        fireEvent(new CommandQueueCleanedEvent(this, null));
443        fireCommandsChanged();
444    }
445
446    /**
447     * Resets all commands that affect the given dataset.
448     * @param dataSet The data set that was affected.
449     * @since 12718
450     */
451    public synchronized void clean(DataSet dataSet) {
452        if (dataSet == null)
453            return;
454        boolean changed = false;
455        for (Iterator<Command> it = commands.iterator(); it.hasNext();) {
456            if (it.next().getAffectedDataSet() == dataSet) {
457                it.remove();
458                changed = true;
459            }
460        }
461        for (Iterator<Command> it = redoCommands.iterator(); it.hasNext();) {
462            if (it.next().getAffectedDataSet() == dataSet) {
463                it.remove();
464                changed = true;
465            }
466        }
467        if (changed) {
468            fireEvent(new CommandQueueCleanedEvent(this, dataSet));
469            fireCommandsChanged();
470        }
471    }
472
473    /**
474     * Removes a command queue listener.
475     * @param l The command queue listener to remove
476     */
477    public void removeCommandQueueListener(CommandQueueListener l) {
478        listenerCommands.remove(l);
479    }
480
481    /**
482     * Adds a command queue listener.
483     * @param l The command queue listener to add
484     * @return {@code true} if the listener has been added, {@code false} otherwise
485     */
486    public boolean addCommandQueueListener(CommandQueueListener l) {
487        return listenerCommands.add(l);
488    }
489
490    /**
491     * Removes a precise command queue listener.
492     * @param l The precise command queue listener to remove
493     * @since 13729
494     */
495    public void removeCommandQueuePreciseListener(CommandQueuePreciseListener l) {
496        preciseListenerCommands.remove(l);
497    }
498
499    /**
500     * Adds a precise command queue listener.
501     * @param l The precise command queue listener to add
502     * @return {@code true} if the listener has been added, {@code false} otherwise
503     * @since 13729
504     */
505    public boolean addCommandQueuePreciseListener(CommandQueuePreciseListener l) {
506        return preciseListenerCommands.add(l);
507    }
508}