terminal/src/types/UiaTextRangeBase.cpp
msftbot[bot] 267deaaf70
Improve Word Navigation/Selection Performance (#4797)
## Summary of the Pull Request
1) Improves the performance of word-recognition operations such as word
   navigation in UIA and selection.

2) Fixes a bug where attempting to find the next word in UIA, when none
   exists, would hang

3) TraceLogging code only runs when somebody is listening

## Detailed Description of the Pull Request / Additional comments
- The concept of a delimiter class got moved to the CharRow.
- The buffer iterator used to save a lot more information than we needed
- I missed updating a tracing function after making GetSelection return
  one text range. That is fixed now.


## Validation Steps Performed
Performed Word Navigation under Narrator and NVDA.
NOTE: The release build should be used when testing to optimize
performance

Closes #4703
2020-03-04 23:10:10 +00:00

1246 lines
42 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "UiaTextRangeBase.hpp"
#include "ScreenInfoUiaProviderBase.h"
#include "..\buffer\out\search.h"
#include "UiaTracing.h"
using namespace Microsoft::Console::Types;
IdType UiaTextRangeBase::id = 1;
// 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
try
{
RETURN_HR_IF_NULL(E_INVALIDARG, pProvider);
RETURN_HR_IF_NULL(E_INVALIDARG, pData);
_pProvider = pProvider;
_pData = pData;
_start = pData->GetViewport().Origin();
_end = pData->GetViewport().Origin();
_wordDelimiters = wordDelimiters;
_id = id;
++id;
UiaTracing::TextRange::Constructor(*this);
return S_OK;
}
CATCH_RETURN();
#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_ const Cursor& cursor,
_In_ std::wstring_view wordDelimiters) noexcept
try
{
RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters));
_start = cursor.GetPosition();
_end = _start;
UiaTracing::TextRange::Constructor(*this);
return S_OK;
}
CATCH_RETURN();
#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_ const COORD start,
_In_ const COORD end,
_In_ std::wstring_view wordDelimiters) noexcept
try
{
RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters));
// start is before/at end, so this is valid
if (_pData->GetTextBuffer().GetSize().CompareInBounds(start, end, true) <= 0)
{
_start = start;
_end = end;
}
else
{
// start is after end, so we need to flip our concept of start/end
_start = end;
_end = start;
}
UiaTracing::TextRange::Constructor(*this);
return S_OK;
}
CATCH_RETURN();
void UiaTextRangeBase::Initialize(_In_ const UiaPoint point)
{
POINT clientPoint;
clientPoint.x = static_cast<LONG>(point.x);
clientPoint.y = static_cast<LONG>(point.y);
// get row that point resides in
const RECT windowRect = _getTerminalRect();
const SMALL_RECT viewport = _pData->GetViewport().ToInclusive();
short row = 0;
if (clientPoint.y <= windowRect.top)
{
row = viewport.Top;
}
else if (clientPoint.y >= windowRect.bottom)
{
row = viewport.Bottom;
}
else
{
// change point coords to pixels relative to window
_TranslatePointFromScreen(&clientPoint);
const COORD currentFontSize = _getScreenFontSize();
row = gsl::narrow<SHORT>(clientPoint.y / static_cast<LONG>(currentFontSize.Y)) + viewport.Top;
}
_start = { 0, row };
_end = _start;
}
#pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize
HRESULT UiaTextRangeBase::RuntimeClassInitialize(const UiaTextRangeBase& a) noexcept
try
{
_pProvider = a._pProvider;
_start = a._start;
_end = a._end;
_pData = a._pData;
_wordDelimiters = a._wordDelimiters;
_id = id;
++id;
UiaTracing::TextRange::Constructor(*this);
return S_OK;
}
CATCH_RETURN();
const IdType UiaTextRangeBase::GetId() const noexcept
{
return _id;
}
const COORD UiaTextRangeBase::GetEndpoint(TextPatternRangeEndpoint endpoint) const noexcept
{
switch (endpoint)
{
case TextPatternRangeEndpoint_End:
return _end;
case TextPatternRangeEndpoint_Start:
default:
return _start;
}
}
// Routine Description:
// - sets the target endpoint to the given COORD value
// - if the target endpoint crosses the other endpoint, become a degenerate range
// Arguments:
// - endpoint - the target endpoint (start or end)
// - val - the value that it will be set to
// Return Value:
// - true if range is degenerate, false otherwise.
bool UiaTextRangeBase::SetEndpoint(TextPatternRangeEndpoint endpoint, const COORD val) noexcept
{
const auto bufferSize = _getBufferSize();
switch (endpoint)
{
case TextPatternRangeEndpoint_End:
_end = val;
// if end is before start...
if (bufferSize.CompareInBounds(_end, _start, true) < 0)
{
// make this range degenerate at end
_start = _end;
}
break;
case TextPatternRangeEndpoint_Start:
_start = val;
// if start is after end...
if (bufferSize.CompareInBounds(_start, _end, true) > 0)
{
// make this range degenerate at start
_end = _start;
}
break;
default:
break;
}
return IsDegenerate();
}
// Routine Description:
// - returns true if the range is currently degenerate (empty range).
// Arguments:
// - <none>
// Return Value:
// - true if range is degenerate, false otherwise.
const bool UiaTextRangeBase::IsDegenerate() const noexcept
{
return _start == _end;
}
#pragma region ITextRangeProvider
IFACEMETHODIMP UiaTextRangeBase::Compare(_In_opt_ ITextRangeProvider* pRange, _Out_ BOOL* pRetVal) noexcept
{
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
*pRetVal = FALSE;
const UiaTextRangeBase* other = static_cast<UiaTextRangeBase*>(pRange);
if (other)
{
*pRetVal = (_start == other->GetEndpoint(TextPatternRangeEndpoint_Start) &&
_end == other->GetEndpoint(TextPatternRangeEndpoint_End));
}
UiaTracing::TextRange::Compare(*this, *other, *pRetVal);
return S_OK;
}
IFACEMETHODIMP UiaTextRangeBase::CompareEndpoints(_In_ TextPatternRangeEndpoint endpoint,
_In_ ITextRangeProvider* pTargetRange,
_In_ TextPatternRangeEndpoint targetEndpoint,
_Out_ int* pRetVal) noexcept
try
{
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
*pRetVal = 0;
// get the text range that we're comparing to
const UiaTextRangeBase* range = static_cast<UiaTextRangeBase*>(pTargetRange);
if (range == nullptr)
{
return E_INVALIDARG;
}
// get endpoint value that we're comparing to
const auto other = range->GetEndpoint(targetEndpoint);
// get the values of our endpoint
const auto mine = GetEndpoint(endpoint);
// compare them
*pRetVal = _pData->GetTextBuffer().GetSize().CompareInBounds(mine, other, true);
UiaTracing::TextRange::CompareEndpoints(*this, endpoint, *range, targetEndpoint, *pRetVal);
return S_OK;
}
CATCH_RETURN();
IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexcept
{
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
try
{
const auto& buffer = _pData->GetTextBuffer();
const auto bufferSize = _getBufferSize();
const auto bufferEnd = bufferSize.EndExclusive();
if (unit == TextUnit_Character)
{
_end = _start;
bufferSize.IncrementInBounds(_end, true);
}
else if (unit <= TextUnit_Word)
{
// expand to word
_start = buffer.GetWordStart(_start, _wordDelimiters, true);
_end = buffer.GetWordEnd(_start, _wordDelimiters, true);
// GetWordEnd may return the actual end of the TextBuffer.
// If so, just set it to this value of bufferEnd
if (!bufferSize.IsInBounds(_end))
{
_end = bufferEnd;
}
}
else if (unit <= TextUnit_Line)
{
// expand to line
_start.X = 0;
_end.X = 0;
_end.Y = base::ClampAdd(_start.Y, 1);
}
else
{
// expand to document
_start = bufferSize.Origin();
_end = bufferSize.EndExclusive();
}
UiaTracing::TextRange::ExpandToEnclosingUnit(unit, *this);
return S_OK;
}
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
{
UiaTracing::TextRange::FindAttribute(*this);
return E_NOTIMPL;
}
IFACEMETHODIMP UiaTextRangeBase::FindText(_In_ BSTR text,
_In_ BOOL searchBackward,
_In_ BOOL ignoreCase,
_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) noexcept
try
{
RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr);
*ppRetVal = nullptr;
const std::wstring queryText{ text, SysStringLen(text) };
const auto bufferSize = _getBufferSize();
const auto sensitivity = ignoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive;
auto searchDirection = Search::Direction::Forward;
auto searchAnchor = _start;
if (searchBackward)
{
searchDirection = Search::Direction::Backward;
// we need to convert the end to inclusive
// because Search operates with an inclusive COORD
searchAnchor = _end;
bufferSize.DecrementInBounds(searchAnchor, true);
}
Search searcher{ *_pData, queryText, searchDirection, sensitivity, searchAnchor };
if (searcher.FindNext())
{
const auto foundLocation = searcher.GetFoundLocation();
const auto start = foundLocation.first;
// we need to increment the position of end because it's exclusive
auto end = foundLocation.second;
bufferSize.IncrementInBounds(end, true);
// make sure what was found is within the bounds of the current range
if ((searchDirection == Search::Direction::Forward && bufferSize.CompareInBounds(end, _end, true) < 0) ||
(searchDirection == Search::Direction::Backward && bufferSize.CompareInBounds(start, _start) > 0))
{
RETURN_IF_FAILED(Clone(ppRetVal));
UiaTextRangeBase& range = static_cast<UiaTextRangeBase&>(**ppRetVal);
range._start = start;
range._end = end;
UiaTracing::TextRange::FindText(*this, queryText, searchBackward, ignoreCase, range);
}
}
return S_OK;
}
CATCH_RETURN();
IFACEMETHODIMP UiaTextRangeBase::GetAttributeValue(_In_ TEXTATTRIBUTEID textAttributeId,
_Out_ VARIANT* pRetVal) noexcept
{
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
if (textAttributeId == UIA_IsReadOnlyAttributeId)
{
pRetVal->vt = VT_BOOL;
pRetVal->boolVal = VARIANT_FALSE;
}
else
{
pRetVal->vt = VT_UNKNOWN;
UiaGetReservedNotSupportedValue(&pRetVal->punkVal);
}
UiaTracing::TextRange::GetAttributeValue(*this, textAttributeId, *pRetVal);
return S_OK;
}
IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept
{
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr);
*ppRetVal = nullptr;
try
{
// vector to put coords into. they go in as four doubles in the
// order: left, top, width, height. each line will have its own
// set of coords.
std::vector<double> coords;
const auto bufferSize = _getBufferSize();
// these viewport vars are converted to the buffer coordinate space
const auto viewport = bufferSize.ConvertToOrigin(_pData->GetViewport());
const auto viewportOrigin = viewport.Origin();
const auto viewportEnd = viewport.EndExclusive();
// startAnchor: the earliest COORD we will get a bounding rect for
auto startAnchor = GetEndpoint(TextPatternRangeEndpoint_Start);
if (bufferSize.CompareInBounds(startAnchor, viewportOrigin, true) < 0)
{
// earliest we can be is the origin
startAnchor = viewportOrigin;
}
// endAnchor: the latest COORD we will get a bounding rect for
auto endAnchor = GetEndpoint(TextPatternRangeEndpoint_End);
if (bufferSize.CompareInBounds(endAnchor, viewportEnd, true) > 0)
{
// latest we can be is the viewport end
endAnchor = viewportEnd;
}
// _end is exclusive, let's be inclusive so we don't have to think about it anymore for bounding rects
bufferSize.DecrementInBounds(endAnchor, true);
if (IsDegenerate() || bufferSize.CompareInBounds(_start, viewportEnd, true) > 0 || bufferSize.CompareInBounds(_end, viewportOrigin, true) < 0)
{
// An empty array is returned for a degenerate (empty) text range.
// reference: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-getboundingrectangles
// Remember, start cannot be past end, so
// if start is past the viewport end,
// or end is past the viewport origin
// draw nothing
}
else
{
for (auto row = startAnchor.Y; row <= endAnchor.Y; ++row)
{
// assume that we are going to draw the entire row
COORD startCoord = { 0, row };
COORD endCoord = { viewport.RightInclusive(), row };
if (row == startAnchor.Y)
{
// first row --> reduce left side
startCoord.X = startAnchor.X;
}
if (row == endAnchor.Y)
{
// last row --> reduce right side
endCoord.X = endAnchor.X;
}
_getBoundingRect(startCoord, endCoord, coords);
}
}
// convert to a safearray
*ppRetVal = SafeArrayCreateVector(VT_R8, 0, gsl::narrow<ULONG>(coords.size()));
if (*ppRetVal == nullptr)
{
return E_OUTOFMEMORY;
}
HRESULT hr = E_UNEXPECTED;
for (LONG i = 0; i < gsl::narrow<LONG>(coords.size()); ++i)
{
hr = SafeArrayPutElement(*ppRetVal, &i, &coords.at(i));
if (FAILED(hr))
{
SafeArrayDestroy(*ppRetVal);
*ppRetVal = nullptr;
return hr;
}
}
}
CATCH_RETURN();
UiaTracing::TextRange::GetBoundingRectangles(*this);
return S_OK;
}
IFACEMETHODIMP UiaTextRangeBase::GetEnclosingElement(_Outptr_result_maybenull_ IRawElementProviderSimple** ppRetVal) noexcept
try
{
RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr);
*ppRetVal = nullptr;
const auto hr = _pProvider->QueryInterface(IID_PPV_ARGS(ppRetVal));
UiaTracing::TextRange::GetEnclosingElement(*this);
return hr;
}
CATCH_RETURN();
IFACEMETHODIMP UiaTextRangeBase::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal) noexcept
try
{
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
*pRetVal = nullptr;
if (maxLength < -1)
{
return E_INVALIDARG;
}
const auto text = _getTextValue(maxLength);
*pRetVal = SysAllocString(text.c_str());
UiaTracing::TextRange::GetText(*this, maxLength, text);
return S_OK;
}
CATCH_RETURN();
// Method Description:
// - Helper method for GetText(). Retrieves the text that the UiaTextRange encompasses as a wstring
// Arguments:
// - maxLength - the maximum size of the retrieved text. -1 means we don't care about the size.
// Return Value:
// - the text that the UiaTextRange encompasses
#pragma warning(push)
#pragma warning(disable : 26447) // compiler isn't filtering throws inside the try/catch
std::wstring UiaTextRangeBase::_getTextValue(int maxLength) const noexcept
try
{
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
if (IsDegenerate())
{
return {};
}
std::wstring result{};
// the caller must pass in a value for the max length of the text
// to retrieve. a value of -1 means they don't want the text
// truncated.
const bool getPartialText = maxLength != -1;
// if _end is at 0, we ignore that row because _end is exclusive
const auto& buffer = _pData->GetTextBuffer();
const short totalRowsInRange = (_end.X == buffer.GetSize().Left()) ?
base::ClampSub(_end.Y, _start.Y) :
base::ClampAdd(base::ClampSub(_end.Y, _start.Y), base::ClampedNumeric<short>(1));
const short lastRowInRange = _start.Y + totalRowsInRange - 1;
short currentScreenInfoRow = 0;
for (short i = 0; i < totalRowsInRange; ++i)
{
currentScreenInfoRow = _start.Y + i;
const ROW& row = buffer.GetRowByOffset(currentScreenInfoRow);
if (row.GetCharRow().ContainsText())
{
const size_t rowRight = row.GetCharRow().MeasureRight();
size_t startIndex = 0;
size_t endIndex = rowRight;
if (currentScreenInfoRow == _start.Y)
{
startIndex = _start.X;
}
if (currentScreenInfoRow == _end.Y)
{
// prevent the end from going past the last non-whitespace char in the row
endIndex = std::max<size_t>(startIndex + 1, std::min(gsl::narrow_cast<size_t>(_end.X), rowRight));
}
// if startIndex >= endIndex then _start is
// further to the right than the last
// non-whitespace char in the row so there
// wouldn't be any text to grab.
if (startIndex < endIndex)
{
result += row.GetText().substr(startIndex, endIndex - startIndex);
}
}
if (currentScreenInfoRow != lastRowInRange)
{
result += L"\r\n";
}
if (getPartialText && result.size() > static_cast<size_t>(maxLength))
{
result.resize(maxLength);
break;
}
}
return result;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return {};
}
#pragma warning(pop)
IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit,
_In_ int count,
_Out_ int* pRetVal) noexcept
{
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
*pRetVal = 0;
if (count == 0)
{
return S_OK;
}
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
// We can abstract this movement by moving _start, but disallowing moving to the end of the buffer
constexpr auto endpoint = TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start;
constexpr auto preventBufferEnd = true;
try
{
if (unit == TextUnit::TextUnit_Character)
{
_moveEndpointByUnitCharacter(count, endpoint, pRetVal, preventBufferEnd);
}
else if (unit <= TextUnit::TextUnit_Word)
{
_moveEndpointByUnitWord(count, endpoint, pRetVal, preventBufferEnd);
}
else if (unit <= TextUnit::TextUnit_Line)
{
_moveEndpointByUnitLine(count, endpoint, pRetVal, preventBufferEnd);
}
else if (unit <= TextUnit::TextUnit_Document)
{
_moveEndpointByUnitDocument(count, endpoint, pRetVal, preventBufferEnd);
}
}
CATCH_RETURN();
// If we actually moved...
if (*pRetVal != 0)
{
// then just expand to get our _end
ExpandToEnclosingUnit(unit);
}
UiaTracing::TextRange::Move(unit, count, *pRetVal, *this);
return S_OK;
}
IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoint endpoint,
_In_ TextUnit unit,
_In_ int count,
_Out_ int* pRetVal) noexcept
{
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
*pRetVal = 0;
if (count == 0)
{
return S_OK;
}
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
try
{
if (unit == TextUnit::TextUnit_Character)
{
_moveEndpointByUnitCharacter(count, endpoint, pRetVal);
}
else if (unit <= TextUnit::TextUnit_Word)
{
_moveEndpointByUnitWord(count, endpoint, pRetVal);
}
else if (unit <= TextUnit::TextUnit_Line)
{
_moveEndpointByUnitLine(count, endpoint, pRetVal);
}
else if (unit <= TextUnit::TextUnit_Document)
{
_moveEndpointByUnitDocument(count, endpoint, pRetVal);
}
}
CATCH_RETURN();
UiaTracing::TextRange::MoveEndpointByUnit(endpoint, unit, count, *pRetVal, *this);
return S_OK;
}
IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByRange(_In_ TextPatternRangeEndpoint endpoint,
_In_ ITextRangeProvider* pTargetRange,
_In_ TextPatternRangeEndpoint targetEndpoint) noexcept
try
{
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
const UiaTextRangeBase* range = static_cast<UiaTextRangeBase*>(pTargetRange);
if (range == nullptr)
{
return E_INVALIDARG;
}
SetEndpoint(endpoint, range->GetEndpoint(targetEndpoint));
UiaTracing::TextRange::MoveEndpointByRange(endpoint, *range, targetEndpoint, *this);
return S_OK;
}
CATCH_RETURN();
IFACEMETHODIMP UiaTextRangeBase::Select() noexcept
try
{
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
if (IsDegenerate())
{
// calling Select on a degenerate range should clear any current selections
_pData->ClearSelection();
}
else
{
auto temp = _end;
_pData->GetTextBuffer().GetSize().DecrementInBounds(temp);
_pData->SelectNewRegion(_start, temp);
}
UiaTracing::TextRange::Select(*this);
return S_OK;
}
CATCH_RETURN();
// we don't support this
IFACEMETHODIMP UiaTextRangeBase::AddToSelection() noexcept
{
UiaTracing::TextRange::AddToSelection(*this);
return E_NOTIMPL;
}
// we don't support this
IFACEMETHODIMP UiaTextRangeBase::RemoveFromSelection() noexcept
{
UiaTracing::TextRange::RemoveFromSelection(*this);
return E_NOTIMPL;
}
IFACEMETHODIMP UiaTextRangeBase::ScrollIntoView(_In_ BOOL alignToTop) noexcept
try
{
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
const auto oldViewport = _pData->GetViewport().ToInclusive();
const auto viewportHeight = _getViewportHeight(oldViewport);
// range rows
const base::ClampedNumeric<short> startScreenInfoRow = _start.Y;
const base::ClampedNumeric<short> endScreenInfoRow = _end.Y;
// screen buffer rows
const base::ClampedNumeric<short> topRow = 0;
const base::ClampedNumeric<short> bottomRow = _pData->GetTextBuffer().TotalRowCount() - 1;
SMALL_RECT newViewport = oldViewport;
// there's a bunch of +1/-1s here for setting the viewport. These
// are to account for the inclusivity of the viewport boundaries.
if (alignToTop)
{
// determine if we can align the start row to the top
if (startScreenInfoRow + viewportHeight <= bottomRow)
{
// we can align to the top
newViewport.Top = startScreenInfoRow;
newViewport.Bottom = startScreenInfoRow + viewportHeight - 1;
}
else
{
// we can align to the top so we'll just move the viewport
// to the bottom of the screen buffer
newViewport.Bottom = bottomRow;
newViewport.Top = bottomRow - viewportHeight + 1;
}
}
else
{
// we need to align to the bottom
// check if we can align to the bottom
if (static_cast<unsigned int>(endScreenInfoRow) >= viewportHeight)
{
// we can align to bottom
newViewport.Bottom = endScreenInfoRow;
newViewport.Top = endScreenInfoRow - viewportHeight + 1;
}
else
{
// we can't align to bottom so we'll move the viewport to
// the top of the screen buffer
newViewport.Top = topRow;
newViewport.Bottom = topRow + viewportHeight - 1;
}
}
FAIL_FAST_IF(!(newViewport.Top >= topRow));
FAIL_FAST_IF(!(newViewport.Bottom <= bottomRow));
FAIL_FAST_IF(!(_getViewportHeight(oldViewport) == _getViewportHeight(newViewport)));
_ChangeViewport(newViewport);
UiaTracing::TextRange::ScrollIntoView(alignToTop, *this);
return S_OK;
}
CATCH_RETURN();
IFACEMETHODIMP UiaTextRangeBase::GetChildren(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept
{
// TODO GitHub #1914: Re-attach Tracing to UIA Tree
//Tracing::s_TraceUia(this, ApiCall::GetChildren, nullptr);
RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr);
// we don't have any children
*ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 0);
if (*ppRetVal == nullptr)
{
return E_OUTOFMEMORY;
}
UiaTracing::TextRange::GetChildren(*this);
return S_OK;
}
#pragma endregion
const COORD UiaTextRangeBase::_getScreenFontSize() const
{
COORD coordRet = _pData->GetFontInfo().GetSize();
// For sanity's sake, make sure not to leak 0 out as a possible value. These values are used in division operations.
coordRet.X = std::max(coordRet.X, 1i16);
coordRet.Y = std::max(coordRet.Y, 1i16);
return coordRet;
}
// Routine Description:
// - Gets the viewport height, measured in char rows.
// Arguments:
// - viewport - The viewport to measure
// Return Value:
// - The viewport height
const unsigned int UiaTextRangeBase::_getViewportHeight(const SMALL_RECT viewport) const noexcept
{
FAIL_FAST_IF(!(viewport.Bottom >= viewport.Top));
// + 1 because COORD is inclusive on both sides so subtracting top
// and bottom gets rid of 1 more then it should.
return viewport.Bottom - viewport.Top + 1;
}
// Routine Description:
// - Gets a viewport representing where valid text may be in the TextBuffer
// - Use this instead of `textBuffer.GetSize()`. This improves performance
// because it's a smaller space to have to search through
// Arguments:
// - <none>
// Return Value:
// - A viewport representing the portion of the TextBuffer that has valid text
const Viewport UiaTextRangeBase::_getBufferSize() const noexcept
{
// we need to add 1 to the X/Y of textBufferEnd
// because we want the returned viewport to include this COORD
const auto textBufferEnd = _pData->GetTextBufferEndPosition();
const auto width = base::ClampAdd<short>(1, textBufferEnd.X);
const auto height = base::ClampAdd<short>(1, textBufferEnd.Y);
return Viewport::FromDimensions({ 0, 0 }, width, height);
}
// Routine Description:
// - adds the relevant coordinate points from the row to coords.
// - it is assumed that startAnchor and endAnchor are within the same row
// and NOT DEGENERATE
// Arguments:
// - startAnchor - the start anchor of interested data within the viewport. In text buffer coordinate space. Inclusive.
// - endAnchor - the end anchor of interested data within the viewport. In text buffer coordinate space. Inclusive
// - coords - vector to add the calculated coords to
// Return Value:
// - <none>
void UiaTextRangeBase::_getBoundingRect(_In_ const COORD startAnchor, _In_ const COORD endAnchor, _Inout_ std::vector<double>& coords) const
{
FAIL_FAST_IF(startAnchor.Y != endAnchor.Y);
FAIL_FAST_IF(startAnchor.X > endAnchor.X);
const auto viewport = _pData->GetViewport();
const auto currentFontSize = _getScreenFontSize();
POINT topLeft{ 0 };
POINT bottomRight{ 0 };
// startAnchor is converted to the viewport coordinate space
#pragma warning(suppress : 26496) // analysis can't see this, TODO GH: 4015 to improve Viewport to be less bad because it'd go away if ConvertToOrigin returned instead of inout'd.
auto startCoord = startAnchor;
viewport.ConvertToOrigin(&startCoord);
// we want to clamp to a long (output type), not a short (input type)
// so we need to explicitly say <long,long>
topLeft.x = base::ClampMul<long, long>(startCoord.X, currentFontSize.X);
topLeft.y = base::ClampMul<long, long>(startCoord.Y, currentFontSize.Y);
// endAnchor is converted to the viewport coordinate space
#pragma warning(suppress : 26496) // analysis can't see this, TODO GH: 4015 to improve Viewport to be less bad because it'd go away if ConvertToOrigin returned instead of inout'd.
auto endCoord = endAnchor;
viewport.ConvertToOrigin(&endCoord);
bottomRight.x = base::ClampMul<long, long>(base::ClampAdd(endCoord.X, 1), currentFontSize.X);
bottomRight.y = base::ClampMul<long, long>(base::ClampAdd(endCoord.Y, 1), currentFontSize.Y);
// convert the coords to be relative to the screen instead of
// the client window
_TranslatePointToScreen(&topLeft);
_TranslatePointToScreen(&bottomRight);
const long width = base::ClampSub(bottomRight.x, topLeft.x);
const long height = base::ClampSub(bottomRight.y, topLeft.y);
// insert the coords
coords.push_back(topLeft.x);
coords.push_back(topLeft.y);
coords.push_back(width);
coords.push_back(height);
}
// Routine Description:
// - moves the UTR's endpoint by moveCount times by character.
// - if endpoints crossed, the degenerate range is created and both endpoints are moved
// Arguments:
// - moveCount - the number of times to move
// - endpoint - the endpoint to move
// - pAmountMoved - the number of times that the return values are "moved"
// - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer
// This is used for general movement, where you are not allowed to
// create a degenerate range
// Return Value:
// - <none>
void UiaTextRangeBase::_moveEndpointByUnitCharacter(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd) noexcept
{
*pAmountMoved = 0;
if (moveCount == 0)
{
return;
}
const bool allowBottomExclusive = !preventBufferEnd;
const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward;
const auto bufferSize = _getBufferSize();
bool success = true;
auto target = GetEndpoint(endpoint);
while (std::abs(*pAmountMoved) < std::abs(moveCount) && success)
{
switch (moveDirection)
{
case MovementDirection::Forward:
success = bufferSize.IncrementInBounds(target, allowBottomExclusive);
if (success)
{
(*pAmountMoved)++;
}
break;
case MovementDirection::Backward:
success = bufferSize.DecrementInBounds(target, allowBottomExclusive);
if (success)
{
(*pAmountMoved)--;
}
break;
default:
break;
}
}
SetEndpoint(endpoint, target);
}
// Routine Description:
// - moves the UTR's endpoint by moveCount times by word.
// - if endpoints crossed, the degenerate range is created and both endpoints are moved
// Arguments:
// - moveCount - the number of times to move
// - endpoint - the endpoint to move
// - pAmountMoved - the number of times that the return values are "moved"
// - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer
// This is used for general movement, where you are not allowed to
// create a degenerate range
// Return Value:
// - <none>
void UiaTextRangeBase::_moveEndpointByUnitWord(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd)
{
*pAmountMoved = 0;
if (moveCount == 0)
{
return;
}
const bool allowBottomExclusive = !preventBufferEnd;
const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward;
const auto& buffer = _pData->GetTextBuffer();
const auto bufferSize = _getBufferSize();
const auto bufferOrigin = bufferSize.Origin();
const auto bufferEnd = bufferSize.EndExclusive();
const auto lastCharPos = buffer.GetLastNonSpaceCharacter(bufferSize);
auto resultPos = GetEndpoint(endpoint);
auto nextPos = resultPos;
bool success = true;
while (std::abs(*pAmountMoved) < std::abs(moveCount) && success)
{
nextPos = resultPos;
switch (moveDirection)
{
case MovementDirection::Forward:
{
if (nextPos == bufferEnd)
{
success = false;
}
else if (buffer.MoveToNextWord(nextPos, _wordDelimiters, lastCharPos))
{
resultPos = nextPos;
(*pAmountMoved)++;
}
else if (allowBottomExclusive)
{
resultPos = bufferEnd;
(*pAmountMoved)++;
}
else
{
success = false;
}
break;
}
case MovementDirection::Backward:
{
if (nextPos == bufferOrigin)
{
success = false;
}
else if (buffer.MoveToPreviousWord(nextPos, _wordDelimiters))
{
resultPos = nextPos;
(*pAmountMoved)--;
}
else
{
resultPos = bufferOrigin;
}
break;
}
default:
return;
}
}
SetEndpoint(endpoint, resultPos);
}
// Routine Description:
// - moves the UTR's endpoint by moveCount times by line.
// - if endpoints crossed, the degenerate range is created and both endpoints are moved
// - a successful movement on start entails start being at Left()
// - a successful movement on end entails end being at Left() of the NEXT line
// Arguments:
// - moveCount - the number of times to move
// - endpoint - the endpoint to move
// - pAmountMoved - the number of times that the return values are "moved"
// - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer
// This is used for general movement, where you are not allowed to
// create a degenerate range
// Return Value:
// - <none>
void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd) noexcept
{
*pAmountMoved = 0;
if (moveCount == 0)
{
return;
}
const bool allowBottomExclusive = !preventBufferEnd;
const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward;
const auto bufferSize = _getBufferSize();
bool success = true;
auto resultPos = GetEndpoint(endpoint);
while (std::abs(*pAmountMoved) < std::abs(moveCount) && success)
{
auto nextPos = resultPos;
switch (moveDirection)
{
case MovementDirection::Forward:
{
// can't move past end
if (nextPos.Y >= bufferSize.BottomInclusive())
{
if (preventBufferEnd || nextPos == bufferSize.EndExclusive())
{
success = false;
break;
}
}
nextPos.X = bufferSize.RightInclusive();
success = bufferSize.IncrementInBounds(nextPos, allowBottomExclusive);
if (success)
{
resultPos = nextPos;
(*pAmountMoved)++;
}
break;
}
case MovementDirection::Backward:
{
// can't move past top
if (!allowBottomExclusive && nextPos.Y == bufferSize.Top())
{
success = false;
break;
}
// NOTE: Automatically detects if we are trying to move past origin
success = bufferSize.DecrementInBounds(nextPos, allowBottomExclusive);
if (success)
{
nextPos.X = bufferSize.Left();
resultPos = nextPos;
(*pAmountMoved)--;
}
break;
}
default:
break;
}
}
SetEndpoint(endpoint, resultPos);
}
// Routine Description:
// - moves the UTR's endpoint by moveCount times by document.
// - if endpoints crossed, the degenerate range is created and both endpoints are moved
// Arguments:
// - moveCount - the number of times to move
// - endpoint - the endpoint to move
// - pAmountMoved - the number of times that the return values are "moved"
// - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer
// This is used for general movement, where you are not allowed to
// create a degenerate range
// Return Value:
// - <none>
void UiaTextRangeBase::_moveEndpointByUnitDocument(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> const pAmountMoved,
_In_ const bool preventBufferEnd) noexcept
{
*pAmountMoved = 0;
if (moveCount == 0)
{
return;
}
const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward;
const auto bufferSize = _getBufferSize();
const auto target = GetEndpoint(endpoint);
switch (moveDirection)
{
case MovementDirection::Forward:
{
const auto documentEnd = bufferSize.EndExclusive();
if (preventBufferEnd || target == documentEnd)
{
return;
}
else
{
SetEndpoint(endpoint, documentEnd);
(*pAmountMoved)++;
}
break;
}
case MovementDirection::Backward:
{
const auto documentBegin = bufferSize.Origin();
if (target == documentBegin)
{
return;
}
else
{
SetEndpoint(endpoint, documentBegin);
(*pAmountMoved)--;
}
break;
}
default:
break;
}
}
RECT UiaTextRangeBase::_getTerminalRect() const
{
UiaRect result{ 0 };
IRawElementProviderFragment* pRawElementProviderFragment;
THROW_IF_FAILED(_pProvider->QueryInterface<IRawElementProviderFragment>(&pRawElementProviderFragment));
if (pRawElementProviderFragment)
{
pRawElementProviderFragment->get_BoundingRectangle(&result);
}
return {
gsl::narrow<LONG>(result.left),
gsl::narrow<LONG>(result.top),
gsl::narrow<LONG>(result.left + result.width),
gsl::narrow<LONG>(result.top + result.height)
};
}