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