diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml
index 625ecac18a..2a30e84349 100644
--- a/demos/gtk-demo/demo.gresource.xml
+++ b/demos/gtk-demo/demo.gresource.xml
@@ -169,6 +169,7 @@
editable_cells.c
entry_buffer.c
entry_completion.c
+ entry_undo.c
expander.c
filtermodel.c
fishbowl.c
@@ -220,6 +221,7 @@
spinner.c
tabs.c
tagged_entry.c
+ textundo.c
textview.c
textscroll.c
theming_style_classes.c
diff --git a/demos/gtk-demo/entry_undo.c b/demos/gtk-demo/entry_undo.c
new file mode 100644
index 0000000000..e94aefd529
--- /dev/null
+++ b/demos/gtk-demo/entry_undo.c
@@ -0,0 +1,52 @@
+/* Entry/Entry Undo
+ *
+ * GtkEntry can provide basic Undo/Redo support using standard keyboard
+ * accelerators such as Primary+z to undo and Primary+Shift+z to redo.
+ * Additionally, Primary+y can be used to redo.
+ *
+ * Use gtk_entry_set_enable_undo() to enable undo/redo support.
+ */
+
+#include
+#include
+
+GtkWidget *
+do_entry_undo (GtkWidget *do_widget)
+{
+ static GtkWidget *window = NULL;
+ GtkWidget *vbox;
+ GtkWidget *label;
+ GtkWidget *entry;
+
+ if (!window)
+ {
+ window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_display (GTK_WINDOW (window),
+ gtk_widget_get_display (do_widget));
+ gtk_window_set_title (GTK_WINDOW (window), "Entry Undo");
+ gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
+ g_signal_connect (window, "destroy",
+ G_CALLBACK (gtk_widget_destroyed), &window);
+
+ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5);
+ g_object_set (vbox, "margin", 5, NULL);
+ gtk_container_add (GTK_CONTAINER (window), vbox);
+
+ label = gtk_label_new (NULL);
+ gtk_label_set_markup (GTK_LABEL (label),
+ "Use Primary+z or Primary+Shift+z to undo or redo changes");
+ gtk_container_add (GTK_CONTAINER (vbox), label);
+
+ /* Create our entry */
+ entry = gtk_entry_new ();
+ gtk_editable_set_enable_undo (GTK_EDITABLE (entry), TRUE);
+ gtk_container_add (GTK_CONTAINER (vbox), entry);
+ }
+
+ if (!gtk_widget_get_visible (window))
+ gtk_widget_show (window);
+ else
+ gtk_widget_destroy (window);
+
+ return window;
+}
diff --git a/demos/gtk-demo/hypertext.c b/demos/gtk-demo/hypertext.c
index b053ed581f..5b6d42befc 100644
--- a/demos/gtk-demo/hypertext.c
+++ b/demos/gtk-demo/hypertext.c
@@ -41,6 +41,7 @@ show_page (GtkTextBuffer *buffer,
gtk_text_buffer_set_text (buffer, "", 0);
gtk_text_buffer_get_iter_at_offset (buffer, &iter, 0);
+ gtk_text_buffer_begin_irreversible_action (buffer);
if (page == 1)
{
gtk_text_buffer_insert (buffer, &iter, "Some text to show that simple ", -1);
@@ -73,6 +74,7 @@ show_page (GtkTextBuffer *buffer,
"so that related items of information are connected.\n", -1);
insert_link (buffer, &iter, "Go back", 1);
}
+ gtk_text_buffer_end_irreversible_action (buffer);
}
/* Looks at all tags covering the position of iter in the text view,
@@ -258,6 +260,7 @@ do_hypertext (GtkWidget *do_widget)
gtk_widget_add_controller (view, controller);
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+ gtk_text_buffer_set_enable_undo (buffer, TRUE);
sw = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
diff --git a/demos/gtk-demo/main.c b/demos/gtk-demo/main.c
index 6460f10388..a188fc3318 100644
--- a/demos/gtk-demo/main.c
+++ b/demos/gtk-demo/main.c
@@ -748,6 +748,9 @@ load_file (const gchar *demoname,
source_buffer = gtk_text_buffer_new (NULL);
+ gtk_text_buffer_begin_irreversible_action (info_buffer);
+ gtk_text_buffer_begin_irreversible_action (source_buffer);
+
resource_filename = g_strconcat ("/sources/", filename, NULL);
bytes = g_resources_lookup_data (resource_filename, 0, &err);
g_free (resource_filename);
@@ -880,9 +883,11 @@ load_file (const gchar *demoname,
fontify (source_buffer);
+ gtk_text_buffer_end_irreversible_action (source_buffer);
gtk_text_view_set_buffer (GTK_TEXT_VIEW (source_view), source_buffer);
g_object_unref (source_buffer);
+ gtk_text_buffer_end_irreversible_action (info_buffer);
gtk_text_view_set_buffer (GTK_TEXT_VIEW (info_view), info_buffer);
g_object_unref (info_buffer);
}
diff --git a/demos/gtk-demo/markup.c b/demos/gtk-demo/markup.c
index 974775e57d..a57c82fb96 100644
--- a/demos/gtk-demo/markup.c
+++ b/demos/gtk-demo/markup.c
@@ -29,8 +29,10 @@ source_toggled (GtkToggleButton *button)
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
gtk_text_buffer_get_bounds (buffer, &start, &end);
+ gtk_text_buffer_begin_irreversible_action (buffer);
gtk_text_buffer_delete (buffer, &start, &end);
gtk_text_buffer_insert_markup (buffer, &start, markup, -1);
+ gtk_text_buffer_end_irreversible_action (buffer);
g_free (markup);
gtk_stack_set_visible_child_name (GTK_STACK (stack), "formatted");
@@ -106,11 +108,15 @@ do_markup (GtkWidget *do_widget)
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
gtk_text_buffer_get_start_iter (buffer, &iter);
+ gtk_text_buffer_begin_irreversible_action (buffer);
gtk_text_buffer_insert_markup (buffer, &iter, markup, -1);
+ gtk_text_buffer_end_irreversible_action (buffer);
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view2));
gtk_text_buffer_get_start_iter (buffer, &iter);
+ gtk_text_buffer_begin_irreversible_action (buffer);
gtk_text_buffer_insert (buffer, &iter, markup, -1);
+ gtk_text_buffer_end_irreversible_action (buffer);
g_bytes_unref (bytes);
diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build
index c021f9f60a..3d20727e6c 100644
--- a/demos/gtk-demo/meson.build
+++ b/demos/gtk-demo/meson.build
@@ -24,6 +24,7 @@ demos = files([
'editable_cells.c',
'entry_buffer.c',
'entry_completion.c',
+ 'entry_undo.c',
'expander.c',
'filtermodel.c',
'fishbowl.c',
@@ -73,6 +74,7 @@ demos = files([
'tabs.c',
'tagged_entry.c',
'textmask.c',
+ 'textundo.c',
'textview.c',
'textscroll.c',
'themes.c',
diff --git a/demos/gtk-demo/textundo.c b/demos/gtk-demo/textundo.c
new file mode 100644
index 0000000000..fe057f00f3
--- /dev/null
+++ b/demos/gtk-demo/textundo.c
@@ -0,0 +1,71 @@
+/* Text View/Undo and Redo
+ *
+ * The GtkTextView supports undo and redo through the use of a
+ * GtkTextBuffer. You can enable or disable undo support using
+ * gtk_text_buffer_set_enable_undo().
+ *
+ * Use Primary+Z to undo and Primary+Shift+Z or Primary+Y to
+ * redo previously undone operations.
+ */
+
+#include
+#include /* for exit() */
+
+GtkWidget *
+do_textundo (GtkWidget *do_widget)
+{
+ static GtkWidget *window = NULL;
+
+ if (!window)
+ {
+ GtkWidget *view;
+ GtkWidget *sw;
+ GtkTextBuffer *buffer;
+ GtkTextIter iter;
+
+ window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_display (GTK_WINDOW (window),
+ gtk_widget_get_display (do_widget));
+ gtk_window_set_default_size (GTK_WINDOW (window),
+ 450, 450);
+
+ g_signal_connect (window, "destroy",
+ G_CALLBACK (gtk_widget_destroyed), &window);
+
+ gtk_window_set_title (GTK_WINDOW (window), "TextView Undo");
+
+ view = gtk_text_view_new ();
+ gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), GTK_WRAP_WORD);
+ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+ gtk_text_buffer_set_enable_undo (buffer, TRUE);
+
+ /* this text cannot be undone */
+ gtk_text_buffer_begin_irreversible_action (buffer);
+ gtk_text_buffer_get_start_iter (buffer, &iter);
+ gtk_text_buffer_insert (buffer, &iter,
+ "Type to add more text.\n"
+ "Use Primary+Z to undo and Primary+Shift+Z to redo a previously undone action.\n"
+ "\n",
+ -1);
+ gtk_text_buffer_end_irreversible_action (buffer);
+
+ sw = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
+ GTK_POLICY_AUTOMATIC,
+ GTK_POLICY_AUTOMATIC);
+ gtk_container_add (GTK_CONTAINER (window), sw);
+ gtk_container_add (GTK_CONTAINER (sw), view);
+ }
+
+ if (!gtk_widget_get_visible (window))
+ {
+ gtk_widget_show (window);
+ }
+ else
+ {
+ gtk_widget_destroy (window);
+ window = NULL;
+ }
+
+ return window;
+}
diff --git a/demos/gtk-demo/textview.c b/demos/gtk-demo/textview.c
index a339d37bb7..c28312a517 100644
--- a/demos/gtk-demo/textview.c
+++ b/demos/gtk-demo/textview.c
@@ -144,6 +144,7 @@ insert_text (GtkTextBuffer *buffer)
*/
gtk_text_buffer_get_iter_at_offset (buffer, &iter, 0);
+ gtk_text_buffer_begin_irreversible_action (buffer);
gtk_text_buffer_insert (buffer, &iter,
"The text widget can display text with all kinds of nifty attributes. "
"It also supports multiple views of the same buffer; this demo is "
@@ -377,6 +378,8 @@ insert_text (GtkTextBuffer *buffer)
gtk_text_buffer_get_bounds (buffer, &start, &end);
gtk_text_buffer_apply_tag_by_name (buffer, "word_wrap", &start, &end);
+ gtk_text_buffer_end_irreversible_action (buffer);
+
g_object_unref (texture);
}
diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt
index 6a246c8e9a..82a10b250e 100644
--- a/docs/reference/gtk/gtk4-sections.txt
+++ b/docs/reference/gtk/gtk4-sections.txt
@@ -882,6 +882,8 @@ gtk_editable_get_width_chars
gtk_editable_set_width_chars
gtk_editable_get_max_width_chars
gtk_editable_set_max_width_chars
+gtk_editable_get_enable_undo
+gtk_editable_set_enable_undo
gtk_editable_install_properties
gtk_editable_init_delegate
@@ -2876,6 +2878,18 @@ gtk_text_buffer_begin_user_action
gtk_text_buffer_end_user_action
gtk_text_buffer_add_selection_clipboard
gtk_text_buffer_remove_selection_clipboard
+gtk_text_buffer_get_can_undo
+gtk_text_buffer_get_can_redo
+gtk_text_buffer_get_enable_undo
+gtk_text_buffer_set_enable_undo
+gtk_text_buffer_get_max_undo_levels
+gtk_text_buffer_set_max_undo_levels
+gtk_text_buffer_undo
+gtk_text_buffer_redo
+gtk_text_buffer_begin_irreversible_action
+gtk_text_buffer_end_irreversible_action
+gtk_text_buffer_begin_user_action
+gtk_text_buffer_end_user_action
GTK_TEXT_BUFFER
diff --git a/docs/reference/gtk/migrating-3to4.xml b/docs/reference/gtk/migrating-3to4.xml
index 39a52e1f21..362619178e 100644
--- a/docs/reference/gtk/migrating-3to4.xml
+++ b/docs/reference/gtk/migrating-3to4.xml
@@ -813,6 +813,21 @@
+
+ GtkEntryBuffer ::deleted-text has changed
+
+ To allow signal handlers to access the deleted text before it
+ has been deleted #GtkEntryBuffer::deleted-text has changed from
+ %G_SIGNAL_RUN_FIRST to %G_SIGNAL_RUN_LAST. The default handler
+ removes the text from the #GtkEntryBuffer.
+
+
+ To adapt existing code, use g_signal_connect_after() or
+ %G_CONNECT_AFTER when using g_signal_connect_data() or
+ g_signal_connect_object().
+
+
+
diff --git a/docs/reference/gtk/text_widget.sgml b/docs/reference/gtk/text_widget.sgml
index 06caf7386f..e0efd6ccd8 100644
--- a/docs/reference/gtk/text_widget.sgml
+++ b/docs/reference/gtk/text_widget.sgml
@@ -112,6 +112,16 @@ multiple bytes in UTF-8, and the two-character sequence "\r\n" is also
considered a line separator.
+
+Text buffers support undo and redo if gtk_text_buffer_set_undo_enabled()
+has been set to %TRUE. Use gtk_text_buffer_undo() or gtk_text_buffer_redo()
+to perform the necessary action. Note that these operations are ignored if
+the buffer is not editable. Developers may want some operations to not be
+undoable. To do this, wrap your changes in
+gtk_text_buffer_begin_irreversible_action() and
+gtk_text_buffer_end_irreversible_action().
+
+
diff --git a/gtk/gtkeditable.c b/gtk/gtkeditable.c
index c61b365012..1ee33acdbf 100644
--- a/gtk/gtkeditable.c
+++ b/gtk/gtkeditable.c
@@ -379,6 +379,13 @@ gtk_editable_default_init (GtkEditableInterface *iface)
0,
GTK_PARAM_READABLE));
+ g_object_interface_install_property (iface,
+ g_param_spec_boolean ("enable-undo",
+ P_("Enable Undo"),
+ P_("If undo/redo should be enabled for the editable"),
+ TRUE,
+ GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY));
+
g_object_interface_install_property (iface,
g_param_spec_int ("selection-bound",
P_("Selection Bound"),
@@ -835,6 +842,46 @@ gtk_editable_set_max_width_chars (GtkEditable *editable,
g_object_set (editable, "max-width-chars", n_chars, NULL);
}
+/**
+ * gtk_editable_get_enable_undo:
+ * @editable: a #GtkEditable
+ *
+ * Gets if undo/redo actions are enabled for @editable
+ *
+ * Returns: %TRUE if undo is enabled
+ */
+gboolean
+gtk_editable_get_enable_undo (GtkEditable *editable)
+{
+ gboolean enable_undo;
+
+ g_return_val_if_fail (GTK_IS_EDITABLE (editable), 0);
+
+ g_object_get (editable, "enable-undo", &enable_undo, NULL);
+
+ return enable_undo;
+}
+
+/**
+ * gtk_editable_set_enable_undo:
+ * @editable: a #GtkEditable
+ * @enable_undo: if undo/redo should be enabled
+ *
+ * If enabled, changes to @editable will be saved for undo/redo actions.
+ *
+ * This results in an additional copy of text changes and are not stored in
+ * secure memory. As such, undo is forcefully disabled when #GtkText:visibility
+ * is set to %FALSE.
+ */
+void
+gtk_editable_set_enable_undo (GtkEditable *editable,
+ gboolean enable_undo)
+{
+ g_return_if_fail (GTK_IS_EDITABLE (editable));
+
+ g_object_set (editable, "enable-undo", enable_undo, NULL);
+}
+
/**
* gtk_editable_install_properties:
* @object_class: a #GObjectClass
@@ -869,6 +916,7 @@ gtk_editable_install_properties (GObjectClass *object_class,
g_object_class_override_property (object_class, first_prop + GTK_EDITABLE_PROP_WIDTH_CHARS, "width-chars");
g_object_class_override_property (object_class, first_prop + GTK_EDITABLE_PROP_MAX_WIDTH_CHARS, "max-width-chars");
g_object_class_override_property (object_class, first_prop + GTK_EDITABLE_PROP_XALIGN, "xalign");
+ g_object_class_override_property (object_class, first_prop + GTK_EDITABLE_PROP_ENABLE_UNDO, "enable-undo");
return GTK_EDITABLE_NUM_PROPERTIES;
}
@@ -982,6 +1030,10 @@ gtk_editable_delegate_set_property (GObject *object,
gtk_editable_set_alignment (delegate, g_value_get_float (value));
break;
+ case GTK_EDITABLE_PROP_ENABLE_UNDO:
+ gtk_editable_set_enable_undo (delegate, g_value_get_boolean (value));
+ break;
+
default:
return FALSE;
}
@@ -1054,6 +1106,10 @@ gtk_editable_delegate_get_property (GObject *object,
g_value_set_float (value, gtk_editable_get_alignment (delegate));
break;
+ case GTK_EDITABLE_PROP_ENABLE_UNDO:
+ g_value_set_boolean (value, gtk_editable_get_enable_undo (delegate));
+ break;
+
default:
return FALSE;
}
diff --git a/gtk/gtkeditable.h b/gtk/gtkeditable.h
index 2de901fa15..c5539fe84e 100644
--- a/gtk/gtkeditable.h
+++ b/gtk/gtkeditable.h
@@ -138,6 +138,11 @@ int gtk_editable_get_max_width_chars (GtkEditable *editable);
GDK_AVAILABLE_IN_ALL
void gtk_editable_set_max_width_chars (GtkEditable *editable,
int n_chars);
+GDK_AVAILABLE_IN_ALL
+gboolean gtk_editable_get_enable_undo (GtkEditable *editable);
+GDK_AVAILABLE_IN_ALL
+void gtk_editable_set_enable_undo (GtkEditable *editable,
+ gboolean enable_undo);
/* api for implementations */
@@ -149,6 +154,7 @@ typedef enum {
GTK_EDITABLE_PROP_WIDTH_CHARS,
GTK_EDITABLE_PROP_MAX_WIDTH_CHARS,
GTK_EDITABLE_PROP_XALIGN,
+ GTK_EDITABLE_PROP_ENABLE_UNDO,
GTK_EDITABLE_NUM_PROPERTIES
} GtkEditableProperties;
diff --git a/gtk/gtkentrybuffer.c b/gtk/gtkentrybuffer.c
index 0ac7cf3a0b..de365ff6a5 100644
--- a/gtk/gtkentrybuffer.c
+++ b/gtk/gtkentrybuffer.c
@@ -192,7 +192,6 @@ gtk_entry_buffer_normal_delete_text (GtkEntryBuffer *buffer,
guint n_chars)
{
GtkEntryBufferPrivate *pv = gtk_entry_buffer_get_instance_private (buffer);
- gsize start, end;
if (position > pv->normal_text_chars)
position = pv->normal_text_chars;
@@ -200,23 +199,7 @@ gtk_entry_buffer_normal_delete_text (GtkEntryBuffer *buffer,
n_chars = pv->normal_text_chars - position;
if (n_chars > 0)
- {
- start = g_utf8_offset_to_pointer (pv->normal_text, position) - pv->normal_text;
- end = g_utf8_offset_to_pointer (pv->normal_text, position + n_chars) - pv->normal_text;
-
- memmove (pv->normal_text + start, pv->normal_text + end, pv->normal_text_bytes + 1 - end);
- pv->normal_text_chars -= n_chars;
- pv->normal_text_bytes -= (end - start);
-
- /*
- * Could be a password, make sure we don't leave anything sensitive after
- * the terminating zero. Note, that the terminating zero already trashed
- * one byte.
- */
- trash_area (pv->normal_text + pv->normal_text_bytes + 1, end - start - 1);
-
- gtk_entry_buffer_emit_deleted_text (buffer, position, n_chars);
- }
+ gtk_entry_buffer_emit_deleted_text (buffer, position, n_chars);
return n_chars;
}
@@ -240,6 +223,23 @@ gtk_entry_buffer_real_deleted_text (GtkEntryBuffer *buffer,
guint position,
guint n_chars)
{
+ GtkEntryBufferPrivate *pv = gtk_entry_buffer_get_instance_private (buffer);
+ gsize start, end;
+
+ start = g_utf8_offset_to_pointer (pv->normal_text, position) - pv->normal_text;
+ end = g_utf8_offset_to_pointer (pv->normal_text, position + n_chars) - pv->normal_text;
+
+ memmove (pv->normal_text + start, pv->normal_text + end, pv->normal_text_bytes + 1 - end);
+ pv->normal_text_chars -= n_chars;
+ pv->normal_text_bytes -= (end - start);
+
+ /*
+ * Could be a password, make sure we don't leave anything sensitive after
+ * the terminating zero. Note, that the terminating zero already trashed
+ * one byte.
+ */
+ trash_area (pv->normal_text + pv->normal_text_bytes + 1, end - start - 1);
+
g_object_notify_by_pspec (G_OBJECT (buffer), entry_buffer_props[PROP_TEXT]);
g_object_notify_by_pspec (G_OBJECT (buffer), entry_buffer_props[PROP_LENGTH]);
}
@@ -405,11 +405,13 @@ gtk_entry_buffer_class_init (GtkEntryBufferClass *klass)
* @position: the position the text was deleted at.
* @n_chars: The number of characters that were deleted.
*
- * This signal is emitted after text is deleted from the buffer.
+ * The text is altered in the default handler for this signal. If you want
+ * access to the text after the text has been modified, use
+ * %G_CONNECT_AFTER.
*/
signals[DELETED_TEXT] = g_signal_new (I_("deleted-text"),
GTK_TYPE_ENTRY_BUFFER,
- G_SIGNAL_RUN_FIRST,
+ G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GtkEntryBufferClass, deleted_text),
NULL, NULL,
_gtk_marshal_VOID__UINT_UINT,
diff --git a/gtk/gtkistringprivate.h b/gtk/gtkistringprivate.h
new file mode 100644
index 0000000000..0af8cb1593
--- /dev/null
+++ b/gtk/gtkistringprivate.h
@@ -0,0 +1,171 @@
+#ifndef __GTK_ISTRING_PRIVATE_H__
+#define __GTK_ISTRING_PRIVATE_H__
+
+#include
+#include
+
+typedef struct
+{
+ guint n_bytes;
+ guint n_chars;
+ union {
+ char buf[24];
+ char *str;
+ } u;
+} IString;
+
+static inline gboolean
+istring_is_inline (const IString *str)
+{
+ return str->n_bytes <= (sizeof str->u.buf - 1);
+}
+
+static inline char *
+istring_str (IString *str)
+{
+ if (istring_is_inline (str))
+ return str->u.buf;
+ else
+ return str->u.str;
+}
+
+static inline void
+istring_clear (IString *str)
+{
+ if (istring_is_inline (str))
+ str->u.buf[0] = 0;
+ else
+ g_clear_pointer (&str->u.str, g_free);
+
+ str->n_bytes = 0;
+ str->n_chars = 0;
+}
+
+static inline void
+istring_set (IString *str,
+ const char *text,
+ guint n_bytes,
+ guint n_chars)
+{
+ if G_LIKELY (n_bytes <= (sizeof str->u.buf - 1))
+ {
+ memcpy (str->u.buf, text, n_bytes);
+ str->u.buf[n_bytes] = 0;
+ }
+ else
+ {
+ str->u.str = g_strndup (text, n_bytes);
+ }
+
+ str->n_bytes = n_bytes;
+ str->n_chars = n_chars;
+}
+
+static inline gboolean
+istring_empty (IString *str)
+{
+ return str->n_bytes == 0;
+}
+
+static inline gboolean
+istring_ends_with_space (IString *str)
+{
+ return g_ascii_isspace (istring_str (str)[str->n_bytes - 1]);
+}
+
+static inline gboolean
+istring_starts_with_space (IString *str)
+{
+ return g_unichar_isspace (g_utf8_get_char (istring_str (str)));
+}
+
+static inline gboolean
+istring_contains_unichar (IString *str,
+ gunichar ch)
+{
+ return g_utf8_strchr (istring_str (str), str->n_bytes, ch) != NULL;
+}
+
+static inline gboolean
+istring_only_contains_space (IString *str)
+{
+ const char *iter;
+
+ for (iter = istring_str (str); *iter; iter = g_utf8_next_char (iter))
+ {
+ if (!g_unichar_isspace (g_utf8_get_char (iter)))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static inline gboolean
+istring_contains_space (IString *str)
+{
+ const char *iter;
+
+ for (iter = istring_str (str); *iter; iter = g_utf8_next_char (iter))
+ {
+ if (g_unichar_isspace (g_utf8_get_char (iter)))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static inline void
+istring_prepend (IString *str,
+ IString *other)
+{
+ if G_LIKELY (str->n_bytes + other->n_bytes < sizeof str->u.buf - 1)
+ {
+ memmove (str->u.buf + other->n_bytes, str->u.buf, str->n_bytes);
+ memcpy (str->u.buf, other->u.buf, other->n_bytes);
+ str->n_bytes += other->n_bytes;
+ str->n_chars += other->n_chars;
+ str->u.buf[str->n_bytes] = 0;
+ }
+ else
+ {
+ gchar *old = NULL;
+
+ if (!istring_is_inline (str))
+ old = str->u.str;
+
+ str->u.str = g_strconcat (istring_str (str), istring_str (other), NULL);
+ str->n_bytes += other->n_bytes;
+ str->n_chars += other->n_chars;
+
+ g_free (old);
+ }
+}
+
+static inline void
+istring_append (IString *str,
+ IString *other)
+{
+ const gchar *text = istring_str (other);
+ guint n_bytes = other->n_bytes;
+ guint n_chars = other->n_chars;
+
+ if G_LIKELY (istring_is_inline (str))
+ {
+ if G_LIKELY (str->n_bytes + n_bytes <= (sizeof str->u.buf - 1))
+ memcpy (str->u.buf + str->n_bytes, text, n_bytes);
+ else
+ str->u.str = g_strconcat (str->u.buf, text, NULL);
+ }
+ else
+ {
+ str->u.str = g_realloc (str->u.str, str->n_bytes + n_bytes + 1);
+ memcpy (str->u.str + str->n_bytes, text, n_bytes);
+ }
+
+ str->n_bytes += n_bytes;
+ str->n_chars += n_chars;
+
+ istring_str (str)[str->n_bytes] = 0;
+}
+
+#endif /* __GTK_ISTRING_PRIVATE_H__ */
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/gtk/gtktextbuffer.c b/gtk/gtktextbuffer.c
index 5434696670..5ddb1e4b9f 100644
--- a/gtk/gtktextbuffer.c
+++ b/gtk/gtktextbuffer.c
@@ -30,6 +30,7 @@
#include "gtkdnd.h"
#include "gtkmarshalers.h"
#include "gtktextbuffer.h"
+#include "gtktexthistoryprivate.h"
#include "gtktextbufferprivate.h"
#include "gtktextbtree.h"
#include "gtktextiterprivate.h"
@@ -38,6 +39,8 @@
#include "gtkprivate.h"
#include "gtkintl.h"
+#define DEFAULT_MAX_UNDO 200
+
/**
* SECTION:gtktextbuffer
* @Short_description: Stores attributed text for display in a GtkTextView
@@ -62,11 +65,15 @@ struct _GtkTextBufferPrivate
GtkTextLogAttrCache *log_attr_cache;
+ GtkTextHistory *history;
+
guint user_action_count;
/* Whether the buffer has been modified since last save */
guint modified : 1;
guint has_selection : 1;
+ guint can_undo : 1;
+ guint can_redo : 1;
};
typedef struct _ClipboardRequest ClipboardRequest;
@@ -93,6 +100,8 @@ enum {
BEGIN_USER_ACTION,
END_USER_ACTION,
PASTE_DONE,
+ UNDO,
+ REDO,
LAST_SIGNAL
};
@@ -108,6 +117,9 @@ enum {
PROP_CURSOR_POSITION,
PROP_COPY_TARGET_LIST,
PROP_PASTE_TARGET_LIST,
+ PROP_CAN_UNDO,
+ PROP_CAN_REDO,
+ PROP_ENABLE_UNDO,
LAST_PROP
};
@@ -138,6 +150,8 @@ static void gtk_text_buffer_real_changed (GtkTextBuffer *buffe
static void gtk_text_buffer_real_mark_set (GtkTextBuffer *buffer,
const GtkTextIter *iter,
GtkTextMark *mark);
+static void gtk_text_buffer_real_undo (GtkTextBuffer *buffer);
+static void gtk_text_buffer_real_redo (GtkTextBuffer *buffer);
static GtkTextBTree* get_btree (GtkTextBuffer *buffer);
static void free_log_attr_cache (GtkTextLogAttrCache *cache);
@@ -154,6 +168,24 @@ static void gtk_text_buffer_get_property (GObject *object,
GValue *value,
GParamSpec *pspec);
+static void gtk_text_buffer_history_change_state (gpointer funcs_data,
+ gboolean is_modified,
+ gboolean can_undo,
+ gboolean can_redo);
+static void gtk_text_buffer_history_insert (gpointer funcs_data,
+ guint begin,
+ guint end,
+ const char *text,
+ guint len);
+static void gtk_text_buffer_history_delete (gpointer funcs_data,
+ guint begin,
+ guint end,
+ const char *expected_text,
+ guint len);
+static void gtk_text_buffer_history_select (gpointer funcs_data,
+ int selection_insert,
+ int selection_bound);
+
static guint signals[LAST_SIGNAL] = { 0 };
static GParamSpec *text_buffer_props[LAST_PROP];
@@ -185,6 +217,13 @@ GType gtk_text_buffer_content_get_type (void) G_GNUC_CONST;
G_DEFINE_TYPE (GtkTextBufferContent, gtk_text_buffer_content, GDK_TYPE_CONTENT_PROVIDER)
+static GtkTextHistoryFuncs history_funcs = {
+ gtk_text_buffer_history_change_state,
+ gtk_text_buffer_history_insert,
+ gtk_text_buffer_history_delete,
+ gtk_text_buffer_history_select,
+};
+
static GdkContentFormats *
gtk_text_buffer_content_ref_formats (GdkContentProvider *provider)
{
@@ -403,6 +442,8 @@ gtk_text_buffer_class_init (GtkTextBufferClass *klass)
klass->remove_tag = gtk_text_buffer_real_remove_tag;
klass->changed = gtk_text_buffer_real_changed;
klass->mark_set = gtk_text_buffer_real_mark_set;
+ klass->undo = gtk_text_buffer_real_undo;
+ klass->redo = gtk_text_buffer_real_redo;
/* Construct */
text_buffer_props[PROP_TAG_TABLE] =
@@ -439,6 +480,45 @@ gtk_text_buffer_class_init (GtkTextBufferClass *klass)
FALSE,
GTK_PARAM_READABLE);
+ /**
+ * GtkTextBuffer:can-undo:
+ *
+ * The :can-undo property denotes that the buffer can undo the last
+ * applied action.
+ */
+ text_buffer_props[PROP_CAN_UNDO] =
+ g_param_spec_boolean ("can-undo",
+ P_("Can Undo"),
+ P_("If the buffer can have the last action undone"),
+ FALSE,
+ GTK_PARAM_READABLE);
+
+ /**
+ * GtkTextBuffer:can-redo:
+ *
+ * The :can-redo property denotes that the buffer can reapply the
+ * last undone action.
+ */
+ text_buffer_props[PROP_CAN_REDO] =
+ g_param_spec_boolean ("can-redo",
+ P_("Can Redo"),
+ P_("If the buffer can have the last undone action reapplied"),
+ FALSE,
+ GTK_PARAM_READABLE);
+
+ /**
+ * GtkTextBuffer:enable-undo:
+ *
+ * The :enable-undo property denotes if support for undoing and redoing
+ * changes to the buffer is allowed.
+ */
+ text_buffer_props[PROP_ENABLE_UNDO] =
+ g_param_spec_boolean ("enable-undo",
+ "Enable Undo",
+ "Enable support for undo and redo in the text view",
+ TRUE,
+ GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
/**
* GtkTextBuffer:cursor-position:
*
@@ -840,6 +920,34 @@ gtk_text_buffer_class_init (GtkTextBufferClass *klass)
1,
GDK_TYPE_CLIPBOARD);
+ /**
+ * GtkTextBuffer::redo:
+ * @buffer: a #GtkTextBuffer
+ *
+ * The "redo" signal is emitted when a request has been made to redo the
+ * previously undone operation.
+ */
+ signals[REDO] =
+ g_signal_new (I_("redo"),
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtkTextBufferClass, redo),
+ NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+ /**
+ * GtkTextBuffer::undo:
+ * @buffer: a #GtkTextBuffer
+ *
+ * The "undo" signal is emitted when a request has been made to undo the
+ * previous operation or set of operations that have been grouped together.
+ */
+ signals[UNDO] =
+ g_signal_new (I_("undo"),
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtkTextBufferClass, undo),
+ NULL, NULL, NULL, G_TYPE_NONE, 0);
+
gtk_text_buffer_register_serializers ();
}
@@ -848,6 +956,9 @@ gtk_text_buffer_init (GtkTextBuffer *buffer)
{
buffer->priv = gtk_text_buffer_get_instance_private (buffer);
buffer->priv->tag_table = NULL;
+ buffer->priv->history = gtk_text_history_new (&history_funcs, buffer);
+
+ gtk_text_history_set_max_undo_levels (buffer->priv->history, DEFAULT_MAX_UNDO);
}
static void
@@ -891,6 +1002,10 @@ gtk_text_buffer_set_property (GObject *object,
switch (prop_id)
{
+ case PROP_ENABLE_UNDO:
+ gtk_text_buffer_set_enable_undo (text_buffer, g_value_get_boolean (value));
+ break;
+
case PROP_TAG_TABLE:
set_table (text_buffer, g_value_get_object (value));
break;
@@ -919,6 +1034,10 @@ gtk_text_buffer_get_property (GObject *object,
switch (prop_id)
{
+ case PROP_ENABLE_UNDO:
+ g_value_set_boolean (value, gtk_text_buffer_get_enable_undo (text_buffer));
+ break;
+
case PROP_TAG_TABLE:
g_value_set_object (value, get_table (text_buffer));
break;
@@ -946,6 +1065,14 @@ gtk_text_buffer_get_property (GObject *object,
g_value_set_int (value, gtk_text_iter_get_offset (&iter));
break;
+ case PROP_CAN_UNDO:
+ g_value_set_boolean (value, gtk_text_buffer_get_can_undo (text_buffer));
+ break;
+
+ case PROP_CAN_REDO:
+ g_value_set_boolean (value, gtk_text_buffer_get_can_redo (text_buffer));
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -981,6 +1108,8 @@ gtk_text_buffer_finalize (GObject *object)
remove_all_selection_clipboards (buffer);
+ g_clear_object (&buffer->priv->history);
+
if (priv->tag_table)
{
_gtk_text_tag_table_remove_buffer (priv->tag_table, buffer);
@@ -1058,6 +1187,8 @@ gtk_text_buffer_set_text (GtkTextBuffer *buffer,
if (len < 0)
len = strlen (text);
+ gtk_text_history_begin_irreversible_action (buffer->priv->history);
+
gtk_text_buffer_get_bounds (buffer, &start, &end);
gtk_text_buffer_delete (buffer, &start, &end);
@@ -1067,6 +1198,8 @@ gtk_text_buffer_set_text (GtkTextBuffer *buffer,
gtk_text_buffer_get_iter_at_offset (buffer, &start, 0);
gtk_text_buffer_insert (buffer, &start, text, len);
}
+
+ gtk_text_history_end_irreversible_action (buffer->priv->history);
}
@@ -1084,6 +1217,11 @@ gtk_text_buffer_real_insert_text (GtkTextBuffer *buffer,
g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
g_return_if_fail (iter != NULL);
+ gtk_text_history_text_inserted (buffer->priv->history,
+ gtk_text_iter_get_offset (iter),
+ text,
+ len);
+
_gtk_text_btree_insert (iter, text, len);
g_signal_emit (buffer, signals[CHANGED], 0);
@@ -1798,6 +1936,28 @@ gtk_text_buffer_real_delete_range (GtkTextBuffer *buffer,
g_return_if_fail (start != NULL);
g_return_if_fail (end != NULL);
+ if (gtk_text_history_get_enabled (buffer->priv->history))
+ {
+ GtkTextIter sel_begin, sel_end;
+ gchar *text;
+
+ if (gtk_text_buffer_get_selection_bounds (buffer, &sel_begin, &sel_end))
+ gtk_text_history_selection_changed (buffer->priv->history,
+ gtk_text_iter_get_offset (&sel_begin),
+ gtk_text_iter_get_offset (&sel_end));
+ else
+ gtk_text_history_selection_changed (buffer->priv->history,
+ gtk_text_iter_get_offset (&sel_begin),
+ -1);
+
+ text = gtk_text_iter_get_slice (start, end);
+ gtk_text_history_text_deleted (buffer->priv->history,
+ gtk_text_iter_get_offset (start),
+ gtk_text_iter_get_offset (end),
+ text, -1);
+ g_free (text);
+ }
+
_gtk_text_btree_delete (start, end);
/* may have deleted the selection... */
@@ -3274,17 +3434,14 @@ void
gtk_text_buffer_set_modified (GtkTextBuffer *buffer,
gboolean setting)
{
- gboolean fixed_setting;
-
g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
- fixed_setting = setting != FALSE;
+ setting = !!setting;
- if (buffer->priv->modified == fixed_setting)
- return;
- else
+ if (buffer->priv->modified != setting)
{
- buffer->priv->modified = fixed_setting;
+ buffer->priv->modified = setting;
+ gtk_text_history_modified_changed (buffer->priv->history, setting);
g_signal_emit (buffer, signals[MODIFIED_CHANGED], 0);
}
}
@@ -4723,3 +4880,258 @@ gtk_text_buffer_insert_markup (GtkTextBuffer *buffer,
pango_attr_list_unref (attributes);
g_free (text);
}
+
+static void
+gtk_text_buffer_real_undo (GtkTextBuffer *buffer)
+{
+ if (gtk_text_history_get_can_undo (buffer->priv->history))
+ gtk_text_history_undo (buffer->priv->history);
+}
+
+static void
+gtk_text_buffer_real_redo (GtkTextBuffer *buffer)
+{
+ if (gtk_text_history_get_can_redo (buffer->priv->history))
+ gtk_text_history_redo (buffer->priv->history);
+}
+
+gboolean
+gtk_text_buffer_get_can_undo (GtkTextBuffer *buffer)
+{
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
+
+ return gtk_text_history_get_can_undo (buffer->priv->history);
+}
+
+gboolean
+gtk_text_buffer_get_can_redo (GtkTextBuffer *buffer)
+{
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
+
+ return gtk_text_history_get_can_redo (buffer->priv->history);
+}
+
+static void
+gtk_text_buffer_history_change_state (gpointer funcs_data,
+ gboolean is_modified,
+ gboolean can_undo,
+ gboolean can_redo)
+{
+ GtkTextBuffer *buffer = funcs_data;
+
+ if (buffer->priv->can_undo != can_undo)
+ {
+ buffer->priv->can_undo = can_undo;
+ g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props[PROP_CAN_UNDO]);
+ }
+
+ if (buffer->priv->can_redo != can_redo)
+ {
+ buffer->priv->can_redo = can_redo;
+ g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props[PROP_CAN_REDO]);
+ }
+
+ if (buffer->priv->modified != is_modified)
+ gtk_text_buffer_set_modified (buffer, is_modified);
+}
+
+static void
+gtk_text_buffer_history_insert (gpointer funcs_data,
+ guint begin,
+ guint end,
+ const char *text,
+ guint len)
+{
+ GtkTextBuffer *buffer = funcs_data;
+ GtkTextIter iter;
+
+ gtk_text_buffer_get_iter_at_offset (buffer, &iter, begin);
+ gtk_text_buffer_insert (buffer, &iter, text, len);
+}
+
+static void
+gtk_text_buffer_history_delete (gpointer funcs_data,
+ guint begin,
+ guint end,
+ const char *expected_text,
+ guint len)
+{
+ GtkTextBuffer *buffer = funcs_data;
+ GtkTextIter iter;
+ GtkTextIter end_iter;
+
+ gtk_text_buffer_get_iter_at_offset (buffer, &iter, begin);
+ gtk_text_buffer_get_iter_at_offset (buffer, &end_iter, end);
+ gtk_text_buffer_delete (buffer, &iter, &end_iter);
+}
+
+static void
+gtk_text_buffer_history_select (gpointer funcs_data,
+ int selection_insert,
+ int selection_bound)
+{
+ GtkTextBuffer *buffer = funcs_data;
+ GtkTextIter insert;
+ GtkTextIter bound;
+
+ if (selection_insert == -1 || selection_bound == -1)
+ return;
+
+ gtk_text_buffer_get_iter_at_offset (buffer, &insert, selection_insert);
+ gtk_text_buffer_get_iter_at_offset (buffer, &bound, selection_bound);
+ gtk_text_buffer_select_range (buffer, &insert, &bound);
+}
+
+/**
+ * gtk_text_buffer_undo:
+ * @buffer: a #GtkTextBuffer
+ *
+ * Undoes the last undoable action on the buffer, if there is one.
+ */
+void
+gtk_text_buffer_undo (GtkTextBuffer *buffer)
+{
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+ if (gtk_text_buffer_get_can_undo (buffer))
+ g_signal_emit (buffer, signals[UNDO], 0);
+}
+
+/**
+ * gtk_text_buffer_redo:
+ * @buffer: a #GtkTextBuffer
+ *
+ * Redoes the next redoable action on the buffer, if there is one.
+ */
+void
+gtk_text_buffer_redo (GtkTextBuffer *buffer)
+{
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+ if (gtk_text_buffer_get_can_redo (buffer))
+ g_signal_emit (buffer, signals[REDO], 0);
+}
+
+/**
+ * gtk_text_buffer_get_enable_undo:
+ * @buffer: a #GtkTextBuffer
+ *
+ * Gets whether the buffer is saving modifications to the buffer to allow for
+ * undo and redo actions.
+ *
+ * See gtk_text_buffer_begin_irreversible_action() and
+ * gtk_text_buffer_end_irreversible_action() to create changes to the buffer
+ * that cannot be undone.
+ */
+gboolean
+gtk_text_buffer_get_enable_undo (GtkTextBuffer *buffer)
+{
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
+
+ return gtk_text_history_get_enabled (buffer->priv->history);
+}
+
+/**
+ * gtk_text_buffer_set_enable_undo:
+ * @buffer: a #GtkTextBuffer
+ *
+ * Sets whether or not to enable undoable actions in the text buffer. If
+ * enabled, the user will be able to undo the last number of actions up to
+ * gtk_text_buffer_get_max_undo_levels().
+ *
+ * See gtk_text_buffer_begin_irreversible_action() and
+ * gtk_text_buffer_end_irreversible_action() to create changes to the buffer
+ * that cannot be undone.
+ */
+void
+gtk_text_buffer_set_enable_undo (GtkTextBuffer *buffer,
+ gboolean enabled)
+{
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+ if (enabled != gtk_text_history_get_enabled (buffer->priv->history))
+ {
+ gtk_text_history_set_enabled (buffer->priv->history, enabled);
+ g_object_notify_by_pspec (G_OBJECT (buffer),
+ text_buffer_props[PROP_ENABLE_UNDO]);
+ }
+}
+
+/**
+ * gtk_text_buffer_begin_irreversible_action:
+ * @self: a #Gtktextbuffer
+ *
+ * Denotes the beginning of an action that may not be undone. This will cause
+ * any previous operations in the undo/redo queue to be cleared.
+ *
+ * This should be paired with a call to
+ * gtk_text_buffer_end_irreversible_action() after the irreversible action
+ * has completed.
+ *
+ * You may nest calls to gtk_text_buffer_begin_irreversible_action() and
+ * gtk_text_buffer_end_irreversible_action() pairs.
+ */
+void
+gtk_text_buffer_begin_irreversible_action (GtkTextBuffer *buffer)
+{
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+ gtk_text_history_begin_irreversible_action (buffer->priv->history);
+}
+
+/**
+ * gtk_text_buffer_end_irreversible_action:
+ * @self: a #Gtktextbuffer
+ *
+ * Denotes the end of an action that may not be undone. This will cause
+ * any previous operations in the undo/redo queue to be cleared.
+ *
+ * This should be called after completing modifications to the text buffer
+ * after gtk_text_buffer_begin_irreversible_action() was called.
+ *
+ * You may nest calls to gtk_text_buffer_begin_irreversible_action() and
+ * gtk_text_buffer_end_irreversible_action() pairs.
+ */
+void
+gtk_text_buffer_end_irreversible_action (GtkTextBuffer *buffer)
+{
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+ gtk_text_history_end_irreversible_action (buffer->priv->history);
+}
+
+/**
+ * gtk_text_buffer_get_max_undo_levels:
+ * @buffer: a #GtkTextBuffer
+ *
+ * Gets the maximum number of undo levels to perform. If 0, unlimited undo
+ * actions may be performed. Note that this may have a memory usage impact
+ * as it requires storing an additional copy of the inserted or removed text
+ * within the text buffer.
+ */
+guint
+gtk_text_buffer_get_max_undo_levels (GtkTextBuffer *buffer)
+{
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), 0);
+
+ return gtk_text_history_get_max_undo_levels (buffer->priv->history);
+}
+
+/**
+ * gtk_text_buffer_set_max_undo_levels:
+ * @buffer: a #GtkTextBuffer
+ * @max_undo_levels: the maximum number of undo actions to perform
+ *
+ * Sets the maximum number of undo levels to perform. If 0, unlimited undo
+ * actions may be performed. Note that this may have a memory usage impact
+ * as it requires storing an additional copy of the inserted or removed text
+ * within the text buffer.
+ */
+void
+gtk_text_buffer_set_max_undo_levels (GtkTextBuffer *buffer,
+ guint max_undo_levels)
+{
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+ gtk_text_history_set_max_undo_levels (buffer->priv->history, max_undo_levels);
+}
diff --git a/gtk/gtktextbuffer.h b/gtk/gtktextbuffer.h
index 51668cbb6e..9517077fe6 100644
--- a/gtk/gtktextbuffer.h
+++ b/gtk/gtktextbuffer.h
@@ -146,6 +146,8 @@ struct _GtkTextBufferClass
void (* paste_done) (GtkTextBuffer *buffer,
GdkClipboard *clipboard);
+ void (* undo) (GtkTextBuffer *buffer);
+ void (* redo) (GtkTextBuffer *buffer);
/*< private >*/
@@ -451,11 +453,32 @@ gboolean gtk_text_buffer_delete_selection (GtkTextBuffer *buffer,
gboolean interactive,
gboolean default_editable);
-/* Called to specify atomic user actions, used to implement undo */
GDK_AVAILABLE_IN_ALL
-void gtk_text_buffer_begin_user_action (GtkTextBuffer *buffer);
+gboolean gtk_text_buffer_get_can_undo (GtkTextBuffer *buffer);
GDK_AVAILABLE_IN_ALL
-void gtk_text_buffer_end_user_action (GtkTextBuffer *buffer);
+gboolean gtk_text_buffer_get_can_redo (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+gboolean gtk_text_buffer_get_enable_undo (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_buffer_set_enable_undo (GtkTextBuffer *buffer,
+ gboolean enable_undo);
+GDK_AVAILABLE_IN_ALL
+guint gtk_text_buffer_get_max_undo_levels (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_buffer_set_max_undo_levels (GtkTextBuffer *buffer,
+ guint max_undo_levels);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_buffer_undo (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_buffer_redo (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_buffer_begin_irreversible_action (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_buffer_end_irreversible_action (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_buffer_begin_user_action (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_buffer_end_user_action (GtkTextBuffer *buffer);
G_END_DECLS
diff --git a/gtk/gtktexthistory.c b/gtk/gtktexthistory.c
new file mode 100644
index 0000000000..c6a787654b
--- /dev/null
+++ b/gtk/gtktexthistory.c
@@ -0,0 +1,1062 @@
+/* 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 .
+ */
+
+#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 discared.
+ */
+
+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 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_slice_new0 (Action);
+ 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_slice_free (Action, 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)
+ {
+ /* 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)
+ action_free (other);
+ else
+ 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;
+ }
+
+ 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 (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;
+ }
+
+ /* 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);
+ return_if_irreversible (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;
+ }
+
+ self->is_modified = !!modified;
+ self->is_modified_set = TRUE;
+
+ 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);
+
+ if (self->in_user == 0 && self->irreversible == 0)
+ {
+ 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;
+
+ 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);
+
+ action = action_new (ACTION_KIND_INSERT);
+ action->u.insert.begin = position;
+ action->u.insert.end = position + g_utf8_strlen (text, len);
+ istring_set (&action->u.insert.istr,
+ text,
+ len,
+ action->u.insert.end);
+
+ 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);
+ }
+ }
+}
+
+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);
+ }
+}
diff --git a/gtk/gtktexthistoryprivate.h b/gtk/gtktexthistoryprivate.h
new file mode 100644
index 0000000000..b65682d295
--- /dev/null
+++ b/gtk/gtktexthistoryprivate.h
@@ -0,0 +1,84 @@
+/* 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 .
+ */
+
+#ifndef __GTK_TEXT_HISTORY_PRIVATE_H__
+#define __GTK_TEXT_HISTORY_PRIVATE_H__
+
+#include
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_TEXT_HISTORY (gtk_text_history_get_type())
+
+typedef struct _GtkTextHistoryFuncs GtkTextHistoryFuncs;
+
+G_DECLARE_FINAL_TYPE (GtkTextHistory, gtk_text_history, GTK, TEXT_HISTORY, GObject)
+
+struct _GtkTextHistoryFuncs
+{
+ void (*change_state) (gpointer funcs_data,
+ gboolean is_modified,
+ gboolean can_undo,
+ gboolean can_redo);
+ void (*insert) (gpointer funcs_data,
+ guint begin,
+ guint end,
+ const char *text,
+ guint len);
+ void (*delete) (gpointer funcs_data,
+ guint begin,
+ guint end,
+ const char *expected_text,
+ guint len);
+ void (*select) (gpointer funcs_data,
+ int selection_insert,
+ int selection_bound);
+};
+
+GtkTextHistory *gtk_text_history_new (const GtkTextHistoryFuncs *funcs,
+ gpointer funcs_data);
+void gtk_text_history_begin_user_action (GtkTextHistory *self);
+void gtk_text_history_end_user_action (GtkTextHistory *self);
+void gtk_text_history_begin_irreversible_action (GtkTextHistory *self);
+void gtk_text_history_end_irreversible_action (GtkTextHistory *self);
+gboolean gtk_text_history_get_can_undo (GtkTextHistory *self);
+gboolean gtk_text_history_get_can_redo (GtkTextHistory *self);
+void gtk_text_history_undo (GtkTextHistory *self);
+void gtk_text_history_redo (GtkTextHistory *self);
+guint gtk_text_history_get_max_undo_levels (GtkTextHistory *self);
+void gtk_text_history_set_max_undo_levels (GtkTextHistory *self,
+ guint max_undo_levels);
+void gtk_text_history_modified_changed (GtkTextHistory *self,
+ gboolean modified);
+void gtk_text_history_selection_changed (GtkTextHistory *self,
+ int selection_insert,
+ int selection_bound);
+void gtk_text_history_text_inserted (GtkTextHistory *self,
+ guint position,
+ const char *text,
+ int len);
+void gtk_text_history_text_deleted (GtkTextHistory *self,
+ guint begin,
+ guint end,
+ const char *text,
+ int len);
+gboolean gtk_text_history_get_enabled (GtkTextHistory *self);
+void gtk_text_history_set_enabled (GtkTextHistory *self,
+ gboolean enabled);
+
+G_END_DECLS
+
+#endif /* __GTK_TEXT_HISTORY_PRIVATE_H__ */
diff --git a/gtk/gtktextprivate.h b/gtk/gtktextprivate.h
index daeed71bce..500389824d 100644
--- a/gtk/gtktextprivate.h
+++ b/gtk/gtktextprivate.h
@@ -85,6 +85,8 @@ struct _GtkTextClass
void (* paste_clipboard) (GtkText *self);
void (* toggle_overwrite) (GtkText *self);
void (* insert_emoji) (GtkText *self);
+ void (* undo) (GtkText *self);
+ void (* redo) (GtkText *self);
};
char * gtk_text_get_display_text (GtkText *entry,
diff --git a/gtk/gtktextview.c b/gtk/gtktextview.c
index 766060aff7..0c1282c011 100644
--- a/gtk/gtktextview.c
+++ b/gtk/gtktextview.c
@@ -619,6 +619,13 @@ static void gtk_text_view_activate_misc_insert_emoji (GtkWidget *widget,
const char *action_name,
GVariant *parameter);
+static void gtk_text_view_real_undo (GtkWidget *widget,
+ const gchar *action_name,
+ GVariant *parameter);
+static void gtk_text_view_real_redo (GtkWidget *widget,
+ const gchar *action_name,
+ GVariant *parameter);
+
/* FIXME probably need the focus methods. */
@@ -1358,6 +1365,9 @@ gtk_text_view_class_init (GtkTextViewClass *klass)
NULL,
G_TYPE_NONE, 0);
+ gtk_widget_class_install_action (widget_class, "text.undo", NULL, gtk_text_view_real_undo);
+ gtk_widget_class_install_action (widget_class, "text.redo", NULL, gtk_text_view_real_redo);
+
/*
* Key bindings
*/
@@ -1550,6 +1560,14 @@ gtk_text_view_class_init (GtkTextViewClass *klass)
gtk_binding_entry_add_signal (binding_set, GDK_KEY_Insert, GDK_SHIFT_MASK,
"paste-clipboard", 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);
+
/* Overwrite */
gtk_binding_entry_add_signal (binding_set, GDK_KEY_Insert, 0,
"toggle-overwrite", 0);
@@ -9835,3 +9853,25 @@ gtk_text_view_get_extra_menu (GtkTextView *text_view)
return priv->extra_menu;
}
+
+static void
+gtk_text_view_real_undo (GtkWidget *widget,
+ const gchar *action_name,
+ GVariant *parameters)
+{
+ GtkTextView *text_view = GTK_TEXT_VIEW (widget);
+
+ if (gtk_text_view_get_editable (text_view))
+ gtk_text_buffer_undo (text_view->priv->buffer);
+}
+
+static void
+gtk_text_view_real_redo (GtkWidget *widget,
+ const gchar *action_name,
+ GVariant *parameters)
+{
+ GtkTextView *text_view = GTK_TEXT_VIEW (widget);
+
+ if (gtk_text_view_get_editable (text_view))
+ gtk_text_buffer_redo (text_view->priv->buffer);
+}
diff --git a/gtk/meson.build b/gtk/meson.build
index 3119e05934..65576f92c6 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -145,6 +145,7 @@ gtk_private_sources = files([
'gtkstylecascade.c',
'gtkstyleproperty.c',
'gtktextbtree.c',
+ 'gtktexthistory.c',
'gtktextviewchild.c',
'gtktrashmonitor.c',
'gtktreedatalist.c',
diff --git a/tests/meson.build b/tests/meson.build
index 176685fe50..7b28428102 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -126,6 +126,7 @@ gtk_tests = [
['testblur'],
['testtexture'],
['testwindowdrag'],
+ ['testtexthistory', ['../gtk/gtktexthistory.c']],
]
if os_unix
diff --git a/tests/testtexthistory.c b/tests/testtexthistory.c
new file mode 100644
index 0000000000..8332b4d8a1
--- /dev/null
+++ b/tests/testtexthistory.c
@@ -0,0 +1,600 @@
+#include "gtktexthistoryprivate.h"
+
+#if 0
+# define DEBUG_COMMANDS
+#endif
+
+typedef struct
+{
+ GtkTextHistory *history;
+ GString *buf;
+ struct {
+ int insert;
+ int bound;
+ } selection;
+ guint can_redo : 1;
+ guint can_undo : 1;
+ guint is_modified : 1;
+} Text;
+
+enum {
+ IGNORE = 0,
+ SET = 1,
+ UNSET = 2,
+};
+
+enum {
+ IGNORE_SELECT = 0,
+ DO_SELECT = 1,
+};
+
+enum {
+ INSERT = 1,
+ INSERT_SEQ,
+ BACKSPACE,
+ DELETE_KEY,
+ UNDO,
+ REDO,
+ BEGIN_IRREVERSIBLE,
+ END_IRREVERSIBLE,
+ BEGIN_USER,
+ END_USER,
+ MODIFIED,
+ UNMODIFIED,
+ SELECT,
+ CHECK_SELECT,
+ SET_MAX_UNDO,
+};
+
+typedef struct
+{
+ int kind;
+ int location;
+ int end_location;
+ const char *text;
+ const char *expected;
+ int can_undo;
+ int can_redo;
+ int is_modified;
+ int select;
+} Command;
+
+static void
+do_change_state (gpointer funcs_data,
+ gboolean is_modified,
+ gboolean can_undo,
+ gboolean can_redo)
+{
+ Text *text = funcs_data;
+
+ text->is_modified = is_modified;
+ text->can_undo = can_undo;
+ text->can_redo = can_redo;
+}
+
+static void
+do_insert (gpointer funcs_data,
+ guint begin,
+ guint end,
+ const char *text,
+ guint len)
+{
+ Text *t = funcs_data;
+
+#ifdef DEBUG_COMMANDS
+ g_printerr ("Insert Into '%s' (begin=%u end=%u text=%s)\n",
+ t->buf->str, begin, end, text);
+#endif
+
+ g_string_insert_len (t->buf, begin, text, len);
+}
+
+static void
+do_delete (gpointer funcs_data,
+ guint begin,
+ guint end,
+ const gchar *expected_text,
+ guint len)
+{
+ Text *t = funcs_data;
+
+#ifdef DEBUG_COMMANDS
+ g_printerr ("Delete(begin=%u end=%u expected_text=%s)\n", begin, end, expected_text);
+#endif
+
+ if (end < begin)
+ {
+ guint tmp = end;
+ end = begin;
+ begin = tmp;
+ }
+
+ g_assert_cmpint (memcmp (t->buf->str + begin, expected_text, len), ==, 0);
+
+ if (end >= t->buf->len)
+ {
+ t->buf->len = begin;
+ t->buf->str[begin] = 0;
+ return;
+ }
+
+ memmove (t->buf->str + begin,
+ t->buf->str + end,
+ t->buf->len - end);
+ g_string_truncate (t->buf, t->buf->len - (end - begin));
+}
+
+static void
+do_select (gpointer funcs_data,
+ gint selection_insert,
+ gint selection_bound)
+{
+ Text *text = funcs_data;
+
+ text->selection.insert = selection_insert;
+ text->selection.bound = selection_bound;
+}
+
+static GtkTextHistoryFuncs funcs = {
+ do_change_state,
+ do_insert,
+ do_delete,
+ do_select,
+};
+
+static Text *
+text_new (void)
+{
+ Text *text = g_slice_new0 (Text);
+
+ text->history = gtk_text_history_new (&funcs, text);
+ text->buf = g_string_new (NULL);
+ text->selection.insert = -1;
+ text->selection.bound = -1;
+
+ return text;
+}
+
+static void
+text_free (Text *text)
+{
+ g_object_unref (text->history);
+ g_string_free (text->buf, TRUE);
+ g_slice_free (Text, text);
+}
+
+static void
+command_insert (const Command *cmd,
+ Text *text)
+{
+ do_insert (text,
+ cmd->location,
+ cmd->location + g_utf8_strlen (cmd->text, -1),
+ cmd->text,
+ strlen (cmd->text));
+ gtk_text_history_text_inserted (text->history, cmd->location, cmd->text, -1);
+}
+
+static void
+command_delete_key (const Command *cmd,
+ Text *text)
+{
+ do_delete (text,
+ cmd->location,
+ cmd->end_location,
+ cmd->text,
+ strlen (cmd->text));
+ gtk_text_history_text_deleted (text->history,
+ cmd->location,
+ cmd->end_location,
+ cmd->text,
+ ABS (cmd->end_location - cmd->location));
+}
+
+static void
+command_undo (const Command *cmd,
+ Text *text)
+{
+ gtk_text_history_undo (text->history);
+}
+
+static void
+command_redo (const Command *cmd,
+ Text *text)
+{
+ gtk_text_history_redo (text->history);
+}
+
+static void
+set_selection (Text *text,
+ int begin,
+ int end)
+{
+ gtk_text_history_selection_changed (text->history, begin, end);
+}
+
+static void
+run_test (const Command *commands,
+ guint n_commands,
+ guint max_undo)
+{
+ Text *text = text_new ();
+
+ if (max_undo)
+ gtk_text_history_set_max_undo_levels (text->history, max_undo);
+
+ for (guint i = 0; i < n_commands; i++)
+ {
+ const Command *cmd = &commands[i];
+
+#ifdef DEBUG_COMMANDS
+ g_printerr ("%d: %d\n", i, cmd->kind);
+#endif
+
+ switch (cmd->kind)
+ {
+ case INSERT:
+ command_insert (cmd, text);
+ break;
+
+ case INSERT_SEQ:
+ for (guint j = 0; cmd->text[j]; j++)
+ {
+ const char seqtext[2] = { cmd->text[j], 0 };
+ Command seq = { INSERT, cmd->location + j, 1, seqtext, NULL };
+ command_insert (&seq, text);
+ }
+ break;
+
+ case DELETE_KEY:
+ if (cmd->select == DO_SELECT)
+ set_selection (text, cmd->location, cmd->end_location);
+ else if (strlen (cmd->text) == 1)
+ set_selection (text, cmd->location, -1);
+ else
+ set_selection (text, -1, -1);
+ command_delete_key (cmd, text);
+ break;
+
+ case BACKSPACE:
+ if (cmd->select == DO_SELECT)
+ set_selection (text, cmd->location, cmd->end_location);
+ else if (strlen (cmd->text) == 1)
+ set_selection (text, cmd->end_location, -1);
+ else
+ set_selection (text, -1, -1);
+ command_delete_key (cmd, text);
+ break;
+
+ case UNDO:
+ command_undo (cmd, text);
+ break;
+
+ case REDO:
+ command_redo (cmd, text);
+ break;
+
+ case BEGIN_USER:
+ gtk_text_history_begin_user_action (text->history);
+ break;
+
+ case END_USER:
+ gtk_text_history_end_user_action (text->history);
+ break;
+
+ case BEGIN_IRREVERSIBLE:
+ gtk_text_history_begin_irreversible_action (text->history);
+ break;
+
+ case END_IRREVERSIBLE:
+ gtk_text_history_end_irreversible_action (text->history);
+ break;
+
+ case MODIFIED:
+ gtk_text_history_modified_changed (text->history, TRUE);
+ break;
+
+ case UNMODIFIED:
+ gtk_text_history_modified_changed (text->history, FALSE);
+ break;
+
+ case SELECT:
+ gtk_text_history_selection_changed (text->history,
+ cmd->location,
+ cmd->end_location);
+ break;
+
+ case CHECK_SELECT:
+ g_assert_cmpint (text->selection.insert, ==, cmd->location);
+ g_assert_cmpint (text->selection.bound, ==, cmd->end_location);
+ break;
+
+ case SET_MAX_UNDO:
+ /* Not ideal use of location, but fine */
+ gtk_text_history_set_max_undo_levels (text->history, cmd->location);
+ break;
+
+ default:
+ break;
+ }
+
+ if (cmd->expected)
+ g_assert_cmpstr (text->buf->str, ==, cmd->expected);
+
+ if (cmd->can_redo == SET)
+ g_assert_cmpint (text->can_redo, ==, TRUE);
+ else if (cmd->can_redo == UNSET)
+ g_assert_cmpint (text->can_redo, ==, FALSE);
+
+ if (cmd->can_undo == SET)
+ g_assert_cmpint (text->can_undo, ==, TRUE);
+ else if (cmd->can_undo == UNSET)
+ g_assert_cmpint (text->can_undo, ==, FALSE);
+
+ if (cmd->is_modified == SET)
+ g_assert_cmpint (text->is_modified, ==, TRUE);
+ else if (cmd->is_modified == UNSET)
+ g_assert_cmpint (text->is_modified, ==, FALSE);
+ }
+
+ text_free (text);
+}
+
+static void
+test1 (void)
+{
+ static const Command commands[] = {
+ { INSERT, 0, -1, "test", "test", SET, UNSET },
+ { INSERT, 2, -1, "s", "tesst", SET, UNSET },
+ { INSERT, 3, -1, "ss", "tesssst", SET, UNSET },
+ { DELETE_KEY, 2, 5, "sss", "test", SET, UNSET },
+ { UNDO, -1, -1, NULL, "tesssst", SET, SET },
+ { REDO, -1, -1, NULL, "test", SET, UNSET },
+ { UNDO, -1, -1, NULL, "tesssst", SET, SET },
+ { DELETE_KEY, 0, 7, "tesssst", "", SET, UNSET },
+ { INSERT, 0, -1, "z", "z", SET, UNSET },
+ { UNDO, -1, -1, NULL, "", SET, SET },
+ { UNDO, -1, -1, NULL, "tesssst", SET, SET },
+ { UNDO, -1, -1, NULL, "test", SET, SET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test2 (void)
+{
+ static const Command commands[] = {
+ { BEGIN_IRREVERSIBLE, -1, -1, NULL, "", UNSET, UNSET },
+ { INSERT, 0, -1, "this is a test", "this is a test", UNSET, UNSET },
+ { END_IRREVERSIBLE, -1, -1, NULL, "this is a test", UNSET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a test", UNSET, UNSET },
+ { REDO, -1, -1, NULL, "this is a test", UNSET, UNSET },
+ { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+ { INSERT, 0, -1, "first", "firstthis is a test", UNSET, UNSET },
+ { INSERT, 5, -1, " ", "first this is a test", UNSET, UNSET },
+ { END_USER, -1, -1, NULL, "first this is a test", SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
+ { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
+ { REDO, -1, -1, NULL, "first this is a test", SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a test", UNSET, SET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test3 (void)
+{
+ static const Command commands[] = {
+ { INSERT_SEQ, 0, -1, "this is a test of insertions.", "this is a test of insertions.", SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a test of", SET, SET },
+ { UNDO, -1, -1, NULL, "this is a test", SET, SET },
+ { UNDO, -1, -1, NULL, "this is a", SET, SET },
+ { UNDO, -1, -1, NULL, "this is", SET, SET },
+ { UNDO, -1, -1, NULL, "this", SET, SET },
+ { UNDO, -1, -1, NULL, "", UNSET, SET },
+ { UNDO, -1, -1, NULL, "" , UNSET, SET },
+ { REDO, -1, -1, NULL, "this", SET, SET },
+ { REDO, -1, -1, NULL, "this is", SET, SET },
+ { REDO, -1, -1, NULL, "this is a", SET, SET },
+ { REDO, -1, -1, NULL, "this is a test", SET, SET },
+ { REDO, -1, -1, NULL, "this is a test of", SET, SET },
+ { REDO, -1, -1, NULL, "this is a test of insertions.", SET, UNSET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test4 (void)
+{
+ static const Command commands[] = {
+ { INSERT, 0, -1, "initial text", "initial text", SET, UNSET },
+ /* Barrier */
+ { BEGIN_IRREVERSIBLE, -1, -1, NULL, NULL, UNSET, UNSET },
+ { END_IRREVERSIBLE, -1, -1, NULL, NULL, UNSET, UNSET },
+ { INSERT, 0, -1, "more text ", "more text initial text", SET, UNSET },
+ { UNDO, -1, -1, NULL, "initial text", UNSET, SET },
+ { UNDO, -1, -1, NULL, "initial text", UNSET, SET },
+ { REDO, -1, -1, NULL, "more text initial text", SET, UNSET },
+ /* Barrier */
+ { BEGIN_IRREVERSIBLE, UNSET, UNSET },
+ { END_IRREVERSIBLE, UNSET, UNSET },
+ { UNDO, -1, -1, NULL, "more text initial text", UNSET, UNSET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test5 (void)
+{
+ static const Command commands[] = {
+ { INSERT, 0, -1, "initial text", "initial text", SET, UNSET },
+ { DELETE_KEY, 0, 12, "initial text", "", SET, UNSET },
+ /* Add empty nested user action (should get ignored) */
+ { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+ { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+ { BEGIN_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+ { END_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+ { END_USER, -1, -1, NULL, NULL, UNSET, UNSET },
+ { END_USER, -1, -1, NULL, NULL, SET, UNSET },
+ { UNDO, -1, -1, NULL, "initial text" },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test6 (void)
+{
+ static const Command commands[] = {
+ { INSERT_SEQ, 0, -1, " \t\t this is some text", " \t\t this is some text", SET, UNSET },
+ { UNDO, -1, -1, NULL, " \t\t this is some", SET, SET },
+ { UNDO, -1, -1, NULL, " \t\t this is", SET, SET },
+ { UNDO, -1, -1, NULL, " \t\t this", SET, SET },
+ { UNDO, -1, -1, NULL, "", UNSET, SET },
+ { UNDO, -1, -1, NULL, "", UNSET, SET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test7 (void)
+{
+ static const Command commands[] = {
+ { MODIFIED, -1, -1, NULL, NULL, UNSET, UNSET, SET },
+ { UNMODIFIED, -1, -1, NULL, NULL, UNSET, UNSET, UNSET },
+ { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
+ { MODIFIED, -1, -1, NULL, NULL, SET, UNSET, SET },
+ { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
+ { REDO, -1, -1, NULL, "foo bar", SET, UNSET, SET },
+ { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
+ { REDO, -1, -1, NULL, "foo bar", SET, UNSET, SET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test8 (void)
+{
+ static const Command commands[] = {
+ { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
+ { MODIFIED, -1, -1, NULL, NULL, SET, UNSET, SET },
+ { INSERT, 0, -1, "f", "ffoo bar", SET, UNSET, SET },
+ { UNMODIFIED, -1, -1, NULL, NULL, SET, UNSET, UNSET },
+ { UNDO, -1, -1, NULL, "foo bar", SET, SET, SET },
+ { UNDO, -1, -1, NULL, "", UNSET, SET, SET },
+ { REDO, -1, -1, NULL, "foo bar", SET, SET, SET },
+ { REDO, -1, -1, NULL, "ffoo bar", SET, UNSET, UNSET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test9 (void)
+{
+ static const Command commands[] = {
+ { INSERT, 0, -1, "foo bar", "foo bar", SET, UNSET, UNSET },
+ { DELETE_KEY, 0, 3, "foo", " bar", SET, UNSET, UNSET, DO_SELECT },
+ { DELETE_KEY, 0, 4, " bar", "", SET, UNSET, UNSET, DO_SELECT },
+ { UNDO, -1, -1, NULL, " bar", SET, SET, UNSET },
+ { CHECK_SELECT, 0, 4, NULL, " bar", SET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "foo bar", SET, SET, UNSET },
+ { CHECK_SELECT, 0, 3, NULL, "foo bar", SET, SET, UNSET },
+ { BEGIN_IRREVERSIBLE, -1, -1, NULL, "foo bar", UNSET, UNSET, UNSET },
+ { END_IRREVERSIBLE, -1, -1, NULL, "foo bar", UNSET, UNSET, UNSET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test10 (void)
+{
+ static const Command commands[] = {
+ { BEGIN_USER }, { INSERT, 0, -1, "t", "t", UNSET, UNSET, UNSET }, { END_USER },
+ { BEGIN_USER }, { INSERT, 1, -1, " ", "t ", UNSET, UNSET, UNSET }, { END_USER },
+ { BEGIN_USER }, { INSERT, 2, -1, "t", "t t", UNSET, UNSET, UNSET }, { END_USER },
+ { BEGIN_USER }, { INSERT, 3, -1, "h", "t th", UNSET, UNSET, UNSET }, { END_USER },
+ { BEGIN_USER }, { INSERT, 4, -1, "i", "t thi", UNSET, UNSET, UNSET }, { END_USER },
+ { BEGIN_USER }, { INSERT, 5, -1, "s", "t this", UNSET, UNSET, UNSET }, { END_USER },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test11 (void)
+{
+ /* Test backspace */
+ static const Command commands[] = {
+ { INSERT_SEQ, 0, -1, "insert some text", "insert some text", SET, UNSET, UNSET },
+ { BACKSPACE, 15, 16, "t", "insert some tex", SET, UNSET, UNSET },
+ { BACKSPACE, 14, 15, "x", "insert some te", SET, UNSET, UNSET },
+ { BACKSPACE, 13, 14, "e", "insert some t", SET, UNSET, UNSET },
+ { BACKSPACE, 12, 13, "t", "insert some ", SET, UNSET, UNSET },
+ { UNDO, -1, -1, NULL, "insert some text", SET, SET, UNSET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test12 (void)
+{
+ static const Command commands[] = {
+ { INSERT_SEQ, 0, -1, "this is a test\nmore", "this is a test\nmore", SET, UNSET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a test\n", SET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a", SET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is", SET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "this", SET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "", UNSET, SET, UNSET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 0);
+}
+
+static void
+test13 (void)
+{
+ static const Command commands[] = {
+ { INSERT_SEQ, 0, -1, "this is a test\nmore", "this is a test\nmore", SET, UNSET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a test\n", SET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a", UNSET, SET, UNSET },
+ { UNDO, -1, -1, NULL, "this is a", UNSET, SET, UNSET },
+ { SET_MAX_UNDO, 2, -1, NULL, "this is a", UNSET, SET, UNSET },
+ { REDO, -1, -1, NULL, "this is a test", SET, SET, UNSET },
+ { REDO, -1, -1, NULL, "this is a test\n", SET, UNSET, UNSET },
+ { REDO, -1, -1, NULL, "this is a test\n", SET, UNSET, UNSET },
+ };
+
+ run_test (commands, G_N_ELEMENTS (commands), 3);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ g_test_init (&argc, &argv, NULL);
+ g_test_add_func ("/Gtk/TextHistory/test1", test1);
+ g_test_add_func ("/Gtk/TextHistory/test2", test2);
+ g_test_add_func ("/Gtk/TextHistory/test3", test3);
+ g_test_add_func ("/Gtk/TextHistory/test4", test4);
+ g_test_add_func ("/Gtk/TextHistory/test5", test5);
+ g_test_add_func ("/Gtk/TextHistory/test6", test6);
+ g_test_add_func ("/Gtk/TextHistory/test7", test7);
+ g_test_add_func ("/Gtk/TextHistory/test8", test8);
+ g_test_add_func ("/Gtk/TextHistory/test9", test9);
+ g_test_add_func ("/Gtk/TextHistory/test10", test10);
+ g_test_add_func ("/Gtk/TextHistory/test11", test11);
+ g_test_add_func ("/Gtk/TextHistory/test12", test12);
+ g_test_add_func ("/Gtk/TextHistory/test13", test13);
+ return g_test_run ();
+}
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 },