[gtk/wip/chergert/textundo: 3/8] text: add undo support to GtkText



commit fb4fbfb2a809947926c8d718a3be215f4415db32
Author: Christian Hergert <chergert redhat com>
Date:   Fri Nov 1 11:13:30 2019 -0700

    text: add undo support to GtkText
    
    This adds support using the GtkTextHistory helper for undo/redo to the
    GtkText widget. It is similar in use to GtkTextView, but with a simplified
    interface.
    
    You can disable undo support using the GtkText:enable-undo property. By
    default, it is enabled.

 gtk/gtktext.c          | 171 +++++++++++++++++++++++++++++++++++++++++++++++++
 testsuite/gtk/action.c |   2 +
 2 files changed, 173 insertions(+)
---
diff --git a/gtk/gtktext.c b/gtk/gtktext.c
index 698b1421eb..736b2bc83e 100644
--- a/gtk/gtktext.c
+++ b/gtk/gtktext.c
@@ -58,6 +58,7 @@
 #include "gtksnapshot.h"
 #include "gtkstylecontextprivate.h"
 #include "gtktexthandleprivate.h"
+#include "gtktexthistoryprivate.h"
 #include "gtktextutil.h"
 #include "gtktooltip.h"
 #include "gtktreeselection.h"
@@ -135,6 +136,8 @@
 
 #define UNDERSHOOT_SIZE 20
 
+#define DEFAULT_MAX_UNDO 200
+
 static GQuark          quark_password_hint  = 0;
 
 typedef struct _GtkTextPasswordHint GtkTextPasswordHint;
@@ -175,6 +178,8 @@ struct _GtkTextPrivate
   GtkWidget     *popup_menu;
   GMenuModel    *extra_menu;
 
+  GtkTextHistory *history;
+
   float         xalign;
 
   int           ascent;                     /* font ascent in pango units  */
@@ -559,6 +564,29 @@ static void gtk_text_activate_selection_select_all   (GtkWidget  *widget,
 static void gtk_text_activate_misc_insert_emoji      (GtkWidget  *widget,
                                                       const char *action_name,
                                                       GVariant   *parameter);
+static void gtk_text_real_undo                       (GtkWidget  *widget,
+                                                      const char *action_name,
+                                                      GVariant   *parameters);
+static void gtk_text_real_redo                       (GtkWidget  *widget,
+                                                      const char *action_name,
+                                                      GVariant   *parameters);
+static void gtk_text_history_change_state_cb         (gpointer    funcs_data,
+                                                      gboolean    is_modified,
+                                                      gboolean    can_undo,
+                                                      gboolean    can_redo);
+static void gtk_text_history_insert_cb               (gpointer    funcs_data,
+                                                      guint       begin,
+                                                      guint       end,
+                                                      const char *text,
+                                                      guint       len);
+static void gtk_text_history_delete_cb               (gpointer    funcs_data,
+                                                      guint       begin,
+                                                      guint       end,
+                                                      const char *expected_text,
+                                                      guint       len);
+static void gtk_text_history_select_cb               (gpointer    funcs_data,
+                                                      int         selection_insert,
+                                                      int         selection_bound);
 
 /* GtkTextContent implementation
  */
@@ -645,6 +673,13 @@ gtk_text_content_init (GtkTextContent *content)
 /* GtkText
  */
 
+static const GtkTextHistoryFuncs history_funcs = {
+  gtk_text_history_change_state_cb,
+  gtk_text_history_insert_cb,
+  gtk_text_history_delete_cb,
+  gtk_text_history_select_cb,
+};
+
 G_DEFINE_TYPE_WITH_CODE (GtkText, gtk_text, GTK_TYPE_WIDGET,
                          G_ADD_PRIVATE (GtkText)
                          G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, gtk_text_editable_init))
@@ -1173,6 +1208,9 @@ gtk_text_class_init (GtkTextClass *class)
                   NULL,
                   G_TYPE_NONE, 0);
 
+  gtk_widget_class_install_action (widget_class, "text.undo", NULL, gtk_text_real_undo);
+  gtk_widget_class_install_action (widget_class, "text.redo", NULL, gtk_text_real_redo);
+
   /*
    * Key bindings
    */
@@ -1346,6 +1384,14 @@ gtk_text_class_init (GtkTextClass *class)
   gtk_binding_entry_add_signal (binding_set, GDK_KEY_semicolon, GDK_CONTROL_MASK,
                                 "insert-emoji", 0);
 
+  /* Undo/Redo */
+  gtk_binding_entry_add_action (binding_set, GDK_KEY_z, GDK_CONTROL_MASK,
+                                "text.undo", NULL);
+  gtk_binding_entry_add_action (binding_set, GDK_KEY_y, GDK_CONTROL_MASK,
+                                "text.redo", NULL);
+  gtk_binding_entry_add_action (binding_set, GDK_KEY_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
+                                "text.redo", NULL);
+
   gtk_widget_class_set_accessible_type (widget_class, GTK_TYPE_TEXT_ACCESSIBLE);
   gtk_widget_class_set_css_name (widget_class, I_("text"));
 
@@ -1447,6 +1493,14 @@ gtk_text_set_property (GObject      *object,
       gtk_text_set_alignment (self, g_value_get_float (value));
       break;
 
+    case NUM_PROPERTIES + GTK_EDITABLE_PROP_ENABLE_UNDO:
+      if (g_value_get_boolean (value) != gtk_text_history_get_enabled (priv->history))
+        {
+          gtk_text_history_set_enabled (priv->history, g_value_get_boolean (value));
+          g_object_notify_by_pspec (object, pspec);
+        }
+      break;
+
     /* GtkText properties */
     case PROP_BUFFER:
       gtk_text_set_buffer (self, g_value_get_object (value));
@@ -1578,6 +1632,10 @@ gtk_text_get_property (GObject    *object,
       g_value_set_float (value, priv->xalign);
       break;
 
+    case NUM_PROPERTIES + GTK_EDITABLE_PROP_ENABLE_UNDO:
+      g_value_set_boolean (value, gtk_text_history_get_enabled (priv->history));
+      break;
+
     /* GtkText properties */
     case PROP_BUFFER:
       g_value_set_object (value, get_buffer (self));
@@ -1678,6 +1736,9 @@ gtk_text_init (GtkText *self)
   priv->xalign = 0.0;
   priv->insert_pos = -1;
   priv->cursor_alpha = 1.0;
+  priv->history = gtk_text_history_new (&history_funcs, self);
+
+  gtk_text_history_set_max_undo_levels (priv->history, DEFAULT_MAX_UNDO);
 
   priv->selection_content = g_object_new (GTK_TYPE_TEXT_CONTENT, NULL);
   GTK_TEXT_CONTENT (priv->selection_content)->self = self;
@@ -1812,6 +1873,7 @@ gtk_text_finalize (GObject *object)
 
   g_clear_object (&priv->selection_content);
 
+  g_clear_object (&priv->history);
   g_clear_object (&priv->cached_layout);
   g_clear_object (&priv->im_context);
   g_clear_pointer (&priv->magnifier_popover, gtk_widget_destroy);
@@ -3344,6 +3406,8 @@ buffer_inserted_text (GtkEntryBuffer *buffer,
   gtk_text_set_positions (self, current_pos, selection_bound);
   gtk_text_recompute (self);
 
+  gtk_text_history_text_inserted (priv->history, position, chars, -1);
+
   /* Calculate the password hint if it needs to be displayed. */
   if (n_chars == 1 && !priv->visible)
     {
@@ -3381,6 +3445,35 @@ buffer_deleted_text (GtkEntryBuffer *buffer,
 {
   GtkTextPrivate *priv = gtk_text_get_instance_private (self);
   guint end_pos = position + n_chars;
+
+  if (gtk_text_history_get_enabled (priv->history))
+    {
+      char *deleted_text;
+
+      deleted_text = gtk_editable_get_chars (GTK_EDITABLE (self),
+                                             position,
+                                             end_pos);
+      gtk_text_history_selection_changed (priv->history,
+                                          priv->current_pos,
+                                          priv->selection_bound);
+      gtk_text_history_text_deleted (priv->history,
+                                     position,
+                                     end_pos,
+                                     deleted_text,
+                                     -1);
+
+      g_free (deleted_text);
+    }
+}
+
+static void
+buffer_deleted_text_after (GtkEntryBuffer *buffer,
+                           guint           position,
+                           guint           n_chars,
+                           GtkText        *self)
+{
+  GtkTextPrivate *priv = gtk_text_get_instance_private (self);
+  guint end_pos = position + n_chars;
   int selection_bound;
   guint current_pos;
 
@@ -3435,6 +3528,7 @@ buffer_connect_signals (GtkText *self)
 {
   g_signal_connect (get_buffer (self), "inserted-text", G_CALLBACK (buffer_inserted_text), self);
   g_signal_connect (get_buffer (self), "deleted-text", G_CALLBACK (buffer_deleted_text), self);
+  g_signal_connect_after (get_buffer (self), "deleted-text", G_CALLBACK (buffer_deleted_text_after), self);
   g_signal_connect (get_buffer (self), "notify::text", G_CALLBACK (buffer_notify_text), self);
   g_signal_connect (get_buffer (self), "notify::max-length", G_CALLBACK (buffer_notify_max_length), self);
 }
@@ -3444,6 +3538,7 @@ buffer_disconnect_signals (GtkText *self)
 {
   g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_inserted_text, self);
   g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_deleted_text, self);
+  g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_deleted_text_after, self);
   g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_notify_text, self);
   g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_notify_max_length, self);
 }
@@ -5236,6 +5331,7 @@ static void
 gtk_text_set_text (GtkText     *self,
                    const char *text)
 {
+  GtkTextPrivate *priv = gtk_text_get_instance_private (self);
   int tmp_pos;
 
   g_return_if_fail (GTK_IS_TEXT (self));
@@ -5247,6 +5343,8 @@ gtk_text_set_text (GtkText     *self,
   if (strcmp (gtk_entry_buffer_get_text (get_buffer (self)), text) == 0)
     return;
 
+  gtk_text_history_begin_irreversible_action (priv->history);
+
   begin_change (self);
   g_object_freeze_notify (G_OBJECT (self));
   gtk_text_delete_text (self, 0, -1);
@@ -5254,6 +5352,8 @@ gtk_text_set_text (GtkText     *self,
   gtk_text_insert_text (self, text, strlen (text), &tmp_pos);
   g_object_thaw_notify (G_OBJECT (self));
   end_change (self);
+
+  gtk_text_history_end_irreversible_action (priv->history);
 }
 
 /**
@@ -5293,6 +5393,9 @@ gtk_text_set_visibility (GtkText  *self,
       g_object_notify (G_OBJECT (self), "visibility");
       gtk_text_recompute (self);
 
+      /* disable undo when invisible text is used */
+      gtk_text_history_set_enabled (priv->history, visible);
+
       gtk_text_update_clipboard_actions (self);
     }
 }
@@ -6815,3 +6918,71 @@ gtk_text_get_extra_menu (GtkText *self)
 
   return priv->extra_menu;
 }
+
+static void
+gtk_text_real_undo (GtkWidget  *widget,
+                    const char *action_name,
+                    GVariant   *parameters)
+{
+  GtkText *text = GTK_TEXT (widget);
+  GtkTextPrivate *priv = gtk_text_get_instance_private (text);
+
+  gtk_text_history_undo (priv->history);
+}
+
+static void
+gtk_text_real_redo (GtkWidget  *widget,
+                    const char *action_name,
+                    GVariant   *parameters)
+{
+  GtkText *text = GTK_TEXT (widget);
+  GtkTextPrivate *priv = gtk_text_get_instance_private (text);
+
+  gtk_text_history_redo (priv->history);
+}
+
+static void
+gtk_text_history_change_state_cb (gpointer funcs_data,
+                                  gboolean is_modified,
+                                  gboolean can_undo,
+                                  gboolean can_redo)
+{
+  /* Do nothing */
+}
+
+static void
+gtk_text_history_insert_cb (gpointer    funcs_data,
+                            guint       begin,
+                            guint       end,
+                            const char *str,
+                            guint       len)
+{
+  GtkText *text = funcs_data;
+  int location = begin;
+
+  gtk_editable_insert_text (GTK_EDITABLE (text), str, len, &location);
+}
+
+static void
+gtk_text_history_delete_cb (gpointer    funcs_data,
+                            guint       begin,
+                            guint       end,
+                            const char *expected_text,
+                            guint       len)
+{
+  GtkText *text = funcs_data;
+
+  gtk_editable_delete_text (GTK_EDITABLE (text), begin, end);
+}
+
+static void
+gtk_text_history_select_cb (gpointer funcs_data,
+                            int      selection_insert,
+                            int      selection_bound)
+{
+  GtkText *text = funcs_data;
+
+  gtk_editable_select_region (GTK_EDITABLE (text),
+                              selection_insert,
+                              selection_bound);
+}
diff --git a/testsuite/gtk/action.c b/testsuite/gtk/action.c
index 12dbb42542..50882c36d6 100644
--- a/testsuite/gtk/action.c
+++ b/testsuite/gtk/action.c
@@ -358,6 +358,8 @@ test_introspection (void)
     const char *params;
     const char *property;
   } expected[] = {
+    { GTK_TYPE_TEXT, "text.undo", NULL, NULL },
+    { GTK_TYPE_TEXT, "text.redo", NULL, NULL },
     { GTK_TYPE_TEXT, "clipboard.cut", NULL, NULL },
     { GTK_TYPE_TEXT, "clipboard.copy", NULL, NULL },
     { GTK_TYPE_TEXT, "clipboard.paste", NULL, NULL },


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]