1 package org.opentrafficsim.editor;
2
3 import java.util.ArrayDeque;
4 import java.util.Deque;
5 import java.util.Iterator;
6 import java.util.LinkedList;
7 import java.util.function.Consumer;
8
9 import javax.swing.AbstractButton;
10
11 import org.djutils.event.Event;
12 import org.djutils.event.EventListener;
13 import org.djutils.exceptions.Throw;
14 import org.opentrafficsim.editor.decoration.validation.CoupledValidator;
15
16
17
18
19
20
21
22
23
24
25
26 public class Undo implements EventListener
27 {
28
29
30 private static final int MAX_UNDO = 50;
31
32
33 private LinkedList<Action> queue = new LinkedList<>();
34
35
36 private int cursor = -1;
37
38
39 private Deque<SubAction> currentSet;
40
41
42 private final OtsEditor editor;
43
44
45 private final AbstractButton undoItem;
46
47
48 private final AbstractButton redoItem;
49
50
51 private boolean ignoreChanges = false;
52
53
54 private Action nextAction;
55
56
57
58
59
60
61
62 public Undo(final OtsEditor editor, final AbstractButton undoItem, final AbstractButton redoItem)
63 {
64 this.editor = editor;
65 this.undoItem = undoItem;
66 this.redoItem = redoItem;
67 this.undoItem.setEnabled(false);
68 this.redoItem.setEnabled(false);
69 editor.addListener(this, OtsEditor.NEW_FILE);
70 }
71
72
73
74
75 public void clear()
76 {
77 this.ignoreChanges = false;
78 this.currentSet = null;
79 this.cursor = -1;
80 this.queue = new LinkedList<>();
81 this.undoItem.setEnabled(false);
82 this.redoItem.setEnabled(false);
83 }
84
85
86
87
88
89 public void setIgnoreChanges(final boolean ignore)
90 {
91 this.ignoreChanges = ignore;
92 }
93
94
95
96
97
98
99
100
101
102
103
104
105
106 public void startAction(final ActionType type, final XsdTreeNode node, final String attribute)
107 {
108 if (this.ignoreChanges)
109 {
110 return;
111 }
112
113
114 this.nextAction = new Action(type, new ArrayDeque<>(), node, node.parent, attribute);
115 }
116
117
118
119
120
121 private void add(final SubAction subAction)
122 {
123 if (this.ignoreChanges)
124 {
125 return;
126 }
127
128 if (this.nextAction != null)
129 {
130
131 while (this.cursor < this.queue.size() - 1)
132 {
133 this.queue.pollLast();
134 }
135 this.currentSet = this.nextAction.subActions;
136 this.queue.add(this.nextAction);
137 while (this.queue.size() > MAX_UNDO)
138 {
139 this.queue.pollFirst();
140 }
141 this.nextAction = null;
142 this.cursor = this.queue.size() - 1;
143 updateButtons();
144 }
145 Throw.when(this.currentSet == null, IllegalStateException.class,
146 "Adding undo action without having called startUndoAction()");
147 this.currentSet.add(subAction);
148 }
149
150
151
152
153
154 public boolean canUndo()
155 {
156 return this.cursor >= 0;
157 }
158
159
160
161
162
163 public boolean canRedo()
164 {
165 return this.cursor < this.queue.size() - 1;
166 }
167
168
169
170
171 public synchronized void undo()
172 {
173 if (this.ignoreChanges)
174 {
175 return;
176 }
177 this.ignoreChanges = true;
178
179 Action action = this.queue.get(this.cursor);
180 if (action.type.equals(ActionType.ACTIVATE))
181 {
182 this.editor.collapse(action.node);
183 }
184
185 Iterator<SubAction> iterator = action.subActions.descendingIterator();
186 while (iterator.hasNext())
187 {
188 iterator.next().undo();
189 }
190 action.parent.children.forEach((n) -> n.invalidate());
191 action.parent.invalidate();
192 this.editor.show(action.node, action.attribute);
193 this.cursor--;
194 updateButtons();
195 this.ignoreChanges = false;
196 }
197
198
199
200
201 public synchronized void redo()
202 {
203 if (this.ignoreChanges)
204 {
205 return;
206 }
207 this.ignoreChanges = true;
208 this.cursor++;
209 Action action = this.queue.get(this.cursor);
210 action.subActions.forEach((a) -> a.redo());
211 action.parent.children.forEach((n) -> n.invalidate());
212 action.parent.invalidate();
213 this.editor.show(action.postActionShowNode, action.attribute);
214 updateButtons();
215 this.ignoreChanges = false;
216 }
217
218
219
220
221 public void updateButtons()
222 {
223 this.undoItem.setEnabled(canUndo());
224 this.undoItem.setText(canUndo() ? ("Undo " + this.queue.get(this.cursor).type) : "Undo");
225 this.redoItem.setEnabled(canRedo());
226 this.redoItem.setText(canRedo() ? ("Redo " + this.queue.get(this.cursor + 1).type) : "Redo");
227 }
228
229 @Override
230 @SuppressWarnings("methodlength")
231 public void notify(final Event event)
232 {
233 listenAndUnlisten(event);
234
235
236 if (this.ignoreChanges)
237 {
238 return;
239 }
240
241
242 if (event.getType().equals(XsdTreeNodeRoot.NODE_CREATED))
243 {
244 Object[] content = (Object[]) event.getContent();
245 XsdTreeNode node = (XsdTreeNode) content[0];
246 XsdTreeNode parent = (XsdTreeNode) content[1];
247 int index = (int) content[2];
248 XsdTreeNode root = node.getRoot();
249 add(new SubAction(() ->
250 {
251 parent.children.remove(node);
252 node.parent = null;
253 root.fireEvent(XsdTreeNodeRoot.NODE_REMOVED, new Object[] {node, parent, index});
254 }, () ->
255 {
256 if (index >= 0)
257 {
258 parent.setChild(index, node);
259 }
260 node.parent = parent;
261 root.fireEvent(XsdTreeNodeRoot.NODE_CREATED, new Object[] {node, parent, index});
262 }, "Create " + node.getPathString()));
263 }
264 else if (event.getType().equals(XsdTreeNodeRoot.NODE_REMOVED))
265 {
266 Object[] content = (Object[]) event.getContent();
267 XsdTreeNode node = (XsdTreeNode) content[0];
268 XsdTreeNode parent = (XsdTreeNode) content[1];
269 int index = (int) content[2];
270 XsdTreeNode root = parent.getRoot();
271 add(new SubAction(() ->
272 {
273 if (index < 0)
274 {
275
276 node.parent = parent;
277 root.fireEvent(XsdTreeNodeRoot.NODE_CREATED, new Object[] {node, parent, parent.children.indexOf(node)});
278 }
279 else
280 {
281 parent.setChild(index, node);
282 root.fireEvent(XsdTreeNodeRoot.NODE_CREATED, new Object[] {node, parent, index});
283 }
284 }, () ->
285 {
286 node.parent.children.remove(node);
287 node.parent = null;
288 root.fireEvent(XsdTreeNodeRoot.NODE_REMOVED, new Object[] {node, parent, index});
289 }, "Remove " + node.getPathString()));
290 }
291 else if (event.getType().equals(XsdTreeNode.VALUE_CHANGED))
292 {
293 Object[] content = (Object[]) event.getContent();
294 XsdTreeNode node = (XsdTreeNode) content[0];
295 String value = node.getValue();
296 add(new SubAction(() ->
297 {
298 node.setValue((String) content[1]);
299 }, () ->
300 {
301 node.setValue(value);
302 }, "Change " + node.getPathString() + " value: " + value));
303 }
304 else if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED))
305 {
306 Object[] content = (Object[]) event.getContent();
307 XsdTreeNode node = (XsdTreeNode) content[0];
308 String attribute = (String) content[1];
309 String prevValue = (String) content[2];
310 String value = node.getAttributeValue(attribute);
311
312 if (node.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
313 {
314 this.currentSet.clear();
315 }
316 add(new SubAction(() ->
317 {
318 node.setAttributeValue(attribute, prevValue);
319 }, () ->
320 {
321 node.setAttributeValue(attribute, value);
322 }, "Create " + node.getPathString() + ".@" + attribute + ": " + value));
323 }
324 else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
325 {
326 Object[] content = (Object[]) event.getContent();
327 XsdTreeNode node = (XsdTreeNode) content[0];
328 boolean activated = (boolean) content[1];
329 add(new SubAction(() ->
330 {
331 node.active = !activated;
332 node.fireEvent(XsdTreeNode.ACTIVATION_CHANGED, new Object[] {node, !activated});
333 }, () ->
334 {
335 node.active = activated;
336 node.fireEvent(XsdTreeNode.ACTIVATION_CHANGED, new Object[] {node, activated});
337 }, "Activation " + node.getPathString() + " " + activated));
338 }
339 else if (event.getType().equals(XsdTreeNode.OPTION_CHANGED))
340 {
341 Object[] content = (Object[]) event.getContent();
342 XsdTreeNode node = (XsdTreeNode) content[1];
343 XsdTreeNode previous = (XsdTreeNode) content[2];
344 if (previous != null)
345 {
346 add(new SubAction(() ->
347 {
348 node.setOption(previous);
349 }, () ->
350 {
351 previous.setOption(node);
352 }, "Set option " + node.getPathString()));
353 }
354 }
355 else if (event.getType().equals(XsdTreeNode.MOVED))
356 {
357 Object[] content = (Object[]) event.getContent();
358 XsdTreeNode node = (XsdTreeNode) content[0];
359 int oldIndex = (int) content[1];
360 int newIndex = (int) content[2];
361 add(new SubAction(() ->
362 {
363 node.parent.children.remove(node);
364 node.parent.children.add(oldIndex, node);
365 node.fireEvent(XsdTreeNode.MOVED, new Object[] {node, newIndex, oldIndex});
366 }, () ->
367 {
368 node.parent.children.remove(node);
369 node.parent.children.add(newIndex, node);
370 node.fireEvent(XsdTreeNode.MOVED, new Object[] {node, oldIndex, newIndex});
371 }, "Move " + node.getPathString()));
372 }
373 else if (event.getType().equals(CoupledValidator.COUPLING))
374 {
375 if (this.currentSet == null)
376 {
377 return;
378 }
379 Object[] content = (Object[]) event.getContent();
380 CoupledValidator validator = (CoupledValidator) content[0];
381 XsdTreeNode fromNode = (XsdTreeNode) content[1];
382 XsdTreeNode toNode = (XsdTreeNode) content[2];
383 XsdTreeNode prevToNode = (XsdTreeNode) content[3];
384 Consumer<XsdTreeNode> consumer = (node) ->
385 {
386 if (node == null)
387 {
388 validator.removeCoupling(fromNode);
389 }
390 else
391 {
392 validator.addCoupling(fromNode, node);
393 }
394 fromNode.invalidate();
395 };
396 add(new SubAction(() -> consumer.accept(prevToNode), () -> consumer.accept(toNode),
397 "Coupling " + fromNode.getNodeName()));
398 }
399 }
400
401
402
403
404
405 private void listenAndUnlisten(final Event event)
406 {
407 if (event.getType().equals(OtsEditor.NEW_FILE))
408 {
409 XsdTreeNodeRoot root = (XsdTreeNodeRoot) event.getContent();
410 root.addListener(this, XsdTreeNodeRoot.NODE_CREATED);
411 root.addListener(this, XsdTreeNodeRoot.NODE_REMOVED);
412 root.addListener(this, CoupledValidator.COUPLING);
413 }
414 else if (event.getType().equals(XsdTreeNodeRoot.NODE_CREATED))
415 {
416 XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[0];
417 node.addListener(this, XsdTreeNode.VALUE_CHANGED);
418 node.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
419 node.addListener(this, XsdTreeNode.OPTION_CHANGED);
420 node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
421 node.addListener(this, XsdTreeNode.MOVED);
422 }
423 else if (event.getType().equals(XsdTreeNodeRoot.NODE_REMOVED))
424 {
425 XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[0];
426 node.removeListener(this, XsdTreeNode.VALUE_CHANGED);
427 node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
428 node.removeListener(this, XsdTreeNode.OPTION_CHANGED);
429 node.removeListener(this, XsdTreeNode.ACTIVATION_CHANGED);
430 node.removeListener(this, XsdTreeNode.MOVED);
431 }
432 }
433
434
435
436
437
438
439
440 public void setPostActionShowNode(final XsdTreeNode node)
441 {
442 this.queue.get(this.cursor).postActionShowNode = node;
443 }
444
445
446
447
448 private class Action
449 {
450
451
452
453 @SuppressWarnings("checkstyle:visibilitymodifier")
454 final ActionType type;
455
456
457 @SuppressWarnings("checkstyle:visibilitymodifier")
458 final Deque<SubAction> subActions;
459
460
461 @SuppressWarnings("checkstyle:visibilitymodifier")
462 final XsdTreeNode node;
463
464
465 @SuppressWarnings("checkstyle:visibilitymodifier")
466 final XsdTreeNode parent;
467
468
469 @SuppressWarnings("checkstyle:visibilitymodifier")
470 final String attribute;
471
472
473 @SuppressWarnings("checkstyle:visibilitymodifier")
474 XsdTreeNode postActionShowNode;
475
476
477
478
479
480
481
482
483
484 Action(final ActionType type, final Deque<SubAction> subActions, final XsdTreeNode node, final XsdTreeNode parent,
485 final String attribute)
486 {
487 this.type = type;
488 this.subActions = subActions;
489 this.node = node;
490 this.parent = parent;
491 this.attribute = attribute;
492 this.postActionShowNode = node;
493 }
494 }
495
496
497
498
499 public enum ActionType
500 {
501
502 ACTIVATE,
503
504
505 ADD,
506
507
508 ATTRIBUTE_CHANGE,
509
510
511 CUT,
512
513
514 DUPLICATE,
515
516
517 ID_CHANGE,
518
519
520 INSERT,
521
522
523 MOVE,
524
525
526 OPTION,
527
528
529 PASTE,
530
531
532 REMOVE,
533
534
535 VALUE_CHANGE,
536
537
538 ACTION;
539
540 @Override
541 public String toString()
542 {
543 return name().toLowerCase().replace("_", " ");
544 }
545 }
546
547
548
549
550 private static class SubAction
551 {
552
553 private Runnable undo;
554
555
556 private Runnable redo;
557
558
559 private String string;
560
561
562
563
564
565
566
567 SubAction(final Runnable undo, final Runnable redo, final String string)
568 {
569 this.undo = undo;
570 this.redo = redo;
571 this.string = string;
572 }
573
574
575
576
577 public void undo()
578 {
579 this.undo.run();
580 }
581
582
583
584
585 public void redo()
586 {
587 this.redo.run();
588 }
589
590 @Override
591 public String toString()
592 {
593 return this.string;
594 }
595 }
596
597 }