source: josm/trunk/src/org/openstreetmap/josm/data/UndoRedoHandler.java@ 17534

Last change on this file since 17534 was 17399, checked in by GerdP, 3 years ago

fix #20213: Command stack: Edits in relation editor are listed in wrong stack and lead to exception

  • revert changes for #17196, this approach was too simple because relation editor may save changes for an inactive edit layer
  • Property svn:eol-style set to native
File size: 14.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import java.util.Collections;
5import java.util.EventObject;
6import java.util.LinkedList;
7import java.util.List;
8import java.util.Objects;
9
10import org.openstreetmap.josm.command.Command;
11import org.openstreetmap.josm.data.osm.DataSet;
12import org.openstreetmap.josm.data.osm.OsmDataManager;
13import org.openstreetmap.josm.gui.util.GuiHelper;
14import org.openstreetmap.josm.spi.preferences.Config;
15import org.openstreetmap.josm.tools.CheckParameterUtil;
16
17/**
18 * This is the global undo/redo handler for all {@link DataSet}s.
19 * <p>
20 * 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.
21 */
22public final class UndoRedoHandler {
23
24 /**
25 * All commands that were made on the dataset
26 *
27 * @see #getLastCommand()
28 * @see #getUndoCommands()
29 */
30 private final LinkedList<Command> commands = new LinkedList<>();
31
32 /**
33 * The stack for redoing commands
34
35 * @see #getRedoCommands()
36 */
37 private final LinkedList<Command> redoCommands = new LinkedList<>();
38
39 private final LinkedList<CommandQueueListener> listenerCommands = new LinkedList<>();
40 private final LinkedList<CommandQueuePreciseListener> preciseListenerCommands = new LinkedList<>();
41
42 private static class InstanceHolder {
43 static final UndoRedoHandler INSTANCE = new UndoRedoHandler();
44 }
45
46 /**
47 * Returns the unique instance.
48 * @return the unique instance
49 * @since 14134
50 */
51 public static UndoRedoHandler getInstance() {
52 return InstanceHolder.INSTANCE;
53 }
54
55 /**
56 * Constructs a new {@code UndoRedoHandler}.
57 */
58 private UndoRedoHandler() {
59 // Hide constructor
60 }
61
62 /**
63 * A simple listener that gets notified of command queue (undo/redo) size changes.
64 * @see CommandQueuePreciseListener
65 * @since 12718 (moved from {@code OsmDataLayer}
66 */
67 @FunctionalInterface
68 public interface CommandQueueListener {
69 /**
70 * Notifies the listener about the new queue size
71 * @param queueSize Undo stack size
72 * @param redoSize Redo stack size
73 */
74 void commandChanged(int queueSize, int redoSize);
75 }
76
77 /**
78 * A listener that gets notified of command queue (undo/redo) operations individually.
79 * @see CommandQueueListener
80 * @since 13729
81 */
82 public interface CommandQueuePreciseListener {
83
84 /**
85 * Notifies the listener about a new command added to the queue.
86 * @param e event
87 */
88 void commandAdded(CommandAddedEvent e);
89
90 /**
91 * Notifies the listener about commands being cleaned.
92 * @param e event
93 */
94 void cleaned(CommandQueueCleanedEvent e);
95
96 /**
97 * Notifies the listener about a command that has been undone.
98 * @param e event
99 */
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}
Note: See TracBrowser for help on using the repository browser.