mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
fix AccessibilityBridgeMacDelegate to grab nswindow from appdelegate (flutter/engine#25698)
This commit is contained in:
parent
5c59e9637d
commit
5effcd39cd
@ -1111,6 +1111,7 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterView
|
||||
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/FlutterAppDelegate.mm
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h
|
||||
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.mm
|
||||
|
||||
@ -167,6 +167,7 @@ executable("flutter_desktop_darwin_unittests") {
|
||||
testonly = true
|
||||
|
||||
sources = [
|
||||
"framework/Source/AccessibilityBridgeMacDelegateTest.mm",
|
||||
"framework/Source/FlutterChannelKeyResponderUnittests.mm",
|
||||
"framework/Source/FlutterEmbedderExternalTextureUnittests.mm",
|
||||
"framework/Source/FlutterEmbedderKeyResponderUnittests.mm",
|
||||
|
||||
@ -48,8 +48,8 @@ class AccessibilityBridgeMacDelegate : public AccessibilityBridge::Accessibility
|
||||
/// accessibility notification system.
|
||||
/// @param[in] native_node The event target, must not be nil.
|
||||
/// @param[in] mac_notification The event name, must not be nil.
|
||||
void DispatchMacOSNotification(gfx::NativeViewAccessible native_node,
|
||||
NSAccessibilityNotificationName mac_notification);
|
||||
virtual void DispatchMacOSNotification(gfx::NativeViewAccessible native_node,
|
||||
NSAccessibilityNotificationName mac_notification);
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
/// @brief Posts the given event against the given node with the
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
|
||||
#include "flutter/shell/platform/embedder/embedder.h"
|
||||
|
||||
namespace flutter {
|
||||
@ -26,6 +27,10 @@ AccessibilityBridgeMacDelegate::AccessibilityBridgeMacDelegate(__weak FlutterEng
|
||||
|
||||
void AccessibilityBridgeMacDelegate::OnAccessibilityEvent(
|
||||
ui::AXEventGenerator::TargetedEvent targeted_event) {
|
||||
if (!flutter_engine_.viewController.viewLoaded || !flutter_engine_.viewController.view.window) {
|
||||
// We 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<AccessibilityBridgeMacDelegate::NSAccessibilityEvent> events =
|
||||
MacOSEventsFromAXEvent(targeted_event.event_params.event, *ax_node);
|
||||
@ -265,9 +270,10 @@ AccessibilityBridgeMacDelegate::MacOSEventsFromAXEvent(ui::AXEventGenerator::Eve
|
||||
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 = [NSApp mainWindow],
|
||||
.target = flutter_engine_.viewController.view.window,
|
||||
.user_info = nil,
|
||||
});
|
||||
break;
|
||||
@ -335,6 +341,9 @@ void AccessibilityBridgeMacDelegate::DispatchAccessibilityAction(ui::AXNode::AXI
|
||||
FlutterSemanticsAction action,
|
||||
const std::vector<uint8_t>& data) {
|
||||
NSCAssert(flutter_engine_, @"Flutter engine should not be deallocated");
|
||||
NSCAssert(flutter_engine_.viewController.viewLoaded && flutter_engine_.viewController.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:data];
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,194 @@
|
||||
// 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/testing/testing.h"
|
||||
|
||||
#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/FlutterEngine_Internal.h"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
|
||||
namespace flutter::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
class AccessibilityBridgeMacDelegateSpy : public AccessibilityBridgeMacDelegate {
|
||||
public:
|
||||
AccessibilityBridgeMacDelegateSpy(__weak FlutterEngine* flutter_engine)
|
||||
: AccessibilityBridgeMacDelegate(flutter_engine) {}
|
||||
|
||||
std::unordered_map<std::string, gfx::NativeViewAccessible> actual_notifications;
|
||||
|
||||
private:
|
||||
void DispatchMacOSNotification(gfx::NativeViewAccessible native_node,
|
||||
NSAccessibilityNotificationName mac_notification) override {
|
||||
actual_notifications[[mac_notification UTF8String]] = native_node;
|
||||
}
|
||||
};
|
||||
|
||||
// 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];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(AccessibilityBridgeMacDelegateTest,
|
||||
sendsAccessibilityCreateNotificationToWindowOfFlutterView) {
|
||||
FlutterEngine* engine = CreateTestEngine();
|
||||
NSString* fixtures = @(testing::GetFixturesPath());
|
||||
FlutterDartProject* project = [[FlutterDartProject alloc]
|
||||
initWithAssetsPath:fixtures
|
||||
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
|
||||
FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
|
||||
[viewController loadView];
|
||||
[engine setViewController:viewController];
|
||||
|
||||
NSWindow* expectedTarget = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
|
||||
styleMask:NSBorderlessWindowMask
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
expectedTarget.contentView = viewController.view;
|
||||
// Setting up bridge so that the AccessibilityBridgeMacDelegateSpy
|
||||
// can query semantics information from.
|
||||
engine.semanticsEnabled = YES;
|
||||
auto bridge = engine.accessibilityBridge.lock();
|
||||
FlutterSemanticsNode root;
|
||||
root.id = 0;
|
||||
root.flags = static_cast<FlutterSemanticsFlag>(0);
|
||||
root.actions = static_cast<FlutterSemanticsAction>(0);
|
||||
root.text_selection_base = -1;
|
||||
root.text_selection_extent = -1;
|
||||
root.label = "root";
|
||||
root.hint = "";
|
||||
root.value = "";
|
||||
root.increased_value = "";
|
||||
root.decreased_value = "";
|
||||
root.child_count = 0;
|
||||
root.custom_accessibility_actions_count = 0;
|
||||
bridge->AddFlutterSemanticsNodeUpdate(&root);
|
||||
|
||||
bridge->CommitUpdates();
|
||||
auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
|
||||
|
||||
AccessibilityBridgeMacDelegateSpy spy(engine);
|
||||
|
||||
// Creates a targeted event.
|
||||
ui::AXTree tree;
|
||||
ui::AXNode ax_node(&tree, nullptr, 0, 0);
|
||||
ui::AXNodeData node_data;
|
||||
node_data.id = 0;
|
||||
ax_node.SetData(node_data);
|
||||
std::vector<ui::AXEventIntent> intent;
|
||||
ui::AXEventGenerator::EventParams event_params(ui::AXEventGenerator::Event::CHILDREN_CHANGED,
|
||||
ax::mojom::EventFrom::kNone, intent);
|
||||
ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params);
|
||||
|
||||
spy.OnAccessibilityEvent(targeted_event);
|
||||
|
||||
EXPECT_EQ(spy.actual_notifications.size(), 1u);
|
||||
EXPECT_EQ(spy.actual_notifications.find([NSAccessibilityCreatedNotification UTF8String])->second,
|
||||
expectedTarget);
|
||||
[engine shutDownEngine];
|
||||
}
|
||||
|
||||
TEST(AccessibilityBridgeMacDelegateTest, doesNotSendAccessibilityCreateNotificationWhenHeadless) {
|
||||
FlutterEngine* engine = CreateTestEngine();
|
||||
// Setting up bridge so that the AccessibilityBridgeMacDelegateSpy
|
||||
// can query semantics information from.
|
||||
engine.semanticsEnabled = YES;
|
||||
auto bridge = engine.accessibilityBridge.lock();
|
||||
FlutterSemanticsNode root;
|
||||
root.id = 0;
|
||||
root.flags = static_cast<FlutterSemanticsFlag>(0);
|
||||
root.actions = static_cast<FlutterSemanticsAction>(0);
|
||||
root.text_selection_base = -1;
|
||||
root.text_selection_extent = -1;
|
||||
root.label = "root";
|
||||
root.hint = "";
|
||||
root.value = "";
|
||||
root.increased_value = "";
|
||||
root.decreased_value = "";
|
||||
root.child_count = 0;
|
||||
root.custom_accessibility_actions_count = 0;
|
||||
bridge->AddFlutterSemanticsNodeUpdate(&root);
|
||||
|
||||
bridge->CommitUpdates();
|
||||
auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
|
||||
|
||||
AccessibilityBridgeMacDelegateSpy spy(engine);
|
||||
|
||||
// Creates a targeted event.
|
||||
ui::AXTree tree;
|
||||
ui::AXNode ax_node(&tree, nullptr, 0, 0);
|
||||
ui::AXNodeData node_data;
|
||||
node_data.id = 0;
|
||||
ax_node.SetData(node_data);
|
||||
std::vector<ui::AXEventIntent> intent;
|
||||
ui::AXEventGenerator::EventParams event_params(ui::AXEventGenerator::Event::CHILDREN_CHANGED,
|
||||
ax::mojom::EventFrom::kNone, intent);
|
||||
ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params);
|
||||
|
||||
spy.OnAccessibilityEvent(targeted_event);
|
||||
|
||||
// Does not send any notification if the engine is headless.
|
||||
EXPECT_EQ(spy.actual_notifications.size(), 0u);
|
||||
[engine shutDownEngine];
|
||||
}
|
||||
|
||||
TEST(AccessibilityBridgeMacDelegateTest, doesNotSendAccessibilityCreateNotificationWhenNoWindow) {
|
||||
FlutterEngine* engine = CreateTestEngine();
|
||||
// Create a view controller without attaching it to a window.
|
||||
NSString* fixtures = @(testing::GetFixturesPath());
|
||||
FlutterDartProject* project = [[FlutterDartProject alloc]
|
||||
initWithAssetsPath:fixtures
|
||||
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
|
||||
FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
|
||||
[viewController loadView];
|
||||
[engine setViewController:viewController];
|
||||
|
||||
// Setting up bridge so that the AccessibilityBridgeMacDelegateSpy
|
||||
// can query semantics information from.
|
||||
engine.semanticsEnabled = YES;
|
||||
auto bridge = engine.accessibilityBridge.lock();
|
||||
FlutterSemanticsNode root;
|
||||
root.id = 0;
|
||||
root.flags = static_cast<FlutterSemanticsFlag>(0);
|
||||
root.actions = static_cast<FlutterSemanticsAction>(0);
|
||||
root.text_selection_base = -1;
|
||||
root.text_selection_extent = -1;
|
||||
root.label = "root";
|
||||
root.hint = "";
|
||||
root.value = "";
|
||||
root.increased_value = "";
|
||||
root.decreased_value = "";
|
||||
root.child_count = 0;
|
||||
root.custom_accessibility_actions_count = 0;
|
||||
bridge->AddFlutterSemanticsNodeUpdate(&root);
|
||||
|
||||
bridge->CommitUpdates();
|
||||
auto platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
|
||||
|
||||
AccessibilityBridgeMacDelegateSpy spy(engine);
|
||||
|
||||
// Creates a targeted event.
|
||||
ui::AXTree tree;
|
||||
ui::AXNode ax_node(&tree, nullptr, 0, 0);
|
||||
ui::AXNodeData node_data;
|
||||
node_data.id = 0;
|
||||
ax_node.SetData(node_data);
|
||||
std::vector<ui::AXEventIntent> intent;
|
||||
ui::AXEventGenerator::EventParams event_params(ui::AXEventGenerator::Event::CHILDREN_CHANGED,
|
||||
ax::mojom::EventFrom::kNone, intent);
|
||||
ui::AXEventGenerator::TargetedEvent targeted_event(&ax_node, event_params);
|
||||
|
||||
spy.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);
|
||||
[engine shutDownEngine];
|
||||
}
|
||||
|
||||
} // flutter::testing
|
||||
@ -156,4 +156,87 @@ TEST(FlutterEngine, CanToggleAccessibility) {
|
||||
[engine shutDownEngine];
|
||||
}
|
||||
|
||||
TEST(FlutterEngine, CanToggleAccessibilityWhenHeadless) {
|
||||
FlutterEngine* engine = CreateTestEngine();
|
||||
// Capture the update callbacks before the embedder API initializes.
|
||||
auto original_init = engine.embedderAPI.Initialize;
|
||||
std::function<void(const FlutterSemanticsNode*, void*)> update_node_callback;
|
||||
std::function<void(const FlutterSemanticsCustomAction*, void*)> update_action_callback;
|
||||
engine.embedderAPI.Initialize = MOCK_ENGINE_PROC(
|
||||
Initialize, ([&update_action_callback, &update_node_callback, &original_init](
|
||||
size_t version, const FlutterRendererConfig* config,
|
||||
const FlutterProjectArgs* args, void* user_data, auto engine_out) {
|
||||
update_node_callback = args->update_semantics_node_callback;
|
||||
update_action_callback = args->update_semantics_custom_action_callback;
|
||||
return original_init(version, config, args, user_data, engine_out);
|
||||
}));
|
||||
EXPECT_TRUE([engine runWithEntrypoint:@"main"]);
|
||||
|
||||
// Enable the semantics without attaching a view controller.
|
||||
bool enabled_called = false;
|
||||
engine.embedderAPI.UpdateSemanticsEnabled =
|
||||
MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) {
|
||||
enabled_called = enabled;
|
||||
return kSuccess;
|
||||
}));
|
||||
engine.semanticsEnabled = YES;
|
||||
EXPECT_TRUE(enabled_called);
|
||||
// Send flutter semantics updates.
|
||||
FlutterSemanticsNode root;
|
||||
root.id = 0;
|
||||
root.flags = static_cast<FlutterSemanticsFlag>(0);
|
||||
root.actions = static_cast<FlutterSemanticsAction>(0);
|
||||
root.text_selection_base = -1;
|
||||
root.text_selection_extent = -1;
|
||||
root.label = "root";
|
||||
root.hint = "";
|
||||
root.value = "";
|
||||
root.increased_value = "";
|
||||
root.decreased_value = "";
|
||||
root.child_count = 1;
|
||||
int32_t children[] = {1};
|
||||
root.children_in_traversal_order = children;
|
||||
root.custom_accessibility_actions_count = 0;
|
||||
update_node_callback(&root, (void*)CFBridgingRetain(engine));
|
||||
|
||||
FlutterSemanticsNode child1;
|
||||
child1.id = 1;
|
||||
child1.flags = static_cast<FlutterSemanticsFlag>(0);
|
||||
child1.actions = static_cast<FlutterSemanticsAction>(0);
|
||||
child1.text_selection_base = -1;
|
||||
child1.text_selection_extent = -1;
|
||||
child1.label = "child 1";
|
||||
child1.hint = "";
|
||||
child1.value = "";
|
||||
child1.increased_value = "";
|
||||
child1.decreased_value = "";
|
||||
child1.child_count = 0;
|
||||
child1.custom_accessibility_actions_count = 0;
|
||||
update_node_callback(&child1, (void*)CFBridgingRetain(engine));
|
||||
|
||||
FlutterSemanticsNode node_batch_end;
|
||||
node_batch_end.id = kFlutterSemanticsNodeIdBatchEnd;
|
||||
update_node_callback(&node_batch_end, (void*)CFBridgingRetain(engine));
|
||||
|
||||
FlutterSemanticsCustomAction action_batch_end;
|
||||
action_batch_end.id = kFlutterSemanticsNodeIdBatchEnd;
|
||||
update_action_callback(&action_batch_end, (void*)CFBridgingRetain(engine));
|
||||
|
||||
// No crashes.
|
||||
EXPECT_EQ(engine.viewController, nil);
|
||||
|
||||
// Disable the semantics.
|
||||
bool semanticsEnabled = true;
|
||||
engine.embedderAPI.UpdateSemanticsEnabled =
|
||||
MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&semanticsEnabled](auto engine, bool enabled) {
|
||||
semanticsEnabled = enabled;
|
||||
return kSuccess;
|
||||
}));
|
||||
engine.semanticsEnabled = NO;
|
||||
EXPECT_FALSE(semanticsEnabled);
|
||||
// Still no crashes
|
||||
EXPECT_EQ(engine.viewController, nil);
|
||||
[engine shutDownEngine];
|
||||
}
|
||||
|
||||
} // namespace flutter::testing
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#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"
|
||||
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
|
||||
#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
|
||||
#include "flutter/third_party/accessibility/ax/ax_action_data.h"
|
||||
|
||||
@ -135,6 +136,22 @@ TEST(FlutterPlatformNodeDelegateMac, SelectableTextWithoutSelectionReturnZeroRan
|
||||
|
||||
TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
|
||||
FlutterEngine* engine = CreateTestEngine();
|
||||
|
||||
// Set up view controller.
|
||||
NSString* fixtures = @(testing::GetFixturesPath());
|
||||
FlutterDartProject* project = [[FlutterDartProject alloc]
|
||||
initWithAssetsPath:fixtures
|
||||
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
|
||||
FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
|
||||
[engine setViewController:viewController];
|
||||
|
||||
// Attach the view to a NSWindow.
|
||||
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
|
||||
styleMask:NSBorderlessWindowMask
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
window.contentView = viewController.view;
|
||||
|
||||
engine.semanticsEnabled = YES;
|
||||
auto bridge = engine.accessibilityBridge.lock();
|
||||
// Initialize ax node data.
|
||||
@ -186,6 +203,8 @@ TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
|
||||
|
||||
EXPECT_EQ(called_action, FlutterSemanticsAction::kFlutterSemanticsActionTap);
|
||||
EXPECT_EQ(called_id, 1u);
|
||||
|
||||
[engine setViewController:nil];
|
||||
[engine shutDownEngine];
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user