Use a AT-SPI socket/plug to export the Flutter accessibility state. (#52355)
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.
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 45ac54a..2daa27b 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -42442,6 +42442,8 @@
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
@@ -45348,6 +45350,8 @@
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 c28c7b5..8e1b8fc 100644
--- a/shell/platform/linux/BUILD.gn
+++ b/shell/platform/linux/BUILD.gn
@@ -132,6 +132,7 @@
"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 0000000..9b2d06c
--- /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 0000000..a3eb6ed
--- /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 <gtk/gtk-a11y.h>
+
+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 0d1fb47..8dc057e 100644
--- a/shell/platform/linux/fl_view.cc
+++ b/shell/platform/linux/fl_view.cc
@@ -6,8 +6,12 @@
#include "flutter/shell/platform/linux/fl_view_private.h"
+#include <atk/atk.h>
+#include <gtk/gtk-a11y.h>
+
#include <cstring>
+#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 +23,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 +69,9 @@
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 +239,7 @@
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 +592,11 @@
}
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 +702,7 @@
}
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 +745,7 @@
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 7ddefe0..77ba12a 100644
--- a/shell/platform/linux/fl_view_accessible.cc
+++ b/shell/platform/linux/fl_view_accessible.cc
@@ -8,45 +8,31 @@
#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 {
- GtkContainerAccessible parent_instance;
+ AtkPlug parent_instance;
FlEngine* engine;
// 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 };
-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<gpointer*>(&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) {
@@ -64,14 +50,19 @@
}
node = create_node(self, semantics);
- if (semantics->id == 0) {
+ if (semantics->id == kRootSemanticsNodeId) {
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<gpointer>(node));
+ // Update when root node is created.
+ 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;
+ }
+
return node;
}
@@ -104,20 +95,11 @@
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) {
@@ -138,16 +120,9 @@
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<GParamFlags>(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
- G_PARAM_STATIC_STRINGS)));
}
static void fl_view_accessible_init(FlViewAccessible* self) {
@@ -155,13 +130,21 @@
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<gpointer*>(&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<GDestroyNotify>(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 0c45d7b..220338d 100644
--- a/shell/platform/linux/fl_view_accessible.h
+++ b/shell/platform/linux/fl_view_accessible.h
@@ -9,17 +9,24 @@
#error "Only <flutter_linux/flutter_linux.h> can be included directly."
#endif
-#include <gtk/gtk-a11y.h>
+#include <atk/atk.h>
#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:
@@ -29,6 +36,16 @@
*/
/**
+ * 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.
* @update: semantic update information.
diff --git a/shell/platform/linux/fl_view_accessible_test.cc b/shell/platform/linux/fl_view_accessible_test.cc
index be4495e..0b6d17a 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, 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,