/* * Copyright (C) 1998, 1999 Alex Roberts, Evan Lawrence * Copyright (C) 2000, 2001 Chema Celorio, Paolo Maggi * Copyright (C) 2002-2005 Paolo Maggi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 * Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include "undo_manager.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 }; #if !defined(NDEBUG) static void print_state(GtkSourceUndoManager* um) { fprintf(stderr, "\n***\n"); GList* actions = um->priv->actions; for (; actions; actions = g_list_next(actions)) { GtkSourceUndoAction* act = actions->data; fprintf(stderr, "* type = %s\n", act->action_type == GTK_SOURCE_UNDO_ACTION_DELETE ? "delete" : "insert"); fprintf(stderr, "\ttext = %s\n", act->action_type == GTK_SOURCE_UNDO_ACTION_DELETE ? act->action.delete.text : act->action.insert.text); fprintf(stderr, "\torder = %d\n", act->order_in_group); } fprintf(stderr, "* next redo: %d\n", um->priv->next_redo); fprintf(stderr, "* num of groups: %d\n", um->priv->num_of_groups); fprintf(stderr, "* actions in group: %d\n", um->priv->actions_in_current_group); } #endif 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, g_cclosure_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, g_cclosure_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_signal_handlers_disconnect_by_func(G_OBJECT(um->priv->document), G_CALLBACK(gtk_source_undo_manager_modified_changed_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) g_free(action->action.delete.text); else g_return_if_reached(); g_free(action); } static void gtk_source_undo_manager_free_action_list(GtkSourceUndoManager *um) { GList *l; l = um->priv->actions; while(l != NULL) { GtkSourceUndoAction *action = l->data; if(action->order_in_group == 1) --um->priv->num_of_groups; um->priv->actions_in_current_group = action->order_in_group - 1; if(action->modified) um->priv->modified_action = INVALID; gtk_source_undo_action_free(action); l = g_list_next(l); } g_list_free(um->priv->actions); um->priv->actions = NULL; } static void gtk_source_undo_manager_insert_text_handler(GtkTextBuffer *buffer, GtkTextIter *pos, const gchar *text, gint length, GtkSourceUndoManager *um) { GtkSourceUndoAction undo_action; if(um->priv->running_not_undoable_actions > 0) return; undo_action.action_type = GTK_SOURCE_UNDO_ACTION_INSERT; undo_action.action.insert.pos = gtk_text_iter_get_offset(pos); undo_action.action.insert.text =(gchar*) text; undo_action.action.insert.length = length; undo_action.action.insert.chars = g_utf8_strlen(text, length); if((undo_action.action.insert.chars > 1) ||(g_utf8_get_char(text) == '\n')) undo_action.mergeable = FALSE; else undo_action.mergeable = TRUE; undo_action.modified = FALSE; gtk_source_undo_manager_add_action(um, &undo_action); } static void gtk_source_undo_manager_delete_range_handler(GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end, GtkSourceUndoManager *um) { GtkSourceUndoAction undo_action; GtkTextIter insert_iter; if(um->priv->running_not_undoable_actions > 0) return; undo_action.action_type = GTK_SOURCE_UNDO_ACTION_DELETE; gtk_text_iter_order(start, end); undo_action.action.delete.start = gtk_text_iter_get_offset(start); undo_action.action.delete.end = gtk_text_iter_get_offset(end); undo_action.action.delete.text = get_chars( buffer, undo_action.action.delete.start, undo_action.action.delete.end); /* figure out if the user used the Delete or the Backspace key */ gtk_text_buffer_get_iter_at_mark(buffer, &insert_iter, gtk_text_buffer_get_insert(buffer)); if(gtk_text_iter_get_offset(&insert_iter) <= undo_action.action.delete.start) undo_action.action.delete.forward = TRUE; else undo_action.action.delete.forward = FALSE; if(((undo_action.action.delete.end - undo_action.action.delete.start) > 1) || (g_utf8_get_char(undo_action.action.delete.text ) == '\n')) undo_action.mergeable = FALSE; else undo_action.mergeable = TRUE; undo_action.modified = FALSE; gtk_source_undo_manager_add_action(um, &undo_action); g_free(undo_action.action.delete.text); } static void gtk_source_undo_manager_begin_user_action_handler(GtkTextBuffer *buffer, GtkSourceUndoManager *um) { g_return_if_fail(GTK_SOURCE_IS_UNDO_MANAGER(um)); g_return_if_fail(um->priv != NULL); if(um->priv->running_not_undoable_actions > 0) return; um->priv->actions_in_current_group = 0; } static void gtk_source_undo_manager_add_action(GtkSourceUndoManager *um, const GtkSourceUndoAction *undo_action) { GtkSourceUndoAction* action; if(um->priv->next_redo >= 0) { gtk_source_undo_manager_free_first_n_actions(um, um->priv->next_redo + 1); } um->priv->next_redo = -1; if(!gtk_source_undo_manager_merge_action(um, undo_action)) { action = g_new(GtkSourceUndoAction, 1); *action = *undo_action; if(action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT) action->action.insert.text = g_strndup(undo_action->action.insert.text, undo_action->action.insert.length); else if(action->action_type == GTK_SOURCE_UNDO_ACTION_DELETE) action->action.delete.text = g_strdup(undo_action->action.delete.text); else { g_free(action); g_return_if_reached(); } ++um->priv->actions_in_current_group; action->order_in_group = um->priv->actions_in_current_group; if(action->order_in_group == 1) ++um->priv->num_of_groups; um->priv->actions = g_list_prepend(um->priv->actions, action); } gtk_source_undo_manager_check_list_size(um); if(!um->priv->can_undo) { um->priv->can_undo = TRUE; g_signal_emit(G_OBJECT(um), undo_manager_signals [CAN_UNDO], 0, TRUE); } if(um->priv->can_redo) { um->priv->can_redo = FALSE; g_signal_emit(G_OBJECT(um), undo_manager_signals [CAN_REDO], 0, FALSE); } } static void gtk_source_undo_manager_free_first_n_actions(GtkSourceUndoManager *um, gint n) { gint i; if(um->priv->actions == NULL) return; for(i = 0; i < n; i++) { GtkSourceUndoAction *action = g_list_first(um->priv->actions)->data; if(action->order_in_group == 1) --um->priv->num_of_groups; um->priv->actions_in_current_group = action->order_in_group - 1; if(action->modified) um->priv->modified_action = INVALID; gtk_source_undo_action_free(action); um->priv->actions = g_list_delete_link(um->priv->actions, um->priv->actions); if(um->priv->actions == NULL) return; } } static void gtk_source_undo_manager_check_list_size(GtkSourceUndoManager *um) { gint undo_levels; g_return_if_fail(GTK_SOURCE_IS_UNDO_MANAGER(um)); g_return_if_fail(um->priv != NULL); undo_levels = gtk_source_undo_manager_get_max_undo_levels(um); if(undo_levels < 1) return; if(um->priv->num_of_groups > undo_levels) { GtkSourceUndoAction *undo_action; GList *last; last = g_list_last(um->priv->actions); undo_action =(GtkSourceUndoAction*) last->data; do { GList *tmp; if(undo_action->order_in_group == 1) --um->priv->num_of_groups; um->priv->actions_in_current_group = undo_action->order_in_group - 1; if(undo_action->modified) um->priv->modified_action = INVALID; gtk_source_undo_action_free(undo_action); tmp = g_list_previous(last); um->priv->actions = g_list_delete_link(um->priv->actions, last); last = tmp; g_return_if_fail(last != NULL); undo_action =(GtkSourceUndoAction*) last->data; } while((undo_action->order_in_group > 1) || (um->priv->num_of_groups > undo_levels)); } } /** * gtk_source_undo_manager_merge_action: * @um: a #GtkSourceUndoManager. * @undo_action: a #GtkSourceUndoAction. * * This function tries to merge the undo action at the top of * the stack with a new undo action. So when we undo for example * typing, we can undo the whole word and not each letter by itself. * * Return Value: %TRUE is merge was successful, %FALSE otherwise. **/ static gboolean gtk_source_undo_manager_merge_action(GtkSourceUndoManager *um, const GtkSourceUndoAction *undo_action) { GtkSourceUndoAction *last_action; g_return_val_if_fail(GTK_SOURCE_IS_UNDO_MANAGER(um), FALSE); g_return_val_if_fail(um->priv != NULL, FALSE); if(um->priv->actions == NULL) return FALSE; last_action =(GtkSourceUndoAction*) g_list_nth_data(um->priv->actions, 0); if(!last_action->mergeable) return FALSE; if((!undo_action->mergeable) || (undo_action->action_type != last_action->action_type)) { last_action->mergeable = FALSE; return FALSE; } if(undo_action->action_type == GTK_SOURCE_UNDO_ACTION_DELETE) { if((last_action->action.delete.forward != undo_action->action.delete.forward) || ((last_action->action.delete.start != undo_action->action.delete.start) && (last_action->action.delete.start != undo_action->action.delete.end))) { last_action->mergeable = FALSE; return FALSE; } if(last_action->action.delete.start == undo_action->action.delete.start) { gchar *str; #define L (last_action->action.delete.end - last_action->action.delete.start - 1) #define g_utf8_get_char_at(p,i) g_utf8_get_char(g_utf8_offset_to_pointer((p),(i))) /* Deleted with the delete key */ if((g_utf8_get_char(undo_action->action.delete.text) != ' ') && (g_utf8_get_char(undo_action->action.delete.text) != '\t') && ((g_utf8_get_char_at(last_action->action.delete.text, L) == ' ') || (g_utf8_get_char_at(last_action->action.delete.text, L) == '\t'))) { last_action->mergeable = FALSE; return FALSE; } str = g_strdup_printf("%s%s", last_action->action.delete.text, undo_action->action.delete.text); g_free(last_action->action.delete.text); last_action->action.delete.end +=(undo_action->action.delete.end - undo_action->action.delete.start); last_action->action.delete.text = str; } else { gchar *str; /* Deleted with the backspace key */ if((g_utf8_get_char(undo_action->action.delete.text) != ' ') && (g_utf8_get_char(undo_action->action.delete.text) != '\t') && ((g_utf8_get_char(last_action->action.delete.text) == ' ') || (g_utf8_get_char(last_action->action.delete.text) == '\t'))) { last_action->mergeable = FALSE; return FALSE; } str = g_strdup_printf("%s%s", undo_action->action.delete.text, last_action->action.delete.text); g_free(last_action->action.delete.text); last_action->action.delete.start = undo_action->action.delete.start; last_action->action.delete.text = str; } } else if(undo_action->action_type == GTK_SOURCE_UNDO_ACTION_INSERT) { gchar* str; #define I (last_action->action.insert.chars - 1) if((undo_action->action.insert.pos != (last_action->action.insert.pos + last_action->action.insert.chars)) || ((g_utf8_get_char(undo_action->action.insert.text) != ' ') && (g_utf8_get_char(undo_action->action.insert.text) != '\t') && ((g_utf8_get_char_at(last_action->action.insert.text, I) == ' ') || (g_utf8_get_char_at(last_action->action.insert.text, I) == '\t'))) ) { last_action->mergeable = FALSE; return FALSE; } str = g_strdup_printf("%s%s", last_action->action.insert.text, undo_action->action.insert.text); g_free(last_action->action.insert.text); last_action->action.insert.length += undo_action->action.insert.length; last_action->action.insert.text = str; last_action->action.insert.chars += undo_action->action.insert.chars; } else /* Unknown action inside undo merge encountered */ g_return_val_if_reached(TRUE); return TRUE; } gint gtk_source_undo_manager_get_max_undo_levels(GtkSourceUndoManager *um) { g_return_val_if_fail(um != NULL, 0); g_return_val_if_fail(GTK_SOURCE_IS_UNDO_MANAGER(um), 0); return um->priv->max_undo_levels; } void gtk_source_undo_manager_set_max_undo_levels(GtkSourceUndoManager *um, gint max_undo_levels) { gint old_levels; g_return_if_fail(um != NULL); g_return_if_fail(GTK_SOURCE_IS_UNDO_MANAGER(um)); old_levels = um->priv->max_undo_levels; um->priv->max_undo_levels = max_undo_levels; if(max_undo_levels < 1) return; if(old_levels > max_undo_levels) { /* strip redo actions first */ while(um->priv->next_redo >= 0 &&(um->priv->num_of_groups > max_undo_levels)) { gtk_source_undo_manager_free_first_n_actions(um, 1); um->priv->next_redo--; } /* now remove undo actions if necessary */ gtk_source_undo_manager_check_list_size(um); /* emit "can_undo" and/or "can_redo" if appropiate */ if(um->priv->next_redo < 0 && um->priv->can_redo) { 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->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); } } } static void gtk_source_undo_manager_modified_changed_handler(GtkTextBuffer *buffer, GtkSourceUndoManager *um) { GtkSourceUndoAction *action; GList *list; g_return_if_fail(GTK_SOURCE_IS_UNDO_MANAGER(um)); g_return_if_fail(um->priv != NULL); if(um->priv->actions == NULL) return; list = g_list_nth(um->priv->actions, um->priv->next_redo + 1); if(list != NULL) action =(GtkSourceUndoAction*) list->data; else action = NULL; if(gtk_text_buffer_get_modified(buffer) == FALSE) { if(action != NULL) action->mergeable = FALSE; if(um->priv->modified_action != NULL) { if(um->priv->modified_action != INVALID) um->priv->modified_action->modified = FALSE; um->priv->modified_action = NULL; } return; } if(action == NULL) { g_return_if_fail(um->priv->running_not_undoable_actions > 0); return; } /* gtk_text_buffer_get_modified(buffer) == TRUE */ g_return_if_fail(um->priv->modified_action == NULL); if(action->order_in_group > 1) um->priv->modified_undoing_group = TRUE; while(action->order_in_group > 1) { list = g_list_next(list); g_return_if_fail(list != NULL); action =(GtkSourceUndoAction*) list->data; g_return_if_fail(action != NULL); } action->modified = TRUE; um->priv->modified_action = action; }