Skip to content

Commit

Permalink
Accessibility: TermControl Automation Peer (#2083)
Browse files Browse the repository at this point in the history
Builds on the work of #1691 and #1915 

Let's start with the easy change:
- `TermControl`'s `controlRoot` was removed. `TermControl` is a `UserControl`
  now.

Ok. Now we've got a story to tell here....

### TermControlAP - the Automation Peer
Here's an in-depth guide on custom automation peers:
https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers

We have a custom XAML element (TermControl). So XAML can't really hold our
hands and determine an accessible behavior for us. So this automation peer is
responsible for enabling that interaction.

We made it a FrameworkElementAutomationPeer to get as much accessibility as
possible from it just being a XAML element (i.e.: where are we on the screen?
what are my dimensions?). This is recommended. Any functions with "Core" at the
end, are overwritten here to tweak this automation peer into what we really
need.

But what kind of interactions can a user expect from this XAML element?
Introducing ControlPatterns! There's a ton of interfaces that just define "what
can I do". Thankfully, we already know that we're supposed to be
`ScreenInfoUiaProvider` and that was an `ITextProvider`, so let's just make the
TermControlAP an `ITextProvider` too.

So now we have a way to define what accessible actions can be performed on us,
but what should those actions do? Well let's just use the automation providers
from ConHost that are now in a shared space! (Note: this is a great place to
stop and get some coffee. We're about to hop into the .cpp file in the next
section)


### Wrapping our shared Automation Providers

Unfortunately, we can't just use the automation providers from ConHost. Or, at
least not just hook them up as easily as we wish. ConHost's UIA Providers were
written using UIAutomationCore and ITextRangeProiuder. XAML's interfaces
ITextProvider and ITextRangeProvider are lined up to be exactly the same.

So we need to wrap our ConHost UIA Providers (UIAutomationCore) with the XAML
ones. We had two providers, so that means we have two wrappers.

#### TermControlAP (XAML) <----> ScreenInfoUiaProvider (UIAutomationCore)
Each of the functions in the pragma region `ITextProvider` for
TermControlAP.cpp is just wrapping what we do in `ScreenInfoUiaProvider`, and
returning an acceptable version of it.

Most of `ScreenInfoUiaProvider`'s functions return `UiaTextRange`s. So we need
to wrap that too. That's this next section...

#### XamlUiaTextRange (XAML) <----> UiaTextRange (UIAutomationCore)
Same idea.  We're wrapping everything that we could do with `UiaTextRange` and
putting it inside of `XamlUiaTextRange`.


### Additional changes to `UiaTextRange` and `ScreenInfoUiaProvider`
If you don't know what I just said, please read this background:
- #1691: how accessibility works and the general responsibility of these two
  classes
- #1915: how we pulled these Accessibility Providers into a shared area

TL;DR: `ScreenInfoUiaProvider` lets you interact with the displayed text.
`UiaTextRange` is specific ranges of text in the display and navigate the text.

Thankfully, we didn't do many changes here. I feel like some of it is hacked
together but now that we have a somewhat working system, making changes
shouldn't be too hard...I hope.

#### UiaTextRange
We don't have access to the window handle. We really only need it to draw the
bounding rects using WinUser's `ScreenToClient()` and `ClientToScreen()`. I
need to figure out how to get around this.

In the meantime, I made the window handle optional. And if we don't have
one....well, we need to figure that out. But other than that, we have a
`UiaTextRange`.

#### ScreenInfoUiaProvider
At some point, we need to hook up this automation provider to the
WindowUiaProvider. This should help with navigation of the UIA Tree and make
everything just look waaaay better. For now, let's just do the same approach
and make the pUiaParent optional.

This one's the one I'm not that proud of, but it works. We need the parent to
get a bounding rect of the terminal. While we figure out how to attach the
WindowUiaProvider, we should at the very least be able to get a bunch of info
from our xaml automation peer. So, I've added a _getBoundingRect optional
function. This is what's called when we don't have a WindowUiaProvider as our
parent.


## Validation Steps Performed
I've been using inspect.exe to see the UIA tree.
I was able to interact with the terminal mostly fine. A few known issues below.

Unfortunately, I tried running Narrator on this and it didn't seem to like it
(by that I mean WT crashed). Then again, I don't really know how to use
narrator other than "click on object" --> "listen voice". I feel like there's a
way to get the other interactions with narrator, but I'll be looking into more
of that soon. I bet if I fix the two issues below, Narrator will be happy.

## Miscellaneous Known Issues
- `GetSelection()` and `GetVisibleRanges()` crashes. I need to debug through
  these. I want to include them in this PR.

Fixes #1353.
  • Loading branch information
carlos-zamora authored and DHowett committed Jul 30, 2019
1 parent 1afab78 commit a08666b
Show file tree
Hide file tree
Showing 20 changed files with 811 additions and 86 deletions.
2 changes: 1 addition & 1 deletion src/cascadia/TerminalApp/App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ namespace winrt::TerminalApp::implementation
_UpdateTitle(tab);
});

term.GetControl().GotFocus([this, weakTabPtr](auto&&, auto&&) {
term.GotFocus([this, weakTabPtr](auto&&, auto&&) {
auto tab = weakTabPtr.lock();
if (!tab)
{
Expand Down
12 changes: 6 additions & 6 deletions src/cascadia/TerminalApp/Pane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocus
_lastFocused{ lastFocused },
_profile{ profile }
{
_root.Children().Append(_control.GetControl());
_root.Children().Append(_control);
_connectionClosedToken = _control.ConnectionClosed({ this, &Pane::_ControlClosedHandler });

// Set the background of the pane to match that of the theme's default grid
Expand Down Expand Up @@ -426,7 +426,7 @@ bool Pane::_HasFocusedChild() const noexcept
// We're intentionally making this one giant expression, so the compiler
// will skip the following lookups if one of the lookups before it returns
// true
return (_control && _control.GetControl().FocusState() != FocusState::Unfocused) ||
return (_control && _control.FocusState() != FocusState::Unfocused) ||
(_firstChild && _firstChild->_HasFocusedChild()) ||
(_secondChild && _secondChild->_HasFocusedChild());
}
Expand All @@ -445,7 +445,7 @@ void Pane::UpdateFocus()
if (_IsLeaf())
{
const auto controlFocused = _control &&
_control.GetControl().FocusState() != FocusState::Unfocused;
_control.FocusState() != FocusState::Unfocused;

_lastFocused = controlFocused;
}
Expand All @@ -468,7 +468,7 @@ void Pane::_FocusFirstChild()
{
if (_IsLeaf())
{
_control.GetControl().Focus(FocusState::Programmatic);
_control.Focus(FocusState::Programmatic);
}
else
{
Expand Down Expand Up @@ -564,11 +564,11 @@ void Pane::_CloseChild(const bool closeFirst)
_separatorRoot = { nullptr };

// Reattach the TermControl to our grid.
_root.Children().Append(_control.GetControl());
_root.Children().Append(_control);

if (_lastFocused)
{
_control.GetControl().Focus(FocusState::Programmatic);
_control.Focus(FocusState::Programmatic);
}

_splitState = SplitState::None;
Expand Down
4 changes: 2 additions & 2 deletions src/cascadia/TerminalApp/Tab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ void Tab::_Focus()
auto lastFocusedControl = _rootPane->GetFocusedTerminalControl();
if (lastFocusedControl)
{
lastFocusedControl.GetControl().Focus(FocusState::Programmatic);
lastFocusedControl.Focus(FocusState::Programmatic);
}
}

Expand Down Expand Up @@ -181,7 +181,7 @@ void Tab::SetTabText(const winrt::hstring& text)
void Tab::Scroll(const int delta)
{
auto control = GetFocusedTerminalControl();
control.GetControl().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [control, delta]() {
control.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [control, delta]() {
const auto currentOffset = control.GetScrollOffset();
control.KeyboardScrollViewport(currentOffset + delta);
});
Expand Down
39 changes: 18 additions & 21 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "..\..\types\inc\GlyphWidth.hpp"

#include "TermControl.g.cpp"
#include "TermControlAutomationPeer.h"

using namespace ::Microsoft::Console::Types;
using namespace ::Microsoft::Terminal::Core;
Expand All @@ -30,7 +31,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_connection{ connection },
_initializedTerminal{ false },
_root{ nullptr },
_controlRoot{ nullptr },
_swapChainPanel{ nullptr },
_settings{ settings },
_closing{ false },
Expand All @@ -52,11 +52,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation

void TermControl::_Create()
{
// Create a dummy UserControl to use as the "root" of our control we'll
// build manually.
Controls::UserControl myControl;
_controlRoot = myControl;

Controls::Grid container;

Controls::ColumnDefinition contentColumn{};
Expand Down Expand Up @@ -108,20 +103,20 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_bgImageLayer = bgImageLayer;

_swapChainPanel = swapChainPanel;
_controlRoot.Content(_root);
this->Content(_root);

_ApplyUISettings();

// These are important:
// 1. When we get tapped, focus us
_controlRoot.Tapped([this](auto&, auto& e) {
_controlRoot.Focus(FocusState::Pointer);
this->Tapped([this](auto&, auto& e) {
this->Focus(FocusState::Pointer);
e.Handled(true);
});
// 2. Make sure we can be focused (why this isn't `Focusable` I'll never know)
_controlRoot.IsTabStop(true);
this->IsTabStop(true);
// 3. Actually not sure about this one. Maybe it isn't necessary either.
_controlRoot.AllowFocusOnInteraction(true);
this->AllowFocusOnInteraction(true);

// DON'T CALL _InitializeTerminal here - wait until the swap chain is loaded to do that.

Expand Down Expand Up @@ -345,14 +340,16 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
Close();
}

UIElement TermControl::GetRoot()
Windows::UI::Xaml::Automation::Peers::AutomationPeer TermControl::OnCreateAutomationPeer()
{
return _root;
// create a custom automation peer with this code pattern:
// (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers)
return winrt::make<winrt::Microsoft::Terminal::TerminalControl::implementation::TermControlAutomationPeer>(*this);
}

Controls::UserControl TermControl::GetControl()
::Microsoft::Console::Render::IRenderData* TermControl::GetRenderData() const
{
return _controlRoot;
return _terminal.get();
}

void TermControl::SwapChainChanged()
Expand Down Expand Up @@ -506,9 +503,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// through CharacterRecieved.
// I don't believe there's a difference between KeyDown and
// PreviewKeyDown for our purposes
// These two handlers _must_ be on _controlRoot, not _root.
_controlRoot.PreviewKeyDown({ this, &TermControl::_KeyDownHandler });
_controlRoot.CharacterReceived({ this, &TermControl::_CharacterHandler });
// These two handlers _must_ be on this, not _root.
this->PreviewKeyDown({ this, &TermControl::_KeyDownHandler });
this->CharacterReceived({ this, &TermControl::_CharacterHandler });

auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1);
_terminal->SetTitleChangedCallback(pfnTitleChanged);
Expand Down Expand Up @@ -542,14 +539,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// import value from WinUser (convert from milli-seconds to micro-seconds)
_multiClickTimer = GetDoubleClickTime() * 1000;

_gotFocusRevoker = _controlRoot.GotFocus(winrt::auto_revoke, { this, &TermControl::_GotFocusHandler });
_lostFocusRevoker = _controlRoot.LostFocus(winrt::auto_revoke, { this, &TermControl::_LostFocusHandler });
_gotFocusRevoker = this->GotFocus(winrt::auto_revoke, { this, &TermControl::_GotFocusHandler });
_lostFocusRevoker = this->LostFocus(winrt::auto_revoke, { this, &TermControl::_LostFocusHandler });

// Focus the control here. If we do it up above (in _Create_), then the
// focus won't actually get passed to us. I believe this is because
// we're not technically a part of the UI tree yet, so focusing us
// becomes a no-op.
_controlRoot.Focus(FocusState::Programmatic);
this->Focus(FocusState::Programmatic);

_connection.Start();
_initializedTerminal = true;
Expand Down
6 changes: 3 additions & 3 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
TermControl();
TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection);

Windows::UI::Xaml::UIElement GetRoot();
Windows::UI::Xaml::Controls::UserControl GetControl();
void UpdateSettings(Settings::IControlSettings newSettings);

hstring Title();
Expand All @@ -55,6 +53,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void SwapChainChanged();
~TermControl();

Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer();
::Microsoft::Console::Render::IRenderData* GetRenderData() const;

static Windows::Foundation::Point GetProposedDimensions(Microsoft::Terminal::Settings::IControlSettings const& settings, const uint32_t dpi);

// clang-format off
Expand All @@ -71,7 +72,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
TerminalConnection::ITerminalConnection _connection;
bool _initializedTerminal;

Windows::UI::Xaml::Controls::UserControl _controlRoot;
Windows::UI::Xaml::Controls::Grid _root;
Windows::UI::Xaml::Controls::Image _bgImageLayer;
Windows::UI::Xaml::Controls::SwapChainPanel _swapChainPanel;
Expand Down
4 changes: 1 addition & 3 deletions src/cascadia/TerminalControl/TermControl.idl
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@ namespace Microsoft.Terminal.TerminalControl
}

[default_interface]
runtimeclass TermControl
runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl
{
TermControl();
TermControl(Microsoft.Terminal.Settings.IControlSettings settings, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection);

static Windows.Foundation.Point GetProposedDimensions(Microsoft.Terminal.Settings.IControlSettings settings, UInt32 dpi);

Windows.UI.Xaml.UIElement GetRoot();
Windows.UI.Xaml.Controls.UserControl GetControl();
void UpdateSettings(Microsoft.Terminal.Settings.IControlSettings newSettings);

event TitleChangedEventArgs TitleChanged;
Expand Down
154 changes: 154 additions & 0 deletions src/cascadia/TerminalControl/TermControlAutomationPeer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "pch.h"
#include <UIAutomationCore.h>
#include "TermControlAutomationPeer.h"
#include "TermControl.h"
#include "TermControlAutomationPeer.g.cpp"

#include "XamlUiaTextRange.h"

using namespace Microsoft::Console::Types;
using namespace winrt::Windows::UI::Xaml::Automation::Peers;

namespace UIA
{
using ::ITextRangeProvider;
using ::SupportedTextSelection;
}

namespace XamlAutomation
{
using winrt::Windows::UI::Xaml::Automation::SupportedTextSelection;
using winrt::Windows::UI::Xaml::Automation::Provider::IRawElementProviderSimple;
using winrt::Windows::UI::Xaml::Automation::Provider::ITextRangeProvider;
}

namespace winrt::Microsoft::Terminal::TerminalControl::implementation
{
TermControlAutomationPeer::TermControlAutomationPeer(winrt::Microsoft::Terminal::TerminalControl::implementation::TermControl const& owner) :
TermControlAutomationPeerT<TermControlAutomationPeer>(owner), // pass owner to FrameworkElementAutomationPeer
_uiaProvider{ owner.GetRenderData(), nullptr, std::bind(&TermControlAutomationPeer::GetBoundingRectWrapped, this) } {};

winrt::hstring TermControlAutomationPeer::GetClassNameCore() const
{
return L"TermControl";
}

AutomationControlType TermControlAutomationPeer::GetAutomationControlTypeCore() const
{
return AutomationControlType::Text;
}

winrt::hstring TermControlAutomationPeer::GetLocalizedControlTypeCore() const
{
// TODO GitHub #2142: Localize string
return L"TerminalControl";
}

winrt::Windows::Foundation::IInspectable TermControlAutomationPeer::GetPatternCore(PatternInterface patternInterface) const
{
switch (patternInterface)
{
case PatternInterface::Text:
return *this;
break;
default:
return nullptr;
}
}

#pragma region ITextProvider
winrt::com_array<XamlAutomation::ITextRangeProvider> TermControlAutomationPeer::GetSelection()
{
SAFEARRAY* pReturnVal;
THROW_IF_FAILED(_uiaProvider.GetSelection(&pReturnVal));
return WrapArrayOfTextRangeProviders(pReturnVal);
}

winrt::com_array<XamlAutomation::ITextRangeProvider> TermControlAutomationPeer::GetVisibleRanges()
{
SAFEARRAY* pReturnVal;
THROW_IF_FAILED(_uiaProvider.GetVisibleRanges(&pReturnVal));
return WrapArrayOfTextRangeProviders(pReturnVal);
}

XamlAutomation::ITextRangeProvider TermControlAutomationPeer::RangeFromChild(XamlAutomation::IRawElementProviderSimple childElement)
{
UIA::ITextRangeProvider* returnVal;
// ScreenInfoUiaProvider doesn't actually use parameter, so just pass in nullptr
THROW_IF_FAILED(_uiaProvider.RangeFromChild(/* IRawElementProviderSimple */ nullptr,
&returnVal));

auto parentProvider = this->ProviderFromPeer(*this);
auto xutr = winrt::make_self<XamlUiaTextRange>(returnVal, parentProvider);
return xutr.as<XamlAutomation::ITextRangeProvider>();
}

XamlAutomation::ITextRangeProvider TermControlAutomationPeer::RangeFromPoint(Windows::Foundation::Point screenLocation)
{
UIA::ITextRangeProvider* returnVal;
THROW_IF_FAILED(_uiaProvider.RangeFromPoint({ screenLocation.X, screenLocation.Y }, &returnVal));

auto parentProvider = this->ProviderFromPeer(*this);
auto xutr = winrt::make_self<XamlUiaTextRange>(returnVal, parentProvider);
return xutr.as<XamlAutomation::ITextRangeProvider>();
}

XamlAutomation::ITextRangeProvider TermControlAutomationPeer::DocumentRange()
{
UIA::ITextRangeProvider* returnVal;
THROW_IF_FAILED(_uiaProvider.get_DocumentRange(&returnVal));

auto parentProvider = this->ProviderFromPeer(*this);
auto xutr = winrt::make_self<XamlUiaTextRange>(returnVal, parentProvider);
return xutr.as<XamlAutomation::ITextRangeProvider>();
}

Windows::UI::Xaml::Automation::SupportedTextSelection TermControlAutomationPeer::SupportedTextSelection()
{
UIA::SupportedTextSelection returnVal;
THROW_IF_FAILED(_uiaProvider.get_SupportedTextSelection(&returnVal));
return static_cast<XamlAutomation::SupportedTextSelection>(returnVal);
}

#pragma endregion

RECT TermControlAutomationPeer::GetBoundingRectWrapped()
{
auto rect = GetBoundingRectangle();
return {
gsl::narrow<LONG>(rect.X),
gsl::narrow<LONG>(rect.Y),
gsl::narrow<LONG>(rect.X + rect.Width),
gsl::narrow<LONG>(rect.Y + rect.Height)
};
}

// Method Description:
// - extracts the UiaTextRanges from the SAFEARRAY and converts them to Xaml ITextRangeProviders
// Arguments:
// - SAFEARRAY of UIA::UiaTextRange (ITextRangeProviders)
// Return Value:
// - com_array of Xaml Wrapped UiaTextRange (ITextRangeProviders)
winrt::com_array<XamlAutomation::ITextRangeProvider> TermControlAutomationPeer::WrapArrayOfTextRangeProviders(SAFEARRAY* textRanges)
{
// transfer ownership of UiaTextRanges to this new vector
auto providers = SafeArrayToOwningVector<::Microsoft::Console::Types::UiaTextRange>(textRanges);
int count = providers.size();

std::vector<XamlAutomation::ITextRangeProvider> vec;
vec.reserve(count);
auto parentProvider = this->ProviderFromPeer(*this);
for (int i = 0; i < count; i++)
{
auto xutr = winrt::make_self<XamlUiaTextRange>(providers[i].detach(), parentProvider);
vec.emplace_back(xutr.as<XamlAutomation::ITextRangeProvider>());
}

winrt::com_array<XamlAutomation::ITextRangeProvider> result{ vec };

return result;
}
}
Loading

0 comments on commit a08666b

Please sign in to comment.