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 if (action.type.equals(ActionType.ACTIVATE))
180 {
181 this.editor.collapse(action.node);
182 }
183 Iterator<SubAction> iterator = action.subActions.descendingIterator();
184 while (iterator.hasNext())
185 {
186 iterator.next().undo();
187 }
188 action.parent.children.forEach((n) -> n.invalidate());
189 action.parent.invalidate();
190 this.editor.show(action.node, action.attribute);
191 this.cursor--;
192 updateButtons();
193 this.ignoreChanges = false;
194 }
195
196
197
198
199 public synchronized void redo()
200 {
201 if (this.ignoreChanges)
202 {
203 return;
204 }
205 this.ignoreChanges = true;
206 this.cursor++;
207 Action action = this.queue.get(this.cursor);
208 Iterator<SubAction> iterator = action.subActions.iterator();
209 while (iterator.hasNext())
210 {
211 iterator.next().redo();
212 }
213 action.parent.children.forEach((n) -> n.invalidate());
214 action.parent.invalidate();
215 this.editor.show(action.postActionShowNode, action.attribute);
216 updateButtons();
217 this.ignoreChanges = false;
218 }
219
220
221
222
223 public void updateButtons()
224 {
225 this.undoItem.setEnabled(canUndo());
226 this.undoItem.setText(canUndo() ? ("Undo " + this.queue.get(this.cursor).type) : "Undo");
227 this.redoItem.setEnabled(canRedo());
228 this.redoItem.setText(canRedo() ? ("Redo " + this.queue.get(this.cursor + 1).type) : "Redo");
229 }
230
231 @Override
232 public void notify(final Event event) throws RemoteException
233 {
234
235 if (event.getType().equals(OtsEditor.NEW_FILE))
236 {
237 XsdTreeNodeRoot root = (XsdTreeNodeRoot) event.getContent();
238 root.addListener(this, XsdTreeNodeRoot.NODE_CREATED);
239 root.addListener(this, XsdTreeNodeRoot.NODE_REMOVED);
240 }
241 else if (event.getType().equals(XsdTreeNodeRoot.NODE_CREATED))
242 {
243 XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[0];
244 node.addListener(this, XsdTreeNode.VALUE_CHANGED);
245 node.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
246 node.addListener(this, XsdTreeNode.OPTION_CHANGED);
247 node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
248 node.addListener(this, XsdTreeNode.MOVED);
249 }
250 else if (event.getType().equals(XsdTreeNodeRoot.NODE_REMOVED))
251 {
252 XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[0];
253 node.removeListener(this, XsdTreeNode.VALUE_CHANGED);
254 node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
255 node.removeListener(this, XsdTreeNode.OPTION_CHANGED);
256 node.removeListener(this, XsdTreeNode.ACTIVATION_CHANGED);
257 node.removeListener(this, XsdTreeNode.MOVED);
258 }
259
260
261 if (this.ignoreChanges)
262 {
263 return;
264 }
265
266
267 if (event.getType().equals(XsdTreeNodeRoot.NODE_CREATED))
268 {
269 Object[] content = (Object[]) event.getContent();
270 XsdTreeNode node = (XsdTreeNode) content[0];
271 XsdTreeNode parent = (XsdTreeNode) content[1];
272 int index = (int) content[2];
273 XsdTreeNode root = node.getRoot();
274 add(new SubActionRunnable(() ->
275 {
276 parent.children.remove(node);
277 node.parent = null;
278 root.fireEvent(XsdTreeNodeRoot.NODE_REMOVED, new Object[] {node, parent, index});
279 }, () ->
280 {
281 if (index >= 0)
282 {
283 parent.setChild(index, node);
284 }
285 node.parent = parent;
286 root.fireEvent(XsdTreeNodeRoot.NODE_CREATED, new Object[] {node, parent, index});
287 }, "Create " + node.getPathString()));
288 }
289 else if (event.getType().equals(XsdTreeNodeRoot.NODE_REMOVED))
290 {
291 Object[] content = (Object[]) event.getContent();
292 XsdTreeNode node = (XsdTreeNode) content[0];
293 XsdTreeNode parent = (XsdTreeNode) content[1];
294 int index = (int) content[2];
295 XsdTreeNode root = parent.getRoot();
296 add(new SubActionRunnable(() ->
297 {
298 if (index < 0)
299 {
300
301 node.parent = parent;
302 root.fireEvent(XsdTreeNodeRoot.NODE_CREATED, new Object[] {node, parent, parent.children.indexOf(node)});
303 }
304 else
305 {
306 parent.setChild(index, node);
307 root.fireEvent(XsdTreeNodeRoot.NODE_CREATED, new Object[] {node, parent, index});
308 }
309 }, () ->
310 {
311 node.parent.children.remove(node);
312 node.parent = null;
313 root.fireEvent(XsdTreeNodeRoot.NODE_REMOVED, new Object[] {node, parent, index});
314 }, "Remove " + node.getPathString()));
315 }
316 else if (event.getType().equals(XsdTreeNode.VALUE_CHANGED))
317 {
318 Object[] content = (Object[]) event.getContent();
319 XsdTreeNode node = (XsdTreeNode) content[0];
320 String value = node.getValue();
321 add(new SubActionRunnable(() ->
322 {
323 node.setValue((String) content[1]);
324 node.invalidate();
325 }, () ->
326 {
327 node.setValue(value);
328 node.invalidate();
329 }, "Change " + node.getPathString() + " value: " + value));
330 }
331 else if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED))
332 {
333 Object[] content = (Object[]) event.getContent();
334 XsdTreeNode node = (XsdTreeNode) content[0];
335 String attribute = (String) content[1];
336 String value = node.getAttributeValue(attribute);
337
338 if (node.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
339 {
340 this.currentSet.clear();
341 }
342 add(new SubActionRunnable(() ->
343 {
344 node.setAttributeValue(attribute, (String) content[2]);
345 node.invalidate();
346 }, () ->
347 {
348 node.setAttributeValue(attribute, value);
349 node.invalidate();
350 }, "Create " + node.getPathString() + ".@" + attribute + ": " + value));
351 }
352 else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
353 {
354 Object[] content = (Object[]) event.getContent();
355 XsdTreeNode node = (XsdTreeNode) content[0];
356 boolean activated = (boolean) content[1];
357 add(new SubActionRunnable(() ->
358 {
359 node.active = !activated;
360 node.fireEvent(XsdTreeNode.ACTIVATION_CHANGED, new Object[] {node, !activated});
361 }, () ->
362 {
363 node.active = activated;
364 node.fireEvent(XsdTreeNode.ACTIVATION_CHANGED, new Object[] {node, activated});
365 }, "Activation " + node.getPathString() + " " + activated));
366 }
367 else if (event.getType().equals(XsdTreeNode.OPTION_CHANGED))
368 {
369 Object[] content = (Object[]) event.getContent();
370 XsdTreeNode node = (XsdTreeNode) content[1];
371 XsdTreeNode previous = (XsdTreeNode) content[2];
372 if (previous != null)
373 {
374 add(new SubActionRunnable(() ->
375 {
376 node.setOption(previous);
377 }, () ->
378 {
379 previous.setOption(node);
380 }, "Set option " + node.getPathString()));
381 }
382 }
383 else if (event.getType().equals(XsdTreeNode.MOVED))
384 {
385 Object[] content = (Object[]) event.getContent();
386 XsdTreeNode node = (XsdTreeNode) content[0];
387 int oldIndex = (int) content[1];
388 int newIndex = (int) content[2];
389 add(new SubActionRunnable(() ->
390 {
391 node.parent.children.remove(node);
392 node.parent.children.add(oldIndex, node);
393 node.fireEvent(XsdTreeNode.MOVED, new Object[] {node, newIndex, oldIndex});
394 }, () ->
395 {
396 node.parent.children.remove(node);
397 node.parent.children.add(newIndex, node);
398 node.fireEvent(XsdTreeNode.MOVED, new Object[] {node, oldIndex, newIndex});
399 }, "Move " + node.getPathString()));
400
401 }
402 }
403
404
405
406
407
408 public void setPostActionShowNode(final XsdTreeNode node)
409 {
410 this.queue.get(this.cursor).postActionShowNode = node;
411 }
412
413
414
415
416
417
418
419
420
421
422 private interface SubAction
423 {
424
425
426
427 void undo();
428
429
430
431
432 void redo();
433 }
434
435
436
437
438
439
440
441
442
443
444 private static class SubActionRunnable implements SubAction
445 {
446
447 private Runnable undo;
448
449
450 private Runnable redo;
451
452
453 private String string;
454
455
456
457
458
459
460
461 public SubActionRunnable(final Runnable undo, final Runnable redo, final String string)
462 {
463 this.undo = undo;
464 this.redo = redo;
465 this.string = string;
466 }
467
468 @Override
469 public void undo()
470 {
471 this.undo.run();
472 }
473
474 @Override
475 public void redo()
476 {
477 this.redo.run();
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 @Override
587 public String toString()
588 {
589 return name().toLowerCase().replace("_", " ");
590 }
591 }
592
593 }