1 /*
2  * tkUndo.c --
3  *
4  *	This module provides the implementation of an undo stack.
5  *
6  * Copyright © 2002 Ludwig Callewaert.
7  * Copyright © 2003-2004 Vincent Darley.
8  *
9  * See the file "license.terms" for information on usage and redistribution of
10  * this file, and for a DISCLAIMER OF ALL WARRANTIES.
11  */
12 
13 #include "tkInt.h"
14 #include "tkUndo.h"
15 
16 static int		EvaluateActionList(Tcl_Interp *interp,
17 			    TkUndoSubAtom *action);
18 
19 /*
20  *----------------------------------------------------------------------
21  *
22  * TkUndoPushStack --
23  *
24  *	Push elem on the stack identified by stack.
25  *
26  * Results:
27  *	None.
28  *
29  * Side effects:
30  *	None.
31  *
32  *----------------------------------------------------------------------
33  */
34 
35 void
TkUndoPushStack(TkUndoAtom ** stack,TkUndoAtom * elem)36 TkUndoPushStack(
37     TkUndoAtom **stack,
38     TkUndoAtom *elem)
39 {
40     elem->next = *stack;
41     *stack = elem;
42 }
43 
44 /*
45  *----------------------------------------------------------------------
46  *
47  * TkUndoPopStack --
48  *
49  *	Remove and return the top element from the stack identified by stack.
50  *
51  * Results:
52  *	None.
53  *
54  * Side effects:
55  *	None.
56  *
57  *----------------------------------------------------------------------
58  */
59 
60 TkUndoAtom *
TkUndoPopStack(TkUndoAtom ** stack)61 TkUndoPopStack(
62     TkUndoAtom **stack)
63 {
64     TkUndoAtom *elem = NULL;
65 
66     if (*stack != NULL) {
67 	elem = *stack;
68 	*stack = elem->next;
69     }
70     return elem;
71 }
72 
73 /*
74  *----------------------------------------------------------------------
75  *
76  * TkUndoInsertSeparator --
77  *
78  *	Insert a separator on the stack, indicating a border for an undo/redo
79  *	chunk.
80  *
81  * Results:
82  *	None.
83  *
84  * Side effects:
85  *	None.
86  *
87  *----------------------------------------------------------------------
88  */
89 
90 int
TkUndoInsertSeparator(TkUndoAtom ** stack)91 TkUndoInsertSeparator(
92     TkUndoAtom **stack)
93 {
94     TkUndoAtom *separator;
95 
96     if (*stack!=NULL && (*stack)->type!=TK_UNDO_SEPARATOR) {
97 	separator = (TkUndoAtom *)ckalloc(sizeof(TkUndoAtom));
98 	separator->type = TK_UNDO_SEPARATOR;
99 	TkUndoPushStack(stack,separator);
100 	return 1;
101     }
102     return 0;
103 }
104 
105 /*
106  *----------------------------------------------------------------------
107  *
108  * TkUndoClearStack --
109  *
110  *	Clear an entire undo or redo stack and destroy all elements in it.
111  *
112  * Results:
113  *	None.
114  *
115  * Side effects:
116  *	None.
117  *
118  *----------------------------------------------------------------------
119  */
120 
121 void
TkUndoClearStack(TkUndoAtom ** stack)122 TkUndoClearStack(
123     TkUndoAtom **stack)		/* An Undo or Redo stack */
124 {
125     TkUndoAtom *elem;
126 
127     while ((elem = TkUndoPopStack(stack)) != NULL) {
128 	if (elem->type != TK_UNDO_SEPARATOR) {
129 	    TkUndoSubAtom *sub;
130 
131 	    sub = elem->apply;
132 	    while (sub != NULL) {
133 		TkUndoSubAtom *next = sub->next;
134 
135 		if (sub->action != NULL) {
136 		    Tcl_DecrRefCount(sub->action);
137 		}
138 		ckfree(sub);
139 		sub = next;
140 	    }
141 
142 	    sub = elem->revert;
143 	    while (sub != NULL) {
144 		TkUndoSubAtom *next = sub->next;
145 
146 		if (sub->action != NULL) {
147 		    Tcl_DecrRefCount(sub->action);
148 		}
149 		ckfree(sub);
150 		sub = next;
151 	    }
152 	}
153 	ckfree(elem);
154     }
155     *stack = NULL;
156 }
157 
158 /*
159  *----------------------------------------------------------------------
160  *
161  * TkUndoPushAction --
162  *
163  *	Push a new elem on the stack identified by stack. Action and revert
164  *	are given through Tcl_Obj's to which we will retain a reference. (So
165  *	they can be passed in with a zero refCount if desired).
166  *
167  * Results:
168  *	None.
169  *
170  * Side effects:
171  *	None.
172  *
173  *----------------------------------------------------------------------
174  */
175 
176 void
TkUndoPushAction(TkUndoRedoStack * stack,TkUndoSubAtom * apply,TkUndoSubAtom * revert)177 TkUndoPushAction(
178     TkUndoRedoStack *stack,	/* An Undo or Redo stack */
179     TkUndoSubAtom *apply,
180     TkUndoSubAtom *revert)
181 {
182     TkUndoAtom *atom;
183 
184     atom = (TkUndoAtom *)ckalloc(sizeof(TkUndoAtom));
185     atom->type = TK_UNDO_ACTION;
186     atom->apply = apply;
187     atom->revert = revert;
188 
189     TkUndoPushStack(&stack->undoStack, atom);
190     TkUndoClearStack(&stack->redoStack);
191 }
192 
193 /*
194  *----------------------------------------------------------------------
195  *
196  * TkUndoMakeCmdSubAtom --
197  *
198  *	Create a new undo/redo step which must later be place into an undo
199  *	stack with TkUndoPushAction. This sub-atom, if evaluated, will take
200  *	the given command (if non-NULL), find its full Tcl command string, and
201  *	then evaluate that command with the list elements of 'actionScript'
202  *	appended.
203  *
204  *	If 'subAtomList' is non-NULL, the newly created sub-atom is added onto
205  *	the end of the linked list of which 'subAtomList' is a part. This
206  *	makes it easy to build up a sequence of actions which will be pushed
207  *	in one step.
208  *
209  *	Note: if the undo stack can persist for longer than the Tcl_Command
210  *	provided, the stack will cause crashes when actions are evaluated. In
211  *	this case the 'command' argument should not be used. This is the case
212  *	with peer text widgets, for example.
213  *
214  * Results:
215  *	The newly created subAtom is returned. It must be passed to
216  *	TkUndoPushAction otherwise a memory leak will result.
217  *
218  * Side effects:
219  *	A refCount is retained on 'actionScript'.
220  *
221  *----------------------------------------------------------------------
222  */
223 
224 TkUndoSubAtom *
TkUndoMakeCmdSubAtom(Tcl_Command command,Tcl_Obj * actionScript,TkUndoSubAtom * subAtomList)225 TkUndoMakeCmdSubAtom(
226     Tcl_Command command,	/* Tcl command token for actions, may be NULL
227 				 * if not needed. */
228     Tcl_Obj *actionScript,	/* The script to append to the command to
229 				 * perform the action (may be NULL if the
230 				 * command is not-null). */
231     TkUndoSubAtom *subAtomList)	/* Add to the end of this list of actions if
232 				 * non-NULL */
233 {
234     TkUndoSubAtom *atom;
235 
236     if (command == NULL && actionScript == NULL) {
237 	Tcl_Panic("NULL command and actionScript in TkUndoMakeCmdSubAtom");
238     }
239 
240     atom = (TkUndoSubAtom *)ckalloc(sizeof(TkUndoSubAtom));
241     atom->command = command;
242     atom->funcPtr = NULL;
243     atom->clientData = NULL;
244     atom->next = NULL;
245     atom->action = actionScript;
246     if (atom->action != NULL) {
247         Tcl_IncrRefCount(atom->action);
248     }
249 
250     if (subAtomList != NULL) {
251 	while (subAtomList->next != NULL) {
252 	    subAtomList = subAtomList->next;
253 	}
254 	subAtomList->next = atom;
255     }
256     return atom;
257 }
258 
259 /*
260  *----------------------------------------------------------------------
261  *
262  * TkUndoMakeSubAtom --
263  *
264  *	Create a new undo/redo step which must later be place into an undo
265  *	stack with TkUndoPushAction. This sub-atom, if evaluated, will take
266  *	the given C-funcPtr (which must be non-NULL), and call it with three
267  *	arguments: the undo stack's 'interp', the 'clientData' given and the
268  *	'actionScript'. The callback should return a standard Tcl return code
269  *	(TCL_OK on success).
270  *
271  *	If 'subAtomList' is non-NULL, the newly created sub-atom is added onto
272  *	the end of the linked list of which 'subAtomList' is a part. This
273  *	makes it easy to build up a sequence of actions which will be pushed
274  *	in one step.
275  *
276  * Results:
277  *	The newly created subAtom is returned. It must be passed to
278  *	TkUndoPushAction otherwise a memory leak will result.
279  *
280  * Side effects:
281  *	A refCount is retained on 'actionScript'.
282  *
283  *----------------------------------------------------------------------
284  */
285 
286 TkUndoSubAtom *
TkUndoMakeSubAtom(TkUndoProc * funcPtr,ClientData clientData,Tcl_Obj * actionScript,TkUndoSubAtom * subAtomList)287 TkUndoMakeSubAtom(
288     TkUndoProc *funcPtr,	/* Callback function to perform the
289 				 * undo/redo. */
290     ClientData clientData,	/* Data to pass to the callback function. */
291     Tcl_Obj *actionScript,	/* Additional Tcl data to pass to the callback
292 				 * function (may be NULL). */
293     TkUndoSubAtom *subAtomList)	/* Add to the end of this list of actions if
294 				 * non-NULL */
295 {
296     TkUndoSubAtom *atom;
297 
298     if (funcPtr == NULL) {
299 	Tcl_Panic("NULL funcPtr in TkUndoMakeSubAtom");
300     }
301 
302     atom = (TkUndoSubAtom *)ckalloc(sizeof(TkUndoSubAtom));
303     atom->command = NULL;
304     atom->funcPtr = funcPtr;
305     atom->clientData = clientData;
306     atom->next = NULL;
307     atom->action = actionScript;
308     if (atom->action != NULL) {
309         Tcl_IncrRefCount(atom->action);
310     }
311 
312     if (subAtomList != NULL) {
313 	while (subAtomList->next != NULL) {
314 	    subAtomList = subAtomList->next;
315 	}
316 	subAtomList->next = atom;
317     }
318     return atom;
319 }
320 
321 /*
322  *----------------------------------------------------------------------
323  *
324  * TkUndoInitStack --
325  *
326  *	Initialize a new undo/redo stack.
327  *
328  * Results:
329  *	An Undo/Redo stack pointer.
330  *
331  * Side effects:
332  *	None.
333  *
334  *----------------------------------------------------------------------
335  */
336 
337 TkUndoRedoStack *
TkUndoInitStack(Tcl_Interp * interp,int maxdepth)338 TkUndoInitStack(
339     Tcl_Interp *interp,		/* The interpreter */
340     int maxdepth)		/* The maximum stack depth */
341 {
342     TkUndoRedoStack *stack;	/* An Undo/Redo stack */
343 
344     stack = (TkUndoRedoStack *)ckalloc(sizeof(TkUndoRedoStack));
345     stack->undoStack = NULL;
346     stack->redoStack = NULL;
347     stack->interp = interp;
348     stack->maxdepth = maxdepth;
349     stack->depth = 0;
350     return stack;
351 }
352 
353 /*
354  *----------------------------------------------------------------------
355  *
356  * TkUndoSetMaxDepth --
357  *
358  *	Set the maximum depth of stack.
359  *
360  * Results:
361  *	None.
362  *
363  * Side effects:
364  *	May delete elements from the stack if the new maximum depth is smaller
365  *	than the number of elements previously in the stack.
366  *
367  *----------------------------------------------------------------------
368  */
369 
370 void
TkUndoSetMaxDepth(TkUndoRedoStack * stack,int maxdepth)371 TkUndoSetMaxDepth(
372     TkUndoRedoStack *stack,	/* An Undo/Redo stack */
373     int maxdepth)		/* The maximum stack depth */
374 {
375     stack->maxdepth = maxdepth;
376 
377     if (stack->maxdepth>0 && stack->depth>stack->maxdepth) {
378 	TkUndoAtom *elem, *prevelem;
379 	int sepNumber = 0;
380 
381 	/*
382 	 * Maximum stack depth exceeded. We have to remove the last compound
383 	 * elements on the stack.
384 	 */
385 
386 	elem = stack->undoStack;
387 	prevelem = NULL;
388 	while ((elem != NULL) && (sepNumber <= stack->maxdepth)) {
389 	    if (elem->type == TK_UNDO_SEPARATOR) {
390 		sepNumber++;
391 	    }
392 	    prevelem = elem;
393 	    elem = elem->next;
394 	}
395 	CLANG_ASSERT(prevelem);
396 	prevelem->next = NULL;
397 	while (elem != NULL) {
398 	    prevelem = elem;
399 	    if (elem->type != TK_UNDO_SEPARATOR) {
400 		TkUndoSubAtom *sub = elem->apply;
401 		while (sub != NULL) {
402 		    TkUndoSubAtom *next = sub->next;
403 
404 		    if (sub->action != NULL) {
405 			Tcl_DecrRefCount(sub->action);
406 		    }
407 		    ckfree(sub);
408 		    sub = next;
409 		}
410 		sub = elem->revert;
411 		while (sub != NULL) {
412 		    TkUndoSubAtom *next = sub->next;
413 
414 		    if (sub->action != NULL) {
415 			Tcl_DecrRefCount(sub->action);
416 		    }
417 		    ckfree(sub);
418 		    sub = next;
419 		}
420 	    }
421 	    elem = elem->next;
422 	    ckfree(prevelem);
423 	}
424 	stack->depth = stack->maxdepth;
425     }
426 }
427 
428 /*
429  *----------------------------------------------------------------------
430  *
431  * TkUndoClearStacks --
432  *
433  *	Clear both the undo and redo stack.
434  *
435  * Results:
436  *	None.
437  *
438  * Side effects:
439  *	None.
440  *
441  *----------------------------------------------------------------------
442  */
443 
444 void
TkUndoClearStacks(TkUndoRedoStack * stack)445 TkUndoClearStacks(
446     TkUndoRedoStack *stack)	/* An Undo/Redo stack */
447 {
448     TkUndoClearStack(&stack->undoStack);
449     TkUndoClearStack(&stack->redoStack);
450     stack->depth = 0;
451 }
452 
453 /*
454  *----------------------------------------------------------------------
455  *
456  * TkUndoFreeStack
457  *
458  *	Clear both the undo and redo stack and free the memory allocated to
459  *	the u/r stack pointer.
460  *
461  * Results:
462  *	None.
463  *
464  * Side effects:
465  *	None.
466  *
467  *----------------------------------------------------------------------
468  */
469 
470 void
TkUndoFreeStack(TkUndoRedoStack * stack)471 TkUndoFreeStack(
472     TkUndoRedoStack *stack)	/* An Undo/Redo stack */
473 {
474     TkUndoClearStacks(stack);
475     ckfree(stack);
476 }
477 
478 /*
479  *----------------------------------------------------------------------
480  *
481  * TkUndoCanRedo --
482  *
483  *	Returns true if redo is possible, i.e. if the redo stack is not empty.
484  *
485  * Results:
486  *	 A boolean.
487  *
488  * Side effects:
489  *	None.
490  *
491  *----------------------------------------------------------------------
492  */
493 
494 int
TkUndoCanRedo(TkUndoRedoStack * stack)495 TkUndoCanRedo(
496     TkUndoRedoStack *stack)	/* An Undo/Redo stack */
497 {
498     return stack->redoStack != NULL;
499 }
500 
501 /*
502  *----------------------------------------------------------------------
503  *
504  * TkUndoCanUndo --
505  *
506  *	Returns true if undo is possible, i.e. if the undo stack is not empty.
507  *
508  * Results:
509  *	 A boolean.
510  *
511  * Side effects:
512  *	None.
513  *
514  *----------------------------------------------------------------------
515  */
516 
517 int
TkUndoCanUndo(TkUndoRedoStack * stack)518 TkUndoCanUndo(
519     TkUndoRedoStack *stack)	/* An Undo/Redo stack */
520 {
521     return stack->undoStack != NULL;
522 }
523 
524 /*
525  *----------------------------------------------------------------------
526  *
527  * TkUndoInsertUndoSeparator --
528  *
529  *	Insert a separator on the undo stack, indicating a border for an
530  *	undo/redo chunk.
531  *
532  * Results:
533  *	None.
534  *
535  * Side effects:
536  *	None.
537  *
538  *----------------------------------------------------------------------
539  */
540 
541 void
TkUndoInsertUndoSeparator(TkUndoRedoStack * stack)542 TkUndoInsertUndoSeparator(
543     TkUndoRedoStack *stack)
544 {
545     if (TkUndoInsertSeparator(&stack->undoStack)) {
546 	stack->depth++;
547 	TkUndoSetMaxDepth(stack, stack->maxdepth);
548     }
549 }
550 
551 /*
552  *----------------------------------------------------------------------
553  *
554  * TkUndoRevert --
555  *
556  *	Undo a compound action on the stack.
557  *
558  * Results:
559  *	A Tcl status code
560  *
561  * Side effects:
562  *	None.
563  *
564  *----------------------------------------------------------------------
565  */
566 
567 int
TkUndoRevert(TkUndoRedoStack * stack)568 TkUndoRevert(
569     TkUndoRedoStack *stack)
570 {
571     TkUndoAtom *elem;
572 
573     /*
574      * Insert a separator on the undo and the redo stack.
575      */
576 
577     TkUndoInsertUndoSeparator(stack);
578     TkUndoInsertSeparator(&stack->redoStack);
579 
580     /*
581      * Pop and skip the first separator if there is one.
582      */
583 
584     elem = TkUndoPopStack(&stack->undoStack);
585     if (elem == NULL) {
586 	return TCL_ERROR;
587     }
588 
589     if (elem->type == TK_UNDO_SEPARATOR) {
590 	ckfree(elem);
591 	elem = TkUndoPopStack(&stack->undoStack);
592     }
593 
594     while (elem != NULL && elem->type != TK_UNDO_SEPARATOR) {
595 	/*
596 	 * Note that we currently ignore errors thrown here.
597 	 */
598 
599 	EvaluateActionList(stack->interp, elem->revert);
600 
601 	TkUndoPushStack(&stack->redoStack, elem);
602 	elem = TkUndoPopStack(&stack->undoStack);
603     }
604 
605     /*
606      * Insert a separator on the redo stack.
607      */
608 
609     TkUndoInsertSeparator(&stack->redoStack);
610     stack->depth--;
611     return TCL_OK;
612 }
613 
614 /*
615  *----------------------------------------------------------------------
616  *
617  * TkUndoApply --
618  *
619  *	Redo a compound action on the stack.
620  *
621  * Results:
622  *	A Tcl status code
623  *
624  * Side effects:
625  *	None.
626  *
627  *----------------------------------------------------------------------
628  */
629 
630 int
TkUndoApply(TkUndoRedoStack * stack)631 TkUndoApply(
632     TkUndoRedoStack *stack)
633 {
634     TkUndoAtom *elem;
635 
636     /*
637      * Insert a separator on the undo stack.
638      */
639 
640     TkUndoInsertSeparator(&stack->undoStack);
641 
642     /*
643      * Pop and skip the first separator if there is one.
644      */
645 
646     elem = TkUndoPopStack(&stack->redoStack);
647     if (elem == NULL) {
648 	return TCL_ERROR;
649     }
650 
651     if (elem->type == TK_UNDO_SEPARATOR) {
652 	ckfree(elem);
653 	elem = TkUndoPopStack(&stack->redoStack);
654     }
655 
656     while (elem != NULL && elem->type != TK_UNDO_SEPARATOR) {
657 	/*
658 	 * Note that we currently ignore errors thrown here.
659 	 */
660 
661 	EvaluateActionList(stack->interp, elem->apply);
662 
663 	TkUndoPushStack(&stack->undoStack, elem);
664 	elem = TkUndoPopStack(&stack->redoStack);
665     }
666 
667     /*
668      * Insert a separator on the undo stack.
669      */
670 
671     TkUndoInsertSeparator(&stack->undoStack);
672     stack->depth++;
673     return TCL_OK;
674 }
675 
676 /*
677  *----------------------------------------------------------------------
678  *
679  * EvaluateActionList --
680  *
681  *	Execute a linked list of undo/redo sub-atoms. If any sub-atom returns
682  *	a non TCL_OK value, execution of subsequent sub-atoms is cancelled and
683  *	the error returned immediately.
684  *
685  * Results:
686  *	A Tcl status code
687  *
688  * Side effects:
689  *	The undo/redo subAtoms can perform arbitrary actions.
690  *
691  *----------------------------------------------------------------------
692  */
693 
694 static int
EvaluateActionList(Tcl_Interp * interp,TkUndoSubAtom * action)695 EvaluateActionList(
696     Tcl_Interp *interp,		/* Interpreter to evaluate the action in. */
697     TkUndoSubAtom *action)	/* Head of linked list of action steps to
698 				 * perform. */
699 {
700     int result = TCL_OK;
701 
702     while (action != NULL) {
703 	if (action->funcPtr != NULL) {
704 	    result = action->funcPtr(interp, action->clientData,
705 		    action->action);
706 	} else if (action->command != NULL) {
707 	    Tcl_Obj *cmdNameObj, *evalObj;
708 
709 	    cmdNameObj = Tcl_NewObj();
710 	    evalObj = Tcl_NewObj();
711 	    Tcl_IncrRefCount(evalObj);
712 	    Tcl_GetCommandFullName(interp, action->command, cmdNameObj);
713 	    Tcl_ListObjAppendElement(NULL, evalObj, cmdNameObj);
714 	    if (action->action != NULL) {
715 	        Tcl_ListObjAppendList(NULL, evalObj, action->action);
716 	    }
717 	    result = Tcl_EvalObjEx(interp, evalObj, TCL_EVAL_GLOBAL);
718 	    Tcl_DecrRefCount(evalObj);
719 	} else {
720 	    result = Tcl_EvalObjEx(interp, action->action, TCL_EVAL_GLOBAL);
721 	}
722 	if (result != TCL_OK) {
723 	    return result;
724 	}
725 	action = action->next;
726     }
727     return result;
728 }
729 
730 /*
731  * Local Variables:
732  * mode: c
733  * c-basic-offset: 4
734  * fill-column: 78
735  * End:
736  */
737