/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Authors: Iain Holmes * * Copyright 2002 - 2006 Iain Holmes * * This file is free software; you can redistribute it and/or * modify it under the terms of version 2 of the GNU Library General Public * License as published by the Free Software Foundation; * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #ifdef HAVE_CONFIG_H #include #endif #include #include "koto-undo-manager.h" G_DEFINE_TYPE (KotoUndoManager, koto_undo_manager, G_TYPE_OBJECT); #define GET_PRIVATE(o) \ (G_TYPE_INSTANCE_GET_PRIVATE ((o), KOTO_TYPE_UNDO_MANAGER, KotoUndoManagerPrivate)) enum { CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; struct _KotoUndoManagerPrivate { GList *contexts, *undo, *redo; KotoUndoContext *working; }; struct _KotoUndoContext { char *name; int count; GList *undoables; }; static void context_free (KotoUndoContext *ctxt) { GList *u; g_free (ctxt->name); for (u = ctxt->undoables; u; u = u->next) { KotoUndoable *undoable = u->data; koto_undoable_free (undoable); } g_list_free (ctxt->undoables); g_free (ctxt); } static KotoUndoContext * context_new (const char *name) { KotoUndoContext *ctxt; ctxt = g_new (KotoUndoContext, 1); ctxt->name = g_strdup (name); ctxt->count = 0; ctxt->undoables = NULL; return ctxt; } static void context_undo (KotoUndoContext *ctxt) { GList *p; for (p = ctxt->undoables; p; p = p->next) { KotoUndoable *u = p->data; if (u->undo) { u->undo (u->closure); } } } static void context_redo (KotoUndoContext *ctxt) { GList *p; for (p = g_list_last (ctxt->undoables); p; p = p->prev) { KotoUndoable *u = p->data; if (u->redo) { u->redo (u->closure); } } } static void finalize (GObject *object) { KotoUndoManager *manager; KotoUndoManagerPrivate *priv; GList *p; manager = KOTO_UNDO_MANAGER (object); priv = manager->priv; for (p = priv->contexts; p; p = p->next) { KotoUndoContext *ctxt = p->data; context_free (ctxt); } g_list_free (priv->contexts); G_OBJECT_CLASS (koto_undo_manager_parent_class)->finalize (object); } static void koto_undo_manager_class_init (KotoUndoManagerClass *klass) { GObjectClass *object_class; g_type_class_add_private (klass, sizeof (KotoUndoManagerPrivate)); object_class = G_OBJECT_CLASS (klass); object_class->finalize = finalize; signals[CHANGED] = g_signal_new ("changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE, G_STRUCT_OFFSET (KotoUndoManagerClass, changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void koto_undo_manager_init (KotoUndoManager *manager) { manager->priv = GET_PRIVATE (manager); } KotoUndoManager * koto_undo_manager_new (void) { KotoUndoManager *manager; manager = g_object_new (KOTO_TYPE_UNDO_MANAGER, NULL); return manager; } gboolean koto_undo_manager_can_undo (KotoUndoManager *manager) { return (manager->priv->undo != NULL); } gboolean koto_undo_manager_can_redo (KotoUndoManager *manager) { return (manager->priv->redo != NULL); } /* FIXME: Should these return copies of the names so that they're threadsafe? */ const char * koto_undo_manager_get_undo_name (KotoUndoManager *manager) { if (manager->priv->undo) { KotoUndoContext *ctxt = manager->priv->undo->data; return ctxt->name; } else { return ""; } } const char * koto_undo_manager_get_redo_name (KotoUndoManager *manager) { if (manager->priv->redo) { KotoUndoContext *ctxt = manager->priv->redo->data; return ctxt->name; } else { return ""; } } KotoUndoContext * koto_undo_manager_context_begin (KotoUndoManager *manager, const char *name) { KotoUndoContext *ctxt; if (manager->priv->working != NULL) { manager->priv->working->count++; return manager->priv->working; } ctxt = context_new (name); ctxt->count++; manager->priv->working = ctxt; return ctxt; } KotoUndoContext * koto_undo_manager_context_begin_formatted (KotoUndoManager *manager, const char *format, ...) { KotoUndoContext *ctxt; va_list args; char *name; va_start (args, format); name = g_strdup_vprintf (format, args); ctxt = koto_undo_manager_context_begin (manager, name); g_free (name); va_end (args); return ctxt; } void koto_undo_manager_context_end (KotoUndoManager *manager, KotoUndoContext *ctxt) { /* TODO: handle start then end without any adds correctly */ KotoUndoManagerPrivate *priv; priv = manager->priv; ctxt->count--; if (ctxt->count > 0) { return; } if (priv->contexts == NULL) { priv->contexts = g_list_append (NULL, ctxt); priv->undo = priv->contexts; } else { GList *pruned = priv->redo; GList *n; if (pruned != NULL) { GList *p; /* Disconnect pruned */ if (priv->redo && priv->redo->prev) { priv->redo->prev->next = NULL; } /* If redo->prev == NULL then we've pruned everything */ if (priv->redo && priv->redo->prev == NULL) { priv->contexts = NULL; } priv->redo = NULL; pruned->prev = NULL; for (p = pruned; p; p = p->next) { KotoUndoContext *c = p->data; context_free (c); } g_list_free (pruned); } /* Append our new context and move the position pointer on */ n = g_list_append (priv->undo, ctxt); if (priv->undo == NULL) { priv->undo = n; } else { priv->undo = priv->undo->next; } /* If priv->contexts is NULL, then we pruned everything and need to start a new list */ if (priv->contexts == NULL) { priv->contexts = priv->undo; } priv->redo = NULL; } priv->working = NULL; g_signal_emit (manager, signals[CHANGED], 0); } void koto_undo_manager_context_cancel (KotoUndoManager *manager, KotoUndoContext *ctxt) { g_return_if_fail (manager->priv->working == ctxt); manager->priv->working = NULL; context_free (ctxt); } void koto_undo_manager_undo (KotoUndoManager *manager) { if (manager->priv->undo) { context_undo ((KotoUndoContext *) manager->priv->undo->data); manager->priv->redo = manager->priv->undo; manager->priv->undo = manager->priv->undo->prev; g_signal_emit (manager, signals[CHANGED], 0); } } void koto_undo_manager_redo (KotoUndoManager *manager) { if (manager->priv->redo) { context_redo ((KotoUndoContext *) manager->priv->redo->data); manager->priv->undo = manager->priv->redo; manager->priv->redo = manager->priv->redo->next; g_signal_emit (manager, signals[CHANGED], 0); } } GList * koto_undo_manager_get_history (KotoUndoManager *manager) { GList *history = NULL; GList *p; KotoUndoHistory *hist; hist = g_new (KotoUndoHistory, 1); hist->name = g_strdup (_("Original Sample")); hist->current = FALSE; hist->ctxt = NULL; history = g_list_prepend (history, hist); for (p = manager->priv->contexts; p; p = p->next) { KotoUndoContext *ctxt = p->data; hist = g_new (KotoUndoHistory, 1); hist->name = g_strdup (ctxt->name); hist->ctxt = ctxt; history = g_list_append (history, hist); if (p == manager->priv->undo) { hist->current = TRUE; } else { hist->current = FALSE; } } return history; } void koto_undo_context_add (KotoUndoContext *ctxt, KotoUndoable *undoable) { ctxt->undoables = g_list_prepend (ctxt->undoables, undoable); }