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