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}