001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import java.util.Collections; 005import java.util.EventObject; 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 026 * 027 * @see #getLastCommand() 028 * @see #getUndoCommands() 029 */ 030 private final LinkedList<Command> commands = new LinkedList<>(); 031 032 /** 033 * The stack for redoing commands 034 035 * @see #getRedoCommands() 036 */ 037 private 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, 16567 (signature) 242 */ 243 public List<Command> getUndoCommands() { 244 return Collections.unmodifiableList(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, 16567 (signature) 251 */ 252 public List<Command> getRedoCommands() { 253 return Collections.unmodifiableList(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 try { 381 c.undoCommand(); 382 } catch (Exception e) { // NOPMD 383 // fix #20098: restore command stack as we will not fire an event 384 commands.add(c); 385 throw e; 386 } 387 redoCommands.addFirst(c); 388 fireEvent(new CommandUndoneEvent(this, c)); 389 if (commands.isEmpty()) { 390 break; 391 } 392 } 393 } finally { 394 if (ds != null) { 395 ds.endUpdate(); 396 } 397 } 398 fireCommandsChanged(); 399 }); 400 } 401 402 /** 403 * Redoes the last undoed command. 404 */ 405 public void redo() { 406 redo(1); 407 } 408 409 /** 410 * Redoes multiple commands. 411 * @param num The number of commands to redo 412 */ 413 public synchronized void redo(int num) { 414 if (redoCommands.isEmpty()) 415 return; 416 for (int i = 0; i < num; ++i) { 417 final Command c = redoCommands.removeFirst(); 418 c.executeCommand(); 419 commands.add(c); 420 fireEvent(new CommandRedoneEvent(this, c)); 421 if (redoCommands.isEmpty()) { 422 break; 423 } 424 } 425 fireCommandsChanged(); 426 } 427 428 /** 429 * Fires a command change to all listeners. 430 */ 431 private void fireCommandsChanged() { 432 for (final CommandQueueListener l : listenerCommands) { 433 l.commandChanged(commands.size(), redoCommands.size()); 434 } 435 } 436 437 private void fireEvent(CommandQueueEvent e) { 438 preciseListenerCommands.forEach(e::fire); 439 } 440 441 /** 442 * Resets the undo/redo list. 443 */ 444 public void clean() { 445 redoCommands.clear(); 446 commands.clear(); 447 fireEvent(new CommandQueueCleanedEvent(this, null)); 448 fireCommandsChanged(); 449 } 450 451 /** 452 * Resets all commands that affect the given dataset. 453 * @param dataSet The data set that was affected. 454 * @since 12718 455 */ 456 public synchronized void clean(DataSet dataSet) { 457 if (dataSet == null) 458 return; 459 boolean changed = false; 460 changed |= commands.removeIf(c -> c.getAffectedDataSet() == dataSet); 461 changed |= redoCommands.removeIf(c -> c.getAffectedDataSet() == dataSet); 462 if (changed) { 463 fireEvent(new CommandQueueCleanedEvent(this, dataSet)); 464 fireCommandsChanged(); 465 } 466 } 467 468 /** 469 * Removes a command queue listener. 470 * @param l The command queue listener to remove 471 */ 472 public void removeCommandQueueListener(CommandQueueListener l) { 473 listenerCommands.remove(l); 474 } 475 476 /** 477 * Adds a command queue listener. 478 * @param l The command queue listener to add 479 * @return {@code true} if the listener has been added, {@code false} otherwise 480 */ 481 public boolean addCommandQueueListener(CommandQueueListener l) { 482 return listenerCommands.add(l); 483 } 484 485 /** 486 * Removes a precise command queue listener. 487 * @param l The precise command queue listener to remove 488 * @since 13729 489 */ 490 public void removeCommandQueuePreciseListener(CommandQueuePreciseListener l) { 491 preciseListenerCommands.remove(l); 492 } 493 494 /** 495 * Adds a precise command queue listener. 496 * @param l The precise command queue listener to add 497 * @return {@code true} if the listener has been added, {@code false} otherwise 498 * @since 13729 499 */ 500 public boolean addCommandQueuePreciseListener(CommandQueuePreciseListener l) { 501 return preciseListenerCommands.add(l); 502 } 503}