From f9be1720bd03716cc13fc00e59ea1239f187ca77 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 11 Mar 2022 15:50:47 -0800 Subject: [PATCH] Use UIA notifications for text output (#12358) ## 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 --- .github/actions/spelling/expect/expect.txt | 1 + .../InteractivityAutomationPeer.cpp | 5 + .../InteractivityAutomationPeer.h | 2 + .../InteractivityAutomationPeer.idl | 1 + src/cascadia/TerminalControl/TermControl.cpp | 5 + .../TermControlAutomationPeer.cpp | 107 ++++++++++++++++++ .../TermControlAutomationPeer.h | 3 + src/cascadia/TerminalCore/Terminal.cpp | 5 + .../UnitTests_TerminalCore/ScrollTest.cpp | 1 + src/host/ScreenBufferRenderTarget.cpp | 10 ++ src/host/ScreenBufferRenderTarget.hpp | 1 + src/renderer/atlas/AtlasEngine.api.cpp | 5 + src/renderer/atlas/AtlasEngine.h | 1 + src/renderer/base/RenderEngineBase.cpp | 5 + src/renderer/base/renderer.cpp | 8 ++ src/renderer/base/renderer.hpp | 2 + src/renderer/inc/DummyRenderTarget.hpp | 1 + src/renderer/inc/IRenderEngine.hpp | 1 + src/renderer/inc/IRenderTarget.hpp | 2 + src/renderer/inc/RenderEngineBase.hpp | 2 + src/renderer/uia/UiaRenderer.cpp | 81 +++++++++---- src/renderer/uia/UiaRenderer.hpp | 3 + src/types/IUiaEventDispatcher.h | 1 + 23 files changed, 232 insertions(+), 21 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 6706e4ef049..b08ed0b9011 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -2084,6 +2084,7 @@ rxvt safearray SAFECAST safemath +sapi sba SBCS SBCSDBCS diff --git a/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp b/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp index d2247311d1e..00621afcab5 100644 --- a/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp +++ b/src/cascadia/TerminalControl/InteractivityAutomationPeer.cpp @@ -93,6 +93,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation _CursorChangedHandlers(*this, nullptr); } + void InteractivityAutomationPeer::NotifyNewOutput(std::wstring_view newOutput) + { + _NewOutputHandlers(*this, hstring{ newOutput }); + } + #pragma region ITextProvider com_array InteractivityAutomationPeer::GetSelection() { diff --git a/src/cascadia/TerminalControl/InteractivityAutomationPeer.h b/src/cascadia/TerminalControl/InteractivityAutomationPeer.h index 57c3a85fc8d..9f0d96fead5 100644 --- a/src/cascadia/TerminalControl/InteractivityAutomationPeer.h +++ b/src/cascadia/TerminalControl/InteractivityAutomationPeer.h @@ -49,6 +49,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SignalSelectionChanged() override; void SignalTextChanged() override; void SignalCursorChanged() override; + void NotifyNewOutput(std::wstring_view newOutput) override; #pragma endregion #pragma region ITextProvider Pattern @@ -73,6 +74,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(SelectionChanged, IInspectable, IInspectable); TYPED_EVENT(TextChanged, IInspectable, IInspectable); TYPED_EVENT(CursorChanged, IInspectable, IInspectable); + TYPED_EVENT(NewOutput, IInspectable, hstring); private: Windows::UI::Xaml::Automation::Provider::ITextRangeProvider _CreateXamlUiaTextRange(::ITextRangeProvider* returnVal) const; diff --git a/src/cascadia/TerminalControl/InteractivityAutomationPeer.idl b/src/cascadia/TerminalControl/InteractivityAutomationPeer.idl index 43d4bd35eac..9eb79a6329c 100644 --- a/src/cascadia/TerminalControl/InteractivityAutomationPeer.idl +++ b/src/cascadia/TerminalControl/InteractivityAutomationPeer.idl @@ -14,5 +14,6 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler SelectionChanged; event Windows.Foundation.TypedEventHandler TextChanged; event Windows.Foundation.TypedEventHandler CursorChanged; + event Windows.Foundation.TypedEventHandler NewOutput; } } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index d329f1132f1..2410f585b7d 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1097,6 +1097,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation keyDown) : true; + if (vkey && keyDown && _automationPeer) + { + get_self(_automationPeer)->RecordKeyEvent(vkey); + } + if (_cursorTimer) { // Manually show the cursor when a key is pressed. Restarting diff --git a/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp b/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp index d910b70bb55..b0203d24208 100644 --- a/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp +++ b/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp @@ -28,6 +28,43 @@ namespace XamlAutomation using winrt::Windows::UI::Xaml::Automation::Provider::ITextRangeProvider; } +static constexpr wchar_t UNICODE_NEWLINE{ L'\n' }; + +// Method Description: +// - creates a copy of the provided text with all of the control characters removed +// Arguments: +// - text: the string we're sanitizing +// Return Value: +// - a copy of "sanitized" with all of the control characters removed +static std::wstring Sanitize(std::wstring_view text) +{ + std::wstring sanitized{ text }; + sanitized.erase(std::remove_if(sanitized.begin(), sanitized.end(), [](wchar_t c) { + return (c < UNICODE_SPACE && c != UNICODE_NEWLINE) || c == 0x7F /*DEL*/; + }), + sanitized.end()); + return sanitized; +} + +// Method Description: +// - verifies if a given string has text that would be read by a screen reader. +// - a string of control characters, for example, would not be read. +// Arguments: +// - text: the string we're validating +// Return Value: +// - true, if the text is readable. false, otherwise. +static constexpr bool IsReadable(std::wstring_view text) +{ + for (const auto c : text) + { + if (c > UNICODE_SPACE) + { + return true; + } + } + return false; +} + namespace winrt::Microsoft::Terminal::Control::implementation { TermControlAutomationPeer::TermControlAutomationPeer(TermControl* owner, @@ -45,6 +82,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _contentAutomationPeer.SelectionChanged([this](auto&&, auto&&) { SignalSelectionChanged(); }); _contentAutomationPeer.TextChanged([this](auto&&, auto&&) { SignalTextChanged(); }); _contentAutomationPeer.CursorChanged([this](auto&&, auto&&) { SignalCursorChanged(); }); + _contentAutomationPeer.NewOutput([this](auto&&, hstring newOutput) { NotifyNewOutput(newOutput); }); _contentAutomationPeer.ParentProvider(*this); }; @@ -68,6 +106,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation _contentAutomationPeer.SetControlPadding(padding); } + void TermControlAutomationPeer::RecordKeyEvent(const WORD vkey) + { + if (const auto charCode{ MapVirtualKey(vkey, MAPVK_VK_TO_CHAR) }) + { + if (const auto keyEventChar{ gsl::narrow_cast(charCode) }; IsReadable({ &keyEventChar, 1 })) + { + _keyEvents.emplace_back(keyEventChar); + } + } + } + // Method Description: // - Signals the ui automation client that the terminal's selection has changed and should be updated // Arguments: @@ -142,8 +191,66 @@ namespace winrt::Microsoft::Terminal::Control::implementation }); } + void TermControlAutomationPeer::NotifyNewOutput(std::wstring_view newOutput) + { + // Try to suppress any events (or event data) + // that is just the keypress the user made + auto sanitized{ Sanitize(newOutput) }; + while (!_keyEvents.empty() && IsReadable(sanitized)) + { + if (til::toupper_ascii(sanitized.front()) == _keyEvents.front()) + { + // the key event's character (i.e. the "A" key) matches + // the output character (i.e. "a" or "A" text). + // We can assume that the output character resulted from + // the pressed key, so we can ignore it. + sanitized = sanitized.substr(1); + _keyEvents.pop_front(); + } + else + { + // The output doesn't match, + // so clear the input stack and + // move on to fire the event. + _keyEvents.clear(); + break; + } + } + + // Suppress event if the remaining text is not readable + if (!IsReadable(sanitized)) + { + return; + } + + auto dispatcher{ Dispatcher() }; + if (!dispatcher) + { + return; + } + + // IMPORTANT: + // [1] make sure the scope returns a copy of "sanitized" so that it isn't accidentally deleted + // [2] AutomationNotificationProcessing::All --> ensures it can be interrupted by keyboard events + // [3] Do not "RunAsync(...).get()". For whatever reason, this causes NVDA to just not receive "SignalTextChanged()"'s events. + dispatcher.RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, [weakThis{ get_weak() }, sanitizedCopy{ hstring{ sanitized } }]() { + if (auto strongThis{ weakThis.get() }) + { + try + { + strongThis->RaiseNotificationEvent(AutomationNotificationKind::ActionCompleted, + AutomationNotificationProcessing::All, + sanitizedCopy, + L"TerminalTextOutput"); + } + CATCH_LOG(); + } + }); + } + hstring TermControlAutomationPeer::GetClassNameCore() const { + // IMPORTANT: Do NOT change the name. Screen readers like JAWS may be dependent on this being "TermControl". return L"TermControl"; } diff --git a/src/cascadia/TerminalControl/TermControlAutomationPeer.h b/src/cascadia/TerminalControl/TermControlAutomationPeer.h index d912a1deca0..758864bc38f 100644 --- a/src/cascadia/TerminalControl/TermControlAutomationPeer.h +++ b/src/cascadia/TerminalControl/TermControlAutomationPeer.h @@ -48,6 +48,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void UpdateControlBounds(); void SetControlPadding(const Core::Padding padding); + void RecordKeyEvent(const WORD vkey); #pragma region FrameworkElementAutomationPeer hstring GetClassNameCore() const; @@ -64,6 +65,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SignalSelectionChanged() override; void SignalTextChanged() override; void SignalCursorChanged() override; + void NotifyNewOutput(std::wstring_view newOutput) override; #pragma endregion #pragma region ITextProvider Pattern @@ -78,5 +80,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation private: winrt::Microsoft::Terminal::Control::implementation::TermControl* _termControl; Control::InteractivityAutomationPeer _contentAutomationPeer; + std::deque _keyEvents; }; } diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index d03bcbb3abb..073d5c71273 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -1002,6 +1002,11 @@ void Terminal::_WriteBuffer(const std::wstring_view& stringView) _AdjustCursorPosition(proposedCursorPosition); } + // 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); + cursor.EndDeferDrawing(); } diff --git a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp index 0365f83a2a2..6513fbfbc9f 100644 --- a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp @@ -53,6 +53,7 @@ namespace }; virtual void TriggerCircling(){}; void TriggerTitleChange(){}; + void TriggerNewTextNotification(const std::wstring_view){}; private: std::optional _triggerScrollDelta; diff --git a/src/host/ScreenBufferRenderTarget.cpp b/src/host/ScreenBufferRenderTarget.cpp index 87f98ce29aa..ed9ebc91608 100644 --- a/src/host/ScreenBufferRenderTarget.cpp +++ b/src/host/ScreenBufferRenderTarget.cpp @@ -110,3 +110,13 @@ void ScreenBufferRenderTarget::TriggerTitleChange() pRenderer->TriggerTitleChange(); } } + +void ScreenBufferRenderTarget::TriggerNewTextNotification(const std::wstring_view newText) +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerNewTextNotification(newText); + } +} diff --git a/src/host/ScreenBufferRenderTarget.hpp b/src/host/ScreenBufferRenderTarget.hpp index 09a189691c7..7f7588faba4 100644 --- a/src/host/ScreenBufferRenderTarget.hpp +++ b/src/host/ScreenBufferRenderTarget.hpp @@ -39,6 +39,7 @@ class ScreenBufferRenderTarget final : public Microsoft::Console::Render::IRende void TriggerScroll(const COORD* const pcoordDelta) override; void TriggerCircling() override; void TriggerTitleChange() override; + void TriggerNewTextNotification(const std::wstring_view newText) override; private: SCREEN_INFORMATION& _owner; diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index 2e0db997333..7c49fb99675 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -143,6 +143,11 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept return S_OK; } +[[nodiscard]] HRESULT AtlasEngine::NotifyNewText(const std::wstring_view newText) noexcept +{ + return S_OK; +} + [[nodiscard]] HRESULT AtlasEngine::UpdateFont(const FontInfoDesired& fontInfoDesired, _Out_ FontInfo& fontInfo) noexcept { return UpdateFont(fontInfoDesired, fontInfo, {}, {}); diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 5c417b7386b..74114ba54f7 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -35,6 +35,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT InvalidateAll() noexcept override; [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* pForcePaint) noexcept override; [[nodiscard]] HRESULT InvalidateTitle(std::wstring_view proposedTitle) noexcept override; + [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override; [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; [[nodiscard]] HRESULT ResetLineTransform() noexcept override; [[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, size_t targetRow, size_t viewportLeft) noexcept override; diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index cba14c72639..ca506b9d2ab 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -36,6 +36,11 @@ HRESULT RenderEngineBase::UpdateTitle(const std::wstring_view newTitle) noexcept return hr; } +HRESULT RenderEngineBase::NotifyNewText(const std::wstring_view /*newText*/) noexcept +{ + return S_FALSE; +} + HRESULT RenderEngineBase::UpdateSoftFont(const gsl::span /*bitPattern*/, const SIZE /*cellSize*/, const size_t /*centeringHint*/) noexcept diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index e58b71fa3f0..66bbcc6b5ad 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -493,6 +493,14 @@ void Renderer::TriggerTitleChange() NotifyPaintFrame(); } +void Renderer::TriggerNewTextNotification(const std::wstring_view newText) +{ + FOREACH_ENGINE(pEngine) + { + LOG_IF_FAILED(pEngine->NotifyNewText(newText)); + } +} + // Routine Description: // - Update the title for a particular engine. // Arguments: diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index b4c1d05ca54..e53afd2a25f 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -54,6 +54,8 @@ namespace Microsoft::Console::Render void TriggerCircling() override; void TriggerTitleChange() override; + void TriggerNewTextNotification(const std::wstring_view newText) override; + void TriggerFontChange(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo); diff --git a/src/renderer/inc/DummyRenderTarget.hpp b/src/renderer/inc/DummyRenderTarget.hpp index e00a22a1646..fb105e599ba 100644 --- a/src/renderer/inc/DummyRenderTarget.hpp +++ b/src/renderer/inc/DummyRenderTarget.hpp @@ -31,4 +31,5 @@ class DummyRenderTarget final : public Microsoft::Console::Render::IRenderTarget void TriggerScroll(const COORD* const /*pcoordDelta*/) override {} void TriggerCircling() override {} void TriggerTitleChange() override {} + void TriggerNewTextNotification(const std::wstring_view) override {} }; diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 85be8e90e8c..3d13595aabc 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -69,6 +69,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT InvalidateAll() noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateCircling(_Out_ bool* pForcePaint) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateTitle(std::wstring_view proposedTitle) noexcept = 0; + [[nodiscard]] virtual HRESULT NotifyNewText(const std::wstring_view newText) noexcept = 0; [[nodiscard]] virtual HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept = 0; [[nodiscard]] virtual HRESULT ResetLineTransform() noexcept = 0; [[nodiscard]] virtual HRESULT PrepareLineTransform(LineRendition lineRendition, size_t targetRow, size_t viewportLeft) noexcept = 0; diff --git a/src/renderer/inc/IRenderTarget.hpp b/src/renderer/inc/IRenderTarget.hpp index f12f243e1a3..6f32c40bd84 100644 --- a/src/renderer/inc/IRenderTarget.hpp +++ b/src/renderer/inc/IRenderTarget.hpp @@ -45,6 +45,8 @@ namespace Microsoft::Console::Render virtual void TriggerScroll(const COORD* const pcoordDelta) = 0; virtual void TriggerCircling() = 0; virtual void TriggerTitleChange() = 0; + + virtual void TriggerNewTextNotification(const std::wstring_view newText) = 0; }; inline Microsoft::Console::Render::IRenderTarget::~IRenderTarget() {} diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 6ad59fec343..452dd0cc39f 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -38,6 +38,8 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept override; + [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, const SIZE cellSize, const size_t centeringHint) noexcept override; diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index 2d0f1580ae9..0a87ce0e619 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -182,6 +182,18 @@ CATCH_RETURN(); return S_FALSE; } +[[nodiscard]] HRESULT UiaEngine::NotifyNewText(const std::wstring_view newText) noexcept +try +{ + if (!newText.empty()) + { + _newOutput.append(newText); + _newOutput.push_back(L'\n'); + } + return S_OK; +} +CATCH_LOG_RETURN_HR(E_FAIL); + // Routine Description: // - This is unused by this renderer. // Arguments: @@ -207,7 +219,7 @@ CATCH_RETURN(); RETURN_HR_IF(S_FALSE, !_isEnabled); // add more events here - const bool somethingToDo = _selectionChanged || _textBufferChanged || _cursorChanged; + const bool somethingToDo = _selectionChanged || _textBufferChanged || _cursorChanged || !_queuedOutput.empty(); // If there's nothing to do, quick return RETURN_HR_IF(S_FALSE, !somethingToDo); @@ -227,6 +239,34 @@ CATCH_RETURN(); RETURN_HR_IF(S_FALSE, !_isEnabled); RETURN_HR_IF(E_INVALIDARG, !_isPainting); // invalid to end paint when we're not painting + // Snap this now while we're still under lock + // so present can work on the copy while another + // thread might start filling the next "frame" + // worth of text data. + std::swap(_queuedOutput, _newOutput); + _newOutput.clear(); + return S_OK; +} + +// RenderEngineBase defines a WaitUntilCanRender() that sleeps for 8ms to throttle rendering. +// But UiaEngine is never the only engine running. Overriding this function prevents +// us from sleeping 16ms per frame, when the other engine also sleeps for 8ms. +void UiaEngine::WaitUntilCanRender() noexcept +{ +} + +// Routine Description: +// - Used to perform longer running presentation steps outside the lock so the +// other threads can continue. +// - Not currently used by UiaEngine. +// Arguments: +// - +// Return Value: +// - S_FALSE since we do nothing. +[[nodiscard]] HRESULT UiaEngine::Present() noexcept +{ + RETURN_HR_IF(S_FALSE, !_isEnabled); + // Fire UIA Events here if (_selectionChanged) { @@ -252,35 +292,34 @@ CATCH_RETURN(); } CATCH_LOG(); } + try + { + // The speech API is limited to 1000 characters at a time. + // Break up the output into 1000 character chunks to ensure + // the output isn't cut off. + static constexpr size_t sapiLimit{ 1000 }; + const std::wstring_view output{ _queuedOutput }; + for (size_t offset = 0;; offset += sapiLimit) + { + const auto croppedText{ output.substr(offset, sapiLimit) }; + if (croppedText.empty()) + { + break; + } + _dispatcher->NotifyNewOutput(croppedText); + } + } + CATCH_LOG(); _selectionChanged = false; _textBufferChanged = false; _cursorChanged = false; _isPainting = false; + _queuedOutput.clear(); return S_OK; } -// RenderEngineBase defines a WaitUntilCanRender() that sleeps for 8ms to throttle rendering. -// But UiaEngine is never the only engine running. Overriding this function prevents -// us from sleeping 16ms per frame, when the other engine also sleeps for 8ms. -void UiaEngine::WaitUntilCanRender() noexcept -{ -} - -// Routine Description: -// - Used to perform longer running presentation steps outside the lock so the -// other threads can continue. -// - Not currently used by UiaEngine. -// Arguments: -// - -// Return Value: -// - S_FALSE since we do nothing. -[[nodiscard]] HRESULT UiaEngine::Present() noexcept -{ - return S_FALSE; -} - // Routine Description: // - This is currently unused. // Arguments: diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 1f5d1ecfe9d..8f1603aeac6 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -47,6 +47,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; [[nodiscard]] HRESULT InvalidateAll() noexcept override; [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override; + [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; [[nodiscard]] HRESULT PaintBufferLine(gsl::span const clusters, const COORD coord, const bool fTrimLeft, const bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept override; @@ -70,6 +71,8 @@ namespace Microsoft::Console::Render bool _selectionChanged; bool _textBufferChanged; bool _cursorChanged; + std::wstring _newOutput; + std::wstring _queuedOutput; Microsoft::Console::Types::IUiaEventDispatcher* _dispatcher; diff --git a/src/types/IUiaEventDispatcher.h b/src/types/IUiaEventDispatcher.h index 1d9fc11e8ec..845e5770c69 100644 --- a/src/types/IUiaEventDispatcher.h +++ b/src/types/IUiaEventDispatcher.h @@ -23,5 +23,6 @@ namespace Microsoft::Console::Types virtual void SignalSelectionChanged() = 0; virtual void SignalTextChanged() = 0; virtual void SignalCursorChanged() = 0; + virtual void NotifyNewOutput(std::wstring_view newOutput) = 0; }; }