Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge MSAA alert functionality with UIA #38745

Merged
merged 23 commits into from Jan 19, 2023

Conversation

yaakovschectman
Copy link
Contributor

Merges the existing MSAA alert implementation with UIA making use of the existing AXFragmentRoot class rather than using separate implementations for each. Also use the above class for holding alert nodes, making AccessibilityRootNode and AccessibilityAlert redundant.

Part of flutter/flutter#116220

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

HWND hwnd,
LONG idObject,
LONG idChild);
virtual void NotifyWinEventWrapper(ui::AXPlatformNodeWin* node, ax::mojom::Event event);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AXPlatformNodeWin already has a helper method for dispatching accessibility events, so rather than dispatching them directly from the view, we use this helper method. This also means UIA events are handled alongside MSAA events.

@@ -204,35 +204,35 @@ LRESULT Window::OnGetObject(UINT const message,
gfx::NativeViewAccessible root_view = GetNativeViewAccessible();
// TODO(schectman): UIA is currently disabled by default.
// https://github.com/flutter/flutter/issues/114547
if (is_uia_request && root_view) {
#ifdef FLUTTER_ENGINE_USE_UIA
if (root_view) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AXFragmentRootWin is now used by both UIA and MSAA, so check if we need to create it in either case.

@@ -5408,7 +5418,7 @@ AXPlatformNodeWin* AXPlatformNodeWin::GetTargetFromChildID(

AXPlatformNodeBase* base =
FromNativeViewAccessible(node->GetNativeViewAccessible());
if (base && !IsDescendant(base))
if (base && !base->IsDescendantOf(this))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows using the descendant's override of IsDescendantOf to handle whichever delegate type it has.

Comment on lines 154 to 157
void Alert(const std::wstring& text) override;

// |WindowBindingHandler|
ui::AXPlatformNodeWin* GetAlert() override;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two separate methods make unit testing simpler.

@yaakovschectman yaakovschectman marked this pull request as ready for review January 10, 2023 19:26

class AlertPlatformNodeDelegate : public ui::AXPlatformNodeDelegateBase {
public:
AlertPlatformNodeDelegate(ui::AXPlatformNodeDelegate* parent_delegate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
AlertPlatformNodeDelegate(ui::AXPlatformNodeDelegate* parent_delegate);
explicit AlertPlatformNodeDelegate(ui::AXPlatformNodeDelegate* parent_delegate);

See: https://google.github.io/styleguide/cppguide.html#Implicit_Conversions


class AlertPlatformNodeDelegate : public ui::AXPlatformNodeDelegateBase {
public:
AlertPlatformNodeDelegate(ui::AXPlatformNodeDelegate* parent_delegate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this disallow copy and assign?

view.AnnounceAlert(message);
IAccessible* alert = root_node->GetOrCreateAlert();

IAccessible* alert = view.AlertNode(); // root_node->GetOrCreateAlert();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
IAccessible* alert = view.AlertNode(); // root_node->GetOrCreateAlert();
IAccessible* alert = view.AlertNode();

@@ -11,6 +11,7 @@
#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"
#include "flutter/third_party/accessibility/ax/platform/ax_platform_node_win.h"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? Feel free to close if yes

}
accessibility_root_ = AccessibilityRootNode::Create();
alert_delegate_ =
std::make_unique<AlertPlatformNodeDelegate>(ax_fragment_root_.get());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the framework requests an alert before the app receives its first WM_GETOBJECT message? From my understanding, ax_fragment_root_ will be nullptr, and the alert node's parent will never be updated. Is that correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case nothing would happen, but if there has been no getobject message, I believe that would mean there is no screen reader open, so an alert would do nothing, is that correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, here's the scenario I'm concerned about:

  1. The Windows embedder receives an alert message from the framework
  2. The user opens a screen reader & the Windows embedder receives the first WM_GETOBJECT message
  3. The Windows embedder receives a second alert message from the framework

In this scenario, I expect that the screen reader is notified of an alert for step 3. However, I suspect it won't. From my understanding, step 1 creates an alert delegate that will always have a nullptr parent delegate. Step 2 creates the ax_fragment_root_, but, the alert delegate is never notified of this parent. Finally for step 3, both AlertPlatformNodeDelegate::GetTargetForNativeAccessibilityEvent and AlertPlatformNodeDelegate::GetParent will return nullptr when called by AXPlatformNodeWin::NotifyAccessibilityEvent. Please let me know if I'm missing something!

AccessibilityRootNode::kAlertChildId);
binding_handler_->Alert(text);
ui::AXPlatformNodeWin* alert_node = binding_handler_->GetAlert();
NotifyWinEventWrapper(alert_node, ax::mojom::Event::kAlert);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why doesn't WindowBindingHandler::Alert call AXPlatformNodeWin::NotifyAccessibilityEvent?

EDIT: See message below instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For mocking in the unit tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a step back, the value of the FlutterWindowsView / FlutterWindow split is to enable mock testing by decoupling Flutter from native win32 APIs. We can't mock win32 APIs, so instead we wrap them in FlutterWindow and mock that.

Ideally FlutterWindow is as small as possible. Any code that's unit testable should be moved out of FlutterWindow and into FlutterWindowsView.

Currently, the alerting logic is split across FlutterWindow and FlutterWindowsView. Do you think we could refactor FlutterWindow to push up more alerting logic to FlutterWindowsView?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing for this patch, but to add to this discussion, I sort of wonder if, from a testing point of view, longer term we should consider wrapping Win32 APIs like we do with, for example the embedder API. It feels like overkill, but that code would be really low-maintenance, and when we've done that with other embedders (e.g. the one we wrote for the google home hub), it's generally worked out well, and makes win32 mockable where desired.

Comment on lines 211 to 214
FlutterPlatformNodeDelegate* child_delegate =
static_cast<FlutterPlatformNodeDelegate*>(
ax_fragment_root_->GetChildNodeDelegate());
child_delegate->GetAXNode();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

} else if (is_msaa_request) {
// Create the accessibility root if it does not already exist.
// Return the IAccessible for the root view.
// Microsoft::WRL::ComPtr<IAccessible> root(root_view);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Microsoft::WRL::ComPtr<IAccessible> root(root_view);

@@ -289,7 +289,7 @@ class AXFragmentRootMapWin {

AXFragmentRootWin::AXFragmentRootWin(gfx::AcceleratedWidget widget,
AXFragmentRootDelegateWin* delegate)
: widget_(widget), delegate_(delegate) {
: widget_(widget), delegate_(delegate), alert_node_(nullptr) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of creating the alert node lazily? Could we create an alert node every time we create the root?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alert node, delegate, and fragment root refer to each other, so one pointer or more will be set after creation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right... but the creation of the fragment root node and alert node is decoupled: a fragment root is created only when I attach a screen reader, and, an alert node is created only when the framework sends an alert. Why is that? Why doesn't creating a fragment root also create the alert node? Do we need to create an alert node if there's no screen reader?

If we do need to create an alert node even without a screen reader, perhaps that should also create a fragment root? That would address concerns like: #38745 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AXFragmentRootWin must be created with a non-null delegate, which does not exist when we are just testing FlutterWindowsView without an engine.

I'll see if I can mock out that method in the mock window.

@yaakovschectman
Copy link
Contributor Author

With the logic of dispatching the alert message moved to the windows view, I will need to change the platform message unit test a bit more.


namespace flutter {

class AlertPlatformNodeDelegate : public ui::AXPlatformNodeDelegateBase {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a doc comment. (True, we lack a lot in existing code but good to remedy in new code)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do a pass, there's several other fields that are also missing comments

AlertPlatformNodeDelegate operator=(const AlertPlatformNodeDelegate& other) =
delete;

void SetText(const std::u16string& text);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider void SetText(std::u16string_view text);. It's super lightweight, sometimes better performance, and allows for passing a char16_t* string as well (unlikely as that is).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AXNodeData methods that this method calls take string& parameters. Am I mistaken that constructing a string from a string_view effectively copies it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also leaning towards keeping this a string reference, but, the string reference is also copied when AXNodeData stores it as an attribute.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AXNodeData methods that this method calls take string& parameters. Am I mistaken that constructing a string from a string_view effectively copies it?

Yes - to benefit from this, you'd need any methods this gets passed to to also take a view rather than a string ref. If the current implementation is doing so, then leave as-is; at some point we can do a sweep of the codebase to clean these up across the board.

IAccessible* alert = root_node->GetOrCreateAlert();

IAccessible* alert = view.AlertNode();
FML_LOG(ERROR) << "Alert = " << (void*)alert;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete before checking in.

@@ -214,6 +215,8 @@ class Window : public KeyboardManager::WindowDelegate {
// Check if the high contrast feature is enabled on the OS
virtual bool GetHighContrastEnabled();

void CreateAlertNode();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a doc comment. Ideally the questions it would address are what the lifetime of alert_node_ looks like:

  • Once created is it ever reset to nullptr? (doesn't look like it)
  • What happens if called when there already is a valid alert_node_ (it's a no-op so long as there's a valid parent or there's no fragment root)

@@ -425,4 +427,18 @@ int AXFragmentRootWin::GetIndexInParentOfChild() const {
return 0;
}

void AXFragmentRootWin::SetAlertNode(AXPlatformNodeWin* alert_node) {
alert_node_ = alert_node;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any assertions we should be applying? For example, is setting to nullptr valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we have a use case for this, but setting to nullptr would remove the alert node from the root. The only time it is accessed by AXFragmentRootWin is in ChildAtIndex, where it is checked for nullity.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it would currently be a developer error to set it null, consider adding an assert(alert_node != nullptr); so we catch it in dev mode. If doing so is harmless, but unexpected, maybe just covering that in the doc comment is sufficient.

@loic-sharma
Copy link
Member

@yaakovschectman Please tag me when this is ready for another review 😄

}
}

ui::AXFragmentRootDelegateWin* FlutterWindowsView::GetAxFragmentRootDelegate() {
auto bridge = engine_->accessibility_bridge().lock().get();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be removed?

@@ -473,6 +473,9 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2"))
static std::optional<PROPERTYID> MojoEventToUIAProperty(
ax::mojom::Event event);

// |AXPlatformNodeBase|
bool IsDescendantOf(AXPlatformNode* acnestor) const override;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool IsDescendantOf(AXPlatformNode* acnestor) const override;
bool IsDescendantOf(AXPlatformNode* ancestor) const override;

Copy link
Member

@loic-sharma loic-sharma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, nice work!

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

LGTM stamp from a Japanese personal seal

AccessibilityRootNode::kAlertChildId);
binding_handler_->Alert(text);
ui::AXPlatformNodeWin* alert_node = binding_handler_->GetAlert();
NotifyWinEventWrapper(alert_node, ax::mojom::Event::kAlert);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing for this patch, but to add to this discussion, I sort of wonder if, from a testing point of view, longer term we should consider wrapping Win32 APIs like we do with, for example the embedder API. It feels like overkill, but that code would be really low-maintenance, and when we've done that with other embedders (e.g. the one we wrote for the google home hub), it's generally worked out well, and makes win32 mockable where desired.

@yaakovschectman yaakovschectman merged commit a0e3c14 into flutter:main Jan 19, 2023
@yaakovschectman yaakovschectman deleted the uia_alert branch January 19, 2023 18:28
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jan 19, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jan 19, 2023
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Jan 19, 2023
…118829)

* a0e3c14d4 Merge MSAA alert functionality with UIA (flutter/engine#38745)

* 78bbea005 [web] dont look up webgl params if no GPU is available (flutter/engine#38948)
ricardoamador pushed a commit to ricardoamador/engine that referenced this pull request Jan 25, 2023
* Use AXFragmentRootWin for MSAA functionality.

Some unused code remains to be removed.

Merge MSAA to AXFragmentRootWin

* Removing unused code

* Remove unused files

* Flip macro

* Formatting

* Licenses

* Make reference

* Disable copy constructor/assignment

* Unused import

* Formatting

* Relocate alert logic

* Remove comment and unused mock

* Fix unit test

* Idempotency

* Formatting

* PR feedback

* Doc comments

* Undo string change for now

* Couple fragment root and alert node

* Formatting

* Add comments

* Pointer to reference

* Typo fix
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
3 participants