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

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

fix #17196 Undo/Redo may change data in inactive layer

  • fix case when there are two layers with changes and one layer is closed. The undo stack for the other layer disappeared.
  • Property svn:eol-style set to native
File size: 15.1 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.HashMap;
7import java.util.LinkedList;
8import java.util.List;
9import java.util.Map;
10import java.util.Objects;
11
12import org.openstreetmap.josm.command.Command;
13import org.openstreetmap.josm.data.osm.DataSet;
14import org.openstreetmap.josm.data.osm.OsmDataManager;
15import org.openstreetmap.josm.gui.MainApplication;
16import org.openstreetmap.josm.gui.layer.OsmDataLayer;
17import org.openstreetmap.josm.gui.util.GuiHelper;
18import org.openstreetmap.josm.spi.preferences.Config;
19import org.openstreetmap.josm.tools.CheckParameterUtil;
20
21/**
22 * This is the global undo/redo handler for all {@link DataSet}s.
23 * <p>
24 * 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.
25 */
26public final class UndoRedoHandler {
27
28 /**
29 * All commands that were made on the dataset
30 *
31 * @see #getLastCommand()
32 * @see #getUndoCommands()
33 */
34 private final LinkedList<Command> commands = new LinkedList<>();
35
36 /**
37 * The stack for redoing commands
38
39 * @see #getRedoCommands()
40 */
41 private final LinkedList<Command> redoCommands = new LinkedList<>();
42
43 private final LinkedList<CommandQueueListener> listenerCommands = new LinkedList<>();
44 private final LinkedList<CommandQueuePreciseListener> preciseListenerCommands = new LinkedList<>();
45
46 private static class InstanceHolder {
47 static final UndoRedoHandler NO_DATA_SET_INSTANCE = new UndoRedoHandler();
48 static final Map<DataSet, UndoRedoHandler> map = new HashMap<>();
49
50 }
51
52 /**
53 * Returns the unique instance.
54 * @return the unique instance
55 * @since 14134
56 */
57 public static UndoRedoHandler getInstance() {
58 OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
59 if (editLayer != null && editLayer == MainApplication.getLayerManager().getActiveLayer()) {
60 return InstanceHolder.map.computeIfAbsent(editLayer.data, k -> new UndoRedoHandler());
61 }
62 return InstanceHolder.NO_DATA_SET_INSTANCE;
63 }
64
65 /**
66 * Constructs a new {@code UndoRedoHandler}.
67 */
68 private UndoRedoHandler() {
69 // Hide constructor
70 }
71
72 /**
73 * A simple listener that gets notified of command queue (undo/redo) size changes.
74 * @see CommandQueuePreciseListener
75 * @since 12718 (moved from {@code OsmDataLayer}
76 */
77 @FunctionalInterface
78 public interface CommandQueueListener {
79 /**
80 * Notifies the listener about the new queue size
81 * @param queueSize Undo stack size
82 * @param redoSize Redo stack size
83 */
84 void commandChanged(int queueSize, int redoSize);
85 }
86
87 /**
88 * A listener that gets notified of command queue (undo/redo) operations individually.
89 * @see CommandQueueListener
90 * @since 13729
91 */
92 public interface CommandQueuePreciseListener {
93
94 /**
95 * Notifies the listener about a new command added to the queue.
96 * @param e event
97 */
98 void commandAdded(CommandAddedEvent e);
99
100 /**
101 * Notifies the listener about commands being cleaned.
102 * @param e event
103 */
104 void cleaned(CommandQueueCleanedEvent e);
105
106 /**
107 * Notifies the listener about a command that has been undone.
108 * @param e event
109 */
110 void commandUndone(CommandUndoneEvent e);
111
112 /**
113 * Notifies the listener about a command that has been redone.
114 * @param e event
115 */
116 void commandRedone(CommandRedoneEvent e);
117 }
118
119 abstract static class CommandQueueEvent extends EventObject {
120 protected CommandQueueEvent(UndoRedoHandler source) {
121 super(Objects.requireNonNull(source));
122 }
123
124 /**
125 * Calls the appropriate method of the listener for this event.
126 * @param listener dataset listener to notify about this event
127 */
128 abstract void fire(CommandQueuePreciseListener listener);
129
130 @Override
131 public final UndoRedoHandler getSource() {
132 return (UndoRedoHandler) super.getSource();
133 }
134 }
135
136 /**
137 * Event fired after a command has been added to the command queue.
138 * @since 13729
139 */
140 public static final class CommandAddedEvent extends CommandQueueEvent {
141
142 private static final long serialVersionUID = 1L;
143 private final Command cmd;
144
145 private CommandAddedEvent(UndoRedoHandler source, Command cmd) {
146 super(source);
147 this.cmd = Objects.requireNonNull(cmd);
148 }
149
150 /**
151 * Returns the added command.
152 * @return the added command
153 */
154 public Command getCommand() {
155 return cmd;
156 }
157
158 @Override
159 void fire(CommandQueuePreciseListener listener) {
160 listener.commandAdded(this);
161 }
162 }
163
164 /**
165 * Event fired after the command queue has been cleaned.
166 * @since 13729
167 */
168 public static final class CommandQueueCleanedEvent extends CommandQueueEvent {
169
170 private static final long serialVersionUID = 1L;
171 private final DataSet ds;
172
173 private CommandQueueCleanedEvent(UndoRedoHandler source, DataSet ds) {
174 super(source);
175 this.ds = ds;
176 }
177
178 /**
179 * Returns the affected dataset.
180 * @return the affected dataset, or null if the queue has been globally emptied
181 */
182 public DataSet getDataSet() {
183 return ds;
184 }
185
186 @Override
187 void fire(CommandQueuePreciseListener listener) {
188 listener.cleaned(this);
189 }
190 }
191
192 /**
193 * Event fired after a command has been undone.
194 * @since 13729
195 */
196 public static final class CommandUndoneEvent extends CommandQueueEvent {
197
198 private static final long serialVersionUID = 1L;
199 private final Command cmd;
200
201 private CommandUndoneEvent(UndoRedoHandler source, Command cmd) {
202 super(source);
203 this.cmd = Objects.requireNonNull(cmd);
204 }
205
206 /**
207 * Returns the undone command.
208 * @return the undone command
209 */
210 public Command getCommand() {
211 return cmd;
212 }
213
214 @Override
215 void fire(CommandQueuePreciseListener listener) {
216 listener.commandUndone(this);
217 }
218 }
219
220 /**
221 * Event fired after a command has been redone.
222 * @since 13729
223 */
224 public static final class CommandRedoneEvent extends CommandQueueEvent {
225
226 private static final long serialVersionUID = 1L;
227 private final Command cmd;
228
229 private CommandRedoneEvent(UndoRedoHandler source, Command cmd) {
230 super(source);
231 this.cmd = Objects.requireNonNull(cmd);
232 }
233
234 /**
235 * Returns the redone command.
236 * @return the redone command
237 */
238 public Command getCommand() {
239 return cmd;
240 }
241
242 @Override
243 void fire(CommandQueuePreciseListener listener) {
244 listener.commandRedone(this);
245 }
246 }
247
248 /**
249 * Returns all commands that were made on the dataset, that can be undone.
250 * @return all commands that were made on the dataset, that can be undone
251 * @since 14281, 16567 (signature)
252 */
253 public List<Command> getUndoCommands() {
254 return Collections.unmodifiableList(commands);
255 }
256
257 /**
258 * Returns all commands that were made and undone on the dataset, that can be redone.
259 * @return all commands that were made and undone on the dataset, that can be redone.
260 * @since 14281, 16567 (signature)
261 */
262 public List<Command> getRedoCommands() {
263 return Collections.unmodifiableList(redoCommands);
264 }
265
266 /**
267 * Gets the last command that was executed on the command stack.
268 * @return That command or <code>null</code> if there is no such command.
269 * @since #12316
270 */
271 public Command getLastCommand() {
272 return commands.peekLast();
273 }
274
275 /**
276 * Determines if commands can be undone.
277 * @return {@code true} if at least a command can be undone
278 * @since 14281
279 */
280 public boolean hasUndoCommands() {
281 return !commands.isEmpty();
282 }
283
284 /**
285 * Determines if commands can be redone.
286 * @return {@code true} if at least a command can be redone
287 * @since 14281
288 */
289 public boolean hasRedoCommands() {
290 return !redoCommands.isEmpty();
291 }
292
293 /**
294 * Executes the command and add it to the intern command queue.
295 * @param c The command to execute. Must not be {@code null}.
296 */
297 public void addNoRedraw(final Command c) {
298 addNoRedraw(c, true);
299 }
300
301 /**
302 * Executes the command and add it to the intern command queue.
303 * @param c The command to execute. Must not be {@code null}.
304 * @param execute true: Execute, else it is assumed that the command was already executed
305 * @since 14845
306 */
307 public void addNoRedraw(final Command c, boolean execute) {
308 CheckParameterUtil.ensureParameterNotNull(c, "c");
309 if (execute) {
310 c.executeCommand();
311 }
312 commands.add(c);
313 // Limit the number of commands in the undo list.
314 // Currently you have to undo the commands one by one. If
315 // this changes, a higher default value may be reasonable.
316 if (commands.size() > Config.getPref().getInt("undo.max", 1000)) {
317 commands.removeFirst();
318 }
319 redoCommands.clear();
320 }
321
322 /**
323 * Fires a commands change event after adding a command.
324 * @param cmd command added
325 * @since 13729
326 */
327 public void afterAdd(Command cmd) {
328 if (cmd != null) {
329 fireEvent(new CommandAddedEvent(this, cmd));
330 }
331 fireCommandsChanged();
332 }
333
334 /**
335 * Fires a commands change event after adding a list of commands.
336 * @param cmds commands added
337 * @since 14381
338 */
339 public void afterAdd(List<? extends Command> cmds) {
340 if (cmds != null) {
341 for (Command cmd : cmds) {
342 fireEvent(new CommandAddedEvent(this, cmd));
343 }
344 }
345 fireCommandsChanged();
346 }
347
348 /**
349 * Executes the command only if wanted and add it to the intern command queue.
350 * @param c The command to execute. Must not be {@code null}.
351 * @param execute true: Execute, else it is assumed that the command was already executed
352 */
353 public void add(final Command c, boolean execute) {
354 addNoRedraw(c, execute);
355 afterAdd(c);
356
357 }
358
359 /**
360 * Executes the command and add it to the intern command queue.
361 * @param c The command to execute. Must not be {@code null}.
362 */
363 public synchronized void add(final Command c) {
364 addNoRedraw(c, true);
365 afterAdd(c);
366 }
367
368 /**
369 * Undoes the last added command.
370 */
371 public void undo() {
372 undo(1);
373 }
374
375 /**
376 * Undoes multiple commands.
377 * @param num The number of commands to undo
378 */
379 public synchronized void undo(int num) {
380 if (commands.isEmpty())
381 return;
382 GuiHelper.runInEDTAndWait(() -> {
383 DataSet ds = OsmDataManager.getInstance().getEditDataSet();
384 if (ds != null) {
385 ds.beginUpdate();
386 }
387 try {
388 for (int i = 1; i <= num; ++i) {
389 final Command c = commands.removeLast();
390 try {
391 c.undoCommand();
392 } catch (Exception e) { // NOPMD
393 // fix #20098: restore command stack as we will not fire an event
394 commands.add(c);
395 throw e;
396 }
397 redoCommands.addFirst(c);
398 fireEvent(new CommandUndoneEvent(this, c));
399 if (commands.isEmpty()) {
400 break;
401 }
402 }
403 } finally {
404 if (ds != null) {
405 ds.endUpdate();
406 }
407 }
408 fireCommandsChanged();
409 });
410 }
411
412 /**
413 * Redoes the last undoed command.
414 */
415 public void redo() {
416 redo(1);
417 }
418
419 /**
420 * Redoes multiple commands.
421 * @param num The number of commands to redo
422 */
423 public synchronized void redo(int num) {
424 if (redoCommands.isEmpty())
425 return;
426 for (int i = 0; i < num; ++i) {
427 final Command c = redoCommands.removeFirst();
428 c.executeCommand();
429 commands.add(c);
430 fireEvent(new CommandRedoneEvent(this, c));
431 if (redoCommands.isEmpty()) {
432 break;
433 }
434 }
435 fireCommandsChanged();
436 }
437
438 /**
439 * Fires a command change to all listeners.
440 */
441 private void fireCommandsChanged() {
442 for (final CommandQueueListener l : listenerCommands) {
443 l.commandChanged(commands.size(), redoCommands.size());
444 }
445 if (getInstance() != InstanceHolder.NO_DATA_SET_INSTANCE) {
446 for (final CommandQueueListener l : InstanceHolder.NO_DATA_SET_INSTANCE.listenerCommands) {
447 l.commandChanged(commands.size(), redoCommands.size());
448 }
449
450 }
451 }
452
453 private void fireEvent(CommandQueueEvent e) {
454 preciseListenerCommands.forEach(e::fire);
455 }
456
457 /**
458 * Resets the undo/redo list.
459 */
460 public void clean() {
461 redoCommands.clear();
462 commands.clear();
463 fireEvent(new CommandQueueCleanedEvent(this, null));
464 fireCommandsChanged();
465 }
466
467 /**
468 * Resets all commands that affect the given dataset.
469 * @param dataSet The data set that was affected.
470 * @since 12718
471 */
472 public synchronized void clean(DataSet dataSet) {
473 if (dataSet == null)
474 return;
475 UndoRedoHandler old = InstanceHolder.map.remove(dataSet);
476 if (old != null) {
477 old.clean();
478 }
479 }
480
481 /**
482 * Removes a command queue listener.
483 * @param l The command queue listener to remove
484 */
485 public void removeCommandQueueListener(CommandQueueListener l) {
486 listenerCommands.remove(l);
487 }
488
489 /**
490 * Adds a command queue listener.
491 * @param l The command queue listener to add
492 * @return {@code true} if the listener has been added, {@code false} otherwise
493 */
494 public boolean addCommandQueueListener(CommandQueueListener l) {
495 return listenerCommands.add(l);
496 }
497
498 /**
499 * Removes a precise command queue listener.
500 * @param l The precise command queue listener to remove
501 * @since 13729
502 */
503 public void removeCommandQueuePreciseListener(CommandQueuePreciseListener l) {
504 preciseListenerCommands.remove(l);
505 }
506
507 /**
508 * Adds a precise command queue listener.
509 * @param l The precise command queue listener to add
510 * @return {@code true} if the listener has been added, {@code false} otherwise
511 * @since 13729
512 */
513 public boolean addCommandQueuePreciseListener(CommandQueuePreciseListener l) {
514 return preciseListenerCommands.add(l);
515 }
516}
Note: See TracBrowser for help on using the repository browser.