Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Preliminary implementation of UIA for A11y on Windows #37754

Merged
merged 19 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -3277,6 +3277,7 @@ FILE: ../../../flutter/shell/platform/windows/windows_proc_table.cc
FILE: ../../../flutter/shell/platform/windows/windows_proc_table.h
FILE: ../../../flutter/shell/platform/windows/windows_registry.cc
FILE: ../../../flutter/shell/platform/windows/windows_registry.h
FILE: ../../../flutter/shell/platform/windows/windowsx_shim.h
FILE: ../../../flutter/shell/profiling/sampling_profiler.cc
FILE: ../../../flutter/shell/profiling/sampling_profiler.h
FILE: ../../../flutter/shell/profiling/sampling_profiler_unittest.cc
Expand Down
1 change: 1 addition & 0 deletions shell/platform/windows/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ source_set("flutter_windows_source") {
"windows_proc_table.h",
"windows_registry.cc",
"windows_registry.h",
"windowsx_shim.h",
]

libs = [
Expand Down
61 changes: 44 additions & 17 deletions shell/platform/windows/accessibility_bridge_windows.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,53 +31,67 @@ void AccessibilityBridgeWindows::OnAccessibilityEvent(

switch (event_type) {
case ui::AXEventGenerator::Event::ALERT:
DispatchWinAccessibilityEvent(win_delegate, EVENT_SYSTEM_ALERT);
DispatchWinAccessibilityEvent(win_delegate, ax::mojom::Event::kAlert);
break;
case ui::AXEventGenerator::Event::CHECKED_STATE_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_VALUECHANGE);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kValueChanged);
break;
case ui::AXEventGenerator::Event::CHILDREN_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_REORDER);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kChildrenChanged);
break;
case ui::AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED:
DispatchWinAccessibilityEvent(
win_delegate, ax::mojom::Event::kDocumentSelectionChanged);
break;
case ui::AXEventGenerator::Event::FOCUS_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_FOCUS);
DispatchWinAccessibilityEvent(win_delegate, ax::mojom::Event::kFocus);
SetFocus(win_delegate);
break;
case ui::AXEventGenerator::Event::IGNORED_CHANGED:
if (ax_node->IsIgnored()) {
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_HIDE);
DispatchWinAccessibilityEvent(win_delegate, ax::mojom::Event::kHide);
}
break;
case ui::AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_NAMECHANGE);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kTextChanged);
break;
case ui::AXEventGenerator::Event::LIVE_REGION_CHANGED:
DispatchWinAccessibilityEvent(win_delegate,
EVENT_OBJECT_LIVEREGIONCHANGED);
ax::mojom::Event::kLiveRegionChanged);
break;
case ui::AXEventGenerator::Event::NAME_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_NAMECHANGE);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kTextChanged);
break;
case ui::AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_SYSTEM_SCROLLINGEND);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kScrollPositionChanged);
break;
case ui::AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_SYSTEM_SCROLLINGEND);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kScrollPositionChanged);
break;
case ui::AXEventGenerator::Event::SELECTED_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_VALUECHANGE);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kValueChanged);
break;
case ui::AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_SELECTIONWITHIN);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kSelectedChildrenChanged);
break;
case ui::AXEventGenerator::Event::SUBTREE_CREATED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_SHOW);
DispatchWinAccessibilityEvent(win_delegate, ax::mojom::Event::kShow);
break;
case ui::AXEventGenerator::Event::VALUE_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_VALUECHANGE);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kValueChanged);
break;
case ui::AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED:
DispatchWinAccessibilityEvent(win_delegate, EVENT_OBJECT_STATECHANGE);
DispatchWinAccessibilityEvent(win_delegate,
ax::mojom::Event::kStateChanged);
break;
case ui::AXEventGenerator::Event::ACCESS_KEY_CHANGED:
case ui::AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED:
Expand All @@ -90,7 +104,6 @@ void AccessibilityBridgeWindows::OnAccessibilityEvent(
case ui::AXEventGenerator::Event::CONTROLS_CHANGED:
case ui::AXEventGenerator::Event::DESCRIBED_BY_CHANGED:
case ui::AXEventGenerator::Event::DESCRIPTION_CHANGED:
case ui::AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED:
case ui::AXEventGenerator::Event::DOCUMENT_TITLE_CHANGED:
case ui::AXEventGenerator::Event::DROPEFFECT_CHANGED:
case ui::AXEventGenerator::Event::ENABLED_CHANGED:
Expand Down Expand Up @@ -151,7 +164,7 @@ AccessibilityBridgeWindows::CreateFlutterPlatformNodeDelegate() {

void AccessibilityBridgeWindows::DispatchWinAccessibilityEvent(
std::shared_ptr<FlutterPlatformNodeDelegateWindows> node_delegate,
DWORD event_type) {
ax::mojom::Event event_type) {
node_delegate->DispatchWinAccessibilityEvent(event_type);
}

Expand All @@ -160,4 +173,18 @@ void AccessibilityBridgeWindows::SetFocus(
node_delegate->SetFocus();
}

gfx::NativeViewAccessible
AccessibilityBridgeWindows::GetChildOfAXFragmentRoot() {
return view_->GetNativeViewAccessible();
}

gfx::NativeViewAccessible
AccessibilityBridgeWindows::GetParentOfAXFragmentRoot() {
return nullptr;
}

bool AccessibilityBridgeWindows::IsAXFragmentRootAControlElement() {
return true;
}

} // namespace flutter
19 changes: 14 additions & 5 deletions shell/platform/windows/accessibility_bridge_windows.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
#define FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_BRIDGE_WINDOWS_H_

#include "flutter/shell/platform/common/accessibility_bridge.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_fragment_root_delegate_win.h"

namespace flutter {

class FlutterWindowsEngine;
class FlutterWindowsView;
class FlutterPlatformNodeDelegateWindows;

// The Win32 implementation of AccessibilityBridge.
Expand All @@ -24,7 +23,8 @@ class FlutterPlatformNodeDelegateWindows;
///
/// AccessibilityBridgeWindows must be created as a shared_ptr, since some
/// methods acquires its weak_ptr.
class AccessibilityBridgeWindows : public AccessibilityBridge {
class AccessibilityBridgeWindows : public AccessibilityBridge,
public ui::AXFragmentRootDelegateWin {
public:
AccessibilityBridgeWindows(FlutterWindowsEngine* engine,
FlutterWindowsView* view);
Expand All @@ -41,7 +41,7 @@ class AccessibilityBridgeWindows : public AccessibilityBridge {
// This is a virtual method for the convenience of unit tests.
virtual void DispatchWinAccessibilityEvent(
std::shared_ptr<FlutterPlatformNodeDelegateWindows> node_delegate,
DWORD event_type);
ax::mojom::Event event_type);

// Sets the accessibility focus to the accessibility node associated with the
// specified semantics node.
Expand All @@ -50,6 +50,15 @@ class AccessibilityBridgeWindows : public AccessibilityBridge {
virtual void SetFocus(
std::shared_ptr<FlutterPlatformNodeDelegateWindows> node_delegate);

// |AXFragmentRootDelegateWin|
gfx::NativeViewAccessible GetChildOfAXFragmentRoot() override;

// |AXFragmentRootDelegateWin|
gfx::NativeViewAccessible GetParentOfAXFragmentRoot() override;

// |AXFragmentRootDelegateWin|
bool IsAXFragmentRootAControlElement() override;

protected:
// |AccessibilityBridge|
void OnAccessibilityEvent(
Expand Down
37 changes: 19 additions & 18 deletions shell/platform/windows/accessibility_bridge_windows_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace {
// A structure representing a Win32 MSAA event targeting a specified node.
struct MsaaEvent {
std::shared_ptr<FlutterPlatformNodeDelegateWindows> node_delegate;
DWORD event_type;
ax::mojom::Event event_type;
};

// Accessibility bridge delegate that captures events dispatched to the OS.
Expand All @@ -43,7 +43,7 @@ class AccessibilityBridgeWindowsSpy : public AccessibilityBridgeWindows {

void DispatchWinAccessibilityEvent(
std::shared_ptr<FlutterPlatformNodeDelegateWindows> node_delegate,
DWORD event_type) override {
ax::mojom::Event event_type) override {
dispatched_events_.push_back({node_delegate, event_type});
}

Expand Down Expand Up @@ -76,7 +76,7 @@ class FlutterWindowsEngineSpy : public FlutterWindowsEngine {
: FlutterWindowsEngine(project) {}

protected:
virtual std::shared_ptr<AccessibilityBridge> CreateAccessibilityBridge(
virtual std::shared_ptr<AccessibilityBridgeWindows> CreateAccessibilityBridge(
FlutterWindowsEngine* engine,
FlutterWindowsView* view) override {
return std::make_shared<AccessibilityBridgeWindowsSpy>(engine, view);
Expand Down Expand Up @@ -169,7 +169,7 @@ std::shared_ptr<AccessibilityBridgeWindowsSpy> GetAccessibilityBridgeSpy(

void ExpectWinEventFromAXEvent(int32_t node_id,
ui::AXEventGenerator::Event ax_event,
DWORD expected_event) {
ax::mojom::Event expected_event) {
auto window_binding_handler =
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>();
FlutterWindowsView view(std::move(window_binding_handler));
Expand Down Expand Up @@ -246,12 +246,12 @@ TEST(AccessibilityBridgeWindows, DispatchAccessibilityAction) {

TEST(AccessibilityBridgeWindows, OnAccessibilityEventAlert) {
ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::ALERT,
EVENT_SYSTEM_ALERT);
ax::mojom::Event::kAlert);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityEventChildrenChanged) {
ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::CHILDREN_CHANGED,
EVENT_OBJECT_REORDER);
ax::mojom::Event::kChildrenChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityEventFocusChanged) {
Expand All @@ -270,7 +270,8 @@ TEST(AccessibilityBridgeWindows, OnAccessibilityEventFocusChanged) {
ax::mojom::EventFrom::kNone,
{}}});
ASSERT_EQ(bridge->dispatched_events().size(), 1);
EXPECT_EQ(bridge->dispatched_events()[0].event_type, EVENT_OBJECT_FOCUS);
EXPECT_EQ(bridge->dispatched_events()[0].event_type,
ax::mojom::Event::kFocus);

ASSERT_EQ(bridge->focused_nodes().size(), 1);
EXPECT_EQ(bridge->focused_nodes()[0], 1);
Expand All @@ -279,62 +280,62 @@ TEST(AccessibilityBridgeWindows, OnAccessibilityEventFocusChanged) {
TEST(AccessibilityBridgeWindows, OnAccessibilityEventIgnoredChanged) {
// Static test nodes with no text, hint, or scrollability are ignored.
ExpectWinEventFromAXEvent(4, ui::AXEventGenerator::Event::IGNORED_CHANGED,
EVENT_OBJECT_HIDE);
ax::mojom::Event::kHide);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityImageAnnotationChanged) {
ExpectWinEventFromAXEvent(
1, ui::AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED,
EVENT_OBJECT_NAMECHANGE);
ax::mojom::Event::kTextChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityLiveRegionChanged) {
ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::LIVE_REGION_CHANGED,
EVENT_OBJECT_LIVEREGIONCHANGED);
ax::mojom::Event::kLiveRegionChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityNameChanged) {
ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::NAME_CHANGED,
EVENT_OBJECT_NAMECHANGE);
ax::mojom::Event::kTextChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityHScrollPosChanged) {
ExpectWinEventFromAXEvent(
1, ui::AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED,
EVENT_SYSTEM_SCROLLINGEND);
ax::mojom::Event::kScrollPositionChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityVScrollPosChanged) {
ExpectWinEventFromAXEvent(
1, ui::AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED,
EVENT_SYSTEM_SCROLLINGEND);
ax::mojom::Event::kScrollPositionChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilitySelectedChanged) {
ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::SELECTED_CHANGED,
EVENT_OBJECT_VALUECHANGE);
ax::mojom::Event::kValueChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilitySelectedChildrenChanged) {
ExpectWinEventFromAXEvent(
2, ui::AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED,
EVENT_OBJECT_SELECTIONWITHIN);
ax::mojom::Event::kSelectedChildrenChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilitySubtreeCreated) {
ExpectWinEventFromAXEvent(0, ui::AXEventGenerator::Event::SUBTREE_CREATED,
EVENT_OBJECT_SHOW);
ax::mojom::Event::kShow);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityValueChanged) {
ExpectWinEventFromAXEvent(1, ui::AXEventGenerator::Event::VALUE_CHANGED,
EVENT_OBJECT_VALUECHANGE);
ax::mojom::Event::kValueChanged);
}

TEST(AccessibilityBridgeWindows, OnAccessibilityStateChanged) {
ExpectWinEventFromAXEvent(
1, ui::AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED,
EVENT_OBJECT_STATECHANGE);
ax::mojom::Event::kStateChanged);
}

} // namespace testing
Expand Down
16 changes: 8 additions & 8 deletions shell/platform/windows/flutter_platform_node_delegate_windows.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#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"
#include "flutter/third_party/accessibility/ax/platform/ax_fragment_root_win.h"

namespace flutter {

Expand Down Expand Up @@ -90,14 +91,8 @@ gfx::Rect FlutterPlatformNodeDelegateWindows::GetBoundsRect(
}

void FlutterPlatformNodeDelegateWindows::DispatchWinAccessibilityEvent(
DWORD event_type) {
HWND hwnd = view_->GetPlatformWindow();
if (!hwnd) {
return;
}
assert(ax_platform_node_);
::NotifyWinEvent(event_type, hwnd, OBJID_CLIENT,
-ax_platform_node_->GetUniqueId());
ax::mojom::Event event_type) {
ax_platform_node_->NotifyAccessibilityEvent(event_type);
}

void FlutterPlatformNodeDelegateWindows::SetFocus() {
Expand All @@ -107,4 +102,9 @@ void FlutterPlatformNodeDelegateWindows::SetFocus() {
GetNativeViewAccessible()->accSelect(SELFLAG_TAKEFOCUS, varchild);
}

gfx::AcceleratedWidget
FlutterPlatformNodeDelegateWindows::GetTargetForNativeAccessibilityEvent() {
return view_->GetPlatformWindow();
}

} // namespace flutter
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ class FlutterPlatformNodeDelegateWindows : public FlutterPlatformNodeDelegate {
// Dispatches a Windows accessibility event of the specified type, generated
// by the accessibility node associated with this object. This is a
// convenience wrapper around |NotifyWinEvent|.
virtual void DispatchWinAccessibilityEvent(DWORD event_type);
virtual void DispatchWinAccessibilityEvent(ax::mojom::Event event_type);

// Sets the accessibility focus to the accessibility node associated with
// this object.
void SetFocus();

// | AXPlatformNodeDelegate |
gfx::AcceleratedWidget GetTargetForNativeAccessibilityEvent() override;

private:
ui::AXPlatformNode* ax_platform_node_;
std::weak_ptr<AccessibilityBridge> bridge_;
Expand Down
4 changes: 4 additions & 0 deletions shell/platform/windows/flutter_window.cc
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,8 @@ AccessibilityRootNode* FlutterWindow::GetAccessibilityRootNode() {
return accessibility_root_;
}

ui::AXFragmentRootDelegateWin* FlutterWindow::GetAxFragmentRootDelegate() {
return binding_handler_delegate_->GetAxFragmentRootDelegate();
}

} // namespace flutter
Loading