From a0e5085b49141dbbb812014d174c9c619b88ae9f Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 9 Jul 2021 16:21:35 -0700 Subject: [PATCH] Expose Text Attributes to UI Automation (#10336) ## Summary of the Pull Request This implements `GetAttributeValue` and `FindAttribute` for `UiaTextRangeBase` (the shared `ITextRangeProvider` for Conhost and Windows Terminal). This also updates `UiaTracing` to collect more useful information on these function calls. ## References #7000 - Epic [Text Attribute Identifiers](https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-textattribute-ids) [ITextRangeProvider::GetAttributeValue](https://docs.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-getattributevalue) [ITextRangeProvider::FindAttribute](https://docs.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-findattribute) ## PR Checklist * [X] Closes #2161 * [X] Tests added/passed ## Detailed Description of the Pull Request / Additional comments - `TextBuffer`: - Exposes a new `TextBufferCellIterator` that takes in an end position. This simplifies the logic drastically as we can now use this iterator to navigate through the text buffer. The iterator can also expose the position in the buffer. - `UiaTextRangeBase`: - Shared logic & helper functions: - Most of the text attributes are stored as `TextAttribute`s in the text buffer. To extract them, we generate an attribute verification function via `_getAttrVerificationFn()`, then use that to verify if a given cell has the desired attribute. - A few attributes are special (i.e. font name, font size, and "is read only"), in that they are (1) acquired differently and (2) consistent across the entire text buffer. These are handled separate from the attribute verification function. - `GetAttributeValue`: Retrieve the attribute verification of the first cell in the range. Then, verify that the entire range has that attribute by iterating through the text range. If a cell does not have that attribute, return the "reserved mixed attribute value". - `FindAttribute`: Iterate through the text range and leverage the attribute verification function to find the first contiguous range with that attribute. Then, make the end exclusive and output a `UiaTextRangeBase`. This function must be able to perform a search backwards, so we abstract the "start" and "end" into `resultFirstAnchor` and `resultSecondAnchor`, then perform post processing to output a valid `UiaTextRangeBase`. - `UiaTracing`: - `GetAttributeValue`: Log uia text range, desired attribute, resulting attribute metadata, and the type of the result. - `FindAttribute`: Log uia text range, desired attribute and attribute metadata, if we were searching backwards, the type of the result, and the resulting text range. - `AttributeType` is a nice way to understand/record if the result was either of the reserved UIA values, a normal result, or an error. - `UiaTextRangeTests`: - `GetAttributeValue`: - verify that we know which attributes we support - test each of the known text attributes (expecting 100% code coverage for `_getAttrVerificationFn()`) - `FindAttribute`: - test each of the known _special_ text attributes - test `IsItalic`. NOTE: I'm explicitly only testing one of the standard text attributes because the logic is largely the same between all of them and they leverage `_getAttrVerificationFn()`. ## Validation Steps Performed - @codeofdusk has been testing this Conhost build - Tests added for Conhost and shared implementation - Windows Terminal changes were manually verified using accessibility insights and NVDA --- src/buffer/out/textBufferCellIterator.cpp | 5 + src/buffer/out/textBufferCellIterator.hpp | 2 + src/cascadia/TerminalControl/ControlCore.cpp | 2 + .../TerminalControl/XamlUiaTextRange.cpp | 50 +- src/cascadia/TerminalCore/Terminal.cpp | 1 - src/cascadia/TerminalCore/Terminal.hpp | 8 +- .../TerminalCore/terminalrenderdata.cpp | 20 +- src/host/renderData.hpp | 3 +- .../UiaTextRangeTests.cpp | 294 ++++++++++++ src/renderer/base/FontInfoBase.cpp | 2 +- src/renderer/inc/FontInfoBase.hpp | 2 +- src/renderer/inc/IRenderData.hpp | 2 - src/types/IBaseData.h | 1 + src/types/UiaTextRangeBase.cpp | 446 +++++++++++++++++- src/types/UiaTextRangeBase.hpp | 5 + src/types/UiaTracing.cpp | 54 ++- src/types/UiaTracing.h | 15 +- 17 files changed, 866 insertions(+), 46 deletions(-) diff --git a/src/buffer/out/textBufferCellIterator.cpp b/src/buffer/out/textBufferCellIterator.cpp index 7ab512c72cd..64ed7acefc4 100644 --- a/src/buffer/out/textBufferCellIterator.cpp +++ b/src/buffer/out/textBufferCellIterator.cpp @@ -265,3 +265,8 @@ const OutputCellView* TextBufferCellIterator::operator->() const noexcept { return &_view; } + +COORD TextBufferCellIterator::Pos() const noexcept +{ + return _pos; +} diff --git a/src/buffer/out/textBufferCellIterator.hpp b/src/buffer/out/textBufferCellIterator.hpp index 8b7604bb6e4..db8a3c89ce6 100644 --- a/src/buffer/out/textBufferCellIterator.hpp +++ b/src/buffer/out/textBufferCellIterator.hpp @@ -47,6 +47,8 @@ class TextBufferCellIterator const OutputCellView& operator*() const noexcept; const OutputCellView* operator->() const noexcept; + COORD Pos() const noexcept; + protected: void _SetPos(const COORD newPos); void _GenerateView(); diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 7e9e9c7d78c..932042a8b1f 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -592,6 +592,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * _compositionScale); + _terminal->SetFontInfo(_actualFont); + // TODO: MSFT:20895307 If the font doesn't exist, this doesn't // actually fail. We need a way to gracefully fallback. _renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont); diff --git a/src/cascadia/TerminalControl/XamlUiaTextRange.cpp b/src/cascadia/TerminalControl/XamlUiaTextRange.cpp index 7fb8d3b3d74..7e5dbcf6767 100644 --- a/src/cascadia/TerminalControl/XamlUiaTextRange.cpp +++ b/src/cascadia/TerminalControl/XamlUiaTextRange.cpp @@ -5,6 +5,7 @@ #include "XamlUiaTextRange.h" #include "../types/TermControlUiaTextRange.hpp" #include +#include // the same as COR_E_NOTSUPPORTED // we don't want to import the CLR headers to get it @@ -89,12 +90,52 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Windows::Foundation::IInspectable XamlUiaTextRange::GetAttributeValue(int32_t textAttributeId) const { - // Copied functionality from Types::UiaTextRange.cpp - if (textAttributeId == UIA_IsReadOnlyAttributeId) + // Call the function off of the underlying UiaTextRange. + VARIANT result; + THROW_IF_FAILED(_uiaProvider->GetAttributeValue(textAttributeId, &result)); + + // Convert the resulting VARIANT into a format that is consumable by XAML. + switch (result.vt) + { + case VT_BSTR: + { + return box_value(result.bstrVal); + } + case VT_I4: + { + // Surprisingly, `long` is _not_ a WinRT type. + // So we have to use `int32_t` to make sure this is output properly. + // Otherwise, you'll get "Attribute does not exist" out the other end. + return box_value(result.lVal); + } + case VT_R8: + { + return box_value(result.dblVal); + } + case VT_BOOL: { - return winrt::box_value(false); + return box_value(result.boolVal); } - else + case VT_UNKNOWN: + { + // This one is particularly special. + // We might return a special value like UiaGetReservedMixedAttributeValue + // or UiaGetReservedNotSupportedValue. + // Some text attributes may return a real value, however, none of those + // are supported at this time. + // So we need to figure out what was actually intended to be returned. + + com_ptr mixedAttributeVal; + UiaGetReservedMixedAttributeValue(mixedAttributeVal.put()); + + if (result.punkVal == mixedAttributeVal.get()) + { + return Windows::UI::Xaml::DependencyProperty::UnsetValue(); + } + + [[fallthrough]]; + } + default: { // We _need_ to return XAML_E_NOT_SUPPORTED here. // Returning nullptr is an improper implementation of it being unsupported. @@ -103,6 +144,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Magically, this doesn't affect other forms of navigation... winrt::throw_hresult(XAML_E_NOT_SUPPORTED); } + } } void XamlUiaTextRange::GetBoundingRectangles(com_array& returnValue) const diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 9d1ae957009..d0dfbe604ab 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -6,7 +6,6 @@ #include "../../terminal/parser/OutputStateMachineEngine.hpp" #include "TerminalDispatch.hpp" #include "../../inc/unicode.hpp" -#include "../../inc/DefaultSettings.h" #include "../../inc/argb.h" #include "../../types/inc/utils.hpp" #include "../../types/inc/colorTable.hpp" diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index df9b1bc71ae..e1feeafd1b9 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -5,6 +5,7 @@ #include +#include "../../inc/DefaultSettings.h" #include "../../buffer/out/textBuffer.hpp" #include "../../types/inc/sgrStack.hpp" #include "../../renderer/inc/BlinkingState.hpp" @@ -68,6 +69,7 @@ class Microsoft::Terminal::Core::Terminal final : void UpdateSettings(winrt::Microsoft::Terminal::Core::ICoreSettings settings); void UpdateAppearance(const winrt::Microsoft::Terminal::Core::ICoreAppearance& appearance); + void SetFontInfo(const FontInfo& fontInfo); // Write goes through the parser void Write(std::wstring_view stringView); @@ -160,6 +162,7 @@ class Microsoft::Terminal::Core::Terminal final : COORD GetTextBufferEndPosition() const noexcept override; const TextBuffer& GetTextBuffer() noexcept override; const FontInfo& GetFontInfo() noexcept override; + std::pair GetAttributeColors(const TextAttribute& attr) const noexcept override; void LockConsole() noexcept override; void UnlockConsole() noexcept override; @@ -168,7 +171,6 @@ class Microsoft::Terminal::Core::Terminal final : #pragma region IRenderData // These methods are defined in TerminalRenderData.cpp const TextAttribute GetDefaultBrushColors() noexcept override; - std::pair GetAttributeColors(const TextAttribute& attr) const noexcept override; COORD GetCursorPosition() const noexcept override; bool IsCursorVisible() const noexcept override; bool IsCursorOn() const noexcept override; @@ -276,6 +278,10 @@ class Microsoft::Terminal::Core::Terminal final : size_t _hyperlinkPatternId; std::wstring _workingDirectory; + + // 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) // the pivot is the COORD that remains selected when you extend a selection in any direction diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 17b711db748..b2ed30f1566 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -27,23 +27,15 @@ const TextBuffer& Terminal::GetTextBuffer() noexcept return *_buffer; } -// Creating a FontInfo can technically throw (on string allocation) and this is noexcept. -// That means this will std::terminate. We could come back and make there be a default constructor -// backup to FontInfo that throws no exceptions and allocates a default FontInfo structure. -#pragma warning(push) -#pragma warning(disable : 26447) const FontInfo& Terminal::GetFontInfo() noexcept { - // TODO: 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. - // The renderer never uses any of the other members from the value returned - // by this method. - // We could very likely replace this with just an IsRasterFont method - // (which would return false) - static const FontInfo _fakeFontInfo(DEFAULT_FONT_FACE, TMPF_TRUETYPE, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false); - return _fakeFontInfo; + return _fontInfo; +} + +void Terminal::SetFontInfo(const FontInfo& fontInfo) +{ + _fontInfo = fontInfo; } -#pragma warning(pop) const TextAttribute Terminal::GetDefaultBrushColors() noexcept { diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 9e1e79b0b56..8b858c541e5 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -27,6 +27,7 @@ class RenderData final : COORD GetTextBufferEndPosition() const noexcept override; const TextBuffer& GetTextBuffer() noexcept override; const FontInfo& GetFontInfo() noexcept override; + std::pair GetAttributeColors(const TextAttribute& attr) const noexcept override; std::vector GetSelectionRects() noexcept override; @@ -37,8 +38,6 @@ class RenderData final : #pragma region IRenderData const TextAttribute GetDefaultBrushColors() noexcept override; - std::pair GetAttributeColors(const TextAttribute& attr) const noexcept override; - COORD GetCursorPosition() const noexcept override; bool IsCursorVisible() const noexcept override; bool IsCursorOn() const noexcept override; diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp index dcdbb32b3fe..5457b98537c 100644 --- a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp +++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp @@ -9,6 +9,7 @@ #include "uiaTextRange.hpp" #include "../types/ScreenInfoUiaProviderBase.h" #include "../../../buffer/out/textBuffer.hpp" +#include "../types/UiaTracing.h" using namespace WEX::Common; using namespace WEX::Logging; @@ -1347,6 +1348,299 @@ class UiaTextRangeTests } } + TEST_METHOD(GetAttributeValue) + { + Log::Comment(L"Check supported attributes"); + Microsoft::WRL::ComPtr notSupportedVal; + UiaGetReservedNotSupportedValue(¬SupportedVal); + + // Iterate over UIA's Text Attribute Identifiers + // Validate that we know which ones are (not) supported + // source: https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-textattribute-ids + for (long uiaAttributeId = UIA_AnimationStyleAttributeId; uiaAttributeId <= UIA_AfterParagraphSpacingAttributeId; ++uiaAttributeId) + { + Microsoft::WRL::ComPtr utr; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider)); + THROW_IF_FAILED(utr->ExpandToEnclosingUnit(TextUnit_Character)); + + Log::Comment(NoThrowString().Format(L"Attribute ID: %d", uiaAttributeId)); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(uiaAttributeId, &result)); + + switch (uiaAttributeId) + { + case UIA_FontNameAttributeId: + { + VERIFY_ARE_EQUAL(VT_BSTR, result.vt); + break; + } + case UIA_BackgroundColorAttributeId: + case UIA_FontWeightAttributeId: + case UIA_ForegroundColorAttributeId: + case UIA_StrikethroughStyleAttributeId: + case UIA_UnderlineStyleAttributeId: + { + VERIFY_ARE_EQUAL(VT_I4, result.vt); + break; + } + case UIA_IsItalicAttributeId: + case UIA_IsReadOnlyAttributeId: + { + VERIFY_ARE_EQUAL(VT_BOOL, result.vt); + break; + } + default: + { + // Expected: not supported + VERIFY_ARE_EQUAL(VT_UNKNOWN, result.vt); + VERIFY_ARE_EQUAL(notSupportedVal.Get(), result.punkVal); + break; + } + } + } + + // This is the text attribute we'll use to update the text buffer. + // We'll modify it, then test if the UiaTextRange can extract/interpret the data properly. + // updateBuffer() will write that text attribute to the first cell in the buffer. + TextAttribute attr; + auto updateBuffer = [&](TextAttribute outputAttr) { + _pTextBuffer->Write({ outputAttr }, { 0, 0 }); + }; + + Microsoft::WRL::ComPtr utr; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider)); + THROW_IF_FAILED(utr->ExpandToEnclosingUnit(TextUnit_Character)); + { + Log::Comment(L"Test Background"); + const auto rawBackgroundColor{ RGB(255, 0, 0) }; + attr.SetBackground(rawBackgroundColor); + updateBuffer(attr); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_BackgroundColorAttributeId, &result)); + + const COLORREF realBackgroundColor{ _pUiaData->GetAttributeColors(attr).second & 0x00ffffff }; + VERIFY_ARE_EQUAL(realBackgroundColor, static_cast(result.lVal)); + } + { + Log::Comment(L"Test Font Weight"); + attr.SetBold(true); + updateBuffer(attr); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_FontWeightAttributeId, &result)); + VERIFY_ARE_EQUAL(FW_BOLD, result.lVal); + + attr.SetBold(false); + updateBuffer(attr); + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_FontWeightAttributeId, &result)); + VERIFY_ARE_EQUAL(FW_NORMAL, result.lVal); + ; + } + { + Log::Comment(L"Test Foreground"); + const auto rawForegroundColor{ RGB(255, 0, 0) }; + attr.SetForeground(rawForegroundColor); + updateBuffer(attr); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_ForegroundColorAttributeId, &result)); + + const auto realForegroundColor{ _pUiaData->GetAttributeColors(attr).first & 0x00ffffff }; + VERIFY_ARE_EQUAL(realForegroundColor, static_cast(result.lVal)); + } + { + Log::Comment(L"Test Italic"); + attr.SetItalic(true); + updateBuffer(attr); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_IsItalicAttributeId, &result)); + VERIFY_IS_TRUE(result.boolVal); + + attr.SetItalic(false); + updateBuffer(attr); + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_IsItalicAttributeId, &result)); + VERIFY_IS_FALSE(result.boolVal); + } + { + Log::Comment(L"Test Strikethrough"); + attr.SetCrossedOut(true); + updateBuffer(attr); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_StrikethroughStyleAttributeId, &result)); + VERIFY_ARE_EQUAL(TextDecorationLineStyle_Single, result.lVal); + + attr.SetCrossedOut(false); + updateBuffer(attr); + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_StrikethroughStyleAttributeId, &result)); + VERIFY_ARE_EQUAL(TextDecorationLineStyle_None, result.lVal); + } + { + Log::Comment(L"Test Underline"); + + // Single underline + attr.SetUnderlined(true); + updateBuffer(attr); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); + VERIFY_ARE_EQUAL(TextDecorationLineStyle_Single, result.lVal); + + // Double underline (double supercedes single) + attr.SetDoublyUnderlined(true); + updateBuffer(attr); + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); + VERIFY_ARE_EQUAL(TextDecorationLineStyle_Double, result.lVal); + + // Double underline (double on its own) + attr.SetUnderlined(false); + updateBuffer(attr); + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); + VERIFY_ARE_EQUAL(TextDecorationLineStyle_Double, result.lVal); + + // No underline + attr.SetDoublyUnderlined(false); + updateBuffer(attr); + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); + VERIFY_ARE_EQUAL(TextDecorationLineStyle_None, result.lVal); + } + { + Log::Comment(L"Test Font Name (special)"); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_FontNameAttributeId, &result)); + const std::wstring actualFontName{ result.bstrVal }; + const auto expectedFontName{ _pUiaData->GetFontInfo().GetFaceName() }; + VERIFY_ARE_EQUAL(expectedFontName, actualFontName); + } + { + Log::Comment(L"Test Read Only (special)"); + VARIANT result; + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_IsReadOnlyAttributeId, &result)); + VERIFY_IS_FALSE(result.boolVal); + } + { + // "Mixed" is when the desired attribute value is inconsistent across the range. + // We'll make our life easier by setting an attribute on a character, + // but getting the attribute for the entire line. + Log::Comment(L"Test Mixed"); + VARIANT result; + THROW_IF_FAILED(utr->ExpandToEnclosingUnit(TextUnit_Line)); + + // set first cell as underlined, but second cell as not underlined + attr.SetUnderlined(true); + _pTextBuffer->Write({ attr }, { 0, 0 }); + attr.SetUnderlined(false); + _pTextBuffer->Write({ attr }, { 1, 0 }); + + VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); + + // Expected: mixed + Microsoft::WRL::ComPtr mixedVal; + THROW_IF_FAILED(UiaGetReservedMixedAttributeValue(&mixedVal)); + VERIFY_ARE_EQUAL(VT_UNKNOWN, result.vt); + VERIFY_ARE_EQUAL(mixedVal.Get(), result.punkVal); + } + } + + TEST_METHOD(FindAttribute) + { + Microsoft::WRL::ComPtr utr; + const COORD startPos{ 0, 0 }; + const COORD endPos{ 0, 2 }; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&utr, _pUiaData, &_dummyProvider, startPos, endPos)); + { + Log::Comment(L"Test Font Name (special)"); + + // Populate query with font name currently in use. + const auto fontName{ _pUiaData->GetFontInfo().GetFaceName() }; + VARIANT var{}; + var.vt = VT_BSTR; + var.bstrVal = SysAllocString(fontName.data()); + + Microsoft::WRL::ComPtr result; + VERIFY_SUCCEEDED(utr->FindAttribute(UIA_FontNameAttributeId, var, false, result.GetAddressOf())); + + // Expecting the same text range endpoints + BOOL isEqual; + THROW_IF_FAILED(utr->Compare(result.Get(), &isEqual)); + VERIFY_IS_TRUE(isEqual); + + // Now perform the same test, but searching backwards + Log::Comment(L"Test Font Name (special) - Backwards"); + Microsoft::WRL::ComPtr resultBackwards; + VERIFY_SUCCEEDED(utr->FindAttribute(UIA_FontNameAttributeId, var, true, resultBackwards.GetAddressOf())); + + // Expecting the same text range endpoints + THROW_IF_FAILED(result->Compare(resultBackwards.Get(), &isEqual)); + VERIFY_IS_TRUE(isEqual); + } + { + Log::Comment(L"Test Read Only (special)"); + + VARIANT var{}; + var.vt = VT_BOOL; + var.boolVal = false; + + Microsoft::WRL::ComPtr result; + VERIFY_SUCCEEDED(utr->FindAttribute(UIA_IsReadOnlyAttributeId, var, false, result.GetAddressOf())); + + // Expecting the same text range endpoints + BOOL isEqual; + THROW_IF_FAILED(utr->Compare(result.Get(), &isEqual)); + VERIFY_IS_TRUE(isEqual); + + // Now perform the same test, but searching backwards + Log::Comment(L"Test Read Only (special) - Backwards"); + Microsoft::WRL::ComPtr resultBackwards; + VERIFY_SUCCEEDED(utr->FindAttribute(UIA_IsReadOnlyAttributeId, var, true, resultBackwards.GetAddressOf())); + + // Expecting the same text range endpoints + THROW_IF_FAILED(result->Compare(resultBackwards.Get(), &isEqual)); + VERIFY_IS_TRUE(isEqual); + } + { + Log::Comment(L"Test IsItalic (standard attribute)"); + + // Since all of the other attributes operate very similarly, + // we're just going to pick one of them and test that. + // The "GetAttribute" tests provide code coverage for + // retrieving an attribute verification function. + // This test is intended to provide code coverage for + // finding a text range with the desired attribute. + + // Set up the buffer's attributes. + TextAttribute italicAttr; + italicAttr.SetItalic(true); + auto iter{ _pUiaData->GetTextBuffer().GetCellDataAt(startPos) }; + for (auto i = 0; i < 5; ++i) + { + _pTextBuffer->Write({ L"X", italicAttr }, iter.Pos()); + ++iter; + } + + // set the expected end (exclusive) + const auto expectedEndPos{ iter.Pos() }; + + VARIANT var{}; + var.vt = VT_BOOL; + var.boolVal = true; + + Microsoft::WRL::ComPtr result; + THROW_IF_FAILED(utr->ExpandToEnclosingUnit(TextUnit_Document)); + VERIFY_SUCCEEDED(utr->FindAttribute(UIA_IsItalicAttributeId, var, false, result.GetAddressOf())); + + Microsoft::WRL::ComPtr resultUtr{ static_cast(result.Get()) }; + VERIFY_ARE_EQUAL(startPos, resultUtr->_start); + VERIFY_ARE_EQUAL(expectedEndPos, resultUtr->_end); + + // Now perform the same test, but searching backwards + Log::Comment(L"Test IsItalic (standard attribute) - Backwards"); + Microsoft::WRL::ComPtr resultBackwards; + VERIFY_SUCCEEDED(utr->FindAttribute(UIA_IsItalicAttributeId, var, true, resultBackwards.GetAddressOf())); + + // Expecting the same text range endpoints + BOOL isEqual; + THROW_IF_FAILED(result->Compare(resultBackwards.Get(), &isEqual)); + VERIFY_IS_TRUE(isEqual); + } + } + TEST_METHOD(BlockRange) { // This test replicates GH#7960. diff --git a/src/renderer/base/FontInfoBase.cpp b/src/renderer/base/FontInfoBase.cpp index 8de26d8baf3..40891cf4940 100644 --- a/src/renderer/base/FontInfoBase.cpp +++ b/src/renderer/base/FontInfoBase.cpp @@ -61,7 +61,7 @@ unsigned int FontInfoBase::GetWeight() const return _weight; } -const std::wstring_view FontInfoBase::GetFaceName() const +const std::wstring_view FontInfoBase::GetFaceName() const noexcept { return _faceName; } diff --git a/src/renderer/inc/FontInfoBase.hpp b/src/renderer/inc/FontInfoBase.hpp index 8e3f32d8210..aa9f3cddbb6 100644 --- a/src/renderer/inc/FontInfoBase.hpp +++ b/src/renderer/inc/FontInfoBase.hpp @@ -38,7 +38,7 @@ class FontInfoBase unsigned char GetFamily() const; unsigned int GetWeight() const; - const std::wstring_view GetFaceName() const; + const std::wstring_view GetFaceName() const noexcept; unsigned int GetCodePage() const; HRESULT FillLegacyNameBuffer(gsl::span buffer) const; diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index 56ab872e217..a8c96be268a 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -48,8 +48,6 @@ namespace Microsoft::Console::Render virtual const TextAttribute GetDefaultBrushColors() noexcept = 0; - virtual std::pair GetAttributeColors(const TextAttribute& attr) const noexcept = 0; - virtual COORD GetCursorPosition() const noexcept = 0; virtual bool IsCursorVisible() const noexcept = 0; virtual bool IsCursorOn() const noexcept = 0; diff --git a/src/types/IBaseData.h b/src/types/IBaseData.h index 58804c38269..6b9f8b722f7 100644 --- a/src/types/IBaseData.h +++ b/src/types/IBaseData.h @@ -37,6 +37,7 @@ namespace Microsoft::Console::Types virtual COORD GetTextBufferEndPosition() const noexcept = 0; virtual const TextBuffer& GetTextBuffer() noexcept = 0; virtual const FontInfo& GetFontInfo() noexcept = 0; + virtual std::pair GetAttributeColors(const TextAttribute& attr) const noexcept = 0; virtual std::vector GetSelectionRects() noexcept = 0; diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index e254134f381..69f531c09cc 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -9,6 +9,12 @@ using namespace Microsoft::Console::Types; +// Foreground/Background text color doesn't care about the alpha. +static constexpr long _RemoveAlpha(COLORREF color) noexcept +{ + return color & 0x00ffffff; +} + // degenerate range constructor. #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ std::wstring_view wordDelimiters) noexcept @@ -311,15 +317,274 @@ IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexc CATCH_RETURN(); } -// we don't support this currently -IFACEMETHODIMP UiaTextRangeBase::FindAttribute(_In_ TEXTATTRIBUTEID /*textAttributeId*/, - _In_ VARIANT /*val*/, - _In_ BOOL /*searchBackward*/, - _Outptr_result_maybenull_ ITextRangeProvider** /*ppRetVal*/) noexcept +// Method Description: +// - 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: +// - 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 UiaTextRangeBase::_verifyAttr(TEXTATTRIBUTEID attributeId, VARIANT val, const TextAttribute& attr) const { - UiaTracing::TextRange::FindAttribute(*this); - return E_NOTIMPL; + // 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 + // has the desired attribute. + switch (attributeId) + { + case UIA_BackgroundColorAttributeId: + { + // Expected type: VT_I4 + THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); + + // The foreground color is stored as a COLORREF. + const auto queryBackgroundColor{ val.lVal }; + return _RemoveAlpha(_pData->GetAttributeColors(attr).second) == queryBackgroundColor; + } + case UIA_FontWeightAttributeId: + { + // Expected type: VT_I4 + THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); + + // The font weight can be any value from 0 to 900. + // The text buffer doesn't store the actual value, + // we just store "IsBold" and "IsFaint". + const auto queryFontWeight{ val.lVal }; + + if (queryFontWeight > FW_NORMAL) + { + // we're looking for a bold font weight + return attr.IsBold(); + } + else + { + // we're looking for "normal" font weight + return !attr.IsBold(); + } + } + case UIA_ForegroundColorAttributeId: + { + // Expected type: VT_I4 + THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); + + // The foreground color is stored as a COLORREF. + const auto queryForegroundColor{ val.lVal }; + return _RemoveAlpha(_pData->GetAttributeColors(attr).first) == queryForegroundColor; + } + case UIA_IsItalicAttributeId: + { + // Expected type: VT_I4 + THROW_HR_IF(E_INVALIDARG, val.vt != VT_BOOL); + + // The text is either italic or it isn't. + const auto queryIsItalic{ val.boolVal }; + return queryIsItalic ? attr.IsItalic() : !attr.IsItalic(); + } + case UIA_StrikethroughStyleAttributeId: + { + // Expected type: VT_I4 + THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); + + // The strikethrough style is stored as a TextDecorationLineStyle. + // However, The text buffer doesn't have different styles for being crossed out. + // Instead, we just store whether or not the text is crossed out. + switch (val.lVal) + { + case TextDecorationLineStyle_None: + return !attr.IsCrossedOut(); + case TextDecorationLineStyle_Single: + return attr.IsCrossedOut(); + default: + return std::nullopt; + } + } + case UIA_UnderlineStyleAttributeId: + { + // Expected type: VT_I4 + THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); + + // The underline style is stored as a TextDecorationLineStyle. + // However, The text buffer doesn't have that many different styles for being underlined. + // Instead, we only have single and double underlined. + switch (val.lVal) + { + case TextDecorationLineStyle_None: + return !attr.IsUnderlined() && !attr.IsDoublyUnderlined(); + case TextDecorationLineStyle_Double: + return attr.IsDoublyUnderlined(); + case TextDecorationLineStyle_Single: + return attr.IsUnderlined(); + default: + return std::nullopt; + } + } + default: + return std::nullopt; + } +} + +IFACEMETHODIMP UiaTextRangeBase::FindAttribute(_In_ TEXTATTRIBUTEID attributeId, + _In_ VARIANT val, + _In_ BOOL searchBackwards, + _Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) noexcept +try +{ + RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); + *ppRetVal = nullptr; + + // AttributeIDs that require special handling + switch (attributeId) + { + 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 circumstance. + + const std::wstring queryFontName{ val.bstrVal }; + if (queryFontName == _pData->GetFontInfo().GetFaceName()) + { + Clone(ppRetVal); + } + UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal)); + return S_OK; + } + case UIA_IsReadOnlyAttributeId: + { + RETURN_HR_IF(E_INVALIDARG, val.vt != VT_BOOL); + if (!val.boolVal) + { + Clone(ppRetVal); + } + UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal)); + return S_OK; + } + default: + break; + } + + // AttributeIDs that are exposed via TextAttribute + try + { + if (!_verifyAttr(attributeId, val, {}).has_value()) + { + // The AttributeID is not supported. + UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal), UiaTracing::AttributeType::Unsupported); + return E_NOTIMPL; + } + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal), UiaTracing::AttributeType::Error); + return E_INVALIDARG; + } + + // Get some useful variables + const auto& buffer{ _pData->GetTextBuffer() }; + const auto bufferSize{ buffer.GetSize() }; + const auto inclusiveEnd{ _getInclusiveEnd() }; + + // Start/End for the resulting range. + // NOTE: we store these as "first" and "second" anchor because, + // we just want to know what the inclusive range is. + // We'll do some post-processing to fix this on the way out. + std::optional resultFirstAnchor; + std::optional resultSecondAnchor; + const auto attemptUpdateAnchors = [=, &resultFirstAnchor, &resultSecondAnchor](const TextBufferCellIterator iter) { + const auto attrFound{ _verifyAttr(attributeId, val, iter->TextAttr()).value() }; + if (attrFound) + { + // 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 + { + resultSecondAnchor = iter.Pos(); + } + } + return attrFound; + }; + + // 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 searchEndInclusive{ searchBackwards ? _start : inclusiveEnd }; + auto searchEndExclusive{ searchEndInclusive }; + 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. + Viewport viewportRange{ bufferSize }; + if (_blockRange) + { + const auto originX{ std::min(_start.X, inclusiveEnd.X) }; + const auto originY{ std::min(_start.Y, inclusiveEnd.Y) }; + const auto width{ gsl::narrow_cast(std::abs(inclusiveEnd.X - _start.X + 1)) }; + const auto height{ gsl::narrow_cast(std::abs(inclusiveEnd.Y - _start.Y + 1)) }; + viewportRange = Viewport::FromDimensions({ originX, originY }, width, height); + } + auto iter{ buffer.GetCellDataAt(searchStart, viewportRange) }; + const auto iterStep{ searchBackwards ? -1 : 1 }; + for (; iter && iter.Pos() != searchEndExclusive; iter += iterStep) + { + if (!attemptUpdateAnchors(iter) && resultFirstAnchor.has_value() && resultSecondAnchor.has_value()) + { + // Exit the loop early if... + // - the cell we're looking at doesn't have the attr we're looking for + // - 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; + } + } + + // Corner case: we couldn't actually move the searchEnd to make it exclusive + // (i.e. DecrementInBounds on Origin doesn't move it) + if (searchEndInclusive == searchEndExclusive) + { + attemptUpdateAnchors(iter); + } + + // If a result was found, populate ppRetVal with the UiaTextRange + // representing the found selection anchors. + if (resultFirstAnchor.has_value() && resultSecondAnchor.has_value()) + { + RETURN_IF_FAILED(Clone(ppRetVal)); + UiaTextRangeBase& range = static_cast(**ppRetVal); + + // IMPORTANT: resultFirstAnchor and resultSecondAnchor make up an inclusive range. + range._start = searchBackwards ? *resultSecondAnchor : *resultFirstAnchor; + range._end = searchBackwards ? *resultFirstAnchor : *resultSecondAnchor; + + // We need to make the end exclusive! + // But be careful here, we might be a block range + auto exclusiveIter{ buffer.GetCellDataAt(range._end, viewportRange) }; + ++exclusiveIter; + range._end = exclusiveIter.Pos(); + } + + UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal)); + return S_OK; } +CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::FindText(_In_ BSTR text, _In_ BOOL searchBackward, @@ -373,24 +638,172 @@ try } CATCH_RETURN(); -IFACEMETHODIMP UiaTextRangeBase::GetAttributeValue(_In_ TEXTATTRIBUTEID textAttributeId, +// Method Description: +// - (1) Checks the current range for the attributeId's sub-type +// - (2) Record the attributeId's 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: +// - 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); + + switch (attributeId) + { + case UIA_BackgroundColorAttributeId: + { + pRetVal->vt = VT_I4; + pRetVal->lVal = _RemoveAlpha(_pData->GetAttributeColors(attr).second); + return true; + } + case UIA_FontWeightAttributeId: + { + // The font weight can be any value from 0 to 900. + // The text buffer doesn't store the actual value, + // we just store "IsBold" and "IsFaint". + // 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 true; + } + case UIA_ForegroundColorAttributeId: + { + pRetVal->vt = VT_I4; + pRetVal->lVal = _RemoveAlpha(_pData->GetAttributeColors(attr).first); + return true; + } + case UIA_IsItalicAttributeId: + { + pRetVal->vt = VT_BOOL; + pRetVal->boolVal = attr.IsItalic(); + return true; + } + case UIA_StrikethroughStyleAttributeId: + { + pRetVal->vt = VT_I4; + pRetVal->lVal = attr.IsCrossedOut() ? TextDecorationLineStyle_Single : TextDecorationLineStyle_None; + return true; + } + case UIA_UnderlineStyleAttributeId: + { + pRetVal->vt = VT_I4; + if (attr.IsDoublyUnderlined()) + { + pRetVal->lVal = TextDecorationLineStyle_Double; + } + else if (attr.IsUnderlined()) + { + pRetVal->lVal = TextDecorationLineStyle_Single; + } + else + { + pRetVal->lVal = TextDecorationLineStyle_None; + } + return true; + } + default: + // This attribute is not supported. + pRetVal->vt = VT_UNKNOWN; + UiaGetReservedNotSupportedValue(&pRetVal->punkVal); + return false; + } +} + +IFACEMETHODIMP UiaTextRangeBase::GetAttributeValue(_In_ TEXTATTRIBUTEID attributeId, _Out_ VARIANT* pRetVal) noexcept +try { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); + VariantInit(pRetVal); - if (textAttributeId == UIA_IsReadOnlyAttributeId) + // AttributeIDs that require special handling + switch (attributeId) + { + case UIA_FontNameAttributeId: + { + pRetVal->vt = VT_BSTR; + pRetVal->bstrVal = SysAllocString(_pData->GetFontInfo().GetFaceName().data()); + UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal); + return S_OK; + } + case UIA_IsReadOnlyAttributeId: { pRetVal->vt = VT_BOOL; pRetVal->boolVal = VARIANT_FALSE; + UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal); + return S_OK; } - else + default: + break; + } + + // AttributeIDs that are exposed via TextAttribute + try { - pRetVal->vt = VT_UNKNOWN; - UiaGetReservedNotSupportedValue(&pRetVal->punkVal); + // 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; + } } - UiaTracing::TextRange::GetAttributeValue(*this, textAttributeId, *pRetVal); + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal, UiaTracing::AttributeType::Error); + return E_INVALIDARG; + } + + // Get some useful variables + const auto& buffer{ _pData->GetTextBuffer() }; + const auto bufferSize{ buffer.GetSize() }; + const auto inclusiveEnd{ _getInclusiveEnd() }; + + // Check if the entire text range has that text attribute + Viewport viewportRange{ bufferSize }; + if (_blockRange) + { + const auto originX{ std::min(_start.X, inclusiveEnd.X) }; + const auto originY{ std::min(_start.Y, inclusiveEnd.Y) }; + const auto width{ gsl::narrow_cast(std::abs(inclusiveEnd.X - _start.X + 1)) }; + const auto height{ gsl::narrow_cast(std::abs(inclusiveEnd.Y - _start.Y + 1)) }; + viewportRange = Viewport::FromDimensions({ originX, originY }, width, height); + } + auto iter{ buffer.GetCellDataAt(_start, viewportRange) }; + for (; iter && iter.Pos() != inclusiveEnd; ++iter) + { + if (!_verifyAttr(attributeId, *pRetVal, iter->TextAttr()).value()) + { + // The value of the specified attribute varies over the text range + // 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); + } + } + + UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal); return S_OK; } +CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept { @@ -1245,3 +1658,10 @@ RECT UiaTextRangeBase::_getTerminalRect() const gsl::narrow(result.top + result.height) }; } + +COORD UiaTextRangeBase::_getInclusiveEnd() noexcept +{ + auto result{ _end }; + _pData->GetTextBuffer().GetSize().DecrementInBounds(result, true); + return result; +} diff --git a/src/types/UiaTextRangeBase.hpp b/src/types/UiaTextRangeBase.hpp index d8d7fcb1de1..47b9778a0b0 100644 --- a/src/types/UiaTextRangeBase.hpp +++ b/src/types/UiaTextRangeBase.hpp @@ -177,6 +177,11 @@ namespace Microsoft::Console::Types gsl::not_null const pAmountMoved, _In_ const bool preventBufferEnd = false) noexcept; + std::optional _verifyAttr(TEXTATTRIBUTEID attributeId, VARIANT val, const TextAttribute& attr) const; + bool _initializeAttrQuery(TEXTATTRIBUTEID attributeId, VARIANT* pRetVal, const TextAttribute& attr) const; + + COORD _getInclusiveEnd() noexcept; + #ifdef UNIT_TESTING friend class ::UiaTextRangeTests; #endif diff --git a/src/types/UiaTracing.cpp b/src/types/UiaTracing.cpp index efee217d0b9..8c8a1c0d315 100644 --- a/src/types/UiaTracing.cpp +++ b/src/types/UiaTracing.cpp @@ -126,6 +126,44 @@ inline std::wstring UiaTracing::_getValue(const TextUnit unit) noexcept } } +inline std::wstring UiaTracing::_getValue(const VARIANT val) noexcept +{ + // This is not a comprehensive conversion of VARIANT result to string + // We're only including the one's we need at this time. + switch (val.vt) + { + case VT_BSTR: + return val.bstrVal; + case VT_R8: + return std::to_wstring(val.dblVal); + case VT_BOOL: + return std::to_wstring(val.boolVal); + case VT_I4: + return std::to_wstring(val.lVal); + case VT_UNKNOWN: + default: + { + return L"unknown"; + } + } +} + +inline std::wstring UiaTracing::_getValue(const AttributeType attrType) noexcept +{ + switch (attrType) + { + case AttributeType::Mixed: + return L"Mixed"; + case AttributeType::Unsupported: + return L"Unsupported"; + case AttributeType::Error: + return L"Error"; + case AttributeType::Standard: + default: + return L"Standard"; + } +} + void UiaTracing::TextRange::Constructor(UiaTextRangeBase& result) noexcept { EnsureRegistration(); @@ -206,15 +244,20 @@ void UiaTracing::TextRange::ExpandToEnclosingUnit(TextUnit unit, const UiaTextRa } } -void UiaTracing::TextRange::FindAttribute(const UiaTextRangeBase& utr) noexcept +void UiaTracing::TextRange::FindAttribute(const UiaTextRangeBase& utr, TEXTATTRIBUTEID id, VARIANT val, BOOL searchBackwards, const UiaTextRangeBase& result, AttributeType attrType) noexcept { EnsureRegistration(); if (TraceLoggingProviderEnabled(g_UiaProviderTraceProvider, WINEVENT_LEVEL_VERBOSE, TIL_KEYWORD_TRACE)) { TraceLoggingWrite( g_UiaProviderTraceProvider, - "UiaTextRange::FindAttribute (UNSUPPORTED)", + "UiaTextRange::FindAttribute", TraceLoggingValue(_getValue(utr).c_str(), "base"), + TraceLoggingValue(id, "text attribute ID"), + TraceLoggingValue(_getValue(val).c_str(), "text attribute sub-data"), + TraceLoggingValue(searchBackwards ? L"true" : L"false", "search backwards"), + TraceLoggingValue(_getValue(attrType).c_str(), "attribute type"), + TraceLoggingValue(_getValue(result).c_str(), "result"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } @@ -238,7 +281,7 @@ void UiaTracing::TextRange::FindText(const UiaTextRangeBase& base, std::wstring } } -void UiaTracing::TextRange::GetAttributeValue(const UiaTextRangeBase& base, TEXTATTRIBUTEID id, VARIANT result) noexcept +void UiaTracing::TextRange::GetAttributeValue(const UiaTextRangeBase& base, TEXTATTRIBUTEID id, VARIANT result, AttributeType attrType) noexcept { EnsureRegistration(); if (TraceLoggingProviderEnabled(g_UiaProviderTraceProvider, WINEVENT_LEVEL_VERBOSE, TIL_KEYWORD_TRACE)) @@ -247,8 +290,9 @@ void UiaTracing::TextRange::GetAttributeValue(const UiaTextRangeBase& base, TEXT g_UiaProviderTraceProvider, "UiaTextRange::GetAttributeValue", TraceLoggingValue(_getValue(base).c_str(), "base"), - TraceLoggingValue(id, "textAttributeId"), - TraceLoggingValue(result.vt, "result (type)"), + TraceLoggingValue(id, "text attribute ID"), + TraceLoggingValue(_getValue(result).c_str(), "result"), + TraceLoggingValue(_getValue(attrType).c_str(), "attribute type"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } diff --git a/src/types/UiaTracing.h b/src/types/UiaTracing.h index 6a8c0a00885..3cb4b7e9106 100644 --- a/src/types/UiaTracing.h +++ b/src/types/UiaTracing.h @@ -29,6 +29,14 @@ namespace Microsoft::Console::Types class UiaTracing final { public: + enum class AttributeType + { + Standard, + Mixed, + Unsupported, + Error + }; + class TextRange final { public: @@ -37,9 +45,9 @@ namespace Microsoft::Console::Types static void Compare(const UiaTextRangeBase& base, const UiaTextRangeBase& other, bool result) noexcept; static void CompareEndpoints(const UiaTextRangeBase& base, const TextPatternRangeEndpoint endpoint, const UiaTextRangeBase& other, TextPatternRangeEndpoint otherEndpoint, int result) noexcept; static void ExpandToEnclosingUnit(TextUnit unit, const UiaTextRangeBase& result) noexcept; - static void FindAttribute(const UiaTextRangeBase& base) noexcept; + static void FindAttribute(const UiaTextRangeBase& base, TEXTATTRIBUTEID attributeId, VARIANT val, BOOL searchBackwards, const UiaTextRangeBase& result, AttributeType attrType = AttributeType::Standard) noexcept; static void FindText(const UiaTextRangeBase& base, std::wstring text, bool searchBackward, bool ignoreCase, const UiaTextRangeBase& result) noexcept; - static void GetAttributeValue(const UiaTextRangeBase& base, TEXTATTRIBUTEID id, VARIANT result) noexcept; + static void GetAttributeValue(const UiaTextRangeBase& base, TEXTATTRIBUTEID id, VARIANT result, AttributeType attrType = AttributeType::Standard) noexcept; static void GetBoundingRectangles(const UiaTextRangeBase& base) noexcept; static void GetEnclosingElement(const UiaTextRangeBase& base) noexcept; static void GetText(const UiaTextRangeBase& base, int maxLength, std::wstring result) noexcept; @@ -101,6 +109,9 @@ namespace Microsoft::Console::Types static inline std::wstring _getValue(const TextPatternRangeEndpoint endpoint) noexcept; static inline std::wstring _getValue(const TextUnit unit) noexcept; + static inline std::wstring _getValue(const AttributeType attrType) noexcept; + static inline std::wstring _getValue(const VARIANT val) noexcept; + // these are used to assign IDs to new UiaTextRanges and ScreenInfoUiaProviders respectively static IdType _utrId; static IdType _siupId;