Undo stack for GTK+ (was: Re: undo in textview)



Cross-posting to move the discussion to gtk-devel-list. Anybody interested
in the topic, please follow up there.

On Do, 24.09.2009 19:23, A. Walton wrote:

>It's definitely something many developers would love to see in Gtk+,
>but only a few have stepped up to the bat with patches and actually
>discussed the problem,

Why don't we take the opportunity to discuss the problem now, then? I
can start by offering my view on how an undo stack should look like,
and provide a reference implementation as a basis of discussion.

The implementation is a git branch called "undo" based on gtk+
2.19.2, and can be found at git://github.com/hb/gtk.git
I attached a cumulative (squashed) version to this mail for convenience,
to be applied onto 2.19.2.

It consists of 3 parts: A GtkUndo class, a GtkUndoView class, and a tiny
test (demo) program in tests/testundo.

The code attached to https://bugzilla.gnome.org/show_bug.cgi?id=322194
and gundo kind of go into the same direction, but fail on specific, in
my oppinion important points (mainly one or multiple of the following:
taking into account that undo and redo operations can fail, providing
human-readable descriptions, limiting stack size, nesting groups).

At the core, there should be a general undo class that should do the
stack management. Let's call it GtkUndo. This class should be derived
directly from GObject.

Basically, a user of the undo stack registers a set of callback
functions for undo and redo operations. They will get a user_data
argument, and return a true value if the undo/redo operation was
successful, or false if problems occured. If necessary, the
set can also contain a free() function to free resources associated
with the user_data.

Signals:
========

can-undo(boolean), can-redo(boolean)
Undo/redo changed from possible to impossible, or vice versa. Useful
for modifying e.g. menu item sensitivity according to whether the
undo/redo stacks have at least one entry or not.

changed(void)
undo and/or redo stacks have changed. Useful updating stack
information displays (e.g. a view of the complete stacks, or just
information about top level items in menus)


Properties:
===========

max-length
Integer that determins the maximum length of the undo stack.
10 means the stack can have at most 10 items, 0 means the undo stack
can't be filled, -1 means it grows indefinitely.


=================================================================

The generic undo class can be regarded as the model in a MVC pattern.
So we could have a view (let's call it GtkUndoView), which is some kind
of GtkWidget, that displays a given undo and/or redo stack, and listens to the
"changed" signal. This would basically provide a journal.

The patch also contains such a view. It currently looks rather clunky, and
is in the current state mainly useful for stack inspection and debugging.


=================================================================

Outlook:

GTK+ could have a GtkUndoable interface, that every class or
widget in GTK+ that wants to provide undo/redo capabilities should
implement. The interface should just be used to tell those widgets
which GtkUndo object to use (and if any, at all).

void gtk_buildable_set_undo(GtkUndo *undo);
GtkUndo* gtk_buildable_get_undo(void);

Candidates for that would probably be at least GtkEntry and
GtkTextView (or rather their respective models).

Holger
diff --git a/gtk/Makefile.am b/gtk/Makefile.am
index 8ec9abf..1a83c22 100644
--- a/gtk/Makefile.am
+++ b/gtk/Makefile.am
@@ -335,6 +335,8 @@ gtk_public_h_sources =          \
 	gtktreeviewcolumn.h	\
 	gtktypeutils.h		\
 	gtkuimanager.h		\
+	gtkundo.h		\
+	gtkundoview.h		\
 	gtkvbbox.h		\
 	gtkvbox.h		\
 	gtkviewport.h		\
@@ -615,6 +617,8 @@ gtk_base_c_sources =            \
 	gtktypebuiltins.c	\
 	gtktypeutils.c		\
 	gtkuimanager.c		\
+	gtkundo.c		\
+	gtkundoview.c		\
 	gtkvbbox.c		\
 	gtkvbox.c		\
 	gtkvolumebutton.c	\
diff --git a/gtk/gtk.h b/gtk/gtk.h
index 07952be..6986105 100644
--- a/gtk/gtk.h
+++ b/gtk/gtk.h
@@ -203,6 +203,8 @@
 #include <gtk/gtktreeviewcolumn.h>
 #include <gtk/gtktypeutils.h>
 #include <gtk/gtkuimanager.h>
+#include <gtk/gtkundo.h>
+#include <gtk/gtkundoview.h>
 #include <gtk/gtkvbbox.h>
 #include <gtk/gtkvbox.h>
 #include <gtk/gtkversion.h>
diff --git a/gtk/gtkundo.c b/gtk/gtkundo.c
new file mode 100644
index 0000000..c51e3a9
--- /dev/null
+++ b/gtk/gtkundo.c
@@ -0,0 +1,888 @@
+/* gtkundo.c
+ * Copyright (C) 2009  Holger Berndt <berndth gmx de>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * 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.
+ */
+
+#include "config.h"
+
+#include "gtkundo.h"
+#include "gtkintl.h"
+#include "gtkmarshalers.h"
+#include "gtkprivate.h"
+
+
+/**
+ * SECTION:gtkundo
+ * @title: GtkUndo
+ * @short_description: Undo stack
+ *
+ * The #GtkUndo class implements an undo stack.
+ *
+ * TODO: Verbose description here
+ *
+ * Since: 2.20
+ */
+
+enum {
+  PROP_0,
+  PROP_MAX_LENGTH,
+};
+
+enum {
+  CAN_UNDO,
+  CAN_REDO,
+  CHANGED,
+  LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+struct _GtkUndoPrivate
+{
+  gint max_length;
+
+  /* these are lists of GNode's */
+  GList *undo_stack;
+  GList *redo_stack;
+  gint undo_length;
+  gint redo_length;
+  GHashTable *method_hash;
+  guint group_depth;
+};
+
+typedef struct _GtkUndoEntry GtkUndoEntry;
+struct _GtkUndoEntry {
+  gchar *description;
+  GtkUndoSet *set;
+  gpointer data;
+};
+
+
+G_DEFINE_TYPE (GtkUndo, gtk_undo, G_TYPE_OBJECT);
+
+
+/* --------------------------------------------------------------------------------
+ *
+ */
+
+/* Free an undo entry itself and all members. */
+static void
+free_entry (GtkUndoEntry *entry)
+{
+  /* Call virtual functions for data entries */
+  if(entry->set) {
+    if (entry->set->do_free)
+      entry->set->do_free(entry->data);
+  }
+  g_free(entry->description);
+  g_free(entry);
+}
+
+/* Change length of the undo stack */
+static void
+change_len_undo (GtkUndo *undo, gint num)
+{
+  undo->priv->undo_length = undo->priv->undo_length + num;
+
+  if ((num > 0) && (undo->priv->undo_length == 1))
+    g_signal_emit(undo, signals[CAN_UNDO], 0, TRUE);
+  else if ((num < 0) && (undo->priv->undo_length == 0))
+    g_signal_emit(undo, signals[CAN_UNDO], 0, FALSE);
+}
+
+/* Change length of the redo stack */
+static void
+change_len_redo(GtkUndo *undo, gint num)
+{
+  undo->priv->redo_length = undo->priv->redo_length + num;
+
+  if ((num > 0) && (undo->priv->redo_length == 1))
+    g_signal_emit (undo, signals[CAN_REDO], 0, TRUE);
+  else if ((num < 0) && (undo->priv->redo_length == 0))
+    g_signal_emit (undo, signals[CAN_REDO], 0, FALSE);
+}
+
+static gboolean
+traverse_free_entry (GNode *node, gpointer data)
+{
+  if (node && node->data)
+    free_entry ((GtkUndoEntry*)node->data);
+  return FALSE;
+}
+
+/* Clear the undo stack */
+static void
+clear_undo (GtkUndo *undo)
+{
+  GList *walk;
+
+  change_len_undo (undo, -undo->priv->undo_length);
+
+  for (walk = undo->priv->undo_stack; walk; walk = walk->next) {
+    g_node_traverse ((GNode*)walk->data, G_POST_ORDER, G_TRAVERSE_ALL, -1, traverse_free_entry, NULL);
+    g_node_destroy (walk->data);
+  }
+  g_list_free (undo->priv->undo_stack);
+  undo->priv->undo_stack = NULL;
+}
+
+static void
+clear_redo (GtkUndo *undo)
+{
+  GList *walk;
+
+  change_len_redo (undo, -undo->priv->redo_length);
+
+  for (walk = undo->priv->redo_stack; walk; walk = walk->next) {
+    g_node_traverse ((GNode*)walk->data, G_POST_ORDER, G_TRAVERSE_ALL, -1, traverse_free_entry, NULL);
+    g_node_destroy (walk->data);
+  }
+  g_list_free (undo->priv->redo_stack);
+  undo->priv->redo_stack = NULL;
+}
+
+static void
+free_stack_entry (GList **stack, GList *element)
+{
+  if (!element || !element->data)
+    return;
+
+  g_node_traverse ((GNode*)element->data, G_POST_ORDER, G_TRAVERSE_ALL, -1, traverse_free_entry, NULL);
+  g_node_destroy (element->data);
+  *stack = g_list_delete_link (*stack, element);
+}
+
+/* Free last element of undo stack (usually because the stack
+ * grew beyond its maximum length). */
+static void
+free_last_entry (GtkUndo *undo)
+{
+  free_stack_entry (&(undo->priv->undo_stack), g_list_last (undo->priv->undo_stack));
+  change_len_undo(undo, -1);
+}
+
+/* Free first element of undo stack (usually because an undo operation failed */
+static void
+free_first_entry (GtkUndo *undo, gboolean of_undo)
+{
+  if (of_undo) {
+    free_stack_entry (&(undo->priv->undo_stack), undo->priv->undo_stack);
+    change_len_undo(undo, -1);
+  }
+  else {
+    free_stack_entry (&(undo->priv->redo_stack), undo->priv->redo_stack);
+    change_len_redo(undo, -1);
+  }
+}
+
+static void
+destroy_undoset(gpointer data)
+{
+  GtkUndoSet *set = data;
+  g_free(set->description);
+  g_free(set);
+}
+
+static gboolean
+traverse_undo_node (GNode *node, gpointer data)
+{
+  GtkUndoEntry *entry;
+  gpointer *success;
+
+  success = data;
+  entry = node->data;
+
+  /* call callabck function */
+  if (entry && entry->set && entry->set->do_undo) {
+    if (!entry->set->do_undo(entry->data))
+      *success = FALSE;
+  }
+
+  return FALSE;
+}
+
+static gboolean
+traverse_redo_node (GNode *node, gpointer data)
+{
+  GtkUndoEntry *entry;
+  gpointer *success;
+
+  success = data;
+  entry = node->data;
+
+  /* call callabck function */
+  if (entry && entry->set && entry->set->do_redo) {
+    if (!entry->set->do_redo(entry->data))
+      *success = FALSE;
+  }
+
+  return FALSE;
+}
+
+static gboolean
+traverse_reverse_children (GNode *node, gpointer data)
+{
+  g_node_reverse_children (node);
+  return FALSE;
+}
+
+/* get best-fitting description for an entry. This is the
+ * description of the entry, or the description of the associated
+ * set, if no entry-description is available, or a dummy string
+ * if no set description is available either. */
+static char*
+get_entry_description (GtkUndoEntry *entry)
+{
+  gchar *desc;
+
+  if(entry->description)
+    desc = entry->description;
+  else if(entry->set && entry->set->description)
+    desc = entry->set->description;
+  else
+    desc = N_("<no description available>");
+  return desc;
+}
+
+static void
+get_descriptions_from_stack_add_node (GtkTreeStore *store, GNode *node, GtkTreeIter *parent_iter)
+{
+  GtkTreeIter iter;
+  guint n_children, ii;
+
+  gtk_tree_store_append (store, &iter, parent_iter);
+  gtk_tree_store_set (store, &iter, 0, get_entry_description (node->data), -1);
+
+  n_children = g_node_n_children (node);
+  for (ii = 0; ii < n_children; ii++) {
+    GNode *child;
+    child = g_node_nth_child (node, ii);
+    get_descriptions_from_stack_add_node (store, child, &iter);
+  }
+}
+
+static GtkTreeStore*
+get_descriptions_from_stack (GList *stack)
+{
+  GtkTreeStore *store;
+  GList *walk;
+
+  store = gtk_tree_store_new (1, G_TYPE_STRING);
+  for (walk = stack; walk; walk = walk->next)
+    get_descriptions_from_stack_add_node (store, walk->data, NULL);
+
+  return store;
+}
+
+static GNode*
+get_node_level (GNode *root, guint depth)
+{
+  GNode *child;
+  depth--;
+  for(child = root; depth; depth--)
+    child = g_node_first_child(child);
+  return child;
+}
+
+/* --------------------------------------------------------------------------------
+ *
+ */
+
+static void
+gtk_undo_init (GtkUndo *undo)
+{
+  GtkUndoPrivate *pv;
+
+  pv = undo->priv = G_TYPE_INSTANCE_GET_PRIVATE (undo, GTK_TYPE_UNDO, GtkUndoPrivate);
+
+  pv->max_length = -1;
+  pv->undo_stack = NULL;
+  pv->redo_stack = NULL;
+  pv->undo_length = 0;
+  pv->redo_length = 0;
+  pv->method_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, destroy_undoset);
+  pv->group_depth = 0;
+}
+
+static void
+gtk_undo_finalize (GObject *obj)
+{
+  GtkUndo *undo = GTK_UNDO (obj);
+
+  gtk_undo_clear (undo);
+  g_hash_table_destroy (undo->priv->method_hash);
+  G_OBJECT_CLASS (gtk_undo_parent_class)->finalize (obj);
+}
+
+static void
+gtk_undo_set_property (GObject      *obj,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
+{
+  GtkUndo *undo = GTK_UNDO (obj);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_LENGTH:
+      gtk_undo_set_max_length (undo, g_value_get_int (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_undo_get_property (GObject    *obj,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  GtkUndo *undo = GTK_UNDO (obj);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_LENGTH:
+      g_value_set_int (value, gtk_undo_get_max_length (undo));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_undo_class_init (GtkUndoClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  gobject_class->finalize = gtk_undo_finalize;
+  gobject_class->set_property = gtk_undo_set_property;
+  gobject_class->get_property = gtk_undo_get_property;
+
+  g_type_class_add_private (gobject_class, sizeof (GtkUndoPrivate));
+
+  /**
+   * GtkUndo:max-length:
+   *
+   * The maximum number of toplevel entries in the undo stack. -1 means no limit
+   *
+   * Since: 2.20
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_MAX_LENGTH,
+                                   g_param_spec_int ("max-length",
+                                                     P_("Maximum length"),
+                                                     P_("Maximum number of toplevel entries in the undo stack. -1 if no maximum"),
+                                   -1, GTK_UNDO_MAX_SIZE, -1,
+                                   GTK_PARAM_READWRITE));
+
+  /**
+   * GtkUndo::can-undo:
+   * @undo: a #GtkUndo
+   * @can_undo: TRUE if undo is possible
+   *
+   * This signal is emitted when the undo changes possibility state
+   *
+   * Since: 2.20
+   */
+  signals[CAN_UNDO] = g_signal_new (I_("can-undo"),
+                                       GTK_TYPE_UNDO,
+                                       G_SIGNAL_RUN_FIRST,
+                                       G_STRUCT_OFFSET (GtkUndoClass, can_undo),
+                                       NULL, NULL,
+                                       _gtk_marshal_VOID__BOOLEAN,
+                                       G_TYPE_NONE, 1,
+                                       G_TYPE_BOOLEAN);
+
+  /**
+   * GtkUndo::can-redo:
+   * @undo: a #GtkUndo
+   * @can_redo: TRUE if redo is possible
+   *
+   * This signal is emitted when the redo changes possibility state
+   *
+   * Since: 2.20
+   */
+  signals[CAN_REDO] = g_signal_new (I_("can-redo"),
+                                       GTK_TYPE_UNDO,
+                                       G_SIGNAL_RUN_FIRST,
+                                       G_STRUCT_OFFSET (GtkUndoClass, can_redo),
+                                       NULL, NULL,
+                                       _gtk_marshal_VOID__BOOLEAN,
+                                       G_TYPE_NONE, 1,
+                                       G_TYPE_BOOLEAN);
+
+  /**
+   * GtkUndo::changed:
+   * @undo: a #GtkUndo
+   *
+   * This signal is emitted whenever the undo stack changes
+   *
+   * Since: 2.20
+   */
+  signals[CHANGED] = g_signal_new (I_("changed"),
+                                      GTK_TYPE_UNDO,
+                                      G_SIGNAL_RUN_FIRST,
+                                      G_STRUCT_OFFSET (GtkUndoClass, can_redo),
+                                      NULL, NULL,
+                                      _gtk_marshal_VOID__VOID,
+                                      G_TYPE_NONE, 0);
+}
+
+/* --------------------------------------------------------------------------------
+ *
+ */
+
+/**
+ * gtk_undo_new:
+ *
+ * Create a new GtkUndo object.
+ *
+ * Return value: A new GtkUndo object.
+ *
+ * Since: 2.20
+ **/
+GtkUndo*
+gtk_undo_new (void)
+{
+  return g_object_new (GTK_TYPE_UNDO, NULL);
+}
+
+/**
+ * gtk_undo_new:
+ * @undo: a #GtkUndo
+ * @name: name for the set
+ * @set: the set
+ *
+ * Register an undo set. A set is a collection of functions
+ * that deal with undo/redo operations.
+ *
+ * Since: 2.20
+ **/
+void
+gtk_undo_register_set (GtkUndo *undo, const char *name, const GtkUndoSet *set)
+{
+  GtkUndoSet *val;
+
+  g_return_if_fail (GTK_IS_UNDO (undo) && name && set);
+
+  /* add the set to the method hash if it's not present yet */
+  if (g_hash_table_lookup (undo->priv->method_hash, name))
+    g_warning ("A set with the name '%s' has already been registered. Overriding.\n", name);
+
+  val = g_new0 (GtkUndoSet, 1);
+  *val = *set;
+  val->description = g_strdup (set->description);
+  g_hash_table_insert (undo->priv->method_hash, g_strdup (name), val);
+}
+
+/**
+ * gtk_undo_add:
+ * @undo: a #GtkUndo
+ * @set_name: name of the set dealing with this data
+ * @data: the data of the set
+ * @description: a human-readable description of what this data does
+ *
+ * Add add an entry to the undo stack. The @set_name has to have
+ * been registered before with gtk_undo_register_set.
+ *
+ * Return value: TRUE if the adding was successful, FALSE otherwise
+ *               (e.g. if a set with the given @set_name was not registered,
+ *               or the maximum allowed length of the undo stack is zero)
+ *
+ * Since: 2.20
+ **/
+gboolean
+gtk_undo_add (GtkUndo *undo, const char *set_name, gpointer data, const gchar *description)
+{
+  GtkUndoSet *set;
+  GtkUndoEntry *entry;
+
+  g_return_val_if_fail (GTK_IS_UNDO (undo) && set_name, FALSE);
+
+  if (undo->priv->max_length == 0)
+    return FALSE;
+
+  set = g_hash_table_lookup (undo->priv->method_hash, set_name);
+  if (!set) {
+    g_warning ("A set with the name '%s' has not been registered\n", set_name);
+    return FALSE;
+  }
+
+  entry = g_new0 (GtkUndoEntry, 1);
+  entry->description = g_strdup (description);
+  entry->set = set;
+  entry->data = data;
+
+  if (!undo->priv->group_depth) {
+    undo->priv->undo_stack = g_list_prepend (undo->priv->undo_stack, g_node_new (entry));
+    change_len_undo (undo, 1);
+    clear_redo (undo);
+    if ((undo->priv->max_length != -1) && (undo->priv->undo_length > undo->priv->max_length))
+      free_last_entry (undo);
+    g_signal_emit (undo, signals[CHANGED], 0);
+  }
+  else {
+    if (!(undo->priv->undo_stack && undo->priv->undo_stack->data)) {
+      g_warning ("Could not add grouped entry.\n");
+      return FALSE;
+    }
+    g_node_insert (get_node_level (undo->priv->undo_stack->data, undo->priv->group_depth), 0, g_node_new (entry));
+  }
+
+  return TRUE;
+}
+
+/**
+ * gtk_undo_undo:
+ * @undo: a #GtkUndo
+ *
+ * Undo the last operation.
+ *
+ * Return value: TRUE if undo was performed.
+ *
+ * Since: 2.20
+ **/
+gboolean
+gtk_undo_undo (GtkUndo *undo)
+{
+  gboolean success = TRUE;
+
+  g_return_val_if_fail (GTK_IS_UNDO (undo), FALSE);
+
+  if (!gtk_undo_can_undo(undo)) {
+    g_warning("Cannot undo.\n");
+    return FALSE;
+  }
+
+  g_node_traverse (undo->priv->undo_stack->data, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1, traverse_undo_node, &success);
+  if (success) {
+    /* move data to redo stack */
+    g_node_traverse (undo->priv->undo_stack->data, G_POST_ORDER, G_TRAVERSE_ALL, -1, traverse_reverse_children, NULL);
+    undo->priv->redo_stack = g_list_prepend (undo->priv->redo_stack, undo->priv->undo_stack->data);
+    change_len_redo (undo, 1);
+    undo->priv->undo_stack = g_list_delete_link (undo->priv->undo_stack, undo->priv->undo_stack);
+    change_len_undo(undo, -1);
+  }
+  else {
+    free_first_entry (undo, TRUE);
+    g_warning("undo operation failed\n");
+  }
+  g_signal_emit(undo, signals[CHANGED], 0);
+  return TRUE;
+}
+
+/**
+ * gtk_undo_redo:
+ * @undo: a #GtkUndo
+ *
+ * Redo the last operation.
+ *
+ * Return value: TRUE if redo was successful.
+ *
+ * Since: 2.20
+ **/
+gboolean
+gtk_undo_redo (GtkUndo *undo)
+{
+  gboolean success;
+
+  g_return_val_if_fail (GTK_IS_UNDO (undo), FALSE);
+
+  if (!gtk_undo_can_redo(undo)) {
+    g_warning("Cannot redo.\n");
+    return FALSE;
+  }
+
+  g_node_traverse (undo->priv->redo_stack->data, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1, traverse_redo_node, &success);
+  if (success) {
+    /* move data back to undo stack */
+    g_node_traverse (undo->priv->redo_stack->data, G_POST_ORDER, G_TRAVERSE_ALL, -1, traverse_reverse_children, NULL);
+    undo->priv->undo_stack = g_list_prepend (undo->priv->undo_stack, undo->priv->redo_stack->data);
+    change_len_undo (undo, 1);
+    undo->priv->redo_stack = g_list_delete_link (undo->priv->redo_stack, undo->priv->redo_stack);
+    change_len_redo (undo, -1);
+  }
+  else {
+    free_first_entry (undo, FALSE);
+    g_warning ("redo operation failed\n");
+  }
+  g_signal_emit(undo, signals[CHANGED], 0);
+  return TRUE;
+}
+
+/**
+ * gtk_undo_can_undo:
+ * @undo: a #GtkUndo
+ *
+ * Check if there's an object on the undo stack that can be undone
+ *
+ * Return value: TRUE if an object can be undone, FALSE otherwise.
+ *
+ * Since: 2.20
+ **/
+gboolean
+gtk_undo_can_undo (GtkUndo *undo)
+{
+  g_return_val_if_fail (GTK_IS_UNDO (undo), FALSE);
+  return ((undo->priv->undo_stack != NULL) && (undo->priv->group_depth == 0));
+}
+
+/**
+ * gtk_undo_can_redo:
+ * @undo: a #GtkUndo
+ *
+ * Check if there's an object on the redo stack that can be undone
+ *
+ * Return value: TRUE if an object can be redone, FALSE otherwise.
+ *
+ * Since: 2.20
+ **/
+gboolean
+gtk_undo_can_redo (GtkUndo *undo)
+{
+  g_return_val_if_fail (GTK_IS_UNDO (undo), FALSE);
+  return ((undo->priv->redo_stack != NULL) && (undo->priv->group_depth == 0));
+}
+
+/**
+ * gtk_undo_set_max_length:
+ * @undo: a #GtkUndo
+ * @max_length: the maximum length of the undo stack, or -1 for no maximum.
+ *   The value passed in will be clamped to the range -1 - 65536.
+ *
+ * Sets the maximum allowed length of the undo stack. If
+ * the current contents are longer than the given length, then they
+ * will be truncated to fit.
+ *
+ * Since: 2.20
+ **/
+void
+gtk_undo_set_max_length (GtkUndo *undo,
+                         gint     max_length)
+{
+  g_return_if_fail (GTK_IS_UNDO (undo));
+
+  if (undo->priv->group_depth != 0) {
+    g_warning ("Currently in group add mode. Cannot modify maximum length.\n");
+    return;
+  }
+
+  max_length = CLAMP (max_length, -1, GTK_UNDO_MAX_SIZE);
+
+  if (max_length != undo->priv->max_length) {
+    /* truncate list if necessary */
+    if (max_length != -1) {
+      gboolean something_changed;
+      if (undo->priv->undo_length > max_length)
+        something_changed = TRUE;
+      else
+        something_changed = FALSE;
+      while (undo->priv->undo_length > max_length)
+        free_last_entry (undo);
+      if (something_changed)
+        g_signal_emit (undo, signals[CHANGED], 0);
+    }
+    undo->priv->max_length = max_length;
+    g_object_notify (G_OBJECT (undo), "max-length");
+  }
+}
+
+/**
+ * gtk_undo_get_max_length:
+ * @undo: a #GtkUndo
+ *
+ * Retrieves the maximum allowed length of the @undo
+ * stack. See gtk_undo_set_max_length().
+ *
+ * Return value: the maximum length in the #GtkUndo stack,
+ *               or -1 if there is no maximum.
+ *
+ * Since: 2.20
+ */
+gint
+gtk_undo_get_max_length (GtkUndo *undo)
+{
+  g_return_val_if_fail (GTK_IS_UNDO (undo), -1);
+  return undo->priv->max_length;
+}
+
+/**
+ * gtk_undo_clear:
+ * @undo: a #GtkUndo
+ *
+ * Clears the undo stack.
+ *
+ * Return value: TRUE if clearing was successful. Clearing can
+ * fail e.g. if the stack is currently in group add mode.
+ *
+ * Since: 2.20
+ */
+gboolean
+gtk_undo_clear (GtkUndo *undo)
+{
+  gboolean something_changed;
+
+  g_return_val_if_fail (GTK_IS_UNDO (undo), FALSE);
+
+  if (undo->priv->group_depth != 0) {
+    return FALSE;
+  }
+
+  if(undo->priv->undo_stack || undo->priv->redo_stack)
+    something_changed = TRUE;
+  else
+    something_changed = FALSE;
+
+  clear_undo (undo);
+  clear_redo (undo);
+
+  if (something_changed)
+    g_signal_emit (undo, signals[CHANGED], 0);
+  return TRUE;
+}
+
+/**
+ * gtk_undo_start_group:
+ * @undo: a #GtkUndo
+ * @description: a human readable description of what the group will undo
+ *
+ * Starts an undo group. The group must be ended with gtk_undo_end_group
+ * before anything can be undone. Groups can be nested, however.
+ *
+ * Since: 2.20
+ */
+void
+gtk_undo_start_group (GtkUndo *undo, const gchar *description)
+{
+  GtkUndoEntry *entry;
+
+  entry = g_new0 (GtkUndoEntry, 1);
+  entry->description = g_strdup(description);
+  entry->set = NULL;
+  entry->data = NULL;
+
+  if (undo->priv->group_depth == 0) {
+    // new toplevel entry
+    undo->priv->undo_stack = g_list_prepend (undo->priv->undo_stack, g_node_new (entry));
+  }
+  else {
+    // descend into tree at the top of the stack
+    if (!(undo->priv->undo_stack && undo->priv->undo_stack->data)) {
+      g_warning ("Could not start group.\n");
+      return;
+    }
+    g_node_insert (get_node_level (undo->priv->undo_stack->data, undo->priv->group_depth), 0, g_node_new (entry));
+  }
+
+  undo->priv->group_depth++;
+  // if group add mode was just started, emit changed signal
+  if (undo->priv->group_depth == 1)
+    g_signal_emit (undo, signals[CHANGED], 0);
+}
+
+/**
+ * gtk_undo_end_group:
+ * @undo: a #GtkUndo
+ *
+ * Ends the innermost undo group.
+ *
+ * Since: 2.20
+ */
+void
+gtk_undo_end_group (GtkUndo *undo)
+{
+  g_return_if_fail (undo->priv->group_depth > 0);
+  undo->priv->group_depth--;
+  if (undo->priv->group_depth == 0) {
+    change_len_undo (undo, 1);
+    clear_redo (undo);
+    if ((undo->priv->max_length != -1) && (undo->priv->undo_length > undo->priv->max_length))
+      free_last_entry (undo);
+    g_signal_emit (undo, signals[CHANGED], 0);
+  }
+}
+
+/**
+ * gtk_undo_is_in_group:
+ * @undo: a #GtkUndo
+ *
+ * Checks whether the stack is currently in group add mode (that is,
+ * gtk_undo_start_group has been called more often than gtk_undo_end_group).
+ *
+ * Return value: TRUE if stack is in group add mode.
+ *
+ * Since: 2.20
+ */
+gboolean
+gtk_undo_is_in_group (GtkUndo *undo)
+{
+  return (undo->priv->group_depth != 0);
+}
+
+/**
+ * gtk_undo_get_group_depth:
+ * @undo: a #GtkUndo
+ *
+ * Returns the current group depth (that is, the number of times that
+ * gtk_undo_start_group has been called more often than gtk_undo_end_group).
+ *
+ * Return value: Group depth.
+ *
+ * Since: 2.20
+ */
+guint
+gtk_undo_get_group_depth (GtkUndo *undo)
+{
+  return undo->priv->group_depth;
+}
+
+/**
+ * gtk_undo_get_undo_descriptions:
+ * @undo: a #GtkUndo
+ *
+ * Get descriptions of the entries of the undo stack
+ *
+ * Return value: A GtkTreeModel of description strings.
+ *
+ * Since: 2.20
+ */
+GtkTreeStore*
+gtk_undo_get_undo_descriptions (GtkUndo *undo)
+{
+  g_return_val_if_fail (GTK_IS_UNDO (undo), NULL);
+  return get_descriptions_from_stack (undo->priv->undo_stack);
+}
+
+/**
+ * gtk_undo_get_redo_descriptions:
+ * @undo: a #GtkUndo
+ *
+ * Get descriptions of the entries of the redo stack
+ *
+ * Return value: A GtkTreeModel of description strings.
+ *
+ * Since: 2.20
+ */
+GtkTreeStore*
+gtk_undo_get_redo_descriptions (GtkUndo *undo)
+{
+  g_return_val_if_fail (GTK_IS_UNDO (undo), NULL);
+  return get_descriptions_from_stack (undo->priv->redo_stack);
+}
diff --git a/gtk/gtkundo.h b/gtk/gtkundo.h
new file mode 100644
index 0000000..95c9cf8
--- /dev/null
+++ b/gtk/gtkundo.h
@@ -0,0 +1,134 @@
+/* gtkundo.h
+ * Copyright (C) 2009  Holger Berndt <berndth gmx de>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * 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.
+ */
+
+#if defined(GTK_DISABLE_SINGLE_INCLUDES) && !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+
+#ifndef __GTK_UNDO_H__
+#define __GTK_UNDO_H__
+
+#include <gtk/gtktreestore.h>
+
+
+G_BEGIN_DECLS
+
+/* Maximum size of text buffer, in bytes */
+#define GTK_UNDO_MAX_SIZE        G_MAXINT
+
+#define GTK_TYPE_UNDO            (gtk_undo_get_type ())
+#define GTK_UNDO(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_UNDO, GtkUndo))
+#define GTK_UNDO_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_UNDO, GtkUndoClass))
+#define GTK_IS_UNDO(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_UNDO))
+#define GTK_IS_UNDO_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_UNDO))
+#define GTK_UNDO_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_UNDO, GtkUndoClass))
+
+typedef struct _GtkUndo            GtkUndo;
+typedef struct _GtkUndoClass       GtkUndoClass;
+typedef struct _GtkUndoPrivate     GtkUndoPrivate;
+
+struct _GtkUndo
+{
+  GObject parent_instance;
+
+  /*< private >*/
+  GtkUndoPrivate *priv;
+};
+
+struct _GtkUndoClass
+{
+  GObjectClass parent_class;
+
+  /* Signals */
+
+  void (*can_undo) (GtkUndo *undo,
+                    gboolean can_undo);
+
+  void (*can_redo) (GtkUndo *undo,
+                    gboolean can_redo);
+
+  void (*changed) (GtkUndo *undo);
+
+  /* Virtual Methods */
+  /* none, currently */
+
+  /* Padding for future expansion */
+  void (*_gtk_reserved0) (void);
+  void (*_gtk_reserved1) (void);
+  void (*_gtk_reserved2) (void);
+  void (*_gtk_reserved3) (void);
+  void (*_gtk_reserved4) (void);
+  void (*_gtk_reserved5) (void);
+};
+
+// TODO: documentation
+typedef struct _GtkUndoSet GtkUndoSet;
+struct _GtkUndoSet
+{
+  gchar *description;
+  gboolean (*do_undo) (gpointer);
+  gboolean (*do_redo) (gpointer);
+  void (*do_free) (gpointer);
+};
+
+
+GType    gtk_undo_get_type (void) G_GNUC_CONST;
+
+GtkUndo* gtk_undo_new            (void);
+
+void     gtk_undo_register_set   (GtkUndo *undo,
+                                  const char *name,
+                                  const GtkUndoSet *set);
+
+gboolean gtk_undo_add            (GtkUndo *undo,
+                                  const char *set_name,
+                                  gpointer data,
+                                  const gchar *description);
+
+gboolean gtk_undo_undo           (GtkUndo *undo);
+
+gboolean gtk_undo_redo           (GtkUndo *undo);
+
+gboolean gtk_undo_can_undo       (GtkUndo *undo);
+
+gboolean gtk_undo_can_redo       (GtkUndo *undo);
+
+void     gtk_undo_set_max_length (GtkUndo *buffer,
+                                  gint max_length);
+
+gint     gtk_undo_get_max_length (GtkUndo *undo);
+
+gboolean gtk_undo_clear          (GtkUndo *undo);
+
+void     gtk_undo_start_group    (GtkUndo *undo, const gchar *description);
+
+void     gtk_undo_end_group      (GtkUndo *undo);
+
+gboolean gtk_undo_is_in_group    (GtkUndo *undo);
+
+guint    gtk_undo_get_group_depth (GtkUndo *undo);
+
+GtkTreeStore* gtk_undo_get_undo_descriptions (GtkUndo *undo);
+
+GtkTreeStore* gtk_undo_get_redo_descriptions (GtkUndo *undo);
+
+G_END_DECLS
+
+#endif /* __GTK_UNDO_H__ */
diff --git a/gtk/gtkundoview.c b/gtk/gtkundoview.c
new file mode 100644
index 0000000..966a719
--- /dev/null
+++ b/gtk/gtkundoview.c
@@ -0,0 +1,340 @@
+/* gtkundoview.c
+ * Copyright (C) 2009  Holger Berndt <berndth gmx de>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * 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.
+ */
+
+#include "config.h"
+
+#include "gtkundoview.h"
+#include "gtkintl.h"
+#include "gtkhbox.h"
+#include "gtkbutton.h"
+#include "gtkstock.h"
+#include "gtkhpaned.h"
+#include "gtktreestore.h"
+#include "gtkcellrenderertext.h"
+#include "gtktreeviewcolumn.h"
+#include "gtktreeselection.h"
+#include "gtkscrolledwindow.h"
+#include "gtkinfobar.h"
+#include "gtklabel.h"
+
+/**
+ * SECTION:gtkundoview
+ * @title: GtkUndoView
+ * @short_description: View for displaying a #GtkUndo stack
+ *
+ * The #GtkUndoView class implements a view for an #GtkUndo stack.
+ *
+ * TODO: Verbose description here
+ *
+ * Since: 2.20
+ */
+
+enum {
+  PROP_0,
+  PROP_UNDO,
+};
+
+struct _GtkUndoViewPrivate
+{
+  GtkUndo *undo;
+
+  GtkWidget *undo_button;
+  GtkWidget *redo_button;
+  GtkWidget *clear_button;
+
+  GtkWidget *undo_view;
+  GtkWidget *redo_view;
+
+  GtkWidget *info_bar;
+};
+
+G_DEFINE_TYPE (GtkUndoView, gtk_undo_view, GTK_TYPE_VBOX);
+
+
+/* --------------------------------------------------------------------------------
+ *
+ */
+
+static void
+update_list_displays(GtkUndoView *view)
+{
+  GtkTreeStore *store;
+
+  g_return_if_fail (GTK_IS_UNDO (view->priv->undo));
+
+  store = gtk_undo_get_undo_descriptions (view->priv->undo);
+  if (store) {
+    gtk_tree_view_set_model (GTK_TREE_VIEW (view->priv->undo_view), GTK_TREE_MODEL (store));
+    g_object_unref (store);
+  }
+
+  store = gtk_undo_get_redo_descriptions (view->priv->undo);
+  if (store) {
+    gtk_tree_view_set_model (GTK_TREE_VIEW (view->priv->redo_view), GTK_TREE_MODEL (store));
+    g_object_unref (store);
+  }
+
+  gtk_widget_set_sensitive (view->priv->clear_button, gtk_undo_can_undo(view->priv->undo) || gtk_undo_can_redo(view->priv->undo));
+
+  if (gtk_undo_is_in_group (view->priv->undo))
+    gtk_widget_show (view->priv->info_bar);
+  else
+    gtk_widget_hide (view->priv->info_bar);
+}
+
+static GtkWidget*
+create_list_display(GtkUndoView *view, gboolean undo_side)
+{
+  GtkWidget *vbox;
+  GtkTreeStore *model;
+  GtkWidget *treeview;
+  GtkCellRenderer *renderer;
+  GtkTreeViewColumn *column;
+  GtkWidget *scrolledwin;
+  gchar *title;
+  GtkTreeSelection *selection;
+
+  vbox = gtk_vbox_new (FALSE, 0);
+
+  /* scrolled window */
+  scrolledwin = gtk_scrolled_window_new (NULL, NULL);
+  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+  gtk_box_pack_start (GTK_BOX (vbox), scrolledwin, TRUE, TRUE, 0);
+
+  model = gtk_tree_store_new (1, G_TYPE_STRING);
+  treeview = gtk_tree_view_new_with_model (GTK_TREE_MODEL(model));
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(treeview));
+  gtk_tree_selection_set_mode (selection, GTK_SELECTION_NONE);
+  renderer = gtk_cell_renderer_text_new ();
+  if (undo_side)
+    title = N_("Undo stack");
+  else
+    title = N_("Redo stack");
+  column = gtk_tree_view_column_new_with_attributes (title, renderer, "text", 0, NULL);
+  gtk_tree_view_column_set_clickable (column, FALSE);
+  gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
+
+  gtk_container_add (GTK_CONTAINER (scrolledwin), treeview);
+
+  if(undo_side)
+    view->priv->undo_view = treeview;
+  else
+    view->priv->redo_view = treeview;
+
+  return vbox;
+}
+
+static void
+on_info_bar_show_signal (GtkWidget *info_bar, gpointer data)
+{
+  GtkUndoView *view;
+  view = data;
+  if (view->priv->undo && gtk_undo_is_in_group (view->priv->undo))
+    gtk_widget_show (info_bar);
+  else
+    gtk_widget_hide (info_bar);
+}
+
+/* --------------------------------------------------------------------------------
+ *
+ */
+
+static void
+gtk_undo_view_init (GtkUndoView *view)
+{
+  GtkUndoViewPrivate *pv;
+  GtkWidget *hbox;
+  GtkWidget *paned;
+  GtkWidget *list_display;
+
+  pv = view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view, GTK_TYPE_UNDO_VIEW, GtkUndoViewPrivate);
+
+  pv->undo = NULL;
+  pv->undo_button = NULL;
+  pv->redo_button = NULL;
+  pv->clear_button = NULL;
+  pv->undo_view = NULL;
+  pv->redo_view = NULL;
+
+  /* set up widget */
+  hbox = gtk_hbox_new (FALSE, 4);
+
+  pv->undo_button = gtk_button_new_from_stock (GTK_STOCK_UNDO);
+  gtk_widget_set_sensitive (pv->undo_button, FALSE);
+  gtk_box_pack_start (GTK_BOX (hbox), pv->undo_button, FALSE, FALSE, 0);
+
+  pv->redo_button = gtk_button_new_from_stock (GTK_STOCK_REDO);
+  gtk_widget_set_sensitive (pv->redo_button, FALSE);
+  gtk_box_pack_start (GTK_BOX (hbox), pv->redo_button, FALSE, FALSE, 0);
+
+  pv->clear_button = gtk_button_new_from_stock (GTK_STOCK_CLEAR);
+  gtk_widget_set_sensitive (pv->clear_button, FALSE);
+  gtk_box_pack_start (GTK_BOX (hbox), pv->clear_button, FALSE, FALSE, 0);
+
+  gtk_box_pack_start (GTK_BOX (view), hbox, FALSE, FALSE, 0);
+  gtk_widget_show_all (hbox);
+
+  /* paned */
+  paned = gtk_hpaned_new ();
+  // TODO: adjust to requisition size
+  gtk_paned_set_position (GTK_PANED (paned), 300);
+
+  /* first pane: undo list */
+  list_display = create_list_display (view, TRUE);
+  gtk_paned_add1 (GTK_PANED (paned), list_display);
+
+  /* second pane: redo list */
+  list_display = create_list_display (view, FALSE);
+  gtk_paned_add2 (GTK_PANED (paned), list_display);
+
+  gtk_box_pack_start (GTK_BOX (view), paned, TRUE, TRUE, 0);
+
+  /* info bar */
+  pv->info_bar = gtk_info_bar_new ();
+  g_signal_connect_after (G_OBJECT (pv->info_bar), "show", G_CALLBACK (on_info_bar_show_signal), view);
+  gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (pv->info_bar))), gtk_label_new (N_("Currently in group add mode")));
+  gtk_box_pack_start (GTK_BOX (view), pv->info_bar, FALSE, FALSE, 0);
+
+  gtk_widget_show_all (GTK_WIDGET (view));
+}
+
+static void
+gtk_undo_view_set_property (GObject      *obj,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  GtkUndoView *view = GTK_UNDO_VIEW (obj);
+
+  switch (prop_id)
+    {
+    case PROP_UNDO:
+      /* construct-only property */
+      if (view->priv->undo)
+        g_object_unref (view->priv->undo);
+      view->priv->undo = g_value_get_pointer (value);
+      if (view->priv->undo) {
+        g_object_ref (view->priv->undo);
+
+        g_signal_connect_swapped (G_OBJECT (view->priv->undo_button), "clicked", G_CALLBACK (gtk_undo_undo), view->priv->undo);
+        g_signal_connect_swapped (G_OBJECT (view->priv->undo), "can-undo", G_CALLBACK (gtk_widget_set_sensitive), view->priv->undo_button);
+        gtk_widget_set_sensitive (view->priv->undo_button, gtk_undo_can_undo (view->priv->undo));
+
+        g_signal_connect_swapped (G_OBJECT (view->priv->redo_button), "clicked", G_CALLBACK (gtk_undo_redo), view->priv->undo);
+        g_signal_connect_swapped (G_OBJECT (view->priv->undo), "can-redo", G_CALLBACK (gtk_widget_set_sensitive), view->priv->redo_button);
+        gtk_widget_set_sensitive (view->priv->redo_button, gtk_undo_can_redo (view->priv->undo));
+
+        g_signal_connect_swapped (G_OBJECT (view->priv->clear_button), "clicked", G_CALLBACK (gtk_undo_clear), view->priv->undo);
+        gtk_widget_set_sensitive (view->priv->clear_button, gtk_undo_can_undo (view->priv->undo) || gtk_undo_can_redo (view->priv->undo));
+
+        g_signal_connect_swapped (G_OBJECT (view->priv->undo), "changed", G_CALLBACK (update_list_displays), view);
+
+        update_list_displays(view);
+      }
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_undo_view_get_property (GObject    *obj,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  GtkUndoView *view = GTK_UNDO_VIEW (obj);
+
+  switch (prop_id)
+    {
+    case PROP_UNDO:
+      g_value_set_pointer (value, view->priv->undo);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_undo_view_finalize (GObject *obj)
+{
+  G_OBJECT_CLASS (gtk_undo_view_parent_class)->finalize (obj);
+}
+
+static void
+gtk_undo_view_dispose (GObject *obj)
+{
+  GtkUndoView *view = GTK_UNDO_VIEW (obj);
+
+  if (view->priv->undo) {
+    g_object_unref (view->priv->undo);
+    view->priv->undo = NULL;
+  }
+
+  G_OBJECT_CLASS (gtk_undo_view_parent_class)->dispose (obj);
+}
+
+static void
+gtk_undo_view_class_init (GtkUndoViewClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  gobject_class->set_property = gtk_undo_view_set_property;
+  gobject_class->get_property = gtk_undo_view_get_property;
+  gobject_class->dispose = gtk_undo_view_dispose;
+  gobject_class->finalize = gtk_undo_view_finalize;
+
+  g_type_class_add_private (gobject_class, sizeof (GtkUndoViewPrivate));
+
+  /**
+   * GtkUndoView:undo:
+   *
+   * The #GtkUndo class.
+   *
+   * Since: 2.20
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_UNDO,
+                                   g_param_spec_pointer ("undo",
+                                                     P_("undo stack"),
+                                                     P_("The undo stack for the view."),
+                                   G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+/* --------------------------------------------------------------------------------
+ *
+ */
+
+/**
+ * gtk_undo_view_new:
+ * @undo: a #GtkUndo
+ *
+ * Create a new GtkUndoView object.
+ *
+ * Return value: A new GtkUndoView object.
+ *
+ * Since: 2.20
+ **/
+GtkWidget*
+gtk_undo_view_new (GtkUndo *undo)
+{
+  return g_object_new(GTK_TYPE_UNDO_VIEW, "undo", undo, NULL);
+}
diff --git a/gtk/gtkundoview.h b/gtk/gtkundoview.h
new file mode 100644
index 0000000..4d680fc
--- /dev/null
+++ b/gtk/gtkundoview.h
@@ -0,0 +1,73 @@
+/* gtkundoview.h
+ * Copyright (C) 2009  Holger Berndt <berndth gmx de>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * 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.
+ */
+
+#if defined(GTK_DISABLE_SINGLE_INCLUDES) && !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+#ifndef __GTK_UNDO_VIEW_H__
+#define __GTK_UNDO_VIEW_H__
+
+#include <glib-object.h>
+#include <gtk/gtkvbox.h>
+#include <gtk/gtkundo.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_UNDO_VIEW            (gtk_undo_view_get_type ())
+#define GTK_UNDO_VIEW(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_UNDO_VIEW, GtkUndoView))
+#define GTK_UNDO_VIEW_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_UNDO_VIEW, GtkUndoViewClass))
+#define GTK_IS_UNDO_VIEW(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_UNDO_VIEW))
+#define GTK_IS_UNDO_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_UNDO_VIEW))
+#define GTK_UNDO_VIEW_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_UNDO_VIEW, GtkUndoViewClass))
+
+typedef struct _GtkUndoView            GtkUndoView;
+typedef struct _GtkUndoViewClass       GtkUndoViewClass;
+typedef struct _GtkUndoViewPrivate     GtkUndoViewPrivate;
+
+
+struct _GtkUndoView
+{
+  GtkVBox parent;
+
+  /*< private >*/
+  GtkUndoViewPrivate *priv;
+};
+
+struct _GtkUndoViewClass
+{
+  GtkVBoxClass parent_class;
+
+  /* Padding for future expansion */
+  void (*_gtk_reserved0) (void);
+  void (*_gtk_reserved1) (void);
+  void (*_gtk_reserved2) (void);
+  void (*_gtk_reserved3) (void);
+  void (*_gtk_reserved4) (void);
+  void (*_gtk_reserved5) (void);
+};
+
+GType      gtk_undo_view_get_type (void) G_GNUC_CONST;
+
+GtkWidget* gtk_undo_view_new      (GtkUndo *undo);
+
+G_END_DECLS
+
+
+#endif /* __GTK_UNDO_VIEW_H__ */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index e9da96d..48ff4af 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -93,7 +93,8 @@ noinst_PROGRAMS =  $(TEST_PROGS)	\
 	testactions			\
 	testgrouping			\
 	testtooltips			\
-	testvolumebutton
+	testvolumebutton		\
+	testundo
 
 if HAVE_CXX
 noinst_PROGRAMS += autotestkeywords
@@ -170,6 +171,7 @@ testgrouping_DEPENDENCIES = $(TEST_DEPS)
 testtooltips_DEPENDENCIES = $(TEST_DEPS)
 testvolumebutton_DEPENDENCIES = $(TEST_DEPS)
 testwindows_DEPENDENCIES = $(TEST_DEPS)
+testundo_DEPENDENCIES = $(TEST_DEPS)
 
 flicker_LDADD = $(LDADDS)
 simple_LDADD = $(LDADDS)
@@ -240,6 +242,7 @@ testgrouping_LDADD = $(LDADDS)
 testtooltips_LDADD = $(LDADDS)
 testvolumebutton_LDADD = $(LDADDS)
 testwindows_LDADD = $(LDADDS)
+testundo_LDADD = $(LDADDS)
 
 
 testentrycompletion_SOURCES = 	\
@@ -343,6 +346,9 @@ testoffscreen_SOURCES = 	\
 testwindow_SOURCES = 	\
 	testwindows.c
 
+testundo_SOURCES =		\
+	testundo.c
+
 EXTRA_DIST += 			\
 	prop-editor.h		\
 	testgtk.1 		\
diff --git a/tests/testundo.c b/tests/testundo.c
new file mode 100644
index 0000000..9eb159f
--- /dev/null
+++ b/tests/testundo.c
@@ -0,0 +1,201 @@
+/* testundo.c: Test application undo code
+ *
+ * Copyright (C) 2009  Holger Berndt <berndth gmx de>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * 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.
+ */
+
+#include <gtk/gtk.h>
+
+#define UNDO_SET_NAME "myUndoSet"
+#define UNDO_DEFAULT_MAX_LENGTH 10
+
+typedef struct _UndoDataTst1 UndoDataTst1;
+struct _UndoDataTst1 {
+};
+
+GtkWidget *g_ok_fail_checkbutton;
+
+
+static void
+do_free (gpointer data)
+{
+  g_print("do free called\n");
+  g_free(data);
+}
+
+static gboolean
+do_undo (gpointer data)
+{
+  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (g_ok_fail_checkbutton))) {
+    g_print("FAIL do undo called\n");
+    return FALSE;
+  }
+  else {
+    g_print("OK   do undo called\n");
+    return TRUE;
+  }
+}
+
+static gboolean
+do_redo (gpointer data)
+{
+  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (g_ok_fail_checkbutton))) {
+    g_print("FAIL do redo called\n");
+    return FALSE;
+  }
+  else {
+    g_print("OK   do redo called\n");
+    return TRUE;
+  }
+}
+
+static void
+add (GtkUndo *undo, const gchar *msg)
+{
+  UndoDataTst1 *dat;
+
+  dat = g_new0 (UndoDataTst1, 1);
+  gtk_undo_add (undo, UNDO_SET_NAME, dat, msg);
+}
+
+static void
+add_with_description_cb (GtkButton *button, gpointer data)
+{
+  static guint counter = 0;
+  gchar *msg;
+
+  msg = g_strdup_printf ("entry description: %d", counter++);
+  add ((GtkUndo*)data, msg);
+  g_free (msg);
+}
+
+static void
+add_without_description_cb (GtkButton *button, gpointer data)
+{
+  add ((GtkUndo*)data, NULL);
+}
+
+static void
+start_group_with_description_cb (GtkButton *button, gpointer data)
+{
+  static guint counter = 0;
+  gchar *msg;
+
+  msg = g_strdup_printf ("group description: %d", counter++);
+  gtk_undo_start_group ((GtkUndo*)data, msg);
+  g_free (msg);
+}
+
+static void
+start_group_without_description_cb (GtkButton *button, gpointer data)
+{
+  gtk_undo_start_group ((GtkUndo*)data, NULL);
+}
+
+void
+max_size_value_changed_cb (GtkSpinButton *spinner, gpointer data)
+{
+  gtk_undo_set_max_length ((GtkUndo*)data, gtk_spin_button_get_value_as_int (spinner));
+}
+
+static GtkWidget*
+create_main_window (GtkUndo *undo)
+{
+  GtkWidget *win;
+  GtkWidget *vbox;
+  GtkWidget *hbox;
+  GtkWidget *view;
+  GtkWidget *button;
+  GtkWidget *sep;
+  GtkWidget *spinner;
+
+  win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+  gtk_widget_set_size_request (win, 600, 600);
+  g_signal_connect (win, "destroy", G_CALLBACK (gtk_main_quit), NULL);
+  gtk_window_set_title (GTK_WINDOW (win), "Undo Test");
+
+  vbox = gtk_vbox_new (FALSE, 0);
+  gtk_container_add (GTK_CONTAINER (win), vbox);
+
+  /* buttons */
+  button = gtk_button_new_with_label ("add with description");
+  g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (add_with_description_cb), undo);
+  gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
+
+  button = gtk_button_new_with_label ("add without description");
+  g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (add_without_description_cb), undo);
+  gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
+
+  button = gtk_button_new_with_label ("start group with description");
+  g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (start_group_with_description_cb), undo);
+  gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
+
+  button = gtk_button_new_with_label ("start group without description");
+  g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (start_group_without_description_cb), undo);
+  gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
+
+  button = gtk_button_new_with_label ("end group");
+  g_signal_connect_swapped (G_OBJECT (button), "clicked", G_CALLBACK (gtk_undo_end_group), undo);
+  gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
+
+  /* checkbox */
+  g_ok_fail_checkbutton = gtk_check_button_new_with_label ("make undo/redo fail");
+  gtk_box_pack_start (GTK_BOX (vbox), g_ok_fail_checkbutton, FALSE, FALSE, 0);
+
+  /* spinner */
+  hbox = gtk_hbox_new (FALSE, 5);
+  gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);
+  gtk_box_pack_start (GTK_BOX (hbox), gtk_label_new ("Maximum stack size"), FALSE, FALSE, 0);
+  spinner = gtk_spin_button_new_with_range (-1., 100000., 1.);
+  g_signal_connect (G_OBJECT (spinner), "value-changed", G_CALLBACK (max_size_value_changed_cb), undo);
+  gtk_spin_button_set_value (GTK_SPIN_BUTTON (spinner), UNDO_DEFAULT_MAX_LENGTH);
+  gtk_box_pack_start (GTK_BOX (hbox), spinner, FALSE, FALSE, 0);
+
+  /* separator */
+  sep = gtk_hseparator_new();
+  gtk_box_pack_start (GTK_BOX (vbox), sep, FALSE, FALSE, 5);
+
+  /* undo view */
+  view = gtk_undo_view_new (undo);
+  gtk_box_pack_start (GTK_BOX (vbox), view, TRUE, TRUE, 0);
+
+  gtk_widget_show_all (win);
+  return win;
+}
+
+int
+main (int argc, char *argv[])
+{
+  GtkUndo *undo;
+  GtkUndoSet set;
+
+  gtk_init (&argc, &argv);
+
+  undo = gtk_undo_new ();
+  gtk_undo_set_max_length (undo, UNDO_DEFAULT_MAX_LENGTH);
+
+  set.do_undo = do_undo;
+  set.do_redo = do_redo;
+  set.do_free = do_free;
+  set.description = "set description";
+  gtk_undo_register_set (undo, UNDO_SET_NAME, &set);
+
+  create_main_window (undo);
+
+  gtk_main ();
+  return 0;
+}

Attachment: signature.asc
Description: PGP signature



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]