[gtk/wip/chergert/textundo: 16/21] text: add undo support to GtkText
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gtk/wip/chergert/textundo: 16/21] text: add undo support to GtkText
- Date: Fri, 1 Nov 2019 18:39:48 +0000 (UTC)
commit 3df14a644546ba187fc831ea5b0d930ef172f487
Author: Christian Hergert <chergert redhat com>
Date: Fri Nov 1 11:13:30 2019 -0700
text: add undo support to GtkText
This adds support using the GtkTextHistory helper for undo/redo to the
GtkText widget. It is similar in use to GtkTextView, but with a simplified
interface.
You can disable undo support using the GtkText:enable-undo property. By
default, it is enabled.
docs/reference/gtk/gtk4-sections.txt | 4 +
gtk/gtktext.c | 293 +++++++++++++++++++++++++++++++++++
gtk/gtktext.h | 12 ++
3 files changed, 309 insertions(+)
---
diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt
index 1781176a9e..97954788d3 100644
--- a/docs/reference/gtk/gtk4-sections.txt
+++ b/docs/reference/gtk/gtk4-sections.txt
@@ -931,6 +931,10 @@ gtk_text_get_tabs
gtk_text_grab_focus_without_selecting
gtk_text_set_extra_menu
gtk_text_get_extra_menu
+gtk_text_get_enable_undo
+gtk_text_set_enable_undo
+gtk_text_get_max_undo_levels
+gtk_text_set_max_undo_levels
<SUBSECTION Private>
gtk_text_get_type
</SECTION>
diff --git a/gtk/gtktext.c b/gtk/gtktext.c
index 698b1421eb..a4e89f7c08 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 */
@@ -243,6 +248,8 @@ enum {
TOGGLE_OVERWRITE,
PREEDIT_CHANGED,
INSERT_EMOJI,
+ UNDO,
+ REDO,
LAST_SIGNAL
};
@@ -266,6 +273,7 @@ enum {
PROP_ENABLE_EMOJI_COMPLETION,
PROP_PROPAGATE_TEXT_WIDTH,
PROP_EXTRA_MENU,
+ PROP_ENABLE_UNDO,
NUM_PROPERTIES
};
@@ -559,6 +567,25 @@ 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 (GtkText *text);
+static void gtk_text_real_redo (GtkText *text);
+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 +672,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))
@@ -719,6 +753,8 @@ gtk_text_class_init (GtkTextClass *class)
class->toggle_overwrite = gtk_text_toggle_overwrite;
class->insert_emoji = gtk_text_insert_emoji;
class->activate = gtk_text_real_activate;
+ class->undo = gtk_text_real_undo;
+ class->redo = gtk_text_real_redo;
quark_password_hint = g_quark_from_static_string ("gtk-entry-password-hint");
@@ -922,6 +958,19 @@ gtk_text_class_init (GtkTextClass *class)
G_TYPE_MENU_MODEL,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+ /**
+ * GtkText:enable-undo:
+ *
+ * Setting this to %TRUE allows the user to use keyboard shortcuts to
+ * undo or redo actions on the #GtkText.
+ */
+ text_props[PROP_ENABLE_UNDO] =
+ g_param_spec_boolean ("enable-undo",
+ "Enable Undo",
+ "Enable support for undo and redo in the text",
+ TRUE,
+ GTK_PARAM_READWRITE);
+
g_object_class_install_properties (gobject_class, NUM_PROPERTIES, text_props);
gtk_editable_install_properties (gobject_class, NUM_PROPERTIES);
@@ -1173,6 +1222,40 @@ gtk_text_class_init (GtkTextClass *class)
NULL,
G_TYPE_NONE, 0);
+ /**
+ * GtkText::undo:
+ * @self: the object which received the signal
+ *
+ * The ::undo signal is a
+ * [keybinding signal][GtkBindingSignal]
+ * which gets emitted to undo the last operation.
+ */
+ signals[UNDO] =
+ g_signal_new (I_("undo"),
+ G_OBJECT_CLASS_TYPE (gobject_class),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (GtkTextClass, undo),
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE, 0);
+
+ /**
+ * GtkText::redo:
+ * @self: the object which received the signal
+ *
+ * The ::redo signal is a
+ * [keybinding signal][GtkBindingSignal]
+ * which gets emitted to redo the last undone operation.
+ */
+ signals[REDO] =
+ g_signal_new (I_("redo"),
+ G_OBJECT_CLASS_TYPE (gobject_class),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (GtkTextClass, redo),
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE, 0);
+
/*
* Key bindings
*/
@@ -1346,6 +1429,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_signal (binding_set, GDK_KEY_z, GDK_CONTROL_MASK,
+ "undo", 0);
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_y, GDK_CONTROL_MASK,
+ "redo", 0);
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
+ "redo", 0);
+
gtk_widget_class_set_accessible_type (widget_class, GTK_TYPE_TEXT_ACCESSIBLE);
gtk_widget_class_set_css_name (widget_class, I_("text"));
@@ -1532,6 +1623,10 @@ gtk_text_set_property (GObject *object,
gtk_text_set_extra_menu (self, g_value_get_object (value));
break;
+ case PROP_ENABLE_UNDO:
+ gtk_text_set_enable_undo (self, g_value_get_boolean (value));
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -1651,6 +1746,10 @@ gtk_text_get_property (GObject *object,
g_value_set_object (value, priv->extra_menu);
break;
+ case PROP_ENABLE_UNDO:
+ g_value_set_boolean (value, gtk_text_get_enable_undo (self));
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -1678,6 +1777,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 +1914,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 +3447,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 +3486,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 +3569,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 +3579,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 +5372,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 +5384,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 +5393,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 +5434,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 +6959,152 @@ gtk_text_get_extra_menu (GtkText *self)
return priv->extra_menu;
}
+
+static void
+gtk_text_real_undo (GtkText *text)
+{
+ GtkTextPrivate *priv = gtk_text_get_instance_private (text);
+
+ g_return_if_fail (GTK_IS_TEXT (text));
+
+ gtk_text_history_undo (priv->history);
+}
+
+static void
+gtk_text_real_redo (GtkText *text)
+{
+ GtkTextPrivate *priv = gtk_text_get_instance_private (text);
+
+ g_return_if_fail (GTK_IS_TEXT (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);
+}
+
+/**
+ * gtk_text_get_enable_undo:
+ * @self: a #GtkText
+ *
+ * Gets whether the #GtkText supports undo or redo actions.
+ *
+ * If enabled, the #GtkText will save changes and allow the user to undo
+ * or redo them using keyboard shortcuts.
+ */
+gboolean
+gtk_text_get_enable_undo (GtkText *self)
+{
+ GtkTextPrivate *priv = gtk_text_get_instance_private (self);
+
+ g_return_val_if_fail (GTK_IS_TEXT (self), FALSE);
+
+ return gtk_text_history_get_enabled (priv->history);
+}
+
+/**
+ * gtk_text_set_enable_undo:
+ * @text: a #GtkText
+ * @enabled: if undo is enabled
+ *
+ * Sets the :enable-undo property.
+ *
+ * If set to %TRUE, the user will be able to undo and redo actions on
+ * the #GtkText.
+ *
+ * Setting the #GtkEditable:text property will create an irreversible action,
+ * clearing any previously saved undo actions.
+ */
+void
+gtk_text_set_enable_undo (GtkText *self,
+ gboolean enabled)
+{
+ GtkTextPrivate *priv = gtk_text_get_instance_private (self);
+
+ g_return_if_fail (GTK_IS_TEXT (self));
+
+ if (enabled != gtk_text_history_get_enabled (priv->history))
+ {
+ gtk_text_history_set_enabled (priv->history, enabled);
+ g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_ENABLE_UNDO]);
+ }
+}
+
+/**
+ * gtk_text_get_max_undo_levels:
+ * @self: a #GtkText
+ *
+ * Gets the maximum number of undo actions to store while editing
+ * within the #GtkText.
+ */
+guint
+gtk_text_get_max_undo_levels (GtkText *self)
+{
+ GtkTextPrivate *priv = gtk_text_get_instance_private (self);
+
+ g_return_val_if_fail (GTK_IS_TEXT (self), 0);
+
+ return gtk_text_history_get_max_undo_levels (priv->history);
+}
+
+/**
+ * gtk_text_set_max_undo_levels:
+ * @self: a #GtkText
+ * @max_undo_levels: maximum number of undo actions
+ *
+ * Sets the maximum number of undo actions that can be performed on the
+ * #GtkText. Setting this to 0 results in unlimited undo operations.
+ */
+void
+gtk_text_set_max_undo_levels (GtkText *self,
+ guint max_undo_levels)
+{
+ GtkTextPrivate *priv = gtk_text_get_instance_private (self);
+
+ g_return_if_fail (GTK_IS_TEXT (self));
+
+ gtk_text_history_set_max_undo_levels (priv->history, max_undo_levels);
+}
diff --git a/gtk/gtktext.h b/gtk/gtktext.h
index 4ce0b781bb..2470301478 100644
--- a/gtk/gtktext.h
+++ b/gtk/gtktext.h
@@ -140,6 +140,18 @@ void gtk_text_set_extra_menu (GtkText *self,
GDK_AVAILABLE_IN_ALL
GMenuModel * gtk_text_get_extra_menu (GtkText *self);
+GDK_AVAILABLE_IN_ALL
+gboolean gtk_text_get_enable_undo (GtkText *self);
+
+GDK_AVAILABLE_IN_ALL
+void gtk_text_set_enable_undo (GtkText *self,
+ gboolean enable_undo);
+GDK_AVAILABLE_IN_ALL
+guint gtk_text_get_max_undo_levels (GtkText *self);
+GDK_AVAILABLE_IN_ALL
+void gtk_text_set_max_undo_levels (GtkText *self,
+ guint max_undo_levels);
+
G_END_DECLS
#endif /* __GTK_TEXT_H__ */
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]