diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 42a60f1fb1cdd..942fc2649744d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2121,6 +2121,9 @@ FILE: ../../../flutter/shell/platform/linux/fl_accessibility_plugin.h FILE: ../../../flutter/shell/platform/linux/fl_accessible_node.cc FILE: ../../../flutter/shell/platform/linux/fl_accessible_node.h FILE: ../../../flutter/shell/platform/linux/fl_accessible_node_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_accessible_text_field.cc +FILE: ../../../flutter/shell/platform/linux/fl_accessible_text_field.h +FILE: ../../../flutter/shell/platform/linux/fl_accessible_text_field_test.cc FILE: ../../../flutter/shell/platform/linux/fl_backing_store_provider.cc FILE: ../../../flutter/shell/platform/linux/fl_backing_store_provider.h FILE: ../../../flutter/shell/platform/linux/fl_basic_message_channel.cc diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index d444ed4d09f28..59d94e3c7d564 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -99,6 +99,7 @@ source_set("flutter_linux_sources") { sources = [ "fl_accessibility_plugin.cc", "fl_accessible_node.cc", + "fl_accessible_text_field.cc", "fl_backing_store_provider.cc", "fl_basic_message_channel.cc", "fl_binary_codec.cc", @@ -192,6 +193,7 @@ executable("flutter_linux_unittests") { sources = [ "fl_accessible_node_test.cc", + "fl_accessible_text_field_test.cc", "fl_basic_message_channel_test.cc", "fl_binary_codec_test.cc", "fl_binary_messenger_test.cc", diff --git a/shell/platform/linux/fl_accessible_node.cc b/shell/platform/linux/fl_accessible_node.cc index 9e5f262d9e81e..c22509425a34e 100644 --- a/shell/platform/linux/fl_accessible_node.cc +++ b/shell/platform/linux/fl_accessible_node.cc @@ -24,6 +24,7 @@ static struct { {ATK_STATE_ENABLED, kFlutterSemanticsFlagIsEnabled, FALSE}, {ATK_STATE_SENSITIVE, kFlutterSemanticsFlagIsEnabled, FALSE}, {ATK_STATE_READ_ONLY, kFlutterSemanticsFlagIsReadOnly, FALSE}, + {ATK_STATE_EDITABLE, kFlutterSemanticsFlagIsTextField, FALSE}, {ATK_STATE_INVALID, static_cast(0), FALSE}, }; @@ -46,7 +47,6 @@ static ActionData action_mapping[] = { "MoveCursorForwardByCharacter"}, {kFlutterSemanticsActionMoveCursorBackwardByCharacter, "MoveCursorBackwardByCharacter"}, - {kFlutterSemanticsActionSetSelection, "SetSelection"}, {kFlutterSemanticsActionCopy, "Copy"}, {kFlutterSemanticsActionCut, "Cut"}, {kFlutterSemanticsActionPaste, "Paste"}, @@ -61,7 +61,7 @@ static ActionData action_mapping[] = { "MoveCursorBackwardByWord"}, {static_cast(0), nullptr}}; -struct _FlAccessibleNode { +struct FlAccessibleNodePrivate { AtkObject parent_instance; // Weak reference to the engine this node is created for. @@ -80,6 +80,12 @@ struct _FlAccessibleNode { FlutterSemanticsFlag flags; }; +enum { PROP_0, PROP_ENGINE, PROP_ID, PROP_LAST }; + +#define FL_ACCESSIBLE_NODE_GET_PRIVATE(node) \ + ((FlAccessibleNodePrivate*)fl_accessible_node_get_instance_private( \ + FL_ACCESSIBLE_NODE(node))) + static void fl_accessible_node_component_interface_init( AtkComponentIface* iface); static void fl_accessible_node_action_interface_init(AtkActionIface* iface); @@ -89,12 +95,13 @@ 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)) + G_ADD_PRIVATE(FlAccessibleNode) + 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 [old_flags] and [flags]. static gboolean flag_is_changed(FlutterSemanticsFlag old_flags, @@ -116,11 +123,11 @@ static gboolean has_action(FlutterSemanticsAction actions, } // Gets the nth action. -static ActionData* get_action(FlAccessibleNode* self, gint index) { - if (index < 0 || static_cast(index) >= self->actions->len) { +static ActionData* get_action(FlAccessibleNodePrivate* priv, gint index) { + if (index < 0 || static_cast(index) >= priv->actions->len) { return nullptr; } - return static_cast(g_ptr_array_index(self->actions, index)); + return static_cast(g_ptr_array_index(priv->actions, index)); } // Checks if [object] is in [children]. @@ -134,94 +141,115 @@ static gboolean has_child(GPtrArray* children, AtkObject* object) { return FALSE; } +static void fl_accessible_node_set_property(GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* pspec) { + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(object); + switch (prop_id) { + case PROP_ENGINE: + g_assert(priv->engine == nullptr); + priv->engine = FL_ENGINE(g_value_get_object(value)); + g_object_add_weak_pointer(object, + reinterpret_cast(&priv->engine)); + break; + case PROP_ID: + priv->id = g_value_get_int(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + static void fl_accessible_node_dispose(GObject* object) { - FlAccessibleNode* self = FL_ACCESSIBLE_NODE(object); + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(object); - if (self->engine != nullptr) { - g_object_remove_weak_pointer(G_OBJECT(self), - reinterpret_cast(&(self->engine))); - self->engine = nullptr; + if (priv->engine != nullptr) { + g_object_remove_weak_pointer(object, + reinterpret_cast(&(priv->engine))); + priv->engine = nullptr; } - if (self->parent != nullptr) { - g_object_remove_weak_pointer(G_OBJECT(self), - reinterpret_cast(&(self->parent))); - self->parent = nullptr; + if (priv->parent != nullptr) { + g_object_remove_weak_pointer(object, + reinterpret_cast(&(priv->parent))); + priv->parent = nullptr; } - 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_clear_pointer(&priv->name, g_free); + g_clear_pointer(&priv->actions, g_ptr_array_unref); + g_clear_pointer(&priv->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; + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(accessible); + return priv->name; } // Implements AtkObject::get_parent. static AtkObject* fl_accessible_node_get_parent(AtkObject* accessible) { - FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); - return self->parent; + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(accessible); + return priv->parent; } // Implements AtkObject::get_index_in_parent. static gint fl_accessible_node_get_index_in_parent(AtkObject* accessible) { - FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); - return self->index; + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(accessible); + return priv->index; } // 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; + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(accessible); + return priv->children->len; } // Implements AtkObject::ref_child. static AtkObject* fl_accessible_node_ref_child(AtkObject* accessible, gint i) { - FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(accessible); - if (i < 0 || static_cast(i) >= self->children->len) { + if (i < 0 || static_cast(i) >= priv->children->len) { return nullptr; } - return ATK_OBJECT(g_object_ref(g_ptr_array_index(self->children, i))); + return ATK_OBJECT(g_object_ref(g_ptr_array_index(priv->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) { + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(accessible); + if ((priv->flags & kFlutterSemanticsFlagIsButton) != 0) { return ATK_ROLE_PUSH_BUTTON; } - if ((self->flags & kFlutterSemanticsFlagIsInMutuallyExclusiveGroup) != 0 && - (self->flags & kFlutterSemanticsFlagHasCheckedState) != 0) { + if ((priv->flags & kFlutterSemanticsFlagIsInMutuallyExclusiveGroup) != 0 && + (priv->flags & kFlutterSemanticsFlagHasCheckedState) != 0) { return ATK_ROLE_RADIO_BUTTON; } - if ((self->flags & kFlutterSemanticsFlagHasCheckedState) != 0) { + if ((priv->flags & kFlutterSemanticsFlagHasCheckedState) != 0) { return ATK_ROLE_CHECK_BOX; } - if ((self->flags & kFlutterSemanticsFlagHasToggledState) != 0) { + if ((priv->flags & kFlutterSemanticsFlagHasToggledState) != 0) { return ATK_ROLE_TOGGLE_BUTTON; } - if ((self->flags & kFlutterSemanticsFlagIsSlider) != 0) { + if ((priv->flags & kFlutterSemanticsFlagIsSlider) != 0) { return ATK_ROLE_SLIDER; } - if ((self->flags & kFlutterSemanticsFlagIsTextField) != 0 && - (self->flags & kFlutterSemanticsFlagIsObscured) != 0) { + if ((priv->flags & kFlutterSemanticsFlagIsTextField) != 0 && + (priv->flags & kFlutterSemanticsFlagIsObscured) != 0) { return ATK_ROLE_PASSWORD_TEXT; } - if ((self->flags & kFlutterSemanticsFlagIsTextField) != 0) { + if ((priv->flags & kFlutterSemanticsFlagIsTextField) != 0) { return ATK_ROLE_TEXT; } - if ((self->flags & kFlutterSemanticsFlagIsHeader) != 0) { + if ((priv->flags & kFlutterSemanticsFlagIsHeader) != 0) { return ATK_ROLE_HEADER; } - if ((self->flags & kFlutterSemanticsFlagIsLink) != 0) { + if ((priv->flags & kFlutterSemanticsFlagIsLink) != 0) { return ATK_ROLE_LINK; } - if ((self->flags & kFlutterSemanticsFlagIsImage) != 0) { + if ((priv->flags & kFlutterSemanticsFlagIsImage) != 0) { return ATK_ROLE_IMAGE; } @@ -230,12 +258,12 @@ static AtkRole fl_accessible_node_get_role(AtkObject* accessible) { // Implements AtkObject::ref_state_set. static AtkStateSet* fl_accessible_node_ref_state_set(AtkObject* accessible) { - FlAccessibleNode* self = FL_ACCESSIBLE_NODE(accessible); + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(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); + gboolean enabled = has_flag(priv->flags, flag_mapping[i].flag); if (flag_mapping[i].invert) { enabled = !enabled; } @@ -254,19 +282,19 @@ static void fl_accessible_node_get_extents(AtkComponent* component, gint* width, gint* height, AtkCoordType coord_type) { - FlAccessibleNode* self = FL_ACCESSIBLE_NODE(component); + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(component); *x = 0; *y = 0; - if (self->parent != nullptr) { - atk_component_get_extents(ATK_COMPONENT(self->parent), x, y, nullptr, + if (priv->parent != nullptr) { + atk_component_get_extents(ATK_COMPONENT(priv->parent), x, y, nullptr, nullptr, coord_type); } - *x += self->x; - *y += self->y; - *width = self->width; - *height = self->height; + *x += priv->x; + *y += priv->y; + *width = priv->width; + *height = priv->height; } // Implements AtkComponent::get_layer. @@ -276,33 +304,33 @@ static AtkLayer fl_accessible_node_get_layer(AtkComponent* component) { // Implements AtkAction::do_action. static gboolean fl_accessible_node_do_action(AtkAction* action, gint i) { - FlAccessibleNode* self = FL_ACCESSIBLE_NODE(action); + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(action); - if (self->engine == nullptr) { + if (priv->engine == nullptr) { return FALSE; } - ActionData* data = get_action(self, i); + ActionData* data = get_action(priv, i); if (data == nullptr) { return FALSE; } - fl_engine_dispatch_semantics_action(self->engine, self->id, data->action, - nullptr); + fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(action), 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; + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(action); + return priv->actions->len; } // Implements AtkAction::get_name. static const gchar* fl_accessible_node_get_name(AtkAction* action, gint i) { - FlAccessibleNode* self = FL_ACCESSIBLE_NODE(action); + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(action); - ActionData* data = get_action(self, i); + ActionData* data = get_action(priv, i); if (data == nullptr) { return nullptr; } @@ -317,7 +345,87 @@ static gchar* fl_accessible_node_get_text(AtkText* text, return nullptr; } +// Implements FlAccessibleNode::set_name. +static void fl_accessible_node_set_name_impl(FlAccessibleNode* self, + const gchar* name) { + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(self); + g_free(priv->name); + priv->name = g_strdup(name); +} + +// Implements FlAccessibleNode::set_extents. +static void fl_accessible_node_set_extents_impl(FlAccessibleNode* self, + gint x, + gint y, + gint width, + gint height) { + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(self); + priv->x = x; + priv->y = y; + priv->width = width; + priv->height = height; +} + +// Implements FlAccessibleNode::set_flags. +static void fl_accessible_node_set_flags_impl(FlAccessibleNode* self, + FlutterSemanticsFlag flags) { + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(self); + + FlutterSemanticsFlag old_flags = priv->flags; + priv->flags = flags; + + for (int i = 0; flag_mapping[i].state != ATK_STATE_INVALID; i++) { + if (flag_is_changed(old_flags, 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); + } + } +} + +// Implements FlAccessibleNode::set_actions. +static void fl_accessible_node_set_actions_impl( + FlAccessibleNode* self, + FlutterSemanticsAction actions) { + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(self); + + // NOTE(robert-ancell): It appears that AtkAction doesn't have a method of + // notifying that actions have changed, and even if it did an ATK client + // might access the old IDs before checking for new ones. Keep an eye + // out for a case where Flutter changes the actions on an item and see + // if we can resolve this in another way. + g_ptr_array_remove_range(priv->actions, 0, priv->actions->len); + for (int i = 0; action_mapping[i].name != nullptr; i++) { + if (has_action(actions, action_mapping[i].action)) { + g_ptr_array_add(priv->actions, &action_mapping[i]); + } + } +} + +// Implements FlAccessibleNode::set_value. +static void fl_accessible_node_set_value_impl(FlAccessibleNode* self, + const gchar* value) {} + +// Implements FlAccessibleNode::set_text_selection. +static void fl_accessible_node_set_text_selection_impl(FlAccessibleNode* self, + gint base, + gint extent) {} + +// Implements FlAccessibleNode::perform_action. +static void fl_accessible_node_perform_action_impl( + FlAccessibleNode* self, + FlutterSemanticsAction action, + GBytes* data) { + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(self); + fl_engine_dispatch_semantics_action(priv->engine, priv->id, action, data); +} + static void fl_accessible_node_class_init(FlAccessibleNodeClass* klass) { + G_OBJECT_CLASS(klass)->set_property = fl_accessible_node_set_property; 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; @@ -327,6 +435,32 @@ static void fl_accessible_node_class_init(FlAccessibleNodeClass* klass) { 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; + FL_ACCESSIBLE_NODE_CLASS(klass)->set_name = fl_accessible_node_set_name_impl; + FL_ACCESSIBLE_NODE_CLASS(klass)->set_extents = + fl_accessible_node_set_extents_impl; + FL_ACCESSIBLE_NODE_CLASS(klass)->set_flags = + fl_accessible_node_set_flags_impl; + FL_ACCESSIBLE_NODE_CLASS(klass)->set_actions = + fl_accessible_node_set_actions_impl; + FL_ACCESSIBLE_NODE_CLASS(klass)->set_value = + fl_accessible_node_set_value_impl; + FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_selection = + fl_accessible_node_set_text_selection_impl; + FL_ACCESSIBLE_NODE_CLASS(klass)->perform_action = + fl_accessible_node_perform_action_impl; + + g_object_class_install_property( + G_OBJECT_CLASS(klass), PROP_ENGINE, + 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))); + g_object_class_install_property( + G_OBJECT_CLASS(klass), PROP_ID, + g_param_spec_int( + "id", "id", "Accessibility node ID", 0, G_MAXINT, 0, + static_cast(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); } static void fl_accessible_node_component_interface_init( @@ -346,17 +480,14 @@ static void fl_accessible_node_text_interface_init(AtkTextIface* iface) { } 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); + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(self); + priv->actions = g_ptr_array_new(); + priv->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 = engine; - g_object_add_weak_pointer(G_OBJECT(self), - reinterpret_cast(&(self->engine))); - self->id = id; + FlAccessibleNode* self = FL_ACCESSIBLE_NODE(g_object_new( + fl_accessible_node_get_type(), "engine", engine, "id", id, nullptr)); return self; } @@ -364,33 +495,35 @@ void fl_accessible_node_set_parent(FlAccessibleNode* self, AtkObject* parent, gint index) { g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); - self->parent = parent; - self->index = index; + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(self); + priv->parent = parent; + priv->index = index; g_object_add_weak_pointer(G_OBJECT(self), - reinterpret_cast(&(self->parent))); + reinterpret_cast(&(priv->parent))); } void fl_accessible_node_set_children(FlAccessibleNode* self, GPtrArray* children) { g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + FlAccessibleNodePrivate* priv = FL_ACCESSIBLE_NODE_GET_PRIVATE(self); // Remove nodes that are no longer required. - for (guint i = 0; i < self->children->len;) { - AtkObject* object = ATK_OBJECT(g_ptr_array_index(self->children, i)); + for (guint i = 0; i < priv->children->len;) { + AtkObject* object = ATK_OBJECT(g_ptr_array_index(priv->children, i)); if (has_child(children, object)) { i++; } else { g_signal_emit_by_name(self, "children-changed::remove", i, object, nullptr); - g_ptr_array_remove_index(self->children, i); + g_ptr_array_remove_index(priv->children, i); } } // Add new nodes. for (guint i = 0; i < children->len; i++) { AtkObject* object = ATK_OBJECT(g_ptr_array_index(children, i)); - if (!has_child(self->children, object)) { - g_ptr_array_add(self->children, g_object_ref(object)); + if (!has_child(priv->children, object)) { + g_ptr_array_add(priv->children, g_object_ref(object)); g_signal_emit_by_name(self, "children-changed::add", i, object, nullptr); } } @@ -398,8 +531,8 @@ void fl_accessible_node_set_children(FlAccessibleNode* self, 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); + + return FL_ACCESSIBLE_NODE_GET_CLASS(self)->set_name(self, name); } void fl_accessible_node_set_extents(FlAccessibleNode* self, @@ -408,45 +541,44 @@ void fl_accessible_node_set_extents(FlAccessibleNode* self, 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; + + return FL_ACCESSIBLE_NODE_GET_CLASS(self)->set_extents(self, x, y, width, + height); } void fl_accessible_node_set_flags(FlAccessibleNode* self, FlutterSemanticsFlag flags) { g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); - FlutterSemanticsFlag old_flags = self->flags; - self->flags = flags; - - for (int i = 0; flag_mapping[i].state != ATK_STATE_INVALID; i++) { - if (flag_is_changed(old_flags, 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); - } - } + return FL_ACCESSIBLE_NODE_GET_CLASS(self)->set_flags(self, flags); } void fl_accessible_node_set_actions(FlAccessibleNode* self, FlutterSemanticsAction actions) { g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); - // NOTE(robert-ancell): It appears that AtkAction doesn't have a method of - // notifying that actions have changed, and even if it did an ATK client - // might access the old IDs before checking for new ones. Keep an eye - // out for a case where Flutter changes the actions on an item and see - // if we can resolve this in another way. - g_ptr_array_remove_range(self->actions, 0, self->actions->len); - 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]); - } - } + return FL_ACCESSIBLE_NODE_GET_CLASS(self)->set_actions(self, actions); +} + +void fl_accessible_node_set_value(FlAccessibleNode* self, const gchar* value) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + + return FL_ACCESSIBLE_NODE_GET_CLASS(self)->set_value(self, value); +} + +void fl_accessible_node_set_text_selection(FlAccessibleNode* self, + gint base, + gint extent) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + + return FL_ACCESSIBLE_NODE_GET_CLASS(self)->set_text_selection(self, base, + extent); +} + +void fl_accessible_node_perform_action(FlAccessibleNode* self, + FlutterSemanticsAction action, + GBytes* data) { + g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self)); + + return FL_ACCESSIBLE_NODE_GET_CLASS(self)->perform_action(self, action, data); } diff --git a/shell/platform/linux/fl_accessible_node.h b/shell/platform/linux/fl_accessible_node.h index 0d392256edae9..ff38ad49c60dc 100644 --- a/shell/platform/linux/fl_accessible_node.h +++ b/shell/platform/linux/fl_accessible_node.h @@ -16,11 +16,12 @@ G_BEGIN_DECLS // https://gitlab.gnome.org/GNOME/atk/-/issues/10 G_DEFINE_AUTOPTR_CLEANUP_FUNC(AtkObject, g_object_unref) -G_DECLARE_FINAL_TYPE(FlAccessibleNode, - fl_accessible_node, - FL, - ACCESSIBLE_NODE, - AtkObject); +#define FL_TYPE_ACCESSIBLE_NODE fl_accessible_node_get_type() +G_DECLARE_DERIVABLE_TYPE(FlAccessibleNode, + fl_accessible_node, + FL, + ACCESSIBLE_NODE, + AtkObject); /** * FlAccessibleNode: @@ -28,6 +29,24 @@ G_DECLARE_FINAL_TYPE(FlAccessibleNode, * #FlAccessibleNode is an object that exposes a Flutter accessibility node to * ATK. */ +struct _FlAccessibleNodeClass { + AtkObjectClass parent_class; + + void (*set_name)(FlAccessibleNode* node, const gchar* name); + void (*set_extents)(FlAccessibleNode* node, + gint x, + gint y, + gint width, + gint height); + void (*set_flags)(FlAccessibleNode* node, FlutterSemanticsFlag flags); + void (*set_actions)(FlAccessibleNode* node, FlutterSemanticsAction actions); + void (*set_value)(FlAccessibleNode* node, const gchar* value); + void (*set_text_selection)(FlAccessibleNode* node, gint base, gint extent); + + void (*perform_action)(FlAccessibleNode* node, + FlutterSemanticsAction action, + GBytes* data); +}; /** * fl_accessible_node_new: @@ -108,6 +127,39 @@ void fl_accessible_node_set_flags(FlAccessibleNode* node, void fl_accessible_node_set_actions(FlAccessibleNode* node, FlutterSemanticsAction actions); +/** + * fl_accessible_node_set_value: + * @node: an #FlAccessibleNode. + * @value: a node value. + * + * Sets the value of this node. + */ +void fl_accessible_node_set_value(FlAccessibleNode* node, const gchar* value); + +/** + * fl_accessible_node_set_text_selection: + * @node: an #FlAccessibleNode. + * @base: the position at which the text selection originates. + * @extent: the position at which the text selection terminates. + * + * Sets the text selection of this node. + */ +void fl_accessible_node_set_text_selection(FlAccessibleNode* node, + gint base, + gint extent); + +/** + * fl_accessible_node_dispatch_action: + * @node: an #FlAccessibleNode. + * @action: the action being dispatched. + * @data: (allow-none): data associated with the action. + * + * Performs a semantic action for this node. + */ +void fl_accessible_node_perform_action(FlAccessibleNode* node, + FlutterSemanticsAction action, + GBytes* data); + G_END_DECLS #endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_ACCESSIBLE_NODE_H_ diff --git a/shell/platform/linux/fl_accessible_text_field.cc b/shell/platform/linux/fl_accessible_text_field.cc new file mode 100644 index 0000000000000..9c483e61fe360 --- /dev/null +++ b/shell/platform/linux/fl_accessible_text_field.cc @@ -0,0 +1,400 @@ +// 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_text_field.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_message_codec.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h" + +struct _FlAccessibleTextField { + FlAccessibleNode parent_instance; + + gint selection_base; + gint selection_extent; + GtkEntryBuffer* buffer; +}; + +static void fl_accessible_text_iface_init(AtkTextIface* iface); +static void fl_accessible_editable_text_iface_init(AtkEditableTextIface* iface); + +G_DEFINE_TYPE_WITH_CODE( + FlAccessibleTextField, + fl_accessible_text_field, + FL_TYPE_ACCESSIBLE_NODE, + G_IMPLEMENT_INTERFACE(ATK_TYPE_TEXT, fl_accessible_text_iface_init) + G_IMPLEMENT_INTERFACE(ATK_TYPE_EDITABLE_TEXT, + fl_accessible_editable_text_iface_init)) + +static gchar* get_substring(FlAccessibleTextField* self, + glong start, + glong end) { + const gchar* value = gtk_entry_buffer_get_text(self->buffer); + if (end == -1) { + // g_utf8_substring() accepts -1 since 2.72 + end = g_utf8_strlen(value, -1); + } + return g_utf8_substring(value, start, end); +} + +static void perform_set_text_action(FlAccessibleTextField* self, + const char* text) { + g_autoptr(FlValue) value = fl_value_new_string(text); + g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new(); + g_autoptr(GBytes) message = + fl_message_codec_encode_message(FL_MESSAGE_CODEC(codec), value, nullptr); + + fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(self), + kFlutterSemanticsActionSetText, message); +} + +static void perform_set_selection_action(FlAccessibleTextField* self, + gint base, + gint extent) { + g_autoptr(FlValue) value = fl_value_new_map(); + fl_value_set_string_take(value, "base", fl_value_new_int(base)); + fl_value_set_string_take(value, "extent", fl_value_new_int(extent)); + + g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new(); + g_autoptr(GBytes) message = + fl_message_codec_encode_message(FL_MESSAGE_CODEC(codec), value, nullptr); + + fl_accessible_node_perform_action( + FL_ACCESSIBLE_NODE(self), kFlutterSemanticsActionSetSelection, message); +} + +// Implements GObject::dispose. +static void fl_accessible_text_field_dispose(GObject* object) { + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(object); + + g_clear_object(&self->buffer); + + G_OBJECT_CLASS(fl_accessible_text_field_parent_class)->dispose(object); +} + +// Implements FlAccessibleNode::set_value. +static void fl_accessible_text_field_set_value(FlAccessibleNode* node, + const gchar* value) { + g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node)); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node); + + if (g_strcmp0(gtk_entry_buffer_get_text(self->buffer), value) == 0) { + return; + } + + gtk_entry_buffer_set_text(self->buffer, value, -1); +} + +// Implements FlAccessibleNode::set_text_selection. +static void fl_accessible_text_field_set_text_selection(FlAccessibleNode* node, + gint base, + gint extent) { + g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node)); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node); + + gboolean caret_moved = extent != self->selection_extent; + gboolean has_selection = base != extent; + gboolean had_selection = self->selection_base != self->selection_extent; + gboolean selection_changed = (has_selection || had_selection) && + (caret_moved || base != self->selection_base); + + self->selection_base = base; + self->selection_extent = extent; + + if (selection_changed) { + g_signal_emit_by_name(self, "text-selection-changed", nullptr); + } + + if (caret_moved) { + g_signal_emit_by_name(self, "text-caret-moved", extent, nullptr); + } +} + +// Overrides FlAccessibleNode::perform_action. +void fl_accessible_text_field_perform_action(FlAccessibleNode* self, + FlutterSemanticsAction action, + GBytes* data) { + FlAccessibleNodeClass* parent_class = + FL_ACCESSIBLE_NODE_CLASS(fl_accessible_text_field_parent_class); + + switch (action) { + case kFlutterSemanticsActionMoveCursorForwardByCharacter: + case kFlutterSemanticsActionMoveCursorBackwardByCharacter: + case kFlutterSemanticsActionMoveCursorForwardByWord: + case kFlutterSemanticsActionMoveCursorBackwardByWord: { + // These actions require a boolean argument that indicates whether the + // selection should be extended or collapsed when moving the cursor. + g_autoptr(FlValue) extend_selection = fl_value_new_bool(false); + g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new(); + g_autoptr(GBytes) message = fl_message_codec_encode_message( + FL_MESSAGE_CODEC(codec), extend_selection, nullptr); + parent_class->perform_action(self, action, message); + break; + } + default: + parent_class->perform_action(self, action, data); + break; + } +} + +// Implements AtkText::get_character_count. +static gint fl_accessible_text_field_get_character_count(AtkText* text) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), 0); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + return gtk_entry_buffer_get_length(self->buffer); +} + +// Implements AtkText::get_text. +static gchar* fl_accessible_text_field_get_text(AtkText* text, + gint start_offset, + gint end_offset) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), nullptr); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + return get_substring(self, start_offset, end_offset); +} + +// Implements AtkText::get_caret_offset. +static gint fl_accessible_text_field_get_caret_offset(AtkText* text) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), -1); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + return self->selection_extent; +} + +// Implements AtkText::set_caret_offset. +static gboolean fl_accessible_text_field_set_caret_offset(AtkText* text, + gint offset) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + perform_set_selection_action(self, offset, offset); + return true; +} + +// Implements AtkText::get_n_selections. +static gint fl_accessible_text_field_get_n_selections(AtkText* text) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), 0); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + if (self->selection_base == self->selection_extent) { + return 0; + } + + return 1; +} + +// Implements AtkText::get_selection. +static gchar* fl_accessible_text_field_get_selection(AtkText* text, + gint selection_num, + gint* start_offset, + gint* end_offset) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), nullptr); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + if (selection_num != 0 || self->selection_base == self->selection_extent) { + return nullptr; + } + + gint start = MIN(self->selection_base, self->selection_extent); + gint end = MAX(self->selection_base, self->selection_extent); + + if (start_offset != nullptr) { + *start_offset = start; + } + if (end_offset != nullptr) { + *end_offset = end; + } + + return get_substring(self, start, end); +} + +// Implements AtkText::add_selection. +static gboolean fl_accessible_text_field_add_selection(AtkText* text, + gint start_offset, + gint end_offset) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + if (self->selection_base != self->selection_extent) { + return false; + } + + perform_set_selection_action(self, start_offset, end_offset); + return true; +} + +// Implements AtkText::remove_selection. +static gboolean fl_accessible_text_field_remove_selection(AtkText* text, + gint selection_num) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + if (selection_num != 0 || self->selection_base == self->selection_extent) { + return false; + } + + perform_set_selection_action(self, self->selection_extent, + self->selection_extent); + return true; +} + +// Implements AtkText::set_selection. +static gboolean fl_accessible_text_field_set_selection(AtkText* text, + gint selection_num, + gint start_offset, + gint end_offset) { + g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text); + + if (selection_num != 0) { + return false; + } + + perform_set_selection_action(self, start_offset, end_offset); + return true; +} + +// Implements AtkEditableText::set_text_contents. +static void fl_accessible_text_field_set_text_contents( + AtkEditableText* editable_text, + const gchar* string) { + g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text)); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text); + + perform_set_text_action(self, string); +} + +// Implements AtkEditableText::insert_text. +static void fl_accessible_text_field_insert_text(AtkEditableText* editable_text, + const gchar* string, + gint length, + gint* position) { + g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text)); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text); + + *position += + gtk_entry_buffer_insert_text(self->buffer, *position, string, length); + + perform_set_text_action(self, gtk_entry_buffer_get_text(self->buffer)); + perform_set_selection_action(self, *position, *position); +} + +// Implements AtkEditableText::delete_text. +static void fl_accessible_node_delete_text(AtkEditableText* editable_text, + gint start_pos, + gint end_pos) { + g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text)); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text); + + gtk_entry_buffer_delete_text(self->buffer, start_pos, end_pos - start_pos); + + perform_set_text_action(self, gtk_entry_buffer_get_text(self->buffer)); + perform_set_selection_action(self, start_pos, start_pos); +} + +// Implement AtkEditableText::copy_text. +static void fl_accessible_text_field_copy_text(AtkEditableText* editable_text, + gint start_pos, + gint end_pos) { + g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text)); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text); + + perform_set_selection_action(self, start_pos, end_pos); + + fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text), + kFlutterSemanticsActionCopy, nullptr); +} + +// Implements AtkEditableText::cut_text. +static void fl_accessible_text_field_cut_text(AtkEditableText* editable_text, + gint start_pos, + gint end_pos) { + g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text)); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text); + + perform_set_selection_action(self, start_pos, end_pos); + + fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text), + kFlutterSemanticsActionCut, nullptr); +} + +// Implements AtkEditableText::paste_text. +static void fl_accessible_text_field_paste_text(AtkEditableText* editable_text, + gint position) { + g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text)); + FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text); + + perform_set_selection_action(self, position, position); + + fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text), + kFlutterSemanticsActionPaste, nullptr); +} + +static void fl_accessible_text_field_class_init( + FlAccessibleTextFieldClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_accessible_text_field_dispose; + FL_ACCESSIBLE_NODE_CLASS(klass)->set_value = + fl_accessible_text_field_set_value; + FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_selection = + fl_accessible_text_field_set_text_selection; + FL_ACCESSIBLE_NODE_CLASS(klass)->perform_action = + fl_accessible_text_field_perform_action; +} + +static void fl_accessible_text_iface_init(AtkTextIface* iface) { + iface->get_character_count = fl_accessible_text_field_get_character_count; + iface->get_text = fl_accessible_text_field_get_text; + // TODO(jpnurmi): get_text_at/before/after_offset + + iface->get_caret_offset = fl_accessible_text_field_get_caret_offset; + iface->set_caret_offset = fl_accessible_text_field_set_caret_offset; + + iface->get_n_selections = fl_accessible_text_field_get_n_selections; + iface->get_selection = fl_accessible_text_field_get_selection; + iface->add_selection = fl_accessible_text_field_add_selection; + iface->remove_selection = fl_accessible_text_field_remove_selection; + iface->set_selection = fl_accessible_text_field_set_selection; +} + +static void fl_accessible_editable_text_iface_init( + AtkEditableTextIface* iface) { + iface->set_text_contents = fl_accessible_text_field_set_text_contents; + iface->insert_text = fl_accessible_text_field_insert_text; + iface->delete_text = fl_accessible_node_delete_text; + + iface->copy_text = fl_accessible_text_field_copy_text; + iface->cut_text = fl_accessible_text_field_cut_text; + iface->paste_text = fl_accessible_text_field_paste_text; +} + +static void fl_accessible_text_field_init(FlAccessibleTextField* self) { + self->selection_base = -1; + self->selection_extent = -1; + + self->buffer = gtk_entry_buffer_new("", 0); + + g_signal_connect_object( + self->buffer, "inserted-text", + G_CALLBACK(+[](FlAccessibleTextField* self, guint position, gchar* chars, + guint n_chars) { + g_signal_emit_by_name(self, "text-insert", position, n_chars, chars, + nullptr); + }), + self, G_CONNECT_SWAPPED); + + g_signal_connect_object(self->buffer, "deleted-text", + G_CALLBACK(+[](FlAccessibleTextField* self, + guint position, guint n_chars) { + g_autofree gchar* chars = atk_text_get_text( + ATK_TEXT(self), position, position + n_chars); + g_signal_emit_by_name(self, "text-remove", position, + n_chars, chars, nullptr); + }), + self, G_CONNECT_SWAPPED); +} + +FlAccessibleNode* fl_accessible_text_field_new(FlEngine* engine, int32_t id) { + return FL_ACCESSIBLE_NODE(g_object_new(fl_accessible_text_field_get_type(), + "engine", engine, "id", id, nullptr)); +} diff --git a/shell/platform/linux/fl_accessible_text_field.h b/shell/platform/linux/fl_accessible_text_field.h new file mode 100644 index 0000000000000..c3346abda1637 --- /dev/null +++ b/shell/platform/linux/fl_accessible_text_field.h @@ -0,0 +1,34 @@ +// 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_TEXT_FIELD_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_ACCESSIBLE_TEXT_FIELD_H_ + +#include + +#include "flutter/shell/platform/linux/fl_accessible_node.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlAccessibleTextField, + fl_accessible_text_field, + FL, + ACCESSIBLE_TEXT_FIELD, + FlAccessibleNode); + +/** + * fl_accessible_text_field_new: + * @engine: the #FlEngine this node came from. + * @id: the semantics node ID this object represents. + * + * Creates a new accessibility object that exposes an editable Flutter text + * field to ATK. + * + * Returns: a new #FlAccessibleNode. + */ +FlAccessibleNode* fl_accessible_text_field_new(FlEngine* engine, int32_t id); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_ACCESSIBLE_TEXT_FIELD_H_ diff --git a/shell/platform/linux/fl_accessible_text_field_test.cc b/shell/platform/linux/fl_accessible_text_field_test.cc new file mode 100644 index 0000000000000..475711345ab67 --- /dev/null +++ b/shell/platform/linux/fl_accessible_text_field_test.cc @@ -0,0 +1,524 @@ +// 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. + +// Included first as it collides with the X11 headers. +#include "gtest/gtest.h" + +#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" +#include "flutter/shell/platform/linux/fl_accessible_text_field.h" +#include "flutter/shell/platform/linux/fl_engine_private.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_message_codec.h" +#include "flutter/shell/platform/linux/testing/fl_test.h" +#include "flutter/shell/platform/linux/testing/mock_signal_handler.h" + +// MOCK_ENGINE_PROC is leaky by design +// NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) + +static FlValue* decode_semantic_data(const uint8_t* data, size_t data_length) { + g_autoptr(GBytes) bytes = g_bytes_new(data, data_length); + g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new(); + return fl_message_codec_decode_message(FL_MESSAGE_CODEC(codec), bytes, + nullptr); +} + +// Tests that semantic node value updates from Flutter emit AtkText::text-insert +// and AtkText::text-remove signals as expected. +TEST(FlAccessibleTextFieldTest, SetValue) { + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + // "" -> "Flutter" + { + flutter::testing::MockSignalHandler2 text_inserted(node, + "text-insert"); + flutter::testing::MockSignalHandler text_removed(node, "text-remove"); + + EXPECT_SIGNAL2(text_inserted, ::testing::Eq(0), ::testing::Eq(7)); + EXPECT_SIGNAL(text_removed).Times(0); + + fl_accessible_node_set_value(node, "Flutter"); + } + + // "Flutter" -> "Flutter" + { + flutter::testing::MockSignalHandler text_inserted(node, "text-insert"); + flutter::testing::MockSignalHandler text_removed(node, "text-remove"); + + EXPECT_SIGNAL(text_inserted).Times(0); + EXPECT_SIGNAL(text_removed).Times(0); + + fl_accessible_node_set_value(node, "Flutter"); + } + + // "Flutter" -> "engine" + { + flutter::testing::MockSignalHandler2 text_inserted(node, + "text-insert"); + flutter::testing::MockSignalHandler2 text_removed(node, + "text-remove"); + + EXPECT_SIGNAL2(text_inserted, ::testing::Eq(0), ::testing::Eq(6)); + EXPECT_SIGNAL2(text_removed, ::testing::Eq(0), ::testing::Eq(7)); + + fl_accessible_node_set_value(node, "engine"); + } + + // "engine" -> "" + { + flutter::testing::MockSignalHandler text_inserted(node, "text-insert"); + flutter::testing::MockSignalHandler2 text_removed(node, + "text-remove"); + + EXPECT_SIGNAL(text_inserted).Times(0); + EXPECT_SIGNAL2(text_removed, ::testing::Eq(0), ::testing::Eq(6)); + + fl_accessible_node_set_value(node, ""); + } +} + +// Tests that semantic node selection updates from Flutter emit +// AtkText::text-selection-changed and AtkText::text-caret-moved signals as +// expected. +TEST(FlAccessibleTextFieldTest, SetTextSelection) { + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + // [-1,-1] -> [2,3] + { + flutter::testing::MockSignalHandler text_selection_changed( + node, "text-selection-changed"); + flutter::testing::MockSignalHandler1 text_caret_moved( + node, "text-caret-moved"); + + EXPECT_SIGNAL(text_selection_changed); + EXPECT_SIGNAL1(text_caret_moved, ::testing::Eq(3)); + + fl_accessible_node_set_text_selection(node, 2, 3); + } + + // [2,3] -> [3,3] + { + flutter::testing::MockSignalHandler text_selection_changed( + node, "text-selection-changed"); + flutter::testing::MockSignalHandler text_caret_moved(node, + "text-caret-moved"); + + EXPECT_SIGNAL(text_selection_changed); + EXPECT_SIGNAL(text_caret_moved).Times(0); + + fl_accessible_node_set_text_selection(node, 3, 3); + } + + // [3,3] -> [3,3] + { + flutter::testing::MockSignalHandler text_selection_changed( + node, "text-selection-changed"); + flutter::testing::MockSignalHandler text_caret_moved(node, + "text-caret-moved"); + + EXPECT_SIGNAL(text_selection_changed).Times(0); + EXPECT_SIGNAL(text_caret_moved).Times(0); + + fl_accessible_node_set_text_selection(node, 3, 3); + } + + // [3,3] -> [4,4] + { + flutter::testing::MockSignalHandler text_selection_changed( + node, "text-selection-changed"); + flutter::testing::MockSignalHandler1 text_caret_moved( + node, "text-caret-moved"); + + EXPECT_SIGNAL(text_selection_changed).Times(0); + EXPECT_SIGNAL1(text_caret_moved, ::testing::Eq(4)); + + fl_accessible_node_set_text_selection(node, 4, 4); + } +} + +// Tests that fl_accessible_text_field_perform_action() passes the required +// "expandSelection" argument for semantic cursor move actions. +TEST(FlAccessibleTextFieldTest, PerformAction) { + g_autoptr(GPtrArray) action_datas = g_ptr_array_new_with_free_func( + reinterpret_cast(fl_value_unref)); + + g_autoptr(FlEngine) engine = make_mock_engine(); + fl_engine_get_embedder_api(engine)->DispatchSemanticsAction = + MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&action_datas](auto engine, uint64_t id, + FlutterSemanticsAction action, const uint8_t* data, + size_t data_length) { + g_ptr_array_add(action_datas, + decode_semantic_data(data, data_length)); + return kSuccess; + })); + + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + fl_accessible_node_set_actions( + node, static_cast( + kFlutterSemanticsActionMoveCursorForwardByCharacter | + kFlutterSemanticsActionMoveCursorBackwardByCharacter | + kFlutterSemanticsActionMoveCursorForwardByWord | + kFlutterSemanticsActionMoveCursorBackwardByWord)); + + g_autoptr(FlValue) expand_selection = fl_value_new_bool(false); + + for (int i = 0; i < 4; ++i) { + atk_action_do_action(ATK_ACTION(node), i); + + FlValue* data = static_cast(g_ptr_array_index(action_datas, i)); + EXPECT_NE(data, nullptr); + EXPECT_TRUE(fl_value_equal(data, expand_selection)); + } +} + +// Tests AtkText::get_character_count. +TEST(FlAccessibleTextFieldTest, GetCharacterCount) { + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + EXPECT_EQ(atk_text_get_character_count(ATK_TEXT(node)), 0); + + fl_accessible_node_set_value(node, "Flutter!"); + + EXPECT_EQ(atk_text_get_character_count(ATK_TEXT(node)), 8); +} + +// Tests AtkText::get_text. +TEST(FlAccessibleTextFieldTest, GetText) { + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + g_autofree gchar* empty = atk_text_get_text(ATK_TEXT(node), 0, -1); + EXPECT_STREQ(empty, ""); + + flutter::testing::MockSignalHandler text_inserted(node, "text-insert"); + EXPECT_SIGNAL(text_inserted).Times(1); + + fl_accessible_node_set_value(node, "Flutter!"); + + g_autofree gchar* flutter = atk_text_get_text(ATK_TEXT(node), 0, -1); + EXPECT_STREQ(flutter, "Flutter!"); + + g_autofree gchar* tt = atk_text_get_text(ATK_TEXT(node), 3, 5); + EXPECT_STREQ(tt, "tt"); +} + +// Tests AtkText::get_caret_offset. +TEST(FlAccessibleTextFieldTest, GetCaretOffset) { + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + EXPECT_EQ(atk_text_get_caret_offset(ATK_TEXT(node)), -1); + + fl_accessible_node_set_text_selection(node, 1, 2); + + EXPECT_EQ(atk_text_get_caret_offset(ATK_TEXT(node)), 2); +} + +// Tests AtkText::set_caret_offset. +TEST(FlAccessibleTextFieldTest, SetCaretOffset) { + int base = -1; + int extent = -1; + + g_autoptr(FlEngine) engine = make_mock_engine(); + fl_engine_get_embedder_api(engine)->DispatchSemanticsAction = + MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&base, &extent](auto engine, uint64_t id, + FlutterSemanticsAction action, const uint8_t* data, + size_t data_length) { + EXPECT_EQ(action, kFlutterSemanticsActionSetSelection); + g_autoptr(FlValue) value = decode_semantic_data(data, data_length); + EXPECT_EQ(fl_value_get_type(value), FL_VALUE_TYPE_MAP); + base = fl_value_get_int(fl_value_lookup_string(value, "base")); + extent = fl_value_get_int(fl_value_lookup_string(value, "extent")); + return kSuccess; + })); + + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + EXPECT_TRUE(atk_text_set_caret_offset(ATK_TEXT(node), 3)); + EXPECT_EQ(base, 3); + EXPECT_EQ(extent, 3); +} + +// Tests AtkText::get_n_selections. +TEST(FlAccessibleTextFieldTest, GetNSelections) { + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + EXPECT_EQ(atk_text_get_n_selections(ATK_TEXT(node)), 0); + + fl_accessible_node_set_text_selection(node, 1, 2); + + EXPECT_EQ(atk_text_get_n_selections(ATK_TEXT(node)), 1); +} + +// Tests AtkText::get_selection. +TEST(FlAccessibleTextFieldTest, GetSelection) { + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + EXPECT_EQ(atk_text_get_selection(ATK_TEXT(node), 0, nullptr, nullptr), + nullptr); + + fl_accessible_node_set_value(node, "Flutter"); + fl_accessible_node_set_text_selection(node, 2, 5); + + gint start, end; + g_autofree gchar* selection = + atk_text_get_selection(ATK_TEXT(node), 0, &start, &end); + EXPECT_STREQ(selection, "utt"); + EXPECT_EQ(start, 2); + EXPECT_EQ(end, 5); + + // reverse + fl_accessible_node_set_text_selection(node, 5, 2); + g_autofree gchar* reverse = + atk_text_get_selection(ATK_TEXT(node), 0, &start, &end); + EXPECT_STREQ(reverse, "utt"); + EXPECT_EQ(start, 2); + EXPECT_EQ(end, 5); + + // empty + fl_accessible_node_set_text_selection(node, 5, 5); + EXPECT_EQ(atk_text_get_selection(ATK_TEXT(node), 0, &start, &end), nullptr); + + // selection num != 0 + EXPECT_EQ(atk_text_get_selection(ATK_TEXT(node), 1, &start, &end), nullptr); +} + +// Tests AtkText::add_selection. +TEST(FlAccessibleTextFieldTest, AddSelection) { + int base = -1; + int extent = -1; + + g_autoptr(FlEngine) engine = make_mock_engine(); + fl_engine_get_embedder_api(engine)->DispatchSemanticsAction = + MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&base, &extent](auto engine, uint64_t id, + FlutterSemanticsAction action, const uint8_t* data, + size_t data_length) { + EXPECT_EQ(action, kFlutterSemanticsActionSetSelection); + g_autoptr(FlValue) value = decode_semantic_data(data, data_length); + EXPECT_EQ(fl_value_get_type(value), FL_VALUE_TYPE_MAP); + base = fl_value_get_int(fl_value_lookup_string(value, "base")); + extent = fl_value_get_int(fl_value_lookup_string(value, "extent")); + return kSuccess; + })); + + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + EXPECT_TRUE(atk_text_add_selection(ATK_TEXT(node), 2, 4)); + EXPECT_EQ(base, 2); + EXPECT_EQ(extent, 4); + + fl_accessible_node_set_text_selection(node, 2, 4); + + // already has selection + EXPECT_FALSE(atk_text_add_selection(ATK_TEXT(node), 6, 7)); + EXPECT_EQ(base, 2); + EXPECT_EQ(extent, 4); +} + +// Tests AtkText::remove_selection. +TEST(FlAccessibleTextFieldTest, RemoveSelection) { + int base = -1; + int extent = -1; + + g_autoptr(FlEngine) engine = make_mock_engine(); + fl_engine_get_embedder_api(engine)->DispatchSemanticsAction = + MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&base, &extent](auto engine, uint64_t id, + FlutterSemanticsAction action, const uint8_t* data, + size_t data_length) { + EXPECT_EQ(action, kFlutterSemanticsActionSetSelection); + g_autoptr(FlValue) value = decode_semantic_data(data, data_length); + EXPECT_EQ(fl_value_get_type(value), FL_VALUE_TYPE_MAP); + base = fl_value_get_int(fl_value_lookup_string(value, "base")); + extent = fl_value_get_int(fl_value_lookup_string(value, "extent")); + return kSuccess; + })); + + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + // no selection + EXPECT_FALSE(atk_text_remove_selection(ATK_TEXT(node), 0)); + EXPECT_EQ(base, -1); + EXPECT_EQ(extent, -1); + + fl_accessible_node_set_text_selection(node, 2, 4); + + // selection num != 0 + EXPECT_FALSE(atk_text_remove_selection(ATK_TEXT(node), 1)); + EXPECT_EQ(base, -1); + EXPECT_EQ(extent, -1); + + // ok, collapses selection + EXPECT_TRUE(atk_text_remove_selection(ATK_TEXT(node), 0)); + EXPECT_EQ(base, 4); + EXPECT_EQ(extent, 4); +} + +// Tests AtkText::set_selection. +TEST(FlAccessibleTextFieldTest, SetSelection) { + int base = -1; + int extent = -1; + + g_autoptr(FlEngine) engine = make_mock_engine(); + fl_engine_get_embedder_api(engine)->DispatchSemanticsAction = + MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&base, &extent](auto engine, uint64_t id, + FlutterSemanticsAction action, const uint8_t* data, + size_t data_length) { + EXPECT_EQ(action, kFlutterSemanticsActionSetSelection); + g_autoptr(FlValue) value = decode_semantic_data(data, data_length); + EXPECT_EQ(fl_value_get_type(value), FL_VALUE_TYPE_MAP); + base = fl_value_get_int(fl_value_lookup_string(value, "base")); + extent = fl_value_get_int(fl_value_lookup_string(value, "extent")); + return kSuccess; + })); + + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + // selection num != 0 + EXPECT_FALSE(atk_text_set_selection(ATK_TEXT(node), 1, 2, 4)); + EXPECT_EQ(base, -1); + EXPECT_EQ(extent, -1); + + EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(node), 0, 2, 4)); + EXPECT_EQ(base, 2); + EXPECT_EQ(extent, 4); + + EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(node), 0, 5, 1)); + EXPECT_EQ(base, 5); + EXPECT_EQ(extent, 1); +} + +// Tests AtkEditableText::set_text_contents. +TEST(FlAccessibleTextFieldTest, SetTextContents) { + g_autofree gchar* text = nullptr; + + g_autoptr(FlEngine) engine = make_mock_engine(); + fl_engine_get_embedder_api(engine)->DispatchSemanticsAction = + MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&text](auto engine, uint64_t id, FlutterSemanticsAction action, + const uint8_t* data, size_t data_length) { + EXPECT_EQ(action, kFlutterSemanticsActionSetText); + g_autoptr(FlValue) value = decode_semantic_data(data, data_length); + EXPECT_EQ(fl_value_get_type(value), FL_VALUE_TYPE_STRING); + text = g_strdup(fl_value_get_string(value)); + return kSuccess; + })); + + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + atk_editable_text_set_text_contents(ATK_EDITABLE_TEXT(node), "Flutter"); + EXPECT_STREQ(text, "Flutter"); +} + +// Tests AtkEditableText::insert/delete_text. +TEST(FlAccessibleTextFieldTest, InsertDeleteText) { + g_autofree gchar* text = nullptr; + int base = -1; + int extent = -1; + + g_autoptr(FlEngine) engine = make_mock_engine(); + fl_engine_get_embedder_api(engine)->DispatchSemanticsAction = + MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&text, &base, &extent](auto engine, uint64_t id, + FlutterSemanticsAction action, + const uint8_t* data, size_t data_length) { + EXPECT_THAT(action, + ::testing::AnyOf(kFlutterSemanticsActionSetText, + kFlutterSemanticsActionSetSelection)); + if (action == kFlutterSemanticsActionSetText) { + g_autoptr(FlValue) value = + decode_semantic_data(data, data_length); + EXPECT_EQ(fl_value_get_type(value), FL_VALUE_TYPE_STRING); + g_free(text); + text = g_strdup(fl_value_get_string(value)); + } else { + g_autoptr(FlValue) value = + decode_semantic_data(data, data_length); + EXPECT_EQ(fl_value_get_type(value), FL_VALUE_TYPE_MAP); + base = fl_value_get_int(fl_value_lookup_string(value, "base")); + extent = + fl_value_get_int(fl_value_lookup_string(value, "extent")); + } + return kSuccess; + })); + + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + fl_accessible_node_set_value(node, "Fler"); + + gint pos = 2; + atk_editable_text_insert_text(ATK_EDITABLE_TEXT(node), "utt", 3, &pos); + EXPECT_EQ(pos, 5); + EXPECT_STREQ(text, "Flutter"); + EXPECT_EQ(base, pos); + EXPECT_EQ(extent, pos); + + atk_editable_text_delete_text(ATK_EDITABLE_TEXT(node), 2, 5); + EXPECT_STREQ(text, "Fler"); + EXPECT_EQ(base, 2); + EXPECT_EQ(extent, 2); +} + +// Tests AtkEditableText::copy/cut/paste_text. +TEST(FlAccessibleTextFieldTest, CopyCutPasteText) { + int base = -1; + int extent = -1; + FlutterSemanticsAction act = kFlutterSemanticsActionCustomAction; + + g_autoptr(FlEngine) engine = make_mock_engine(); + fl_engine_get_embedder_api(engine)->DispatchSemanticsAction = + MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&act, &base, &extent](auto engine, uint64_t id, + FlutterSemanticsAction action, + const uint8_t* data, size_t data_length) { + EXPECT_THAT(action, + ::testing::AnyOf(kFlutterSemanticsActionCut, + kFlutterSemanticsActionCopy, + kFlutterSemanticsActionPaste, + kFlutterSemanticsActionSetSelection)); + act = action; + if (action == kFlutterSemanticsActionSetSelection) { + g_autoptr(FlValue) value = + decode_semantic_data(data, data_length); + EXPECT_EQ(fl_value_get_type(value), FL_VALUE_TYPE_MAP); + base = fl_value_get_int(fl_value_lookup_string(value, "base")); + extent = + fl_value_get_int(fl_value_lookup_string(value, "extent")); + } + return kSuccess; + })); + + g_autoptr(FlAccessibleNode) node = fl_accessible_text_field_new(engine, 1); + + atk_editable_text_copy_text(ATK_EDITABLE_TEXT(node), 2, 5); + EXPECT_EQ(base, 2); + EXPECT_EQ(extent, 5); + EXPECT_EQ(act, kFlutterSemanticsActionCopy); + + atk_editable_text_cut_text(ATK_EDITABLE_TEXT(node), 1, 4); + EXPECT_EQ(base, 1); + EXPECT_EQ(extent, 4); + EXPECT_EQ(act, kFlutterSemanticsActionCut); + + atk_editable_text_paste_text(ATK_EDITABLE_TEXT(node), 3); + EXPECT_EQ(base, 3); + EXPECT_EQ(extent, 3); + EXPECT_EQ(act, kFlutterSemanticsActionPaste); +} + +// NOLINTEND(clang-analyzer-core.StackAddressEscape) diff --git a/shell/platform/linux/fl_view_accessible.cc b/shell/platform/linux/fl_view_accessible.cc index 0bc0e44f489c6..0f0b2ac11787a 100644 --- a/shell/platform/linux/fl_view_accessible.cc +++ b/shell/platform/linux/fl_view_accessible.cc @@ -4,6 +4,8 @@ #include "flutter/shell/platform/linux/fl_view_accessible.h" #include "flutter/shell/platform/linux/fl_accessible_node.h" +#include "flutter/shell/platform/linux/fl_accessible_text_field.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" struct _FlViewAccessible { @@ -13,6 +15,9 @@ struct _FlViewAccessible { // Semantics nodes keyed by ID GHashTable* semantics_nodes_by_id; + + // Child IDs stored until commit_updates is called + GHashTable* pending_children; }; enum { PROP_0, PROP_ENGINE, PROP_LAST }; @@ -36,40 +41,91 @@ static FlEngine* get_engine(FlViewAccessible* self) { return self->engine; } +static FlAccessibleNode* create_node(FlViewAccessible* self, + const FlutterSemanticsNode* semantics) { + FlEngine* engine = get_engine(self); + + if (semantics->flags & kFlutterSemanticsFlagIsTextField) { + return fl_accessible_text_field_new(engine, semantics->id); + } + + return fl_accessible_node_new(engine, semantics->id); +} + +static FlAccessibleNode* lookup_node(FlViewAccessible* self, int32_t id) { + return FL_ACCESSIBLE_NODE( + g_hash_table_lookup(self->semantics_nodes_by_id, GINT_TO_POINTER(id))); +} + // Gets 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))); +static FlAccessibleNode* get_node(FlViewAccessible* self, + const FlutterSemanticsNode* semantics) { + FlAccessibleNode* node = lookup_node(self, semantics->id); if (node != nullptr) { return node; } - FlEngine* engine = get_engine(self); - node = fl_accessible_node_new(engine, id); - if (id == 0) { + 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(id), + g_hash_table_insert(self->semantics_nodes_by_id, + GINT_TO_POINTER(semantics->id), reinterpret_cast(node)); return node; } +static void commit_updates(FlViewAccessible* self) { + g_hash_table_foreach_remove( + self->pending_children, + [](gpointer key, gpointer value, gpointer user_data) -> gboolean { + FlViewAccessible* self = FL_VIEW_ACCESSIBLE(user_data); + + FlAccessibleNode* parent = FL_ACCESSIBLE_NODE(key); + + size_t child_count = fl_value_get_length(static_cast(value)); + const int32_t* children_in_traversal_order = + fl_value_get_int32_list(static_cast(value)); + + g_autoptr(GPtrArray) children = g_ptr_array_new(); + for (size_t i = 0; i < child_count; i++) { + FlAccessibleNode* child = + lookup_node(self, children_in_traversal_order[i]); + g_assert(child != nullptr); + fl_accessible_node_set_parent(child, ATK_OBJECT(parent), i); + g_ptr_array_add(children, child); + } + fl_accessible_node_set_children(parent, children); + + return true; + }, + self); +} + // Implements AtkObject::get_n_children static gint fl_view_accessible_get_n_children(AtkObject* accessible) { + FlViewAccessible* self = FL_VIEW_ACCESSIBLE(accessible); + FlAccessibleNode* node = lookup_node(self, 0); + + if (node == nullptr) { + return 0; + } + return 1; } // Implements AtkObject::ref_child static AtkObject* fl_view_accessible_ref_child(AtkObject* accessible, gint i) { FlViewAccessible* self = FL_VIEW_ACCESSIBLE(accessible); + FlAccessibleNode* node = lookup_node(self, 0); - if (i != 0) { + if (i != 0 || node == nullptr) { return nullptr; } - FlAccessibleNode* node = get_node(self, 0); return ATK_OBJECT(g_object_ref(node)); } @@ -97,6 +153,9 @@ static void fl_view_accessible_set_property(GObject* object, static void fl_view_accessible_dispose(GObject* object) { FlViewAccessible* self = FL_VIEW_ACCESSIBLE(object); + g_clear_pointer(&self->semantics_nodes_by_id, g_hash_table_unref); + g_clear_pointer(&self->pending_children, g_hash_table_unref); + if (self->engine != nullptr) { g_object_remove_weak_pointer(object, reinterpret_cast(&self->engine)); @@ -125,16 +184,20 @@ static void fl_view_accessible_class_init(FlViewAccessibleClass* klass) { 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); + self->pending_children = + g_hash_table_new_full(g_direct_hash, g_direct_equal, nullptr, + reinterpret_cast(fl_value_unref)); } void fl_view_accessible_handle_update_semantics_node( FlViewAccessible* self, const FlutterSemanticsNode* node) { - if (node->id == kFlutterSemanticsCustomActionIdBatchEnd) { + if (node->id == kFlutterSemanticsNodeIdBatchEnd) { + commit_updates(self); return; } - FlAccessibleNode* atk_node = get_node(self, node->id); + FlAccessibleNode* atk_node = get_node(self, node); fl_accessible_node_set_flags(atk_node, node->flags); fl_accessible_node_set_actions(atk_node, node->actions); @@ -143,13 +206,11 @@ void fl_view_accessible_handle_update_semantics_node( 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); + fl_accessible_node_set_value(atk_node, node->value); + fl_accessible_node_set_text_selection(atk_node, node->text_selection_base, + node->text_selection_extent); - 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(atk_node), i); - g_ptr_array_add(children, child); - } - fl_accessible_node_set_children(atk_node, children); + FlValue* children = fl_value_new_int32_list(node->children_in_traversal_order, + node->child_count); + g_hash_table_insert(self->pending_children, atk_node, children); } diff --git a/shell/platform/linux/fl_view_accessible_test.cc b/shell/platform/linux/fl_view_accessible_test.cc index adbe48dc14bf2..bb34774f98c01 100644 --- a/shell/platform/linux/fl_view_accessible_test.cc +++ b/shell/platform/linux/fl_view_accessible_test.cc @@ -10,6 +10,9 @@ #include "flutter/shell/platform/linux/testing/fl_test.h" #include "flutter/shell/platform/linux/testing/mock_signal_handler.h" +static const FlutterSemanticsNode kBatchEndNode = { + .id = kFlutterSemanticsNodeIdBatchEnd}; + TEST(FlViewAccessibleTest, BuildTree) { g_autoptr(FlEngine) engine = make_mock_engine(); g_autoptr(FlViewAccessible) accessible = FL_VIEW_ACCESSIBLE( @@ -30,6 +33,8 @@ TEST(FlViewAccessibleTest, BuildTree) { const FlutterSemanticsNode child2_node = {.id = 222, .label = "child 2"}; fl_view_accessible_handle_update_semantics_node(accessible, &child2_node); + fl_view_accessible_handle_update_semantics_node(accessible, &kBatchEndNode); + AtkObject* root_object = atk_object_ref_accessible_child(ATK_OBJECT(accessible), 0); EXPECT_STREQ(atk_object_get_name(root_object), "root"); @@ -61,6 +66,8 @@ TEST(FlViewAccessibleTest, AddRemoveChildren) { }; fl_view_accessible_handle_update_semantics_node(accessible, &root_node); + fl_view_accessible_handle_update_semantics_node(accessible, &kBatchEndNode); + AtkObject* root_object = atk_object_ref_accessible_child(ATK_OBJECT(accessible), 0); EXPECT_EQ(atk_object_get_n_accessible_children(root_object), 0); @@ -80,6 +87,8 @@ TEST(FlViewAccessibleTest, AddRemoveChildren) { const FlutterSemanticsNode child1_node = {.id = 111, .label = "child 1"}; fl_view_accessible_handle_update_semantics_node(accessible, &child1_node); + + fl_view_accessible_handle_update_semantics_node(accessible, &kBatchEndNode); } EXPECT_EQ(atk_object_get_n_accessible_children(root_object), 1); @@ -105,6 +114,8 @@ TEST(FlViewAccessibleTest, AddRemoveChildren) { const FlutterSemanticsNode child2_node = {.id = 222, .label = "child 2"}; fl_view_accessible_handle_update_semantics_node(accessible, &child2_node); + + fl_view_accessible_handle_update_semantics_node(accessible, &kBatchEndNode); } EXPECT_EQ(atk_object_get_n_accessible_children(root_object), 2); @@ -132,6 +143,8 @@ TEST(FlViewAccessibleTest, AddRemoveChildren) { root_node.child_count = 1; root_node.children_in_traversal_order = children; fl_view_accessible_handle_update_semantics_node(accessible, &root_node); + + fl_view_accessible_handle_update_semantics_node(accessible, &kBatchEndNode); } EXPECT_EQ(atk_object_get_n_accessible_children(root_object), 1); @@ -151,6 +164,8 @@ TEST(FlViewAccessibleTest, AddRemoveChildren) { root_node.child_count = 0; fl_view_accessible_handle_update_semantics_node(accessible, &root_node); + + fl_view_accessible_handle_update_semantics_node(accessible, &kBatchEndNode); } EXPECT_EQ(atk_object_get_n_accessible_children(root_object), 0); diff --git a/shell/platform/linux/testing/mock_engine.cc b/shell/platform/linux/testing/mock_engine.cc index ccb1cb1c0c448..0c53f8c15ffd6 100644 --- a/shell/platform/linux/testing/mock_engine.cc +++ b/shell/platform/linux/testing/mock_engine.cc @@ -19,6 +19,7 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_method_codec.h" #include "gtest/gtest.h" +const int32_t kFlutterSemanticsNodeIdBatchEnd = -1; const int32_t kFlutterSemanticsCustomActionIdBatchEnd = -1; struct _FlutterEngineTexture {