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 },