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
Conversation
HWND hwnd, | ||
LONG idObject, | ||
LONG idChild); | ||
virtual void NotifyWinEventWrapper(ui::AXPlatformNodeWin* node, ax::mojom::Event event); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
void Alert(const std::wstring& text) override; | ||
|
||
// |WindowBindingHandler| | ||
ui::AXPlatformNodeWin* GetAlert() override; |
There was a problem hiding this comment.
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.
|
||
class AlertPlatformNodeDelegate : public ui::AXPlatformNodeDelegateBase { | ||
public: | ||
AlertPlatformNodeDelegate(ui::AXPlatformNodeDelegate* parent_delegate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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" |
There was a problem hiding this comment.
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
shell/platform/windows/window.cc
Outdated
} | ||
accessibility_root_ = AccessibilityRootNode::Create(); | ||
alert_delegate_ = | ||
std::make_unique<AlertPlatformNodeDelegate>(ax_fragment_root_.get()); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- The Windows embedder receives an alert message from the framework
- The user opens a screen reader & the Windows embedder receives the first
WM_GETOBJECT
message - 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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
shell/platform/windows/window.cc
Outdated
FlutterPlatformNodeDelegate* child_delegate = | ||
static_cast<FlutterPlatformNodeDelegate*>( | ||
ax_fragment_root_->GetChildNodeDelegate()); | ||
child_delegate->GetAXNode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do?
shell/platform/windows/window.cc
Outdated
} 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
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 { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delete before checking in.
shell/platform/windows/window.h
Outdated
@@ -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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Some unused code remains to be removed. Merge MSAA to AXFragmentRootWin
b57d097
to
42d7721
Compare
@yaakovschectman Please tag me when this is ready for another review 😄 |
} | ||
} | ||
|
||
ui::AXFragmentRootDelegateWin* FlutterWindowsView::GetAxFragmentRootDelegate() { | ||
auto bridge = engine_->accessibility_bridge().lock().get(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool IsDescendantOf(AXPlatformNode* acnestor) const override; | |
bool IsDescendantOf(AXPlatformNode* ancestor) const override; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, nice work!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AccessibilityRootNode::kAlertChildId); | ||
binding_handler_->Alert(text); | ||
ui::AXPlatformNodeWin* alert_node = binding_handler_->GetAlert(); | ||
NotifyWinEventWrapper(alert_node, ax::mojom::Event::kAlert); |
There was a problem hiding this comment.
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.
…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)
* 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
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, makingAccessibilityRootNode
andAccessibilityAlert
redundant.Part of flutter/flutter#116220
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.