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