| // 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/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 { | |
| GtkContainerAccessible parent_instance; | |
| FlEngine* engine; | |
| // Semantics nodes keyed by ID | |
| GHashTable* semantics_nodes_by_id; | |
| // Child IDs stored until commit_updates is called | |
| GHashTable* pending_children; | |
| }; | |
| enum { kProp0, kPropEngine, kPropLast }; | |
| G_DEFINE_TYPE(FlViewAccessible, | |
| fl_view_accessible, | |
| GTK_TYPE_CONTAINER_ACCESSIBLE) | |
| static void init_engine(FlViewAccessible* self, FlEngine* engine) { | |
| g_assert(self->engine == nullptr); | |
| self->engine = engine; | |
| g_object_add_weak_pointer(G_OBJECT(self), | |
| reinterpret_cast<gpointer*>(&self->engine)); | |
| } | |
| static FlEngine* get_engine(FlViewAccessible* self) { | |
| if (self->engine == nullptr) { | |
| FlView* view = FL_VIEW(gtk_accessible_get_widget(GTK_ACCESSIBLE(self))); | |
| init_engine(self, fl_view_get_engine(view)); | |
| } | |
| return self->engine; | |
| } | |
| 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, | |
| const FlutterSemanticsNode* semantics) { | |
| FlAccessibleNode* node = lookup_node(self, semantics->id); | |
| if (node != nullptr) { | |
| return node; | |
| } | |
| node = create_node(self, semantics); | |
| if (semantics->id == 0) { | |
| fl_accessible_node_set_parent(node, ATK_OBJECT(self), 0); | |
| g_signal_emit_by_name(self, "children-changed::add", 0, node, nullptr); | |
| } | |
| g_hash_table_insert(self->semantics_nodes_by_id, | |
| GINT_TO_POINTER(semantics->id), | |
| reinterpret_cast<gpointer>(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<FlValue*>(value)); | |
| const int32_t* children_in_traversal_order = | |
| fl_value_get_int32_list(static_cast<FlValue*>(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 || node == nullptr) { | |
| return nullptr; | |
| } | |
| return ATK_OBJECT(g_object_ref(node)); | |
| } | |
| // Implements AtkObject::get_role | |
| static AtkRole fl_view_accessible_get_role(AtkObject* accessible) { | |
| return ATK_ROLE_PANEL; | |
| } | |
| // Implements GObject::set_property | |
| static void fl_view_accessible_set_property(GObject* object, | |
| guint prop_id, | |
| const GValue* value, | |
| GParamSpec* pspec) { | |
| FlViewAccessible* self = FL_VIEW_ACCESSIBLE(object); | |
| switch (prop_id) { | |
| case kPropEngine: | |
| init_engine(self, FL_ENGINE(g_value_get_object(value))); | |
| break; | |
| default: | |
| G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); | |
| break; | |
| } | |
| } | |
| 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<gpointer*>(&self->engine)); | |
| self->engine = nullptr; | |
| } | |
| G_OBJECT_CLASS(fl_view_accessible_parent_class)->dispose(object); | |
| } | |
| 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; | |
| G_OBJECT_CLASS(klass)->dispose = fl_view_accessible_dispose; | |
| G_OBJECT_CLASS(klass)->set_property = fl_view_accessible_set_property; | |
| g_object_class_install_property( | |
| G_OBJECT_CLASS(klass), kPropEngine, | |
| g_param_spec_object( | |
| "engine", "engine", "Flutter engine", fl_engine_get_type(), | |
| static_cast<GParamFlags>(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | | |
| G_PARAM_STATIC_STRINGS))); | |
| } | |
| static void fl_view_accessible_init(FlViewAccessible* self) { | |
| 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<GDestroyNotify>(fl_value_unref)); | |
| } | |
| void fl_view_accessible_handle_update_semantics_node( | |
| FlViewAccessible* self, | |
| const FlutterSemanticsNode* node) { | |
| if (node->id == kFlutterSemanticsNodeIdBatchEnd) { | |
| commit_updates(self); | |
| return; | |
| } | |
| 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); | |
| 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); | |
| 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); | |
| fl_accessible_node_set_text_direction(atk_node, node->text_direction); | |
| 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); | |
| } |