--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pidgin/gtksourceundomanager.c Sun Jul 01 00:55:03 2007 +0000 @@ -0,0 +1,1123 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * gtksourceundomanager.c + * This file is part of GtkSourceView + * + * Copyright (C) 1998, 1999 Alex Roberts, Evan Lawrence + * Copyright (C) 2000, 2001 Chema Celorio, Paolo Maggi + * Copyright (C) 2002-2005 Paolo Maggi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <glib.h> +#include <stdlib.h> +#include <string.h> + +#include "gtksourceundomanager.h" +#include "gtksourceview-marshal.h" + + +#define DEFAULT_MAX_UNDO_LEVELS 25 + + +typedef struct _GtkSourceUndoAction GtkSourceUndoAction; +typedef struct _GtkSourceUndoInsertAction GtkSourceUndoInsertAction; +typedef struct _GtkSourceUndoDeleteAction GtkSourceUndoDeleteAction; + +typedef enum { + GTK_SOURCE_UNDO_ACTION_INSERT, + GTK_SOURCE_UNDO_ACTION_DELETE +} GtkSourceUndoActionType; + +/* + * We use offsets instead of GtkTextIters because the last ones + * require to much memory in this context without giving us any advantage. + */ + +struct _GtkSourceUndoInsertAction +{ + gint pos; + gchar *text; + gint length; + gint chars; +}; + +struct _GtkSourceUndoDeleteAction +{ + gint start; + gint end; + gchar *text; + gboolean forward; +}; + +struct _GtkSourceUndoAction +{ + GtkSourceUndoActionType action_type; + + union { + GtkSourceUndoInsertAction insert; + GtkSourceUndoDeleteAction delete; + } action; + + gint order_in_group; + + /* It is TRUE whether the action can be merged with the following action. */ + guint mergeable : 1; + + /* It is TRUE whether the action is marked as "modified". + * An action is marked as "modified" if it changed the + * state of the buffer from "not modified" to "modified". Only the first + * action of a group can be marked as modified. + * There can be a single action marked as "modified" in the actions list. + */ + guint modified : 1; +}; + +/* INVALID is a pointer to an invalid action */ +#define INVALID ((void *) "IA") + +struct _GtkSourceUndoManagerPrivate +{ + GtkTextBuffer *document; + + GList* actions; + gint next_redo; + + gint actions_in_current_group; + + gint running_not_undoable_actions; + + gint num_of_groups; + + gint max_undo_levels; + + guint can_undo : 1; + guint can_redo : 1; + + /* It is TRUE whether, while undoing an action of the current group (with order_in_group > 1), + * the state of the buffer changed from "not modified" to "modified". + */ + guint modified_undoing_group : 1; + + /* Pointer to the action (in the action list) marked as "modified". + * It is NULL when no action is marked as "modified". + * It is INVALID when the action marked as "modified" has been removed + * from the action list (freeing the list or resizing it) */ + GtkSourceUndoAction *modified_action; +}; + +enum { + CAN_UNDO, + CAN_REDO, + LAST_SIGNAL +}; + +static void gtk_source_undo_manager_class_init (GtkSourceUndoManagerClass *klass); +static void gtk_source_undo_manager_init (GtkSourceUndoManager *um); +static void gtk_source_undo_manager_finalize (GObject *object); + +static void gtk_source_undo_manager_insert_text_handler (GtkTextBuffer *buffer, + GtkTextIter *pos, + const gchar *text, + gint length, + GtkSourceUndoManager *um); +static void gtk_source_undo_manager_delete_range_handler (GtkTextBuffer *buffer, + GtkTextIter *start, + GtkTextIter *end, + GtkSourceUndoManager *um); +static void gtk_source_undo_manager_begin_user_action_handler (GtkTextBuffer *buffer, + GtkSourceUndoManager *um); +static void gtk_source_undo_manager_modified_changed_handler (GtkTextBuffer *buffer, + GtkSourceUndoManager *um); + +static void gtk_source_undo_manager_free_action_list (GtkSourceUndoManager *um); + +static void gtk_source_undo_manager_add_action (GtkSourceUndoManager *um, + const GtkSourceUndoAction *undo_action); +static void gtk_source_undo_manager_free_first_n_actions (GtkSourceUndoManager *um, + gint n); +static void gtk_source_undo_manager_check_list_size (GtkSourceUndoManager *um); + +static gboolean gtk_source_undo_manager_merge_action (GtkSourceUndoManager *um, + const GtkSourceUndoAction *undo_action); + +static GObjectClass *parent_class = NULL; +static guint undo_manager_signals [LAST_SIGNAL] = { 0 }; + +GType +gtk_source_undo_manager_get_type (void) +{ + static GType undo_manager_type = 0; + + if (undo_manager_type == 0) + { + static const GTypeInfo our_info = + { + sizeof (GtkSourceUndoManagerClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) gtk_source_undo_manager_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (GtkSourceUndoManager), + 0, /* n_preallocs */ + (GInstanceInitFunc) gtk_source_undo_manager_init, + NULL /* value_table */ + }; + + undo_manager_type = g_type_register_static (G_TYPE_OBJECT, + "GtkSourceUndoManager", + &our_info, + 0); + } + + return undo_manager_type; +} + +static void +gtk_source_undo_manager_class_init (GtkSourceUndoManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + parent_class = g_type_class_peek_parent (klass); + + object_class->finalize = gtk_source_undo_manager_finalize; + + klass->can_undo = NULL; + klass->can_redo = NULL; + + undo_manager_signals[CAN_UNDO] = + g_signal_new ("can_undo", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtkSourceUndoManagerClass, can_undo), + NULL, NULL, + gtksourceview_marshal_VOID__BOOLEAN, + G_TYPE_NONE, + 1, + G_TYPE_BOOLEAN); + + undo_manager_signals[CAN_REDO] = + g_signal_new ("can_redo", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtkSourceUndoManagerClass, can_redo), + NULL, NULL, + gtksourceview_marshal_VOID__BOOLEAN, + G_TYPE_NONE, + 1, + G_TYPE_BOOLEAN); +} + +static void +gtk_source_undo_manager_init (GtkSourceUndoManager *um) +{ + um->priv = g_new0 (GtkSourceUndoManagerPrivate, 1); + + um->priv->actions = NULL; + um->priv->next_redo = 0; + + um->priv->can_undo = FALSE; + um->priv->can_redo = FALSE; + + um->priv->running_not_undoable_actions = 0; + + um->priv->num_of_groups = 0; + + um->priv->max_undo_levels = DEFAULT_MAX_UNDO_LEVELS; + + um->priv->modified_action = NULL; + + um->priv->modified_undoing_group = FALSE; +} + +static void +gtk_source_undo_manager_finalize (GObject *object) +{ + GtkSourceUndoManager *um; + + g_return_if_fail (object != NULL); + g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (object)); + + um = GTK_SOURCE_UNDO_MANAGER (object); + + g_return_if_fail (um->priv != NULL); + + if (um->priv->actions != NULL) + { + gtk_source_undo_manager_free_action_list (um); + } + + g_signal_handlers_disconnect_by_func (G_OBJECT (um->priv->document), + G_CALLBACK (gtk_source_undo_manager_delete_range_handler), + um); + + g_signal_handlers_disconnect_by_func (G_OBJECT (um->priv->document), + G_CALLBACK (gtk_source_undo_manager_insert_text_handler), + um); + + g_signal_handlers_disconnect_by_func (G_OBJECT (um->priv->document), + G_CALLBACK (gtk_source_undo_manager_begin_user_action_handler), + um); + + g_free (um->priv); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +GtkSourceUndoManager* +gtk_source_undo_manager_new (GtkTextBuffer* buffer) +{ + GtkSourceUndoManager *um; + + um = GTK_SOURCE_UNDO_MANAGER (g_object_new (GTK_SOURCE_TYPE_UNDO_MANAGER, NULL)); + + g_return_val_if_fail (um->priv != NULL, NULL); + um->priv->document = buffer; + + g_signal_connect (G_OBJECT (buffer), "insert_text", + G_CALLBACK (gtk_source_undo_manager_insert_text_handler), + um); + + g_signal_connect (G_OBJECT (buffer), "delete_range", + G_CALLBACK (gtk_source_undo_manager_delete_range_handler), + um); + + g_signal_connect (G_OBJECT (buffer), "begin_user_action", + G_CALLBACK (gtk_source_undo_manager_begin_user_action_handler), + um); + + g_signal_connect (G_OBJECT (buffer), "modified_changed", + G_CALLBACK (gtk_source_undo_manager_modified_changed_handler), + um); + return um; +} + +void +gtk_source_undo_manager_begin_not_undoable_action (GtkSourceUndoManager *um) +{ + g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um)); + g_return_if_fail (um->priv != NULL); + + ++um->priv->running_not_undoable_actions; +} + +static void +gtk_source_undo_manager_end_not_undoable_action_internal (GtkSourceUndoManager *um) +{ + g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um)); + g_return_if_fail (um->priv != NULL); + + g_return_if_fail (um->priv->running_not_undoable_actions > 0); + + --um->priv->running_not_undoable_actions; +} + +void +gtk_source_undo_manager_end_not_undoable_action (GtkSourceUndoManager *um) +{ + g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um)); + g_return_if_fail (um->priv != NULL); + + gtk_source_undo_manager_end_not_undoable_action_internal (um); + + if (um->priv->running_not_undoable_actions == 0) + { + gtk_source_undo_manager_free_action_list (um); + + um->priv->next_redo = -1; + + if (um->priv->can_undo) + { + um->priv->can_undo = FALSE; + g_signal_emit (G_OBJECT (um), + undo_manager_signals [CAN_UNDO], + 0, + FALSE); + } + + if (um->priv->can_redo) + { + um->priv->can_redo = FALSE; + g_signal_emit (G_OBJECT (um), + undo_manager_signals [CAN_REDO], + 0, + FALSE); + } + } +} + +gboolean +gtk_source_undo_manager_can_undo (const GtkSourceUndoManager *um) +{ + g_return_val_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um), FALSE); + g_return_val_if_fail (um->priv != NULL, FALSE); + + return um->priv->can_undo; +} + +gboolean +gtk_source_undo_manager_can_redo (const GtkSourceUndoManager *um) +{ + g_return_val_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um), FALSE); + g_return_val_if_fail (um->priv != NULL, FALSE); + + return um->priv->can_redo; +} + +static void +set_cursor (GtkTextBuffer *buffer, gint cursor) +{ + GtkTextIter iter; + + /* Place the cursor at the requested position */ + gtk_text_buffer_get_iter_at_offset (buffer, &iter, cursor); + gtk_text_buffer_place_cursor (buffer, &iter); +} + +static void +insert_text (GtkTextBuffer *buffer, gint pos, const gchar *text, gint len) +{ + GtkTextIter iter; + + gtk_text_buffer_get_iter_at_offset (buffer, &iter, pos); + gtk_text_buffer_insert (buffer, &iter, text, len); +} + +static void +delete_text (GtkTextBuffer *buffer, gint start, gint end) +{ + GtkTextIter start_iter; + GtkTextIter end_iter; + + gtk_text_buffer_get_iter_at_offset (buffer, &start_iter, start); + + if (end < 0) + gtk_text_buffer_get_end_iter (buffer, &end_iter); + else + gtk_text_buffer_get_iter_at_offset (buffer, &end_iter, end); + + gtk_text_buffer_delete (buffer, &start_iter, &end_iter); +} + +static gchar* +get_chars (GtkTextBuffer *buffer, gint start, gint end) +{ + GtkTextIter start_iter; + GtkTextIter end_iter; + + gtk_text_buffer_get_iter_at_offset (buffer, &start_iter, start); + + if (end < 0) + gtk_text_buffer_get_end_iter (buffer, &end_iter); + else + gtk_text_buffer_get_iter_at_offset (buffer, &end_iter, end); + + return gtk_text_buffer_get_slice (buffer, &start_iter, &end_iter, TRUE); +} + +void +gtk_source_undo_manager_undo (GtkSourceUndoManager *um) +{ + GtkSourceUndoAction *undo_action; + gboolean modified = FALSE; + + g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um)); + g_return_if_fail (um->priv != NULL); + g_return_if_fail (um->priv->can_undo); + + um->priv->modified_undoing_group = FALSE; + + gtk_source_undo_manager_begin_not_undoable_action (um); + + do + { + undo_action = g_list_nth_data (um->priv->actions, um->priv->next_redo + 1); + g_return_if_fail (undo_action != NULL); + + /* undo_action->modified can be TRUE only if undo_action->order_in_group <= 1 */ + g_return_if_fail ((undo_action->order_in_group <= 1) || + ((undo_action->order_in_group > 1) && !undo_action->modified)); + + if (undo_action->order_in_group <= 1) + { + /* Set modified to TRUE only if the buffer did not change its state from + * "not modified" to "modified" undoing an action (with order_in_group > 1) + * in current group. */ + modified = (undo_action->modified && !um->priv->modified_undoing_group); + } + + switch (undo_action->action_type) + { + case GTK_SOURCE_UNDO_ACTION_DELETE: + insert_text ( + um->priv->document, + undo_action->action.delete.start, + undo_action->action.delete.text, + strlen (undo_action->action.delete.text)); + + if (undo_action->action.delete.forward) + set_cursor ( + um->priv->document, + undo_action->action.delete.start); + else + set_cursor ( + um->priv->document, + undo_action->action.delete.end); + + break; + + case GTK_SOURCE_UNDO_ACTION_INSERT: + delete_text ( + um->priv->document, + undo_action->action.insert.pos, + undo_action->action.insert.pos + + undo_action->action.insert.chars); + + set_cursor ( + um->priv->document, + undo_action->action.insert.pos); + break; + + default: + /* Unknown action type. */ + g_return_if_reached (); + } + + ++um->priv->next_redo; + + } while (undo_action->order_in_group > 1); + + if (modified) + { + --um->priv->next_redo; + gtk_text_buffer_set_modified (um->priv->document, FALSE); + ++um->priv->next_redo; + } + + gtk_source_undo_manager_end_not_undoable_action_internal (um); + + um->priv->modified_undoing_group = FALSE; + + if (!um->priv->can_redo) + { + um->priv->can_redo = TRUE; + g_signal_emit (G_OBJECT (um), + undo_manager_signals [CAN_REDO], + 0, + TRUE); + } + + if (um->priv->next_redo >= (gint)(g_list_length (um->priv->actions) - 1)) + { + um->priv->can_undo = FALSE; + g_signal_emit (G_OBJECT (um), + undo_manager_signals [CAN_UNDO], + 0, + FALSE); + } +} + +void +gtk_source_undo_manager_redo (GtkSourceUndoManager *um) +{ + GtkSourceUndoAction *undo_action; + gboolean modified = FALSE; + + g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER (um)); + g_return_if_fail (um->priv != NULL); + g_return_if_fail (um->priv->can_redo); + + undo_action = g_list_nth_data (um->priv->actions, um->priv->next_redo); + g_return_if_fail (undo_action != NULL); + + gtk_source_undo_manager_begin_not_undoable_action (um); + + do + { + if (undo_action->modified) + { + g_return_if_fail (undo_action->order_in_group <= 1); + modified = TRUE; + } + + --um->priv->next_redo; + + switch (undo_action->action_type) + { + case GTK_SOURCE_UNDO_ACTION_DELETE: + delete_text ( + um->priv->document, + undo_action->action.delete.start, + undo_action->action.delete.end); + + set_cursor ( + um->priv->document, + undo_action->action.delete.start); + + break; + + case GTK_SOURCE_UNDO_ACTION_INSERT: + set_cursor ( + um->priv->document, + undo_action->action.insert.pos); + + insert_text ( + um->priv->document, + undo_action->action.insert.pos, + undo_action->action.insert.text, + undo_action->action.insert.length); + + break; + + default: + /* Unknown action type */ + ++um->priv->next_redo; + g_return_if_reached (); + } + + if (um->priv->next_redo < 0) + undo_action = NULL; + else + undo_action = g_list_nth_data (um->priv->actions, um->priv->next_redo); + + } while ((undo_action != NULL) && (undo_action->order_in_group > 1)); + + if (modified) + { + ++um->priv->next_redo; + gtk_text_buffer_set_modified (um->priv->document, FALSE); + --um->priv->next_redo; + } + + gtk_source_undo_manager_end_not_undoable_action_internal (um); + + if (um->priv->next_redo < 0) + { + um->priv->can_redo = FALSE; + g_signal_emit (G_OBJECT (um), undo_manager_signals [CAN_REDO], 0, FALSE); + } + + if (!um->priv->can_undo) + { + um->priv->can_undo = TRUE; + g_signal_emit (G_OBJECT (um), undo_manager_signals [CAN_UNDO], 0, TRUE); + } +} + +static void +gtk_source_undo_action_free (GtkSourceUndoAction *action) +{ + if (action == NULL) + return; + + if (action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT) + g_free (action->action.insert.text); + else if (action->action_type == GTK_SOURCE_UNDO_ACTION_DELETE)