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