From d6e45f2a836cf9855a0a9ac5246146cc4d49816c Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Wed, 24 Apr 2024 16:06:15 +1200 Subject: [PATCH 1/4] Don't emit a11y signal until table is updated --- shell/platform/linux/fl_view_accessible.cc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shell/platform/linux/fl_view_accessible.cc b/shell/platform/linux/fl_view_accessible.cc index 7ddefe0b0bbfb..bf470bde37ba7 100644 --- a/shell/platform/linux/fl_view_accessible.cc +++ b/shell/platform/linux/fl_view_accessible.cc @@ -15,6 +15,9 @@ struct _FlViewAccessible { // Semantics nodes keyed by ID GHashTable* semantics_nodes_by_id; + + // Flag to track when root node is created. + gboolean root_node_created; }; enum { kProp0, kPropEngine, kPropLast }; @@ -66,12 +69,17 @@ static FlAccessibleNode* get_node(FlViewAccessible* self, node = create_node(self, semantics); if (semantics->id == 0) { fl_accessible_node_set_parent(node, ATK_OBJECT(self), 0); - g_signal_emit_by_name(self, "children-changed::add", 0, node, nullptr); } g_hash_table_insert(self->semantics_nodes_by_id, GINT_TO_POINTER(semantics->id), reinterpret_cast(node)); + // Update when root node is created. + if (!self->root_node_created && semantics->id == 0) { + g_signal_emit_by_name(self, "children-changed::add", 0, node, nullptr); + self->root_node_created = true; + } + return node; } From 5b3bc6a0e39a33428afb7f6032dcf32d446c6536 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Fri, 22 Mar 2024 11:40:30 +1300 Subject: [PATCH 2/4] Use a AT-SPI socket/plug to export the Flutter accessibility state. This will be useful when using GTK4, which doesn't support custom a11y code in GTK and will require the Flutter code to be exported in a plug. --- ci/licenses_golden/licenses_flutter | 4 ++ shell/platform/linux/BUILD.gn | 1 + shell/platform/linux/fl_socket_accessible.cc | 57 ++++++++++++++++ shell/platform/linux/fl_socket_accessible.h | 22 +++++++ shell/platform/linux/fl_view.cc | 19 ++++-- shell/platform/linux/fl_view_accessible.cc | 65 ++++++------------- shell/platform/linux/fl_view_accessible.h | 21 +++++- .../platform/linux/fl_view_accessible_test.cc | 6 +- 8 files changed, 139 insertions(+), 56 deletions(-) create mode 100644 shell/platform/linux/fl_socket_accessible.cc create mode 100644 shell/platform/linux/fl_socket_accessible.h diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index a76e2b9f70e1b..016f10fd428c4 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -42351,6 +42351,8 @@ ORIGIN: ../../../flutter/shell/platform/linux/fl_settings_plugin_test.cc + ../.. ORIGIN: ../../../flutter/shell/platform/linux/fl_settings_portal.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/fl_settings_portal.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/fl_settings_portal_test.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/linux/fl_socket_accessible.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/linux/fl_socket_accessible.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/fl_standard_message_codec.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/fl_standard_message_codec_test.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/linux/fl_standard_method_codec.cc + ../../../flutter/LICENSE @@ -45264,6 +45266,8 @@ FILE: ../../../flutter/shell/platform/linux/fl_settings_plugin_test.cc FILE: ../../../flutter/shell/platform/linux/fl_settings_portal.cc FILE: ../../../flutter/shell/platform/linux/fl_settings_portal.h FILE: ../../../flutter/shell/platform/linux/fl_settings_portal_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_socket_accessible.cc +FILE: ../../../flutter/shell/platform/linux/fl_socket_accessible.h FILE: ../../../flutter/shell/platform/linux/fl_standard_message_codec.cc FILE: ../../../flutter/shell/platform/linux/fl_standard_message_codec_test.cc FILE: ../../../flutter/shell/platform/linux/fl_standard_method_codec.cc diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index c28c7b558bfc7..8e1b8fc86e18c 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -132,6 +132,7 @@ source_set("flutter_linux_sources") { "fl_settings.cc", "fl_settings_plugin.cc", "fl_settings_portal.cc", + "fl_socket_accessible.cc", "fl_standard_message_codec.cc", "fl_standard_method_codec.cc", "fl_string_codec.cc", diff --git a/shell/platform/linux/fl_socket_accessible.cc b/shell/platform/linux/fl_socket_accessible.cc new file mode 100644 index 0000000000000..9b2d06c1320f0 --- /dev/null +++ b/shell/platform/linux/fl_socket_accessible.cc @@ -0,0 +1,57 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/linux/fl_socket_accessible.h" + +// This is a copy of GtkSocketAccessible, which requires GTK 3.24.30 + +struct _FlSocketAccessible { + GtkContainerAccessible parent; + AtkObject* accessible_socket; +}; + +G_DEFINE_TYPE(FlSocketAccessible, + fl_socket_accessible, + GTK_TYPE_CONTAINER_ACCESSIBLE) + +static AtkObject* fl_socket_accessible_ref_child(AtkObject* object, int i) { + FlSocketAccessible* self = FL_SOCKET_ACCESSIBLE(object); + return i == 0 ? ATK_OBJECT(g_object_ref(self->accessible_socket)) : nullptr; +} + +static int fl_socket_accessible_get_n_children(AtkObject* object) { + return 1; +} + +static void fl_socket_accessible_finalize(GObject* object) { + FlSocketAccessible* self = FL_SOCKET_ACCESSIBLE(object); + + g_clear_object(&self->accessible_socket); + + G_OBJECT_CLASS(fl_socket_accessible_parent_class)->finalize(object); +} + +static void fl_socket_accessible_initialize(AtkObject* object, gpointer data) { + FlSocketAccessible* self = FL_SOCKET_ACCESSIBLE(object); + + ATK_OBJECT_CLASS(fl_socket_accessible_parent_class)->initialize(object, data); + + self->accessible_socket = atk_socket_new(); +} + +static void fl_socket_accessible_class_init(FlSocketAccessibleClass* klass) { + GObjectClass* object_class = G_OBJECT_CLASS(klass); + object_class->finalize = fl_socket_accessible_finalize; + + AtkObjectClass* atk_class = ATK_OBJECT_CLASS(klass); + atk_class->initialize = fl_socket_accessible_initialize; + atk_class->get_n_children = fl_socket_accessible_get_n_children; + atk_class->ref_child = fl_socket_accessible_ref_child; +} + +static void fl_socket_accessible_init(FlSocketAccessible* self) {} + +void fl_socket_accessible_embed(FlSocketAccessible* self, gchar* id) { + atk_socket_embed(ATK_SOCKET(self->accessible_socket), id); +} diff --git a/shell/platform/linux/fl_socket_accessible.h b/shell/platform/linux/fl_socket_accessible.h new file mode 100644 index 0000000000000..a3eb6ed10d85b --- /dev/null +++ b/shell/platform/linux/fl_socket_accessible.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_SOCKET_ACCESSIBLE_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_SOCKET_ACCESSIBLE_H_ + +#include + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlSocketAccessible, + fl_socket_accessible, + FL, + SOCKET_ACCESSIBLE, + GtkContainerAccessible); + +void fl_socket_accessible_embed(FlSocketAccessible* self, gchar* id); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_SOCKET_ACCESSIBLE_H_ diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index 0d1fb4709f703..e1afb9aab8277 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -6,8 +6,11 @@ #include "flutter/shell/platform/linux/fl_view_private.h" +#include +#include #include +#include "flutter/shell/platform/linux/fl_accessible_node.h" #include "flutter/shell/platform/linux/fl_backing_store_provider.h" #include "flutter/shell/platform/linux/fl_engine_private.h" #include "flutter/shell/platform/linux/fl_key_event.h" @@ -19,6 +22,7 @@ #include "flutter/shell/platform/linux/fl_renderer_gdk.h" #include "flutter/shell/platform/linux/fl_scrolling_manager.h" #include "flutter/shell/platform/linux/fl_scrolling_view_delegate.h" +#include "flutter/shell/platform/linux/fl_socket_accessible.h" #include "flutter/shell/platform/linux/fl_text_input_plugin.h" #include "flutter/shell/platform/linux/fl_text_input_view_delegate.h" #include "flutter/shell/platform/linux/fl_view_accessible.h" @@ -64,6 +68,9 @@ struct _FlView { gulong keymap_keys_changed_cb_id; // Signal connection ID for // keymap-keys-changed gulong window_state_cb_id; // Signal connection ID for window-state-changed + + // Accessible tree from Flutter, exposed as an AtkPlug. + FlViewAccessible* view_accessible; }; enum { kPropFlutterProject = 1, kPropLast }; @@ -231,9 +238,7 @@ static void update_semantics_cb(FlEngine* engine, gpointer user_data) { FlView* self = FL_VIEW(user_data); - AtkObject* accessible = gtk_widget_get_accessible(GTK_WIDGET(self)); - fl_view_accessible_handle_update_semantics(FL_VIEW_ACCESSIBLE(accessible), - update); + fl_view_accessible_handle_update_semantics(self->view_accessible, update); } // Invoked by the engine right before the engine is restarted. @@ -586,6 +591,11 @@ static void realize_cb(FlView* self) { } handle_geometry_changed(self); + + self->view_accessible = fl_view_accessible_new(self->engine); + fl_socket_accessible_embed( + FL_SOCKET_ACCESSIBLE(gtk_widget_get_accessible(GTK_WIDGET(self))), + atk_plug_get_id(ATK_PLUG(self->view_accessible))); } static gboolean render_cb(FlView* self, GdkGLContext* context) { @@ -691,6 +701,7 @@ static void fl_view_dispose(GObject* object) { } g_clear_object(&self->mouse_cursor_plugin); g_clear_object(&self->platform_plugin); + g_clear_object(&self->view_accessible); G_OBJECT_CLASS(fl_view_parent_class)->dispose(object); } @@ -733,7 +744,7 @@ static void fl_view_class_init(FlViewClass* klass) { G_PARAM_STATIC_STRINGS))); gtk_widget_class_set_accessible_type(GTK_WIDGET_CLASS(klass), - fl_view_accessible_get_type()); + fl_socket_accessible_get_type()); } static void fl_view_init(FlView* self) { diff --git a/shell/platform/linux/fl_view_accessible.cc b/shell/platform/linux/fl_view_accessible.cc index bf470bde37ba7..75796a108463f 100644 --- a/shell/platform/linux/fl_view_accessible.cc +++ b/shell/platform/linux/fl_view_accessible.cc @@ -9,7 +9,7 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" struct _FlViewAccessible { - GtkContainerAccessible parent_instance; + AtkPlug parent_instance; FlEngine* engine; @@ -22,34 +22,15 @@ struct _FlViewAccessible { enum { kProp0, kPropEngine, kPropLast }; -G_DEFINE_TYPE(FlViewAccessible, - fl_view_accessible, - GTK_TYPE_CONTAINER_ACCESSIBLE) - -static void init_engine(FlViewAccessible* self, FlEngine* engine) { - g_assert(self->engine == nullptr); - self->engine = engine; - g_object_add_weak_pointer(G_OBJECT(self), - reinterpret_cast(&self->engine)); -} - -static FlEngine* get_engine(FlViewAccessible* self) { - if (self->engine == nullptr) { - FlView* view = FL_VIEW(gtk_accessible_get_widget(GTK_ACCESSIBLE(self))); - init_engine(self, fl_view_get_engine(view)); - } - return self->engine; -} +G_DEFINE_TYPE(FlViewAccessible, fl_view_accessible, ATK_TYPE_PLUG) static FlAccessibleNode* create_node(FlViewAccessible* self, FlutterSemanticsNode2* semantics) { - FlEngine* engine = get_engine(self); - if (semantics->flags & kFlutterSemanticsFlagIsTextField) { - return fl_accessible_text_field_new(engine, semantics->id); + return fl_accessible_text_field_new(self->engine, semantics->id); } - return fl_accessible_node_new(engine, semantics->id); + return fl_accessible_node_new(self->engine, semantics->id); } static FlAccessibleNode* lookup_node(FlViewAccessible* self, int32_t id) { @@ -112,20 +93,11 @@ static AtkRole fl_view_accessible_get_role(AtkObject* accessible) { return ATK_ROLE_PANEL; } -// Implements GObject::set_property -static void fl_view_accessible_set_property(GObject* object, - guint prop_id, - const GValue* value, - GParamSpec* pspec) { - FlViewAccessible* self = FL_VIEW_ACCESSIBLE(object); - switch (prop_id) { - case kPropEngine: - init_engine(self, FL_ENGINE(g_value_get_object(value))); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); - break; - } +// Implements AtkObject::ref_state_set +static AtkStateSet* fl_view_accessible_ref_state_set(AtkObject* accessible) { + FlViewAccessible* self = FL_VIEW_ACCESSIBLE(accessible); + FlAccessibleNode* node = lookup_node(self, 0); + return node != nullptr ? atk_object_ref_state_set(ATK_OBJECT(node)) : nullptr; } static void fl_view_accessible_dispose(GObject* object) { @@ -146,16 +118,9 @@ static void fl_view_accessible_class_init(FlViewAccessibleClass* klass) { ATK_OBJECT_CLASS(klass)->get_n_children = fl_view_accessible_get_n_children; ATK_OBJECT_CLASS(klass)->ref_child = fl_view_accessible_ref_child; ATK_OBJECT_CLASS(klass)->get_role = fl_view_accessible_get_role; + ATK_OBJECT_CLASS(klass)->ref_state_set = fl_view_accessible_ref_state_set; G_OBJECT_CLASS(klass)->dispose = fl_view_accessible_dispose; - G_OBJECT_CLASS(klass)->set_property = fl_view_accessible_set_property; - - g_object_class_install_property( - G_OBJECT_CLASS(klass), kPropEngine, - g_param_spec_object( - "engine", "engine", "Flutter engine", fl_engine_get_type(), - static_cast(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_STATIC_STRINGS))); } static void fl_view_accessible_init(FlViewAccessible* self) { @@ -163,13 +128,21 @@ static void fl_view_accessible_init(FlViewAccessible* self) { g_direct_hash, g_direct_equal, nullptr, g_object_unref); } +FlViewAccessible* fl_view_accessible_new(FlEngine* engine) { + FlViewAccessible* self = + FL_VIEW_ACCESSIBLE(g_object_new(fl_view_accessible_get_type(), nullptr)); + self->engine = engine; + g_object_add_weak_pointer(G_OBJECT(self), + reinterpret_cast(&self->engine)); + return self; +} + void fl_view_accessible_handle_update_semantics( FlViewAccessible* self, const FlutterSemanticsUpdate2* update) { g_autoptr(GHashTable) pending_children = g_hash_table_new_full(g_direct_hash, g_direct_equal, nullptr, reinterpret_cast(fl_value_unref)); - for (size_t i = 0; i < update->node_count; i++) { FlutterSemanticsNode2* node = update->nodes[i]; FlAccessibleNode* atk_node = get_node(self, node); diff --git a/shell/platform/linux/fl_view_accessible.h b/shell/platform/linux/fl_view_accessible.h index 0c45d7b2b85a6..220338d274bb1 100644 --- a/shell/platform/linux/fl_view_accessible.h +++ b/shell/platform/linux/fl_view_accessible.h @@ -9,17 +9,24 @@ #error "Only can be included directly." #endif -#include +#include #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" G_BEGIN_DECLS +// ATK g_autoptr macros weren't added until 2.37. Add them manually. +// https://gitlab.gnome.org/GNOME/atk/-/issues/10 +#if !ATK_CHECK_VERSION(2, 37, 0) +G_DEFINE_AUTOPTR_CLEANUP_FUNC(AtkPlug, g_object_unref) +#endif + G_DECLARE_FINAL_TYPE(FlViewAccessible, fl_view_accessible, FL, VIEW_ACCESSIBLE, - GtkContainerAccessible) + AtkPlug) /** * FlViewAccessible: @@ -28,6 +35,16 @@ G_DECLARE_FINAL_TYPE(FlViewAccessible, * #FlView. */ +/** + * fl_view_accessible_new: + * + * Creates a new accessibility object that exposes Flutter accessibility + * information to ATK. + * + * Returns: a new #FlViewAccessible. + */ +FlViewAccessible* fl_view_accessible_new(FlEngine* engine); + /** * fl_view_accessible_handle_update_semantics: * @accessible: an #FlViewAccessible. diff --git a/shell/platform/linux/fl_view_accessible_test.cc b/shell/platform/linux/fl_view_accessible_test.cc index be4495e7ebf2c..0b6d17ab49a21 100644 --- a/shell/platform/linux/fl_view_accessible_test.cc +++ b/shell/platform/linux/fl_view_accessible_test.cc @@ -12,8 +12,7 @@ TEST(FlViewAccessibleTest, BuildTree) { g_autoptr(FlEngine) engine = make_mock_engine(); - g_autoptr(FlViewAccessible) accessible = FL_VIEW_ACCESSIBLE( - g_object_new(fl_view_accessible_get_type(), "engine", engine, nullptr)); + g_autoptr(FlViewAccessible) accessible = fl_view_accessible_new(engine); int32_t children[] = {111, 222}; FlutterSemanticsNode2 root_node = { @@ -49,8 +48,7 @@ TEST(FlViewAccessibleTest, BuildTree) { TEST(FlViewAccessibleTest, AddRemoveChildren) { g_autoptr(FlEngine) engine = make_mock_engine(); - g_autoptr(FlViewAccessible) accessible = FL_VIEW_ACCESSIBLE( - g_object_new(fl_view_accessible_get_type(), "engine", engine, nullptr)); + g_autoptr(FlViewAccessible) accessible = fl_view_accessible_new(engine); FlutterSemanticsNode2 root_node = { .id = 0, From fdf402173f419680a3df01f6ad07a074422f5494 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Mon, 29 Apr 2024 08:51:29 +1200 Subject: [PATCH 3/4] Add a constant for the root semantics node ID --- shell/platform/linux/fl_view_accessible.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shell/platform/linux/fl_view_accessible.cc b/shell/platform/linux/fl_view_accessible.cc index 75796a108463f..77ba12aec1282 100644 --- a/shell/platform/linux/fl_view_accessible.cc +++ b/shell/platform/linux/fl_view_accessible.cc @@ -8,6 +8,8 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" +static constexpr int32_t kRootSemanticsNodeId = 0; + struct _FlViewAccessible { AtkPlug parent_instance; @@ -48,7 +50,7 @@ static FlAccessibleNode* get_node(FlViewAccessible* self, } node = create_node(self, semantics); - if (semantics->id == 0) { + if (semantics->id == kRootSemanticsNodeId) { fl_accessible_node_set_parent(node, ATK_OBJECT(self), 0); } g_hash_table_insert(self->semantics_nodes_by_id, @@ -56,7 +58,7 @@ static FlAccessibleNode* get_node(FlViewAccessible* self, reinterpret_cast(node)); // Update when root node is created. - if (!self->root_node_created && semantics->id == 0) { + if (!self->root_node_created && semantics->id == kRootSemanticsNodeId) { g_signal_emit_by_name(self, "children-changed::add", 0, node, nullptr); self->root_node_created = true; } From dce81f403fb209819bf93dd8f3c75c4bb5759e13 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Mon, 29 Apr 2024 08:51:59 +1200 Subject: [PATCH 4/4] Add whitespace between C/C++ includes --- shell/platform/linux/fl_view.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index e1afb9aab8277..8dc057e6fa027 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -8,6 +8,7 @@ #include #include + #include #include "flutter/shell/platform/linux/fl_accessible_node.h"