-
Notifications
You must be signed in to change notification settings - Fork 8.5k
Use UIA notifications for text output #12358
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
Conversation
This definitely opens some exciting possibilities, but will need to be handled with care on the client side! How can/should UIA clients know that notifications are supported and there's no need to use the old strategy? |
Could you do something like "if we received a notification event with the ID 'TerminalTextOutput', this is the newest iteration"? |
Maybe? @josephsl, @LeonarddeR, @michaelDCurran, @jcsteh is it possible to change the overlay class associated with an NVDA object in response to receiving a particular event? (I'm thinking not, but being proven wrong would ease this). Other idea: maybe all terminals could inherit from I think it'd probably be a lot simpler if they set the |
No. An overlay has to be determined at the time the object is created. That
could be due to a notification event, but it could just as well be due to
some other event; e.g. focus.
That said, you could set a flag if you see such an event. That's not as
clean, but life isn't clean. :)
|
I see that the terminal edit control currently doesn't have an automation ID. I guess one could be added at this point to distinguish the old from the new implementation. |
dispatcher.RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, [weakThis{ get_weak() }, notificationText{ hstring(newOutput) }]() { | ||
if (auto strongThis{ weakThis.get() }) | ||
{ | ||
strongThis->RaiseNotificationEvent(AutomationNotificationKind::ActionCompleted, | ||
AutomationNotificationProcessing::ImportantAll, | ||
notificationText, | ||
L"TerminalTextOutput"); | ||
} | ||
}); |
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.
Don't you need to react to back-pressure here? What if you're called really fast on a slow system? Won't the background jobs just pile up more and more?
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.
We need the jobs to pile up. The screen reader wants to receive the entirety of the output.
Converting this thread to track synchronous notification dispatching. There's a bit of a concern here that the notification events being dispatched in an async way may be processed out of order.
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.
We need the jobs to pile up.
This could be a misunderstanding, but I think "piling them up" is not a good idea and needs to be prevented in this PR for the reasons I mentioned before.
Windows Terminal can write the famous big.txt
within half a second, but it has 128457 lines of text. Do we really want 128457 asynchronous jobs to be put into the job queue or some other queue? This is why I mentioned earlier we need to control for back pressure. Personally I can imagine two solutions:
- A semaphore to let it run ahead by let's say one viewport height
- Only write the next line to the screen when the previous one finished being narrated (a semaphore of size 1)
I can imagine that a user of a narrator might want to have it finish the execution as fast as possible, but I'm worried that creating 128457 async tasks isn't a viable solution either.
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.
Would it be acceptable to make it read out something like "128,000 lines skipped"?
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.
How would an out-of-band announcement like that distinguish itself from output 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.
Good question... I'm just throwing ideas at the wall and seeing if anything sticks. Reading 128,000 lines seems intense.
// - <none> | ||
// Return Value: | ||
// - S_FALSE since we do nothing. | ||
[[nodiscard]] HRESULT UiaEngine::Present() noexcept |
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 diff is weird here. I basically just moved all of the contents of EndPaint()
over to Present()
. This way we're not doing this under lock. Leonard said that's the main difference between the two functions.
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.
It's also necessary in this case, because RaiseNotificationEvent()
calls back into us (UiaTextRangeBase::GetAttributeValue
specifically) which locks the console. This is why we have to do this outside of the console lock inside Present()
.
@codeofdusk Our concern with the automation ID approach is that a user could have multiple terminals open within the same instance (via panes or tabs), so each pane's How does this sound:
That way you also get two different versioning systems in one go. |
Large output is acting a little weird in this latest build. Printing 5 lines of text works fine with...
But printing 10 lines of text with...
is inconsistent. Occasionally a random number is missing. More often, the new prompt also isn't read. I'm still debugging through this issue, so if anybody has any ideas, let me know. Might have to do with |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
Hi, which notification processing instruction is the event using? I can see that Narrator (and other screen readers) can cut off speech if it receives UIA notification with an “important” processing instruction. Thanks.
|
@josephsl |
This comment was marked as resolved.
This comment was marked as resolved.
Hi, in this case, Narrator ought to announce everything serially (I know that NVDA cuts off current notification if notification processing instruction is “important” or “most recent”; I added notification event for NVDA in 2018 and folks added notification processing instruction handling after that).
|
This comment was marked as resolved.
This comment was marked as resolved.
…notification-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.
If the rest of the team is comfortable with this, let's ship it. It scares me, but it's better than what we have today...
Just confirming: none of this happens if there's no screen reader enabled, right?
// Notify UIA of new text. | ||
// It's important to do this here instead of in TextBuffer, because here you have access to the entire line of text, | ||
// whereas TextBuffer writes it one character at a time via the OutputCellIterator. | ||
_buffer->GetRenderTarget().TriggerNewTextNotification(stringView); |
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.
It feels weird to reach all the way down through the renderer to tell the TermControlAutomationPeer that there was text.
sequenceDiagram
TerminalControl ->> TerminalCore: Text
TerminalCore ->> Renderer: Text
Renderer ->> DX Engine: Text
Renderer ->> UIA Engine: Text
UIA Engine ->> TCAP: Text
TCAP ->> Narrator: 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.
To be fair, the UIA Engine already operated this way. It just didn't know what text was written (only if text was written).
@@ -141,6 +141,11 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept | |||
return S_OK; | |||
} | |||
|
|||
[[nodiscard]] HRESULT AtlasEngine::NotifyNewText(const std::wstring_view newText) noexcept | |||
{ | |||
return S_OK; |
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.
you added a base implementation that returns S_FALSE
; does atlas need an S_OK
version?
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.
AtlasEngine doesn't inherit from RenderEngineBase
.
(I did this so I can control the implementation as much as possible.)
@@ -110,3 +110,13 @@ void ScreenBufferRenderTarget::TriggerTitleChange() | |||
pRenderer->TriggerTitleChange(); | |||
} | |||
} | |||
|
|||
void ScreenBufferRenderTarget::TriggerNewTextNotification(const std::wstring_view newText) |
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.
Does conhost need 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.
Not yet! Conhost doesn't even use a UIA engine, so this code never gets called. Bill has expressed interest that Conhost should switch over to this notification system because it would make screen readers' lives so much easier (it removes the need to diff). But it makes sense to test this out on Windows Terminal first, then update Conhost later.
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.
Note that @j4james is removing the IRenderTarget
interface... which may complicate this for you.
This change makes Windows Terminal raise a `RaiseNotificationEvent()` ([docs](https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.automation.peers.automationpeer.raisenotificationevent?view=winrt-22000)) for new text output to the buffer. This is intended to help Narrator identify what new output appears and reduce the workload of diffing the buffer when a `TextChanged` event occurs. The flow of the event occurs as follows: - `Terminal::_WriteBuffer()` - New text is output to the text buffer. Notify the renderer that we have new text (and what that text is). - `Renderer::TriggerNewTextNotification()` - Cycle through all the rendering engines and tell them to notify handle the new text output. - None of the rendering engines _except_ `UiaEngine` has it implemented, so really we're just notifying UIA. - `UiaEngine::NotifyNewText()` - Concatenate any new output into a string. - When we're done painting, tell the notification system to actually notify of new events occurring and clear any stored output text. That way, we're ready for the next renderer frame. - `InteractivityAutomationPeer::NotifyNewOutput()` --> `TermControlAutomationPeer::NotifyNewOutput` - NOTE: these are split because of the in-proc and out-of-proc separation of the buffer. - Actually `RaiseNotificationEvent()` for the new text output. Additionally, we had to handle the "local echo" problem: when a key is pressed, the character is said twice (once for the keyboard event, and again for the character being written to the buffer). To accomplish this, we did the following: - `TermControl`: - here, we already handle keyboard events, so I added a line saying "if we have an automation peer attached, record the keyboard event in the automation peer". - `TermControlAutomationPeer`: - just before the notification is dispatched, check if the string of recent keyboard events match the beginning of the string of new output. If that's the case, we can assume that the common prefix was the "local echo". This is a fairly naive heuristic, but it's been working. Closes the following ADO bugs: - https://dev.azure.com/microsoft/OS/_workitems/edit/36506838 - (Probably) https://dev.azure.com/microsoft/OS/_workitems/edit/38011453 - [x] Base case: "echo hello" - [x] Partial line change - [x] Scrolling (should be unaffected) - [x] Large output - [x] "local echo": keyboard events read input character twice (cherry picked from commit f9be172)
## Summary of the Pull Request This change makes Windows Terminal raise a `RaiseNotificationEvent()` ([docs](https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.automation.peers.automationpeer.raisenotificationevent?view=winrt-22000)) for new text output to the buffer. This is intended to help Narrator identify what new output appears and reduce the workload of diffing the buffer when a `TextChanged` event occurs. ## Detailed Description of the Pull Request / Additional comments The flow of the event occurs as follows: - `Terminal::_WriteBuffer()` - New text is output to the text buffer. Notify the renderer that we have new text (and what that text is). - `Renderer::TriggerNewTextNotification()` - Cycle through all the rendering engines and tell them to notify handle the new text output. - None of the rendering engines _except_ `UiaEngine` has it implemented, so really we're just notifying UIA. - `UiaEngine::NotifyNewText()` - Concatenate any new output into a string. - When we're done painting, tell the notification system to actually notify of new events occurring and clear any stored output text. That way, we're ready for the next renderer frame. - `InteractivityAutomationPeer::NotifyNewOutput()` --> `TermControlAutomationPeer::NotifyNewOutput` - NOTE: these are split because of the in-proc and out-of-proc separation of the buffer. - Actually `RaiseNotificationEvent()` for the new text output. Additionally, we had to handle the "local echo" problem: when a key is pressed, the character is said twice (once for the keyboard event, and again for the character being written to the buffer). To accomplish this, we did the following: - `TermControl`: - here, we already handle keyboard events, so I added a line saying "if we have an automation peer attached, record the keyboard event in the automation peer". - `TermControlAutomationPeer`: - just before the notification is dispatched, check if the string of recent keyboard events match the beginning of the string of new output. If that's the case, we can assume that the common prefix was the "local echo". This is a fairly naive heuristic, but it's been working. Closes the following ADO bugs: - https://dev.azure.com/microsoft/OS/_workitems/edit/36506838 - (Probably) https://dev.azure.com/microsoft/OS/_workitems/edit/38011453 ## Test cases - [x] Base case: "echo hello" - [x] Partial line change - [x] Scrolling (should be unaffected) - [x] Large output - [x] "local echo": keyboard events read input character twice (cherry picked from commit f9be172)
🎉 Handy links: |
🎉 Handy links: |
How are screen readers supposed to find out whether events are supported or not? See my suggestion in #12358 (comment) @jcsteh pointed out in #12358 (comment) that we could do something with a flag as soon as an event is received, but I agree that this is far from clean. It would really help if we could find this out from the start. We can look at the app version to begin with, but that would bug with conhost as that has an entirely different versioning scheme. |
I believe we settled on giving the control in Terminal a name; we can revisit this once we port the changes to conhost :) |
Did we? I thought JAWS had issues with that... CC @carlos-zamora? Did you implement the custom property? |
…14048) microsoft/terminal#12358 Summary of the issue: A comment in NVDAObjects.UIA.FindOverlayClasses does not reflect reality. Description of how this pull request fixes the issue: Comment replaced with a more accurate one.
Related to microsoft/terminal#12358 Summary of the issue: For microsoft/terminal#12358, we discussed having a second `TermControl2` class name to distinguish terminals that do and don't support UIA notifications, but this was never implemented and there are no plans to do so. Description of how this pull request fixes the issue: Remove unneeded constant Testing strategy: Alpha testing Known issues with pull request: None known
Summary of the Pull Request
This change makes Windows Terminal raise a
RaiseNotificationEvent()
(docs) for new text output to the buffer.This is intended to help Narrator identify what new output appears and reduce the workload of diffing the buffer when a
TextChanged
event occurs.Detailed Description of the Pull Request / Additional comments
The flow of the event occurs as follows:
Terminal::_WriteBuffer()
Renderer::TriggerNewTextNotification()
UiaEngine
has it implemented, so really we're just notifying UIA.UiaEngine::NotifyNewText()
InteractivityAutomationPeer::NotifyNewOutput()
-->TermControlAutomationPeer::NotifyNewOutput
RaiseNotificationEvent()
for the new text output.Additionally, we had to handle the "local echo" problem: when a key is pressed, the character is said twice (once for the keyboard event, and again for the character being written to the buffer). To accomplish this, we did the following:
TermControl
:TermControlAutomationPeer
:This is a fairly naive heuristic, but it's been working.
Closes the following ADO bugs:
Test cases