atspi: Make text change notification work

Make text change notification work for editables, by connecting
to the ::insert-text and ::delete-text signals on the wrapped
GtkText widget, and for GtkTextView by connecting to the
corresponding GtkTextBuffer signals.

This code is more or less directly copied from GtkTextViewAccessible
and GtkEntryAccessible in GTK 3.
This commit is contained in:
Matthias Clasen
2020-10-13 00:50:19 -04:00
parent a02c50e72f
commit bf0f3a82cf
3 changed files with 379 additions and 10 deletions

View File

@@ -39,6 +39,7 @@
#include "gtkdebug.h"
#include "gtkeditable.h"
#include "gtkentryprivate.h"
#include "gtkroot.h"
#include "gtktextview.h"
#include "gtkwindow.h"
@@ -670,6 +671,38 @@ gtk_at_spi_context_unregister_object (GtkAtSpiContext *self)
}
}
static void
emit_text_changed (GtkAtSpiContext *self,
const char *kind,
int start,
int end,
const char *text)
{
g_dbus_connection_emit_signal (self->connection,
NULL,
self->context_path,
"org.a11y.atspi.Event.Object",
"TextChanged",
g_variant_new ("(siiva{sv})",
kind, start, end, g_variant_new_string (text), NULL),
NULL);
}
static void
emit_selection_changed (GtkAtSpiContext *self,
const char *kind,
int cursor_position)
{
g_dbus_connection_emit_signal (self->connection,
NULL,
self->context_path,
"org.a11y.atspi.Event.Object",
"TextChanged",
g_variant_new ("(siiva{sv})",
kind, cursor_position, 0, g_variant_new_string (""), NULL),
NULL);
}
static void
emit_state_changed (GtkAtSpiContext *self,
const char *name,
@@ -874,12 +907,45 @@ gtk_at_spi_context_state_change (GtkATContext *ctx,
}
}
static void
insert_text_cb (GtkEditable *editable,
char *new_text,
int new_text_length,
int *position,
GtkAtSpiContext *self)
{
int length;
if (new_text_length == 0)
return;
length = g_utf8_strlen (new_text, new_text_length);
emit_text_changed (self, "insert", *position - length, length, new_text);
}
static void
delete_text_cb (GtkEditable *editable,
int start,
int end,
GtkAtSpiContext *self)
{
char *text;
if (start == end)
return;
text = gtk_editable_get_chars (editable, start, end);
emit_text_changed (self, "delete", start, end - start, text);
}
static void
gtk_at_spi_context_dispose (GObject *gobject)
{
GtkAtSpiContext *self = GTK_AT_SPI_CONTEXT (gobject);
GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self));
gtk_at_spi_context_unregister_object (self);
gtk_atspi_disconnect_text_signals (GTK_WIDGET (accessible));
G_OBJECT_CLASS (gtk_at_spi_context_parent_class)->dispose (gobject);
}
@@ -1002,6 +1068,11 @@ gtk_at_spi_context_constructed (GObject *gobject)
g_free (base_path);
g_free (uuid);
GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self));
gtk_atspi_connect_text_signals (GTK_WIDGET (accessible),
emit_text_changed,
emit_selection_changed,
self);
gtk_at_spi_context_register_object (self);
G_OBJECT_CLASS (gtk_at_spi_context_parent_class)->constructed (gobject);

View File

@@ -404,6 +404,20 @@ static const GDBusInterfaceVTable label_vtable = {
NULL,
};
static GtkText *
gtk_editable_get_text_widget (GtkWidget *widget)
{
if (GTK_IS_ENTRY (widget))
return gtk_entry_get_text_widget (GTK_ENTRY (widget));
else if (GTK_IS_SEARCH_ENTRY (widget))
return gtk_search_entry_get_text_widget (GTK_SEARCH_ENTRY (widget));
else if (GTK_IS_PASSWORD_ENTRY (widget))
return gtk_password_entry_get_text_widget (GTK_PASSWORD_ENTRY (widget));
else if (GTK_IS_SPIN_BUTTON (widget))
return gtk_spin_button_get_text_widget (GTK_SPIN_BUTTON (widget));
return NULL;
}
static void
entry_handle_method (GDBusConnection *connection,
@@ -418,16 +432,7 @@ entry_handle_method (GDBusConnection *connection,
GtkATContext *self = user_data;
GtkAccessible *accessible = gtk_at_context_get_accessible (self);
GtkWidget *widget = GTK_WIDGET (accessible);
GtkText *text_widget;
if (GTK_IS_ENTRY (widget))
text_widget = gtk_entry_get_text_widget (GTK_ENTRY (widget));
else if (GTK_IS_SEARCH_ENTRY (widget))
text_widget = gtk_search_entry_get_text_widget (GTK_SEARCH_ENTRY (widget));
else if (GTK_IS_PASSWORD_ENTRY (widget))
text_widget = gtk_password_entry_get_text_widget (GTK_PASSWORD_ENTRY (widget));
else if (GTK_IS_SPIN_BUTTON (widget))
text_widget = gtk_spin_button_get_text_widget (GTK_SPIN_BUTTON (widget));
GtkText *text_widget = gtk_editable_get_text_widget (widget);
if (g_strcmp0 (method_name, "GetCaretOffset") == 0)
{
@@ -1165,3 +1170,281 @@ gtk_atspi_get_text_vtable (GtkWidget *widget)
return NULL;
}
typedef struct {
void (* text_changed) (gpointer data,
const char *kind,
int start,
int end,
const char *text);
void (* selection_changed) (gpointer data,
const char *kind,
int cursor_position);
gpointer data;
GtkTextBuffer *buffer;
int cursor_position;
int selection_bound;
} TextChanged;
static void
insert_text_cb (GtkEditable *editable,
char *new_text,
int new_text_length,
int *position,
TextChanged *changed)
{
int length;
if (new_text_length == 0)
return;
length = g_utf8_strlen (new_text, new_text_length);
changed->text_changed (changed->data, "insert", *position - length, length, new_text);
}
static void
delete_text_cb (GtkEditable *editable,
int start,
int end,
TextChanged *changed)
{
char *text;
if (start == end)
return;
text = gtk_editable_get_chars (editable, start, end);
changed->text_changed (changed->data, "delete", start, end - start, text);
g_free (text);
}
static void
update_selection (TextChanged *changed,
int cursor_position,
int selection_bound)
{
gboolean caret_moved, bound_moved;
caret_moved = cursor_position != changed->cursor_position;
bound_moved = selection_bound != changed->selection_bound;
if (!caret_moved && !bound_moved)
return;
changed->cursor_position = cursor_position;
changed->selection_bound = selection_bound;
if (caret_moved)
changed->selection_changed (changed->data, "text-caret-moved", changed->cursor_position);
if (caret_moved || bound_moved)
changed->selection_changed (changed->data, "text-selection-changed", 0);
}
static void
notify_cb (GObject *object,
GParamSpec *pspec,
TextChanged *changed)
{
if (g_strcmp0 (pspec->name, "cursor-position") == 0 ||
g_strcmp0 (pspec->name, "selection-bound") == 0)
{
int cursor_position, selection_bound;
gtk_editable_get_selection_bounds (GTK_EDITABLE (object), &cursor_position, &selection_bound);
update_selection (changed, cursor_position, selection_bound);
}
}
static void
update_cursor (GtkTextBuffer *buffer,
TextChanged *changed)
{
GtkTextIter iter;
int cursor_position, selection_bound;
gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer));
cursor_position = gtk_text_iter_get_offset (&iter);
gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_selection_bound (buffer));
selection_bound = gtk_text_iter_get_offset (&iter);
update_selection (changed, cursor_position, selection_bound);
}
static void
insert_range_cb (GtkTextBuffer *buffer,
GtkTextIter *iter,
char *text,
int len,
TextChanged *changed)
{
int position;
int length;
position = gtk_text_iter_get_offset (iter);
length = g_utf8_strlen (text, len);
changed->text_changed (changed->data, "insert", position - length, length, text);
update_cursor (buffer, changed);
}
static void
delete_range_cb (GtkTextBuffer *buffer,
GtkTextIter *start,
GtkTextIter *end,
TextChanged *changed)
{
int offset, length;
char *text;
text = gtk_text_buffer_get_slice (buffer, start, end, FALSE);
offset = gtk_text_iter_get_offset (start);
length = gtk_text_iter_get_offset (end) - offset;
changed->text_changed (changed->data, "delete", offset, length, text);
g_free (text);
}
static void
delete_range_after_cb (GtkTextBuffer *buffer,
GtkTextIter *start,
GtkTextIter *end,
TextChanged *changed)
{
update_cursor (buffer, changed);
}
static void
mark_set_cb (GtkTextBuffer *buffer,
GtkTextIter *location,
GtkTextMark *mark,
TextChanged *changed)
{
if (mark == gtk_text_buffer_get_insert (buffer) ||
mark == gtk_text_buffer_get_selection_bound (buffer))
update_cursor (buffer, changed);
}
static void
buffer_changed (GtkWidget *widget,
GParamSpec *pspec,
TextChanged *changed)
{
GtkTextBuffer *buffer;
GtkTextIter start, end;
char *text;
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
if (changed->buffer)
{
g_signal_handlers_disconnect_by_func (changed->buffer, insert_range_cb, changed);
g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_cb, changed);
g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_after_cb, changed);
g_signal_handlers_disconnect_by_func (changed->buffer, mark_set_cb, changed);
gtk_text_buffer_get_bounds (changed->buffer, &start, &end);
text = gtk_text_buffer_get_slice (changed->buffer, &start, &end, FALSE);
changed->text_changed (changed->data, "delete", 0, gtk_text_buffer_get_char_count (changed->buffer), text);
g_free (text);
update_selection (changed, 0, 0);
g_clear_object (&changed->buffer);
}
changed->buffer = buffer;
if (changed->buffer)
{
g_object_ref (changed->buffer);
g_signal_connect (changed->buffer, "insert-text", G_CALLBACK (insert_range_cb), changed);
g_signal_connect (changed->buffer, "delete-range", G_CALLBACK (delete_range_cb), changed);
g_signal_connect_after (changed->buffer, "delete-range", G_CALLBACK (delete_range_after_cb), changed);
g_signal_connect_after (changed->buffer, "mark-set", G_CALLBACK (mark_set_cb), changed);
gtk_text_buffer_get_bounds (changed->buffer, &start, &end);
text = gtk_text_buffer_get_slice (changed->buffer, &start, &end, FALSE);
changed->text_changed (changed->data, "insert", 0, gtk_text_buffer_get_char_count (changed->buffer), text);
g_free (text);
update_cursor (changed->buffer, changed);
}
}
void
gtk_atspi_connect_text_signals (GtkWidget *widget,
GtkAtspiTextChangedCallback text_changed,
GtkAtspiSelectionChangedCallback selection_changed,
gpointer data)
{
TextChanged *changed;
changed = g_new0 (TextChanged, 1);
changed->text_changed = text_changed;
changed->selection_changed = selection_changed;
changed->data = data;
g_object_set_data_full (G_OBJECT (widget), "accessible-text-data", changed, g_free);
if (GTK_IS_EDITABLE (widget))
{
GtkText *text = gtk_editable_get_text_widget (widget);
if (text)
{
g_signal_connect_after (text, "insert-text", G_CALLBACK (insert_text_cb), changed);
g_signal_connect (text, "delete-text", G_CALLBACK (delete_text_cb), changed);
g_signal_connect (text, "notify", G_CALLBACK (notify_cb), changed);
gtk_editable_get_selection_bounds (GTK_EDITABLE (text), &changed->cursor_position, &changed->selection_bound);
}
}
else if (GTK_IS_TEXT_VIEW (widget))
{
g_signal_connect (widget, "notify::buffer", G_CALLBACK (buffer_changed), changed);
buffer_changed (widget, NULL, changed);
}
}
void
gtk_atspi_disconnect_text_signals (GtkWidget *widget)
{
TextChanged *changed;
changed = g_object_get_data (G_OBJECT (widget), "accessible-text-data");
g_assert (changed != NULL);
if (GTK_IS_EDITABLE (widget))
{
GtkText *text = gtk_editable_get_text_widget (widget);
if (text)
{
g_signal_handlers_disconnect_by_func (text, insert_text_cb, changed);
g_signal_handlers_disconnect_by_func (text, delete_text_cb, changed);
g_signal_handlers_disconnect_by_func (text, notify_cb, changed);
}
}
else if (GTK_IS_TEXT_VIEW (widget))
{
g_signal_handlers_disconnect_by_func (widget, buffer_changed, changed);
if (changed->buffer)
{
g_signal_handlers_disconnect_by_func (changed->buffer, insert_range_cb, changed);
g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_cb, changed);
g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_after_cb, changed);
g_signal_handlers_disconnect_by_func (changed->buffer, mark_set_cb, changed);
}
g_clear_object (&changed->buffer);
}
g_object_set_data (G_OBJECT (widget), "accessible-text-data", NULL);
}

View File

@@ -27,4 +27,19 @@ G_BEGIN_DECLS
const GDBusInterfaceVTable *gtk_atspi_get_text_vtable (GtkWidget *widget);
typedef void (GtkAtspiTextChangedCallback) (gpointer data,
const char *kind,
int start,
int end,
const char *text);
typedef void (GtkAtspiSelectionChangedCallback) (gpointer data,
const char *kind,
int position);
void gtk_atspi_connect_text_signals (GtkWidget *widget,
GtkAtspiTextChangedCallback text_changed,
GtkAtspiSelectionChangedCallback selection_changed,
gpointer data);
void gtk_atspi_disconnect_text_signals (GtkWidget *widget);
G_END_DECLS