a11y: Fix bug where some characters were not spoken while moving cursor

The problem occurred when we moved the cursor to the penultimate
character of the string, because the boundary condition was wrong. It
is important to realize that the offsets are moved *between* each
character (and also before and after the whole string), just like you
would move a cursor. This means that the offsets can be in the range

  [0, len]     (closed interval)

The problem could only be reproduced with JAWS.

Pick-to: 6.6 6.5 6.2
Fixes: QTBUG-115156
Change-Id: I0c5f05fa391e6c7744ab22d71afe8904b49e89bc
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
Reviewed-by: Michael Weghorn <m.weghorn@posteo.de>
This commit is contained in:
Jan Arve Sæther 2023-11-10 11:19:11 +01:00
parent 1082038bd8
commit bc8141d286
3 changed files with 157 additions and 7 deletions

View File

@ -16,6 +16,7 @@
//
#include <unknwn.h>
#include "uiatypes_p.h"
#ifndef __IUIAutomationElement_INTERFACE_DEFINED__
@ -30,6 +31,8 @@ struct IUIAutomationFocusChangedEventHandler;
struct IUIAutomationProxyFactory;
struct IUIAutomationProxyFactoryEntry;
struct IUIAutomationProxyFactoryMapping;
struct IUIAutomationTextRangeArray;
struct IUIAutomationTextRange;
#ifndef __IAccessible_FWD_DEFINED__
#define __IAccessible_FWD_DEFINED__
struct IAccessible;
@ -225,6 +228,59 @@ __CRT_UUID_DECL(IUIAutomationTreeWalker, 0x4042c624, 0x389c, 0x4afc, 0xa6,0x30,
#endif
#endif
#ifndef __IUIAutomationTextPattern_INTERFACE_DEFINED__
#define __IUIAutomationTextPattern_INTERFACE_DEFINED__
DEFINE_GUID(IID_IUIAutomationTextPattern, 0x32eba289, 0x3583, 0x42c9, 0x9c,0x59, 0x3b, 0x6d, 0x9a, 0x1e, 0x9b, 0x6a);
MIDL_INTERFACE("32eba289-3583-42c9-9c59-3b6d9a1e9b6a")
IUIAutomationTextPattern : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE RangeFromPoint(POINT pt, __RPC__deref_out_opt IUIAutomationTextRange **range) = 0;
virtual HRESULT STDMETHODCALLTYPE RangeFromChild(__RPC__in_opt IUIAutomationElement *child, __RPC__deref_out_opt IUIAutomationTextRange **range) = 0;
virtual HRESULT STDMETHODCALLTYPE GetSelection(__RPC__deref_out_opt IUIAutomationTextRangeArray **ranges) = 0;
virtual HRESULT STDMETHODCALLTYPE GetVisibleRanges(__RPC__deref_out_opt IUIAutomationTextRangeArray **ranges) = 0;
virtual HRESULT STDMETHODCALLTYPE get_DocumentRange(__RPC__deref_out_opt IUIAutomationTextRange **range) = 0;
virtual HRESULT STDMETHODCALLTYPE get_SupportedTextSelection(__RPC__out enum SupportedTextSelection *supportedTextSelection) = 0;
};
#ifdef __CRT_UUID_DECL
__CRT_UUID_DECL(IUIAutomationTextPattern, 0x32eba289, 0x3583, 0x42c9, 0x9c,0x59, 0x3b, 0x6d, 0x9a, 0x1e, 0x9b, 0x6a)
#endif
#endif
#ifndef __IUIAutomationTextRange_INTERFACE_DEFINED__
#define __IUIAutomationTextRange_INTERFACE_DEFINED__
DEFINE_GUID(IID_IUIAutomationTextRange, 0xa543cc6a, 0xf4ae, 0x494b, 0x82,0x39, 0xc8, 0x14, 0x48, 0x11, 0x87, 0xa8);
MIDL_INTERFACE("a543cc6a-f4ae-494b-8239-c814481187a8")
IUIAutomationTextRange : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Clone(__RPC__deref_out_opt IUIAutomationTextRange **clonedRange) = 0;
virtual HRESULT STDMETHODCALLTYPE Compare(__RPC__in_opt IUIAutomationTextRange *range, __RPC__out BOOL *areSame) = 0;
virtual HRESULT STDMETHODCALLTYPE CompareEndpoints(enum TextPatternRangeEndpoint srcEndPoint, __RPC__in_opt IUIAutomationTextRange *range, enum TextPatternRangeEndpoint targetEndPoint, __RPC__out int *compValue) = 0;
virtual HRESULT STDMETHODCALLTYPE ExpandToEnclosingUnit(enum TextUnit textUnit) = 0;
virtual HRESULT STDMETHODCALLTYPE FindAttribute(TEXTATTRIBUTEID attr, VARIANT val, BOOL backward, __RPC__deref_out_opt IUIAutomationTextRange **found) = 0;
virtual HRESULT STDMETHODCALLTYPE FindText(__RPC__in BSTR text, BOOL backward, BOOL ignoreCase, __RPC__deref_out_opt IUIAutomationTextRange **found) = 0;
virtual HRESULT STDMETHODCALLTYPE GetAttributeValue(TEXTATTRIBUTEID attr, __RPC__out VARIANT *value) = 0;
virtual HRESULT STDMETHODCALLTYPE GetBoundingRectangles(__RPC__deref_out_opt SAFEARRAY * *boundingRects) = 0;
virtual HRESULT STDMETHODCALLTYPE GetEnclosingElement(__RPC__deref_out_opt IUIAutomationElement **enclosingElement) = 0;
virtual HRESULT STDMETHODCALLTYPE GetText(int maxLength, __RPC__deref_out_opt BSTR *text) = 0;
virtual HRESULT STDMETHODCALLTYPE Move(enum TextUnit unit, int count, __RPC__out int *moved) = 0;
virtual HRESULT STDMETHODCALLTYPE MoveEndpointByUnit(enum TextPatternRangeEndpoint endpoint, enum TextUnit unit, int count, __RPC__out int *moved) = 0;
virtual HRESULT STDMETHODCALLTYPE MoveEndpointByRange(enum TextPatternRangeEndpoint srcEndPoint, __RPC__in_opt IUIAutomationTextRange *range, enum TextPatternRangeEndpoint targetEndPoint) = 0;
virtual HRESULT STDMETHODCALLTYPE Select( void) = 0;
virtual HRESULT STDMETHODCALLTYPE AddToSelection( void) = 0;
virtual HRESULT STDMETHODCALLTYPE RemoveFromSelection( void) = 0;
virtual HRESULT STDMETHODCALLTYPE ScrollIntoView(BOOL alignToTop) = 0;
virtual HRESULT STDMETHODCALLTYPE GetChildren(__RPC__deref_out_opt IUIAutomationElementArray **children) = 0;
};
#ifdef __CRT_UUID_DECL
__CRT_UUID_DECL(IUIAutomationTextRange, 0xa543cc6a, 0xf4ae, 0x494b, 0x82,0x39, 0xc8, 0x14, 0x48, 0x11, 0x87, 0xa8)
#endif
#endif
DEFINE_GUID(CLSID_CUIAutomation, 0xff48dba4, 0x60ef, 0x4201, 0xaa,0x87, 0x54,0x10,0x3e,0xef,0x59,0x4e);
#endif

View File

@ -316,14 +316,14 @@ HRESULT QWindowsUiaTextRangeProvider::Move(TextUnit unit, int count, int *pRetVa
int len = textInterface->characterCount();
if (len < 1)
if (len < 1 || count == 0) // MSDN: "Zero has no effect."
return S_OK;
if (unit == TextUnit_Character) {
// Moves the start point, ensuring it lies within the bounds.
int start = qBound(0, m_startOffset + count, len - 1);
int start = qBound(0, m_startOffset + count, len);
// If range was initially empty, leaves it as is; otherwise, normalizes it to one char.
m_endOffset = (m_endOffset > m_startOffset) ? start + 1 : start;
m_endOffset = (m_endOffset > m_startOffset) ? qMin(start + 1, len) : start;
*pRetVal = start - m_startOffset; // Returns the actually moved distance.
m_startOffset = start;
} else {
@ -394,7 +394,7 @@ HRESULT QWindowsUiaTextRangeProvider::MoveEndpointByUnit(TextPatternRangeEndpoin
if (unit == TextUnit_Character) {
if (endpoint == TextPatternRangeEndpoint_Start) {
int boundedValue = qBound(0, m_startOffset + count, len - 1);
int boundedValue = qBound(0, m_startOffset + count, len);
*pRetVal = boundedValue - m_startOffset;
m_startOffset = boundedValue;
m_endOffset = qBound(m_startOffset, m_endOffset, len);

View File

@ -3966,6 +3966,7 @@ void tst_QAccessibility::bridgeTest()
// For now this is a simple test to see if the bridge is working at all.
// Ideally it should be extended to test all aspects of the bridge.
#if defined(Q_OS_WIN)
auto guard = qScopeGuard([]() { QTestAccessibility::clearEvents(); });
QWidget window;
QVBoxLayout *lay = new QVBoxLayout(&window);
@ -4103,10 +4104,105 @@ void tst_QAccessibility::bridgeTest()
QCOMPARE(controlTypeId, UIA_ButtonControlTypeId);
// Edit
hr = nodeList.at(2)->get_CurrentControlType(&controlTypeId);
IUIAutomationElement *uiaElement = nodeList.at(2);
hr = uiaElement->get_CurrentControlType(&controlTypeId);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(controlTypeId, UIA_EditControlTypeId);
// "hello world\nhow are you today?\n"
IUIAutomationTextPattern *textPattern = nullptr;
hr = uiaElement->GetCurrentPattern(UIA_TextPattern2Id, reinterpret_cast<IUnknown**>(&textPattern));
QVERIFY(SUCCEEDED(hr));
QVERIFY(textPattern);
IUIAutomationTextRange *docRange = nullptr;
hr = textPattern->get_DocumentRange(&docRange);
QVERIFY(SUCCEEDED(hr));
QVERIFY(docRange);
IUIAutomationTextRange *textRange = nullptr;
hr = docRange->Clone(&textRange);
QVERIFY(SUCCEEDED(hr));
QVERIFY(textRange);
int moved;
auto rangeText = [](IUIAutomationTextRange *textRange) {
BSTR str;
QString res = "IUIAutomationTextRange::GetText() failed";
HRESULT hr = textRange->GetText(-1, &str);
if (SUCCEEDED(hr)) {
res = QString::fromWCharArray(str);
::SysFreeString(str);
}
return res;
};
// Move start endpoint past "hello " to "world"
hr = textRange->Move(TextUnit_Character, 6, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(moved, 6);
// If the range was not empty, it should be collapsed to contain a single text unit
QCOMPARE(rangeText(textRange), QString("w"));
// Move end endpoint to end of "world"
hr = textRange->MoveEndpointByUnit(TextPatternRangeEndpoint_End, TextUnit_Character, 4, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(moved, 4);
QCOMPARE(rangeText(textRange), QString("world"));
// MSDN: "Zero has no effect". This behavior was also verified with native controls.
hr = textRange->Move(TextUnit_Character, 0, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(moved, 0);
QCOMPARE(rangeText(textRange), QString("world"));
hr = textRange->Move(TextUnit_Character, 1, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(rangeText(textRange), QString("o"));
// move as far towards the end as possible
hr = textRange->Move(TextUnit_Character, 999, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(rangeText(textRange), QString(""));
hr = textRange->MoveEndpointByUnit(TextPatternRangeEndpoint_Start, TextUnit_Character, -1, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(rangeText(textRange), QString("\n"));
// move one forward (last possible position again)
hr = textRange->Move(TextUnit_Character, 1, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(rangeText(textRange), QString(""));
hr = textRange->Move(TextUnit_Character, -7, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(moved, -7);
QCOMPARE(rangeText(textRange), QString(""));
// simulate moving cursor (empty range) towards (and past) the end
QString today(" today?\n");
for (int i = 1; i < 9; ++i) { // 9 is deliberately too much
// peek one character back
hr = textRange->MoveEndpointByUnit(TextPatternRangeEndpoint_Start, TextUnit_Character, -1, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(rangeText(textRange), today.mid(i - 1, 1));
hr = textRange->Move(TextUnit_Character, 1, &moved);
QVERIFY(SUCCEEDED(hr));
QCOMPARE(rangeText(textRange), today.mid(i, moved)); // when we cannot move further, moved will be 0
// Make the range empty again
hr = textRange->MoveEndpointByUnit(TextPatternRangeEndpoint_End, TextUnit_Character, -moved, &moved);
QVERIFY(SUCCEEDED(hr));
// advance the empty range
hr = textRange->Move(TextUnit_Character, 1, &moved);
QVERIFY(SUCCEEDED(hr));
}
docRange->Release();
textRange->Release();
textPattern->Release();
// Table
hr = nodeList.at(3)->get_CurrentControlType(&controlTypeId);
QVERIFY(SUCCEEDED(hr));
@ -4124,8 +4220,6 @@ void tst_QAccessibility::bridgeTest()
controlWalker->Release();
windowElement->Release();
automation->Release();
QTestAccessibility::clearEvents();
#endif
}