From cc92000f1f4ae6c7fcee330ffd38842365bdf811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 18:36:15 +0100 Subject: [PATCH 01/14] data: Introduce the DBus interface description for the a11y manager This object will for now only provide a way for assistive technologies to receive keyboard events, however it is expected that it will be used for the new a11y communication protocol in the future. Part-of: (cherry picked from commit 84fc62a2806b893b215fcfb287390ced07a98aa7) --- data/dbus-interfaces/org.freedesktop.a11y.xml | 92 +++++++++++++++++++ src/meson.build | 5 + 2 files changed, 97 insertions(+) create mode 100644 data/dbus-interfaces/org.freedesktop.a11y.xml diff --git a/data/dbus-interfaces/org.freedesktop.a11y.xml b/data/dbus-interfaces/org.freedesktop.a11y.xml new file mode 100644 index 0000000000..c60480e89c --- /dev/null +++ b/data/dbus-interfaces/org.freedesktop.a11y.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/meson.build b/src/meson.build index e3c92aaf27..6c19300b8f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -947,6 +947,11 @@ if mutter_private_enum_sources.length() > 0 endif dbus_interfaces = [ + { + 'name': 'meta-dbus-a11y', + 'interface': 'org.freedesktop.a11y.xml', + 'prefix': 'org.freedesktop.a11y', + }, { 'name': 'meta-dbus-display-config', 'interface': 'org.gnome.Mutter.DisplayConfig.xml', -- 2.44.0.501.g19981daefd.dirty From 7c4e773276b9cd87373e4231db371849157db325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 18:37:28 +0100 Subject: [PATCH 02/14] clutter: Add ClutterEventFlag to notify a11y modifier click Use a Clutter event flag to communicate the the fact that the event is an a11y modifier first click. The accessibility modifiers will require special handling in the input and main threads. Part-of: (cherry picked from commit d3a0bbdf524f160b683f5db261382c306e71c58d) --- clutter/clutter/clutter-enums.h | 1 + 1 file changed, 1 insertion(+) diff --git a/clutter/clutter/clutter-enums.h b/clutter/clutter/clutter-enums.h index 9bd508fb67..451e1b8c67 100644 --- a/clutter/clutter/clutter-enums.h +++ b/clutter/clutter/clutter-enums.h @@ -519,6 +519,7 @@ typedef enum /*< flags prefix=CLUTTER_EVENT >*/ CLUTTER_EVENT_FLAG_RELATIVE_MOTION = 1 << 3, CLUTTER_EVENT_FLAG_GRAB_NOTIFY = 1 << 4, CLUTTER_EVENT_FLAG_POINTER_EMULATED = 1 << 5, + CLUTTER_EVENT_FLAG_A11Y_MODIFIER_FIRST_CLICK = 1 << 6, } ClutterEventFlags; /** -- 2.44.0.501.g19981daefd.dirty From 7971ccea8aa76745165cdf44f61a19b5f82a17b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 18:52:00 +0100 Subject: [PATCH 03/14] backends/native: Handle a11y modifier presses in MetaSeatImpl The state handling about whether the a11y modifier is a first press (so could be consumed for other actions), or results in the modifier action (e.g. caps lock) is performed in the input thread. This information will be propagated through the CLUTTER_EVENT_FLAG_A11Y_MODIFIER_FIRST_CLICK flag in the related key events. Carlos Garnacho: Drop synchronous wait for configuration changes Part-of: (cherry picked from commit d86796413929bd5d5fb59cb3c2a9ab9d714f70cc) --- src/backends/native/meta-seat-impl.c | 133 ++++++++++++++++++++++++++- src/backends/native/meta-seat-impl.h | 4 + 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/src/backends/native/meta-seat-impl.c b/src/backends/native/meta-seat-impl.c index c1e46a0502..0bdf1606fc 100644 --- a/src/backends/native/meta-seat-impl.c +++ b/src/backends/native/meta-seat-impl.c @@ -103,6 +103,14 @@ static guint signals[N_SIGNALS] = { 0 }; typedef struct _MetaSeatImplPrivate { GHashTable *device_files; + + struct { + GHashTable *grabbed_modifiers; + GHashTable *pressed_modifiers; + uint32_t last_keysym; + uint32_t last_keysym_time; + gboolean saw_first_release; + } a11y; } MetaSeatImplPrivate; static void meta_seat_impl_initable_iface_init (GInitableIface *iface); @@ -389,6 +397,65 @@ emit_signal (MetaSeatImpl *seat_impl, (GDestroyNotify) signal_data_free); } +static gboolean +is_a11y_modifier_first_click (MetaSeatImpl *seat_impl, + uint32_t keysym, + uint32_t event_time, + gboolean is_press) +{ + MetaSeatImplPrivate *priv = meta_seat_impl_get_instance_private (seat_impl); + gboolean is_same_keysym = keysym == priv->a11y.last_keysym; + gboolean event_soon_enough = + event_time - priv->a11y.last_keysym_time < seat_impl->repeat_delay; + gboolean is_grabbed_modifier = + g_hash_table_contains (priv->a11y.grabbed_modifiers, GUINT_TO_POINTER (keysym)); + + priv->a11y.last_keysym = keysym; + priv->a11y.last_keysym_time = event_time; + + /* This is not an event for a grabbed modifier */ + if (!is_grabbed_modifier) + return FALSE; + + if (!is_press && g_hash_table_contains (priv->a11y.pressed_modifiers, + GUINT_TO_POINTER (keysym))) + { + g_hash_table_remove (priv->a11y.pressed_modifiers, + GUINT_TO_POINTER (keysym)); + /* This is a release event for a previously pressed modifier */ + return FALSE; + } + + if (is_same_keysym && event_soon_enough) + { + if (is_press && priv->a11y.saw_first_release) + { + priv->a11y.saw_first_release = FALSE; + g_hash_table_add (priv->a11y.pressed_modifiers, + GUINT_TO_POINTER (keysym)); + + /* This is the second press event and it is on time, process + * it normally + */ + return FALSE; + } + else + { + priv->a11y.saw_first_release = TRUE; + /* This is the first release event, wait for the second press event */ + return TRUE; + } + } + else + { + /* This is either a different modifier, the first press + * event, or not on time to progress + */ + priv->a11y.saw_first_release = FALSE; + return TRUE; + } +} + void meta_seat_impl_notify_key_in_impl (MetaSeatImpl *seat_impl, ClutterInputDevice *device, @@ -401,6 +468,8 @@ meta_seat_impl_notify_key_in_impl (MetaSeatImpl *seat_impl, ClutterEventFlags flags = CLUTTER_EVENT_NONE; enum xkb_state_component changed_state; uint32_t keycode; + uint32_t keysym; + gboolean should_ignore; if (state != AUTOREPEAT_VALUE) { @@ -421,6 +490,16 @@ meta_seat_impl_notify_key_in_impl (MetaSeatImpl *seat_impl, flags = CLUTTER_EVENT_FLAG_REPEATED; } + keycode = meta_xkb_evdev_to_keycode (key); + keysym = xkb_state_key_get_one_sym (seat_impl->xkb, keycode); + + should_ignore = is_a11y_modifier_first_click (seat_impl, + keysym, + time_us / 1000, + state); + if (should_ignore) + flags |= CLUTTER_EVENT_FLAG_A11Y_MODIFIER_FIRST_CLICK; + event = meta_key_event_new_from_evdev (device, seat_impl->core_keyboard, flags, @@ -428,11 +507,9 @@ meta_seat_impl_notify_key_in_impl (MetaSeatImpl *seat_impl, seat_impl->button_state, time_us, key, state); - keycode = meta_xkb_evdev_to_keycode (key); - /* We must be careful and not pass multiple releases to xkb, otherwise it gets confused and locks the modifiers */ - if (state != AUTOREPEAT_VALUE) + if (!should_ignore && state != AUTOREPEAT_VALUE) { changed_state = xkb_state_update_key (seat_impl->xkb, keycode, state ? XKB_KEY_DOWN : XKB_KEY_UP); @@ -2938,6 +3015,9 @@ input_thread (MetaSeatImpl *seat_impl) NULL, (GDestroyNotify) meta_device_file_release); + priv->a11y.grabbed_modifiers = g_hash_table_new (NULL, NULL); + priv->a11y.pressed_modifiers = g_hash_table_new (NULL, NULL); + seat_impl->input_settings = meta_input_settings_native_new_in_impl (seat_impl); g_signal_connect_object (seat_impl->input_settings, "kbd-a11y-changed", G_CALLBACK (kbd_a11y_changed_cb), seat_impl, 0); @@ -3099,6 +3179,9 @@ destroy_in_impl (GTask *task) g_clear_pointer (&priv->device_files, g_hash_table_destroy); + g_clear_pointer (&priv->a11y.grabbed_modifiers, g_hash_table_destroy); + g_clear_pointer (&priv->a11y.pressed_modifiers, g_hash_table_destroy); + g_main_loop_quit (seat_impl->input_loop); g_task_return_boolean (task, TRUE); @@ -3820,6 +3903,50 @@ meta_seat_impl_set_viewports (MetaSeatImpl *seat_impl, g_cond_clear (&data.cond); } +static gboolean +set_a11y_modifiers (GTask *task) +{ + MetaSeatImpl *seat_impl = g_task_get_source_object (task); + MetaSeatImplPrivate *priv = meta_seat_impl_get_instance_private (seat_impl); + GArray *modifiers = g_task_get_task_data (task); + int i; + + g_hash_table_remove_all (priv->a11y.grabbed_modifiers); + + for (i = 0; i < modifiers->len; i++) + { + uint32_t keysym; + + keysym = g_array_index (modifiers, uint32_t, i); + g_hash_table_add (priv->a11y.grabbed_modifiers, + GUINT_TO_POINTER (keysym)); + } + + g_task_return_boolean (task, TRUE); + + return G_SOURCE_REMOVE; +} + +void +meta_seat_impl_set_a11y_modifiers (MetaSeatImpl *seat_impl, + const uint32_t *modifiers, + int n_modifiers) +{ + g_autoptr (GTask) task = NULL; + GArray *modifiers_copy; + + g_return_if_fail (META_IS_SEAT_IMPL (seat_impl)); + + modifiers_copy = g_array_new (FALSE, FALSE, sizeof (uint32_t)); + g_array_append_vals (modifiers_copy, modifiers, n_modifiers); + + task = g_task_new (seat_impl, NULL, NULL, NULL); + g_task_set_task_data (task, modifiers_copy, + (GDestroyNotify) g_array_unref); + meta_seat_impl_run_input_task (seat_impl, task, + (GSourceFunc) set_a11y_modifiers); +} + MetaSeatImpl * meta_seat_impl_new (MetaSeatNative *seat_native, const char *seat_id, diff --git a/src/backends/native/meta-seat-impl.h b/src/backends/native/meta-seat-impl.h index d95fb7176b..7d85fc6fc7 100644 --- a/src/backends/native/meta-seat-impl.h +++ b/src/backends/native/meta-seat-impl.h @@ -263,3 +263,7 @@ void meta_seat_impl_add_virtual_input_device (MetaSeatImpl *seat_impl, void meta_seat_impl_remove_virtual_input_device (MetaSeatImpl *seat_impl, ClutterInputDevice *device); + +void meta_seat_impl_set_a11y_modifiers (MetaSeatImpl *seat_impl, + const uint32_t *modifiers, + int n_modifiers); -- 2.44.0.501.g19981daefd.dirty From 4615e7ce13211ee7aea8aa83f265a4318356495e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 18:58:41 +0100 Subject: [PATCH 04/14] backends/native: Plumb a11y modifiers through MetaSeatNative These modifiers will be set by the backend from the main thread, and need to be handled specially for them to be usable as both modifier buttons, and their own regular action. Carlos Garnacho: pass modifiers as array+lenght instead of hashtable. Part-of: (cherry picked from commit 8c52e243c03c03c4e1fe0c13c730fd24d84a40a6) --- src/backends/native/meta-seat-native.c | 8 ++++++++ src/backends/native/meta-seat-native.h | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/backends/native/meta-seat-native.c b/src/backends/native/meta-seat-native.c index d912875684..ed3842bcf1 100644 --- a/src/backends/native/meta-seat-native.c +++ b/src/backends/native/meta-seat-native.c @@ -656,6 +656,14 @@ meta_seat_native_set_viewports (MetaSeatNative *seat, meta_seat_impl_set_viewports (seat->impl, viewports); } +void +meta_seat_native_set_a11y_modifiers (MetaSeatNative *seat, + const uint32_t *modifiers, + int n_modifiers) +{ + meta_seat_impl_set_a11y_modifiers (seat->impl, modifiers, n_modifiers); +} + void meta_seat_native_run_impl_task (MetaSeatNative *seat, GSourceFunc dispatch_func, diff --git a/src/backends/native/meta-seat-native.h b/src/backends/native/meta-seat-native.h index e82d0043be..cccfa6ce63 100644 --- a/src/backends/native/meta-seat-native.h +++ b/src/backends/native/meta-seat-native.h @@ -137,3 +137,7 @@ void meta_seat_native_run_impl_task (MetaSeatNative *seat, GSourceFunc dispatch_func, gpointer user_data, GDestroyNotify destroy_notify); + +void meta_seat_native_set_a11y_modifiers (MetaSeatNative *seat, + const uint32_t *modifiers, + int n_modifiers); -- 2.44.0.501.g19981daefd.dirty From 893688f075578a5d8f660094105a53d20b1143fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 19:01:26 +0100 Subject: [PATCH 05/14] backends: Add MetaA11yManager This a11y manager will handle key event emission to screen readers and other ATs. This initial commit only introduces the object. Carlos Garnacho: Make the object take a ::backend property Part-of: (cherry picked from commit c0c452445236d98fc4297603f269938ba255a206) --- src/backends/meta-a11y-manager.c | 102 +++++++++++++++++++++++++++++++ src/backends/meta-a11y-manager.h | 29 +++++++++ src/meson.build | 2 + 3 files changed, 133 insertions(+) create mode 100644 src/backends/meta-a11y-manager.c create mode 100644 src/backends/meta-a11y-manager.h diff --git a/src/backends/meta-a11y-manager.c b/src/backends/meta-a11y-manager.c new file mode 100644 index 0000000000..22de01030d --- /dev/null +++ b/src/backends/meta-a11y-manager.c @@ -0,0 +1,102 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* + * Copyright 2024 GNOME Foundation + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#include "config.h" + +#include "backends/meta-a11y-manager.h" +#include "meta/meta-backend.h" +#include "meta/meta-context.h" +#include "meta/util.h" + +#include "meta-dbus-a11y.h" + +enum +{ + A11Y_MODIFIERS_CHANGED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +enum +{ + PROP_0, + PROP_BACKEND, + N_PROPS, +}; + +static GParamSpec *props[N_PROPS]; + +typedef struct _MetaA11yManager +{ + GObject parent; + MetaBackend *backend; +} MetaA11yManager; + +G_DEFINE_TYPE (MetaA11yManager, meta_a11y_manager, G_TYPE_OBJECT) + +static void +meta_a11y_manager_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MetaA11yManager *a11y_manager = META_A11Y_MANAGER (object); + + switch (prop_id) + { + case PROP_BACKEND: + a11y_manager->backend = g_value_get_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +meta_a11y_manager_class_init (MetaA11yManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = meta_a11y_manager_set_property; + + props[PROP_BACKEND] = + g_param_spec_object ("backend", NULL, NULL, + META_TYPE_BACKEND, + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, props); +} + +static void +meta_a11y_manager_init (MetaA11yManager *a11y_manager) +{ +} + +MetaA11yManager * +meta_a11y_manager_new (MetaBackend *backend) +{ + return g_object_new (META_TYPE_A11Y_MANAGER, + "backend", backend, + NULL); +} diff --git a/src/backends/meta-a11y-manager.h b/src/backends/meta-a11y-manager.h new file mode 100644 index 0000000000..df5c9a5d21 --- /dev/null +++ b/src/backends/meta-a11y-manager.h @@ -0,0 +1,29 @@ +/* + * Copyright 2024 GNOME Foundation + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#pragma once + +#include + +#include "backends/meta-backend-types.h" +#include "clutter/clutter.h" + +#define META_TYPE_A11Y_MANAGER (meta_a11y_manager_get_type ()) +G_DECLARE_FINAL_TYPE (MetaA11yManager, meta_a11y_manager, META, A11Y_MANAGER, GObject) + +MetaA11yManager * meta_a11y_manager_new (MetaBackend *backend); diff --git a/src/meson.build b/src/meson.build index 6c19300b8f..c57fd935f6 100644 --- a/src/meson.build +++ b/src/meson.build @@ -193,6 +193,8 @@ mutter_sources = [ 'backends/edid.h', 'backends/edid-parse.c', 'backends/gsm-inhibitor-flag.h', + 'backends/meta-a11y-manager.c', + 'backends/meta-a11y-manager.h', 'backends/meta-backend.c', 'backends/meta-backend-private.h', 'backends/meta-barrier.c', -- 2.44.0.501.g19981daefd.dirty From 3976dc0c8accf9fcd2968e5980408fca32589a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 19:03:10 +0100 Subject: [PATCH 06/14] backends: Manage MetaA11yManager in MetaBackend The MetaBackend will own the MetaA11yManager, being actually put to use in backend implementations. Part-of: (cherry picked from commit f4ce1e8a462b96e207ff0ba21040c50e91c62cc2) --- src/backends/meta-backend-private.h | 3 +++ src/backends/meta-backend.c | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/backends/meta-backend-private.h b/src/backends/meta-backend-private.h index 1d2af368bd..411a0a5d57 100644 --- a/src/backends/meta-backend-private.h +++ b/src/backends/meta-backend-private.h @@ -28,6 +28,7 @@ #include "meta/meta-backend.h" #include "meta/meta-idle-monitor.h" #include "meta/meta-orientation-manager.h" +#include "backends/meta-a11y-manager.h" #include "backends/meta-backend-types.h" #include "backends/meta-cursor-renderer.h" #include "backends/meta-egl.h" @@ -159,6 +160,8 @@ MetaScreenCast * meta_backend_get_screen_cast (MetaBackend *backend); MetaInputCapture * meta_backend_get_input_capture (MetaBackend *backend); +MetaA11yManager * meta_backend_get_a11y_manager (MetaBackend *backend); + gboolean meta_backend_grab_device (MetaBackend *backend, int device_id, uint32_t timestamp); diff --git a/src/backends/meta-backend.c b/src/backends/meta-backend.c index cdca232abc..2cdc7edf6c 100644 --- a/src/backends/meta-backend.c +++ b/src/backends/meta-backend.c @@ -52,6 +52,7 @@ #include +#include "backends/meta-a11y-manager.h" #include "backends/meta-barrier-private.h" #include "backends/meta-color-manager-private.h" #include "backends/meta-cursor-renderer.h" @@ -154,6 +155,7 @@ struct _MetaBackendPrivate MetaRemoteDesktop *remote_desktop; #endif MetaInputCapture *input_capture; + MetaA11yManager *a11y_manager; #ifdef HAVE_LIBWACOM WacomDeviceDatabase *wacom_db; @@ -226,6 +228,7 @@ meta_backend_dispose (GObject *object) g_clear_object (&priv->input_capture); g_clear_object (&priv->dbus_session_watcher); g_clear_object (&priv->remote_access_controller); + g_clear_object (&priv->a11y_manager); g_clear_object (&priv->dnd); #ifdef HAVE_LIBWACOM @@ -607,6 +610,8 @@ meta_backend_real_post_init (MetaBackend *backend) priv->remote_access_controller, META_DBUS_SESSION_MANAGER (priv->input_capture)); + priv->a11y_manager = meta_a11y_manager_new (backend); + if (!meta_monitor_manager_is_headless (priv->monitor_manager)) init_pointer_position (backend); @@ -1548,6 +1553,19 @@ meta_backend_get_remote_access_controller (MetaBackend *backend) return priv->remote_access_controller; } +/** + * meta_backend_get_a11y_manager: + * + * Returns: (transfer none): the #MetaA11yManager + */ +MetaA11yManager * +meta_backend_get_a11y_manager (MetaBackend *backend) +{ + MetaBackendPrivate *priv = meta_backend_get_instance_private (backend); + + return priv->a11y_manager; +} + /** * meta_backend_is_rendering_hardware_accelerated: * @backend: A #MetaBackend -- 2.44.0.501.g19981daefd.dirty From b6e5ec24b95f0b389b18927af0956f3b59d7b21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 19:15:54 +0100 Subject: [PATCH 07/14] backends: Implement A11y monitor interface Implement the org.freedesktop.a11y.KeyboardMonitor interface, allowing screen readers to interact with Mutter and grab shortcuts or full keyboard interaction. Carlos Garnacho: Move setup to ::constructed. Part-of: (cherry picked from commit 0bcda4ecf47f04cb76fb642f05d498d1dd941e15) --- src/backends/meta-a11y-manager.c | 274 +++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git a/src/backends/meta-a11y-manager.c b/src/backends/meta-a11y-manager.c index 22de01030d..5f8d5b1ee4 100644 --- a/src/backends/meta-a11y-manager.c +++ b/src/backends/meta-a11y-manager.c @@ -44,14 +44,253 @@ enum static GParamSpec *props[N_PROPS]; +typedef struct _MetaA11yKeystroke +{ + uint32_t keysym; + ClutterModifierType modifiers; +} MetaA11yKeystroke; + +typedef struct _MetaA11yKeyGrabber +{ + MetaA11yManager *manager; + GDBusConnection *connection; + char *bus_name; + guint bus_name_watcher_id; + gboolean grab_all; + GArray *modifiers; + GArray *keystrokes; +} MetaA11yKeyGrabber; + typedef struct _MetaA11yManager { GObject parent; MetaBackend *backend; + guint dbus_name_id; + MetaDBusKeyboardMonitor *keyboard_monitor_skeleton; + + GList *key_grabbers; + GHashTable *grabbed_keypresses; + GHashTable *all_grabbed_modifiers; } MetaA11yManager; G_DEFINE_TYPE (MetaA11yManager, meta_a11y_manager, G_TYPE_OBJECT) +static void +key_grabber_free (MetaA11yKeyGrabber *grabber) +{ + if (grabber->bus_name_watcher_id) + { + g_bus_unwatch_name (grabber->bus_name_watcher_id); + grabber->bus_name_watcher_id = 0; + } + + g_clear_pointer (&grabber->keystrokes, g_array_unref); + g_clear_pointer (&grabber->modifiers, g_array_unref); + g_clear_object (&grabber->connection); + g_clear_pointer (&grabber->bus_name, g_free); + + g_free (grabber); +} + +static void +rebuild_all_grabbed_modifiers (MetaA11yManager *a11y_manager, + MetaA11yKeyGrabber *ignored_grabber) +{ + GList *l; + int i; + + g_hash_table_remove_all (a11y_manager->all_grabbed_modifiers); + + for (l = a11y_manager->key_grabbers; l; l = l->next) + { + MetaA11yKeyGrabber *grabber = l->data; + + if (grabber == ignored_grabber) + continue; + + for (i = 0; i < grabber->modifiers->len; i++) + { + uint32_t modifier_keysym = g_array_index (grabber->modifiers, uint32_t, i); + g_hash_table_add (a11y_manager->all_grabbed_modifiers, + GUINT_TO_POINTER (modifier_keysym)); + } + } +} + +static void +key_grabber_bus_name_vanished_callback (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + MetaA11yKeyGrabber *grabber = user_data; + MetaA11yManager *a11y_manager = grabber->manager; + + grabber->manager->key_grabbers = + g_list_remove (grabber->manager->key_grabbers, grabber); + + if (grabber->modifiers) + { + rebuild_all_grabbed_modifiers (a11y_manager, grabber); + g_signal_emit (grabber->manager, signals[A11Y_MODIFIERS_CHANGED], 0); + } + + key_grabber_free (grabber); +} + +static MetaA11yKeyGrabber * +ensure_key_grabber (MetaA11yManager *a11y_manager, + GDBusMethodInvocation *invocation) +{ + GDBusConnection *connection = + g_dbus_method_invocation_get_connection (invocation); + const char *sender = g_dbus_method_invocation_get_sender (invocation); + MetaA11yKeyGrabber *grabber; + GList *l; + + for (l = a11y_manager->key_grabbers; l; l = l->next) + { + grabber = l->data; + + if (g_strcmp0 (grabber->bus_name, sender) == 0) + return grabber; + } + + grabber = g_new0 (MetaA11yKeyGrabber, 1); + grabber->manager = a11y_manager; + grabber->bus_name = g_strdup (sender); + grabber->connection = g_object_ref (connection); + + grabber->bus_name_watcher_id = + g_bus_watch_name_on_connection (connection, + grabber->bus_name, + G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, + key_grabber_bus_name_vanished_callback, + grabber, + NULL); + + a11y_manager->key_grabbers = + g_list_prepend (a11y_manager->key_grabbers, grabber); + + return grabber; +} + +static gboolean +handle_grab_keyboard (MetaDBusKeyboardMonitor *skeleton, + GDBusMethodInvocation *invocation, + MetaA11yManager *a11y_manager) +{ + MetaA11yKeyGrabber *grabber; + + grabber = ensure_key_grabber (a11y_manager, invocation); + grabber->grab_all = TRUE; + meta_dbus_keyboard_monitor_complete_grab_keyboard (skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_ungrab_keyboard (MetaDBusKeyboardMonitor *skeleton, + GDBusMethodInvocation *invocation, + MetaA11yManager *a11y_manager) +{ + MetaA11yKeyGrabber *grabber; + + grabber = ensure_key_grabber (a11y_manager, invocation); + grabber->grab_all = FALSE; + meta_dbus_keyboard_monitor_complete_ungrab_keyboard (skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_set_key_grabs (MetaDBusKeyboardMonitor *skeleton, + GDBusMethodInvocation *invocation, + GVariant *modifiers, + GVariant *keystrokes, + MetaA11yManager *a11y_manager) +{ + MetaA11yKeyGrabber *grabber; + GVariantIter iter; + uint32_t modifier_keysym; + MetaA11yKeystroke keystroke; + + grabber = ensure_key_grabber (a11y_manager, invocation); + + g_clear_pointer (&grabber->modifiers, g_array_unref); + g_clear_pointer (&grabber->keystrokes, g_array_unref); + grabber->modifiers = g_array_new (FALSE, FALSE, sizeof (uint32_t)); + grabber->keystrokes = g_array_new (FALSE, FALSE, sizeof (MetaA11yKeystroke)); + + g_variant_iter_init (&iter, modifiers); + while (g_variant_iter_next (&iter, "u", &modifier_keysym)) + g_array_append_val (grabber->modifiers, modifier_keysym); + + g_variant_iter_init (&iter, keystrokes); + while (g_variant_iter_next (&iter, "(uu)", &keystroke.keysym, + &keystroke.modifiers)) + g_array_append_val (grabber->keystrokes, keystroke); + + rebuild_all_grabbed_modifiers (a11y_manager, NULL); + g_signal_emit (a11y_manager, signals[A11Y_MODIFIERS_CHANGED], 0); + meta_dbus_keyboard_monitor_complete_set_key_grabs (skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + MetaA11yManager *manager = user_data; + + manager->keyboard_monitor_skeleton = meta_dbus_keyboard_monitor_skeleton_new (); + + g_signal_connect (manager->keyboard_monitor_skeleton, "handle-grab-keyboard", + G_CALLBACK (handle_grab_keyboard), manager); + g_signal_connect (manager->keyboard_monitor_skeleton, "handle-ungrab-keyboard", + G_CALLBACK (handle_ungrab_keyboard), manager); + g_signal_connect (manager->keyboard_monitor_skeleton, "handle-set-key-grabs", + G_CALLBACK (handle_set_key_grabs), manager); + + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (manager->keyboard_monitor_skeleton), + connection, + "/org/freedesktop/a11y/Manager", + NULL); +} + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Acquired name %s", name); +} + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + +static void +meta_a11y_manager_finalize (GObject *object) +{ + MetaA11yManager *a11y_manager = META_A11Y_MANAGER (object); + + g_list_free_full (a11y_manager->key_grabbers, + (GDestroyNotify) key_grabber_free); + g_clear_object (&a11y_manager->keyboard_monitor_skeleton); + g_clear_pointer (&a11y_manager->grabbed_keypresses, g_hash_table_destroy); + g_clear_pointer (&a11y_manager->all_grabbed_modifiers, g_hash_table_destroy); + g_bus_unown_name (a11y_manager->dbus_name_id); + + G_OBJECT_CLASS (meta_a11y_manager_parent_class)->finalize (object); +} + static void meta_a11y_manager_set_property (GObject *object, guint prop_id, @@ -71,12 +310,47 @@ meta_a11y_manager_set_property (GObject *object, } } +static void +meta_a11y_manager_constructed (GObject *object) +{ + MetaA11yManager *a11y_manager = META_A11Y_MANAGER (object); + MetaContext *context; + + g_assert (a11y_manager->backend); + context = meta_backend_get_context (a11y_manager->backend); + + a11y_manager->grabbed_keypresses = g_hash_table_new (NULL, NULL); + a11y_manager->all_grabbed_modifiers = g_hash_table_new (NULL, NULL); + + a11y_manager->dbus_name_id = + g_bus_own_name (G_BUS_TYPE_SESSION, + "org.freedesktop.a11y.Manager", + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + (meta_context_is_replacing (context) ? + G_BUS_NAME_OWNER_FLAGS_REPLACE : + G_BUS_NAME_OWNER_FLAGS_NONE), + on_bus_acquired, + on_name_acquired, + on_name_lost, + a11y_manager, + NULL); +} + static void meta_a11y_manager_class_init (MetaA11yManagerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = meta_a11y_manager_finalize; object_class->set_property = meta_a11y_manager_set_property; + object_class->constructed = meta_a11y_manager_constructed; + + signals[A11Y_MODIFIERS_CHANGED] = + g_signal_new ("a11y-modifiers-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); props[PROP_BACKEND] = g_param_spec_object ("backend", NULL, NULL, -- 2.44.0.501.g19981daefd.dirty From c60b8ee816991f6ca0ccd6e1c66b19a649a563df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 19:17:08 +0100 Subject: [PATCH 08/14] backends: Add MetaA11yManager method to get configured a11y modifiers The ::a11y-modifiers-change method allows tracking for changes in the configured modifiers, add this method to allow backends to get the modifiers so that they can be passed along the lower layers. Carlos Garnacho: Turn into a method instead of a signal argument, turn into an array+length instead of a hashtable. Part-of: (cherry picked from commit 1498724774976fad1dbd4e5bc210205cef1faf9d) --- src/backends/meta-a11y-manager.c | 24 ++++++++++++++++++++++++ src/backends/meta-a11y-manager.h | 3 +++ 2 files changed, 27 insertions(+) diff --git a/src/backends/meta-a11y-manager.c b/src/backends/meta-a11y-manager.c index 5f8d5b1ee4..647962c866 100644 --- a/src/backends/meta-a11y-manager.c +++ b/src/backends/meta-a11y-manager.c @@ -374,3 +374,27 @@ meta_a11y_manager_new (MetaBackend *backend) "backend", backend, NULL); } + +uint32_t * +meta_a11y_manager_get_modifier_keysyms (MetaA11yManager *a11y_manager, + int *n_modifiers) +{ + GArray *modifier_keysyms; + GHashTableIter iter; + gpointer key; + + modifier_keysyms = g_array_new (FALSE, FALSE, sizeof (uint32_t)); + + g_hash_table_iter_init (&iter, a11y_manager->all_grabbed_modifiers); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + uint32_t keysym = GPOINTER_TO_UINT (key); + + g_array_append_val (modifier_keysyms, keysym); + } + + if (n_modifiers) + *n_modifiers = modifier_keysyms->len; + + return (uint32_t *) g_array_free (modifier_keysyms, FALSE); +} diff --git a/src/backends/meta-a11y-manager.h b/src/backends/meta-a11y-manager.h index df5c9a5d21..df07c94de2 100644 --- a/src/backends/meta-a11y-manager.h +++ b/src/backends/meta-a11y-manager.h @@ -27,3 +27,6 @@ G_DECLARE_FINAL_TYPE (MetaA11yManager, meta_a11y_manager, META, A11Y_MANAGER, GObject) MetaA11yManager * meta_a11y_manager_new (MetaBackend *backend); + +uint32_t * meta_a11y_manager_get_modifier_keysyms (MetaA11yManager *a11y_manager, + int *n_modifiers); -- 2.44.0.501.g19981daefd.dirty From 3ce02680be1fea0c488c9681e87dfafac315fc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 19:03:50 +0100 Subject: [PATCH 09/14] backends/native: Propagate a11y modifiers from MetaA11yManager Listen to changes in MetaA11yManager configured modifiers, and propagate these along to the MetaSeatNative. This lets the backend keep track of configuration changes in a11y modifiers. Part-of: (cherry picked from commit 45f6bed7809dadb40df98308b9041fe08b453871) --- src/backends/native/meta-backend-native.c | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/backends/native/meta-backend-native.c b/src/backends/native/meta-backend-native.c index ee02b2783a..8732f3f7d2 100644 --- a/src/backends/native/meta-backend-native.c +++ b/src/backends/native/meta-backend-native.c @@ -38,6 +38,7 @@ #include +#include "backends/meta-a11y-manager.h" #include "backends/meta-color-manager.h" #include "backends/meta-cursor-tracker-private.h" #include "backends/meta-idle-manager.h" @@ -183,6 +184,23 @@ update_viewports (MetaBackend *backend) g_object_unref (viewports); } +static void +on_a11y_modifiers_changed (MetaA11yManager *a11y_manager, + MetaBackend *backend) +{ + MetaSeatNative *seat; + ClutterBackend *clutter_backend; + g_autofree uint32_t *modifiers; + int n_modifiers; + + clutter_backend = meta_backend_get_clutter_backend (backend); + seat = META_SEAT_NATIVE (clutter_backend_get_default_seat (clutter_backend)); + modifiers = meta_a11y_manager_get_modifier_keysyms (a11y_manager, + &n_modifiers); + + meta_seat_native_set_a11y_modifiers (seat, modifiers, n_modifiers); +} + static void meta_backend_native_post_init (MetaBackend *backend) { @@ -191,6 +209,7 @@ meta_backend_native_post_init (MetaBackend *backend) meta_backend_native_get_instance_private (backend_native); MetaMonitorManager *monitor_manager = meta_backend_get_monitor_manager (backend); + MetaA11yManager *a11y_manager = meta_backend_get_a11y_manager (backend); META_BACKEND_CLASS (meta_backend_native_parent_class)->post_init (backend); @@ -200,6 +219,12 @@ meta_backend_native_post_init (MetaBackend *backend) g_signal_connect_swapped (monitor_manager, "monitors-changed-internal", G_CALLBACK (update_viewports), backend); update_viewports (backend); + + g_signal_connect_object (a11y_manager, + "a11y-modifiers-changed", + G_CALLBACK (on_a11y_modifiers_changed), + backend, + G_CONNECT_DEFAULT); } static MetaBackendCapabilities -- 2.44.0.501.g19981daefd.dirty From b03c4412be7fec86f43c975e418556f17398fd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 5 Feb 2025 19:16:41 +0100 Subject: [PATCH 10/14] backends: Add method to pass key events to screen readers Accessibility shortcuts and keyboard grabs need to be consumed, and key events propagated to screen readers. Add a function that does it all at once. Part-of: (cherry picked from commit 800981c40c4f18f496a79227b3d3d5f0cdc195c9) --- src/backends/meta-a11y-manager.c | 148 +++++++++++++++++++++++++++++++ src/backends/meta-a11y-manager.h | 3 + 2 files changed, 151 insertions(+) diff --git a/src/backends/meta-a11y-manager.c b/src/backends/meta-a11y-manager.c index 647962c866..a6ec4f6f3a 100644 --- a/src/backends/meta-a11y-manager.c +++ b/src/backends/meta-a11y-manager.c @@ -27,6 +27,12 @@ #include "meta-dbus-a11y.h" +#define MOUSE_BUTTONS_MASK (CLUTTER_BUTTON1_MASK | \ + CLUTTER_BUTTON2_MASK | \ + CLUTTER_BUTTON3_MASK | \ + CLUTTER_BUTTON4_MASK | \ + CLUTTER_BUTTON5_MASK) + enum { A11Y_MODIFIERS_CHANGED, @@ -375,6 +381,148 @@ meta_a11y_manager_new (MetaBackend *backend) NULL); } +static gboolean +should_grab_keypress (MetaA11yManager *a11y_manager, + MetaA11yKeyGrabber *grabber, + uint32_t keysym, + ClutterModifierType modifiers) +{ + int i; + + if (grabber->grab_all) + return TRUE; + + if (grabber->modifiers) + { + for (i = 0; i < grabber->modifiers->len; i++) + { + uint32_t modifier_keysym; + + modifier_keysym = g_array_index (grabber->modifiers, uint32_t, i); + + if (keysym == modifier_keysym || + g_hash_table_contains (a11y_manager->grabbed_keypresses, + GUINT_TO_POINTER (modifier_keysym))) + return TRUE; + } + } + + if (grabber->keystrokes) + { + for (i = 0; i < grabber->keystrokes->len; i++) + { + MetaA11yKeystroke *keystroke = + &(g_array_index (grabber->keystrokes, MetaA11yKeystroke, i)); + + if (keysym == keystroke->keysym && modifiers == keystroke->modifiers) + return TRUE; + } + } + + return FALSE; +} + +static gboolean +is_grabbed_modifier_key (MetaA11yManager *a11y_manager, + uint32_t keysym) +{ + return g_hash_table_contains (a11y_manager->all_grabbed_modifiers, + GUINT_TO_POINTER (keysym)); +} + +static void +notify_client (MetaA11yManager *a11y_manager, + MetaA11yKeyGrabber *key_grabber, + gboolean released, + ClutterModifierType state, + uint32_t keysym, + uint32_t unichar, + uint32_t keycode) +{ + g_autoptr (GError) error = NULL; + + if (!g_dbus_connection_emit_signal (key_grabber->connection, + key_grabber->bus_name, + "/org/freedesktop/a11y/Manager", + "org.freedesktop.a11y.KeyboardMonitor", + "KeyEvent", + g_variant_new ("(buuuq)", + released, + state, + keysym, + unichar, + (uint16_t) keycode), + &error)) + g_warning ("Could not emit a11y KeyEvent: %s", error->message); +} + +gboolean +meta_a11y_manager_notify_clients (MetaA11yManager *a11y_manager, + const ClutterEvent *event) +{ + gboolean a11y_grabbed = FALSE; + gboolean released = clutter_event_type (event) == CLUTTER_KEY_RELEASE; + /* A grabbed modifier is a11y grabbed if it was not double pressed, otherwise we process it normally */ + gboolean is_ignorable = + !!(clutter_event_get_flags (event) & + CLUTTER_EVENT_FLAG_A11Y_MODIFIER_FIRST_CLICK); + /* The Clutter event modifiers mask includes mouse buttons as well, + * but they're not expected by ATs, so we filter them out. + */ + uint32_t keysym = clutter_event_get_key_symbol (event); + uint32_t unichar = clutter_event_get_key_unicode (event); + uint32_t keycode = clutter_event_get_key_code (event); + ClutterModifierType state; + GList *l; + + state = clutter_event_get_state (event) & ~MOUSE_BUTTONS_MASK; + + for (l = a11y_manager->key_grabbers; l; l = l->next) + { + MetaA11yKeyGrabber *grabber = l->data; + + if (should_grab_keypress (a11y_manager, grabber, keysym, state)) + { + notify_client (a11y_manager, grabber, released, + state, keysym, unichar, keycode); + } + } + + if (is_grabbed_modifier_key (a11y_manager, keysym) && !is_ignorable) + return FALSE; + + if (released) + { + if (g_hash_table_contains (a11y_manager->grabbed_keypresses, + GUINT_TO_POINTER (keysym))) + { + g_hash_table_remove (a11y_manager->grabbed_keypresses, + GUINT_TO_POINTER (keysym)); + a11y_grabbed = TRUE; + } + } + else + { + if (g_hash_table_contains (a11y_manager->grabbed_keypresses, + GUINT_TO_POINTER (keysym))) + a11y_grabbed = TRUE; + + for (l = a11y_manager->key_grabbers; l; l = l->next) + { + MetaA11yKeyGrabber *grabber = l->data; + + if (should_grab_keypress (a11y_manager, grabber, keysym, state)) + { + g_hash_table_add (a11y_manager->grabbed_keypresses, + GUINT_TO_POINTER (keysym)); + a11y_grabbed = TRUE; + } + } + } + + return a11y_grabbed; +} + uint32_t * meta_a11y_manager_get_modifier_keysyms (MetaA11yManager *a11y_manager, int *n_modifiers) diff --git a/src/backends/meta-a11y-manager.h b/src/backends/meta-a11y-manager.h index df07c94de2..754cb65394 100644 --- a/src/backends/meta-a11y-manager.h +++ b/src/backends/meta-a11y-manager.h @@ -28,5 +28,8 @@ G_DECLARE_FINAL_TYPE (MetaA11yManager, meta_a11y_manager, META, A11Y_MANAGER, GO MetaA11yManager * meta_a11y_manager_new (MetaBackend *backend); +gboolean meta_a11y_manager_notify_clients (MetaA11yManager *a11y_manager, + const ClutterEvent *event); + uint32_t * meta_a11y_manager_get_modifier_keysyms (MetaA11yManager *a11y_manager, int *n_modifiers); -- 2.44.0.501.g19981daefd.dirty From 921a010f93b4b89d50b7ba97666af367bd8719a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Fri, 7 Feb 2025 14:21:17 +0100 Subject: [PATCH 11/14] backends: Add a11y interface methods to subscribe to key event input This adds a pair of methods to signal an interest in receiving all key events without grabbing them, e. g. the previously expected behavior by screen readers. Part-of: (cherry picked from commit 20c9b8cf0c85dabbe73b5b0ce155729a92f47bb0) --- data/dbus-interfaces/org.freedesktop.a11y.xml | 31 +++++++++++- src/backends/meta-a11y-manager.c | 47 ++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/data/dbus-interfaces/org.freedesktop.a11y.xml b/data/dbus-interfaces/org.freedesktop.a11y.xml index c60480e89c..4f1d210423 100644 --- a/data/dbus-interfaces/org.freedesktop.a11y.xml +++ b/data/dbus-interfaces/org.freedesktop.a11y.xml @@ -16,7 +16,7 @@ GrabKeyboard: Starts grabbing all key events. The client receives the events - through the KeyEvent signal as always, but the events aren't handled + through the KeyEvent signal, and in addition, the events aren't handled normally by the compositor. This includes changes to the state of toggles like Caps Lock, Num Lock, and Scroll Lock. @@ -33,9 +33,36 @@ After calling this method, the key grabs specified in the last call to SetKeyGrabs, if any, are still in effect. + Also, the client will still receive key events through the KeyEvent + signal, if it has called WatchKeyboard. --> + + + + + +