diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index bc681ffb3ba48..1b55a31d56689 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1289,6 +1289,10 @@ FILE: ../../../flutter/shell/platform/glfw/text_input_plugin.h FILE: ../../../flutter/shell/platform/linux/egl_utils.cc FILE: ../../../flutter/shell/platform/linux/egl_utils.h FILE: ../../../flutter/shell/platform/linux/egl_utils_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_accessibility_plugin.cc +FILE: ../../../flutter/shell/platform/linux/fl_accessibility_plugin.h +FILE: ../../../flutter/shell/platform/linux/fl_accessibile_node.cc +FILE: ../../../flutter/shell/platform/linux/fl_accessibile_node.h FILE: ../../../flutter/shell/platform/linux/fl_basic_message_channel.cc FILE: ../../../flutter/shell/platform/linux/fl_basic_message_channel_test.cc FILE: ../../../flutter/shell/platform/linux/fl_binary_codec.cc @@ -1349,6 +1353,8 @@ FILE: ../../../flutter/shell/platform/linux/fl_text_input_plugin.h FILE: ../../../flutter/shell/platform/linux/fl_value.cc FILE: ../../../flutter/shell/platform/linux/fl_value_test.cc FILE: ../../../flutter/shell/platform/linux/fl_view.cc +FILE: ../../../flutter/shell/platform/linux/fl_view_accessible.cc +FILE: ../../../flutter/shell/platform/linux/fl_view_accessible.h FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_basic_message_channel.h FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_binary_codec.h FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index 6dcc1779f1930..09c3f97a0d642 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -88,6 +88,8 @@ source_set("flutter_linux_sources") { sources = [ "egl_utils.cc", + "fl_accessibility_plugin.cc", + "fl_accessible_node.cc", "fl_basic_message_channel.cc", "fl_binary_codec.cc", "fl_binary_messenger.cc", @@ -116,6 +118,7 @@ source_set("flutter_linux_sources") { "fl_text_input_plugin.cc", "fl_value.cc", "fl_view.cc", + "fl_view_accessible.cc", ] # Set flag to stop headers being directly included (library users should not do this) diff --git a/shell/platform/linux/fl_accessibility_plugin.cc b/shell/platform/linux/fl_accessibility_plugin.cc new file mode 100644 index 0000000000000..33ba2971b3d1e --- /dev/null +++ b/shell/platform/linux/fl_accessibility_plugin.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_accessibility_plugin.h" +#include "flutter/shell/platform/linux/fl_view_accessible.h" + +struct _FlAccessibilityPlugin { + GObject parent_instance; + + FlView* view; +}; + +G_DEFINE_TYPE(FlAccessibilityPlugin, fl_accessibility_plugin, G_TYPE_OBJECT) + +static void view_weak_notify_cb(gpointer user_data, GObject* object) { + FlAccessibilityPlugin* self = FL_ACCESSIBILITY_PLUGIN(object); + self->view = nullptr; +} + +static void fl_accessibility_plugin_dispose(GObject* object) { + FlAccessibilityPlugin* self = FL_ACCESSIBILITY_PLUGIN(object); + + if (self->view != nullptr) { + g_object_weak_unref(G_OBJECT(self->view), view_weak_notify_cb, self); + self->view = nullptr; + } + + G_OBJECT_CLASS(fl_accessibility_plugin_parent_class)->dispose(object); +} + +static void fl_accessibility_plugin_class_init( + FlAccessibilityPluginClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_accessibility_plugin_dispose; +} + +static void fl_accessibility_plugin_init(FlAccessibilityPlugin* self) {} + +FlAccessibilityPlugin* fl_accessibility_plugin_new(FlView* view) { + FlAccessibilityPlugin* self = FL_ACCESSIBILITY_PLUGIN( + g_object_new(fl_accessibility_plugin_get_type(), nullptr)); + + self->view = view; + g_object_weak_ref(G_OBJECT(view), view_weak_notify_cb, self); + + return self; +} + +void fl_accessibility_plugin_handle_update_semantics_node( + FlAccessibilityPlugin* self, + const FlutterSemanticsNode* node) { + if (self->view != nullptr) { + AtkObject* accessible = gtk_widget_get_accessible(GTK_WIDGET(self->view)); + fl_view_accessible_handle_update_semantics_node( + FL_VIEW_ACCESSIBLE(accessible), node); + } +} diff --git a/shell/platform/linux/fl_accessibility_plugin.h b/shell/platform/linux/fl_accessibility_plugin.h new file mode 100644 index 0000000000000..40b9333753b75 --- /dev/null +++ b/shell/platform/linux/fl_accessibility_plugin.h @@ -0,0 +1,50 @@ +// 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_ACCESSIBILITY_PLUGIN_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_ACCESSIBILITY_PLUGIN_H_ + +#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" + +#include "flutter/shell/platform/embedder/embedder.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlAccessibilityPlugin, + fl_accessibility_plugin, + FL, + ACCESSIBILITY_PLUGIN, + GObject); + +/** + * FlAccessibilityPlugin: + * + * #FlAccessibilityPlugin is a plugin that handles semantic node updates and + * converts them to ATK events. + */ + +/** + * fl_accessibility_plugin_new: + * @view: an #FlView to export accessibility information to. + * + * Creates a new plugin handles semantic node updates. + * + * Returns: a new #FlAccessibilityPlugin. + */ +FlAccessibilityPlugin* fl_accessibility_plugin_new(FlView* view); + +/** + * fl_accessibility_plugin_handle_update_semantics_node: + * @plugin: an #FlAccessibilityPlugin. + * @node: semantic node information. + * + * Handle a semantics node update. + */ +void fl_accessibility_plugin_handle_update_semantics_node( + FlAccessibilityPlugin* plugin, + const FlutterSemanticsNode* node); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_ACCESSIBILITY_PLUGIN_H_ diff --git a/shell/platform/linux/fl_accessible_node.cc b/shell/platform/linux/fl_accessible_node.cc new file mode 100644 index 0000000000000..103e2646c6815 --- /dev/null +++ b/shell/platform/linux/fl_accessible_node.cc @@ -0,0 +1,357 @@ +// 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_accessible_node.h" +#include "flutter/shell/platform/linux/fl_engine_private.h" + +// Maps Flutter semantics flags to ATK flags. +static struct { + AtkStateType state; + FlutterSemanticsFlag flag; + gboolean invert; +} flag_mapping[] = { + {ATK_STATE_SHOWING, kFlutterSemanticsFlagIsObscured, TRUE}, + {ATK_STATE_VISIBLE, kFlutterSemanticsFlagIsHidden, TRUE}, + {ATK_STATE_CHECKABLE, kFlutterSemanticsFlagHasCheckedState, FALSE}, + {ATK_STATE_FOCUSABLE, kFlutterSemanticsFlagIsFocusable, FALSE}, + {ATK_STATE_FOCUSED, kFlutterSemanticsFlagIsFocused, FALSE}, + {ATK_STATE_CHECKED, kFlutterSemanticsFlagIsChecked, FALSE}, + {ATK_STATE_SELECTED, kFlutterSemanticsFlagIsSelected, FALSE}, + {ATK_STATE_ENABLED, kFlutterSemanticsFlagIsEnabled, FALSE}, + {ATK_STATE_READ_ONLY, kFlutterSemanticsFlagIsReadOnly, FALSE}, + {ATK_STATE_INVALID, static_cast(0), FALSE}, +}; + +// Maps Flutter semantics actions to ATK actions. +typedef struct { + FlutterSemanticsAction action; + const gchar* name; +} ActionData; +static ActionData action_mapping[] = { + {kFlutterSemanticsActionTap, "Tap"}, + {kFlutterSemanticsActionLongPress, "LongPress"}, + {kFlutterSemanticsActionScrollLeft, "ScrollLeft"}, + {kFlutterSemanticsActionScrollRight, "ScrollRight"}, + {kFlutterSemanticsActionScrollUp, "ScrollUp"}, + {kFlutterSemanticsActionScrollDown, "ScrollDown"}, + {kFlutterSemanticsActionIncrease, "Increase"}, + {kFlutterSemanticsActionDecrease, "Decrease"}, + {kFlutterSemanticsActionShowOnScreen, "ShowOnScreen"}, + {kFlutterSemanticsActionMoveCursorForwardByCharacter, + "MoveCursorForwardByCharacter"}, + {kFlutterSemanticsActionMoveCursorBackwardByCharacter, + "MoveCursorBackwardByCharacter"}, + {kFlutterSemanticsActionSetSelection, "SetSelection"}, + {kFlutterSemanticsActionCopy, "Copy"}, + {kFlutterSemanticsActionCut, "Cut"}, + {kFlutterSemanticsActionPaste, "Paste"}, + {kFlutterSemanticsActionDidGainAccessibilityFocus, + "DidGainAccessibilityFocus"}, + {kFlutterSemanticsActionDidLoseAccessibilityFocus, + "DidLoseAccessibilityFocus"}, + {kFlutterSemanticsActionCustomAction, "CustomAction"}, + {kFlutterSemanticsActionDismiss, "Dismiss"}, + {kFlutterSemanticsActionMoveCursorForwardByWord, "MoveCursorForwardByWord"}, + {kFlutterSemanticsActionMoveCursorBackwardByWord, + "MoveCursorBackwardByWord"}, + {static_cast(0), nullptr}}; + +struct _FlAccessibleNode { + AtkObject parent_instance; + + FlEngine* engine; + AtkObject* parent; + int32_t id; + gchar* name; + gint x, y, width, height; + GPtrArray* actions; + gsize actions_length; + GPtrArray* children; + FlutterSemanticsFlag flags; +}; + +static void fl_accessible_node_component_interface_init( + AtkComponentIface* iface); +static void fl_accessible_node_action_interface_init(AtkActionIface* iface); +static void fl_accessible_node_text_interface_init(AtkTextIface* iface); + +G_DEFINE_TYPE_WITH_CODE( + FlAccessibleNode, + fl_accessible_node, + ATK_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(ATK_TYPE_COMPONENT, + fl_accessible_node_component_interface_init) + G_IMPLEMENT_INTERFACE(ATK_TYPE_ACTION, + fl_accessible_node_action_interface_init) + G_IMPLEMENT_INTERFACE(ATK_TYPE_TEXT, + fl_accessible_node_text_interface_init)) + +// Returns TRUE if [flag] has changed between [oldFlags] and [flags]. +static gboolean flag_is_changed(FlutterSemanticsFlag oldFlags, + FlutterSemanticsFlag flags, + FlutterSemanticsFlag flag) { + return (oldFlags & flag) != (flags & flag); +} + +// Returns TRUE if [flag] is set in [flags]. +static gboolean has_flag(FlutterSemanticsFlag flags, + FlutterSemanticsFlag flag) { + return (flags & flag) != 0; +} + +// Returns TRUE if [action] is set in [actions]. +static gboolean has_action(FlutterSemanticsAction actions, + FlutterSemanticsAction action) { + return (actions & action) != 0; +} + +static void fl_accessible_node_dispose(GObject* object) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(object); + + g_clear_object(&self->engine); + g_clear_pointer(&self->name, g_free); + g_clear_pointer(&self->actions, g_ptr_array_unref); + g_clear_pointer(&self->children, g_ptr_array_unref); + + G_OBJECT_CLASS(fl_accessible_node_parent_class)->dispose(object); +} + +// Implements AtkObject::get_name. +static const gchar* fl_accessible_node_get_name(AtkObject* accessible) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); + return self->name; +} + +// Implements AtkObject::get_parent. +static AtkObject* fl_accessible_node_get_parent(AtkObject* accessible) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); + return self->parent; +} + +// Implements AtkObject::get_n_children. +static gint fl_accessible_node_get_n_children(AtkObject* accessible) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); + return self->children->len; +} + +// Implements AtkObject::ref_child. +static AtkObject* fl_accessible_node_ref_child(AtkObject* accessible, gint i) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); + + if (i < 0 || static_cast(i) >= self->children->len) { + return nullptr; + } + + return ATK_OBJECT(g_object_ref(g_ptr_array_index(self->children, i))); +} + +// Implements AtkObject::get_role. +static AtkRole fl_accessible_node_get_role(AtkObject* accessible) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); + if ((self->flags & kFlutterSemanticsFlagIsButton) != 0) { + return ATK_ROLE_PUSH_BUTTON; + } + if ((self->flags & kFlutterSemanticsFlagIsTextField) != 0) { + return ATK_ROLE_TEXT; + } + if ((self->flags & kFlutterSemanticsFlagIsHeader) != 0) { + return ATK_ROLE_HEADER; + } + if ((self->flags & kFlutterSemanticsFlagIsLink) != 0) { + return ATK_ROLE_LINK; + } + if ((self->flags & kFlutterSemanticsFlagIsImage) != 0) { + return ATK_ROLE_IMAGE; + } + + return ATK_ROLE_FRAME; +} + +// Implements AtkObject::ref_state_set. +static AtkStateSet* fl_accessible_node_ref_state_set(AtkObject* accessible) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); + + AtkStateSet* state_set = atk_state_set_new(); + + for (int i = 0; flag_mapping[i].state != ATK_STATE_INVALID; i++) { + gboolean enabled = has_flag(self->flags, flag_mapping[i].flag); + if (flag_mapping[i].invert) { + enabled = !enabled; + } + if (enabled) { + atk_state_set_add_state(state_set, flag_mapping[i].state); + } + } + + return state_set; +} + +// Implements AtkComponent::get_extents. +static void fl_accessible_node_get_extents(AtkComponent* component, + gint* x, + gint* y, + gint* width, + gint* height, + AtkCoordType coord_type) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(component); + + *x = 0; + *y = 0; + if (self->parent != nullptr) { + atk_component_get_extents(ATK_COMPONENT(self->parent), x, y, nullptr, + nullptr, coord_type); + } + + *x += self->x; + *y += self->y; + *width = self->width; + *height = self->height; +} + +// Implements AtkComponent::get_layer. +static AtkLayer fl_accessible_node_get_layer(AtkComponent* component) { + return ATK_LAYER_WIDGET; +} + +// Implements AtkAction::do_action. +static gboolean fl_accessible_node_do_action(AtkAction* action, gint i) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(action); + + ActionData* data = + static_cast(g_ptr_array_index(self->actions, i)); + fl_engine_dispatch_semantics_action(self->engine, self->id, data->action, + nullptr); + return TRUE; +} + +// Implements AtkAction::get_n_actions. +static gint fl_accessible_node_get_n_actions(AtkAction* action) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(action); + return self->actions->len; +} + +// Implements AtkAction::get_name. +static const gchar* fl_accessible_node_get_name(AtkAction* action, gint i) { + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(action); + + ActionData* data = + static_cast(g_ptr_array_index(self->actions, i)); + return data->name; +} + +// Implements AtkText::get_text. +static gchar* fl_accessible_node_get_text(AtkText* text, + gint start_offset, + gint end_offset) { + return nullptr; +} + +static void fl_accessible_node_class_init(FlAccessibleNodeClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_accessible_node_dispose; + ATK_OBJECT_CLASS(klass)->get_name = fl_accessible_node_get_name; + ATK_OBJECT_CLASS(klass)->get_parent = fl_accessible_node_get_parent; + ATK_OBJECT_CLASS(klass)->get_n_children = fl_accessible_node_get_n_children; + ATK_OBJECT_CLASS(klass)->ref_child = fl_accessible_node_ref_child; + ATK_OBJECT_CLASS(klass)->get_role = fl_accessible_node_get_role; + ATK_OBJECT_CLASS(klass)->ref_state_set = fl_accessible_node_ref_state_set; +} + +static void fl_accessible_node_component_interface_init( + AtkComponentIface* iface) { + iface->get_extents = fl_accessible_node_get_extents; + iface->get_layer = fl_accessible_node_get_layer; +} + +static void fl_accessible_node_action_interface_init(AtkActionIface* iface) { + iface->do_action = fl_accessible_node_do_action; + iface->get_n_actions = fl_accessible_node_get_n_actions; + iface->get_name = fl_accessible_node_get_name; +} + +static void fl_accessible_node_text_interface_init(AtkTextIface* iface) { + iface->get_text = fl_accessible_node_get_text; +} + +static void fl_accessible_node_init(FlAccessibleNode* self) { + self->actions = g_ptr_array_new(); + self->children = g_ptr_array_new_with_free_func(g_object_unref); +} + +FlAccessibleNode* fl_accessible_node_new(FlEngine* engine, int32_t id) { + FlAccessibleNode* self = + FL_ACCESSIBLE_NODE(g_object_new(fl_accessible_node_get_type(), nullptr)); + self->engine = FL_ENGINE(g_object_ref(engine)); // FIXME: Weak ref? + self->id = id; + return self; +} + +void fl_accessible_node_set_parent(FlAccessibleNode* self, AtkObject* parent) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + self->parent = parent; // FIXME: Weak ref? +} + +void fl_accessible_node_set_children(FlAccessibleNode* self, + GPtrArray* children) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + + // FIXME: Do the delta + g_ptr_array_remove_range(self->children, 0, self->children->len); + for (guint i = 0; i < children->len; i++) { + AtkObject* object = ATK_OBJECT(g_ptr_array_index(children, i)); + g_ptr_array_add(self->children, g_object_ref(object)); + g_signal_emit_by_name(self, "children-changed::add", i, object, nullptr); + } +} + +void fl_accessible_node_set_name(FlAccessibleNode* self, const gchar* name) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + g_free(self->name); + self->name = g_strdup(name); +} + +void fl_accessible_node_set_extents(FlAccessibleNode* self, + gint x, + gint y, + gint width, + gint height) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + self->x = x; + self->y = y; + self->width = width; + self->height = height; +} + +void fl_accessible_node_set_flags(FlAccessibleNode* self, + FlutterSemanticsFlag flags) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + + FlutterSemanticsFlag oldFlags = self->flags; + self->flags = flags; + + for (int i = 0; flag_mapping[i].state != ATK_STATE_INVALID; i++) { + if (flag_is_changed(oldFlags, flags, flag_mapping[i].flag)) { + gboolean enabled = has_flag(flags, flag_mapping[i].flag); + if (flag_mapping[i].invert) { + enabled = !enabled; + } + + atk_object_notify_state_change(ATK_OBJECT(self), flag_mapping[i].state, + enabled); + } + } +} + +void fl_accessible_node_set_actions(FlAccessibleNode* self, + FlutterSemanticsAction actions) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + + // FIXME(robert-ancell): Just detect changes. + g_ptr_array_remove_range(self->actions, 0, + self->actions->len); // FIXME: Race? + for (int i = 0; action_mapping[i].name != nullptr; i++) { + if (has_action(actions, action_mapping[i].action)) { + g_ptr_array_add(self->actions, &action_mapping[i]); + } + } +} diff --git a/shell/platform/linux/fl_accessible_node.h b/shell/platform/linux/fl_accessible_node.h new file mode 100644 index 0000000000000..61b108fc31738 --- /dev/null +++ b/shell/platform/linux/fl_accessible_node.h @@ -0,0 +1,116 @@ +// 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_ACCESSIBLE_NODE_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_ACCESSIBLE_NODE_H_ + +#include + +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" + +G_BEGIN_DECLS + +GType fl_accessible_node_get_type(void); + +// FIXME: Why not bolierplate? +#define FL_ACCESSIBLE_NODE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), fl_accessible_node_get_type(), \ + FlAccessibleNode)) +#define FL_IS_ACCESSIBLE_NODE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), fl_accessible_node_get_type())) + +typedef struct _FlAccessibleNode FlAccessibleNode; +typedef struct _FlAccessibleNodeClass FlAccessibleNodeClass; + +struct _FlAccessibleNodeClass { + AtkObjectClass parent_instance; +}; + +/** + * FlAccessibleNode: + * + * #FlAccessibleNode is an object that exposes a Flutter accessibility node to + * ATK. + */ + +/** + * fl_accessible_node_new: + * @engine: the #FlEngine this node came from. + * @id: the semantics node ID this object represents. + * + * Creates a new accessibility object that exposes Flutter accessibility + * information to ATK. + * + * Returns: a new #FlAccessibleNode. + */ +FlAccessibleNode* fl_accessible_node_new(FlEngine* engine, int32_t id); + +/** + * fl_accessible_node_new: + * @node: an #FlAccessibleNode. + * @parent: an #AtkObject. + * + * Sets the parent of this node. The parent can be changed at any time. + */ +void fl_accessible_node_set_parent(FlAccessibleNode* node, AtkObject* parent); + +/** + * fl_accessible_node_new: + * @node: an #FlAccessibleNode. + * @children: (transfer none) (element-type AtkObject): a list of #AtkObject. + * + * Sets the children of this node. The children can be changed at any time. + */ +void fl_accessible_node_set_children(FlAccessibleNode* node, + GPtrArray* children); + +/** + * fl_accessible_node_set_name: + * @node: an #FlAccessibleNode. + * @name: a node name. + * + * Sets the name of this node as reported to the a11y consumer. + */ +void fl_accessible_node_set_name(FlAccessibleNode* node, const gchar* name); + +/** + * fl_accessible_node_set_extents: + * @node: an #FlAccessibleNode. + * @x: x co-ordinate of this node relative to its parent. + * @y: y co-ordinate of this node relative to its parent. + * @width: width of this node in pixels. + * @height: height of this node in pixels. + * + * Sets the position and size of this node. + */ +void fl_accessible_node_set_extents(FlAccessibleNode* node, + gint x, + gint y, + gint width, + gint height); + +/** + * fl_accessible_node_set_flags: + * @node: an #FlAccessibleNode. + * @flags: the flags for this node. + * + * Sets the flags for this node. + */ +void fl_accessible_node_set_flags(FlAccessibleNode* node, + FlutterSemanticsFlag flags); + +/** + * fl_accessible_node_set_actions: + * @node: an #FlAccessibleNode. + * @actions: the actions this node can perform. + * + * Sets the actions that this node can perform. + */ +void fl_accessible_node_set_actions(FlAccessibleNode* node, + FlutterSemanticsAction actions); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_ACCESSIBLE_NODE_H_ diff --git a/shell/platform/linux/fl_engine.cc b/shell/platform/linux/fl_engine.cc index a5a05eb759095..cfead36939e55 100644 --- a/shell/platform/linux/fl_engine.cc +++ b/shell/platform/linux/fl_engine.cc @@ -38,6 +38,11 @@ struct _FlEngine { FlEnginePlatformMessageHandler platform_message_handler; gpointer platform_message_handler_data; GDestroyNotify platform_message_handler_destroy_notify; + + // Function to call when a semantic node is received. + FlEngineUpdateSemanticsNodeHandler update_semantics_node_handler; + gpointer update_semantics_node_handler_data; + GDestroyNotify update_semantics_node_handler_destroy_notify; }; G_DEFINE_QUARK(fl_engine_error_quark, fl_engine_error) @@ -275,6 +280,17 @@ static void fl_engine_platform_message_cb(const FlutterPlatformMessage* message, } } +// Called when a semantic node update is received from the engine. +static void fl_engine_update_semantics_node_cb(const FlutterSemanticsNode* node, + void* user_data) { + FlEngine* self = FL_ENGINE(user_data); + + if (self->update_semantics_node_handler != nullptr) { + self->update_semantics_node_handler( + self, node, self->update_semantics_node_handler_data); + } +} + // Called when a response to a sent platform message is received from the // engine. static void fl_engine_platform_message_response_cb(const uint8_t* data, @@ -323,6 +339,13 @@ static void fl_engine_dispose(GObject* object) { self->platform_message_handler_data = nullptr; self->platform_message_handler_destroy_notify = nullptr; + if (self->update_semantics_node_handler_destroy_notify) { + self->update_semantics_node_handler_destroy_notify( + self->update_semantics_node_handler_data); + } + self->update_semantics_node_handler_data = nullptr; + self->update_semantics_node_handler_destroy_notify = nullptr; + G_OBJECT_CLASS(fl_engine_parent_class)->dispose(object); } @@ -397,6 +420,7 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { args.command_line_argv = reinterpret_cast(command_line_args->pdata); args.platform_message_callback = fl_engine_platform_message_cb; + args.update_semantics_node_callback = fl_engine_update_semantics_node_cb; args.custom_task_runners = &custom_task_runners; args.shutdown_dart_vm_when_done = true; args.dart_entrypoint_argc = @@ -434,6 +458,10 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { setup_locales(self); + result = self->embedder_api.UpdateSemanticsEnabled(self->engine, TRUE); + if (result != kSuccess) + g_warning("Failed to enable accessibility features on Flutter engine"); + return TRUE; } @@ -459,6 +487,24 @@ void fl_engine_set_platform_message_handler( self->platform_message_handler_destroy_notify = destroy_notify; } +void fl_engine_set_update_semantics_node_handler( + FlEngine* self, + FlEngineUpdateSemanticsNodeHandler handler, + gpointer user_data, + GDestroyNotify destroy_notify) { + g_return_if_fail(FL_IS_ENGINE(self)); + g_return_if_fail(handler != nullptr); + + if (self->update_semantics_node_handler_destroy_notify) { + self->update_semantics_node_handler_destroy_notify( + self->update_semantics_node_handler_data); + } + + self->update_semantics_node_handler = handler; + self->update_semantics_node_handler_data = user_data; + self->update_semantics_node_handler_destroy_notify = destroy_notify; +} + gboolean fl_engine_send_platform_message_response( FlEngine* self, const FlutterPlatformMessageResponseHandle* handle, @@ -607,6 +653,26 @@ void fl_engine_send_mouse_pointer_event(FlEngine* self, self->embedder_api.SendPointerEvent(self->engine, &fl_event, 1); } +void fl_engine_dispatch_semantics_action(FlEngine* self, + uint64_t id, + FlutterSemanticsAction action, + GBytes* data) { + g_return_if_fail(FL_IS_ENGINE(self)); + + if (self->engine == nullptr) { + return; + } + + const uint8_t* action_data = nullptr; + size_t action_data_length = 0; + if (data != nullptr) { + g_bytes_get_data(data, &action_data_length); + } + + self->embedder_api.DispatchSemanticsAction(self->engine, id, action, + action_data, action_data_length); +} + G_MODULE_EXPORT FlBinaryMessenger* fl_engine_get_binary_messenger( FlEngine* self) { g_return_val_if_fail(FL_IS_ENGINE(self), nullptr); diff --git a/shell/platform/linux/fl_engine_private.h b/shell/platform/linux/fl_engine_private.h index ebace0979a418..23f186921fca6 100644 --- a/shell/platform/linux/fl_engine_private.h +++ b/shell/platform/linux/fl_engine_private.h @@ -44,6 +44,19 @@ typedef gboolean (*FlEnginePlatformMessageHandler)( const FlutterPlatformMessageResponseHandle* response_handle, gpointer user_data); +/** + * FlEngineUpdateSemanticsNodeHandler: + * @engine: an #FlEngine. + * @node: semantic node information. + * @user_data: (closure): data provided when registering this handler. + * + * Function called when semantics node updates are received. + */ +typedef void (*FlEngineUpdateSemanticsNodeHandler)( + FlEngine* engine, + const FlutterSemanticsNode* node, + gpointer user_data); + /** * fl_engine_new: * @project: an #FlDartProject. @@ -85,6 +98,22 @@ void fl_engine_set_platform_message_handler( gpointer user_data, GDestroyNotify destroy_notify); +/** + * fl_engine_set_update_semantics_node_handler: + * @engine: an #FlEngine. + * @handler: function to call when a semantics node update is received. + * @user_data: (closure): user data to pass to @handler. + * @destroy_notify: (allow-none): a function which gets called to free + * @user_data, or %NULL. + * + * Registers the function called when a semantics node update is reveived. + */ +void fl_engine_set_update_semantics_node_handler( + FlEngine* engine, + FlEngineUpdateSemanticsNodeHandler handler, + gpointer user_data, + GDestroyNotify destroy_notify); + /** * fl_engine_start: * @engine: an #FlEngine. @@ -133,6 +162,18 @@ void fl_engine_send_mouse_pointer_event(FlEngine* engine, double scroll_delta_y, int64_t buttons); +/** + * fl_engine_dispatch_semantics_action: + * @engine: an #FlEngine. + * @id: the semantics action identifier. + * @action: the action being dispatched. + * @data: (allow-none): data associated with the action. + */ +void fl_engine_dispatch_semantics_action(FlEngine* engine, + uint64_t id, + FlutterSemanticsAction action, + GBytes* data); + /** * fl_engine_send_platform_message_response: * @engine: an #FlEngine. diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index 87decdce97c88..44d89cdb5ab35 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -10,6 +10,7 @@ #endif #include +#include "flutter/shell/platform/linux/fl_accessibility_plugin.h" #include "flutter/shell/platform/linux/fl_engine_private.h" #include "flutter/shell/platform/linux/fl_key_event_plugin.h" #include "flutter/shell/platform/linux/fl_mouse_cursor_plugin.h" @@ -18,6 +19,7 @@ #include "flutter/shell/platform/linux/fl_renderer_wayland.h" #include "flutter/shell/platform/linux/fl_renderer_x11.h" #include "flutter/shell/platform/linux/fl_text_input_plugin.h" +#include "flutter/shell/platform/linux/fl_view_accessible.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" @@ -39,6 +41,7 @@ struct _FlView { int64_t button_state; // Flutter system channel handlers. + FlAccessibilityPlugin* accessibility_plugin; FlKeyEventPlugin* key_event_plugin; FlMouseCursorPlugin* mouse_cursor_plugin; FlPlatformPlugin* platform_plugin; @@ -57,6 +60,15 @@ G_DEFINE_TYPE_WITH_CODE( G_IMPLEMENT_INTERFACE(fl_plugin_registry_get_type(), fl_view_plugin_registry_iface_init)) +static void fl_view_update_semantics_node_cb(FlEngine* engine, + const FlutterSemanticsNode* node, + gpointer user_data) { + FlView* self = FL_VIEW(user_data); + + fl_accessibility_plugin_handle_update_semantics_node( + self->accessibility_plugin, node); +} + // Converts a GDK button event into a Flutter event and sends it to the engine. static gboolean fl_view_send_pointer_button_event(FlView* self, GdkEventButton* event) { @@ -155,9 +167,12 @@ static void fl_view_constructed(GObject* object) { GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(self)); self->renderer = fl_view_get_renderer_for_display(display); self->engine = fl_engine_new(self->project, self->renderer); + fl_engine_set_update_semantics_node_handler( + self->engine, fl_view_update_semantics_node_cb, self, nullptr); // Create system channel handlers. FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine); + self->accessibility_plugin = fl_accessibility_plugin_new(self); self->key_event_plugin = fl_key_event_plugin_new(messenger); self->mouse_cursor_plugin = fl_mouse_cursor_plugin_new(messenger, self); self->platform_plugin = fl_platform_plugin_new(messenger); @@ -215,6 +230,7 @@ static void fl_view_dispose(GObject* object) { g_clear_object(&self->project); g_clear_object(&self->renderer); g_clear_object(&self->engine); + g_clear_object(&self->accessibility_plugin); g_clear_object(&self->key_event_plugin); g_clear_object(&self->mouse_cursor_plugin); g_clear_object(&self->platform_plugin); @@ -388,6 +404,9 @@ static void fl_view_class_init(FlViewClass* klass) { fl_dart_project_get_type(), static_cast(G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS))); + + gtk_widget_class_set_accessible_type(GTK_WIDGET_CLASS(klass), + fl_view_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 new file mode 100644 index 0000000000000..3b79897191947 --- /dev/null +++ b/shell/platform/linux/fl_view_accessible.cc @@ -0,0 +1,93 @@ +// 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_view_accessible.h" +#include "flutter/shell/platform/linux/fl_accessible_node.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" + +struct _FlViewAccessible { + GtkWidgetAccessible parent_instance; + + // Semantics nodes keyed by ID + GHashTable* semantics_nodes_by_id; +}; + +G_DEFINE_TYPE(FlViewAccessible, fl_view_accessible, GTK_TYPE_WIDGET_ACCESSIBLE) + +// Get the ATK node for the given id. +// If the node doesn't exist it will be created. +static FlAccessibleNode* get_node(FlViewAccessible* self, int32_t id) { + FlAccessibleNode* node = FL_ACCESSIBLE_NODE( + g_hash_table_lookup(self->semantics_nodes_by_id, GINT_TO_POINTER(id))); + if (node == nullptr) { + FlView* view = FL_VIEW(gtk_accessible_get_widget(GTK_ACCESSIBLE(self))); + node = fl_accessible_node_new(fl_view_get_engine(view), id); + if (id == 0) { + fl_accessible_node_set_parent(node, ATK_OBJECT(self)); + } + g_hash_table_insert(self->semantics_nodes_by_id, GINT_TO_POINTER(id), + reinterpret_cast(node)); + } + return node; +} + +// Implements AtkObject::get_n_children +static gint fl_view_accessible_get_n_children(AtkObject* accessible) { + return 1; +} + +// Implements AtkObject::ref_child +static AtkObject* fl_view_accessible_ref_child(AtkObject* accessible, gint i) { + FlViewAccessible* self = FL_VIEW_ACCESSIBLE(accessible); + + if (i != 0) { + return nullptr; + } + + FlAccessibleNode* node = get_node(self, 0); + return ATK_OBJECT(g_object_ref(node)); +} + +// Implements AtkObject::get_role +static AtkRole fl_view_accessible_get_role(AtkObject* accessible) { + return ATK_ROLE_FRAME; +} + +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; +} + +static void fl_view_accessible_init(FlViewAccessible* self) { + self->semantics_nodes_by_id = g_hash_table_new_full( + g_direct_hash, g_direct_equal, nullptr, g_object_unref); +} + +void fl_view_accessible_handle_update_semantics_node( + FlViewAccessible* self, + const FlutterSemanticsNode* node) { + if (node->id == kFlutterSemanticsCustomActionIdBatchEnd) { + return; + } + + FlAccessibleNode* atk_node = get_node(self, node->id); + + fl_accessible_node_set_flags(atk_node, node->flags); + fl_accessible_node_set_actions(atk_node, node->actions); + fl_accessible_node_set_name(atk_node, node->label); + fl_accessible_node_set_extents( + atk_node, node->rect.left + node->transform.transX, + node->rect.top + node->transform.transY, + node->rect.right - node->rect.left, node->rect.bottom - node->rect.top); + + g_autoptr(GPtrArray) children = g_ptr_array_new(); + for (size_t i = 0; i < node->child_count; i++) { + FlAccessibleNode* child = + get_node(self, node->children_in_traversal_order[i]); + fl_accessible_node_set_parent(child, ATK_OBJECT(node)); + g_ptr_array_add(children, child); + } + fl_accessible_node_set_children(atk_node, children); +} diff --git a/shell/platform/linux/fl_view_accessible.h b/shell/platform/linux/fl_view_accessible.h new file mode 100644 index 0000000000000..612823272287c --- /dev/null +++ b/shell/platform/linux/fl_view_accessible.h @@ -0,0 +1,44 @@ +// 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_VIEW_ACCESSIBLE_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_VIEW_ACCESSIBLE_H_ + +#if !defined(__FLUTTER_LINUX_INSIDE__) && !defined(FLUTTER_LINUX_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +#include "flutter/shell/platform/embedder/embedder.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlViewAccessible, + fl_view_accessible, + FL, + VIEW_ACCESSIBLE, + GtkWidgetAccessible) + +/** + * FlViewAccessible: + * + * #FlViewAccessible is an object that exposes accessibility information for an + * #FlView. + */ + +/** + * fl_view_accessible_handle_update_semantics_node: + * @accessible: an #FlViewAccessible. + * @node: semantic node information. + * + * Handle a semantics node update from Flutter. + */ +void fl_view_accessible_handle_update_semantics_node( + FlViewAccessible* accessible, + const FlutterSemanticsNode* node); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_VIEW_ACCESSIBLE_H_ diff --git a/shell/platform/linux/testing/mock_engine.cc b/shell/platform/linux/testing/mock_engine.cc index c498e92f955b2..6afe2466a4bf2 100644 --- a/shell/platform/linux/testing/mock_engine.cc +++ b/shell/platform/linux/testing/mock_engine.cc @@ -17,6 +17,8 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_method_codec.h" #include "gtest/gtest.h" +const int32_t kFlutterSemanticsCustomActionIdBatchEnd = -1; + struct _FlutterEngine { bool running; FlutterPlatformMessageCallback platform_message_callback; @@ -411,6 +413,21 @@ FlutterEngineResult FlutterEngineUpdateLocales(FLUTTER_API_SYMBOL(FlutterEngine) return kSuccess; } +FlutterEngineResult FlutterEngineUpdateSemanticsEnabled( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + bool enabled) { + return kSuccess; +} + +FlutterEngineResult FlutterEngineDispatchSemanticsAction( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + uint64_t id, + FlutterSemanticsAction action, + const uint8_t* data, + size_t data_length) { + return kSuccess; +} + } // namespace FlutterEngineResult FlutterEngineGetProcAddresses( @@ -440,6 +457,8 @@ FlutterEngineResult FlutterEngineGetProcAddresses( &FlutterEngineSendPlatformMessageResponse; table->RunTask = &FlutterEngineRunTask; table->UpdateLocales = &FlutterEngineUpdateLocales; + table->UpdateSemanticsEnabled = &FlutterEngineUpdateSemanticsEnabled; + table->DispatchSemanticsAction = &FlutterEngineDispatchSemanticsAction; table->RunsAOTCompiledDartCode = &FlutterEngineRunsAOTCompiledDartCode; return kSuccess;