gtksourceview3/gtksourceview/gtksourceundomanagerdefault.c

1591 lines
41 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* gtksourceundomanagerdefault.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
* Copyright (C) 2014, 2015 - Sébastien Wilmet <swilmet@gnome.org>
*
* 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
*/
#include "gtksourceundomanagerdefault.h"
#include <string.h>
#include "gtksourceundomanager.h"
/* unlimited by default */
#define DEFAULT_MAX_UNDO_LEVELS -1
typedef struct _Action Action;
typedef struct _ActionGroup ActionGroup;
typedef enum _ActionType
{
ACTION_TYPE_INSERT,
ACTION_TYPE_DELETE
} ActionType;
/* A more precise deletion type. But currently it's only a guess, we are not
* 100% sure of the deletion type. To be sure, we would need to listen to key
* events on the GtkSourceView widget, which is more complicated than simply
* listening to the insert-text and delete-range GtkTextBuffer signals.
*/
typedef enum _DeletionType
{
DELETION_TYPE_SELECTION_DELETED,
DELETION_TYPE_BACKSPACE_KEY,
DELETION_TYPE_DELETE_KEY,
DELETION_TYPE_PROGRAMMATICALLY
} DeletionType;
struct _Action
{
ActionType type;
/* Character offset for the start of @text in the GtkTextBuffer. */
gint start;
/* Character offset for the end of @text in the GtkTextBuffer. */
gint end;
/* Nul-terminated text.
* TODO A possible memory optimization is to store the text only when
* needed. For an insertion that is located in the history on the undo
* side, the text is not needed since it is already present in the
* buffer. The same for a deletion on the redo side. But the last action
* text is needed for the merging.
*/
gchar *text;
/* Character offsets of the insert and selection bound marks.
* They are both -1 or they both match @start or @end.
* If the text cursor or the selected text is not related to the action,
* the selection is not stored (i.e. -1).
* If not -1, when undoing or redoing an action, the insert and
* selection bound marks are restored to where they were.
* For an insert, @selection_insert and @selection_bound must match
* @start, otherwise the selection or cursor position is unrelated to
* the insertion.
* For a deletion, if @selection_insert and @selection_bound are -1, it
* corresponds to DELETION_TYPE_PROGRAMMATICALLY. For all the other
* deletion types, the selection is stored.
*/
gint selection_insert;
gint selection_bound;
};
struct _ActionGroup
{
/* One or several Action's that forms a single undo or redo step. The
* most recent action is at the end of the list.
* In fact, actions can be grouped with
* gtk_text_buffer_begin_user_action() and
* gtk_text_buffer_end_user_action().
*/
GQueue *actions;
/* If force_not_mergeable is FALSE, there are dynamic checks to see if
* the action group is mergeable. For example if the saved_location is
* just after the action group, the action group is not mergeable, so
* the saved_location isn't lost.
*/
guint force_not_mergeable : 1;
};
struct _GtkSourceUndoManagerDefaultPrivate
{
/* Weak ref to the buffer. */
GtkTextBuffer *buffer;
/* List of ActionGroup's. The most recent ActionGroup is at the end of
* the list.
*/
GQueue *action_groups;
/* Current location in 'action_groups', where we are located in the
* history. The redo steps are on the right of the pointer, and the undo
* steps are on the left. In other words, the next redo step is
* location->data. The next undo step is location->prev->data. But the
* location should not be seen as a node, it should be seen as a
* vertical bar between two nodes, like a GtkTextIter between two
* characters.
*/
GList *location;
/* A new ActionGroup that is created when some text is inserted or
* deleted in the buffer. As long as a user action is running (when
* 'running_user_action' is TRUE) the new actions are inserted into
* 'new_action_group'. When the user action ends, we try to merge
* 'new_action_group' with the previous ActionGroup in 'action_groups'
* (the node on the left of 'location'). If the merging fails, a new
* node is inserted on the left of 'location'.
*/
ActionGroup *new_action_group;
/* The number of nested calls to
* gtk_source_buffer_begin_not_undoable_action().
*/
guint running_not_undoable_actions;
/* Max number of action groups. */
gint max_undo_levels;
/* The location in 'action_groups' where the buffer is saved. I.e. when
* gtk_text_buffer_set_modified (buffer, FALSE) was called for the last
* time.
* NULL is for the end of 'action_groups'.
* 'has_saved_location' is FALSE if the history doesn't contain a saved
* location.
*/
GList *saved_location;
guint has_saved_location : 1;
guint can_undo : 1;
guint can_redo : 1;
/* Whether we are between a begin-user-action and a end-user-action.
* Some operations, like undo and redo, are not allowed during a user
* action (it would screw up the history).
* At the beginning of a user action, a new action group is created. At
* the end of the user action, we try to merge the group with the
* previous one. So when an insertion or deletion occurs when
* running_user_action is TRUE, we don't need to create a new group. But
* when running_user_action is FALSE, we need to put the insertion or
* deletion into a new group and try to merge it directly with the
* previous group.
*/
guint running_user_action : 1;
};
enum
{
PROP_0,
PROP_BUFFER,
PROP_MAX_UNDO_LEVELS
};
static void gtk_source_undo_manager_iface_init (GtkSourceUndoManagerIface *iface);
static gboolean action_merge (Action *action,
Action *new_action);
G_DEFINE_TYPE_WITH_CODE (GtkSourceUndoManagerDefault,
gtk_source_undo_manager_default,
G_TYPE_OBJECT,
G_ADD_PRIVATE (GtkSourceUndoManagerDefault)
G_IMPLEMENT_INTERFACE (GTK_SOURCE_TYPE_UNDO_MANAGER,
gtk_source_undo_manager_iface_init))
/* Utilities functions */
static Action *
action_new (void)
{
Action *action;
action = g_slice_new0 (Action);
action->selection_insert = -1;
action->selection_bound = -1;
return action;
}
static void
action_free (Action *action)
{
if (action != NULL)
{
g_free (action->text);
g_slice_free (Action, action);
}
}
static ActionGroup *
action_group_new (void)
{
ActionGroup *group;
group = g_slice_new (ActionGroup);
group->actions = g_queue_new ();
group->force_not_mergeable = FALSE;
return group;
}
static void
action_group_free (ActionGroup *group)
{
if (group != NULL)
{
g_queue_free_full (group->actions, (GDestroyNotify) action_free);
g_slice_free (ActionGroup, group);
}
}
static void
update_can_undo_can_redo (GtkSourceUndoManagerDefault *manager)
{
gboolean can_undo;
gboolean can_redo;
if (manager->priv->running_user_action)
{
can_undo = FALSE;
can_redo = FALSE;
}
else if (manager->priv->location != NULL)
{
can_undo = manager->priv->location->prev != NULL;
can_redo = TRUE;
}
else
{
can_undo = manager->priv->action_groups->tail != NULL;
can_redo = FALSE;
}
if (manager->priv->can_undo != can_undo)
{
manager->priv->can_undo = can_undo;
gtk_source_undo_manager_can_undo_changed (GTK_SOURCE_UNDO_MANAGER (manager));
}
if (manager->priv->can_redo != can_redo)
{
manager->priv->can_redo = can_redo;
gtk_source_undo_manager_can_redo_changed (GTK_SOURCE_UNDO_MANAGER (manager));
}
}
static void
clear_all (GtkSourceUndoManagerDefault *manager)
{
GList *l;
if (manager->priv->has_saved_location &&
manager->priv->saved_location != manager->priv->location)
{
manager->priv->has_saved_location = FALSE;
}
for (l = manager->priv->action_groups->head; l != NULL; l = l->next)
{
ActionGroup *group = l->data;
action_group_free (group);
}
g_queue_clear (manager->priv->action_groups);
manager->priv->location = NULL;
manager->priv->saved_location = NULL;
action_group_free (manager->priv->new_action_group);
manager->priv->new_action_group = NULL;
update_can_undo_can_redo (manager);
}
static void
remove_last_action_group (GtkSourceUndoManagerDefault *manager)
{
ActionGroup *group;
if (manager->priv->action_groups->length == 0)
{
return;
}
if (manager->priv->location == manager->priv->action_groups->tail)
{
manager->priv->location = NULL;
}
if (manager->priv->has_saved_location)
{
if (manager->priv->saved_location == NULL)
{
manager->priv->has_saved_location = FALSE;
}
else if (manager->priv->saved_location == manager->priv->action_groups->tail)
{
manager->priv->saved_location = NULL;
}
}
group = g_queue_pop_tail (manager->priv->action_groups);
action_group_free (group);
}
static void
remove_first_action_group (GtkSourceUndoManagerDefault *manager)
{
GList *first_node;
ActionGroup *group;
first_node = manager->priv->action_groups->head;
if (first_node == NULL)
{
return;
}
if (manager->priv->location == first_node)
{
manager->priv->location = first_node->next;
}
if (manager->priv->has_saved_location &&
manager->priv->saved_location == first_node)
{
manager->priv->has_saved_location = FALSE;
}
group = g_queue_pop_head (manager->priv->action_groups);
action_group_free (group);
}
static void
check_history_size (GtkSourceUndoManagerDefault *manager)
{
if (manager->priv->max_undo_levels == -1)
{
return;
}
if (manager->priv->max_undo_levels == 0)
{
clear_all (manager);
return;
}
g_return_if_fail (manager->priv->max_undo_levels > 0);
while (manager->priv->action_groups->length > (guint)manager->priv->max_undo_levels)
{
/* Strip redo action groups first. */
if (manager->priv->location != NULL)
{
remove_last_action_group (manager);
}
else
{
remove_first_action_group (manager);
}
}
update_can_undo_can_redo (manager);
}
static void
remove_redo_action_groups (GtkSourceUndoManagerDefault *manager)
{
while (manager->priv->location != NULL)
{
remove_last_action_group (manager);
}
}
/* Try to merge @new_group into @group. Returns TRUE if merged. It is up to the
* caller to free @new_group.
*/
static gboolean
action_group_merge (ActionGroup *group,
ActionGroup *new_group)
{
Action *action;
Action *new_action;
g_assert (group != NULL);
g_assert (new_group != NULL);
if (new_group->actions->length == 0)
{
return TRUE;
}
if (group->force_not_mergeable ||
new_group->force_not_mergeable ||
group->actions->length > 1 ||
new_group->actions->length > 1)
{
return FALSE;
}
action = g_queue_peek_head (group->actions);
new_action = g_queue_peek_head (new_group->actions);
return action_merge (action, new_action);
}
/* Try to merge the new action group with the previous one (the one located on
* the left of priv->location). If the merge fails, a new node is inserted into
* the history.
*/
static void
insert_new_action_group (GtkSourceUndoManagerDefault *manager)
{
GList *prev_node = NULL;
ActionGroup *prev_group = NULL;
ActionGroup *new_group = manager->priv->new_action_group;
gboolean can_merge = TRUE;
if (new_group == NULL || new_group->actions->length == 0)
{
return;
}
remove_redo_action_groups (manager);
g_assert (manager->priv->location == NULL);
prev_node = manager->priv->action_groups->tail;
if (prev_node != NULL)
{
prev_group = prev_node->data;
/* If the previous group is empty, it means that it was not correctly
* inserted into the history.
*/
g_assert_cmpuint (prev_group->actions->length, >, 0);
}
/* If the saved_location is equal to the current location, the two
* ActionGroups cannot be merged, to not lose the saved_location.
*/
if (manager->priv->has_saved_location &&
manager->priv->saved_location == manager->priv->location)
{
g_assert (manager->priv->saved_location == NULL);
can_merge = FALSE;
}
if (can_merge &&
prev_group != NULL &&
action_group_merge (prev_group, new_group))
{
/* new_group merged into prev_group */
action_group_free (manager->priv->new_action_group);
manager->priv->new_action_group = NULL;
update_can_undo_can_redo (manager);
return;
}
g_queue_push_tail (manager->priv->action_groups, new_group);
manager->priv->new_action_group = NULL;
if (manager->priv->has_saved_location &&
manager->priv->saved_location == NULL)
{
manager->priv->saved_location = manager->priv->action_groups->tail;
}
/* "Archive" prev_group. It will never be mergeable again. If the user
* does some undo's to return to this location, a new action won't be
* merged with an "archived" action group.
*/
if (prev_group != NULL)
{
prev_group->force_not_mergeable = TRUE;
}
check_history_size (manager);
update_can_undo_can_redo (manager);
}
static void
insert_action (GtkSourceUndoManagerDefault *manager,
Action *new_action)
{
ActionGroup *new_group;
g_assert (new_action != NULL);
if (manager->priv->new_action_group == NULL)
{
manager->priv->new_action_group = action_group_new ();
}
new_group = manager->priv->new_action_group;
/* Inside a group, don't try to merge the actions. It is needed to keep
* them separate so when undoing or redoing, the cursor position is set
* at the right place.
* For example with the search and replace, we replace all occurrences
* of 'a' by '' (i.e. delete all a's). The text "aaba" becomes "b". On
* undo, the cursor position should be placed at "a|aba", not "aa|ba"
* (but it's a detail).
*/
g_queue_push_tail (new_group->actions, new_action);
/* An action is mergeable only for an insertion or deletion of a single
* character. If the text contains several characters, the new_action
* can for example come from a copy/paste.
*/
if (new_action->end - new_action->start > 1 ||
g_str_equal (new_action->text, "\n"))
{
new_group->force_not_mergeable = TRUE;
}
if (!manager->priv->running_user_action)
{
insert_new_action_group (manager);
}
}
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);
gtk_text_buffer_get_iter_at_offset (buffer, &end_iter, end);
gtk_text_buffer_begin_user_action (buffer);
gtk_text_buffer_delete (buffer, &start_iter, &end_iter);
gtk_text_buffer_end_user_action (buffer);
}
static void
insert_text (GtkTextBuffer *buffer,
gint offset,
const gchar *text)
{
GtkTextIter iter;
gtk_text_buffer_get_iter_at_offset (buffer, &iter, offset);
gtk_text_buffer_begin_user_action (buffer);
gtk_text_buffer_insert (buffer, &iter, text, -1);
gtk_text_buffer_end_user_action (buffer);
}
static gunichar
get_last_char (const gchar *text)
{
gchar *pos;
pos = g_utf8_find_prev_char (text, text + strlen (text));
if (pos == NULL)
{
return '\0';
}
return g_utf8_get_char (pos);
}
/* ActionInsert implementation */
static void
action_insert_undo (GtkTextBuffer *buffer,
Action *action)
{
g_assert_cmpint (action->type, ==, ACTION_TYPE_INSERT);
delete_text (buffer, action->start, action->end);
}
static void
action_insert_redo (GtkTextBuffer *buffer,
Action *action)
{
g_assert_cmpint (action->type, ==, ACTION_TYPE_INSERT);
insert_text (buffer, action->start, action->text);
}
static gboolean
action_insert_merge (Action *action,
Action *new_action)
{
gint new_text_length;
gunichar new_char;
gunichar last_char;
gchar *merged_text;
g_assert_cmpint (action->type, ==, ACTION_TYPE_INSERT);
g_assert_cmpint (new_action->type, ==, ACTION_TYPE_INSERT);
new_text_length = new_action->end - new_action->start;
g_assert_cmpint (new_text_length, ==, 1);
new_char = g_utf8_get_char (new_action->text);
g_assert (new_char != '\n');
if (action->end != new_action->start)
{
return FALSE;
}
last_char = get_last_char (action->text);
/* If I type character by character the text "hello world", there will
* be two actions: "hello" and " world". If I click on undo, only
* "hello" remains, not the space. The space makes sense only when
* a second word is present.
* Note that the spaces or tabs at the beginning of a line (for code
* indentation) are removed with the first word of the line. For example
* if I type character by character " return FALSE;", there are two
* actions: " return" and " FALSE;". If I undo two times, maybe I still
* want the indentation. But with auto-indent, when we press Enter to
* create a newline, the indentation is part of the action that adds the
* newline, i.e. we have the three actions "\n ", "return" and
* " FALSE;".
*/
if ((new_char == ' ' || new_char == '\t') &&
(last_char != ' ' && last_char != '\t'))
{
return FALSE;
}
merged_text = g_strdup_printf ("%s%s", action->text, new_action->text);
g_free (action->text);
action->text = merged_text;
action->end = new_action->end;
/* No need to update the selection, action->start is not modified. */
g_assert ((action->selection_insert == -1 &&
action->selection_bound == -1) ||
(action->selection_insert == action->start &&
action->selection_bound == action->start));
return TRUE;
}
static void
action_insert_restore_selection (GtkTextBuffer *buffer,
Action *action,
gboolean undo)
{
GtkTextIter iter;
g_assert_cmpint (action->type, ==, ACTION_TYPE_INSERT);
/* No need to take into account action->selection_insert and
* action->selection_bound, because:
* - If they are both -1, we still want to place the cursor correctly,
* as done below, because if the cursor is not moved the user won't
* see the modification.
* - If they are set, their values are both action->start, so the undo
* works as expeceted in this case. The redo is also the expected
* behavior because after inserting a character the cursor is _after_
* the character, not before.
*/
if (undo)
{
gtk_text_buffer_get_iter_at_offset (buffer, &iter, action->start);
}
else /* redo */
{
gtk_text_buffer_get_iter_at_offset (buffer, &iter, action->end);
}
gtk_text_buffer_place_cursor (buffer, &iter);
}
/* ActionDelete implementation */
static void
action_delete_undo (GtkTextBuffer *buffer,
Action *action)
{
g_assert_cmpint (action->type, ==, ACTION_TYPE_DELETE);
insert_text (buffer, action->start, action->text);
}
static void
action_delete_redo (GtkTextBuffer *buffer,
Action *action)
{
g_assert_cmpint (action->type, ==, ACTION_TYPE_DELETE);
delete_text (buffer, action->start, action->end);
}
static DeletionType
get_deletion_type (Action *action)
{
g_assert_cmpint (action->type, ==, ACTION_TYPE_DELETE);
if (action->selection_insert == -1)
{
g_assert_cmpint (action->selection_bound, ==, -1);
return DELETION_TYPE_PROGRAMMATICALLY;
}
if (action->selection_insert == action->end &&
action->selection_bound == action->end)
{
return DELETION_TYPE_BACKSPACE_KEY;
}
if (action->selection_insert == action->start &&
action->selection_bound == action->start)
{
return DELETION_TYPE_DELETE_KEY;
}
g_assert (action->selection_insert == action->start ||
action->selection_insert == action->end);
g_assert (action->selection_bound == action->start ||
action->selection_bound == action->end);
return DELETION_TYPE_SELECTION_DELETED;
}
static gboolean
action_delete_merge (Action *action,
Action *new_action)
{
gint new_text_length;
gunichar new_char;
DeletionType deletion_type;
DeletionType new_deletion_type;
g_assert_cmpint (action->type, ==, ACTION_TYPE_DELETE);
g_assert_cmpint (new_action->type, ==, ACTION_TYPE_DELETE);
new_text_length = new_action->end - new_action->start;
g_assert_cmpint (new_text_length, ==, 1);
new_char = g_utf8_get_char (new_action->text);
g_assert (new_char != '\n');
deletion_type = get_deletion_type (action);
new_deletion_type = get_deletion_type (new_action);
if (deletion_type != new_deletion_type)
{
return FALSE;
}
switch (deletion_type)
{
/* If the user has selected some text and then has deleted it,
* it should be seen as a single action group, not mergeable. A
* good reason for that is to correctly restore the selection.
*/
case DELETION_TYPE_SELECTION_DELETED:
return FALSE;
/* For memory use it would be better to take it into account,
* but the code is simpler like that.
*/
case DELETION_TYPE_PROGRAMMATICALLY:
return FALSE;
/* Two Backspaces or two Deletes must follow each other. In
* "abc", if the cursor is at offset 2 and I press the Backspace
* key, then move the cursor after 'c' and press Backspace
* again, the two deletes won't be merged, since there was a
* cursor movement in between.
*/
case DELETION_TYPE_DELETE_KEY:
/* Not consecutive deletes. */
if (action->start != new_action->start)
{
return FALSE;
}
break;
case DELETION_TYPE_BACKSPACE_KEY:
/* Not consecutive backspaces. */
if (action->start != new_action->end)
{
return FALSE;
}
break;
default:
g_assert_not_reached ();
}
/* Delete key pressed several times. */
if (action->start == new_action->start)
{
gunichar last_char;
gchar *merged_text;
last_char = get_last_char (action->text);
/* Same as action_insert_merge(). */
if ((new_char == ' ' || new_char == '\t') &&
(last_char != ' ' && last_char != '\t'))
{
return FALSE;
}
merged_text = g_strdup_printf ("%s%s", action->text, new_action->text);
g_free (action->text);
action->text = merged_text;
action->end += new_text_length;
/* No need to update the selection, action->start is not
* modified.
*/
g_assert_cmpint (action->selection_insert, ==, action->start);
g_assert_cmpint (action->selection_bound, ==, action->start);
return TRUE;
}
/* Backspace key pressed several times. */
if (action->start == new_action->end)
{
gunichar last_char;
gchar *merged_text;
/* The last char deleted, but since it's with the Backspace key,
* it's the first char in action->text.
*/
last_char = g_utf8_get_char (action->text);
/* Same as action_insert_merge(). */
if ((new_char != ' ' && new_char != '\t') &&
(last_char == ' ' || last_char == '\t'))
{
return FALSE;
}
merged_text = g_strdup_printf ("%s%s", new_action->text, action->text);
g_free (action->text);
action->text = merged_text;
action->start = new_action->start;
/* No need to update the selection, action->end is not modified. */
g_assert_cmpint (action->selection_insert, ==, action->end);
g_assert_cmpint (action->selection_bound, ==, action->end);
return TRUE;
}
g_assert_not_reached ();
return FALSE;
}
static void
action_delete_restore_selection (GtkTextBuffer *buffer,
Action *action,
gboolean undo)
{
g_assert_cmpint (action->type, ==, ACTION_TYPE_DELETE);
if (undo)
{
if (action->selection_insert == -1)
{
GtkTextIter iter;
g_assert_cmpint (action->selection_bound, ==, -1);
gtk_text_buffer_get_iter_at_offset (buffer, &iter, action->end);
gtk_text_buffer_place_cursor (buffer, &iter);
}
else
{
GtkTextIter insert_iter;
GtkTextIter bound_iter;
gtk_text_buffer_get_iter_at_offset (buffer,
&insert_iter,
action->selection_insert);
gtk_text_buffer_get_iter_at_offset (buffer,
&bound_iter,
action->selection_bound);
gtk_text_buffer_select_range (buffer, &insert_iter, &bound_iter);
}
}
else /* redo */
{
GtkTextIter iter;
gtk_text_buffer_get_iter_at_offset (buffer, &iter, action->start);
gtk_text_buffer_place_cursor (buffer, &iter);
}
}
/* Action interface.
* The Action struct can be seen as an interface. All the explicit case analysis
* on the action type are grouped in this code section. This can easily be
* modified as an object-oriented architecture with polymorphism.
*/
static void
action_undo (GtkTextBuffer *buffer,
Action *action)
{
g_assert (action != NULL);
switch (action->type)
{
case ACTION_TYPE_INSERT:
action_insert_undo (buffer, action);
break;
case ACTION_TYPE_DELETE:
action_delete_undo (buffer, action);
break;
default:
g_return_if_reached ();
break;
}
}
static void
action_redo (GtkTextBuffer *buffer,
Action *action)
{
g_assert (action != NULL);
switch (action->type)
{
case ACTION_TYPE_INSERT:
action_insert_redo (buffer, action);
break;
case ACTION_TYPE_DELETE:
action_delete_redo (buffer, action);
break;
default:
g_return_if_reached ();
break;
}
}
/* Try to merge @new_action into @action. Returns TRUE if merged. It is up to
* the caller to free @new_action if needed.
*/
static gboolean
action_merge (Action *action,
Action *new_action)
{
g_assert (action != NULL);
g_assert (new_action != NULL);
if (action->type != new_action->type)
{
return FALSE;
}
switch (action->type)
{
case ACTION_TYPE_INSERT:
return action_insert_merge (action, new_action);
case ACTION_TYPE_DELETE:
return action_delete_merge (action, new_action);
default:
g_return_val_if_reached (FALSE);
break;
}
}
/* Restore the selection (or cursor position) according to @action.
* If @undo is TRUE, @action has just been undone. If @undo is FALSE, @action
* has just been redone.
*/
static void
action_restore_selection (GtkTextBuffer *buffer,
Action *action,
gboolean undo)
{
g_assert (action != NULL);
switch (action->type)
{
case ACTION_TYPE_INSERT:
action_insert_restore_selection (buffer, action, undo);
break;
case ACTION_TYPE_DELETE:
action_delete_restore_selection (buffer, action, undo);
break;
default:
g_return_if_reached ();
break;
}
}
/* Buffer signal handlers */
static void
set_selection_bounds (GtkTextBuffer *buffer,
Action *action)
{
GtkTextMark *insert_mark;
GtkTextMark *bound_mark;
GtkTextIter insert_iter;
GtkTextIter bound_iter;
insert_mark = gtk_text_buffer_get_insert (buffer);
bound_mark = gtk_text_buffer_get_selection_bound (buffer);
gtk_text_buffer_get_iter_at_mark (buffer, &insert_iter, insert_mark);
gtk_text_buffer_get_iter_at_mark (buffer, &bound_iter, bound_mark);
action->selection_insert = gtk_text_iter_get_offset (&insert_iter);
action->selection_bound = gtk_text_iter_get_offset (&bound_iter);
}
static void
insert_text_cb (GtkTextBuffer *buffer,
GtkTextIter *location,
const gchar *text,
gint length,
GtkSourceUndoManagerDefault *manager)
{
Action *action = action_new ();
action->type = ACTION_TYPE_INSERT;
action->start = gtk_text_iter_get_offset (location);
action->text = g_strndup (text, length);
action->end = action->start + g_utf8_strlen (action->text, -1);
set_selection_bounds (buffer, action);
if (action->selection_insert != action->selection_bound ||
action->selection_insert != action->start)
{
action->selection_insert = -1;
action->selection_bound = -1;
}
else
{
/* The insertion occurred at the cursor. */
g_assert_cmpint (action->selection_insert, ==, action->start);
g_assert_cmpint (action->selection_bound, ==, action->start);
}
insert_action (manager, action);
}
static void
delete_range_cb (GtkTextBuffer *buffer,
GtkTextIter *start,
GtkTextIter *end,
GtkSourceUndoManagerDefault *manager)
{
Action *action = action_new ();
action->type = ACTION_TYPE_DELETE;
action->start = gtk_text_iter_get_offset (start);
action->end = gtk_text_iter_get_offset (end);
action->text = gtk_text_buffer_get_slice (buffer, start, end, TRUE);
g_assert_cmpint (action->start, <, action->end);
set_selection_bounds (buffer, action);
if ((action->selection_insert != action->start &&
action->selection_insert != action->end) ||
(action->selection_bound != action->start &&
action->selection_bound != action->end))
{
action->selection_insert = -1;
action->selection_bound = -1;
}
insert_action (manager, action);
}
static void
begin_user_action_cb (GtkTextBuffer *buffer,
GtkSourceUndoManagerDefault *manager)
{
manager->priv->running_user_action = TRUE;
update_can_undo_can_redo (manager);
}
static void
end_user_action_cb (GtkTextBuffer *buffer,
GtkSourceUndoManagerDefault *manager)
{
insert_new_action_group (manager);
manager->priv->running_user_action = FALSE;
update_can_undo_can_redo (manager);
}
static void
modified_changed_cb (GtkTextBuffer *buffer,
GtkSourceUndoManagerDefault *manager)
{
if (gtk_text_buffer_get_modified (buffer))
{
/* It can happen for example when the file on disk has been
* deleted.
*/
if (manager->priv->has_saved_location &&
manager->priv->saved_location == manager->priv->location &&
(manager->priv->new_action_group == NULL ||
manager->priv->new_action_group->actions->length == 0))
{
manager->priv->has_saved_location = FALSE;
}
}
/* saved */
else
{
/* Saving a buffer during a user action is allowed, the user
* action is split.
* FIXME and/or a warning should be printed?
*/
if (manager->priv->running_user_action)
{
insert_new_action_group (manager);
}
manager->priv->saved_location = manager->priv->location;
manager->priv->has_saved_location = TRUE;
}
}
static void
block_signal_handlers (GtkSourceUndoManagerDefault *manager)
{
if (manager->priv->buffer == NULL)
{
return;
}
g_signal_handlers_block_by_func (manager->priv->buffer,
insert_text_cb,
manager);
g_signal_handlers_block_by_func (manager->priv->buffer,
delete_range_cb,
manager);
g_signal_handlers_block_by_func (manager->priv->buffer,
modified_changed_cb,
manager);
}
static void
unblock_signal_handlers (GtkSourceUndoManagerDefault *manager)
{
if (manager->priv->buffer == NULL)
{
return;
}
g_signal_handlers_unblock_by_func (manager->priv->buffer,
insert_text_cb,
manager);
g_signal_handlers_unblock_by_func (manager->priv->buffer,
delete_range_cb,
manager);
g_signal_handlers_unblock_by_func (manager->priv->buffer,
modified_changed_cb,
manager);
}
static void
set_buffer (GtkSourceUndoManagerDefault *manager,
GtkTextBuffer *buffer)
{
g_assert (manager->priv->buffer == NULL);
if (buffer == NULL)
{
return;
}
manager->priv->buffer = buffer;
g_object_add_weak_pointer (G_OBJECT (buffer),
(gpointer *)&manager->priv->buffer);
g_signal_connect_object (buffer,
"insert-text",
G_CALLBACK (insert_text_cb),
manager,
0);
g_signal_connect_object (buffer,
"delete-range",
G_CALLBACK (delete_range_cb),
manager,
0);
g_signal_connect_object (buffer,
"begin-user-action",
G_CALLBACK (begin_user_action_cb),
manager,
0);
g_signal_connect_object (buffer,
"end-user-action",
G_CALLBACK (end_user_action_cb),
manager,
0);
g_signal_connect_object (buffer,
"modified-changed",
G_CALLBACK (modified_changed_cb),
manager,
0);
modified_changed_cb (manager->priv->buffer, manager);
}
/* GObject construction, destruction and properties */
static void
gtk_source_undo_manager_default_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (object);
switch (prop_id)
{
case PROP_BUFFER:
set_buffer (manager, g_value_get_object (value));
break;
case PROP_MAX_UNDO_LEVELS:
gtk_source_undo_manager_default_set_max_undo_levels (manager, g_value_get_int (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gtk_source_undo_manager_default_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (object);
switch (prop_id)
{
case PROP_BUFFER:
g_value_set_object (value, manager->priv->buffer);
break;
case PROP_MAX_UNDO_LEVELS:
g_value_set_int (value, manager->priv->max_undo_levels);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gtk_source_undo_manager_default_dispose (GObject *object)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (object);
if (manager->priv->buffer != NULL)
{
g_object_remove_weak_pointer (G_OBJECT (manager->priv->buffer),
(gpointer *)&manager->priv->buffer);
manager->priv->buffer = NULL;
}
G_OBJECT_CLASS (gtk_source_undo_manager_default_parent_class)->dispose (object);
}
static void
gtk_source_undo_manager_default_finalize (GObject *object)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (object);
g_queue_free_full (manager->priv->action_groups,
(GDestroyNotify) action_group_free);
action_group_free (manager->priv->new_action_group);
G_OBJECT_CLASS (gtk_source_undo_manager_default_parent_class)->finalize (object);
}
static void
gtk_source_undo_manager_default_class_init (GtkSourceUndoManagerDefaultClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->set_property = gtk_source_undo_manager_default_set_property;
object_class->get_property = gtk_source_undo_manager_default_get_property;
object_class->dispose = gtk_source_undo_manager_default_dispose;
object_class->finalize = gtk_source_undo_manager_default_finalize;
g_object_class_install_property (object_class,
PROP_BUFFER,
g_param_spec_object ("buffer",
"Buffer",
"The text buffer to add undo support on",
GTK_TYPE_TEXT_BUFFER,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class,
PROP_MAX_UNDO_LEVELS,
g_param_spec_int ("max-undo-levels",
"Max Undo Levels",
"Number of undo levels for the buffer",
-1,
G_MAXINT,
DEFAULT_MAX_UNDO_LEVELS,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
}
static void
gtk_source_undo_manager_default_init (GtkSourceUndoManagerDefault *manager)
{
manager->priv = gtk_source_undo_manager_default_get_instance_private (manager);
manager->priv->action_groups = g_queue_new ();
manager->priv->max_undo_levels = DEFAULT_MAX_UNDO_LEVELS;
}
/* Interface implementation */
static gboolean
gtk_source_undo_manager_can_undo_impl (GtkSourceUndoManager *undo_manager)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (undo_manager);
return manager->priv->can_undo;
}
static gboolean
gtk_source_undo_manager_can_redo_impl (GtkSourceUndoManager *undo_manager)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (undo_manager);
return manager->priv->can_redo;
}
static void
restore_modified_state (GtkSourceUndoManagerDefault *manager,
GList *old_location,
GList *new_location)
{
if (manager->priv->has_saved_location)
{
if (old_location == manager->priv->saved_location)
{
gtk_text_buffer_set_modified (manager->priv->buffer, TRUE);
}
else if (new_location == manager->priv->saved_location)
{
gtk_text_buffer_set_modified (manager->priv->buffer, FALSE);
}
}
}
static void
gtk_source_undo_manager_undo_impl (GtkSourceUndoManager *undo_manager)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (undo_manager);
GList *old_location;
GList *new_location;
ActionGroup *group;
Action *action;
GList *l;
g_return_if_fail (manager->priv->can_undo);
old_location = manager->priv->location;
if (old_location != NULL)
{
new_location = manager->priv->location->prev;
}
else
{
new_location = manager->priv->action_groups->tail;
}
g_assert (new_location != NULL);
group = new_location->data;
g_assert_cmpuint (group->actions->length, >, 0);
block_signal_handlers (manager);
for (l = group->actions->tail; l != NULL; l = l->prev)
{
action = l->data;
action_undo (manager->priv->buffer, action);
}
restore_modified_state (manager, old_location, new_location);
/* After an undo, place the cursor at the first action in the group. For
* a search and replace, it will be the first occurrence in the buffer.
*/
action = g_queue_peek_head (group->actions);
action_restore_selection (manager->priv->buffer, action, TRUE);
unblock_signal_handlers (manager);
manager->priv->location = new_location;
update_can_undo_can_redo (manager);
}
static void
gtk_source_undo_manager_redo_impl (GtkSourceUndoManager *undo_manager)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (undo_manager);
GList *old_location;
GList *new_location;
ActionGroup *group;
GList *l;
g_return_if_fail (manager->priv->can_redo);
old_location = manager->priv->location;
g_assert (old_location != NULL);
new_location = old_location->next;
group = old_location->data;
block_signal_handlers (manager);
for (l = group->actions->head; l != NULL; l = l->next)
{
Action *action = l->data;
action_redo (manager->priv->buffer, action);
/* For a redo, place the cursor at the first action in the
* group. For an undo the first action is also chosen, so when
* undoing/redoing a search and replace, the cursor position
* stays at the first occurrence and the user can see the
* replacement easily.
* For a redo, if we choose the last action in the group, when
* undoing/redoing a search and replace, the cursor position
* will jump between the first occurrence and the last
* occurrence. Staying at the same place is probably better.
*/
if (l == group->actions->head)
{
action_restore_selection (manager->priv->buffer, action, FALSE);
}
}
restore_modified_state (manager, old_location, new_location);
unblock_signal_handlers (manager);
manager->priv->location = new_location;
update_can_undo_can_redo (manager);
}
static void
gtk_source_undo_manager_begin_not_undoable_action_impl (GtkSourceUndoManager *undo_manager)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (undo_manager);
manager->priv->running_not_undoable_actions++;
if (manager->priv->running_not_undoable_actions == 1)
{
block_signal_handlers (manager);
}
}
static void
gtk_source_undo_manager_end_not_undoable_action_impl (GtkSourceUndoManager *undo_manager)
{
GtkSourceUndoManagerDefault *manager = GTK_SOURCE_UNDO_MANAGER_DEFAULT (undo_manager);
g_return_if_fail (manager->priv->running_not_undoable_actions > 0);
manager->priv->running_not_undoable_actions--;
if (manager->priv->running_not_undoable_actions == 0)
{
unblock_signal_handlers (manager);
clear_all (manager);
modified_changed_cb (manager->priv->buffer, manager);
}
}
static void
gtk_source_undo_manager_iface_init (GtkSourceUndoManagerIface *iface)
{
iface->can_undo = gtk_source_undo_manager_can_undo_impl;
iface->can_redo = gtk_source_undo_manager_can_redo_impl;
iface->undo = gtk_source_undo_manager_undo_impl;
iface->redo = gtk_source_undo_manager_redo_impl;
iface->begin_not_undoable_action = gtk_source_undo_manager_begin_not_undoable_action_impl;
iface->end_not_undoable_action = gtk_source_undo_manager_end_not_undoable_action_impl;
}
/* Public functions */
void
gtk_source_undo_manager_default_set_max_undo_levels (GtkSourceUndoManagerDefault *manager,
gint max_undo_levels)
{
g_return_if_fail (GTK_SOURCE_IS_UNDO_MANAGER_DEFAULT (manager));
g_return_if_fail (max_undo_levels >= -1);
if (manager->priv->max_undo_levels != max_undo_levels)
{
if (max_undo_levels == 0)
{
/* disable the undo manager */
block_signal_handlers (manager);
}
else if (manager->priv->max_undo_levels == 0)
{
unblock_signal_handlers (manager);
modified_changed_cb (manager->priv->buffer, manager);
}
manager->priv->max_undo_levels = max_undo_levels;
check_history_size (manager);
g_object_notify (G_OBJECT (manager), "max-undo-levels");
}
}