diff --git a/gtk/gtkenums.h b/gtk/gtkenums.h index eed63be988..e0ee0e8845 100644 --- a/gtk/gtkenums.h +++ b/gtk/gtkenums.h @@ -1880,4 +1880,27 @@ typedef enum { GTK_FONT_RENDERING_MANUAL, } GtkFontRendering; +/** + * GtkTextBufferNotifyFlags: + * @GTK_TEXT_BUFFER_NOTIFY_BEFORE_INSERT: Be notified before text + * is inserted into the underlying buffer. + * @GTK_TEXT_BUFFER_NOTIFY_AFTER_INSERT: Be notified after text + * has been inserted into the underlying buffer. + * @GTK_TEXT_BUFFER_NOTIFY_BEFORE_DELETE: Be notified before text + * is deleted from the underlying buffer. + * @GTK_TEXT_BUFFER_NOTIFY_AFTER_DELETE: Be notified after text + * has been deleted from the underlying buffer. + * + * Values for [callback@Gtk.TextBufferCommitNotify] to denote the + * point of the notification. + * + * Since: 4.16 + */ +typedef enum { + GTK_TEXT_BUFFER_NOTIFY_BEFORE_INSERT = 1 << 0, + GTK_TEXT_BUFFER_NOTIFY_AFTER_INSERT = 1 << 1, + GTK_TEXT_BUFFER_NOTIFY_BEFORE_DELETE = 1 << 2, + GTK_TEXT_BUFFER_NOTIFY_AFTER_DELETE = 1 << 3, +} GtkTextBufferNotifyFlags; + G_END_DECLS diff --git a/gtk/gtktextbuffer.c b/gtk/gtktextbuffer.c index 16f8e7b953..7a62c36a41 100644 --- a/gtk/gtktextbuffer.c +++ b/gtk/gtktextbuffer.c @@ -68,6 +68,9 @@ struct _GtkTextBufferPrivate GtkTextHistory *history; + GArray *commit_funcs; + guint last_commit_handler; + guint user_action_count; /* Whether the buffer has been modified since last save */ @@ -75,6 +78,7 @@ struct _GtkTextBufferPrivate guint has_selection : 1; guint can_undo : 1; guint can_redo : 1; + guint in_commit_notify : 1; }; typedef struct _ClipboardRequest ClipboardRequest; @@ -87,6 +91,15 @@ struct _ClipboardRequest guint replace_selection : 1; }; +typedef struct _CommitFunc +{ + GtkTextBufferCommitNotify callback; + gpointer user_data; + GDestroyNotify user_data_destroy; + GtkTextBufferNotifyFlags flags; + guint handler_id; +} CommitFunc; + enum { INSERT_TEXT, INSERT_PAINTABLE, @@ -151,6 +164,10 @@ static void gtk_text_buffer_real_mark_set (GtkTextBuffer *buffe GtkTextMark *mark); static void gtk_text_buffer_real_undo (GtkTextBuffer *buffer); static void gtk_text_buffer_real_redo (GtkTextBuffer *buffer); +static void gtk_text_buffer_commit_notify (GtkTextBuffer *buffer, + GtkTextBufferNotifyFlags flags, + guint position, + guint length); static GtkTextBTree* get_btree (GtkTextBuffer *buffer); static void free_log_attr_cache (GtkTextLogAttrCache *cache); @@ -1081,6 +1098,8 @@ gtk_text_buffer_finalize (GObject *object) remove_all_selection_clipboards (buffer); + g_clear_pointer (&buffer->priv->commit_funcs, g_array_unref); + g_clear_object (&buffer->priv->history); if (priv->tag_table) @@ -1198,7 +1217,29 @@ gtk_text_buffer_real_insert_text (GtkTextBuffer *buffer, text, len); - _gtk_text_btree_insert (iter, text, len); + if (buffer->priv->commit_funcs == NULL) + { + _gtk_text_btree_insert (iter, text, len); + } + else + { + guint position; + guint n_chars; + + if (len < 0) + len = strlen (text); + + position = gtk_text_iter_get_offset (iter); + n_chars = g_utf8_strlen (text, len); + + gtk_text_buffer_commit_notify (buffer, + GTK_TEXT_BUFFER_NOTIFY_BEFORE_INSERT, + position, n_chars); + _gtk_text_btree_insert (iter, text, len); + gtk_text_buffer_commit_notify (buffer, + GTK_TEXT_BUFFER_NOTIFY_AFTER_INSERT, + position, n_chars); + } g_signal_emit (buffer, signals[CHANGED], 0); g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props[PROP_CURSOR_POSITION]); @@ -1211,6 +1252,7 @@ gtk_text_buffer_emit_insert (GtkTextBuffer *buffer, int len) { g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer)); + g_return_if_fail (buffer->priv->in_commit_notify == FALSE); g_return_if_fail (iter != NULL); g_return_if_fail (text != NULL); @@ -1955,7 +1997,36 @@ gtk_text_buffer_real_delete_range (GtkTextBuffer *buffer, g_free (text); } - _gtk_text_btree_delete (start, end); + + + if (buffer->priv->commit_funcs == NULL) + { + _gtk_text_btree_delete (start, end); + } + else + { + guint off1 = gtk_text_iter_get_offset (start); + guint off2 = gtk_text_iter_get_offset (end); + + if (off2 < off1) + { + guint tmp = off1; + off1 = off2; + off2 = tmp; + } + + buffer->priv->in_commit_notify = TRUE; + + gtk_text_buffer_commit_notify (buffer, + GTK_TEXT_BUFFER_NOTIFY_BEFORE_DELETE, + off1, off2 - off1); + _gtk_text_btree_delete (start, end); + gtk_text_buffer_commit_notify (buffer, + GTK_TEXT_BUFFER_NOTIFY_AFTER_DELETE, + off1, 0); + + buffer->priv->in_commit_notify = FALSE; + } /* may have deleted the selection... */ update_selection_clipboards (buffer); @@ -1979,6 +2050,7 @@ gtk_text_buffer_emit_delete (GtkTextBuffer *buffer, g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer)); g_return_if_fail (start != NULL); g_return_if_fail (end != NULL); + g_return_if_fail (buffer->priv->in_commit_notify == FALSE); if (gtk_text_iter_equal (start, end)) return; @@ -5720,3 +5792,130 @@ gtk_text_buffer_add_run_attributes (GtkTextBuffer *buffer, g_slist_free (tags); } + +static void +clear_commit_func (gpointer data) +{ + CommitFunc *func = data; + + if (func->user_data_destroy) + func->user_data_destroy (func->user_data); +} + +/** + * gtk_text_buffer_add_commit_notify: + * @buffer: a [type@Gtk.TextBuffer] + * @flags: which notifications should be dispatched to @callback + * @commit_notify: (scope async) (closure user_data) (destroy destroy): a + * [callback@Gtk.TextBufferCommitNotify] to call for commit notifications + * @user_data: closure data for @commit_notify + * @destroy: a callback to free @user_data when @commit_notify is removed + * + * Adds a [callback@Gtk.TextBufferCommitNotify] to be called when a change + * is to be made to the [type@Gtk.TextBuffer]. + * + * Functions are explicitly forbidden from making changes to the + * [type@Gtk.TextBuffer] from this callback. It is intended for tracking + * changes to the buffer only. + * + * It may be advantageous to use [callback@Gtk.TextBufferCommitNotify] over + * connecting to the [signal@Gtk.TextBuffer::insert-text] or + * [signal@Gtk.TextBuffer::delete-range] signals to avoid ordering issues with + * other signal handlers which may further modify the [type@Gtk.TextBuffer]. + * + * Returns: a handler id which may be used to remove the commit notify + * callback using [method@Gtk.TextBuffer.remove_commit_notify]. + * + * Since: 4.16 + */ +guint +gtk_text_buffer_add_commit_notify (GtkTextBuffer *buffer, + GtkTextBufferNotifyFlags flags, + GtkTextBufferCommitNotify commit_notify, + gpointer user_data, + GDestroyNotify destroy) +{ + CommitFunc func; + + g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), 0); + g_return_val_if_fail (buffer->priv->in_commit_notify == FALSE, 0); + + func.callback = commit_notify; + func.user_data = user_data; + func.user_data_destroy = destroy; + func.handler_id = ++buffer->priv->last_commit_handler; + func.flags = flags; + + if (buffer->priv->commit_funcs == NULL) + { + buffer->priv->commit_funcs = g_array_new (FALSE, FALSE, sizeof (CommitFunc)); + g_array_set_clear_func (buffer->priv->commit_funcs, clear_commit_func); + } + + g_array_append_val (buffer->priv->commit_funcs, func); + + return func.handler_id; +} + +/** + * gtk_text_buffer_remove_commit_notify: + * @buffer: a `GtkTextBuffer` + * @commit_notify_handler: the notify handler identifier returned from + * [method@Gtk.TextBuffer.add_commit_notify]. + * + * Removes the `GtkTextBufferCommitNotify` handler previously registered + * with [method@Gtk.TextBuffer.add_commit_notify]. + * + * This may result in the `user_data_destroy` being called that was passed when registering + * the commit notify functions. + * + * Since: 4.16 + */ +void +gtk_text_buffer_remove_commit_notify (GtkTextBuffer *buffer, + guint commit_notify_handler) +{ + g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer)); + g_return_if_fail (commit_notify_handler > 0); + g_return_if_fail (buffer->priv->in_commit_notify == FALSE); + + if (buffer->priv->commit_funcs != NULL) + { + for (guint i = 0; i < buffer->priv->commit_funcs->len; i++) + { + const CommitFunc *func = &g_array_index (buffer->priv->commit_funcs, CommitFunc, i); + + if (func->handler_id == commit_notify_handler) + { + g_array_remove_index (buffer->priv->commit_funcs, i); + + if (buffer->priv->commit_funcs->len == 0) + g_clear_pointer (&buffer->priv->commit_funcs, g_array_unref); + + return; + } + } + } + + g_warning ("No such GtkTextBufferCommitNotify matching %u", + commit_notify_handler); +} + +static void +gtk_text_buffer_commit_notify (GtkTextBuffer *buffer, + GtkTextBufferNotifyFlags flags, + guint position, + guint length) +{ + buffer->priv->in_commit_notify = TRUE; + + for (guint i = 0; i < buffer->priv->commit_funcs->len; i++) + { + const CommitFunc *func = &g_array_index (buffer->priv->commit_funcs, CommitFunc, i); + + if (func->flags & flags) + func->callback (buffer, flags, position, length, func->user_data); + } + + buffer->priv->in_commit_notify = FALSE; +} diff --git a/gtk/gtktextbuffer.h b/gtk/gtktextbuffer.h index f6d2613f4d..2c07b4bc97 100644 --- a/gtk/gtktextbuffer.h +++ b/gtk/gtktextbuffer.h @@ -53,6 +53,55 @@ struct _GtkTextBuffer GtkTextBufferPrivate *priv; }; +/** + * GtkTextBufferCommitNotify: + * @buffer: the text buffer being notified + * @flags: the type of commit notification + * @position: the position of the text operation + * @length: the length of the text operation in characters + * @user_data: (closure): user data passed to the callback + * + * A notification callback used by [method@Gtk.TextBuffer.add_commit_notify]. + * + * You may not modify the [class@Gtk.TextBuffer] from a + * [callback@Gtk.TextBufferCommitNotify] callback and that is enforced + * by the [class@Gtk.TextBuffer] API. + * + * [callback@Gtk.TextBufferCommitNotify] may be used to be notified about + * changes to the underlying buffer right before-or-after the changes are + * committed to the underlying B-Tree. This is useful if you want to observe + * changes to the buffer without other signal handlers potentially modifying + * state on the way to the default signal handler. + * + * When @flags is `GTK_TEXT_BUFFER_NOTIFY_BEFORE_INSERT`, `position` is set to + * the offset in characters from the start of the buffer where the insertion + * will occur. `length` is set to the number of characters to be inserted. You + * may not yet retrieve the text until it has been inserted. You may access the + * text from `GTK_TEXT_BUFFER_NOTIFY_AFTER_INSERT` using + * [method@Gtk.TextBuffer.get_slice]. + * + * When @flags is `GTK_TEXT_BUFFER_NOTIFY_AFTER_INSERT`, `position` is set to + * offset in characters where the insertion occurred and `length` is set + * to the number of characters inserted. + * + * When @flags is `GTK_TEXT_BUFFER_NOTIFY_BEFORE_DELETE`, `position` is set to + * offset in characters where the deletion will occur and `length` is set + * to the number of characters that will be removed. You may still retrieve + * the text from this handler using `position` and `length`. + * + * When @flags is `GTK_TEXT_BUFFER_NOTIFY_AFTER_DELETE`, `length` is set to + * zero to denote that the delete-range has already been committed to the + * underlying B-Tree. You may no longer retrieve the text that has been + * deleted from the [class@Gtk.TextBuffer]. + * + * Since: 4.16 + */ +typedef void (*GtkTextBufferCommitNotify) (GtkTextBuffer *buffer, + GtkTextBufferNotifyFlags flags, + guint position, + guint length, + gpointer user_data); + /** * GtkTextBufferClass: * @parent_class: The object class structure needs to be the first. @@ -459,6 +508,15 @@ 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); +GDK_AVAILABLE_IN_4_16 +guint gtk_text_buffer_add_commit_notify (GtkTextBuffer *buffer, + GtkTextBufferNotifyFlags flags, + GtkTextBufferCommitNotify commit_notify, + gpointer user_data, + GDestroyNotify destroy); +GDK_AVAILABLE_IN_4_16 +void gtk_text_buffer_remove_commit_notify (GtkTextBuffer *buffer, + guint commit_notify_handler); G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkTextBuffer, g_object_unref)