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

Last change on this file since 17335 was 16568, checked in by simon04, 4 years ago

see #19354 - UndoRedoHandler: make commands, redoCommands private

  • 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.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 c.undoCommand();
381 redoCommands.addFirst(c);
382 fireEvent(new CommandUndoneEvent(this, c));
383 if (commands.isEmpty()) {
384 break;
385 }
386 }
387 } finally {
388 if (ds != null) {
389 ds.endUpdate();
390 }
391 }
392 fireCommandsChanged();
393 });
394 }
395
396 /**
397 * Redoes the last undoed command.
398 */
399 public void redo() {
400 redo(1);
401 }
402
403 /**
404 * Redoes multiple commands.
405 * @param num The number of commands to redo
406 */
407 public synchronized void redo(int num) {
408 if (redoCommands.isEmpty())
409 return;
410 for (int i = 0; i < num; ++i) {
411 final Command c = redoCommands.removeFirst();
412 c.executeCommand();
413 commands.add(c);
414 fireEvent(new CommandRedoneEvent(this, c));
415 if (redoCommands.isEmpty()) {
416 break;
417 }
418 }
419 fireCommandsChanged();
420 }
421
422 /**
423 * Fires a command change to all listeners.
424 */
425 private void fireCommandsChanged() {
426 for (final CommandQueueListener l : listenerCommands) {
427 l.commandChanged(commands.size(), redoCommands.size());
428 }
429 }
430
431 private void fireEvent(CommandQueueEvent e) {
432 preciseListenerCommands.forEach(e::fire);
433 }
434
435 /**
436 * Resets the undo/redo list.
437 */
438 public void clean() {
439 redoCommands.clear();
440 commands.clear();
441 fireEvent(new CommandQueueCleanedEvent(this, null));
442 fireCommandsChanged();
443 }
444
445 /**
446 * Resets all commands that affect the given dataset.
447 * @param dataSet The data set that was affected.
448 * @since 12718
449 */
450 public synchronized void clean(DataSet dataSet) {
451 if (dataSet == null)
452 return;
453 boolean changed = false;
454 changed |= commands.removeIf(c -> c.getAffectedDataSet() == dataSet);
455 changed |= redoCommands.removeIf(c -> c.getAffectedDataSet() == dataSet);
456 if (changed) {
457 fireEvent(new CommandQueueCleanedEvent(this, dataSet));
458 fireCommandsChanged();
459 }
460 }
461
462 /**
463 * Removes a command queue listener.
464 * @param l The command queue listener to remove
465 */
466 public void removeCommandQueueListener(CommandQueueListener l) {
467 listenerCommands.remove(l);
468 }
469
470 /**
471 * Adds a command queue listener.
472 * @param l The command queue listener to add
473 * @return {@code true} if the listener has been added, {@code false} otherwise
474 */
475 public boolean addCommandQueueListener(CommandQueueListener l) {
476 return listenerCommands.add(l);
477 }
478
479 /**
480 * Removes a precise command queue listener.
481 * @param l The precise command queue listener to remove
482 * @since 13729
483 */
484 public void removeCommandQueuePreciseListener(CommandQueuePreciseListener l) {
485 preciseListenerCommands.remove(l);
486 }
487
488 /**
489 * Adds a precise command queue listener.
490 * @param l The precise command queue listener to add
491 * @return {@code true} if the listener has been added, {@code false} otherwise
492 * @since 13729
493 */
494 public boolean addCommandQueuePreciseListener(CommandQueuePreciseListener l) {
495 return preciseListenerCommands.add(l);
496 }
497}
Note: See TracBrowser for help on using the repository browser.