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}