diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index c225182016aa6..1d4d1becba56f 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -3059,9 +3059,13 @@ FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_texture_regi FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_value.h 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_root_node.cc +FILE: ../../../flutter/shell/platform/windows/accessibility_root_node.h FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.cc FILE: ../../../flutter/shell/platform/windows/angle_surface_manager.h FILE: ../../../flutter/shell/platform/windows/client_wrapper/dart_project_unittests.cc diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index 9f6b3b1b7fdbd..1ed92aec50f1d 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -38,8 +38,12 @@ source_set("flutter_windows_headers") { source_set("flutter_windows_source") { # Common Windows sources. sources = [ + "accessibility_alert.cc", + "accessibility_alert.h", "accessibility_bridge_delegate_windows.cc", "accessibility_bridge_delegate_windows.h", + "accessibility_root_node.cc", + "accessibility_root_node.h", "angle_surface_manager.cc", "angle_surface_manager.h", "cursor_handler.cc", diff --git a/shell/platform/windows/accessibility_alert.cc b/shell/platform/windows/accessibility_alert.cc new file mode 100644 index 0000000000000..9ac8cbda5fe3a --- /dev/null +++ b/shell/platform/windows/accessibility_alert.cc @@ -0,0 +1,168 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/windows/accessibility_alert.h" + +#include "flutter/shell/platform/windows/accessibility_root_node.h" + +namespace flutter { + +AccessibilityAlert::AccessibilityAlert() : text_(L""), parent_(nullptr) {} + +// IAccessible methods. + +IFACEMETHODIMP AccessibilityAlert::accHitTest(LONG screen_physical_pixel_x, + LONG screen_physical_pixel_y, + VARIANT* child) { + child->vt = VT_EMPTY; + return S_FALSE; +} + +// Performs the object's default action. +IFACEMETHODIMP AccessibilityAlert::accDoDefaultAction(VARIANT var_id) { + return E_FAIL; +} + +// Retrieves an IDispatch interface pointer for the specified child. +IFACEMETHODIMP AccessibilityAlert::get_accChild(VARIANT var_child, + IDispatch** disp_child) { + if (V_VT(&var_child) == VT_I4 && V_I4(&var_child) == CHILDID_SELF) { + *disp_child = this; + AddRef(); + return S_OK; + } + *disp_child = nullptr; + return E_FAIL; +} + +// Retrieves the number of accessible children. +IFACEMETHODIMP AccessibilityAlert::get_accChildCount(LONG* child_count) { + *child_count = 0; + return S_OK; +} + +// Retrieves the tooltip description. +IFACEMETHODIMP AccessibilityAlert::get_accDescription(VARIANT var_id, + BSTR* desc) { + *desc = SysAllocString(text_.c_str()); + return S_OK; +} + +// Retrieves the name of the specified object. +IFACEMETHODIMP AccessibilityAlert::get_accName(VARIANT var_id, BSTR* name) { + *name = SysAllocString(text_.c_str()); + return S_OK; +} + +// Retrieves the IDispatch interface of the object's parent. +IFACEMETHODIMP AccessibilityAlert::get_accParent(IDispatch** disp_parent) { + *disp_parent = parent_; + if (*disp_parent) { + (*disp_parent)->AddRef(); + return S_OK; + } + return S_FALSE; +} + +// Retrieves information describing the role of the specified object. +IFACEMETHODIMP AccessibilityAlert::get_accRole(VARIANT var_id, VARIANT* role) { + *role = {.vt = VT_I4, .lVal = ROLE_SYSTEM_ALERT}; + return S_OK; +} + +// Retrieves the current state of the specified object. +IFACEMETHODIMP AccessibilityAlert::get_accState(VARIANT var_id, + VARIANT* state) { + *state = {.vt = VT_I4, .lVal = STATE_SYSTEM_DEFAULT}; + return S_OK; +} + +// Gets the help string for the specified object. +IFACEMETHODIMP AccessibilityAlert::get_accHelp(VARIANT var_id, BSTR* help) { + *help = SysAllocString(L""); + return S_OK; +} + +// Retrieve or set the string value associated with the specified object. +// Setting the value is not typically used by screen readers, but it's +// used frequently by automation software. +IFACEMETHODIMP AccessibilityAlert::get_accValue(VARIANT var_id, BSTR* value) { + *value = SysAllocString(text_.c_str()); + return S_OK; +} + +// IAccessible methods not implemented. +IFACEMETHODIMP AccessibilityAlert::get_accSelection(VARIANT* selected) { + selected->vt = VT_EMPTY; + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::accSelect(LONG flags_sel, VARIANT var_id) { + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::put_accValue(VARIANT var_id, + BSTR new_value) { + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::get_accFocus(VARIANT* focus_child) { + focus_child->vt = VT_EMPTY; + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::get_accHelpTopic(BSTR* help_file, + VARIANT var_id, + LONG* topic_id) { + if (help_file) { + *help_file = nullptr; + } + if (topic_id) { + *topic_id = 0; + } + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::put_accName(VARIANT var_id, BSTR put_name) { + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::get_accKeyboardShortcut(VARIANT var_id, + BSTR* access_key) { + *access_key = nullptr; + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::accLocation(LONG* physical_pixel_left, + LONG* physical_pixel_top, + LONG* width, + LONG* height, + VARIANT var_id) { + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::accNavigate(LONG nav_dir, + VARIANT start, + VARIANT* end) { + end->vt = VT_EMPTY; + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityAlert::get_accDefaultAction(VARIANT var_id, + BSTR* default_action) { + *default_action = nullptr; + return E_NOTIMPL; +} + +// End of IAccessible methods. + +void AccessibilityAlert::SetText(const std::wstring& text) { + text_ = text; +} + +void AccessibilityAlert::SetParent(AccessibilityRootNode* parent) { + parent_ = parent; +} + +} // namespace flutter diff --git a/shell/platform/windows/accessibility_alert.h b/shell/platform/windows/accessibility_alert.h new file mode 100644 index 0000000000000..c60ede1479030 --- /dev/null +++ b/shell/platform/windows/accessibility_alert.h @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_ALERT_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_ALERT_H_ + +#include +#include +#include + +#include + +namespace flutter { + +class AccessibilityRootNode; + +// An IAccessible node representing an alert read to the screen reader. +// When an announcement is requested by the framework, an instance of +// this class, if none exists already, is created and made a child of +// the root AccessibilityRootNode node, and is therefore also a sibling +// of the window's root node. +// This node is not interactable to the user. +class AccessibilityAlert : public CComObjectRootEx, + public IDispatchImpl { + public: + BEGIN_COM_MAP(AccessibilityAlert) + COM_INTERFACE_ENTRY(IAccessible) + END_COM_MAP() + // + // IAccessible methods. + // + + // Retrieves the child element or child object at a given point on the screen. + IFACEMETHODIMP accHitTest(LONG screen_physical_pixel_x, + LONG screen_physical_pixel_y, + VARIANT* child) override; + + // Retrieves an IDispatch interface pointer for the specified child. + IFACEMETHODIMP get_accChild(VARIANT var_child, + IDispatch** disp_child) override; + + // Retrieves the number of accessible children. + IFACEMETHODIMP get_accChildCount(LONG* child_count) override; + + // Retrieves a string that describes the object's default action. + IFACEMETHODIMP get_accDefaultAction(VARIANT var_id, + BSTR* default_action) override; + + // Retrieves the tooltip description. + IFACEMETHODIMP get_accDescription(VARIANT var_id, BSTR* desc) override; + + // Retrieves the name of the specified object. + IFACEMETHODIMP get_accName(VARIANT var_id, BSTR* name) override; + + // Retrieves the IDispatch interface of the object's parent. + IFACEMETHODIMP get_accParent(IDispatch** disp_parent) override; + + // Retrieves information describing the role of the specified object. + IFACEMETHODIMP get_accRole(VARIANT var_id, VARIANT* role) override; + + // Retrieves the current state of the specified object. + IFACEMETHODIMP get_accState(VARIANT var_id, VARIANT* state) override; + + // Gets the help string for the specified object. + IFACEMETHODIMP get_accHelp(VARIANT var_id, BSTR* help) override; + + // Retrieve the string value associated with the specified object. + IFACEMETHODIMP get_accValue(VARIANT var_id, BSTR* value) override; + + // IAccessible methods not implemented. + IFACEMETHODIMP accLocation(LONG* physical_pixel_left, + LONG* physical_pixel_top, + LONG* width, + LONG* height, + VARIANT var_id) override; + IFACEMETHODIMP accNavigate(LONG nav_dir, + VARIANT start, + VARIANT* end) override; + IFACEMETHODIMP accDoDefaultAction(VARIANT var_id) override; + IFACEMETHODIMP get_accFocus(VARIANT* focus_child) override; + IFACEMETHODIMP get_accKeyboardShortcut(VARIANT var_id, + BSTR* access_key) override; + IFACEMETHODIMP get_accSelection(VARIANT* selected) override; + IFACEMETHODIMP accSelect(LONG flags_sel, VARIANT var_id) override; + IFACEMETHODIMP get_accHelpTopic(BSTR* help_file, + VARIANT var_id, + LONG* topic_id) override; + IFACEMETHODIMP put_accName(VARIANT var_id, BSTR put_name) override; + IFACEMETHODIMP put_accValue(VARIANT var_id, BSTR new_value) override; + + // End of IAccessible methods. + + AccessibilityAlert(); + ~AccessibilityAlert() = default; + + // Sets the text of this alert to the provided message. + void SetText(const std::wstring& text); + + void SetParent(AccessibilityRootNode* parent); + + private: + std::wstring text_; + + AccessibilityRootNode* parent_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_ALERT_H_ diff --git a/shell/platform/windows/accessibility_root_node.cc b/shell/platform/windows/accessibility_root_node.cc new file mode 100644 index 0000000000000..350cf66f17b27 --- /dev/null +++ b/shell/platform/windows/accessibility_root_node.cc @@ -0,0 +1,287 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/windows/accessibility_root_node.h" + +#include "flutter/fml/logging.h" +#include "flutter/third_party/accessibility/base/win/atl_module.h" + +namespace flutter { + +static constexpr LONG kWindowChildId = 1; +static constexpr LONG kInvalidChildId = 3; + +AccessibilityRootNode::AccessibilityRootNode() : alert_accessible_(nullptr) {} + +AccessibilityRootNode::~AccessibilityRootNode() { + if (alert_accessible_) { + alert_accessible_->Release(); + alert_accessible_ = nullptr; + } +} + +IAccessible* AccessibilityRootNode::GetTargetAndChildID(VARIANT* var_id) { + LONG& child_id = var_id->lVal; + if (V_VT(var_id) != VT_I4) { + child_id = kInvalidChildId; + return nullptr; + } + child_id = V_I4(var_id); + if (!window_accessible_) { + return nullptr; + } + if (child_id == CHILDID_SELF || child_id == kWindowChildId) { + child_id = CHILDID_SELF; + return window_accessible_; + } + if (child_id == kAlertChildId && alert_accessible_) { + child_id = CHILDID_SELF; + return alert_accessible_; + } + // A negative child ID can be used to refer to an AX node directly by its ID. + if (child_id < 0) { + return window_accessible_; + } + return nullptr; +} + +IFACEMETHODIMP AccessibilityRootNode::accHitTest(LONG screen_physical_pixel_x, + LONG screen_physical_pixel_y, + VARIANT* child) { + if (window_accessible_) { + return window_accessible_->accHitTest(screen_physical_pixel_x, + screen_physical_pixel_y, child); + } + child->vt = VT_EMPTY; + return S_FALSE; +} + +IFACEMETHODIMP AccessibilityRootNode::accDoDefaultAction(VARIANT var_id) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->accDoDefaultAction(var_id); + } + return E_FAIL; +} + +IFACEMETHODIMP AccessibilityRootNode::accLocation(LONG* physical_pixel_left, + LONG* physical_pixel_top, + LONG* width, + LONG* height, + VARIANT var_id) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->accLocation(physical_pixel_left, physical_pixel_top, width, + height, var_id); + } + return S_FALSE; +} + +IFACEMETHODIMP AccessibilityRootNode::accNavigate(LONG nav_dir, + VARIANT start, + VARIANT* end) { + IAccessible* target; + if ((target = GetTargetAndChildID(&start))) { + return target->accNavigate(nav_dir, start, end); + } + return S_FALSE; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accChild(VARIANT var_child, + IDispatch** disp_child) { + if (V_VT(&var_child) != VT_I4) { + return E_FAIL; + } + LONG child_id = V_I4(&var_child); + if (child_id == CHILDID_SELF) { + *disp_child = this; + } else if (!window_accessible_) { + return E_FAIL; + } else if (child_id == kWindowChildId) { + *disp_child = window_accessible_; + } else if (child_id == kAlertChildId && alert_accessible_) { + *disp_child = alert_accessible_; + } else if (child_id < 0) { + // A negative child ID can be used to refer to an AX node directly by its + // ID. + return window_accessible_->get_accChild(var_child, disp_child); + } else { + return E_FAIL; + } + (*disp_child)->AddRef(); + return S_OK; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accChildCount(LONG* child_count) { + LONG children = 0; + if (window_accessible_) { + children++; + } + if (alert_accessible_) { + children++; + } + *child_count = children; + return S_OK; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accDefaultAction(VARIANT var_id, + BSTR* def_action) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->get_accDefaultAction(var_id, def_action); + } + *def_action = nullptr; + return S_FALSE; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accDescription(VARIANT var_id, + BSTR* desc) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->get_accDescription(var_id, desc); + } + return E_FAIL; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accFocus(VARIANT* focus_child) { + if (window_accessible_) { + return window_accessible_->get_accFocus(focus_child); + } + focus_child->vt = VT_EMPTY; + return S_OK; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accKeyboardShortcut(VARIANT var_id, + BSTR* acc_key) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->get_accKeyboardShortcut(var_id, acc_key); + } + return E_FAIL; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accName(VARIANT var_id, + BSTR* name_bstr) { + if (V_I4(&var_id) == CHILDID_SELF) { + std::wstring name = L"ROOT_NODE_VIEW"; + *name_bstr = SysAllocString(name.c_str()); + return S_OK; + } + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->get_accName(var_id, name_bstr); + } + return S_FALSE; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accParent(IDispatch** disp_parent) { + return S_FALSE; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accRole(VARIANT var_id, + VARIANT* role) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->get_accRole(var_id, role); + } + return E_FAIL; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accState(VARIANT var_id, + VARIANT* state) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->get_accState(var_id, state); + } + return E_FAIL; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accHelp(VARIANT var_id, BSTR* help) { + if (!help) { + return E_INVALIDARG; + } + *help = {}; + return S_FALSE; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accValue(VARIANT var_id, + BSTR* value) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->get_accValue(var_id, value); + } + return E_FAIL; +} + +IFACEMETHODIMP AccessibilityRootNode::put_accValue(VARIANT var_id, + BSTR new_value) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->put_accValue(var_id, new_value); + } + return E_FAIL; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accSelection(VARIANT* selected) { + selected->vt = VT_EMPTY; + return S_OK; +} + +IFACEMETHODIMP AccessibilityRootNode::accSelect(LONG flagsSelect, + VARIANT var_id) { + IAccessible* target; + if ((target = GetTargetAndChildID(&var_id))) { + return target->accSelect(flagsSelect, var_id); + } + return E_FAIL; +} + +IFACEMETHODIMP AccessibilityRootNode::get_accHelpTopic(BSTR* help_file, + VARIANT var_id, + LONG* topic_id) { + if (help_file) { + *help_file = nullptr; + } + if (topic_id) { + *topic_id = -1; + } + return E_NOTIMPL; +} + +IFACEMETHODIMP AccessibilityRootNode::put_accName(VARIANT var_id, + BSTR put_name) { + return E_NOTIMPL; +} + +void AccessibilityRootNode::SetWindow(IAccessible* window) { + window_accessible_ = window; +} + +AccessibilityAlert* AccessibilityRootNode::GetOrCreateAlert() { + if (!alert_accessible_) { + CComObject* instance = nullptr; + HRESULT hr = CComObject::CreateInstance(&instance); + if (!SUCCEEDED(hr)) { + FML_LOG(FATAL) << "Failed to create alert accessible"; + } + instance->AddRef(); + instance->SetParent(this); + alert_accessible_ = instance; + } + return alert_accessible_; +} + +// static +AccessibilityRootNode* AccessibilityRootNode::Create() { + ui::win::CreateATLModuleIfNeeded(); + CComObject* instance = nullptr; + HRESULT hr = CComObject::CreateInstance(&instance); + if (!SUCCEEDED(hr) || !instance) { + FML_LOG(FATAL) << "Failed to create accessibility root node"; + } + instance->AddRef(); + return instance; +} + +} // namespace flutter diff --git a/shell/platform/windows/accessibility_root_node.h b/shell/platform/windows/accessibility_root_node.h new file mode 100644 index 0000000000000..efe72d64f4721 --- /dev/null +++ b/shell/platform/windows/accessibility_root_node.h @@ -0,0 +1,123 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_ROOT_NODE_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_ROOT_NODE_H_ + +#include +#include +#include + +#include + +#include "flutter/shell/platform/windows/accessibility_alert.h" + +namespace flutter { + +// A parent node that wraps the window IAccessible node. +class AccessibilityRootNode : public CComObjectRootEx, + public IDispatchImpl { + public: + static constexpr LONG kAlertChildId = 2; + BEGIN_COM_MAP(AccessibilityRootNode) + COM_INTERFACE_ENTRY(IAccessible) + END_COM_MAP() + + // + // IAccessible methods. + // + + // Retrieves the child element or child object at a given point on the screen. + IFACEMETHODIMP accHitTest(LONG screen_physical_pixel_x, + LONG screen_physical_pixel_y, + VARIANT* child) override; + + // Performs the object's default action. + IFACEMETHODIMP accDoDefaultAction(VARIANT var_id) override; + + // Retrieves the specified object's current screen location. + IFACEMETHODIMP accLocation(LONG* physical_pixel_left, + LONG* physical_pixel_top, + LONG* width, + LONG* height, + VARIANT var_id) override; + + // Traverses to another UI element and retrieves the object. + IFACEMETHODIMP accNavigate(LONG nav_dir, + VARIANT start, + VARIANT* end) override; + + // Retrieves an IDispatch interface pointer for the specified child. + IFACEMETHODIMP get_accChild(VARIANT var_child, + IDispatch** disp_child) override; + + // Retrieves the number of accessible children. + IFACEMETHODIMP get_accChildCount(LONG* child_count) override; + + // Retrieves a string that describes the object's default action. + IFACEMETHODIMP get_accDefaultAction(VARIANT var_id, + BSTR* default_action) override; + + // Retrieves the tooltip description. + IFACEMETHODIMP get_accDescription(VARIANT var_id, BSTR* desc) override; + + // Retrieves the object that has the keyboard focus. + IFACEMETHODIMP get_accFocus(VARIANT* focus_child) override; + + // Retrieves the specified object's shortcut. + IFACEMETHODIMP get_accKeyboardShortcut(VARIANT var_id, + BSTR* access_key) override; + + // Retrieves the name of the specified object. + IFACEMETHODIMP get_accName(VARIANT var_id, BSTR* name) override; + + // Retrieves the IDispatch interface of the object's parent. + IFACEMETHODIMP get_accParent(IDispatch** disp_parent) override; + + // Retrieves information describing the role of the specified object. + IFACEMETHODIMP get_accRole(VARIANT var_id, VARIANT* role) override; + + // Retrieves the current state of the specified object. + IFACEMETHODIMP get_accState(VARIANT var_id, VARIANT* state) override; + + // Gets the help string for the specified object. + IFACEMETHODIMP get_accHelp(VARIANT var_id, BSTR* help) override; + + // Retrieve or set the string value associated with the specified object. + // Setting the value is not typically used by screen readers, but it's + // used frequently by automation software. + IFACEMETHODIMP get_accValue(VARIANT var_id, BSTR* value) override; + IFACEMETHODIMP put_accValue(VARIANT var_id, BSTR new_value) override; + + // IAccessible methods not implemented. + IFACEMETHODIMP get_accSelection(VARIANT* selected) override; + IFACEMETHODIMP accSelect(LONG flags_sel, VARIANT var_id) override; + IFACEMETHODIMP get_accHelpTopic(BSTR* help_file, + VARIANT var_id, + LONG* topic_id) override; + IFACEMETHODIMP put_accName(VARIANT var_id, BSTR put_name) override; + + AccessibilityRootNode(); + virtual ~AccessibilityRootNode(); + + void SetWindow(IAccessible* window); + + void SetAlert(AccessibilityAlert* alert); + + AccessibilityAlert* GetOrCreateAlert(); + + static AccessibilityRootNode* Create(); + + private: + // Helper method to redirect method calls to the contained window or alert. + IAccessible* GetTargetAndChildID(VARIANT* var_id); + + IAccessible* window_accessible_; + + AccessibilityAlert* alert_accessible_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_ACCESSIBILITY_ROOT_NODE_H_ diff --git a/shell/platform/windows/fixtures/main.dart b/shell/platform/windows/fixtures/main.dart index 43de4bea59496..fe84072e6908d 100644 --- a/shell/platform/windows/fixtures/main.dart +++ b/shell/platform/windows/fixtures/main.dart @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:io' as io; -import 'dart:typed_data' show ByteData; +import 'dart:typed_data' show ByteData, Uint8List; import 'dart:ui' as ui; // Signals a waiting latch in the native test. @@ -36,6 +37,50 @@ void hiPlatformChannels() { }); } +@pragma('vm:entry-point') +void alertPlatformChannel() async { + // Serializers for data types are in the framework, so this will be hardcoded. + const int valueMap = 13, valueString = 7; + // Corresponds to: + // Map data = + // {"type": "announce", "data": {"message": ""}}; + final Uint8List data = Uint8List.fromList([ + valueMap, // _valueMap + 2, // Size + // key: "type" + valueString, + 'type'.length, + ...'type'.codeUnits, + // value: "announce" + valueString, + 'announce'.length, + ...'announce'.codeUnits, + // key: "data" + valueString, + 'data'.length, + ...'data'.codeUnits, + // value: map + valueMap, // _valueMap + 1, // Size + // key: "message" + valueString, + 'message'.length, + ...'message'.codeUnits, + // value: "" + valueString, + 0, // Length of empty string == 0. + ]); + final ByteData byteData = data.buffer.asByteData(); + + final Completer enabled = Completer(); + ui.PlatformDispatcher.instance.sendPlatformMessage('semantics', ByteData(0), (ByteData? reply){ + enabled.complete(reply); + }); + await enabled.future; + + ui.PlatformDispatcher.instance.sendPlatformMessage('flutter/accessibility', byteData, (ByteData? _){}); +} + @pragma('vm:entry-point') void customEntrypoint() {} diff --git a/shell/platform/windows/flutter_window.cc b/shell/platform/windows/flutter_window.cc index 968744209f214..c8d3e6a23e673 100644 --- a/shell/platform/windows/flutter_window.cc +++ b/shell/platform/windows/flutter_window.cc @@ -291,4 +291,11 @@ void FlutterWindow::SendInitialAccessibilityFeatures() { OnThemeChange(); } +AccessibilityRootNode* FlutterWindow::GetAccessibilityRootNode() { + if (!accessibility_root_) { + CreateAccessibilityRootNode(); + } + return accessibility_root_; +} + } // namespace flutter diff --git a/shell/platform/windows/flutter_window.h b/shell/platform/windows/flutter_window.h index 29c9c69554638..5af03bad9c213 100644 --- a/shell/platform/windows/flutter_window.h +++ b/shell/platform/windows/flutter_window.h @@ -148,6 +148,9 @@ class FlutterWindow : public Window, public WindowBindingHandler { // |WindowBindingHandler| void SendInitialAccessibilityFeatures() override; + // |WindowBindingHandler| + AccessibilityRootNode* GetAccessibilityRootNode() override; + private: // A pointer to a FlutterWindowsView that can be used to update engine // windowing and input state. diff --git a/shell/platform/windows/flutter_window_unittests.cc b/shell/platform/windows/flutter_window_unittests.cc index 9044c601c3685..2d149b6294367 100644 --- a/shell/platform/windows/flutter_window_unittests.cc +++ b/shell/platform/windows/flutter_window_unittests.cc @@ -136,6 +136,7 @@ class MockFlutterWindow : public FlutterWindow { MOCK_METHOD3(Win32DispatchMessage, UINT(UINT, WPARAM, LPARAM)); MOCK_METHOD4(Win32PeekMessage, BOOL(LPMSG, UINT, UINT, UINT)); MOCK_METHOD1(Win32MapVkToChar, uint32_t(uint32_t)); + MOCK_METHOD0(GetPlatformWindow, HWND()); protected: // |KeyboardManager::WindowDelegate| @@ -153,10 +154,13 @@ class TestFlutterWindowsView : public FlutterWindowsView { public: TestFlutterWindowsView(std::unique_ptr window_binding) : FlutterWindowsView(std::move(window_binding)) {} + ~TestFlutterWindowsView() {} SpyKeyboardKeyHandler* key_event_handler; SpyTextInputPlugin* text_input_plugin; + MOCK_METHOD4(NotifyWinEventWrapper, void(DWORD, HWND, LONG, LONG)); + protected: std::unique_ptr CreateKeyboardKeyHandler( flutter::BinaryMessenger* messenger, @@ -400,5 +404,37 @@ TEST(FlutterWindowTest, InitialAccessibilityFeatures) { win32window.SendInitialAccessibilityFeatures(); } +// Ensure that announcing the alert propagates the message to the alert node. +// Different screen readers use different properties for alerts. +TEST(FlutterWindowTest, AlertNode) { + std::unique_ptr win32window = + std::make_unique(); + ON_CALL(*win32window, GetPlatformWindow()).WillByDefault(Return(nullptr)); + AccessibilityRootNode* root_node = win32window->GetAccessibilityRootNode(); + TestFlutterWindowsView view(std::move(win32window)); + EXPECT_CALL(view, + NotifyWinEventWrapper(EVENT_SYSTEM_ALERT, nullptr, OBJID_CLIENT, + AccessibilityRootNode::kAlertChildId)) + .Times(1); + std::wstring message = L"Test alert"; + view.AnnounceAlert(message); + IAccessible* alert = root_node->GetOrCreateAlert(); + VARIANT self{.vt = VT_I4, .lVal = CHILDID_SELF}; + BSTR strptr; + alert->get_accName(self, &strptr); + EXPECT_EQ(message, strptr); + + alert->get_accDescription(self, &strptr); + EXPECT_EQ(message, strptr); + + alert->get_accValue(self, &strptr); + EXPECT_EQ(message, strptr); + + VARIANT role; + alert->get_accRole(self, &role); + EXPECT_EQ(role.vt, VT_I4); + EXPECT_EQ(role.lVal, ROLE_SYSTEM_ALERT); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index b75764cf68d45..887a50145af5a 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -14,15 +14,19 @@ #include "flutter/fml/paths.h" #include "flutter/fml/platform/win/wstring_conversion.h" #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/flutter_windows_view.h" #include "flutter/shell/platform/windows/system_utils.h" #include "flutter/shell/platform/windows/task_runner.h" +#include "flutter/third_party/accessibility/ax/ax_node.h" // winbase.h defines GetCurrentTime as a macro. #undef GetCurrentTime +static constexpr char kAccessibilityChannelName[] = "flutter/accessibility"; + namespace flutter { namespace { @@ -183,6 +187,14 @@ FlutterWindowsEngine::FlutterWindowsEngine( messenger_wrapper_ = std::make_unique(messenger_.get()); message_dispatcher_ = std::make_unique(messenger_.get()); + message_dispatcher_->SetMessageCallback( + kAccessibilityChannelName, + [](FlutterDesktopMessengerRef messenger, + const FlutterDesktopMessage* message, void* data) { + FlutterWindowsEngine* engine = static_cast(data); + engine->HandleAccessibilityMessage(messenger, message); + }, + static_cast(this)); FlutterWindowsTextureRegistrar::ResolveGlFunctions(gl_procs_); texture_registrar_ = @@ -660,4 +672,25 @@ int FlutterWindowsEngine::EnabledAccessibilityFeatures() const { return flags; } +void FlutterWindowsEngine::HandleAccessibilityMessage( + FlutterDesktopMessengerRef messenger, + const FlutterDesktopMessage* message) { + const auto& codec = StandardMessageCodec::GetInstance(); + auto data = codec.DecodeMessage(message->message, message->message_size); + EncodableMap map = std::get(*data); + std::string type = std::get(map.at(EncodableValue("type"))); + if (type.compare("announce") == 0) { + if (semantics_enabled_) { + EncodableMap data_map = + std::get(map.at(EncodableValue("data"))); + std::string text = + std::get(data_map.at(EncodableValue("message"))); + std::wstring wide_text = fml::Utf8ToWideString(text); + view_->AnnounceAlert(wide_text); + } + } + SendPlatformMessageResponse(message->response_handle, + reinterpret_cast(""), 0); +} + } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index a5dfe145e3021..3cdee97e3cb92 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -254,6 +254,9 @@ class FlutterWindowsEngine { // system changes. void SendSystemLocales(); + void HandleAccessibilityMessage(FlutterDesktopMessengerRef messenger, + const FlutterDesktopMessage* message); + // The handle to the embedder.h engine instance. FLUTTER_API_SYMBOL(FlutterEngine) engine_ = nullptr; diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc index 4b44a0919f9ad..a89322f0c51b9 100644 --- a/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -6,10 +6,13 @@ #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" +#include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/shell/platform/windows/testing/engine_modifier.h" +#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" #include "flutter/shell/platform/windows/testing/test_keyboard.h" #include "flutter/shell/platform/windows/testing/windows_test.h" #include "fml/synchronization/waitable_event.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" // winbase.h defines GetCurrentTime as a macro. @@ -516,5 +519,59 @@ TEST_F(FlutterWindowsEngineTest, PostRasterThreadTask) { EXPECT_TRUE(called); } +class MockFlutterWindowsView : public FlutterWindowsView { + public: + MockFlutterWindowsView(std::unique_ptr wbh) + : FlutterWindowsView(std::move(wbh)) {} + ~MockFlutterWindowsView() {} + + MOCK_METHOD4(NotifyWinEventWrapper, void(DWORD, HWND, LONG, LONG)); +}; + +TEST_F(FlutterWindowsEngineTest, AlertPlatformMessage) { + FlutterDesktopEngineProperties properties = {}; + properties.assets_path = GetContext().GetAssetsPath().c_str(); + properties.icu_data_path = GetContext().GetIcuDataPath().c_str(); + properties.dart_entrypoint = "alertPlatformChannel"; + + FlutterProjectBundle project(properties); + + auto window_binding_handler = + std::make_unique<::testing::NiceMock>(); + AccessibilityRootNode* root_node = AccessibilityRootNode::Create(); + ON_CALL(*window_binding_handler, GetAccessibilityRootNode) + .WillByDefault(::testing::Return(root_node)); + MockFlutterWindowsView view(std::move(window_binding_handler)); + view.SetEngine(std::make_unique(project)); + FlutterWindowsEngine* engine = view.GetEngine(); + + EngineModifier modifier(engine); + modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; + + auto binary_messenger = + std::make_unique(engine->messenger()); + binary_messenger->SetMessageHandler( + "semantics", [&engine](const uint8_t* message, size_t message_size, + BinaryReply reply) { + engine->UpdateSemanticsEnabled(true); + char response[] = ""; + reply(reinterpret_cast(response), 0); + }); + + bool did_call = false; + ON_CALL(view, NotifyWinEventWrapper) + .WillByDefault([&did_call](DWORD event, HWND hwnd, LONG obj, LONG child) { + did_call = true; + }); + + engine->UpdateSemanticsEnabled(true); + engine->Run(); + + // Rely on timeout mechanism in CI. + while (!did_call) { + engine->task_runner()->ProcessTasks(); + } +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index e212b6cf6140a..0f04f57bf7ee6 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -656,4 +656,25 @@ FlutterWindowsEngine* FlutterWindowsView::GetEngine() { return engine_.get(); } +void FlutterWindowsView::AnnounceAlert(const std::wstring& text) { + AccessibilityRootNode* root_node = + binding_handler_->GetAccessibilityRootNode(); + AccessibilityAlert* alert = + binding_handler_->GetAccessibilityRootNode()->GetOrCreateAlert(); + alert->SetText(text); + HWND hwnd = GetPlatformWindow(); + NotifyWinEventWrapper(EVENT_SYSTEM_ALERT, hwnd, OBJID_CLIENT, + AccessibilityRootNode::kAlertChildId); +} + +void FlutterWindowsView::NotifyWinEventWrapper(DWORD event, + HWND hwnd, + LONG idObject, + LONG idChild) { + if (hwnd) { + NotifyWinEvent(EVENT_SYSTEM_ALERT, hwnd, OBJID_CLIENT, + AccessibilityRootNode::kAlertChildId); + } +} + } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index 6075903309479..f2b38ed22e724 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -90,6 +90,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, // Send the initial accessibility features to the window void SendInitialAccessibilityFeatures(); + // Set the text of the alert, and create it if it does not yet exist. + void AnnounceAlert(const std::wstring& text); + // |WindowBindingHandlerDelegate| void UpdateHighContrastEnabled(bool enabled) override; @@ -211,6 +214,11 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, virtual std::unique_ptr CreateTextInputPlugin( BinaryMessenger* messenger); + virtual void NotifyWinEventWrapper(DWORD event, + HWND hwnd, + LONG idObject, + LONG idChild); + private: // Struct holding the state of an individual pointer. The engine doesn't keep // track of which buttons have been pressed, so it's the embedding's diff --git a/shell/platform/windows/testing/mock_window_binding_handler.h b/shell/platform/windows/testing/mock_window_binding_handler.h index dc8ea7433fa17..6a191b0e1817b 100644 --- a/shell/platform/windows/testing/mock_window_binding_handler.h +++ b/shell/platform/windows/testing/mock_window_binding_handler.h @@ -37,6 +37,7 @@ class MockWindowBindingHandler : public WindowBindingHandler { bool(const void* allocation, size_t row_bytes, size_t height)); MOCK_METHOD0(GetPrimaryPointerLocation, PointerLocation()); MOCK_METHOD0(SendInitialAccessibilityFeatures, void()); + MOCK_METHOD0(GetAccessibilityRootNode, AccessibilityRootNode*()); }; } // namespace testing diff --git a/shell/platform/windows/window.cc b/shell/platform/windows/window.cc index 913a3f85fef07..9581d05ab9d51 100644 --- a/shell/platform/windows/window.cc +++ b/shell/platform/windows/window.cc @@ -58,7 +58,8 @@ Window::Window(std::unique_ptr windows_proc_table, std::unique_ptr text_input_manager) : touch_id_generator_(kMinTouchDeviceId, kMaxTouchDeviceId), windows_proc_table_(std::move(windows_proc_table)), - text_input_manager_(std::move(text_input_manager)) { + text_input_manager_(std::move(text_input_manager)), + accessibility_root_(nullptr) { // Get the DPI of the primary monitor as the initial DPI. If Per-Monitor V2 is // supported, |current_dpi_| should be updated in the // kWmDpiChangedBeforeParent message. @@ -210,8 +211,14 @@ LRESULT Window::OnGetObject(UINT const message, // TODO(cbracken): https://github.com/flutter/flutter/issues/94782 // Implement when we adopt UIA support. } else if (is_msaa_request && root_view) { + // Create the accessibility root if it does not already exist. + if (!accessibility_root_) { + CreateAccessibilityRootNode(); + } // Return the IAccessible for the root view. - Microsoft::WRL::ComPtr root(root_view); + // Microsoft::WRL::ComPtr root(root_view); + accessibility_root_->SetWindow(root_view); + Microsoft::WRL::ComPtr root(accessibility_root_); LRESULT lresult = LresultFromObject(IID_IAccessible, wparam, root.Get()); return lresult; } @@ -596,6 +603,11 @@ void Window::Destroy() { window_handle_ = nullptr; } + if (accessibility_root_) { + accessibility_root_->Release(); + accessibility_root_ = nullptr; + } + UnregisterClass(window_class_name_.c_str(), nullptr); } @@ -648,4 +660,11 @@ bool Window::GetHighContrastEnabled() { } } +void Window::CreateAccessibilityRootNode() { + if (accessibility_root_) { + accessibility_root_->Release(); + } + accessibility_root_ = AccessibilityRootNode::Create(); +} + } // namespace flutter diff --git a/shell/platform/windows/window.h b/shell/platform/windows/window.h index e31be30f1619d..95784d0eff986 100644 --- a/shell/platform/windows/window.h +++ b/shell/platform/windows/window.h @@ -14,6 +14,7 @@ #include #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/windows/accessibility_root_node.h" #include "flutter/shell/platform/windows/direct_manipulation.h" #include "flutter/shell/platform/windows/keyboard_manager.h" #include "flutter/shell/platform/windows/sequential_id_generator.h" @@ -223,6 +224,9 @@ class Window : public KeyboardManager::WindowDelegate { // Returns the root view accessibility node, or nullptr if none. virtual gfx::NativeViewAccessible GetNativeViewAccessible() = 0; + // Create the wrapper node. + void CreateAccessibilityRootNode(); + // Handles running DirectManipulation on the window to receive trackpad // gestures. std::unique_ptr direct_manipulation_owner_; @@ -230,6 +234,9 @@ class Window : public KeyboardManager::WindowDelegate { // Called when a theme change message is issued virtual void OnThemeChange() = 0; + // A parent node wrapping the window root, used for siblings. + AccessibilityRootNode* accessibility_root_; + private: // Release OS resources associated with window. void Destroy(); diff --git a/shell/platform/windows/window_binding_handler.h b/shell/platform/windows/window_binding_handler.h index abde74e59822f..13d04eb604b1f 100644 --- a/shell/platform/windows/window_binding_handler.h +++ b/shell/platform/windows/window_binding_handler.h @@ -11,6 +11,7 @@ #include #include "flutter/shell/platform/common/geometry.h" +#include "flutter/shell/platform/windows/accessibility_root_node.h" #include "flutter/shell/platform/windows/public/flutter_windows.h" #include "flutter/shell/platform/windows/window_binding_handler_delegate.h" @@ -92,6 +93,9 @@ class WindowBindingHandler { // Called to set the initial state of accessibility features virtual void SendInitialAccessibilityFeatures() = 0; + + // Returns the wrapper parent accessibility node. + virtual AccessibilityRootNode* GetAccessibilityRootNode() = 0; }; } // namespace flutter