Skip to content
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

Expose Text Attributes to UI Automation #10336

Merged
10 commits merged into from
Jul 9, 2021
4 changes: 2 additions & 2 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ class Microsoft::Terminal::Core::Terminal final :

std::wstring _workingDirectory;

// This font value is only used to check if the font is a raster font.
// Otherwise, the font is changed with the renderer via TriggerFontChange.
// This default fake font value is only used to check if the font is a raster font.
// Otherwise, the font is changed to a real value with the renderer via TriggerFontChange.
FontInfo _fontInfo{ DEFAULT_FONT_FACE, TMPF_TRUETYPE, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false };
#pragma region Text Selection
// a selection is represented as a range between two COORDs (start and end)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1617,5 +1617,26 @@ class UiaTextRangeTests
THROW_IF_FAILED(result->Compare(resultBackwards.Get(), &isEqual));
VERIFY_IS_TRUE(isEqual);
}
TEST_METHOD(BlockRange)
{
// This test replicates GH#7960.
// It was caused by _blockRange being uninitialized, resulting in it occasionally being set to true.
// Additionally, all of the ctors _except_ the copy ctor initialized it. So this would be more apparent
// when calling Clone.
Microsoft::WRL::ComPtr<UiaTextRange> utr;
THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize<UiaTextRange>(&utr, _pUiaData, &_dummyProvider));
VERIFY_IS_FALSE(utr->_blockRange);

Microsoft::WRL::ComPtr<ITextRangeProvider> clone1;
THROW_IF_FAILED(utr->Clone(&clone1));

UiaTextRange* cloneUtr1 = static_cast<UiaTextRange*>(clone1.Get());
VERIFY_IS_FALSE(cloneUtr1->_blockRange);
cloneUtr1->_blockRange = true;

Microsoft::WRL::ComPtr<ITextRangeProvider> clone2;
cloneUtr1->Clone(&clone2);
UiaTextRange* cloneUtr2 = static_cast<UiaTextRange*>(clone2.Get());
VERIFY_IS_TRUE(cloneUtr2->_blockRange);
}
};
143 changes: 64 additions & 79 deletions src/types/UiaTextRangeBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,16 @@ IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexc
}

// Method Description:
// - Generate an attribute verification function for that attributeId and sub-type
// - Verify that the given attribute has the desired formatting saved in the attributeId and val
// Arguments:
// - attributeId - the UIA text attribute identifier we're looking for
// - val - the attributeId's sub-type we're looking for
// - attr - the text attribute we're checking
// Return Value:
// - a function that can be used to verify if a given TextAttribute meets the attributeId's sub-type
std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerificationFn(TEXTATTRIBUTEID attributeId, VARIANT val) const
// - true, if the given attribute has the desired formatting.
// - false, if the given attribute does not have the desired formatting.
// - nullopt, if checking for the desired formatting is not supported.
std::optional<bool> UiaTextRangeBase::_verifyAttr(TEXTATTRIBUTEID attributeId, VARIANT val, const TextAttribute& attr) const
{
// Most of the attributes we're looking for just require us to check TextAttribute.
// So if we support it, we'll return a function to verify if the TextAttribute
Expand All @@ -338,9 +341,7 @@ std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerification

// The foreground color is stored as a COLORREF.
const auto queryBackgroundColor{ val.lVal };
carlos-zamora marked this conversation as resolved.
Show resolved Hide resolved
return [this, queryBackgroundColor](const TextAttribute& attr) noexcept {
return _RemoveAlpha(_pData->GetAttributeColors(attr).second) == queryBackgroundColor;
};
return _RemoveAlpha(_pData->GetAttributeColors(attr).second) == queryBackgroundColor;
}
case UIA_FontWeightAttributeId:
{
Expand All @@ -355,16 +356,12 @@ std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerification
if (queryFontWeight > FW_NORMAL)
{
// we're looking for a bold font weight
return [](const TextAttribute& attr) noexcept {
return attr.IsBold();
};
return attr.IsBold();
}
else
{
// we're looking for "normal" font weight
return [](const TextAttribute& attr) noexcept {
return !attr.IsBold();
};
return !attr.IsBold();
}
}
case UIA_ForegroundColorAttributeId:
Expand All @@ -374,9 +371,7 @@ std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerification

// The foreground color is stored as a COLORREF.
const auto queryForegroundColor{ base::ClampedNumeric<COLORREF>(val.lVal) };
return [this, queryForegroundColor](const TextAttribute& attr) noexcept {
return _RemoveAlpha(_pData->GetAttributeColors(attr).first) == queryForegroundColor;
};
return _RemoveAlpha(_pData->GetAttributeColors(attr).first) == queryForegroundColor;
}
case UIA_IsItalicAttributeId:
{
Expand All @@ -385,18 +380,7 @@ std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerification

// The text is either italic or it isn't.
const auto queryIsItalic{ val.boolVal };
if (queryIsItalic)
{
return [](const TextAttribute& attr) noexcept {
return attr.IsItalic();
};
}
else
{
return [](const TextAttribute& attr) noexcept {
return !attr.IsItalic();
};
}
return queryIsItalic ? attr.IsItalic() : !attr.IsItalic();
}
case UIA_StrikethroughStyleAttributeId:
{
Expand All @@ -409,15 +393,11 @@ std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerification
switch (val.lVal)
{
case TextDecorationLineStyle_None:
return [](const TextAttribute& attr) noexcept {
return !attr.IsCrossedOut();
};
return !attr.IsCrossedOut();
case TextDecorationLineStyle_Single:
return [](const TextAttribute& attr) noexcept {
return attr.IsCrossedOut();
};
return attr.IsCrossedOut();
default:
return nullptr;
return std::nullopt;
}
}
case UIA_UnderlineStyleAttributeId:
Expand All @@ -431,23 +411,17 @@ std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerification
switch (val.lVal)
{
case TextDecorationLineStyle_None:
return [](const TextAttribute& attr) noexcept {
return !attr.IsUnderlined() && !attr.IsDoublyUnderlined();
};
return !attr.IsUnderlined() && !attr.IsDoublyUnderlined();
case TextDecorationLineStyle_Double:
return [](const TextAttribute& attr) noexcept {
return attr.IsDoublyUnderlined();
};
return attr.IsDoublyUnderlined();
case TextDecorationLineStyle_Single:
return [](const TextAttribute& attr) noexcept {
return attr.IsUnderlined();
};
return attr.IsUnderlined();
default:
return nullptr;
return std::nullopt;
}
}
default:
return nullptr;
return std::nullopt;
}
}

Expand All @@ -466,6 +440,9 @@ try
case UIA_FontNameAttributeId:
{
RETURN_HR_IF(E_INVALIDARG, val.vt != VT_BSTR);

// Technically, we'll truncate early if there's an embedded null in the BSTR.
// But we're probably fine in this curcumstance.
carlos-zamora marked this conversation as resolved.
Show resolved Hide resolved
const std::wstring queryFontName{ val.bstrVal };
carlos-zamora marked this conversation as resolved.
Show resolved Hide resolved
if (queryFontName == _pData->GetFontInfo().GetFaceName())
{
Expand All @@ -489,11 +466,9 @@ try
}

// AttributeIDs that are exposed via TextAttribute
std::function<bool(const TextAttribute&)> checkIfAttrFound;
try
{
checkIfAttrFound = _getAttrVerificationFn(attributeId, val);
if (!checkIfAttrFound)
if (!_verifyAttr(attributeId, val, {}).has_value())
{
// The AttributeID is not supported.
UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast<UiaTextRangeBase&>(**ppRetVal), UiaTracing::AttributeType::Unsupported);
Expand All @@ -520,8 +495,19 @@ try
std::optional<COORD> resultSecondAnchor;

// Start/End for the direction to perform the search in
// We need searchEnd to be exclusive. This allows the for-loop below to
// iterate up until the exclusive searchEnd, and not attempt to read the
// data at that position.
const auto searchStart{ searchBackwards ? inclusiveEnd : _start };
const auto searchEnd{ searchBackwards ? _start : inclusiveEnd };
auto searchEndExclusive{ searchBackwards ? _start : inclusiveEnd };
if (searchBackwards)
{
bufferSize.DecrementInBounds(searchEndExclusive, true);
}
else
{
bufferSize.IncrementInBounds(searchEndExclusive, true);
}

// Iterate from searchStart to searchEnd in the buffer.
// If we find the attribute we're looking for, we update resultFirstAnchor/SecondAnchor appropriately.
Expand All @@ -536,16 +522,16 @@ try
}
auto iter{ buffer.GetCellDataAt(searchStart, viewportRange) };
const auto iterStep{ searchBackwards ? -1 : 1 };
for (; iter && iter.Pos() != searchEnd; iter += iterStep)
for (; iter && iter.Pos() != searchEndExclusive; iter += iterStep)
{
const auto& attr{ iter->TextAttr() };
if (checkIfAttrFound(attr))
if (_verifyAttr(attributeId, val, iter->TextAttr()).value())
{
// populate the first anchor if it's not populated.
// otherwise, populate the second anchor.
if (!resultFirstAnchor.has_value())
{
resultFirstAnchor = iter.Pos();
resultSecondAnchor = iter.Pos();
}
else
{
Expand All @@ -559,6 +545,7 @@ try
// - the anchors have been populated
// This means that we've found a contiguous range where the text attribute was found.
// No point in searching through the rest of the search space.
// TLDR: keep updating the second anchor and make the range wider until the attribute changes.
break;
carlos-zamora marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand Down Expand Up @@ -641,24 +628,24 @@ CATCH_RETURN();
// Method Description:
// - (1) Checks the current range for the attributeId's sub-type
// - (2) Record the attributeId's sub-type
// - (3) Generate an attribute verification function for that attributeId sub-type
// Arguments:
// - attributeId - the UIA text attribute identifier we're looking for
// - pRetVal - the attributeId's sub-type for the first cell in the range (i.e. foreground color)
// - attr - the text attribute we're checking
// Return Value:
// - a function that can be used to verify if a given TextAttribute meets the attributeId's sub-type
std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerificationFnForFirstAttr(TEXTATTRIBUTEID attributeId, VARIANT* pRetVal) const
// - true, if the attributeId is supported. false, otherwise.
// - pRetVal is populated with the appropriate response relevant to the returned bool.
bool UiaTextRangeBase::_initializeAttrQuery(TEXTATTRIBUTEID attributeId, VARIANT* pRetVal, const TextAttribute& attr) const
{
THROW_HR_IF(E_INVALIDARG, pRetVal == nullptr);

const auto attr{ _pData->GetTextBuffer().GetCellDataAt(_start)->TextAttr() };
switch (attributeId)
{
case UIA_BackgroundColorAttributeId:
{
pRetVal->vt = VT_I4;
pRetVal->lVal = _RemoveAlpha(_pData->GetAttributeColors(attr).second);
return _getAttrVerificationFn(attributeId, *pRetVal);
return true;
}
case UIA_FontWeightAttributeId:
{
Expand All @@ -668,25 +655,25 @@ std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerification
// Source: https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-textattribute-ids
pRetVal->vt = VT_I4;
pRetVal->lVal = attr.IsBold() ? FW_BOLD : FW_NORMAL;
return _getAttrVerificationFn(attributeId, *pRetVal);
return true;
}
case UIA_ForegroundColorAttributeId:
{
pRetVal->vt = VT_I4;
pRetVal->lVal = _RemoveAlpha(_pData->GetAttributeColors(attr).first);
return _getAttrVerificationFn(attributeId, *pRetVal);
return true;
}
case UIA_IsItalicAttributeId:
{
pRetVal->vt = VT_BOOL;
pRetVal->boolVal = attr.IsItalic();
return _getAttrVerificationFn(attributeId, *pRetVal);
return true;
}
case UIA_StrikethroughStyleAttributeId:
{
pRetVal->vt = VT_I4;
pRetVal->lVal = attr.IsCrossedOut() ? TextDecorationLineStyle_Single : TextDecorationLineStyle_None;
return _getAttrVerificationFn(attributeId, *pRetVal);
return true;
}
case UIA_UnderlineStyleAttributeId:
{
Expand All @@ -703,13 +690,13 @@ std::function<bool(const TextAttribute&)> UiaTextRangeBase::_getAttrVerification
{
pRetVal->lVal = TextDecorationLineStyle_None;
}
return _getAttrVerificationFn(attributeId, *pRetVal);
return true;
}
default:
// This attribute is not supported.
pRetVal->vt = VT_UNKNOWN;
UiaGetReservedNotSupportedValue(&pRetVal->punkVal);
return nullptr;
return false;
}
}

Expand Down Expand Up @@ -742,17 +729,27 @@ try
}

// AttributeIDs that are exposed via TextAttribute
std::function<bool(const TextAttribute&)> checkIfAttrFound;
try
{
checkIfAttrFound = _getAttrVerificationFnForFirstAttr(attributeId, pRetVal);
if (!checkIfAttrFound)
// Unlike a normal text editor, which applies formatting at the caret,
// we don't know what attributes are written at a degenerate range.
// So instead, we'll use GetCurrentAttributes to get an idea of the default
// text attributes used. And return a result based off of that.
const auto attr{ IsDegenerate() ? _pData->GetTextBuffer().GetCurrentAttributes() :
_pData->GetTextBuffer().GetCellDataAt(_start)->TextAttr() };
if (!_initializeAttrQuery(attributeId, pRetVal, attr))
{
// The AttributeID is not supported.
pRetVal->vt = VT_UNKNOWN;
UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal, UiaTracing::AttributeType::Unsupported);
return UiaGetReservedNotSupportedValue(&pRetVal->punkVal);
}
else if (IsDegenerate())
{
// If we're a degenerate range, we have all the information we need.
UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal);
return S_OK;
}
}
catch (...)
{
Expand All @@ -761,17 +758,6 @@ try
return E_INVALIDARG;
}

if (IsDegenerate())
{
// Unlike a normal text editor, which applies formatting at the caret,
// we don't know what attributes are written at a degenerate range.
// So let's return UiaGetReservedMixedAttributeValue.
// Source: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-getattributevalue
pRetVal->vt = VT_UNKNOWN;
UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal, UiaTracing::AttributeType::Mixed);
return UiaGetReservedMixedAttributeValue(&pRetVal->punkVal);
}

// Get some useful variables
const auto& buffer{ _pData->GetTextBuffer() };
const auto bufferSize{ buffer.GetSize() };
Expand All @@ -790,8 +776,7 @@ try
auto iter{ buffer.GetCellDataAt(_start, viewportRange) };
for (; iter && iter.Pos() != inclusiveEnd; ++iter)
{
const auto& attr{ iter->TextAttr() };
if (!checkIfAttrFound(attr))
if (!_verifyAttr(attributeId, *pRetVal, iter->TextAttr()).value())
{
// The value of the specified attribute varies over the text range
// return UiaGetReservedMixedAttributeValue.
Expand Down
4 changes: 2 additions & 2 deletions src/types/UiaTextRangeBase.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ namespace Microsoft::Console::Types
gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd = false) noexcept;

std::function<bool(const TextAttribute&)> _getAttrVerificationFn(TEXTATTRIBUTEID attributeId, VARIANT val) const;
std::function<bool(const TextAttribute&)> _getAttrVerificationFnForFirstAttr(TEXTATTRIBUTEID attributeId, VARIANT* pRetVal) const;
std::optional<bool> _verifyAttr(TEXTATTRIBUTEID attributeId, VARIANT val, const TextAttribute& attr) const;
bool _initializeAttrQuery(TEXTATTRIBUTEID attributeId, VARIANT* pRetVal, const TextAttribute& attr) const;

COORD _getInclusiveEnd() noexcept;

Expand Down