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