View Javadoc

1   /*
2    * Copyright 1999-2006 Faculty of Mathematics, Physics and Informatics, Comenius
3    * University, Bratislava. This file is protected by the Mozilla Public License
4    * version 1.1 (the License); you may not use this file except in compliance
5    * with the License. You may obtain a copy of the License at
6    * http://euromath2.sourceforge.net/license.html Unless required by applicable
7    * law or agreed to in writing, software distributed under the License is
8    * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
9    * KIND, either express or implied. See the License for the specific language
10   * governing permissions and limitations under the License.
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  		// create an empty undo group.
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  		// check if previous mark has some actions. if not then there is no need
74  		// to create new mark
75  		final UndoGroup last = getLastGroup();
76  		if (!last.canUndo()) {
77  			if (savePoint)
78  				last.isSavePoint = savePoint;
79  			return;
80  		}
81  		// create new undo group.
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 			// temporarily remove our listener.
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 			// temporarily remove our listener.
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."); //$NON-NLS-1$
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 		// remove all redoable history items.
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 		// check if previous mark has some actions. if not then delete it.
241 		UndoGroup last = getLastGroup();
242 		if (!last.canUndo() && (historyItem > 0))
243 			historyItem--;
244 		// get all actions of the last mark.
245 		last = getLastGroup();
246 		// rollback the last group.
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 		// redo.
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."); //$NON-NLS-1$
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 		 * (non-Javadoc)
376 		 * @see sk.uniba.euromath.document.UndoManager.IUndoAction#undo()
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"); //$NON-NLS-1$
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"); //$NON-NLS-1$
406 			node.getParentNode().removeChild(node);
407 		}
408 		/*
409 		 * (non-Javadoc)
410 		 * @see sk.uniba.euromath.document.UndoManager.IUndoAction#redo()
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 		 * (non-Javadoc)
448 		 * @see sk.uniba.euromath.document.UndoManager.IUndoAction#undo()
449 		 */
450 		public void undo() {
451 			DOMMutils.setText(node, prevData);
452 		}
453 		/*
454 		 * (non-Javadoc)
455 		 * @see sk.uniba.euromath.document.UndoManager.IUndoAction#redo()
456 		 */
457 		public void redo() {
458 			DOMMutils.setText(node, newData);
459 		}
460 	}
461 	/*
462 	 * (non-Javadoc)
463 	 * @see org.w3c.dom.events.EventListener#handleEvent(org.w3c.dom.events.Event)
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 		// create undo action
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 			// the node is about to be deleted. When undoing then it shall be
478 			// created back.
479 			last.add(new UndoActionMoveNode(mutatedNode, false));
480 		} else if (EventClassEnum.Mutation.isAddition(mevt)) {
481 			// the node was added. Undoing shall delete it.
482 			last.add(new UndoActionMoveNode(mutatedNode, true));
483 		} else {
484 			assert EventClassEnum.Mutation.isModification(mevt);
485 			// the node was modified
486 			last.add(new UndoActionModifyNode(mevt));
487 		}
488 	}
489 	/*
490 	 * (non-Javadoc)
491 	 * @see sk.baka.ikslibs.INodeObserver#observe(org.w3c.dom.Node)
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 	 * (non-Javadoc)
502 	 * @see sk.baka.ikslibs.INodeObserver#stopObservation(org.w3c.dom.Node)
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 }