From 638c6d029168500c5d95b33314d83bae093c8006 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 18 Aug 2021 14:26:43 -0700 Subject: [PATCH] Ensure automation peer is created regardless of terminal initialization (#10971) ## Summary of the Pull Request The bug was that Narrator would still read the content of the old tab/pane although a new tab/pane was introduced. This is caused by the automation peer not being created when XAML requests it. Normally, we would prevent the automation peer from being created if the terminal was not fully initialized. This change allows the automation peer to be created regardless of the terminal being fully initialized by... - `TermControl`: `_InitializeTerminal` updates the padding (dependent on the `SwapChainPanel`) upon full initialization - `ControlCore`: initialize the `_renderer` in the ctor so that we can attach the UIA Engine before `ControlCore::Initialize()` is called (dependent on `SwapChainPanel` loading) As a bonus, this also fixes a locking issue where logging would attempt to get the text range's text and lock twice. The locking fix is very similar to #10937. ## PR Checklist Closes [MSFT 33353327](https://microsoft.visualstudio.com/OS/_workitems/edit/33353327) ## Validation Steps Performed - New pane from key binding is announced by Narrator - New tab from key binding is announced by Narrator --- src/cascadia/TerminalControl/ControlCore.cpp | 55 ++++++++++--------- .../TerminalControl/ControlInteractivity.cpp | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 30 +++++++--- src/types/UiaTextRangeBase.cpp | 10 ++-- 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 7ee396a3c..d498bd690 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -104,6 +104,32 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto pfnTerminalTaskbarProgressChanged = std::bind(&ControlCore::_terminalTaskbarProgressChanged, this); _terminal->TaskbarProgressChangedCallback(pfnTerminalTaskbarProgressChanged); + // MSFT 33353327: Initialize the renderer in the ctor instead of Initialize(). + // We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go. + // If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach + // the UIA Engine to the renderer. This prevents us from signaling changes to the cursor or buffer. + { + // First create the render thread. + // Then stash a local pointer to the render thread so we can initialize it and enable it + // to paint itself *after* we hand off its ownership to the renderer. + // We split up construction and initialization of the render thread object this way + // because the renderer and render thread have circular references to each other. + auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); + auto* const localPointerToThread = renderThread.get(); + + // Now create the renderer and initialize the render thread. + _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread)); + + _renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() { + if (auto strongThis{ weakThis.get() }) + { + strongThis->_RendererEnteredErrorStateHandlers(*strongThis, nullptr); + } + }); + + THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); + } + // Get our dispatcher. If we're hosted in-proc with XAML, this will get // us the same dispatcher as TermControl::Dispatcher(). If we're out of // proc, this'll return null. We'll need to instead make a new @@ -196,27 +222,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation return false; } - // First create the render thread. - // Then stash a local pointer to the render thread so we can initialize it and enable it - // to paint itself *after* we hand off its ownership to the renderer. - // We split up construction and initialization of the render thread object this way - // because the renderer and render thread have circular references to each other. - auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); - auto* const localPointerToThread = renderThread.get(); - - // Now create the renderer and initialize the render thread. - _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread)); - ::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer; - - _renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() { - if (auto strongThis{ weakThis.get() }) - { - strongThis->_RendererEnteredErrorStateHandlers(*strongThis, nullptr); - } - }); - - THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); - // Set up the DX Engine auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); _renderer->AddRenderEngine(dxEngine.get()); @@ -248,7 +253,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _settings.InitialCols(width); _settings.InitialRows(height); - _terminal->CreateFromSettings(_settings, renderTarget); + _terminal->CreateFromSettings(_settings, *_renderer); // IMPORTANT! Set this callback up sooner than later. If we do it // after Enable, then it'll be possible to paint the frame once @@ -1465,10 +1470,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::AttachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine) { - if (_renderer) - { - _renderer->AddRenderEngine(pEngine); - } + // _renderer will always exist since it's introduced in the ctor + _renderer->AddRenderEngine(pEngine); } bool ControlCore::IsInReadOnlyMode() const diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index dc3f61490..55d389869 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -614,7 +614,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::InteractivityAutomationPeer ControlInteractivity::OnCreateAutomationPeer() try { - auto autoPeer = winrt::make_self(this); + const auto autoPeer = winrt::make_self(this); _uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get()); _core->AttachUiaEngine(_uiaEngine.get()); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 8d477fadf..e54515bcd 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -377,7 +377,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _changeBackgroundColor(bg); // Apply padding as swapChainPanel's margin - auto newMargin = ParseThicknessFromPadding(newSettings.Padding()); + const auto newMargin = ParseThicknessFromPadding(newSettings.Padding()); SwapChainPanel().Margin(newMargin); TSFInputControl().Margin(newMargin); @@ -511,18 +511,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The automation peer for our control Windows::UI::Xaml::Automation::Peers::AutomationPeer TermControl::OnCreateAutomationPeer() { - if (_initializedTerminal && !_IsClosing()) // only set up the automation peer if we're ready to go live + // MSFT 33353327: We're purposefully not using _initializedTerminal to ensure we're fully initialized. + // Doing so makes us return nullptr when XAML requests an automation peer. + // Instead, we need to give XAML an automation peer, then fix it later. + if (!_IsClosing()) { // create a custom automation peer with this code pattern: // (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers) if (const auto& interactivityAutoPeer{ _interactivity.OnCreateAutomationPeer() }) { - auto margins{ SwapChainPanel().Margin() }; - - Core::Padding padding{ margins.Left, - margins.Top, - margins.Right, - margins.Bottom }; + const auto margins{ SwapChainPanel().Margin() }; + const Core::Padding padding{ margins.Left, + margins.Top, + margins.Right, + margins.Bottom }; _automationPeer = winrt::make(this, padding, interactivityAutoPeer); return _automationPeer; } @@ -717,6 +719,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation _initializedTerminal = true; + // MSFT 33353327: If the AutomationPeer was created before we were done initializing, + // make sure it's properly set up now. + if (_automationPeer) + { + _automationPeer.UpdateControlBounds(); + const auto margins{ GetPadding() }; + _automationPeer.SetControlPadding(Core::Padding{ margins.Left, + margins.Top, + margins.Right, + margins.Bottom }); + } + // Likewise, run the event handlers outside of lock (they could // be reentrant) _InitializedHandlers(*this, nullptr); diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 02e8abdb6..d6aac19cd 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -938,7 +938,12 @@ try const auto maxLengthOpt = (maxLength == -1) ? std::nullopt : std::optional{ maxLength }; + _pData->LockConsole(); + auto Unlock = wil::scope_exit([this]() noexcept { + _pData->UnlockConsole(); + }); const auto text = _getTextValue(maxLengthOpt); + Unlock.reset(); *pRetVal = SysAllocString(text.c_str()); RETURN_HR_IF_NULL(E_OUTOFMEMORY, *pRetVal); @@ -958,11 +963,6 @@ CATCH_RETURN(); #pragma warning(disable : 26447) // compiler isn't filtering throws inside the try/catch std::wstring UiaTextRangeBase::_getTextValue(std::optional maxLength) const { - _pData->LockConsole(); - auto Unlock = wil::scope_exit([&]() noexcept { - _pData->UnlockConsole(); - }); - std::wstring textData{}; if (!IsDegenerate()) {