/*================================================================== * SwamiUndo.c - Swami undo routines * * Swami * Copyright (C) 1999-2003 Josh Green * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA or point your web browser to http://www.gnu.org. * * To contact the author of this program: * Email: Josh Green * Swami homepage: http://swami.sourceforge.net *==================================================================*/ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include "SwamiLog.h" #include "SwamiObject.h" #include "SwamiUndoFuncs.h" #define ENTRY_CHUNK_OPTIMUM_AREA 64 #define ITEM_CHUNK_OPTIMUM_AREA 128 #define ERRMSG_NO_ACTIVE_UNDO_GROUP "No active undo group" #define ERRMSG_ACTIVE_UNDO_GROUP "No undo group should be active" /* --- private function prototypes --- */ static SwamiUndoItem *new_item (void); static void free_item (SwamiUndoItem *doitem); static void new_entry (SwamiObject *swami); static void remove_curpos_entry (SwamiObject *swami); /* --- private data --- */ static GMemChunk *entry_chunk; static GMemChunk *item_chunk; static gboolean undo_initialized = FALSE; /* --- functions --- */ /** * swami_undo_system_init: * * Used internally to initialize undo system */ void swami_undo_system_init (void) { if (undo_initialized) return; entry_chunk = g_mem_chunk_create (SwamiUndoEntry, ENTRY_CHUNK_OPTIMUM_AREA, G_ALLOC_AND_FREE); item_chunk = g_mem_chunk_create (SwamiUndoItem, ITEM_CHUNK_OPTIMUM_AREA, G_ALLOC_AND_FREE); undo_initialized = TRUE; } #if 0 /* dump undo tree for debugging purposes */ void sfdo_debug_dump (GNode *start, gint sibling, gboolean traverse, gboolean detail) { GList *l = NULL, *p, *p2; GNode *n; SwamiUndoEntry *entry; SwamiUndoItem *item; gint prev, next; if (!start) start = sfdo_tree.curpos; /* no start specified, set to current pos */ else if (sibling >= 0) { /* start from the 'sibling' index of 'start' */ n = start->parent; /* get parent of 'start' */ if (n) { n = g_node_nth_child (n, sibling); /* find the sibling by index */ if (n) start = n; } } if (traverse) { /* traverse up the tree */ n = start; while (n && n->data) { entry = (SwamiUndoEntry *)(n->data); l = g_list_prepend (l, n); n = n->parent; } /* traverse down the tree via the default redo path */ n = start; if (n) n = n->children; while (n && n->data) { entry = (SwamiUndoEntry *)(n->data); l = g_list_append (l, n); n = n->children; } } else l = g_list_append (l, start); /* !traverse, show just one node */ printf (" %c ROOT\n", (sfdo_tree.curpos == sfdo_tree.root) ? '+' : '/'); p = l; prev = 0; next = 0; while (p) { n = (GNode *)(p->data); p = g_list_next (p); entry = (SwamiUndoEntry *)(n->data); if (n->parent) { prev = g_node_child_position (n->parent, n); next = g_node_n_children (n->parent) - prev - 1; } printf ("%c%-2d%c%2d%c (%lx) %s\n", prev ? '<' : ' ', prev, (n == sfdo_tree.curpos) ? '+' : '|', next, next ? '>' : ' ', (long)(n), entry->descr); if (!detail) continue; p2 = g_list_last (entry->items); if (!p2) printf ("\t\n"); while (p2) { item = (SwamiUndoItem *)(p2->data); printf ("\t| (state = %lx) %s\n", (long)(item->state), sfdofuncs[item->type].descr); p2 = g_list_previous (p2); } } } #endif /** * swami_undo_group_start: * @swami: Swami object * @descr: Description of group action * * Start an undo group. Undo groups are used to group multiple actions. Groups * can be nested, but sub-groups are only remembered for the currently active * #SwamiUndoEntry. */ void swami_undo_group_start (SwamiObject *swami, const char *descr) { GList *p; SwamiUndo *undo; SwamiUndoEntry *entry; g_return_if_fail (swami != NULL); undo = swami->undo; if (undo->running) return; if (undo->groups) /* sub group? */ { /* ? grouping already active? */ /* ?: yes, get last GList * SwamiUndoItem of last group */ entry = (SwamiUndoEntry *) (undo->curpos->data); p = entry->items; /* first (most recent) group item */ } else /* top level group */ { new_entry (swami); /* ?: no, create new entry */ p = NULL; /* last GList * of previous group is NULL */ /* copy the description to the entry if provided */ if (descr) ((SwamiUndoEntry *)(undo->curpos->data))->descr = g_strdup (descr); } undo->groups = g_list_prepend (undo->groups, p); } /** * swami_undo_group_end: * @swami: Swami object * * End an undo group */ void swami_undo_group_end (SwamiObject *swami) { GList *p; SwamiUndo *undo; g_return_if_fail (swami != NULL); undo = swami->undo; if (undo->running) return; if (undo->groups == NULL) { SWAMI_CRITICAL (ERRMSG_NO_ACTIVE_UNDO_GROUP); return; } p = undo->groups; /* first (most recent) group item */ /* remove and free group node */ undo->groups = g_list_remove_link (undo->groups, p); g_list_free_1 (p); } /** * swami_undo_item_add: * @swami: Swami object * @type: Undo item type * @state: Pointer to state data for this item * * Add an item to the active open group entry */ void swami_undo_item_add (SwamiObject *swami, int type, gpointer state) { SwamiUndo *undo; SwamiUndoEntry *entry; SwamiUndoItem *item; g_return_if_fail (swami != NULL); undo = swami->undo; /* create a new group if there isn't one already open */ if (undo->groups == NULL) new_entry (swami); if (type <= SWAMI_UNDO_NONE || type >= SWAMI_UNDO_LAST) { SWAMI_PARAM_ERROR ("type"); return; } /* create and load the SwamiUndoItem */ item = new_item (); item->type = type; item->state = state; entry = (SwamiUndoEntry *) (undo->curpos->data); entry->items = g_list_prepend (entry->items, item); } /** * swami_undo_retract: * @swami: Swami object * * Undo all items in current sub group and remove it. Does not store redo * information for any items. This function is useful when doing complex * operations with multiple actions that could fail. * See #swami_undo_retract_all to retract all nested group actions. */ void swami_undo_retract (SwamiObject *swami) { GList *p, *p2; SwamiUndo *undo; SwamiUndoEntry *entry; SwamiUndoItem *doitem; g_return_if_fail (swami != NULL); undo = swami->undo; if (undo->groups == NULL) { SWAMI_CRITICAL (ERRMSG_NO_ACTIVE_UNDO_GROUP); return; } undo->running = TRUE; /* to stop recursive undo calls */ entry = (SwamiUndoEntry *)(undo->curpos->data); /* current entry */ p = entry->items; /* first item of current entry */ /* loop while items and item isn't the last item of the previous group */ while (p && p->data != undo->groups->data) { doitem = (SwamiUndoItem *)(p->data); /* call undo function */ (*swami_undo_type_info[doitem->type].restore) (swami, doitem); /* call free state function */ if (swami_undo_type_info[doitem->type].free) (*swami_undo_type_info[doitem->type].free) (swami, doitem); p2 = p; p = g_list_next (p); /* must advance here, as p will be removed */ /* remove SwamiUndoItem GList node */ entry->items = g_list_remove_link (entry->items, p2); g_list_free_1 (p2); free_item (doitem); /* free the SwamiUndoItem structure */ } /* free last group SwamiUndoItem pointer, thereby destroying the group */ undo->groups = g_list_remove_link (undo->groups, undo->groups); if (!undo->groups) remove_curpos_entry (swami); undo->running = FALSE; } /** * swami_undo_retract_all: * @swami: Swami object * * Undo items in all current nested groups and remove them. Does not store redo * information for any items. This function is useful when doing complex * operations with multiple actions that could fail. * See #swami_undo_retract to retract only top level sub group. */ void swami_undo_retract_all (SwamiObject *swami) { SwamiUndo *undo; g_return_if_fail (swami != NULL); undo = swami->undo; if (undo->groups == NULL) { SWAMI_CRITICAL (ERRMSG_NO_ACTIVE_UNDO_GROUP); return; } while (undo->groups) swami_undo_retract (swami); } /** * swami_undo: * @swami: Swami Object * * Undo current entry in undo history */ void swami_undo (SwamiObject *swami) { GList *p; SwamiUndo *undo; SwamiUndoEntry *entry; SwamiUndoItem *undoitem, *newitem; g_return_if_fail (swami != NULL); undo = swami->undo; /* groups should not be active */ if (undo->groups != NULL) { SWAMI_CRITICAL (ERRMSG_ACTIVE_UNDO_GROUP); return; } /* return if no entries to undo */ if (undo->curpos == undo->root) return; undo->running = TRUE; /* to stop recursive undo calls */ entry = (SwamiUndoEntry *)(undo->curpos->data); /* current entry */ p = entry->items; /* first item of current entry */ /* loop while items */ while (p) { undoitem = (SwamiUndoItem *)(p->data); /* item to undo */ newitem = new_item (); /* new item to store redo state data */ /* load redo item using undo item as reference */ (*swami_undo_type_info[undoitem->type].restate) (swami, undoitem, newitem); /* restore state from undo item */ (*swami_undo_type_info[undoitem->type].restore) (swami, undoitem); /* free old undo item state and structure */ if (swami_undo_type_info[undoitem->type].free) (*swami_undo_type_info[undoitem->type].free) (swami, undoitem); free_item (undoitem); p->data = newitem; /* replace old undo GList data ptr with redo ptr */ p = g_list_next (p); /* advance forward in list (backwards in time) */ } /* advance curpos up the tree */ undo->curpos = undo->curpos->parent; undo->running = FALSE; } /** * swami_redo: * @swami: Swami object * @node: Child of current position in history or NULL to redo most * recent entry * * Redo an entry in undo history tree */ void swami_redo (SwamiObject *swami, GNode *node) { GList *p; GNode *n; SwamiUndo *undo; SwamiUndoEntry *entry; SwamiUndoItem *redoitem, *newitem; g_return_if_fail (swami != NULL); undo = swami->undo; /* groups should not be active */ if (undo->groups != NULL) { SWAMI_CRITICAL (ERRMSG_ACTIVE_UNDO_GROUP); return; } n = undo->curpos->children; if (node) /* redo node specified? */ { /* look for child node matching "node" */ while (n && n != node) n = g_node_next_sibling (n); if (n == NULL) { SWAMI_PARAM_ERROR ("node"); return; } } /* return if no entries to redo */ if (undo->curpos->children == NULL) return; undo->running = TRUE; /* to stop recursive undo calls */ entry = (SwamiUndoEntry *)(n->data); /* entry to redo */ p = g_list_last (entry->items); /* last item of entry to redo */ /* loop while items */ while (p) { redoitem = (SwamiUndoItem *)(p->data); /* item to redo */ newitem = new_item (); /* new item to store undo state data */ /* load undo item using redo item as reference */ (*swami_undo_type_info[redoitem->type].restate) (swami, redoitem, newitem); /* restore state from redo item */ (*swami_undo_type_info[redoitem->type].restore) (swami, redoitem); /* free old redo item state and structure */ if (swami_undo_type_info[redoitem->type].free) (*swami_undo_type_info[redoitem->type].free) (swami, redoitem); free_item (redoitem); p->data = newitem; /* replace old redo GList data ptr with undo ptr */ p = g_list_previous (p); /* advance back in list (forwards in time) */ } /* advance curpos to redone node */ undo->curpos = n; undo->running = FALSE; } /** * swami_undo_jump: * @swami: Swami object * @node: #SwamiUndoEntry node in undo state history to jump to * * Jump to a position in undo state history, undoing and redoing items to * get to restore state. */ void swami_undo_jump (SwamiObject *swami, GNode *node) { GNode *n; GSList *srclist = NULL, *dstlist = NULL; GSList *srcp, *dstp; SwamiUndo *undo; g_return_if_fail (swami != NULL); undo = swami->undo; /* groups should not be active */ if (undo->groups != NULL) { SWAMI_CRITICAL (ERRMSG_ACTIVE_UNDO_GROUP); return; } n = undo->curpos; while (n) /* make a list of curpos ancestry */ { srclist = g_slist_prepend (srclist, n); n = n->parent; } n = node; while (n) /* make a list of destination ancestry */ { dstlist = g_slist_prepend (dstlist, n); n = n->parent; } srcp = srclist; dstp = dstlist; while (srcp && dstp) /* skip common ancestry */ { if (srcp->data != dstp->data) break; srcp = g_slist_next (srcp); dstp = g_slist_next (dstp); } while (srcp) /* undo from curpos up to common parent */ { swami_undo (swami); srcp = g_slist_next (srcp); } while (dstp) /* redo from common parent to destination */ { swami_redo (swami, (GNode *)(dstp->data)); dstp = g_slist_next (dstp); } g_slist_free (srclist); g_slist_free (dstlist); } /* allocate a new SwamiUndoItem and initialize to a NULL state */ static SwamiUndoItem * new_item (void) { SwamiUndoItem *item; item = g_chunk_new (SwamiUndoItem, item_chunk); item->type = SWAMI_UNDO_NONE; item->state = NULL; return (item); } static void free_item (SwamiUndoItem *doitem) { g_mem_chunk_free (item_chunk, doitem); } /* appends a new entry onto the curpos node, and makes it the curpos */ static void new_entry (SwamiObject *swami) { SwamiUndo *undo = swami->undo; SwamiUndoEntry *entry; /* grouping should not be active! */ g_return_if_fail (undo->groups == NULL); entry = g_chunk_new (SwamiUndoEntry, entry_chunk); entry->descr = NULL; entry->items = NULL; undo->curpos = g_node_prepend_data (undo->curpos, entry); } /* removes the current entry which must have NO SwamiUndoItems and NO children (i.e. bottom of the tree) */ static void remove_curpos_entry (SwamiObject *swami) { GNode *n; SwamiUndo *undo = swami->undo; SwamiUndoEntry *entry; /* curpos should be the bottom of the tree (no GNode children!) */ g_return_if_fail (undo->curpos->children == NULL); n = undo->curpos; /* current position GNode */ entry = (SwamiUndoEntry *)(n->data); /* get current entry */ g_return_if_fail (entry->items == NULL); /* fail if it has SwamiUndoItems */ undo->curpos = undo->curpos->parent; /* move curpos to parent */ /* if this entry has a description, free it */ if (entry->descr) g_free (entry->descr); g_mem_chunk_free (entry_chunk, entry); /* free the SwamiUndoEntry atom */ g_node_unlink (n); /* unlink the node from the tree */ g_node_destroy (n); /* destroy node */ }