Learning_GTK4_tree/gtk/gtktexthistory.c

1253 lines
34 KiB
C
Raw Normal View History

2023-12-12 11:36:42 +01:00
/* Copyright (C) 2019 Red Hat, Inc.
*
* 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 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, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "gtkistringprivate.h"
#include "gtktexthistoryprivate.h"
/*
* The GtkTextHistory works in a way that allows text widgets to deliver
* information about changes to the underlying text at given offsets within
* their text. The GtkTextHistory object uses a series of callback functions
* (see GtkTextHistoryFuncs) to apply changes as undo/redo is performed.
*
* The GtkTextHistory object is careful to avoid tracking changes while
* applying specific undo/redo actions.
*
* Changes are tracked within a series of actions, contained in groups. The
* group may be coalesced when gtk_text_history_end_user_action() is
* called.
*
* Calling gtk_text_history_begin_irreversible_action() and
* gtk_text_history_end_irreversible_action() can be used to denote a
* section of operations that cannot be undone. This will cause all previous
* changes tracked by the GtkTextHistory to be discarded.
*/
typedef struct _Action Action;
typedef enum _ActionKind ActionKind;
enum _ActionKind
{
ACTION_KIND_BARRIER = 1,
ACTION_KIND_DELETE_BACKSPACE = 2,
ACTION_KIND_DELETE_KEY = 3,
ACTION_KIND_DELETE_PROGRAMMATIC = 4,
ACTION_KIND_DELETE_SELECTION = 5,
ACTION_KIND_GROUP = 6,
ACTION_KIND_INSERT = 7,
};
struct _Action
{
ActionKind kind;
GList link;
guint is_modified : 1;
guint is_modified_set : 1;
union {
struct {
IString istr;
guint begin;
guint end;
} insert;
struct {
IString istr;
guint begin;
guint end;
struct {
int insert;
int bound;
} selection;
} delete;
struct {
GQueue actions;
guint depth;
} group;
} u;
};
struct _GtkTextHistory
{
GObject parent_instance;
GtkTextHistoryFuncs funcs;
gpointer funcs_data;
GQueue undo_queue;
GQueue redo_queue;
struct {
int insert;
int bound;
} selection;
guint irreversible;
guint in_user;
guint max_undo_levels;
guint can_undo : 1;
guint can_redo : 1;
guint is_modified : 1;
guint is_modified_set : 1;
guint applying : 1;
guint enabled : 1;
};
static void action_free (Action *action);
G_DEFINE_TYPE (GtkTextHistory, gtk_text_history, G_TYPE_OBJECT)
#define return_if_applying(instance) \
G_STMT_START { \
if ((instance)->applying) \
return; \
} G_STMT_END
#define return_if_irreversible(instance) \
G_STMT_START { \
if ((instance)->irreversible) \
return; \
} G_STMT_END
#define return_if_not_enabled(instance) \
G_STMT_START { \
if (!(instance)->enabled) \
return; \
} G_STMT_END
static const char *
action_kind_name (ActionKind kind)
{
switch (kind)
{
case ACTION_KIND_BARRIER: return "Barrier";
case ACTION_KIND_DELETE_BACKSPACE: return "Delete_Backspace";
case ACTION_KIND_DELETE_KEY: return "Delete_Key";
case ACTION_KIND_DELETE_PROGRAMMATIC: return "Delete_Programmatic";
case ACTION_KIND_DELETE_SELECTION: return "Delete_Selection";
case ACTION_KIND_GROUP: return "Group";
case ACTION_KIND_INSERT: return "Insert";
default: return "unknown";
}
}
static inline void
gtk_text_history_printf_space (GString *str,
guint depth)
{
for (guint i = 0; i < depth; i++)
g_string_append (str, " ");
}
static void
gtk_text_history_printf_action (Action *action,
GString *str,
guint depth)
{
gtk_text_history_printf_space (str, depth);
g_string_append_printf (str, "%s {\n", action_kind_name (action->kind));
gtk_text_history_printf_space (str, depth+1);
g_string_append_printf (str, "is_modified: %s\n", action->is_modified ? "true" : "false");
gtk_text_history_printf_space (str, depth+1);
g_string_append_printf (str, "is_modified_set: %s\n", action->is_modified_set ? "true" : "false");
switch (action->kind)
{
case ACTION_KIND_BARRIER:
gtk_text_history_printf_space (str, depth+1);
g_string_append_printf (str, "depth: %u\n", action->u.group.depth);
break;
case ACTION_KIND_DELETE_BACKSPACE:
case ACTION_KIND_DELETE_KEY:
case ACTION_KIND_DELETE_PROGRAMMATIC:
case ACTION_KIND_DELETE_SELECTION:
{
char *escaped;
gtk_text_history_printf_space (str, depth+1);
g_string_append_printf (str, "begin: %u\n", action->u.delete.begin);
gtk_text_history_printf_space (str, depth+1);
g_string_append_printf (str, "end: %u\n", action->u.delete.end);
gtk_text_history_printf_space (str, depth+1);
g_string_append (str, "selection {\n");
gtk_text_history_printf_space (str, depth+2);
g_string_append_printf (str, "insert: %d\n", action->u.delete.selection.insert);
gtk_text_history_printf_space (str, depth+2);
g_string_append_printf (str, "bound: %d\n", action->u.delete.selection.bound);
gtk_text_history_printf_space (str, depth+1);
g_string_append (str, "}\n");
gtk_text_history_printf_space (str, depth+1);
escaped = g_strescape (istring_str (&action->u.delete.istr), NULL);
g_string_append_printf (str, "text: \"%s\"\n", escaped);
g_free (escaped);
}
break;
case ACTION_KIND_INSERT:
{
char *escaped;
gtk_text_history_printf_space (str, depth+1);
g_string_append_printf (str, "begin: %u\n", action->u.insert.begin);
gtk_text_history_printf_space (str, depth+1);
g_string_append_printf (str, "end: %u\n", action->u.insert.end);
gtk_text_history_printf_space (str, depth+1);
escaped = g_strescape (istring_str (&action->u.insert.istr), NULL);
g_string_append_printf (str, "text: \"%s\"\n", escaped);
g_free (escaped);
}
break;
case ACTION_KIND_GROUP:
gtk_text_history_printf_space (str, depth+1);
g_string_append_printf (str, "depth: %u\n", action->u.group.depth);
for (const GList *iter = action->u.group.actions.head; iter; iter = iter->next)
{
Action *child = iter->data;
gtk_text_history_printf_space (str, depth+1);
g_string_append (str, "children {\n");
gtk_text_history_printf_action (child, str, depth+2);
gtk_text_history_printf_space (str, depth+1);
g_string_append (str, "}\n");
}
break;
default:
break;
}
gtk_text_history_printf_space (str, depth);
g_string_append (str, "}\n");
}
G_GNUC_UNUSED static char *
gtk_text_history_printf (GtkTextHistory *history)
{
GString *str = g_string_new (NULL);
g_string_append (str, "undo {\n");
for (const GList *iter = history->undo_queue.head; iter; iter = iter->next)
gtk_text_history_printf_action (iter->data, str, 1);
g_string_append (str, "}\n");
g_string_append (str, "redo {\n");
for (const GList *iter = history->redo_queue.head; iter; iter = iter->next)
gtk_text_history_printf_action (iter->data, str, 1);
g_string_append (str, "}\n");
return g_string_free (str, FALSE);
}
static inline void
uint_order (guint *a,
guint *b)
{
if (*a > *b)
{
guint tmp = *a;
*a = *b;
*b = tmp;
}
}
static void
clear_action_queue (GQueue *queue)
{
g_assert (queue != NULL);
while (queue->length > 0)
{
Action *action = g_queue_peek_head (queue);
g_queue_unlink (queue, &action->link);
action_free (action);
}
}
static Action *
action_new (ActionKind kind)
{
Action *action;
action = g_new0 (Action, 1);
action->kind = kind;
action->link.data = action;
return action;
}
static void
action_free (Action *action)
{
if (action->kind == ACTION_KIND_INSERT)
istring_clear (&action->u.insert.istr);
else if (action->kind == ACTION_KIND_DELETE_BACKSPACE ||
action->kind == ACTION_KIND_DELETE_KEY ||
action->kind == ACTION_KIND_DELETE_PROGRAMMATIC ||
action->kind == ACTION_KIND_DELETE_SELECTION)
istring_clear (&action->u.delete.istr);
else if (action->kind == ACTION_KIND_GROUP)
clear_action_queue (&action->u.group.actions);
g_free (action);
}
static gboolean
action_group_is_empty (const Action *action)
{
const GList *iter;
g_assert (action->kind == ACTION_KIND_GROUP);
for (iter = action->u.group.actions.head; iter; iter = iter->next)
{
const Action *child = iter->data;
if (child->kind == ACTION_KIND_BARRIER)
continue;
if (child->kind == ACTION_KIND_GROUP && action_group_is_empty (child))
continue;
return FALSE;
}
return TRUE;
}
static gboolean
action_chain (Action *action,
Action *other,
gboolean in_user_action)
{
g_assert (action != NULL);
g_assert (other != NULL);
if (action->kind == ACTION_KIND_GROUP)
{
Action *tail = g_queue_peek_tail (&action->u.group.actions);
/* Always push new items onto a group, so that we can coalesce
* items when gtk_text_history_end_user_action() is called.
*
* But we don't care if this is a barrier since we will always
* apply things as a group anyway.
*/
if (other->kind == ACTION_KIND_BARRIER)
{
/* If we're not in a user action, this barrier is meant to
* stop items from coalescing into this group.
*/
if (!in_user_action && action->u.group.depth == 0)
return FALSE;
action_free (other);
return TRUE;
}
/* Try to chain onto the tail item in the group to increase
* the chances we have a single action within the group. That
* way we are more likely to hoist out of the group when the
* user action is ended.
*/
if (tail != NULL && tail->kind == other->kind)
{
if (action_chain (tail, other, in_user_action))
return TRUE;
}
g_queue_push_tail_link (&action->u.group.actions, &other->link);
return TRUE;
}
/* The rest can only be merged to themselves */
if (action->kind != other->kind)
return FALSE;
switch (action->kind)
{
case ACTION_KIND_INSERT: {
/* Make sure the new insert is at the end of the previous */
if (action->u.insert.end != other->u.insert.begin)
return FALSE;
/* If we are not within a user action, be more selective */
if (!in_user_action)
{
/* Avoid pathological cases */
if (other->u.insert.istr.n_chars > 1000)
return FALSE;
/* We will coalesce space, but not new lines. */
if (istring_contains_unichar (&action->u.insert.istr, '\n') ||
istring_contains_unichar (&other->u.insert.istr, '\n'))
return FALSE;
/* Chain space to items that ended in space. This is generally
* just at the start of a line where we could have indentation
* space.
*/
if ((istring_empty (&action->u.insert.istr) ||
istring_ends_with_space (&action->u.insert.istr)) &&
istring_only_contains_space (&other->u.insert.istr))
goto do_chain;
/* Starting a new word, don't chain this */
if (istring_starts_with_space (&other->u.insert.istr))
return FALSE;
/* Check for possible paste (multi-character input) or word input that
* has spaces in it (and should treat as one operation).
*/
if (other->u.insert.istr.n_chars > 1 &&
istring_contains_space (&other->u.insert.istr))
return FALSE;
}
do_chain:
istring_append (&action->u.insert.istr, &other->u.insert.istr);
action->u.insert.end += other->u.insert.end - other->u.insert.begin;
action_free (other);
return TRUE;
}
case ACTION_KIND_DELETE_PROGRAMMATIC:
/* We can't tell if this should be chained because we don't
* have a group to coalesce. But unless each action deletes
* a single character, the overhead isn't too bad as we embed
* the strings in the action.
*/
return FALSE;
case ACTION_KIND_DELETE_SELECTION:
/* Don't join selection deletes as they should appear as a single
* operation and have selection reinstanted when performing undo.
*/
return FALSE;
case ACTION_KIND_DELETE_BACKSPACE:
if (other->u.delete.end == action->u.delete.begin)
{
istring_prepend (&action->u.delete.istr,
&other->u.delete.istr);
action->u.delete.begin = other->u.delete.begin;
action_free (other);
return TRUE;
}
return FALSE;
case ACTION_KIND_DELETE_KEY:
if (action->u.delete.begin == other->u.delete.begin)
{
if (!istring_contains_space (&other->u.delete.istr) ||
istring_only_contains_space (&action->u.delete.istr))
{
istring_append (&action->u.delete.istr, &other->u.delete.istr);
action->u.delete.end += other->u.delete.istr.n_chars;
action_free (other);
return TRUE;
}
}
return FALSE;
case ACTION_KIND_BARRIER:
/* Only allow a single barrier to be added. */
action_free (other);
return TRUE;
case ACTION_KIND_GROUP:
default:
g_return_val_if_reached (FALSE);
}
}
static void
gtk_text_history_do_change_state (GtkTextHistory *self,
gboolean is_modified,
gboolean can_undo,
gboolean can_redo)
{
g_assert (GTK_IS_TEXT_HISTORY (self));
self->funcs.change_state (self->funcs_data, is_modified, can_undo, can_redo);
}
static void
gtk_text_history_do_insert (GtkTextHistory *self,
guint begin,
guint end,
const char *text,
guint len)
{
g_assert (GTK_IS_TEXT_HISTORY (self));
g_assert (text != NULL);
uint_order (&begin, &end);
self->funcs.insert (self->funcs_data, begin, end, text, len);
}
static void
gtk_text_history_do_delete (GtkTextHistory *self,
guint begin,
guint end,
const char *expected_text,
guint len)
{
g_assert (GTK_IS_TEXT_HISTORY (self));
uint_order (&begin, &end);
self->funcs.delete (self->funcs_data, begin, end, expected_text, len);
}
static void
gtk_text_history_do_select (GtkTextHistory *self,
guint selection_insert,
guint selection_bound)
{
g_assert (GTK_IS_TEXT_HISTORY (self));
self->funcs.select (self->funcs_data, selection_insert, selection_bound);
}
static void
gtk_text_history_truncate_one (GtkTextHistory *self)
{
if (self->undo_queue.length > 0)
{
Action *action = g_queue_peek_head (&self->undo_queue);
g_queue_unlink (&self->undo_queue, &action->link);
action_free (action);
}
else if (self->redo_queue.length > 0)
{
Action *action = g_queue_peek_tail (&self->redo_queue);
g_queue_unlink (&self->redo_queue, &action->link);
action_free (action);
}
else
{
g_assert_not_reached ();
}
}
static void
gtk_text_history_truncate (GtkTextHistory *self)
{
g_assert (GTK_IS_TEXT_HISTORY (self));
if (self->max_undo_levels == 0)
return;
while (self->undo_queue.length + self->redo_queue.length > self->max_undo_levels)
gtk_text_history_truncate_one (self);
}
static void
gtk_text_history_finalize (GObject *object)
{
GtkTextHistory *self = (GtkTextHistory *)object;
clear_action_queue (&self->undo_queue);
clear_action_queue (&self->redo_queue);
G_OBJECT_CLASS (gtk_text_history_parent_class)->finalize (object);
}
static void
gtk_text_history_class_init (GtkTextHistoryClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = gtk_text_history_finalize;
}
static void
gtk_text_history_init (GtkTextHistory *self)
{
self->enabled = TRUE;
self->selection.insert = -1;
self->selection.bound = -1;
}
static gboolean
has_actionable (const GQueue *queue)
{
const GList *iter;
for (iter = queue->head; iter; iter = iter->next)
{
const Action *action = iter->data;
if (action->kind == ACTION_KIND_BARRIER)
continue;
if (action->kind == ACTION_KIND_GROUP)
{
if (has_actionable (&action->u.group.actions))
return TRUE;
else
continue;
}
return TRUE;
}
return FALSE;
}
static void
gtk_text_history_update_state (GtkTextHistory *self)
{
g_assert (GTK_IS_TEXT_HISTORY (self));
if (self->irreversible || self->in_user)
{
self->can_undo = FALSE;
self->can_redo = FALSE;
}
else
{
self->can_undo = has_actionable (&self->undo_queue);
self->can_redo = has_actionable (&self->redo_queue);
}
gtk_text_history_do_change_state (self, self->is_modified, self->can_undo, self->can_redo);
}
static void
gtk_text_history_push (GtkTextHistory *self,
Action *action)
{
Action *peek;
gboolean in_user_action;
g_assert (GTK_IS_TEXT_HISTORY (self));
g_assert (self->enabled);
g_assert (action != NULL);
while (self->redo_queue.length > 0)
{
peek = g_queue_peek_head (&self->redo_queue);
g_queue_unlink (&self->redo_queue, &peek->link);
action_free (peek);
}
peek = g_queue_peek_tail (&self->undo_queue);
in_user_action = self->in_user > 0;
if (peek == NULL || !action_chain (peek, action, in_user_action))
g_queue_push_tail_link (&self->undo_queue, &action->link);
gtk_text_history_truncate (self);
gtk_text_history_update_state (self);
}
GtkTextHistory *
gtk_text_history_new (const GtkTextHistoryFuncs *funcs,
gpointer funcs_data)
{
GtkTextHistory *self;
g_return_val_if_fail (funcs != NULL, NULL);
self = g_object_new (GTK_TYPE_TEXT_HISTORY, NULL);
self->funcs = *funcs;
self->funcs_data = funcs_data;
return g_steal_pointer (&self);
}
gboolean
gtk_text_history_get_can_undo (GtkTextHistory *self)
{
g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
return self->can_undo;
}
gboolean
gtk_text_history_get_can_redo (GtkTextHistory *self)
{
g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
return self->can_redo;
}
static void
gtk_text_history_apply (GtkTextHistory *self,
Action *action,
Action *peek)
{
g_assert (GTK_IS_TEXT_HISTORY (self));
g_assert (action != NULL);
switch (action->kind)
{
case ACTION_KIND_INSERT:
gtk_text_history_do_insert (self,
action->u.insert.begin,
action->u.insert.end,
istring_str (&action->u.insert.istr),
action->u.insert.istr.n_bytes);
/* If the next item is a DELETE_SELECTION, then we want to
* pre-select the text for the user. Otherwise, just place
* the cursor were we think it was.
*/
if (peek != NULL && peek->kind == ACTION_KIND_DELETE_SELECTION)
gtk_text_history_do_select (self,
peek->u.delete.begin,
peek->u.delete.end);
else
gtk_text_history_do_select (self,
action->u.insert.end,
action->u.insert.end);
break;
case ACTION_KIND_DELETE_BACKSPACE:
case ACTION_KIND_DELETE_KEY:
case ACTION_KIND_DELETE_PROGRAMMATIC:
case ACTION_KIND_DELETE_SELECTION:
gtk_text_history_do_delete (self,
action->u.delete.begin,
action->u.delete.end,
istring_str (&action->u.delete.istr),
action->u.delete.istr.n_bytes);
gtk_text_history_do_select (self,
action->u.delete.begin,
action->u.delete.begin);
break;
case ACTION_KIND_GROUP: {
const GList *actions = action->u.group.actions.head;
for (const GList *iter = actions; iter; iter = iter->next)
gtk_text_history_apply (self, iter->data, NULL);
break;
}
case ACTION_KIND_BARRIER:
break;
default:
g_assert_not_reached ();
}
if (action->is_modified_set)
self->is_modified = action->is_modified;
}
static void
gtk_text_history_reverse (GtkTextHistory *self,
Action *action)
{
g_assert (GTK_IS_TEXT_HISTORY (self));
g_assert (action != NULL);
switch (action->kind)
{
case ACTION_KIND_INSERT:
gtk_text_history_do_delete (self,
action->u.insert.begin,
action->u.insert.end,
istring_str (&action->u.insert.istr),
action->u.insert.istr.n_bytes);
gtk_text_history_do_select (self,
action->u.insert.begin,
action->u.insert.begin);
break;
case ACTION_KIND_DELETE_BACKSPACE:
case ACTION_KIND_DELETE_KEY:
case ACTION_KIND_DELETE_PROGRAMMATIC:
case ACTION_KIND_DELETE_SELECTION:
gtk_text_history_do_insert (self,
action->u.delete.begin,
action->u.delete.end,
istring_str (&action->u.delete.istr),
action->u.delete.istr.n_bytes);
if (action->u.delete.selection.insert != -1 &&
action->u.delete.selection.bound != -1)
gtk_text_history_do_select (self,
action->u.delete.selection.insert,
action->u.delete.selection.bound);
else if (action->u.delete.selection.insert != -1)
gtk_text_history_do_select (self,
action->u.delete.selection.insert,
action->u.delete.selection.insert);
break;
case ACTION_KIND_GROUP: {
const GList *actions = action->u.group.actions.tail;
for (const GList *iter = actions; iter; iter = iter->prev)
gtk_text_history_reverse (self, iter->data);
break;
}
case ACTION_KIND_BARRIER:
break;
default:
g_assert_not_reached ();
}
if (action->is_modified_set)
self->is_modified = !action->is_modified;
}
static void
move_barrier (GQueue *from_queue,
Action *action,
GQueue *to_queue,
gboolean head)
{
g_queue_unlink (from_queue, &action->link);
if (head)
g_queue_push_head_link (to_queue, &action->link);
else
g_queue_push_tail_link (to_queue, &action->link);
}
void
gtk_text_history_undo (GtkTextHistory *self)
{
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
return_if_irreversible (self);
#if 0
{
char *str = gtk_text_history_printf (self);
g_print ("%s\n", str);
g_free (str);
}
#endif
if (gtk_text_history_get_can_undo (self))
{
Action *action;
self->applying = TRUE;
action = g_queue_peek_tail (&self->undo_queue);
if (action->kind == ACTION_KIND_BARRIER)
{
move_barrier (&self->undo_queue, action, &self->redo_queue, TRUE);
action = g_queue_peek_tail (&self->undo_queue);
}
g_queue_unlink (&self->undo_queue, &action->link);
g_queue_push_head_link (&self->redo_queue, &action->link);
gtk_text_history_reverse (self, action);
gtk_text_history_update_state (self);
self->applying = FALSE;
}
}
void
gtk_text_history_redo (GtkTextHistory *self)
{
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
return_if_irreversible (self);
if (gtk_text_history_get_can_redo (self))
{
Action *action;
Action *peek;
self->applying = TRUE;
action = g_queue_peek_head (&self->redo_queue);
if (action->kind == ACTION_KIND_BARRIER)
{
move_barrier (&self->redo_queue, action, &self->undo_queue, FALSE);
action = g_queue_peek_head (&self->redo_queue);
}
g_queue_unlink (&self->redo_queue, &action->link);
g_queue_push_tail_link (&self->undo_queue, &action->link);
peek = g_queue_peek_head (&self->redo_queue);
gtk_text_history_apply (self, action, peek);
gtk_text_history_update_state (self);
self->applying = FALSE;
}
}
void
gtk_text_history_begin_user_action (GtkTextHistory *self)
{
Action *group;
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
return_if_irreversible (self);
self->in_user++;
group = g_queue_peek_tail (&self->undo_queue);
if (group == NULL || group->kind != ACTION_KIND_GROUP)
{
group = action_new (ACTION_KIND_GROUP);
gtk_text_history_push (self, group);
}
group->u.group.depth++;
gtk_text_history_update_state (self);
}
void
gtk_text_history_end_user_action (GtkTextHistory *self)
{
Action *peek;
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
return_if_irreversible (self);
clear_action_queue (&self->redo_queue);
peek = g_queue_peek_tail (&self->undo_queue);
if (peek->kind != ACTION_KIND_GROUP)
{
g_warning ("miss-matched %s end_user_action. Expected group, got %d",
G_OBJECT_TYPE_NAME (self),
peek->kind);
return;
}
self->in_user--;
peek->u.group.depth--;
/* Unless this is the last user action, short-circuit */
if (peek->u.group.depth > 0)
return;
/* Unlikely, but if the group is empty, just remove it */
if (action_group_is_empty (peek))
{
g_queue_unlink (&self->undo_queue, &peek->link);
action_free (peek);
goto update_state;
}
/* If there is a single item within the group, we can hoist
* it up increasing the chances that we can join actions.
*/
if (peek->u.group.actions.length == 1)
{
GList *link_ = peek->u.group.actions.head;
Action *replaced = link_->data;
replaced->is_modified = peek->is_modified;
replaced->is_modified_set = peek->is_modified_set;
g_queue_unlink (&peek->u.group.actions, link_);
g_queue_unlink (&self->undo_queue, &peek->link);
action_free (peek);
gtk_text_history_push (self, replaced);
goto update_state;
}
/* Now insert a barrier action so we don't allow
* joining items to this node in the future.
*/
gtk_text_history_push (self, action_new (ACTION_KIND_BARRIER));
update_state:
gtk_text_history_update_state (self);
}
void
gtk_text_history_begin_irreversible_action (GtkTextHistory *self)
{
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
if (self->in_user)
{
g_warning ("Cannot begin irreversible action while in user action");
return;
}
self->irreversible++;
clear_action_queue (&self->undo_queue);
clear_action_queue (&self->redo_queue);
gtk_text_history_update_state (self);
}
void
gtk_text_history_end_irreversible_action (GtkTextHistory *self)
{
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
if (self->in_user)
{
g_warning ("Cannot end irreversible action while in user action");
return;
}
self->irreversible--;
clear_action_queue (&self->undo_queue);
clear_action_queue (&self->redo_queue);
gtk_text_history_update_state (self);
}
static void
gtk_text_history_clear_modified (GtkTextHistory *self)
{
const GList *iter;
for (iter = self->undo_queue.head; iter; iter = iter->next)
{
Action *action = iter->data;
action->is_modified = FALSE;
action->is_modified_set = FALSE;
}
for (iter = self->redo_queue.head; iter; iter = iter->next)
{
Action *action = iter->data;
action->is_modified = FALSE;
action->is_modified_set = FALSE;
}
}
void
gtk_text_history_modified_changed (GtkTextHistory *self,
gboolean modified)
{
Action *peek;
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
/* If we have a new save point, clear all previous modified states. */
gtk_text_history_clear_modified (self);
if ((peek = g_queue_peek_tail (&self->undo_queue)))
{
if (peek->kind == ACTION_KIND_BARRIER)
{
if (!(peek = peek->link.prev->data))
return;
}
peek->is_modified = !!modified;
peek->is_modified_set = TRUE;
}
if ((peek = g_queue_peek_head (&self->redo_queue)))
{
if (peek->kind == ACTION_KIND_BARRIER)
{
if (!(peek = peek->link.next->data))
return;
}
peek->is_modified = TRUE;
peek->is_modified_set = TRUE;
}
self->is_modified = !!modified;
self->is_modified_set = TRUE;
if (!self->irreversible)
gtk_text_history_update_state (self);
}
void
gtk_text_history_selection_changed (GtkTextHistory *self,
int selection_insert,
int selection_bound)
{
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
return_if_irreversible (self);
self->selection.insert = CLAMP (selection_insert, -1, G_MAXINT);
self->selection.bound = CLAMP (selection_bound, -1, G_MAXINT);
}
void
gtk_text_history_text_inserted (GtkTextHistory *self,
guint position,
const char *text,
int len)
{
Action *action;
guint n_chars;
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
return_if_irreversible (self);
if (len < 0)
len = strlen (text);
n_chars = g_utf8_strlen (text, len);
action = action_new (ACTION_KIND_INSERT);
action->u.insert.begin = position;
action->u.insert.end = position + n_chars;
istring_set (&action->u.insert.istr, text, len, n_chars);
gtk_text_history_push (self, action);
}
void
gtk_text_history_text_deleted (GtkTextHistory *self,
guint begin,
guint end,
const char *text,
int len)
{
Action *action;
ActionKind kind;
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
return_if_not_enabled (self);
return_if_applying (self);
return_if_irreversible (self);
if (len < 0)
len = strlen (text);
if (self->selection.insert == -1 && self->selection.bound == -1)
kind = ACTION_KIND_DELETE_PROGRAMMATIC;
else if (self->selection.insert == end && self->selection.bound == -1)
kind = ACTION_KIND_DELETE_BACKSPACE;
else if (self->selection.insert == begin && self->selection.bound == -1)
kind = ACTION_KIND_DELETE_KEY;
else
kind = ACTION_KIND_DELETE_SELECTION;
action = action_new (kind);
action->u.delete.begin = begin;
action->u.delete.end = end;
action->u.delete.selection.insert = self->selection.insert;
action->u.delete.selection.bound = self->selection.bound;
istring_set (&action->u.delete.istr, text, len, ABS (end - begin));
gtk_text_history_push (self, action);
}
gboolean
gtk_text_history_get_enabled (GtkTextHistory *self)
{
g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
return self->enabled;
}
void
gtk_text_history_set_enabled (GtkTextHistory *self,
gboolean enabled)
{
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
enabled = !!enabled;
if (self->enabled != enabled)
{
self->enabled = enabled;
if (!self->enabled)
{
self->irreversible = 0;
self->in_user = 0;
clear_action_queue (&self->undo_queue);
clear_action_queue (&self->redo_queue);
}
gtk_text_history_update_state (self);
}
}
guint
gtk_text_history_get_max_undo_levels (GtkTextHistory *self)
{
g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), 0);
return self->max_undo_levels;
}
void
gtk_text_history_set_max_undo_levels (GtkTextHistory *self,
guint max_undo_levels)
{
g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
if (self->max_undo_levels != max_undo_levels)
{
self->max_undo_levels = max_undo_levels;
gtk_text_history_truncate (self);
}
}