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