diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index cc116ffcef933..c7baa1e5e7986 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2580,9 +2580,9 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterPlug FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Info.plist -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegateTest.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.mm @@ -3079,9 +3079,9 @@ FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_view.h FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/flutter_linux.h FILE: ../../../flutter/shell/platform/windows/accessibility_alert.cc FILE: ../../../flutter/shell/platform/windows/accessibility_alert.h -FILE: ../../../flutter/shell/platform/windows/accessibility_bridge_delegate_windows.cc -FILE: ../../../flutter/shell/platform/windows/accessibility_bridge_delegate_windows.h -FILE: ../../../flutter/shell/platform/windows/accessibility_bridge_delegate_windows_unittests.cc +FILE: ../../../flutter/shell/platform/windows/accessibility_bridge_windows.cc +FILE: ../../../flutter/shell/platform/windows/accessibility_bridge_windows.h +FILE: ../../../flutter/shell/platform/windows/accessibility_bridge_windows_unittests.cc FILE: ../../../flutter/shell/platform/windows/accessibility_root_node.cc FILE: ../../../flutter/shell/platform/windows/accessibility_root_node.h FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.cc diff --git a/shell/platform/common/accessibility_bridge.cc b/shell/platform/common/accessibility_bridge.cc index 0482abe70dc3c..f95998d15b167 100644 --- a/shell/platform/common/accessibility_bridge.cc +++ b/shell/platform/common/accessibility_bridge.cc @@ -19,9 +19,7 @@ constexpr int kHasScrollingAction = FlutterSemanticsAction::kFlutterSemanticsActionScrollDown; // AccessibilityBridge -AccessibilityBridge::AccessibilityBridge( - std::unique_ptr delegate) - : delegate_(std::move(delegate)) { +AccessibilityBridge::AccessibilityBridge() { event_generator_.SetTree(&tree_); tree_.AddObserver(static_cast(this)); } @@ -107,7 +105,7 @@ void AccessibilityBridge::CommitUpdates() { continue; } - delegate_->OnAccessibilityEvent(targeted_event); + OnAccessibilityEvent(targeted_event); } event_generator_.ClearEvents(); } @@ -128,20 +126,16 @@ const ui::AXTreeData& AccessibilityBridge::GetAXTreeData() const { } const std::vector -AccessibilityBridge::GetPendingEvents() { +AccessibilityBridge::GetPendingEvents() const { std::vector result( event_generator_.begin(), event_generator_.end()); return result; } -void AccessibilityBridge::UpdateDelegate( - std::unique_ptr delegate) { - delegate_ = std::move(delegate); - // Recreate FlutterPlatformNodeDelegates since they may contain stale state - // from the previous AccessibilityBridgeDelegate. +void AccessibilityBridge::RecreateNodeDelegates() { for (const auto& [node_id, old_platform_node_delegate] : id_wrapper_map_) { std::shared_ptr platform_node_delegate = - delegate_->CreateFlutterPlatformNodeDelegate(); + CreateFlutterPlatformNodeDelegate(); platform_node_delegate->Init( std::static_pointer_cast( shared_from_this()), @@ -166,7 +160,7 @@ void AccessibilityBridge::OnRoleChanged(ui::AXTree* tree, void AccessibilityBridge::OnNodeCreated(ui::AXTree* tree, ui::AXNode* node) { BASE_DCHECK(node); - id_wrapper_map_[node->id()] = delegate_->CreateFlutterPlatformNodeDelegate(); + id_wrapper_map_[node->id()] = CreateFlutterPlatformNodeDelegate(); id_wrapper_map_[node->id()]->Init( std::static_pointer_cast( shared_from_this()), @@ -629,7 +623,7 @@ void AccessibilityBridge::SetLastFocusedId(AccessibilityNodeId node_id) { auto last_focused_child = GetFlutterPlatformNodeDelegateFromID(last_focused_id_); if (!last_focused_child.expired()) { - delegate_->DispatchAccessibilityAction( + DispatchAccessibilityAction( last_focused_id_, FlutterSemanticsAction:: kFlutterSemanticsActionDidLoseAccessibilityFocus, @@ -659,11 +653,4 @@ gfx::RectF AccessibilityBridge::RelativeToGlobalBounds(const ui::AXNode* node, clip_bounds); } -void AccessibilityBridge::DispatchAccessibilityAction( - AccessibilityNodeId target, - FlutterSemanticsAction action, - fml::MallocMapping data) { - delegate_->DispatchAccessibilityAction(target, action, std::move(data)); -} - } // namespace flutter diff --git a/shell/platform/common/accessibility_bridge.h b/shell/platform/common/accessibility_bridge.h index 2eec7f7d939a8..fe88f27fa91e1 100644 --- a/shell/platform/common/accessibility_bridge.h +++ b/shell/platform/common/accessibility_bridge.h @@ -31,85 +31,20 @@ namespace flutter { /// FlutterPlatformNodeDelegate to wrap each AXNode in order to provide /// an accessibility tree in the native format. /// -/// This class takes in a AccessibilityBridgeDelegate instance and is in charge -/// of its lifecycle. The delegate are used to handle the accessibility events -/// and actions. +/// To use this class, one must subclass this class and provide their own +/// implementation of FlutterPlatformNodeDelegate. /// -/// To use this class, you must provide your own implementation of -/// FlutterPlatformNodeDelegate and AccessibilityBridgeDelegate. +/// AccessibilityBridge must be created as a shared_ptr, since some methods +/// acquires its weak_ptr. class AccessibilityBridge : public std::enable_shared_from_this, public FlutterPlatformNodeDelegate::OwnerBridge, private ui::AXTreeObserver { public: - //----------------------------------------------------------------------------- - /// Delegate to handle requests from the accessibility bridge. The requests - /// include sending accessibility event to native accessibility system, - /// routing accessibility action to the Flutter framework, and creating - /// platform specific FlutterPlatformNodeDelegate. - /// - /// The accessibility events are generated when accessibility tree changes. - /// These events must be sent to the native accessibility system through - /// the native API for the system to pick up the changes - /// (e.g. NSAccessibilityPostNotification in MacOS). - /// - /// The accessibility actions are generated by the native accessibility system - /// when users interacted with the assistive technologies. Those actions - /// needed to be sent to the Flutter framework. - /// - /// Each platform needs to implement the FlutterPlatformNodeDelegate and - /// returns its platform specific instance of FlutterPlatformNodeDelegate - /// in this delegate. - class AccessibilityBridgeDelegate { - public: - virtual ~AccessibilityBridgeDelegate() = default; - //--------------------------------------------------------------------------- - /// @brief Handle accessibility events generated due to accessibility - /// tree changes. These events are generated in accessibility - /// bridge and needed to be sent to native accessibility system. - /// See ui::AXEventGenerator::Event for possible events. - /// - /// @param[in] targeted_event The object that contains both the - /// generated event and the event target. - virtual void OnAccessibilityEvent( - ui::AXEventGenerator::TargetedEvent targeted_event) = 0; - - //--------------------------------------------------------------------------- - /// @brief Dispatch accessibility action back to the Flutter framework. - /// These actions are generated in the native accessibility - /// system when users interact with the assistive technologies. - /// For example, a - /// FlutterSemanticsAction::kFlutterSemanticsActionTap is - /// fired when user click or touch the screen. - /// - /// @param[in] target The semantics node id of the action - /// target. - /// @param[in] action The generated flutter semantics action. - /// @param[in] data Additional data associated with the - /// action. - virtual void DispatchAccessibilityAction(AccessibilityNodeId target, - FlutterSemanticsAction action, - fml::MallocMapping data) = 0; - - //--------------------------------------------------------------------------- - /// @brief Creates a platform specific FlutterPlatformNodeDelegate. - /// Ownership passes to the caller. This method will be called - /// by accessibility bridge whenever a new AXNode is created in - /// AXTree. Each platform needs to implement this method in - /// order to inject its subclass into the accessibility bridge. - virtual std::shared_ptr - CreateFlutterPlatformNodeDelegate() = 0; - }; - //----------------------------------------------------------------------------- /// @brief Creates a new instance of a accessibility bridge. - /// - /// @param[in] user_data A custom pointer to the data of your - /// choice. This pointer can be retrieve later - /// through GetUserData(). - explicit AccessibilityBridge( - std::unique_ptr delegate); - ~AccessibilityBridge(); + AccessibilityBridge(); + virtual ~AccessibilityBridge(); //----------------------------------------------------------------------------- /// @brief The ID of the root node in the accessibility tree. In Flutter, @@ -168,12 +103,39 @@ class AccessibilityBridge /// events in AccessibilityBridgeDelegate::OnAccessibilityEvent in /// case one may decide to handle an event differently based on /// all pending events. - const std::vector GetPendingEvents(); + const std::vector GetPendingEvents() + const; + + protected: + //--------------------------------------------------------------------------- + /// @brief Handle accessibility events generated due to accessibility + /// tree changes. These events are needed to be sent to native + /// accessibility system. See ui::AXEventGenerator::Event for + /// possible events. + /// + /// @param[in] targeted_event The object that contains both the + /// generated event and the event target. + virtual void OnAccessibilityEvent( + ui::AXEventGenerator::TargetedEvent targeted_event) = 0; + + //--------------------------------------------------------------------------- + /// @brief Creates a platform specific FlutterPlatformNodeDelegate. + /// Ownership passes to the caller. This method will be called + /// whenever a new AXNode is created in AXTree. Each platform + /// needs to implement this method in order to inject its + /// subclass into the accessibility bridge. + virtual std::shared_ptr + CreateFlutterPlatformNodeDelegate() = 0; //------------------------------------------------------------------------------ - /// @brief Update the AccessibilityBridgeDelegate stored in the - /// accessibility bridge to a new one. - void UpdateDelegate(std::unique_ptr delegate); + /// @brief Recreate all FlutterPlatformNodeDelegates. + /// + /// This can be useful for subclasses when updating some + /// properties that are used by node delegates, such as views. + /// Each node is recreated using + /// CreateFlutterPlatformNodeDelegate, then initialized using + /// AXNodes from their corresponding old one. + void RecreateNodeDelegates(); private: // See FlutterSemanticsNode in embedder.h @@ -220,7 +182,6 @@ class AccessibilityBridge std::unordered_map pending_semantics_custom_action_updates_; AccessibilityNodeId last_focused_id_ = ui::AXNode::kInvalidAXID; - std::unique_ptr delegate_; void InitAXTree(const ui::AXTreeUpdate& initial_state); @@ -295,11 +256,6 @@ class AccessibilityBridge gfx::NativeViewAccessible GetNativeAccessibleFromId( AccessibilityNodeId id) override; - // |FlutterPlatformNodeDelegate::OwnerBridge| - void DispatchAccessibilityAction(AccessibilityNodeId target, - FlutterSemanticsAction action, - fml::MallocMapping data) override; - // |FlutterPlatformNodeDelegate::OwnerBridge| gfx::RectF RelativeToGlobalBounds(const ui::AXNode* node, bool& offscreen, diff --git a/shell/platform/common/accessibility_bridge_unittests.cc b/shell/platform/common/accessibility_bridge_unittests.cc index 6b6c2129f5a25..5dc52393ec737 100644 --- a/shell/platform/common/accessibility_bridge_unittests.cc +++ b/shell/platform/common/accessibility_bridge_unittests.cc @@ -37,9 +37,8 @@ FlutterSemanticsNode CreateSemanticsNode( } TEST(AccessibilityBridgeTest, basicTest) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); std::vector children{1, 2}; FlutterSemanticsNode root = CreateSemanticsNode(0, "root", &children); @@ -67,11 +66,8 @@ TEST(AccessibilityBridgeTest, basicTest) { } TEST(AccessibilityBridgeTest, canFireChildrenChangedCorrectly) { - TestAccessibilityBridgeDelegate* delegate = - new TestAccessibilityBridgeDelegate(); - std::unique_ptr ptr(delegate); - std::shared_ptr bridge = - std::make_shared(std::move(ptr)); + std::shared_ptr bridge = + std::make_shared(); std::vector children{1}; FlutterSemanticsNode root = CreateSemanticsNode(0, "root", &children); @@ -90,7 +86,7 @@ TEST(AccessibilityBridgeTest, canFireChildrenChangedCorrectly) { EXPECT_EQ(child1_node->GetChildCount(), 0); EXPECT_EQ(child1_node->GetName(), "child 1"); - delegate->accessibility_events.clear(); + bridge->accessibility_events.clear(); // Add a child to root. root.child_count = 2; @@ -108,20 +104,18 @@ TEST(AccessibilityBridgeTest, canFireChildrenChangedCorrectly) { EXPECT_EQ(root_node->GetChildCount(), 2); EXPECT_EQ(root_node->GetData().child_ids[0], 1); EXPECT_EQ(root_node->GetData().child_ids[1], 2); - EXPECT_EQ(delegate->accessibility_events.size(), size_t{2}); + EXPECT_EQ(bridge->accessibility_events.size(), size_t{2}); std::set actual_event{ - delegate->accessibility_events.begin(), - delegate->accessibility_events.end()}; + bridge->accessibility_events.begin(), bridge->accessibility_events.end()}; EXPECT_THAT(actual_event, Contains(ui::AXEventGenerator::Event::CHILDREN_CHANGED)); EXPECT_THAT(actual_event, Contains(ui::AXEventGenerator::Event::SUBTREE_CREATED)); } -TEST(AccessibilityBridgeTest, canUpdateDelegate) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); +TEST(AccessibilityBridgeTest, canRecreateNodeDelegates) { + std::shared_ptr bridge = + std::make_shared(); std::vector children{1}; FlutterSemanticsNode root = CreateSemanticsNode(0, "root", &children); @@ -135,8 +129,8 @@ TEST(AccessibilityBridgeTest, canUpdateDelegate) { auto child1_node = bridge->GetFlutterPlatformNodeDelegateFromID(1); EXPECT_FALSE(root_node.expired()); EXPECT_FALSE(child1_node.expired()); - // Update Delegate - bridge->UpdateDelegate(std::make_unique()); + + bridge->RecreateNodeDelegates(); // Old tree is destroyed. EXPECT_TRUE(root_node.expired()); @@ -154,11 +148,8 @@ TEST(AccessibilityBridgeTest, canUpdateDelegate) { } TEST(AccessibilityBridgeTest, canHandleSelectionChangeCorrectly) { - TestAccessibilityBridgeDelegate* delegate = - new TestAccessibilityBridgeDelegate(); - std::unique_ptr ptr(delegate); - std::shared_ptr bridge = - std::make_shared(std::move(ptr)); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root = CreateSemanticsNode(0, "root"); root.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField; bridge->AddFlutterSemanticsNodeUpdate(&root); @@ -166,7 +157,7 @@ TEST(AccessibilityBridgeTest, canHandleSelectionChangeCorrectly) { const ui::AXTreeData& tree = bridge->GetAXTreeData(); EXPECT_EQ(tree.sel_anchor_object_id, ui::AXNode::kInvalidAXID); - delegate->accessibility_events.clear(); + bridge->accessibility_events.clear(); // Update the selection. root.text_selection_base = 0; @@ -179,17 +170,16 @@ TEST(AccessibilityBridgeTest, canHandleSelectionChangeCorrectly) { EXPECT_EQ(tree.sel_anchor_offset, 0); EXPECT_EQ(tree.sel_focus_object_id, 0); EXPECT_EQ(tree.sel_focus_offset, 5); - ASSERT_EQ(delegate->accessibility_events.size(), size_t{2}); - EXPECT_EQ(delegate->accessibility_events[0], + ASSERT_EQ(bridge->accessibility_events.size(), size_t{2}); + EXPECT_EQ(bridge->accessibility_events[0], ui::AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED); - EXPECT_EQ(delegate->accessibility_events[1], + EXPECT_EQ(bridge->accessibility_events[1], ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED); } TEST(AccessibilityBridgeTest, doesNotAssignEditableRootToSelectableText) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root = CreateSemanticsNode(0, "root"); root.flags = static_cast( FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField | @@ -204,9 +194,8 @@ TEST(AccessibilityBridgeTest, doesNotAssignEditableRootToSelectableText) { } TEST(AccessibilityBridgeTest, ToggleHasToggleButtonRole) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root = CreateSemanticsNode(0, "root"); root.flags = static_cast( FlutterSemanticsFlag::kFlutterSemanticsFlagHasToggledState | @@ -220,9 +209,8 @@ TEST(AccessibilityBridgeTest, ToggleHasToggleButtonRole) { } TEST(AccessibilityBridgeTest, SliderHasSliderRole) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root = CreateSemanticsNode(0, "root"); root.flags = static_cast( FlutterSemanticsFlag::kFlutterSemanticsFlagIsSlider | @@ -242,9 +230,8 @@ TEST(AccessibilityBridgeTest, SliderHasSliderRole) { // https://github.com/flutter/flutter/issues/96218 // As this fix involved code run on all platforms, it is included here. TEST(AccessibilityBridgeTest, CanSetCheckboxChecked) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root = CreateSemanticsNode(0, "root"); root.flags = static_cast( FlutterSemanticsFlag::kFlutterSemanticsFlagHasCheckedState | @@ -260,11 +247,8 @@ TEST(AccessibilityBridgeTest, CanSetCheckboxChecked) { // Verify that a node can be moved from one parent to another. TEST(AccessibilityBridgeTest, CanReparentNode) { - TestAccessibilityBridgeDelegate* delegate = - new TestAccessibilityBridgeDelegate(); - std::unique_ptr ptr(delegate); - std::shared_ptr bridge = - std::make_shared(std::move(ptr)); + std::shared_ptr bridge = + std::make_shared(); std::vector root_children{1}; std::vector child1_children{2}; @@ -277,7 +261,7 @@ TEST(AccessibilityBridgeTest, CanReparentNode) { bridge->AddFlutterSemanticsNodeUpdate(&child1); bridge->AddFlutterSemanticsNodeUpdate(&child2); bridge->CommitUpdates(); - delegate->accessibility_events.clear(); + bridge->accessibility_events.clear(); // Reparent child2 from child1 to the root. child1.child_count = 0; @@ -307,30 +291,27 @@ TEST(AccessibilityBridgeTest, CanReparentNode) { EXPECT_EQ(child2_node->GetChildCount(), 0); EXPECT_EQ(child2_node->GetName(), "child 2"); - ASSERT_EQ(delegate->accessibility_events.size(), size_t{5}); + ASSERT_EQ(bridge->accessibility_events.size(), size_t{5}); // Child2 is moved from child1 to root. - EXPECT_THAT(delegate->accessibility_events, + EXPECT_THAT(bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::CHILDREN_CHANGED).Times(2)); - EXPECT_THAT(delegate->accessibility_events, + EXPECT_THAT(bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::SUBTREE_CREATED).Times(1)); // Child1 is no longer a parent. It loses its group role and disables its // 'clip children' attribute. EXPECT_THAT( - delegate->accessibility_events, + bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED).Times(1)); - EXPECT_THAT(delegate->accessibility_events, + EXPECT_THAT(bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::ROLE_CHANGED).Times(1)); } // Verify that multiple nodes can be moved to new parents. TEST(AccessibilityBridgeTest, CanReparentMultipleNodes) { - TestAccessibilityBridgeDelegate* delegate = - new TestAccessibilityBridgeDelegate(); - std::unique_ptr ptr(delegate); - std::shared_ptr bridge = - std::make_shared(std::move(ptr)); + std::shared_ptr bridge = + std::make_shared(); int32_t root_id = 0; int32_t intermediary1_id = 1; @@ -359,7 +340,7 @@ TEST(AccessibilityBridgeTest, CanReparentMultipleNodes) { bridge->AddFlutterSemanticsNodeUpdate(&leaf2); bridge->AddFlutterSemanticsNodeUpdate(&leaf3); bridge->CommitUpdates(); - delegate->accessibility_events.clear(); + bridge->accessibility_events.clear(); // Swap intermediary 1's and intermediary2's children. int32_t new_intermediary1_children[] = {leaf2_id, leaf3_id}; @@ -414,20 +395,17 @@ TEST(AccessibilityBridgeTest, CanReparentMultipleNodes) { // Intermediary 1 and intermediary 2 have new children. // Leaf 1, 2, and 3 are all moved. - ASSERT_EQ(delegate->accessibility_events.size(), size_t{5}); - EXPECT_THAT(delegate->accessibility_events, + ASSERT_EQ(bridge->accessibility_events.size(), size_t{5}); + EXPECT_THAT(bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::CHILDREN_CHANGED).Times(2)); - EXPECT_THAT(delegate->accessibility_events, + EXPECT_THAT(bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::SUBTREE_CREATED).Times(3)); } // Verify that a node with a child can be moved from one parent to another. TEST(AccessibilityBridgeTest, CanReparentNodeWithChild) { - TestAccessibilityBridgeDelegate* delegate = - new TestAccessibilityBridgeDelegate(); - std::unique_ptr ptr(delegate); - std::shared_ptr bridge = - std::make_shared(std::move(ptr)); + std::shared_ptr bridge = + std::make_shared(); int32_t root_id = 0; int32_t intermediary1_id = 1; @@ -449,7 +427,7 @@ TEST(AccessibilityBridgeTest, CanReparentNodeWithChild) { bridge->AddFlutterSemanticsNodeUpdate(&intermediary2); bridge->AddFlutterSemanticsNodeUpdate(&leaf1); bridge->CommitUpdates(); - delegate->accessibility_events.clear(); + bridge->accessibility_events.clear(); // Move intermediary1 from root to intermediary 2. int32_t new_root_children[] = {intermediary2_id}; @@ -489,19 +467,19 @@ TEST(AccessibilityBridgeTest, CanReparentNodeWithChild) { EXPECT_EQ(leaf1_node->GetChildCount(), 0); EXPECT_EQ(leaf1_node->GetName(), "leaf 1"); - ASSERT_EQ(delegate->accessibility_events.size(), size_t{5}); + ASSERT_EQ(bridge->accessibility_events.size(), size_t{5}); - EXPECT_THAT(delegate->accessibility_events, + EXPECT_THAT(bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::CHILDREN_CHANGED).Times(2)); - EXPECT_THAT(delegate->accessibility_events, + EXPECT_THAT(bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::SUBTREE_CREATED).Times(1)); // Intermediary 2 becomes a parent node. It updates to group role and enables // its 'clip children' attribute. EXPECT_THAT( - delegate->accessibility_events, + bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED).Times(1)); - EXPECT_THAT(delegate->accessibility_events, + EXPECT_THAT(bridge->accessibility_events, Contains(ui::AXEventGenerator::Event::ROLE_CHANGED).Times(1)); } diff --git a/shell/platform/common/flutter_platform_node_delegate_unittests.cc b/shell/platform/common/flutter_platform_node_delegate_unittests.cc index aa25e2457186d..cc91ae0506736 100644 --- a/shell/platform/common/flutter_platform_node_delegate_unittests.cc +++ b/shell/platform/common/flutter_platform_node_delegate_unittests.cc @@ -13,11 +13,8 @@ namespace flutter { namespace testing { TEST(FlutterPlatformNodeDelegateTest, NodeDelegateHasUniqueId) { - TestAccessibilityBridgeDelegate* delegate = - new TestAccessibilityBridgeDelegate(); - std::unique_ptr ptr(delegate); - std::shared_ptr bridge = - std::make_shared(std::move(ptr)); + std::shared_ptr bridge = + std::make_shared(); // Add node 0: root. FlutterSemanticsNode node0{sizeof(FlutterSemanticsNode), 0}; @@ -41,11 +38,8 @@ TEST(FlutterPlatformNodeDelegateTest, NodeDelegateHasUniqueId) { } TEST(FlutterPlatformNodeDelegateTest, canPerfomActions) { - TestAccessibilityBridgeDelegate* delegate = - new TestAccessibilityBridgeDelegate(); - std::unique_ptr ptr(delegate); - std::shared_ptr bridge = - std::make_shared(std::move(ptr)); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root; root.id = 0; root.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField; @@ -69,29 +63,28 @@ TEST(FlutterPlatformNodeDelegateTest, canPerfomActions) { ui::AXActionData action_data; action_data.action = ax::mojom::Action::kDoDefault; accessibility->AccessibilityPerformAction(action_data); - EXPECT_EQ(delegate->performed_actions.size(), size_t{1}); - EXPECT_EQ(delegate->performed_actions[0], + EXPECT_EQ(bridge->performed_actions.size(), size_t{1}); + EXPECT_EQ(bridge->performed_actions[0], FlutterSemanticsAction::kFlutterSemanticsActionTap); action_data.action = ax::mojom::Action::kFocus; accessibility->AccessibilityPerformAction(action_data); - EXPECT_EQ(delegate->performed_actions.size(), size_t{2}); + EXPECT_EQ(bridge->performed_actions.size(), size_t{2}); EXPECT_EQ( - delegate->performed_actions[1], + bridge->performed_actions[1], FlutterSemanticsAction::kFlutterSemanticsActionDidGainAccessibilityFocus); action_data.action = ax::mojom::Action::kScrollToMakeVisible; accessibility->AccessibilityPerformAction(action_data); - EXPECT_EQ(delegate->performed_actions.size(), size_t{3}); - EXPECT_EQ(delegate->performed_actions[2], + EXPECT_EQ(bridge->performed_actions.size(), size_t{3}); + EXPECT_EQ(bridge->performed_actions[2], FlutterSemanticsAction::kFlutterSemanticsActionShowOnScreen); } TEST(FlutterPlatformNodeDelegateTest, canGetAXNode) { // Set up a flutter accessibility node. - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root; root.id = 0; root.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField; @@ -115,9 +108,8 @@ TEST(FlutterPlatformNodeDelegateTest, canGetAXNode) { } TEST(FlutterPlatformNodeDelegateTest, canCalculateBoundsCorrectly) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root; root.id = 0; root.label = "root"; @@ -162,9 +154,8 @@ TEST(FlutterPlatformNodeDelegateTest, canCalculateBoundsCorrectly) { } TEST(FlutterPlatformNodeDelegateTest, canCalculateOffScreenBoundsCorrectly) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root; root.id = 0; root.label = "root"; @@ -209,9 +200,8 @@ TEST(FlutterPlatformNodeDelegateTest, canCalculateOffScreenBoundsCorrectly) { } TEST(FlutterPlatformNodeDelegateTest, canUseOwnerBridge) { - std::shared_ptr bridge = - std::make_shared( - std::make_unique()); + std::shared_ptr bridge = + std::make_shared(); FlutterSemanticsNode root; root.id = 0; root.label = "root"; diff --git a/shell/platform/common/test_accessibility_bridge.cc b/shell/platform/common/test_accessibility_bridge.cc index 299f25a9f2536..aa1a9def262c6 100644 --- a/shell/platform/common/test_accessibility_bridge.cc +++ b/shell/platform/common/test_accessibility_bridge.cc @@ -7,16 +7,16 @@ namespace flutter { std::shared_ptr -TestAccessibilityBridgeDelegate::CreateFlutterPlatformNodeDelegate() { +TestAccessibilityBridge::CreateFlutterPlatformNodeDelegate() { return std::make_unique(); }; -void TestAccessibilityBridgeDelegate::OnAccessibilityEvent( +void TestAccessibilityBridge::OnAccessibilityEvent( ui::AXEventGenerator::TargetedEvent targeted_event) { accessibility_events.push_back(targeted_event.event_params.event); } -void TestAccessibilityBridgeDelegate::DispatchAccessibilityAction( +void TestAccessibilityBridge::DispatchAccessibilityAction( AccessibilityNodeId target, FlutterSemanticsAction action, fml::MallocMapping data) { diff --git a/shell/platform/common/test_accessibility_bridge.h b/shell/platform/common/test_accessibility_bridge.h index bb833cb2ea0fa..c81a3b59d1a14 100644 --- a/shell/platform/common/test_accessibility_bridge.h +++ b/shell/platform/common/test_accessibility_bridge.h @@ -9,21 +9,25 @@ namespace flutter { -class TestAccessibilityBridgeDelegate - : public AccessibilityBridge::AccessibilityBridgeDelegate { +class TestAccessibilityBridge : public AccessibilityBridge { public: - TestAccessibilityBridgeDelegate() = default; + using AccessibilityBridge::RecreateNodeDelegates; + + TestAccessibilityBridge() = default; - void OnAccessibilityEvent( - ui::AXEventGenerator::TargetedEvent targeted_event) override; void DispatchAccessibilityAction(AccessibilityNodeId target, FlutterSemanticsAction action, fml::MallocMapping data) override; - std::shared_ptr - CreateFlutterPlatformNodeDelegate() override; std::vector accessibility_events; std::vector performed_actions; + + protected: + void OnAccessibilityEvent( + ui::AXEventGenerator::TargetedEvent targeted_event) override; + + std::shared_ptr + CreateFlutterPlatformNodeDelegate() override; }; } // namespace flutter diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index abe1fc3f76cb2..72ba77e0aed1b 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -54,8 +54,8 @@ source_set("flutter_framework_source") { visibility = [ ":*" ] sources = [ - "framework/Source/AccessibilityBridgeMacDelegate.h", - "framework/Source/AccessibilityBridgeMacDelegate.mm", + "framework/Source/AccessibilityBridgeMac.h", + "framework/Source/AccessibilityBridgeMac.mm", "framework/Source/FlutterAppDelegate.mm", "framework/Source/FlutterBackingStore.h", "framework/Source/FlutterBackingStore.mm", @@ -183,7 +183,7 @@ executable("flutter_desktop_darwin_unittests") { testonly = true sources = [ - "framework/Source/AccessibilityBridgeMacDelegateTest.mm", + "framework/Source/AccessibilityBridgeMacTest.mm", "framework/Source/FlutterChannelKeyResponderUnittests.mm", "framework/Source/FlutterEmbedderExternalTextureUnittests.mm", "framework/Source/FlutterEmbedderKeyResponderUnittests.mm", diff --git a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h similarity index 75% rename from shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h rename to shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h index 772234b5b606c..0d7689fbc91fc 100644 --- a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h +++ b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_ACCESSIBILITYBRIDGEMACDELEGATE_H_ -#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_ACCESSIBILITYBRIDGEMACDELEGATE_H_ +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_ACCESSIBILITY_BRIDGE_MAC_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_ACCESSIBILITY_BRIDGE_MAC_H_ #import @@ -15,27 +15,41 @@ namespace flutter { //------------------------------------------------------------------------------ -/// The macOS implementation of AccessibilityBridge::AccessibilityBridgeDelegate. -/// This delegate is used to create AccessibilityBridge in the macOS embedding. -class AccessibilityBridgeMacDelegate : public AccessibilityBridge::AccessibilityBridgeDelegate { +/// The macOS implementation of AccessibilityBridge. +/// +/// This interacts with macOS accessibility APIs, which includes routing +/// accessibility events fired from the framework to macOS, routing native +/// macOS accessibility events to the framework, and creating macOS-specific +/// FlutterPlatformNodeDelegate objects for each node in the semantics tree. +/// +/// AccessibilityBridgeMac must be created as a shared_ptr, since some methods +/// acquires its weak_ptr. +class AccessibilityBridgeMac : public AccessibilityBridge { public: //--------------------------------------------------------------------------- /// @brief Creates an AccessibilityBridgeMacDelegate. /// @param[in] flutter_engine The weak reference to the FlutterEngine. /// @param[in] view_controller The weak reference to the FlutterViewController. - explicit AccessibilityBridgeMacDelegate(__weak FlutterEngine* flutter_engine, - __weak FlutterViewController* view_controller); - virtual ~AccessibilityBridgeMacDelegate() = default; + explicit AccessibilityBridgeMac(__weak FlutterEngine* flutter_engine, + __weak FlutterViewController* view_controller); + virtual ~AccessibilityBridgeMac() = default; - // |AccessibilityBridge::AccessibilityBridgeDelegate| - void OnAccessibilityEvent(ui::AXEventGenerator::TargetedEvent targeted_event) override; - - // |AccessibilityBridge::AccessibilityBridgeDelegate| + // |FlutterPlatformNodeDelegate::OwnerBridge| void DispatchAccessibilityAction(AccessibilityNodeId target, FlutterSemanticsAction action, fml::MallocMapping data) override; - // |AccessibilityBridge::AccessibilityBridgeDelegate| + // Update the default view controller, and recreate the corresponding + // accessibility node delegate. + // + // This is called by the engine when the default view controller is updated. + void UpdateDefaultViewController(__weak FlutterViewController* view_controller); + + protected: + // |AccessibilityBridge| + void OnAccessibilityEvent(ui::AXEventGenerator::TargetedEvent targeted_event) override; + + // |AccessibilityBridge| std::shared_ptr CreateFlutterPlatformNodeDelegate() override; private: @@ -84,4 +98,4 @@ class AccessibilityBridgeMacDelegate : public AccessibilityBridge::Accessibility } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_ACCESSIBILITYBRIDGEMACDELEGATE_H_ +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_ACCESSIBILITY_BRIDGE_MAC_H_ diff --git a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm similarity index 86% rename from shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm rename to shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm index 21917a72ed1dd..a0ce01dd3aeca 100644 --- a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.mm +++ b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h" @@ -20,21 +20,26 @@ static NSString* const kAccessibilityExpandedChanged = @"AXExpandedChanged"; static NSString* const kAccessibilityMenuItemSelectedNotification = @"AXMenuItemSelected"; -AccessibilityBridgeMacDelegate::AccessibilityBridgeMacDelegate( - __weak FlutterEngine* flutter_engine, - __weak FlutterViewController* view_controller) +AccessibilityBridgeMac::AccessibilityBridgeMac(__weak FlutterEngine* flutter_engine, + __weak FlutterViewController* view_controller) : flutter_engine_(flutter_engine), view_controller_(view_controller) {} -void AccessibilityBridgeMacDelegate::OnAccessibilityEvent( +void AccessibilityBridgeMac::UpdateDefaultViewController( + __weak FlutterViewController* view_controller) { + view_controller_ = view_controller; + RecreateNodeDelegates(); +} + +void AccessibilityBridgeMac::OnAccessibilityEvent( ui::AXEventGenerator::TargetedEvent targeted_event) { - if (!flutter_engine_.viewController.viewLoaded || !flutter_engine_.viewController.view.window) { + if (!view_controller_.viewLoaded || !view_controller_.view.window) { // Don't need to send accessibility events if the there is no view or window. return; } ui::AXNode* ax_node = targeted_event.node; - std::vector events = + std::vector events = MacOSEventsFromAXEvent(targeted_event.event_params.event, *ax_node); - for (AccessibilityBridgeMacDelegate::NSAccessibilityEvent event : events) { + for (AccessibilityBridgeMac::NSAccessibilityEvent event : events) { if (event.user_info != nil) { DispatchMacOSNotificationWithUserInfo(event.target, event.name, event.user_info); } else { @@ -43,20 +48,18 @@ } } -std::vector -AccessibilityBridgeMacDelegate::MacOSEventsFromAXEvent(ui::AXEventGenerator::Event event_type, - const ui::AXNode& ax_node) const { +std::vector +AccessibilityBridgeMac::MacOSEventsFromAXEvent(ui::AXEventGenerator::Event event_type, + const ui::AXNode& ax_node) const { // Gets the native_node with the node_id. NSCAssert(flutter_engine_, @"Flutter engine should not be deallocated"); - auto bridge = flutter_engine_.accessibilityBridge.lock(); - NSCAssert(bridge, @"Accessibility bridge in flutter engine must not be null."); - auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(ax_node.id()).lock(); + auto platform_node_delegate = GetFlutterPlatformNodeDelegateFromID(ax_node.id()).lock(); NSCAssert(platform_node_delegate, @"Event target must exist in accessibility bridge."); auto mac_platform_node_delegate = std::static_pointer_cast(platform_node_delegate); gfx::NativeViewAccessible native_node = mac_platform_node_delegate->GetNativeViewAccessible(); - std::vector events; + std::vector events; switch (event_type) { case ui::AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED: if (ax_node.data().role == ax::mojom::Role::kTree) { @@ -140,12 +143,12 @@ }); // WebKit fires a notification both on the focused object and the page // root. - const ui::AXTreeData& tree_data = bridge->GetAXTreeData(); + const ui::AXTreeData& tree_data = GetAXTreeData(); int32_t focus = tree_data.focus_id; if (focus == ui::AXNode::kInvalidAXID || focus != tree_data.sel_anchor_object_id) { break; // Just fire a notification on the root. } - auto focus_node = bridge->GetFlutterPlatformNodeDelegateFromID(focus).lock(); + auto focus_node = GetFlutterPlatformNodeDelegateFromID(focus).lock(); if (!focus_node) { break; // Just fire a notification on the root. } @@ -184,7 +187,7 @@ if (ax_node.data().HasState(ax::mojom::State::kEditable)) { events.push_back({ .name = NSAccessibilityValueChangedNotification, - .target = bridge->GetFlutterPlatformNodeDelegateFromID(AccessibilityBridge::kRootNodeId) + .target = GetFlutterPlatformNodeDelegateFromID(AccessibilityBridge::kRootNodeId) .lock() ->GetNativeViewAccessible(), .user_info = nil, @@ -292,10 +295,9 @@ case ui::AXEventGenerator::Event::CHILDREN_CHANGED: { // NSAccessibilityCreatedNotification seems to be the only way to let // Voiceover pick up layout changes. - NSCAssert(flutter_engine_.viewController, @"The viewController must not be nil"); events.push_back({ .name = NSAccessibilityCreatedNotification, - .target = flutter_engine_.viewController.view.window, + .target = view_controller_.view.window, .user_info = nil, }); break; @@ -359,23 +361,23 @@ return events; } -void AccessibilityBridgeMacDelegate::DispatchAccessibilityAction(ui::AXNode::AXID target, - FlutterSemanticsAction action, - fml::MallocMapping data) { +void AccessibilityBridgeMac::DispatchAccessibilityAction(ui::AXNode::AXID target, + FlutterSemanticsAction action, + fml::MallocMapping data) { NSCAssert(flutter_engine_, @"Flutter engine should not be deallocated"); - NSCAssert(flutter_engine_.viewController.viewLoaded && flutter_engine_.viewController.view.window, + NSCAssert(view_controller_.viewLoaded && view_controller_.view.window, @"The accessibility bridge should not receive accessibility actions if the flutter view" @"is not loaded or attached to a NSWindow."); [flutter_engine_ dispatchSemanticsAction:action toTarget:target withData:std::move(data)]; } std::shared_ptr -AccessibilityBridgeMacDelegate::CreateFlutterPlatformNodeDelegate() { - return std::make_shared(flutter_engine_, view_controller_); +AccessibilityBridgeMac::CreateFlutterPlatformNodeDelegate() { + return std::make_shared(weak_from_this(), view_controller_); } // Private method -void AccessibilityBridgeMacDelegate::DispatchMacOSNotification( +void AccessibilityBridgeMac::DispatchMacOSNotification( gfx::NativeViewAccessible native_node, NSAccessibilityNotificationName mac_notification) { NSCAssert(mac_notification, @"The notification must not be null."); @@ -383,7 +385,7 @@ NSAccessibilityPostNotification(native_node, mac_notification); } -void AccessibilityBridgeMacDelegate::DispatchMacOSNotificationWithUserInfo( +void AccessibilityBridgeMac::DispatchMacOSNotificationWithUserInfo( gfx::NativeViewAccessible native_node, NSAccessibilityNotificationName mac_notification, NSDictionary* user_info) { @@ -393,12 +395,10 @@ NSAccessibilityPostNotificationWithUserInfo(native_node, mac_notification, user_info); } -bool AccessibilityBridgeMacDelegate::HasPendingEvent(ui::AXEventGenerator::Event event) const { +bool AccessibilityBridgeMac::HasPendingEvent(ui::AXEventGenerator::Event event) const { NSCAssert(flutter_engine_, @"Flutter engine should not be deallocated"); - auto bridge = flutter_engine_.accessibilityBridge.lock(); - NSCAssert(bridge, @"Accessibility bridge in flutter engine must not be null."); - std::vector pending_events = bridge->GetPendingEvents(); - for (const auto& pending_event : bridge->GetPendingEvents()) { + std::vector pending_events = GetPendingEvents(); + for (const auto& pending_event : GetPendingEvents()) { if (pending_event.event_params.event == event) { return true; } diff --git a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegateTest.mm b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm similarity index 75% rename from shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegateTest.mm rename to shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm index 625272b192d24..0ad11af0e3e8f 100644 --- a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegateTest.mm +++ b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm @@ -3,19 +3,22 @@ // found in the LICENSE file. #include "flutter/testing/testing.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" + namespace flutter::testing { namespace { -class AccessibilityBridgeMacDelegateSpy : public AccessibilityBridgeMacDelegate { +class AccessibilityBridgeMacSpy : public AccessibilityBridgeMac { public: - AccessibilityBridgeMacDelegateSpy(__weak FlutterEngine* flutter_engine, - __weak FlutterViewController* view_controller) - : AccessibilityBridgeMacDelegate(flutter_engine, view_controller) {} + using AccessibilityBridgeMac::OnAccessibilityEvent; + + AccessibilityBridgeMacSpy(__weak FlutterEngine* flutter_engine, + __weak FlutterViewController* view_controller) + : AccessibilityBridgeMac(flutter_engine, view_controller) {} std::unordered_map actual_notifications; @@ -26,18 +29,40 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, } }; +} // namespace +} // namespace flutter::testing + +@interface AccessibilityBridgeTestEngine : FlutterEngine +- (std::shared_ptr) + createAccessibilityBridge:(nonnull FlutterEngine*)engine + viewController:(nonnull FlutterViewController*)viewController; +@end + +@implementation AccessibilityBridgeTestEngine +- (std::shared_ptr) + createAccessibilityBridge:(nonnull FlutterEngine*)engine + viewController:(nonnull FlutterViewController*)viewController { + return std::make_shared(engine, viewController); +} +@end + +namespace flutter::testing { + +namespace { + // Returns an engine configured for the text fixture resource configuration. FlutterEngine* CreateTestEngine() { NSString* fixtures = @(testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] initWithAssetsPath:fixtures ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - return [[FlutterEngine alloc] initWithName:@"test" project:project allowHeadlessExecution:true]; + return [[AccessibilityBridgeTestEngine alloc] initWithName:@"test" + project:project + allowHeadlessExecution:true]; } } // namespace -TEST(AccessibilityBridgeMacDelegateTest, - sendsAccessibilityCreateNotificationToWindowOfFlutterView) { +TEST(AccessibilityBridgeMacTest, sendsAccessibilityCreateNotificationToWindowOfFlutterView) { FlutterEngine* engine = CreateTestEngine(); NSString* fixtures = @(testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] @@ -55,7 +80,8 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, // Setting up bridge so that the AccessibilityBridgeMacDelegateSpy // can query semantics information from. engine.semanticsEnabled = YES; - auto bridge = engine.accessibilityBridge.lock(); + auto bridge = + std::reinterpret_pointer_cast(engine.accessibilityBridge.lock()); FlutterSemanticsNode root; root.id = 0; root.flags = static_cast(0); @@ -75,8 +101,6 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, bridge->CommitUpdates(); auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); - AccessibilityBridgeMacDelegateSpy spy(engine, viewController); - // Creates a targeted event. ui::AXTree tree; ui::AXNode ax_node(&tree, nullptr, 0, 0); @@ -88,15 +112,16 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, ax::mojom::EventFrom::kNone, intent); ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params); - spy.OnAccessibilityEvent(targeted_event); + bridge->OnAccessibilityEvent(targeted_event); - EXPECT_EQ(spy.actual_notifications.size(), 1u); - EXPECT_EQ(spy.actual_notifications.find([NSAccessibilityCreatedNotification UTF8String])->second, - expectedTarget); + EXPECT_EQ(bridge->actual_notifications.size(), 1u); + EXPECT_EQ( + bridge->actual_notifications.find([NSAccessibilityCreatedNotification UTF8String])->second, + expectedTarget); [engine shutDownEngine]; } -TEST(AccessibilityBridgeMacDelegateTest, doesNotSendAccessibilityCreateNotificationWhenHeadless) { +TEST(AccessibilityBridgeMacTest, doesNotSendAccessibilityCreateNotificationWhenHeadless) { FlutterEngine* engine = CreateTestEngine(); NSString* fixtures = @(testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] @@ -108,7 +133,8 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, // Setting up bridge so that the AccessibilityBridgeMacDelegateSpy // can query semantics information from. engine.semanticsEnabled = YES; - auto bridge = engine.accessibilityBridge.lock(); + auto bridge = + std::reinterpret_pointer_cast(engine.accessibilityBridge.lock()); FlutterSemanticsNode root; root.id = 0; root.flags = static_cast(0); @@ -128,8 +154,6 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, bridge->CommitUpdates(); auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); - AccessibilityBridgeMacDelegateSpy spy(engine, viewController); - // Creates a targeted event. ui::AXTree tree; ui::AXNode ax_node(&tree, nullptr, 0, 0); @@ -141,14 +165,14 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, ax::mojom::EventFrom::kNone, intent); ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params); - spy.OnAccessibilityEvent(targeted_event); + bridge->OnAccessibilityEvent(targeted_event); // Does not send any notification if the engine is headless. - EXPECT_EQ(spy.actual_notifications.size(), 0u); + EXPECT_EQ(bridge->actual_notifications.size(), 0u); [engine shutDownEngine]; } -TEST(AccessibilityBridgeMacDelegateTest, doesNotSendAccessibilityCreateNotificationWhenNoWindow) { +TEST(AccessibilityBridgeMacTest, doesNotSendAccessibilityCreateNotificationWhenNoWindow) { FlutterEngine* engine = CreateTestEngine(); // Create a view controller without attaching it to a window. NSString* fixtures = @(testing::GetFixturesPath()); @@ -162,7 +186,8 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, // Setting up bridge so that the AccessibilityBridgeMacDelegateSpy // can query semantics information from. engine.semanticsEnabled = YES; - auto bridge = engine.accessibilityBridge.lock(); + auto bridge = + std::reinterpret_pointer_cast(engine.accessibilityBridge.lock()); FlutterSemanticsNode root; root.id = 0; root.flags = static_cast(0); @@ -182,8 +207,6 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, bridge->CommitUpdates(); auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); - AccessibilityBridgeMacDelegateSpy spy(engine, viewController); - // Creates a targeted event. ui::AXTree tree; ui::AXNode ax_node(&tree, nullptr, 0, 0); @@ -195,10 +218,10 @@ void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, ax::mojom::EventFrom::kNone, intent); ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params); - spy.OnAccessibilityEvent(targeted_event); + bridge->OnAccessibilityEvent(targeted_event); // Does not send any notification if the flutter view is not attached to a NSWindow. - EXPECT_EQ(spy.actual_notifications.size(), 0u); + EXPECT_EQ(bridge->actual_notifications.size(), 0u); [engine shutDownEngine]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index adffefbd027b0..d8bf40e3b2cf9 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -9,7 +9,6 @@ #include #include -#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterExternalTextureGL.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterGLCompositor.h" @@ -189,7 +188,7 @@ @implementation FlutterEngine { FLUTTER_API_SYMBOL(FlutterEngine) _engine; // The private member for accessibility. - std::shared_ptr _bridge; + std::shared_ptr _bridge; // The project being run by this engine. FlutterDartProject* _project; @@ -418,8 +417,7 @@ - (void)setViewController:(FlutterViewController*)controller { [_renderer setFlutterView:controller.flutterView]; if (_semanticsEnabled && _bridge) { - _bridge->UpdateDelegate( - std::make_unique(self, _viewController)); + _bridge->UpdateDefaultViewController(_viewController); } if (!controller && !_allowHeadlessExecution) { @@ -551,7 +549,7 @@ - (void)sendInitialSettings { return _embedderAPI; } -- (std::weak_ptr)accessibilityBridge { +- (std::weak_ptr)accessibilityBridge { return _bridge; } @@ -601,12 +599,17 @@ - (void)setSemanticsEnabled:(BOOL)enabled { if (!_semanticsEnabled && _bridge) { _bridge.reset(); } else if (_semanticsEnabled && !_bridge) { - _bridge = std::make_shared( - std::make_unique(self, self.viewController)); + _bridge = [self createAccessibilityBridge:self viewController:self.viewController]; } _embedderAPI.UpdateSemanticsEnabled(_engine, _semanticsEnabled); } +- (std::shared_ptr) + createAccessibilityBridge:(nonnull FlutterEngine*)engine + viewController:(nonnull FlutterViewController*)viewController { + return std::make_shared(engine, _viewController); +} + - (void)dispatchSemanticsAction:(FlutterSemanticsAction)action toTarget:(uint16_t)target withData:(fml::MallocMapping)data { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index 1be0f3d886eb1..0b618e4aed2f7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -8,7 +8,7 @@ #include -#include "flutter/shell/platform/common/accessibility_bridge.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h" @@ -31,7 +31,7 @@ */ @property(nonatomic) FlutterEngineProcTable& embedderAPI; -@property(nonatomic, readonly) std::weak_ptr accessibilityBridge; +@property(nonatomic, readonly) std::weak_ptr accessibilityBridge; /** * True if the semantics is enabled. The Flutter framework starts sending @@ -94,3 +94,14 @@ withData:(fml::MallocMapping)data; @end + +@interface FlutterEngine (TestMethods) +/* Creates an accessibility bridge with the provided parameters. + * + * By default this method calls AccessibilityBridgeMac's initializer. Exposing + * this method allows unit tests to override in order to capture information. + */ +- (std::shared_ptr) + createAccessibilityBridge:(nonnull FlutterEngine*)engine + viewController:(nonnull FlutterViewController*)viewController; +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h index 3077ab2fef6d2..3c394efd0c97a 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h @@ -7,8 +7,9 @@ #import -#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" +#include "flutter/shell/platform/common/accessibility_bridge.h" #include "flutter/shell/platform/common/flutter_platform_node_delegate.h" #include "flutter/shell/platform/embedder/embedder.h" @@ -19,9 +20,8 @@ namespace flutter { /// AXPlatformNodeMac to manage the macOS-specific accessibility objects. class FlutterPlatformNodeDelegateMac : public FlutterPlatformNodeDelegate { public: - explicit FlutterPlatformNodeDelegateMac( - __weak FlutterEngine* engine, - __weak FlutterViewController* view_controller); + FlutterPlatformNodeDelegateMac(std::weak_ptr bridge, + __weak FlutterViewController* view_controller); virtual ~FlutterPlatformNodeDelegateMac(); void Init(std::weak_ptr bridge, ui::AXNode* node) override; @@ -49,7 +49,7 @@ class FlutterPlatformNodeDelegateMac : public FlutterPlatformNodeDelegate { private: ui::AXPlatformNode* ax_platform_node_; - __weak FlutterEngine* engine_; + std::weak_ptr bridge_; __weak FlutterViewController* view_controller_; gfx::RectF ConvertBoundsFromLocalToScreen( diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm index ab95291d504c9..1828f94d92807 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm @@ -21,9 +21,9 @@ namespace flutter { // namespace FlutterPlatformNodeDelegateMac::FlutterPlatformNodeDelegateMac( - __weak FlutterEngine* engine, + std::weak_ptr bridge, __weak FlutterViewController* view_controller) - : engine_(engine), view_controller_(view_controller) {} + : bridge_(std::move(bridge)), view_controller_(view_controller) {} void FlutterPlatformNodeDelegateMac::Init(std::weak_ptr bridge, ui::AXNode* node) { FlutterPlatformNodeDelegate::Init(bridge, node); @@ -48,9 +48,8 @@ gfx::NativeViewAccessible FlutterPlatformNodeDelegateMac::GetParent() { gfx::NativeViewAccessible parent = FlutterPlatformNodeDelegate::GetParent(); if (!parent) { - NSCAssert(engine_, @"Flutter engine should not be deallocated"); - NSCAssert(engine_.viewController.viewLoaded, @"Flutter view must be loaded"); - return engine_.viewController.flutterView; + NSCAssert(view_controller_.viewLoaded, @"Flutter view must be loaded"); + return view_controller_.flutterView; } return parent; } @@ -80,8 +79,7 @@ if (!text.empty()) { return text; }; - NSCAssert(engine_, @"Flutter engine should not be deallocated"); - auto bridge_ptr = engine_.accessibilityBridge.lock(); + auto bridge_ptr = bridge_.lock(); NSCAssert(bridge_ptr, @"Accessibility bridge in flutter engine must not be null."); for (int32_t child : GetData().child_ids) { auto delegate_child = bridge_ptr->GetFlutterPlatformNodeDelegateFromID(child).lock(); @@ -105,14 +103,11 @@ // it converts the bounds from flutter coordinates to macOS coordinates. ns_local_bounds.origin.y = -ns_local_bounds.origin.y - ns_local_bounds.size.height; - NSCAssert(engine_, @"Flutter engine should not be deallocated"); - NSCAssert(engine_.viewController.viewLoaded, @"Flutter view must be loaded."); - NSRect ns_view_bounds = - [engine_.viewController.flutterView convertRectFromBacking:ns_local_bounds]; - NSRect ns_window_bounds = [engine_.viewController.flutterView convertRect:ns_view_bounds - toView:nil]; + NSCAssert(view_controller_.viewLoaded, @"Flutter view must be loaded."); + NSRect ns_view_bounds = [view_controller_.flutterView convertRectFromBacking:ns_local_bounds]; + NSRect ns_window_bounds = [view_controller_.flutterView convertRect:ns_view_bounds toView:nil]; NSRect ns_screen_bounds = - [[engine_.viewController.flutterView window] convertRectToScreen:ns_window_bounds]; + [[view_controller_.flutterView window] convertRectToScreen:ns_window_bounds]; gfx::RectF screen_bounds(ns_screen_bounds.origin.x, ns_screen_bounds.origin.y, ns_screen_bounds.size.width, ns_screen_bounds.size.height); return screen_bounds; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm index a7fa46c57bb7d..a5809bef78d92 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm @@ -4,7 +4,7 @@ #include "flutter/testing/testing.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacDelegate.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 4efb4d3e30bf5..0a1d2a2b70876 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1427,7 +1427,7 @@ - (bool)testSelectorsAreForwardedToFramework { engine.semanticsEnabled = YES; auto bridge = engine.accessibilityBridge.lock(); - FlutterPlatformNodeDelegateMac delegate(engine, viewController); + FlutterPlatformNodeDelegateMac delegate(bridge, viewController); ui::AXTree tree; ui::AXNode ax_node(&tree, nullptr, 0, 0); ui::AXNodeData node_data; @@ -1486,7 +1486,7 @@ - (bool)testSelectorsAreForwardedToFramework { engine.semanticsEnabled = YES; auto bridge = engine.accessibilityBridge.lock(); - FlutterPlatformNodeDelegateMac delegate(engine, viewController); + FlutterPlatformNodeDelegateMac delegate(bridge, viewController); ui::AXTree tree; ui::AXNode ax_node(&tree, nullptr, 0, 0); ui::AXNodeData node_data; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm index 2080cdbdb8fce..3032f5cc054af 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm @@ -44,7 +44,7 @@ engine.semanticsEnabled = YES; auto bridge = engine.accessibilityBridge.lock(); - FlutterPlatformNodeDelegateMac delegate(engine, viewController); + FlutterPlatformNodeDelegateMac delegate(bridge, viewController); ui::AXTree tree; ui::AXNode ax_node(&tree, nullptr, 0, 0); ui::AXNodeData node_data; diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index 1ed92aec50f1d..8e6af332d7ad5 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -40,8 +40,8 @@ source_set("flutter_windows_source") { sources = [ "accessibility_alert.cc", "accessibility_alert.h", - "accessibility_bridge_delegate_windows.cc", - "accessibility_bridge_delegate_windows.h", + "accessibility_bridge_windows.cc", + "accessibility_bridge_windows.h", "accessibility_root_node.cc", "accessibility_root_node.h", "angle_surface_manager.cc", @@ -175,7 +175,7 @@ executable("flutter_windows_unittests") { # Common Windows test sources. sources = [ - "accessibility_bridge_delegate_windows_unittests.cc", + "accessibility_bridge_windows_unittests.cc", "direct_manipulation_unittests.cc", "dpi_utils_unittests.cc", "flutter_project_bundle_unittests.cc", diff --git a/shell/platform/windows/accessibility_bridge_delegate_windows.cc b/shell/platform/windows/accessibility_bridge_windows.cc similarity index 88% rename from shell/platform/windows/accessibility_bridge_delegate_windows.cc rename to shell/platform/windows/accessibility_bridge_windows.cc index dd2a4b0592154..add1fc7f7c036 100644 --- a/shell/platform/windows/accessibility_bridge_delegate_windows.cc +++ b/shell/platform/windows/accessibility_bridge_windows.cc @@ -2,30 +2,28 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/windows/accessibility_bridge_delegate_windows.h" +#include "flutter/shell/platform/windows/accessibility_bridge_windows.h" #include "flutter/shell/platform/windows/flutter_platform_node_delegate_windows.h" -#include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.h" namespace flutter { -AccessibilityBridgeDelegateWindows::AccessibilityBridgeDelegateWindows( - FlutterWindowsEngine* engine) - : engine_(engine) { +AccessibilityBridgeWindows::AccessibilityBridgeWindows( + FlutterWindowsEngine* engine, + FlutterWindowsView* view) + : engine_(engine), view_(view) { assert(engine_); + assert(view_); } -void AccessibilityBridgeDelegateWindows::OnAccessibilityEvent( +void AccessibilityBridgeWindows::OnAccessibilityEvent( ui::AXEventGenerator::TargetedEvent targeted_event) { ui::AXNode* ax_node = targeted_event.node; ui::AXEventGenerator::Event event_type = targeted_event.event_params.event; - // Look up the flutter platform node delegate. - auto bridge = engine_->accessibility_bridge().lock(); - assert(bridge); auto node_delegate = - bridge->GetFlutterPlatformNodeDelegateFromID(ax_node->id()).lock(); + GetFlutterPlatformNodeDelegateFromID(ax_node->id()).lock(); assert(node_delegate); std::shared_ptr win_delegate = std::static_pointer_cast( @@ -138,7 +136,7 @@ void AccessibilityBridgeDelegateWindows::OnAccessibilityEvent( } } -void AccessibilityBridgeDelegateWindows::DispatchAccessibilityAction( +void AccessibilityBridgeWindows::DispatchAccessibilityAction( AccessibilityNodeId target, FlutterSemanticsAction action, fml::MallocMapping data) { @@ -146,17 +144,18 @@ void AccessibilityBridgeDelegateWindows::DispatchAccessibilityAction( } std::shared_ptr -AccessibilityBridgeDelegateWindows::CreateFlutterPlatformNodeDelegate() { - return std::make_shared(engine_); +AccessibilityBridgeWindows::CreateFlutterPlatformNodeDelegate() { + return std::make_shared( + shared_from_this(), view_); } -void AccessibilityBridgeDelegateWindows::DispatchWinAccessibilityEvent( +void AccessibilityBridgeWindows::DispatchWinAccessibilityEvent( std::shared_ptr node_delegate, DWORD event_type) { node_delegate->DispatchWinAccessibilityEvent(event_type); } -void AccessibilityBridgeDelegateWindows::SetFocus( +void AccessibilityBridgeWindows::SetFocus( std::shared_ptr node_delegate) { node_delegate->SetFocus(); } diff --git a/shell/platform/windows/accessibility_bridge_delegate_windows.h b/shell/platform/windows/accessibility_bridge_windows.h similarity index 51% rename from shell/platform/windows/accessibility_bridge_delegate_windows.h rename to shell/platform/windows/accessibility_bridge_windows.h index d9c2060867876..68f75298a83e9 100644 --- a/shell/platform/windows/accessibility_bridge_delegate_windows.h +++ b/shell/platform/windows/accessibility_bridge_windows.h @@ -2,59 +2,68 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_BRIDGE_DELEGATE_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_BRIDGE_DELEGATE_H_ +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_BRIDGE_WINDOWS_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_BRIDGE_WINDOWS_H_ #include "flutter/shell/platform/common/accessibility_bridge.h" -#include "flutter/shell/platform/windows/flutter_platform_node_delegate_windows.h" #include "flutter/shell/platform/windows/flutter_windows_engine.h" +#include "flutter/shell/platform/windows/flutter_windows_view.h" namespace flutter { class FlutterWindowsEngine; +class FlutterPlatformNodeDelegateWindows; -// The Win32 implementation of AccessibilityBridgeDelegate. +// The Win32 implementation of AccessibilityBridge. // -// Handles requests from the accessibility bridge to interact with Windows -// accessibility APIs. This includes routing accessibility events fired from -// the framework to Windows, routing native Windows accessibility events to the -// framework, and creating Windows-specific FlutterPlatformNodeDelegate objects -// for each node in the semantics tree. -class AccessibilityBridgeDelegateWindows - : public AccessibilityBridge::AccessibilityBridgeDelegate { +// This interacts with Windows accessibility APIs, which includes routing +// accessibility events fired from the framework to Windows, routing native +// Windows accessibility events to the framework, and creating Windows-specific +// FlutterPlatformNodeDelegate objects for each node in the semantics tree. +/// +/// AccessibilityBridgeWindows must be created as a shared_ptr, since some +/// methods acquires its weak_ptr. +class AccessibilityBridgeWindows : public AccessibilityBridge { public: - explicit AccessibilityBridgeDelegateWindows(FlutterWindowsEngine* engine); - virtual ~AccessibilityBridgeDelegateWindows() = default; + AccessibilityBridgeWindows(FlutterWindowsEngine* engine, + FlutterWindowsView* view); + virtual ~AccessibilityBridgeWindows() = default; - // |AccessibilityBridge::AccessibilityBridgeDelegate| - void OnAccessibilityEvent( - ui::AXEventGenerator::TargetedEvent targeted_event) override; - - // |AccessibilityBridge::AccessibilityBridgeDelegate| + // |AccessibilityBridge| void DispatchAccessibilityAction(AccessibilityNodeId target, FlutterSemanticsAction action, fml::MallocMapping data) override; - // |AccessibilityBridge::AccessibilityBridgeDelegate| - std::shared_ptr - CreateFlutterPlatformNodeDelegate() override; - // Dispatches a Windows accessibility event of the specified type, generated // by the accessibility node associated with the specified semantics node. + // + // This is a virtual method for the convenience of unit tests. virtual void DispatchWinAccessibilityEvent( std::shared_ptr node_delegate, DWORD event_type); // Sets the accessibility focus to the accessibility node associated with the // specified semantics node. + // + // This is a virtual method for the convenience of unit tests. virtual void SetFocus( std::shared_ptr node_delegate); + protected: + // |AccessibilityBridge| + void OnAccessibilityEvent( + ui::AXEventGenerator::TargetedEvent targeted_event) override; + + // |AccessibilityBridge| + std::shared_ptr + CreateFlutterPlatformNodeDelegate() override; + private: FlutterWindowsEngine* engine_; + FlutterWindowsView* view_; }; } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_BRIDGE_DELEGATE_H_ +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_BRIDGE_WINDOWS_H_ diff --git a/shell/platform/windows/accessibility_bridge_delegate_windows_unittests.cc b/shell/platform/windows/accessibility_bridge_windows_unittests.cc similarity index 72% rename from shell/platform/windows/accessibility_bridge_delegate_windows_unittests.cc rename to shell/platform/windows/accessibility_bridge_windows_unittests.cc index 5db7fc4c7de38..2dae9d8b41e01 100644 --- a/shell/platform/windows/accessibility_bridge_delegate_windows_unittests.cc +++ b/shell/platform/windows/accessibility_bridge_windows_unittests.cc @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "flutter/shell/platform/windows/accessibility_bridge_delegate_windows.h" +#include "flutter/shell/platform/windows/accessibility_bridge_windows.h" #include #include @@ -33,11 +33,13 @@ struct MsaaEvent { }; // Accessibility bridge delegate that captures events dispatched to the OS. -class AccessibilityBridgeDelegateWindowsSpy - : public AccessibilityBridgeDelegateWindows { +class AccessibilityBridgeWindowsSpy : public AccessibilityBridgeWindows { public: - explicit AccessibilityBridgeDelegateWindowsSpy(FlutterWindowsEngine* engine) - : AccessibilityBridgeDelegateWindows(engine) {} + using AccessibilityBridgeWindows::OnAccessibilityEvent; + + explicit AccessibilityBridgeWindowsSpy(FlutterWindowsEngine* engine, + FlutterWindowsView* view) + : AccessibilityBridgeWindows(engine, view) {} void DispatchWinAccessibilityEvent( std::shared_ptr node_delegate, @@ -50,7 +52,7 @@ class AccessibilityBridgeDelegateWindowsSpy focused_nodes_.push_back(node_delegate->GetAXNode()->id()); } - void Reset() { + void ResetRecords() { dispatched_events_.clear(); focused_nodes_.clear(); } @@ -66,16 +68,31 @@ class AccessibilityBridgeDelegateWindowsSpy std::vector focused_nodes_; }; +// A FlutterWindowsEngine whose accessibility bridge is a +// AccessibilityBridgeWindowsSpy. +class FlutterWindowsEngineSpy : public FlutterWindowsEngine { + public: + explicit FlutterWindowsEngineSpy(const FlutterProjectBundle& project) + : FlutterWindowsEngine(project) {} + + protected: + virtual std::shared_ptr CreateAccessibilityBridge( + FlutterWindowsEngine* engine, + FlutterWindowsView* view) override { + return std::make_shared(engine, view); + } +}; + // Returns an engine instance configured with dummy project path values, and // overridden methods for sending platform messages, so that the engine can // respond as if the framework were connected. -std::unique_ptr GetTestEngine() { +std::unique_ptr GetTestEngine() { FlutterDesktopEngineProperties properties = {}; properties.assets_path = L"C:\\foo\\flutter_assets"; properties.icu_data_path = L"C:\\foo\\icudtl.dat"; properties.aot_library_path = L"C:\\foo\\aot.so"; FlutterProjectBundle project(properties); - auto engine = std::make_unique(project); + auto engine = std::make_unique(project); EngineModifier modifier(engine.get()); modifier.embedder_api().UpdateSemanticsEnabled = @@ -142,6 +159,14 @@ ui::AXNode* AXNodeFromID(std::shared_ptr bridge, return node_delegate ? node_delegate->GetAXNode() : nullptr; } +std::shared_ptr GetAccessibilityBridgeSpy( + FlutterWindowsEngine* engine) { + FlutterWindowsEngineSpy* engine_spy = + reinterpret_cast(engine); + return std::reinterpret_pointer_cast( + engine_spy->accessibility_bridge().lock()); +} + void ExpectWinEventFromAXEvent(int32_t node_id, ui::AXEventGenerator::Event ax_event, DWORD expected_event) { @@ -151,19 +176,19 @@ void ExpectWinEventFromAXEvent(int32_t node_id, view.SetEngine(GetTestEngine()); view.OnUpdateSemanticsEnabled(true); - auto bridge = view.GetEngine()->accessibility_bridge().lock(); + auto bridge = GetAccessibilityBridgeSpy(view.GetEngine()); PopulateAXTree(bridge); - AccessibilityBridgeDelegateWindowsSpy spy(view.GetEngine()); - spy.OnAccessibilityEvent({AXNodeFromID(bridge, node_id), - {ax_event, ax::mojom::EventFrom::kNone, {}}}); - ASSERT_EQ(spy.dispatched_events().size(), 1); - EXPECT_EQ(spy.dispatched_events()[0].event_type, expected_event); + bridge->ResetRecords(); + bridge->OnAccessibilityEvent({AXNodeFromID(bridge, node_id), + {ax_event, ax::mojom::EventFrom::kNone, {}}}); + ASSERT_EQ(bridge->dispatched_events().size(), 1); + EXPECT_EQ(bridge->dispatched_events()[0].event_type, expected_event); } } // namespace -TEST(AccessibilityBridgeDelegateWindows, GetParent) { +TEST(AccessibilityBridgeWindows, GetParent) { auto window_binding_handler = std::make_unique<::testing::NiceMock>(); FlutterWindowsView view(std::move(window_binding_handler)); @@ -179,7 +204,7 @@ TEST(AccessibilityBridgeDelegateWindows, GetParent) { node1_delegate->GetParent()); } -TEST(AccessibilityBridgeDelegateWindows, GetParentOnRootRetunsNullptr) { +TEST(AccessibilityBridgeWindows, GetParentOnRootRetunsNullptr) { auto window_binding_handler = std::make_unique<::testing::NiceMock>(); FlutterWindowsView view(std::move(window_binding_handler)); @@ -193,7 +218,7 @@ TEST(AccessibilityBridgeDelegateWindows, GetParentOnRootRetunsNullptr) { ASSERT_TRUE(node0_delegate->GetParent() == nullptr); } -TEST(AccessibilityBridgeDelegateWindows, DispatchAccessibilityAction) { +TEST(AccessibilityBridgeWindows, DispatchAccessibilityAction) { auto window_binding_handler = std::make_unique<::testing::NiceMock>(); FlutterWindowsView view(std::move(window_binding_handler)); @@ -214,101 +239,99 @@ TEST(AccessibilityBridgeDelegateWindows, DispatchAccessibilityAction) { return kSuccess; })); - AccessibilityBridgeDelegateWindows delegate(view.GetEngine()); + AccessibilityBridgeWindows delegate(view.GetEngine(), &view); delegate.DispatchAccessibilityAction(1, kFlutterSemanticsActionCopy, {}); EXPECT_EQ(actual_action, kFlutterSemanticsActionCopy); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityEventAlert) { +TEST(AccessibilityBridgeWindows, OnAccessibilityEventAlert) { ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::ALERT, EVENT_SYSTEM_ALERT); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityEventChildrenChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityEventChildrenChanged) { ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::CHILDREN_CHANGED, EVENT_OBJECT_REORDER); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityEventFocusChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityEventFocusChanged) { auto window_binding_handler = std::make_unique<::testing::NiceMock>(); FlutterWindowsView view(std::move(window_binding_handler)); view.SetEngine(GetTestEngine()); view.OnUpdateSemanticsEnabled(true); - auto bridge = view.GetEngine()->accessibility_bridge().lock(); + auto bridge = GetAccessibilityBridgeSpy(view.GetEngine()); PopulateAXTree(bridge); - AccessibilityBridgeDelegateWindowsSpy spy(view.GetEngine()); - spy.OnAccessibilityEvent({AXNodeFromID(bridge, 1), - {ui::AXEventGenerator::Event::FOCUS_CHANGED, - ax::mojom::EventFrom::kNone, - {}}}); - ASSERT_EQ(spy.dispatched_events().size(), 1); - EXPECT_EQ(spy.dispatched_events()[0].event_type, EVENT_OBJECT_FOCUS); + bridge->ResetRecords(); + bridge->OnAccessibilityEvent({AXNodeFromID(bridge, 1), + {ui::AXEventGenerator::Event::FOCUS_CHANGED, + ax::mojom::EventFrom::kNone, + {}}}); + ASSERT_EQ(bridge->dispatched_events().size(), 1); + EXPECT_EQ(bridge->dispatched_events()[0].event_type, EVENT_OBJECT_FOCUS); - ASSERT_EQ(spy.focused_nodes().size(), 1); - EXPECT_EQ(spy.focused_nodes()[0], 1); + ASSERT_EQ(bridge->focused_nodes().size(), 1); + EXPECT_EQ(bridge->focused_nodes()[0], 1); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityEventIgnoredChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityEventIgnoredChanged) { // Static test nodes with no text, hint, or scrollability are ignored. ExpectWinEventFromAXEvent(4, ui::AXEventGenerator::Event::IGNORED_CHANGED, EVENT_OBJECT_HIDE); } -TEST(AccessibilityBridgeDelegateWindows, - OnAccessibilityImageAnnotationChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityImageAnnotationChanged) { ExpectWinEventFromAXEvent( 1, ui::AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED, EVENT_OBJECT_NAMECHANGE); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityLiveRegionChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityLiveRegionChanged) { ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::LIVE_REGION_CHANGED, EVENT_OBJECT_LIVEREGIONCHANGED); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityNameChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityNameChanged) { ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::NAME_CHANGED, EVENT_OBJECT_NAMECHANGE); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityHScrollPosChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityHScrollPosChanged) { ExpectWinEventFromAXEvent( 1, ui::AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED, EVENT_SYSTEM_SCROLLINGEND); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityVScrollPosChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityVScrollPosChanged) { ExpectWinEventFromAXEvent( 1, ui::AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED, EVENT_SYSTEM_SCROLLINGEND); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilitySelectedChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilitySelectedChanged) { ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::SELECTED_CHANGED, EVENT_OBJECT_VALUECHANGE); } -TEST(AccessibilityBridgeDelegateWindows, - OnAccessibilitySelectedChildrenChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilitySelectedChildrenChanged) { ExpectWinEventFromAXEvent( 2, ui::AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED, EVENT_OBJECT_SELECTIONWITHIN); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilitySubtreeCreated) { +TEST(AccessibilityBridgeWindows, OnAccessibilitySubtreeCreated) { ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::SUBTREE_CREATED, EVENT_OBJECT_SHOW); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityValueChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityValueChanged) { ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::VALUE_CHANGED, EVENT_OBJECT_VALUECHANGE); } -TEST(AccessibilityBridgeDelegateWindows, OnAccessibilityStateChanged) { +TEST(AccessibilityBridgeWindows, OnAccessibilityStateChanged) { ExpectWinEventFromAXEvent( 1, ui::AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED, EVENT_OBJECT_STATECHANGE); diff --git a/shell/platform/windows/flutter_platform_node_delegate_windows.cc b/shell/platform/windows/flutter_platform_node_delegate_windows.cc index a2c95daa817bd..5f5ad62d4adcd 100644 --- a/shell/platform/windows/flutter_platform_node_delegate_windows.cc +++ b/shell/platform/windows/flutter_platform_node_delegate_windows.cc @@ -6,6 +6,7 @@ #include "flutter/shell/platform/windows/flutter_platform_node_delegate_windows.h" +#include "flutter/shell/platform/windows/accessibility_bridge_windows.h" #include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/third_party/accessibility/ax/ax_clipping_behavior.h" #include "flutter/third_party/accessibility/ax/ax_coordinate_system.h" @@ -13,9 +14,11 @@ namespace flutter { FlutterPlatformNodeDelegateWindows::FlutterPlatformNodeDelegateWindows( - FlutterWindowsEngine* engine) - : engine_(engine) { - assert(engine_); + std::weak_ptr bridge, + FlutterWindowsView* view) + : bridge_(bridge), view_(view) { + assert(!bridge_.expired()); + assert(view_); } FlutterPlatformNodeDelegateWindows::~FlutterPlatformNodeDelegateWindows() { @@ -53,7 +56,7 @@ gfx::NativeViewAccessible FlutterPlatformNodeDelegateWindows::HitTestSync( } // If any child in this node's subtree contains the point, return that child. - auto bridge = engine_->accessibility_bridge().lock(); + auto bridge = bridge_.lock(); assert(bridge); for (const ui::AXNode* child : GetAXNode()->children()) { std::shared_ptr win_delegate = @@ -80,19 +83,15 @@ gfx::Rect FlutterPlatformNodeDelegateWindows::GetBoundsRect( coordinate_system, clipping_behavior, offscreen_result); POINT origin{bounds.x(), bounds.y()}; POINT extent{bounds.x() + bounds.width(), bounds.y() + bounds.height()}; - ClientToScreen(engine_->view()->GetPlatformWindow(), &origin); - ClientToScreen(engine_->view()->GetPlatformWindow(), &extent); + ClientToScreen(view_->GetPlatformWindow(), &origin); + ClientToScreen(view_->GetPlatformWindow(), &extent); return gfx::Rect(origin.x, origin.y, extent.x - origin.x, extent.y - origin.y); } void FlutterPlatformNodeDelegateWindows::DispatchWinAccessibilityEvent( DWORD event_type) { - FlutterWindowsView* view = engine_->view(); - if (!view) { - return; - } - HWND hwnd = view->GetPlatformWindow(); + HWND hwnd = view_->GetPlatformWindow(); if (!hwnd) { return; } diff --git a/shell/platform/windows/flutter_platform_node_delegate_windows.h b/shell/platform/windows/flutter_platform_node_delegate_windows.h index b77cc0ca7e107..8bac155b9c14d 100644 --- a/shell/platform/windows/flutter_platform_node_delegate_windows.h +++ b/shell/platform/windows/flutter_platform_node_delegate_windows.h @@ -2,25 +2,24 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PLATFORM_NODE_DELEGATE_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PLATFORM_NODE_DELEGATE_H_ +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PLATFORM_NODE_DELEGATE_WINDOWS_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PLATFORM_NODE_DELEGATE_WINDOWS_H_ #include "flutter/shell/platform/common/flutter_platform_node_delegate.h" -#include "flutter/shell/platform/windows/flutter_windows_engine.h" +#include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/third_party/accessibility/ax/platform/ax_platform_node.h" #include "flutter/third_party/accessibility/ax/platform/ax_unique_id.h" namespace flutter { -class FlutterWindowsEngine; - // The Windows implementation of FlutterPlatformNodeDelegate. // // This class implements a wrapper around the Windows accessibility objects // that compose the accessibility tree. class FlutterPlatformNodeDelegateWindows : public FlutterPlatformNodeDelegate { public: - explicit FlutterPlatformNodeDelegateWindows(FlutterWindowsEngine* engine); + FlutterPlatformNodeDelegateWindows(std::weak_ptr bridge, + FlutterWindowsView* view); virtual ~FlutterPlatformNodeDelegateWindows(); // |ui::AXPlatformNodeDelegate| @@ -51,9 +50,10 @@ class FlutterPlatformNodeDelegateWindows : public FlutterPlatformNodeDelegate { private: ui::AXPlatformNode* ax_platform_node_; - FlutterWindowsEngine* engine_; + std::weak_ptr bridge_; + FlutterWindowsView* view_; }; } // namespace flutter -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PLATFORM_NODE_DELEGATE_H_ +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_FLUTTER_PLATFORM_NODE_DELEGATE_WINDOWS_H_ diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index 887a50145af5a..1007bbed80ac1 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -16,7 +16,7 @@ #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h" #include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h" #include "flutter/shell/platform/common/path_utils.h" -#include "flutter/shell/platform/windows/accessibility_bridge_delegate_windows.h" +#include "flutter/shell/platform/windows/accessibility_bridge_windows.h" #include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/shell/platform/windows/system_utils.h" #include "flutter/shell/platform/windows/task_runner.h" @@ -610,12 +610,17 @@ void FlutterWindowsEngine::UpdateSemanticsEnabled(bool enabled) { if (!semantics_enabled_ && accessibility_bridge_) { accessibility_bridge_.reset(); } else if (semantics_enabled_ && !accessibility_bridge_) { - accessibility_bridge_ = std::make_shared( - std::make_unique(this)); + accessibility_bridge_ = CreateAccessibilityBridge(this, view()); } } } +std::shared_ptr +FlutterWindowsEngine::CreateAccessibilityBridge(FlutterWindowsEngine* engine, + FlutterWindowsView* view) { + return std::make_shared(engine, view); +} + gfx::NativeViewAccessible FlutterWindowsEngine::GetNativeAccessibleFromId( AccessibilityNodeId id) { if (!accessibility_bridge_) { diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index 3cdee97e3cb92..64a4f0fa5bfd7 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -244,6 +244,15 @@ class FlutterWindowsEngine { // Updates accessibility, e.g. switch to high contrast mode void UpdateAccessibilityFeatures(FlutterAccessibilityFeature flags); + protected: + // Creates an accessibility bridge with the provided parameters. + // + // By default this method calls AccessibilityBridge's constructor. Exposing + // this method allows unit tests to override in order to capture information. + virtual std::shared_ptr CreateAccessibilityBridge( + FlutterWindowsEngine* engine, + FlutterWindowsView* view); + private: // Allows swapping out embedder_api_ calls in tests. friend class EngineModifier;