[prev in list] [next in list] [prev in thread] [next in thread]
List: gtk
Subject: Undo stack for GTK+ (was: Re: undo in textview)
From: Holger Berndt <berndth () gmx ! de>
Date: 2009-12-28 17:02:07
Message-ID: 20091228170309.6A047750050 () menubar ! gnome ! org
[Download RAW message or body]
[Attachment #2 (multipart/signed)]
[Attachment #4 (multipart/mixed)]
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
[Attachment #7 (text/x-patch)]
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;
+}
["signature.asc" (application/pgp-signature)]
_______________________________________________
gtk-list mailing list
gtk-list@gnome.org
http://mail.gnome.org/mailman/listinfo/gtk-list
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic