From 8e8ee8142ee67597d35af0dd2568ed54a0261cb8 Mon Sep 17 00:00:00 2001 From: Benjamin Otte Date: Sun, 16 Aug 2015 03:09:09 +0200 Subject: [PATCH] entry: Rework undo Instead of storing the actions that were performed, store the before and after state of the entry. This also allows for a very easy way to do merges. --- gtk/gtkentry.c | 64 ++++++----- gtk/gtkentryundocommand.c | 184 +++++++++++++++++++++++-------- gtk/gtkentryundocommandprivate.h | 17 ++- 3 files changed, 184 insertions(+), 81 deletions(-) diff --git a/gtk/gtkentry.c b/gtk/gtkentry.c index 25fda154c9..438d0e07e5 100644 --- a/gtk/gtkentry.c +++ b/gtk/gtkentry.c @@ -5371,6 +5371,17 @@ gtk_entry_remove_password_hint (gpointer data) return FALSE; } +static void +gtk_entry_record_undo_command (GtkEntry *entry, + const GtkEntrySnapshot *before) +{ + GtkUndoCommand *command; + + command = gtk_entry_undo_command_new (entry, before); + gtk_undo_stack_push (entry->priv->undo_stack, command); + g_object_unref (command); +} + /* Default signal handlers */ static void @@ -5379,6 +5390,9 @@ gtk_entry_real_insert_text (GtkEditable *editable, gint new_text_length, gint *position) { + GtkEntry *entry = GTK_ENTRY (editable); + GtkEntryPrivate *priv = entry->priv; + GtkEntrySnapshot snapshot = { NULL, }; guint n_inserted; gint n_chars; @@ -5389,11 +5403,20 @@ gtk_entry_real_insert_text (GtkEditable *editable, * following signal handlers: buffer_inserted_text(), buffer_notify_display_text(), * buffer_notify_text(), buffer_notify_length() */ - begin_change (GTK_ENTRY (editable)); + if (priv->undo_mode == GTK_ENTRY_UNDO_RECORD) + gtk_entry_snapshot_init_from_entry (&snapshot, entry); - n_inserted = gtk_entry_buffer_insert_text (get_buffer (GTK_ENTRY (editable)), *position, new_text, n_chars); + begin_change (entry); - end_change (GTK_ENTRY (editable)); + n_inserted = gtk_entry_buffer_insert_text (get_buffer (entry), *position, new_text, n_chars); + + end_change (entry); + + if (priv->undo_mode == GTK_ENTRY_UNDO_RECORD) + { + gtk_entry_record_undo_command (entry, &snapshot); + gtk_entry_snapshot_clear (&snapshot); + } if (n_inserted != n_chars) gtk_widget_error_bell (GTK_WIDGET (editable)); @@ -5401,19 +5424,6 @@ gtk_entry_real_insert_text (GtkEditable *editable, *position += n_inserted; } -static void -gtk_entry_record_undo_command (GtkEntry *entry, - int start_pos, - const char *deleted_text, - const char *inserted_text) -{ - GtkUndoCommand *command; - - command = gtk_entry_undo_command_new (entry, start_pos, deleted_text, inserted_text); - gtk_undo_stack_push (entry->priv->undo_stack, command); - g_object_unref (command); -} - static void gtk_entry_real_delete_text (GtkEditable *editable, gint start_pos, @@ -5421,6 +5431,7 @@ gtk_entry_real_delete_text (GtkEditable *editable, { GtkEntry *entry = GTK_ENTRY (editable); GtkEntryPrivate *priv = entry->priv; + GtkEntrySnapshot snapshot = { NULL, }; /* * The actual deletion from the buffer. This will end up firing the @@ -5428,23 +5439,20 @@ gtk_entry_real_delete_text (GtkEditable *editable, * buffer_notify_text(), buffer_notify_length() */ - begin_change (entry); - if (priv->undo_mode == GTK_ENTRY_UNDO_RECORD) - { - char *deleted_text = gtk_entry_get_chars (GTK_EDITABLE (entry), start_pos, end_pos); + gtk_entry_snapshot_init_from_entry (&snapshot, entry); - gtk_entry_record_undo_command (entry, - start_pos, - deleted_text, - NULL); - - g_free (deleted_text); - } + begin_change (entry); gtk_entry_buffer_delete_text (get_buffer (entry), start_pos, end_pos - start_pos); end_change (entry); + + if (priv->undo_mode == GTK_ENTRY_UNDO_RECORD) + { + gtk_entry_record_undo_command (entry, &snapshot); + gtk_entry_snapshot_clear (&snapshot); + } } /* GtkEntryBuffer signal handlers @@ -5473,8 +5481,6 @@ buffer_inserted_text (GtkEntryBuffer *buffer, if (priv->undo_mode == GTK_ENTRY_UNDO_RESET) gtk_undo_stack_clear (priv->undo_stack); - else if (priv->undo_mode == GTK_ENTRY_UNDO_RECORD) - gtk_entry_record_undo_command (entry, position, NULL, chars); /* Calculate the password hint if it needs to be displayed. */ if (n_chars == 1 && !priv->visible) diff --git a/gtk/gtkentryundocommand.c b/gtk/gtkentryundocommand.c index e9fa703aca..6a902ea38d 100644 --- a/gtk/gtkentryundocommand.c +++ b/gtk/gtkentryundocommand.c @@ -27,21 +27,50 @@ #include "gtkentryprivate.h" typedef struct _GtkEntryUndoCommandPrivate GtkEntryUndoCommandPrivate; + struct _GtkEntryUndoCommandPrivate { GtkEntry *entry; /* entry we're operating on or NULL if entry was deleted */ - gsize char_offset; /* offset in characters into entry's text */ - char *text_deleted; /* text that will be deleted if we *redo* this command */ - char *text_entered; /* text that will be entered if we *redo* this command */ + GtkEntrySnapshot before; /* what we undo to */ + GtkEntrySnapshot after; /* what we redo to */ }; G_DEFINE_TYPE_WITH_CODE (GtkEntryUndoCommand, gtk_entry_undo_command, GTK_TYPE_UNDO_COMMAND, G_ADD_PRIVATE (GtkEntryUndoCommand)) +void +gtk_entry_snapshot_init_from_entry (GtkEntrySnapshot *snapshot, + GtkEntry *entry) +{ + gint start, end; + + snapshot->text = g_strdup (gtk_entry_get_text (entry)); + gtk_editable_get_selection_bounds (GTK_EDITABLE (entry), &start, &end); + snapshot->cursor = gtk_editable_get_position (GTK_EDITABLE (entry)); + if (start == snapshot->cursor) + snapshot->selection_start = end; + else + snapshot->selection_start = start; +} + +void +gtk_entry_snapshot_clear (GtkEntrySnapshot *snapshot) +{ + g_free (snapshot->text); + snapshot->text = NULL; +} + +static void +gtk_entry_snapshot_copy (GtkEntrySnapshot *target, + const GtkEntrySnapshot *source) +{ + target->text = g_strdup (source->text); + target->cursor = source->cursor; + target->selection_start = source->selection_start; +} + static gboolean -gtk_entry_undo_command_run (GtkEntry *entry, - int position, - int chars_to_delete, - const char *text_to_insert) +gtk_entry_undo_command_run (GtkEntry *entry, + const GtkEntrySnapshot *snapshot) { GtkEntryUndoMode old_mode; @@ -50,18 +79,8 @@ gtk_entry_undo_command_run (GtkEntry *entry, old_mode = gtk_entry_set_undo_mode (entry, GTK_ENTRY_UNDO_REPLAY); - if (chars_to_delete) - gtk_editable_delete_text (GTK_EDITABLE (entry), - position, - position + chars_to_delete); - - if (text_to_insert) - gtk_editable_insert_text (GTK_EDITABLE (entry), - text_to_insert, - -1, - &position); - - gtk_editable_set_position (GTK_EDITABLE (entry), position); + gtk_entry_set_text (entry, snapshot->text); + gtk_editable_select_region (GTK_EDITABLE (entry), snapshot->cursor, snapshot->selection_start); gtk_entry_set_undo_mode (entry, old_mode); @@ -73,10 +92,7 @@ gtk_entry_undo_command_undo (GtkUndoCommand *command) { GtkEntryUndoCommandPrivate *priv = gtk_entry_undo_command_get_instance_private (GTK_ENTRY_UNDO_COMMAND (command)); - return gtk_entry_undo_command_run (priv->entry, - priv->char_offset, - priv->text_entered ? g_utf8_strlen (priv->text_entered, -1) : 0, - priv->text_deleted); + return gtk_entry_undo_command_run (priv->entry, &priv->before); } gboolean @@ -84,30 +100,105 @@ gtk_entry_undo_command_redo (GtkUndoCommand *command) { GtkEntryUndoCommandPrivate *priv = gtk_entry_undo_command_get_instance_private (GTK_ENTRY_UNDO_COMMAND (command)); - return gtk_entry_undo_command_run (priv->entry, - priv->char_offset, - priv->text_deleted ? g_utf8_strlen (priv->text_deleted, -1) : 0, - priv->text_entered); + return gtk_entry_undo_command_run (priv->entry, &priv->after); +} + +static GtkUndoCommand * +gtk_entry_undo_command_new_from_snapshots (GtkEntry *entry, + const GtkEntrySnapshot *before, + const GtkEntrySnapshot *after) +{ + GtkEntryUndoCommand *command; + GtkEntryUndoCommandPrivate *priv; + + command = g_object_new (GTK_TYPE_ENTRY_UNDO_COMMAND, NULL); + priv = gtk_entry_undo_command_get_instance_private (command); + + priv->entry = entry; + gtk_entry_snapshot_copy (&priv->before, before); + gtk_entry_snapshot_copy (&priv->after, after); + + return GTK_UNDO_COMMAND (command); } GtkUndoCommand * gtk_entry_undo_command_merge (GtkUndoCommand *command, GtkUndoCommand *followup) { - return NULL; + GtkEntryUndoCommandPrivate *command_priv = gtk_entry_undo_command_get_instance_private (GTK_ENTRY_UNDO_COMMAND (command)); + GtkEntryUndoCommandPrivate *followup_priv = gtk_entry_undo_command_get_instance_private (GTK_ENTRY_UNDO_COMMAND (followup)); + + if (!GTK_IS_ENTRY_UNDO_COMMAND (followup)) + return NULL; + + if (command_priv->entry != followup_priv->entry) + return NULL; + + if (!g_str_equal (command_priv->after.text, followup_priv->before.text)) + return NULL; + + /* We don't insist on cursor positions being equal here, someone + * might ie. move the cursor to correct a typo + */ + return gtk_entry_undo_command_new_from_snapshots (command_priv->entry, &command_priv->before, &followup_priv->after); +} + +static guint +get_prefix_len (const char *str1, + const char *str2) +{ + guint i; + + for (i = 0; str1[i] == str2[i] && str1[i] != 0; i++) + { + /* nothing to do here */ + } + + return i; +} + +static guint +get_suffix_len (const char *str1, + guint len1, + const char *str2, + guint len2) +{ + const char *cur1, *cur2; + guint i, max_len; + + cur1 = str1 + len1 - 1; + cur2 = str2 + len2 - 1; + max_len = MIN (len1, len2); + + for (i = 0; *cur1 == *cur2 && i < max_len; i++) + { + cur1--; + cur2--; + } + + return i; } char * gtk_entry_undo_command_describe (GtkUndoCommand *command) { GtkEntryUndoCommandPrivate *priv = gtk_entry_undo_command_get_instance_private (GTK_ENTRY_UNDO_COMMAND (command)); + guint before_len, after_len, prefix_len, suffix_len; - if (priv->text_entered) - return g_strdup_printf (_("Entered `%s'"), priv->text_entered); - else if (priv->text_deleted) - return g_strdup_printf (_("Deleted `%s'"), priv->text_deleted); + before_len = strlen (priv->before.text); + after_len = strlen (priv->after.text); + prefix_len = get_prefix_len (priv->before.text, priv->after.text); + suffix_len = get_suffix_len (priv->before.text, before_len, + priv->after.text, after_len); + + if (before_len == after_len && before_len == prefix_len) + return g_strdup (_("No changes")); /* huh? */ + else if (prefix_len + suffix_len == before_len) + return g_strdup_printf (_("Entered `%*s'"), after_len - prefix_len - suffix_len, priv->after.text + prefix_len); + else if (prefix_len + suffix_len == after_len) + return g_strdup_printf (_("Deleted `%*s'"), before_len - prefix_len - suffix_len, priv->before.text + prefix_len); else - return g_strdup (_("Did nothing")); + return g_strdup (_("Text changed")); } static void @@ -115,8 +206,8 @@ gtk_entry_undo_command_finalize (GObject *object) { GtkEntryUndoCommandPrivate *priv = gtk_entry_undo_command_get_instance_private (GTK_ENTRY_UNDO_COMMAND (object)); - g_free (priv->text_deleted); - g_free (priv->text_entered); + gtk_entry_snapshot_clear (&priv->before); + gtk_entry_snapshot_clear (&priv->after); G_OBJECT_CLASS (gtk_entry_undo_command_parent_class)->finalize (object); } @@ -141,24 +232,21 @@ gtk_entry_undo_command_init (GtkEntryUndoCommand *command) } GtkUndoCommand * -gtk_entry_undo_command_new (GtkEntry *entry, - int position, - const char *text_deleted, - const char *text_entered) +gtk_entry_undo_command_new (GtkEntry *entry, + const GtkEntrySnapshot *before) { - GtkEntryUndoCommand *result; - GtkEntryUndoCommandPrivate *priv; + GtkEntrySnapshot after; + GtkUndoCommand *result; g_return_val_if_fail (GTK_IS_ENTRY (entry), NULL); + g_return_val_if_fail (before != NULL, NULL); - result = g_object_new (GTK_TYPE_ENTRY_UNDO_COMMAND, NULL); + gtk_entry_snapshot_init_from_entry (&after, entry); - priv = gtk_entry_undo_command_get_instance_private (result); - priv->entry = entry; - priv->char_offset = position; - priv->text_deleted = g_strdup (text_deleted); - priv->text_entered = g_strdup (text_entered); + result = gtk_entry_undo_command_new_from_snapshots (entry, before, &after); + + gtk_entry_snapshot_clear (&after); - return GTK_UNDO_COMMAND (result); + return result; } diff --git a/gtk/gtkentryundocommandprivate.h b/gtk/gtkentryundocommandprivate.h index 23b69a648e..267c35284b 100644 --- a/gtk/gtkentryundocommandprivate.h +++ b/gtk/gtkentryundocommandprivate.h @@ -34,6 +34,13 @@ G_BEGIN_DECLS typedef struct _GtkEntryUndoCommand GtkEntryUndoCommand; typedef struct _GtkEntryUndoCommandClass GtkEntryUndoCommandClass; +typedef struct _GtkEntrySnapshot GtkEntrySnapshot; + +struct _GtkEntrySnapshot { + char *text; /* text of the whole entry */ + guint cursor; /* cursor position */ + guint selection_start; /* selection start. Equal to cursor if no selection */ +}; struct _GtkEntryUndoCommand { @@ -47,10 +54,12 @@ struct _GtkEntryUndoCommandClass GType gtk_entry_undo_command_get_type (void) G_GNUC_CONST; -GtkUndoCommand * gtk_entry_undo_command_new (GtkEntry *entry, - int position, - const char *text_deleted, - const char *text_entered); +GtkUndoCommand * gtk_entry_undo_command_new (GtkEntry *entry, + const GtkEntrySnapshot *before); + +void gtk_entry_snapshot_init_from_entry (GtkEntrySnapshot *snapshot, + GtkEntry *entry); +void gtk_entry_snapshot_clear (GtkEntrySnapshot *snapshot); G_END_DECLS