1
2
3
4
5
6
7
8
9
10
11
12 package sk.uniba.euromath.document;
13 import java.util.ArrayList;
14 import java.util.EnumSet;
15 import java.util.List;
16 import org.w3c.dom.Node;
17 import org.w3c.dom.events.Event;
18 import org.w3c.dom.events.EventListener;
19 import org.w3c.dom.events.EventTarget;
20 import org.w3c.dom.events.MutationEvent;
21 import sk.baka.ikslibs.EventClassEnum;
22 import sk.baka.ikslibs.EventTypesEnum;
23 import sk.baka.ikslibs.INodeObserver;
24 import sk.baka.ikslibs.modify.DOMMutils;
25 import sk.baka.ikslibs.ptr.DomPointer;
26 import sk.baka.ikslibs.ptr.DomPointerFactory;
27 /***
28 * Manages undoing of the modification operations. Thread-unsafe.
29 * @author Martin Vysny
30 * @TODO Move to the ikslibs library.
31 */
32 public final class UndoManager implements EventListener, INodeObserver {
33 /***
34 * Constructs an empty undo manager.
35 */
36 public UndoManager() {
37 super();
38
39 getLastGroup();
40 }
41 /***
42 * Contains the undo history. A list of undo groups. Each list is a list of
43 * actions that has to be taken (in reversed order - from latest to first)
44 * in order to undo to the mark.
45 */
46 private final List<UndoGroup> history = new ArrayList<UndoGroup>();
47 /***
48 * Index of active undo group in history. All items before this item are the
49 * undo history, all items after this item are operations that were redoed.
50 */
51 private int historyItem = 0;
52 /***
53 * If <code>true</code> then we are performing an {@link #undo() undo} or
54 * {@link #redo() redo} operation and node mutation events must be ignored.
55 */
56 private boolean ignoringEvents = false;
57 /***
58 * Marks the end of previous modification operations group and the start of
59 * a new group. When undo is requested all modifications are rolled back to
60 * nearest mark point.
61 */
62 public void mark() {
63 mark(false);
64 }
65 /***
66 * Marks the end of previous modification operations group and the start of
67 * a new group. When undo is requested all modifications are rolled back to
68 * nearest mark point.
69 * @param savePoint if <code>true</code> then this mark marks a point when
70 * the document was saved.
71 */
72 public void mark(final boolean savePoint) {
73
74
75 final UndoGroup last = getLastGroup();
76 if (!last.canUndo()) {
77 if (savePoint)
78 last.isSavePoint = savePoint;
79 return;
80 }
81
82 final UndoGroup newGroup = new UndoGroup();
83 newGroup.isSavePoint = savePoint;
84 history.add(newGroup);
85 purgeHistory();
86 }
87 /***
88 * Checks if the manager is currently 'standing' on the savepoint mark.
89 * @return <code>true</code> if last undo group is empty (it contains no
90 * actions) and savepoint.
91 */
92 public boolean isSavePoint() {
93 final UndoGroup last = getLastGroup();
94 if (last.canUndo())
95 return false;
96 return last.isSavePoint;
97 }
98 /***
99 * Represents an undo actions group. Contains undoable actions.
100 * @author Martin Vysny
101 */
102 private final class UndoGroup {
103 /***
104 * A list of actions that has to be taken (in reversed order - from
105 * latest to first) in order to undo the whole group.
106 */
107 private final List<IUndoAction> actions = new ArrayList<IUndoAction>();
108 /***
109 * Current action that is about to be undone. After
110 * {@link #performUndo()} it will be zero, after {@link #performRedo()}
111 * it will be equal to <code>actions.size() - 1</code>.
112 */
113 private int currentAction = -1;
114 /***
115 * Checks if there are some actions to be undone.
116 * @return <code>true</code> if some actions are left to be undone.
117 */
118 public boolean canUndo() {
119 return currentAction >= 0;
120 }
121 /***
122 * Checks if there are some actions that were undone.
123 * @return <code>true</code> if some actions can be redone.
124 */
125 public boolean canRedo() {
126 return currentAction < actions.size() - 1;
127 }
128 /***
129 * If <code>true</code> then this mark marks a point when the document
130 * was saved.
131 */
132 private boolean isSavePoint;
133 /***
134 * Performs the undo operation. Rollbacks all actions. If no actions
135 * were performed then nothing is done.
136 */
137 public void performUndo() {
138 if (!canUndo())
139 return;
140
141 ignoringEvents = true;
142 try {
143 for (; currentAction >= 0; currentAction--) {
144 actions.get(currentAction).undo();
145 }
146 } finally {
147 ignoringEvents = false;
148 }
149 }
150 /***
151 * Performs the redo operation. Re-executes all previously undone
152 * actions. Does nothing when there are no actions to be redone.
153 */
154 public void performRedo() {
155 if (!canRedo())
156 return;
157
158 ignoringEvents = true;
159 try {
160 for (; currentAction < actions.size() - 1; currentAction++) {
161 actions.get(currentAction + 1).redo();
162 }
163 } finally {
164 ignoringEvents = false;
165 }
166 }
167 /***
168 * Adds new undo action.
169 * @param action the undo action to add.
170 */
171 public void add(final IUndoAction action) {
172 if (canRedo())
173 throw new IllegalStateException(
174 "There are some actions that were undone.");
175 actions.add(action);
176 currentAction++;
177 }
178 /***
179 * Removes all redoable operations.
180 */
181 public void clearRedoOps() {
182 while (actions.size() - 1 > currentAction) {
183 actions.remove(actions.size() - 1);
184 }
185 assert !canRedo();
186 }
187 }
188 /***
189 * Deletes all oldest marks in order for the history list to contain at most
190 * maximum allowed number of the marks.
191 */
192 private void purgeHistory() {
193 int purgeCount = history.size() - maxMarkCount;
194 if (purgeCount <= 0)
195 return;
196 historyItem -= purgeCount;
197 assert historyItem >= 0;
198 for (; purgeCount > 0; purgeCount--) {
199 history.remove(0);
200 }
201 }
202 /***
203 * Removes all operations that were undoed and still can be redoed.
204 */
205 private void clearRedoOps() {
206
207 while (history.size() - 1 > historyItem) {
208 history.remove(history.size() - 1);
209 }
210 getLastGroup().clearRedoOps();
211 assert !canRedo();
212 }
213 /***
214 * Returns last undo group.
215 * @return undo list of last mark, never <code>null</code>.
216 */
217 private UndoGroup getLastGroup() {
218 if (history.isEmpty()) {
219 final UndoGroup result = new UndoGroup();
220 result.isSavePoint = true;
221 history.add(result);
222 historyItem = 0;
223 return result;
224 }
225 return history.get(historyItem);
226 }
227 /***
228 * <p>
229 * Rollbacks all previous modification operations up to the nearest mark
230 * point. If no operations were performed then nothing is done.
231 * </p>
232 * <p>
233 * The document must be in the modifying mode ({@link DocumentModifier#startModify()}
234 * must have been called).
235 * </p>
236 */
237 public void undo() {
238 if (!canUndo())
239 return;
240
241 UndoGroup last = getLastGroup();
242 if (!last.canUndo() && (historyItem > 0))
243 historyItem--;
244
245 last = getLastGroup();
246
247 last.performUndo();
248 }
249 /***
250 * Checks if there are some undoable operations.
251 * @return <code>true</code> if there are some undoable operations,
252 * <code>false</code> otherwise.
253 */
254 public boolean canUndo() {
255 return getLastGroup().canUndo() || (historyItem > 0);
256 }
257 /***
258 * <p>
259 * Redoes previously undone modification operations up to the nearest mark
260 * point. If no operations can be redone then nothing is done.
261 * </p>
262 * <p>
263 * The document must be in the modifying mode ({@link DocumentModifier#startModify()}
264 * must have been called).
265 * </p>
266 */
267 public void redo() {
268 if (!canRedo())
269 return;
270
271 final UndoGroup last = getLastGroup();
272 assert last.canRedo();
273 last.performRedo();
274 if (historyItem < history.size() - 1)
275 historyItem++;
276 }
277 /***
278 * Checks if there are some undo actions that can be applied back to the
279 * document.
280 * @return <code>true</code> if there are some redoable operations,
281 * <code>false</code> otherwise.
282 */
283 public boolean canRedo() {
284 if (history.size() - 1 > historyItem)
285 return true;
286 return getLastGroup().canRedo();
287 }
288 /***
289 * Maximum mark count, defaults to <code>10</code>.
290 */
291 private int maxMarkCount = 10;
292 /***
293 * Returns the maximum number of marks. Defaults to <code>5</code>.
294 * @return maximum length of undo history.
295 */
296 public int getMarkCount() {
297 return maxMarkCount;
298 }
299 /***
300 * Sets new maximum count of marks - a maximum undo history length. If new
301 * mark exceeds this limit then first (oldest) mark is forgotten and undo is
302 * impossible beyond that mark point.
303 * @param count the maximum marks count.
304 */
305 public void setMarkCount(int count) {
306 if (count <= 0)
307 throw new IllegalArgumentException("Count must be greater than 0.");
308 maxMarkCount = count;
309 purgeHistory();
310 }
311 /***
312 * Standard interface for all remembered undo actions.
313 * @author Martin Vysny
314 */
315 private interface IUndoAction {
316 /***
317 * Undoes the action. The operation performed depends on the type of the
318 * action.
319 */
320 public void undo();
321 /***
322 * Redoes the action. Can be called only after the {@link #undo()}
323 * method was called.
324 */
325 public void redo();
326 }
327 /***
328 * The node was deleted or inserted.
329 * @author Martin Vysny
330 */
331 private class UndoActionMoveNode implements IUndoAction {
332 /***
333 * Which node to insert.
334 */
335 private final Node node;
336 /***
337 * Where to insert the node. If <code>isParent</code> is false then
338 * the node is inserted before this node, otherwise it is inserted as a
339 * last child node in this node.
340 */
341 private final Node where;
342 /***
343 * Where to insert the node. If false then the node is inserted before
344 * the <code>where</code> node otherwise it is inserted as a last
345 * child node in the <code>where</code> node.
346 */
347 private final boolean isParent;
348 /***
349 * If <code>true</code> then the node was inserted. {@link #undo()}
350 * will delete the node, {@link #redo()} will reinsert the node. If
351 * <code>false</code> then the node is about to be deleted.
352 * {@link #undo()} will reinsert the node, {@link #redo()} will delete
353 * the node.
354 */
355 private final boolean inserted;
356 /***
357 * Constructs the action.
358 * @param node Which node to insert. The node must not be present in the
359 * document.
360 * @param inserted if <code>true</code> then the node was inserted.
361 * {@link #undo()} will delete the node, {@link #redo()} will reinsert
362 * the node. If <code>false</code> then the node is about to be
363 * deleted. {@link #undo()} will reinsert the node, {@link #redo()} will
364 * delete the node.
365 */
366 private UndoActionMoveNode(final Node node, final boolean inserted) {
367 super();
368 this.node = node;
369 this.isParent = (node.getNextSibling() == null);
370 this.where = isParent ? node.getParentNode() : node
371 .getNextSibling();
372 this.inserted = inserted;
373 }
374
375
376
377
378 public void undo() {
379 if (inserted)
380 deleteNode();
381 else
382 insertNode();
383 }
384 /***
385 * Inserts the node at given position.
386 */
387 private void insertNode() {
388 if (node.getParentNode() != null)
389 throw new IllegalStateException(
390 "Node is present in the document");
391 final DomPointer ptr;
392 if (isParent) {
393 ptr = DomPointerFactory.create(where, (Node) null);
394 } else {
395 ptr = DomPointerFactory.create(where);
396 }
397 DOMMutils.insertNode(node, ptr, false);
398 }
399 /***
400 * Removes the node from given position.
401 */
402 private void deleteNode() {
403 if (node.getParentNode() == null)
404 throw new IllegalStateException(
405 "Node is not present in the document");
406 node.getParentNode().removeChild(node);
407 }
408
409
410
411
412 public void redo() {
413 if (inserted)
414 insertNode();
415 else
416 deleteNode();
417 }
418 }
419 /***
420 * Modifies the node using old values from the <code>ChangeTracer</code>.
421 * @author Martin Vysny
422 */
423 private class UndoActionModifyNode implements IUndoAction {
424 /***
425 * New data of node, see {@link MutationEvent#getNewValue()}.
426 */
427 private final String newData;
428 /***
429 * New data of node, see {@link MutationEvent#getPrevValue()}.
430 */
431 private final String prevData;
432 /***
433 * The mutated node.
434 */
435 private final Node node;
436 /***
437 * Constructs action that modifies the node.
438 * @param event used to perform the modification operation.
439 */
440 private UndoActionModifyNode(final MutationEvent event) {
441 super();
442 newData = event.getNewValue();
443 prevData = event.getPrevValue();
444 node = EventClassEnum.Mutation.getMutatedNode(event);
445 }
446
447
448
449
450 public void undo() {
451 DOMMutils.setText(node, prevData);
452 }
453
454
455
456
457 public void redo() {
458 DOMMutils.setText(node, newData);
459 }
460 }
461
462
463
464
465 public void handleEvent(Event evt) {
466 if (ignoringEvents)
467 return;
468 final EventTypesEnum e = EventTypesEnum.fromEvent(evt);
469 if ((e.getEventClass() != EventClassEnum.Mutation) || !e.isRaw())
470 return;
471
472 final MutationEvent mevt = (MutationEvent) evt;
473 clearRedoOps();
474 final UndoGroup last = getLastGroup();
475 final Node mutatedNode = EventClassEnum.Mutation.getMutatedNode(mevt);
476 if (EventClassEnum.Mutation.isRemoval(mevt)) {
477
478
479 last.add(new UndoActionMoveNode(mutatedNode, false));
480 } else if (EventClassEnum.Mutation.isAddition(mevt)) {
481
482 last.add(new UndoActionMoveNode(mutatedNode, true));
483 } else {
484 assert EventClassEnum.Mutation.isModification(mevt);
485
486 last.add(new UndoActionModifyNode(mevt));
487 }
488 }
489
490
491
492
493 public void observe(Node node) {
494 final EventTarget et = (EventTarget) node;
495 EventTypesEnum.addEventListenerTo(et, this, true, EnumSet.of(
496 EventTypesEnum.MAttrModified,
497 EventTypesEnum.MCharacterDataModified,
498 EventTypesEnum.MNodeInserted, EventTypesEnum.MNodeRemoved));
499 }
500
501
502
503
504 public void stopObservation(Node node) {
505 final EventTarget et = (EventTarget) node;
506 EventTypesEnum.removeEventListenerFrom(et, this, true, EnumSet.of(
507 EventTypesEnum.MAttrModified,
508 EventTypesEnum.MCharacterDataModified,
509 EventTypesEnum.MNodeInserted, EventTypesEnum.MNodeRemoved));
510 }
511 }