aboutsummaryrefslogtreecommitdiff
path: root/src/undo.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/undo.c')
-rw-r--r--src/undo.c646
1 files changed, 646 insertions, 0 deletions
diff --git a/src/undo.c b/src/undo.c
new file mode 100644
index 00000000..3828aebb
--- /dev/null
+++ b/src/undo.c
@@ -0,0 +1,646 @@
+/*
+ * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
+ * Copyright (C) 1999-2001 Hiroyuki Yamamoto
+ *
+ * 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.
+ */
+
+/* code ported from gedit */
+/* This is for my patient girlfirend Regina */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <glib.h>
+#include <gtk/gtktextview.h>
+
+#include <string.h> /* for strlen */
+#include <stdlib.h> /* for mbstowcs */
+
+#include "undo.h"
+#include "utils.h"
+#include "prefs_common.h"
+
+typedef struct _UndoInfo UndoInfo;
+
+struct _UndoInfo
+{
+ UndoAction action;
+ gchar *text;
+ gint start_pos;
+ gint end_pos;
+ gfloat window_position;
+ gint mergeable;
+};
+
+static void undo_free_list (GList **list_pointer);
+static void undo_check_size (UndoMain *undostruct);
+static gint undo_merge (GList *list,
+ guint start_pos,
+ guint end_pos,
+ gint action,
+ const guchar *text);
+static void undo_add (const gchar *text,
+ gint start_pos,
+ gint end_pos,
+ UndoAction action,
+ UndoMain *undostruct);
+static gint undo_get_selection (GtkTextView *textview,
+ guint *start,
+ guint *end);
+static void undo_insert_text_cb (GtkTextBuffer *textbuf,
+ GtkTextIter *iter,
+ gchar *new_text,
+ gint new_text_length,
+ UndoMain *undostruct);
+static void undo_delete_text_cb (GtkTextBuffer *textbuf,
+ GtkTextIter *start,
+ GtkTextIter *end,
+ UndoMain *undostruct);
+
+static void undo_paste_clipboard_cb (GtkTextView *textview,
+ UndoMain *undostruct);
+
+void undo_undo (UndoMain *undostruct);
+void undo_redo (UndoMain *undostruct);
+
+
+UndoMain *undo_init(GtkWidget *text)
+{
+ UndoMain *undostruct;
+ GtkTextView *textview = GTK_TEXT_VIEW(text);
+ GtkTextBuffer *textbuf;
+
+ g_return_val_if_fail(text != NULL, NULL);
+
+ textbuf = gtk_text_view_get_buffer(textview);
+
+ undostruct = g_new(UndoMain, 1);
+ undostruct->textview = textview;
+ undostruct->undo = NULL;
+ undostruct->redo = NULL;
+ undostruct->paste = 0;
+ undostruct->undo_state = FALSE;
+ undostruct->redo_state = FALSE;
+
+ g_signal_connect(G_OBJECT(text), "insert-text",
+ G_CALLBACK(undo_insert_text_cb), undostruct);
+ g_signal_connect(G_OBJECT(text), "delete-text",
+ G_CALLBACK(undo_delete_text_cb), undostruct);
+ g_signal_connect(G_OBJECT(text), "paste-clipboard",
+ G_CALLBACK(undo_paste_clipboard_cb), undostruct);
+
+ return undostruct;
+}
+
+void undo_destroy (UndoMain *undostruct)
+{
+ undo_free_list(&undostruct->undo);
+ undo_free_list(&undostruct->redo);
+ g_free(undostruct);
+}
+
+static UndoInfo *undo_object_new(gchar *text, gint start_pos, gint end_pos,
+ UndoAction action, gfloat window_position)
+{
+ UndoInfo *undoinfo;
+ undoinfo = g_new (UndoInfo, 1);
+ undoinfo->text = text;
+ undoinfo->start_pos = start_pos;
+ undoinfo->end_pos = end_pos;
+ undoinfo->action = action;
+ undoinfo->window_position = window_position;
+ return undoinfo;
+}
+
+static void undo_object_free(UndoInfo *undo)
+{
+ g_free (undo->text);
+ g_free (undo);
+}
+
+/**
+ * undo_free_list:
+ * @list_pointer: list to be freed
+ *
+ * frees and undo structure list
+ **/
+static void undo_free_list(GList **list_pointer)
+{
+ UndoInfo *undo;
+ GList *cur, *list = *list_pointer;
+
+ if (list == NULL) return;
+
+ for (cur = list; cur != NULL; cur = cur->next) {
+ undo = (UndoInfo *)cur->data;
+ undo_object_free(undo);
+ }
+
+ g_list_free(list);
+ *list_pointer = NULL;
+}
+
+void undo_set_change_state_func(UndoMain *undostruct, UndoChangeStateFunc func,
+ gpointer data)
+{
+ g_return_if_fail(undostruct != NULL);
+
+ undostruct->change_state_func = func;
+ undostruct->change_state_data = data;
+}
+
+/**
+ * undo_check_size:
+ * @compose: document to check
+ *
+ * Checks that the size of compose->undo does not excede settings->undo_levels and
+ * frees any undo level above sett->undo_level.
+ *
+ **/
+static void undo_check_size(UndoMain *undostruct)
+{
+ UndoInfo *last_undo;
+ guint length;
+
+ if (prefs_common.undolevels < 1) return;
+
+ /* No need to check for the redo list size since the undo
+ list gets freed on any call to compose_undo_add */
+ length = g_list_length(undostruct->undo);
+ if (length >= prefs_common.undolevels && prefs_common.undolevels > 0) {
+ last_undo = (UndoInfo *)g_list_last(undostruct->undo)->data;
+ undostruct->undo = g_list_remove(undostruct->undo, last_undo);
+ undo_object_free(last_undo);
+ }
+}
+
+/**
+ * undo_merge:
+ * @last_undo:
+ * @start_pos:
+ * @end_pos:
+ * @action:
+ *
+ * This function tries to merge the undo object at the top of
+ * the stack with a new set of data. 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
+ **/
+static gint undo_merge(GList *list, guint start_pos, guint end_pos,
+ gint action, const guchar *text)
+{
+ guchar *temp_string;
+ UndoInfo *last_undo;
+
+ /* This are the cases in which we will NOT merge :
+ 1. if (last_undo->mergeable == FALSE)
+ [mergeable = FALSE when the size of the undo data was not 1.
+ or if the data was size = 1 but = '\n' or if the undo object
+ has been "undone" already ]
+ 2. The size of text is not 1
+ 3. If the new merging data is a '\n'
+ 4. If the last char of the undo_last data is a space/tab
+ and the new char is not a space/tab ( so that we undo
+ words and not chars )
+ 5. If the type (action) of undo is different from the last one
+ Chema */
+
+ if (list == NULL) return FALSE;
+
+ last_undo = list->data;
+
+ if (!last_undo->mergeable) return FALSE;
+
+ if (end_pos - start_pos != 1 ||
+ text[0] == '\n' ||
+ action != last_undo->action ||
+ action == UNDO_ACTION_REPLACE_INSERT ||
+ action == UNDO_ACTION_REPLACE_DELETE) {
+ last_undo->mergeable = FALSE;
+ return FALSE;
+ }
+
+ if (action == UNDO_ACTION_DELETE) {
+ gboolean checkit = TRUE;
+
+ if (last_undo->start_pos != end_pos &&
+ last_undo->start_pos != start_pos) {
+ last_undo->mergeable = FALSE;
+ return FALSE;
+ } else if (last_undo->start_pos == start_pos) {
+ /* Deleted with the delete key */
+ if (text[0] != ' ' && text[0] != '\t' &&
+ (last_undo->text[last_undo->end_pos - last_undo->start_pos - 1] == ' ' ||
+ last_undo->text[last_undo->end_pos - last_undo->start_pos - 1] == '\t'))
+ checkit = FALSE;
+
+ temp_string = g_strdup_printf("%s%s", last_undo->text, text);
+ last_undo->end_pos++;
+ g_free(last_undo->text);
+ last_undo->text = temp_string;
+ } else {
+ /* Deleted with the backspace key */
+ if (text[0] != ' ' && text[0] != '\t' &&
+ (last_undo->text[0] == ' ' ||
+ last_undo->text[0] == '\t'))
+ checkit = FALSE;
+
+ temp_string = g_strdup_printf("%s%s", text, last_undo->text);
+ last_undo->start_pos = start_pos;
+ g_free(last_undo->text);
+ last_undo->text = temp_string;
+ }
+
+ if (!checkit) {
+ last_undo->mergeable = FALSE;
+ return FALSE;
+ }
+ } else if (action == UNDO_ACTION_INSERT) {
+ if (last_undo->end_pos != start_pos) {
+ last_undo->mergeable = FALSE;
+ return FALSE;
+ } else {
+ temp_string = g_strdup_printf("%s%s", last_undo->text, text);
+ g_free(last_undo->text);
+ last_undo->end_pos = end_pos;
+ last_undo->text = temp_string;
+ }
+ } else
+ debug_print("Unknown action [%i] inside undo merge encountered", action);
+
+ return TRUE;
+}
+
+/**
+ * compose_undo_add:
+ * @text:
+ * @start_pos:
+ * @end_pos:
+ * @action: either UNDO_ACTION_INSERT or UNDO_ACTION_DELETE
+ * @compose:
+ * @view: The view so that we save the scroll bar position.
+ *
+ * Adds text to the undo stack. It also performs test to limit the number
+ * of undo levels and deltes the redo list
+ **/
+
+static void undo_add(const gchar *text,
+ gint start_pos, gint end_pos,
+ UndoAction action, UndoMain *undostruct)
+{
+ UndoInfo *undoinfo;
+ GtkAdjustment *vadj;
+
+ g_return_if_fail(text != NULL);
+ g_return_if_fail(end_pos >= start_pos);
+
+ undo_free_list(&undostruct->redo);
+
+ /* Set the redo sensitivity */
+ undostruct->change_state_func(undostruct,
+ UNDO_STATE_UNCHANGED, UNDO_STATE_FALSE,
+ undostruct->change_state_data);
+
+ if (undostruct->paste != 0) {
+ if (action == UNDO_ACTION_INSERT)
+ action = UNDO_ACTION_REPLACE_INSERT;
+ else
+ action = UNDO_ACTION_REPLACE_DELETE;
+ undostruct->paste = undostruct->paste + 1;
+ if (undostruct->paste == 3)
+ undostruct->paste = 0;
+ }
+
+ if (undo_merge(undostruct->undo, start_pos, end_pos, action, text))
+ return;
+
+ undo_check_size(undostruct);
+
+ vadj = GTK_ADJUSTMENT(GTK_TEXT_VIEW(undostruct->textview)->vadjustment);
+ undoinfo = undo_object_new(g_strdup(text), start_pos, end_pos, action,
+ vadj->value);
+
+ if (end_pos - start_pos != 1 || text[0] == '\n')
+ undoinfo->mergeable = FALSE;
+ else
+ undoinfo->mergeable = TRUE;
+
+ undostruct->undo = g_list_prepend(undostruct->undo, undoinfo);
+
+ undostruct->change_state_func(undostruct,
+ UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED,
+ undostruct->change_state_data);
+}
+
+/**
+ * undo_undo:
+ * @w: not used
+ * @data: not used
+ *
+ * Executes an undo request on the current document
+ **/
+void undo_undo(UndoMain *undostruct)
+{
+ UndoInfo *undoinfo;
+ GtkTextView *textview;
+ GtkTextBuffer *buffer;
+ GtkTextIter iter, start_iter, end_iter;
+ GtkTextMark *mark;
+
+ g_return_if_fail(undostruct != NULL);
+
+ if (undostruct->undo == NULL) return;
+
+ /* The undo data we need is always at the top op the
+ stack. So, therefore, the first one */
+ undoinfo = (UndoInfo *)undostruct->undo->data;
+ g_return_if_fail(undoinfo != NULL);
+ undoinfo->mergeable = FALSE;
+ undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
+ undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
+
+ textview = undostruct->textview;
+ buffer = gtk_text_view_get_buffer(textview);
+
+ undo_block(undostruct);
+
+ /* Check if there is a selection active */
+ mark = gtk_text_buffer_get_insert(buffer);
+ gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
+ gtk_text_buffer_place_cursor(buffer, &iter);
+
+ /* Move the view (scrollbars) to the correct position */
+ gtk_adjustment_set_value(GTK_ADJUSTMENT(textview->vadjustment),
+ undoinfo->window_position);
+
+ switch (undoinfo->action) {
+ case UNDO_ACTION_DELETE:
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &iter, undoinfo->start_pos);
+ gtk_text_buffer_insert(buffer, &iter, undoinfo->text, -1);
+ debug_print("UNDO_ACTION_DELETE %s\n", undoinfo->text);
+ break;
+ case UNDO_ACTION_INSERT:
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &start_iter, undoinfo->start_pos);
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &end_iter, undoinfo->end_pos);
+ gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
+ debug_print("UNDO_ACTION_INSERT %d\n", undoinfo->end_pos-undoinfo->start_pos);
+ break;
+ case UNDO_ACTION_REPLACE_INSERT:
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &start_iter, undoinfo->start_pos);
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &end_iter, undoinfo->end_pos);
+ debug_print("UNDO_ACTION_REPLACE %s\n", undoinfo->text);
+ /* "pull" another data structure from the list */
+ undoinfo = (UndoInfo *)undostruct->undo->data;
+ g_return_if_fail(undoinfo != NULL);
+ undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
+ undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
+ g_return_if_fail(undoinfo->action == UNDO_ACTION_REPLACE_DELETE);
+ gtk_text_buffer_insert(buffer, &start_iter, undoinfo->text, -1);
+ debug_print("UNDO_ACTION_REPLACE %s\n", undoinfo->text);
+ break;
+ case UNDO_ACTION_REPLACE_DELETE:
+ g_warning("This should not happen. UNDO_REPLACE_DELETE");
+ break;
+ default:
+ g_assert_not_reached();
+ break;
+ }
+
+ undostruct->change_state_func(undostruct,
+ UNDO_STATE_UNCHANGED, UNDO_STATE_TRUE,
+ undostruct->change_state_data);
+
+ if (undostruct->undo == NULL)
+ undostruct->change_state_func(undostruct,
+ UNDO_STATE_FALSE,
+ UNDO_STATE_UNCHANGED,
+ undostruct->change_state_data);
+
+ undo_unblock(undostruct);
+}
+
+/**
+ * undo_redo:
+ * @w: not used
+ * @data: not used
+ *
+ * executes a redo request on the current document
+ **/
+void undo_redo(UndoMain *undostruct)
+{
+ UndoInfo *redoinfo;
+ GtkTextView *textview;
+ GtkTextBuffer *buffer;
+ GtkTextIter iter, start_iter, end_iter;
+ GtkTextMark *mark;
+
+ g_return_if_fail(undostruct != NULL);
+
+ if (undostruct->redo == NULL) return;
+
+ redoinfo = (UndoInfo *)undostruct->redo->data;
+ g_return_if_fail (redoinfo != NULL);
+ undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
+ undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
+
+ textview = undostruct->textview;
+ buffer = gtk_text_view_get_buffer(textview);
+
+ undo_block(undostruct);
+
+ /* Check if there is a selection active */
+ mark = gtk_text_buffer_get_insert(buffer);
+ gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
+ gtk_text_buffer_place_cursor(buffer, &iter);
+
+ /* Move the view to the right position. */
+ gtk_adjustment_set_value(textview->vadjustment,
+ redoinfo->window_position);
+
+ switch (redoinfo->action) {
+ case UNDO_ACTION_INSERT:
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &iter, redoinfo->start_pos);
+ gtk_text_buffer_insert(buffer, &iter, redoinfo->text, -1);
+ debug_print("UNDO_ACTION_DELETE %s\n",redoinfo->text);
+ break;
+ case UNDO_ACTION_DELETE:
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &start_iter, redoinfo->start_pos);
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &end_iter, redoinfo->end_pos);
+ gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
+ debug_print("UNDO_ACTION_INSERT %d\n",
+ redoinfo->end_pos-redoinfo->start_pos);
+ break;
+ case UNDO_ACTION_REPLACE_DELETE:
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &start_iter, redoinfo->start_pos);
+ gtk_text_buffer_get_iter_at_offset
+ (buffer, &end_iter, redoinfo->end_pos);
+ gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
+ debug_print("UNDO_ACTION_REPLACE %s\n", redoinfo->text);
+ /* "pull" another data structure from the list */
+ redoinfo = (UndoInfo *)undostruct->redo->data;
+ g_return_if_fail(redoinfo != NULL);
+ undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
+ undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
+ g_return_if_fail(redoinfo->action == UNDO_ACTION_REPLACE_INSERT);
+ gtk_text_buffer_insert(buffer, &start_iter, redoinfo->text, -1);
+ break;
+ case UNDO_ACTION_REPLACE_INSERT:
+ g_warning("This should not happen. Redo: UNDO_REPLACE_INSERT");
+ break;
+ default:
+ g_assert_not_reached();
+ break;
+ }
+
+ undostruct->change_state_func(undostruct,
+ UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED,
+ undostruct->change_state_data);
+
+ if (undostruct->redo == NULL)
+ undostruct->change_state_func(undostruct,
+ UNDO_STATE_UNCHANGED,
+ UNDO_STATE_FALSE,
+ undostruct->change_state_data);
+
+ undo_unblock(undostruct);
+}
+
+void undo_block(UndoMain *undostruct)
+{
+ GtkTextBuffer *buffer;
+
+ g_return_if_fail(GTK_IS_TEXT_VIEW(undostruct->textview));
+
+ buffer = gtk_text_view_get_buffer(undostruct->textview);
+ g_signal_handlers_block_by_func
+ (buffer, undo_insert_text_cb, undostruct);
+ g_signal_handlers_block_by_func
+ (buffer, undo_delete_text_cb, undostruct);
+ g_signal_handlers_block_by_func
+ (buffer, undo_paste_clipboard_cb, undostruct);
+}
+
+void undo_unblock(UndoMain *undostruct)
+{
+ GtkTextBuffer *buffer;
+
+ g_return_if_fail(GTK_IS_TEXT_VIEW(undostruct->textview));
+
+ buffer = gtk_text_view_get_buffer(undostruct->textview);
+ g_signal_handlers_unblock_by_func
+ (buffer, undo_insert_text_cb, undostruct);
+ g_signal_handlers_unblock_by_func
+ (buffer, undo_delete_text_cb, undostruct);
+ g_signal_handlers_unblock_by_func
+ (buffer, undo_paste_clipboard_cb, undostruct);
+}
+
+void undo_insert_text_cb(GtkTextBuffer *textbuf, GtkTextIter *iter,
+ gchar *new_text, gint new_text_length,
+ UndoMain *undostruct)
+{
+ gchar *text_to_insert;
+ gint pos;
+
+ if (prefs_common.undolevels <= 0) return;
+
+ pos = gtk_text_iter_get_offset(iter);
+
+ Xstrndup_a(text_to_insert, new_text, new_text_length, return);
+ undo_add(text_to_insert, pos, pos + g_utf8_strlen(text_to_insert, -1),
+ UNDO_ACTION_INSERT, undostruct);
+}
+
+void undo_delete_text_cb(GtkTextBuffer *textbuf, GtkTextIter *start,
+ GtkTextIter *end, UndoMain *undostruct)
+{
+ gchar *text_to_delete;
+ gint start_pos, end_pos;
+
+ if (prefs_common.undolevels <= 0) return;
+
+ text_to_delete = gtk_text_buffer_get_text(textbuf, start, end, FALSE);
+ if (!text_to_delete || !*text_to_delete) return;
+
+ start_pos = gtk_text_iter_get_offset(start);
+ end_pos = gtk_text_iter_get_offset(end);
+
+ undo_add(text_to_delete, start_pos, end_pos, UNDO_ACTION_DELETE,
+ undostruct);
+ g_free(text_to_delete);
+}
+
+void undo_paste_clipboard_cb(GtkTextView *textview, UndoMain *undostruct)
+{
+ debug_print("before Paste: %d\n", undostruct->paste);
+ if (prefs_common.undolevels > 0)
+ if (undo_get_selection(textview, NULL, NULL))
+ undostruct->paste = TRUE;
+ debug_print("after Paste: %d\n", undostruct->paste);
+}
+
+/**
+ * undo_get_selection:
+ * @text: Text to get the selection from
+ * @start: return here the start position of the selection
+ * @end: return here the end position of the selection
+ *
+ * Gets the current selection for View
+ *
+ * Return Value: TRUE if there is a selection active, FALSE if not
+ **/
+static gint undo_get_selection(GtkTextView *textview, guint *start, guint *end)
+{
+ GtkTextBuffer *buffer;
+ GtkTextIter start_iter, end_iter;
+ guint start_pos, end_pos;
+
+ buffer = gtk_text_view_get_buffer(textview);
+ gtk_text_buffer_get_selection_bounds(buffer, &start_iter, &end_iter);
+
+ start_pos = gtk_text_iter_get_offset(&start_iter);
+ end_pos = gtk_text_iter_get_offset(&end_iter);
+
+ /* The user can select from end to start too. If so, swap it*/
+ if (end_pos < start_pos) {
+ guint swap_pos;
+ swap_pos = end_pos;
+ end_pos = start_pos;
+ start_pos = swap_pos;
+ }
+
+ if (start != NULL)
+ *start = start_pos;
+
+ if (end != NULL)
+ *end = end_pos;
+
+ if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
+ return TRUE;
+ else
+ return FALSE;
+}