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

Last change on this file since 14845 was 14845, checked in by GerdP, 5 years ago

fix #17401: create a SequenceCommand instead of adding single commands for each fixed error

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