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