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}