Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwindowsuiatextrangeprovider.cpp
Go to the documentation of this file.
1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <QtGui/qtguiglobal.h>
5#if QT_CONFIG(accessibility)
6
7#include "qwindowsuiatextrangeprovider.h"
8#include "qwindowsuiamainprovider.h"
9#include "qwindowsuiautils.h"
10#include "qwindowscontext.h"
11
12#include <QtGui/qaccessible.h>
13#include <QtCore/qloggingcategory.h>
14#include <QtCore/qstring.h>
15#include <QtCore/qvarlengtharray.h>
16#include <QtCore/private/qcomvariant_p.h>
17
18QT_BEGIN_NAMESPACE
19
20using namespace QWindowsUiAutomation;
21
22
23QWindowsUiaTextRangeProvider::QWindowsUiaTextRangeProvider(QAccessible::Id id, int startOffset, int endOffset) :
24 QWindowsUiaBaseProvider(id),
25 m_startOffset(startOffset),
26 m_endOffset(endOffset)
27{
28 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this << startOffset << endOffset;
29}
30
31QWindowsUiaTextRangeProvider::~QWindowsUiaTextRangeProvider()
32{
33 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
34}
35
36HRESULT QWindowsUiaTextRangeProvider::AddToSelection()
37{
38 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
39 return Select();
40}
41
42HRESULT QWindowsUiaTextRangeProvider::Clone(ITextRangeProvider **pRetVal)
43{
44 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
45
46 if (!pRetVal)
47 return E_INVALIDARG;
48
49 *pRetVal = makeComObject<QWindowsUiaTextRangeProvider>(id(), m_startOffset, m_endOffset).Detach();
50 return S_OK;
51}
52
53// Two ranges are considered equal if their start/end points are the same.
54HRESULT QWindowsUiaTextRangeProvider::Compare(ITextRangeProvider *range, BOOL *pRetVal)
55{
56 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
57
58 if (!range || !pRetVal)
59 return E_INVALIDARG;
60
61 auto *targetProvider = static_cast<QWindowsUiaTextRangeProvider *>(range);
62 *pRetVal = ((targetProvider->m_startOffset == m_startOffset) && (targetProvider->m_endOffset == m_endOffset));
63 return S_OK;
64}
65
66// Compare different endpoinds between two providers.
67HRESULT QWindowsUiaTextRangeProvider::CompareEndpoints(TextPatternRangeEndpoint endpoint,
68 ITextRangeProvider *targetRange,
69 TextPatternRangeEndpoint targetEndpoint,
70 int *pRetVal)
71{
72 qCDebug(lcQpaUiAutomation) << __FUNCTION__
73 << "endpoint=" << endpoint << "targetRange=" << targetRange
74 << "targetEndpoint=" << targetEndpoint << "this: " << this;
75
76 if (!targetRange || !pRetVal)
77 return E_INVALIDARG;
78
79 auto *targetProvider = static_cast<QWindowsUiaTextRangeProvider *>(targetRange);
80
81 int point = (endpoint == TextPatternRangeEndpoint_Start) ? m_startOffset : m_endOffset;
82 int targetPoint = (targetEndpoint == TextPatternRangeEndpoint_Start) ?
83 targetProvider->m_startOffset : targetProvider->m_endOffset;
84 *pRetVal = point - targetPoint;
85 return S_OK;
86}
87
88// Expands/normalizes the range for a given text unit.
89HRESULT QWindowsUiaTextRangeProvider::ExpandToEnclosingUnit(TextUnit unit)
90{
91 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << "unit=" << unit << "this: " << this;
92
93 QAccessibleInterface *accessible = accessibleInterface();
94 if (!accessible)
95 return UIA_E_ELEMENTNOTAVAILABLE;
96
97 QAccessibleTextInterface *textInterface = accessible->textInterface();
98 if (!textInterface)
99 return UIA_E_ELEMENTNOTAVAILABLE;
100
101 int len = textInterface->characterCount();
102 if (len < 1) {
103 m_startOffset = 0;
104 m_endOffset = 0;
105 } else {
106 if (unit == TextUnit_Character) {
107 m_startOffset = qBound(0, m_startOffset, len - 1);
108 m_endOffset = m_startOffset + 1;
109 } else {
110 QString text = textInterface->text(0, len);
111 const int start = m_startOffset >= 0 && m_startOffset < len
112 ? m_startOffset : len - 1;
113 for (int t = start; t >= 0; --t) {
114 if (!text.isEmpty() && !isTextUnitSeparator(unit, text[t]) && ((t == 0) || isTextUnitSeparator(unit, text[t - 1]))) {
115 m_startOffset = t;
116 break;
117 }
118 }
119 for (int t = m_startOffset; t < len; ++t) {
120 if ((t == len - 1) || (isTextUnitSeparator(unit, text[t]) && ((unit == TextUnit_Word) || !isTextUnitSeparator(unit, text[t + 1])))) {
121 m_endOffset = t + 1;
122 break;
123 }
124 }
125 }
126 }
127 return S_OK;
128}
129
130// Not supported.
131HRESULT QWindowsUiaTextRangeProvider::FindAttribute(TEXTATTRIBUTEID /* attributeId */,
132 VARIANT /* val */, BOOL /* backward */,
133 ITextRangeProvider **pRetVal)
134{
135 if (!pRetVal)
136 return E_INVALIDARG;
137 *pRetVal = nullptr;
138 return S_OK;
139}
140
141// Returns the value of a given attribute.
142HRESULT STDMETHODCALLTYPE QWindowsUiaTextRangeProvider::GetAttributeValue(TEXTATTRIBUTEID attributeId,
143 VARIANT *pRetVal)
144{
145 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << "attributeId=" << attributeId << "this: " << this;
146
147 if (!pRetVal)
148 return E_INVALIDARG;
149 clearVariant(pRetVal);
150
151 QAccessibleInterface *accessible = accessibleInterface();
152 if (!accessible)
153 return UIA_E_ELEMENTNOTAVAILABLE;
154
155 QAccessibleTextInterface *textInterface = accessible->textInterface();
156 if (!textInterface)
157 return UIA_E_ELEMENTNOTAVAILABLE;
158
159 switch (attributeId) {
160 case UIA_IsReadOnlyAttributeId:
161 *pRetVal = QComVariant{ accessible->state().readOnly ? true : false }.release();
162 break;
163 case UIA_CaretPositionAttributeId:
164 if (textInterface->cursorPosition() == 0)
165 *pRetVal = QComVariant{ static_cast<long>(CaretPosition_BeginningOfLine) }.release();
166 else if (textInterface->cursorPosition() == textInterface->characterCount())
167 *pRetVal = QComVariant{ static_cast<long>(CaretPosition_EndOfLine) }.release();
168 else
169 *pRetVal = QComVariant{ static_cast<long>(CaretPosition_Unknown) }.release();
170 break;
171 case UIA_StrikethroughStyleAttributeId:
172 {
173 const QString value = valueForIA2Attribute(textInterface, QStringLiteral("text-line-through-type"));
174 if (value.isEmpty())
175 break;
176 const TextDecorationLineStyle uiaLineStyle = uiaLineStyleForIA2LineStyle(value);
177 *pRetVal = QComVariant{ static_cast<long>(uiaLineStyle) }.release();
178 break;
179 }
180 default:
181 break;
182 }
183 return S_OK;
184}
185
186// Returns an array of bounding rectangles for text lines within the range.
187HRESULT QWindowsUiaTextRangeProvider::GetBoundingRectangles(SAFEARRAY **pRetVal)
188{
189 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
190
191 if (!pRetVal)
192 return E_INVALIDARG;
193
194 QAccessibleInterface *accessible = accessibleInterface();
195 if (!accessible)
196 return UIA_E_ELEMENTNOTAVAILABLE;
197
198 QAccessibleTextInterface *textInterface = accessible->textInterface();
199 if (!textInterface)
200 return UIA_E_ELEMENTNOTAVAILABLE;
201
202 QWindow *window = windowForAccessible(accessible);
203 if (!window)
204 return UIA_E_ELEMENTNOTAVAILABLE;
205
206 int len = textInterface->characterCount();
207 QVarLengthArray<QRect> rectList;
208
209 if ((m_startOffset >= 0) && (m_endOffset <= len) && (m_startOffset < m_endOffset)) {
210 int start, end;
211 textInterface->textAtOffset(m_startOffset, QAccessible::LineBoundary, &start, &end);
212 while ((start >= 0) && (end >= 0)) {
213 int startRange = qMax(start, m_startOffset);
214 int endRange = qMin(end, m_endOffset);
215 if (startRange < endRange) {
216 // Calculates a bounding rectangle for the line and adds it to the list.
217 const QRect startRect = textInterface->characterRect(startRange);
218 const QRect endRect = textInterface->characterRect(endRange - 1);
219 const QRect lineRect(qMin(startRect.x(), endRect.x()),
220 qMin(startRect.y(), endRect.y()),
221 qMax(startRect.x() + startRect.width(), endRect.x() + endRect.width()) - qMin(startRect.x(), endRect.x()),
222 qMax(startRect.y() + startRect.height(), endRect.y() + endRect.height()) - qMin(startRect.y(), endRect.y()));
223 rectList.append(lineRect);
224 }
225 if (end >= len) break;
226 textInterface->textAfterOffset(end + 1, QAccessible::LineBoundary, &start, &end);
227 }
228 }
229
230 if ((*pRetVal = SafeArrayCreateVector(VT_R8, 0, 4 * rectList.size()))) {
231 for (int i = 0; i < rectList.size(); ++i) {
232 // Scale rect for high DPI screens.
233 UiaRect uiaRect;
234 rectToNativeUiaRect(rectList[i], window, &uiaRect);
235 double coords[4] = { uiaRect.left, uiaRect.top, uiaRect.width, uiaRect.height };
236 for (int j = 0; j < 4; ++j) {
237 LONG idx = 4 * i + j;
238 SafeArrayPutElement(*pRetVal, &idx, &coords[j]);
239 }
240 }
241 }
242 return S_OK;
243}
244
245// Returns an array of children elements embedded within the range.
246HRESULT QWindowsUiaTextRangeProvider::GetChildren(SAFEARRAY **pRetVal)
247{
248 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
249
250 if (!pRetVal)
251 return E_INVALIDARG;
252 // Not supporting any children.
253 *pRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 0);
254 return S_OK;
255}
256
257// Returns a provider for the enclosing element (text to which the range belongs).
258HRESULT QWindowsUiaTextRangeProvider::GetEnclosingElement(IRawElementProviderSimple **pRetVal)
259{
260 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
261
262 if (!pRetVal)
263 return E_INVALIDARG;
264
265 QAccessibleInterface *accessible = accessibleInterface();
266 if (!accessible)
267 return UIA_E_ELEMENTNOTAVAILABLE;
268
269 *pRetVal = QWindowsUiaMainProvider::providerForAccessible(accessible).Detach();
270 return S_OK;
271}
272
273// Gets the text within the range.
274HRESULT QWindowsUiaTextRangeProvider::GetText(int maxLength, BSTR *pRetVal)
275{
276 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << maxLength << "this: " << this;
277
278 if (!pRetVal)
279 return E_INVALIDARG;
280 *pRetVal = nullptr;
281
282 QAccessibleInterface *accessible = accessibleInterface();
283 if (!accessible)
284 return UIA_E_ELEMENTNOTAVAILABLE;
285
286 QAccessibleTextInterface *textInterface = accessible->textInterface();
287 if (!textInterface)
288 return UIA_E_ELEMENTNOTAVAILABLE;
289
290 int len = textInterface->characterCount();
291 QString rangeText;
292 if ((m_startOffset >= 0) && (m_endOffset <= len) && (m_startOffset < m_endOffset))
293 rangeText = textInterface->text(m_startOffset, m_endOffset);
294
295 if ((maxLength > -1) && (rangeText.size() > maxLength))
296 rangeText.truncate(maxLength);
297 *pRetVal = QBStr(rangeText).release();
298 return S_OK;
299}
300
301// Moves the range a specified number of units (and normalizes it).
302HRESULT QWindowsUiaTextRangeProvider::Move(TextUnit unit, int count, int *pRetVal)
303{
304 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << "unit=" << unit << "count=" << count << "this: " << this;
305
306 if (!pRetVal)
307 return E_INVALIDARG;
308 *pRetVal = 0;
309
310 QAccessibleInterface *accessible = accessibleInterface();
311 if (!accessible)
312 return UIA_E_ELEMENTNOTAVAILABLE;
313
314 QAccessibleTextInterface *textInterface = accessible->textInterface();
315 if (!textInterface)
316 return UIA_E_ELEMENTNOTAVAILABLE;
317
318 int len = textInterface->characterCount();
319
320 if (len < 1 || count == 0) // MSDN: "Zero has no effect."
321 return S_OK;
322
323 if (unit == TextUnit_Character) {
324 // Moves the start point, ensuring it lies within the bounds.
325 int start = qBound(0, m_startOffset + count, len);
326 // If range was initially empty, leaves it as is; otherwise, normalizes it to one char.
327 m_endOffset = (m_endOffset > m_startOffset) ? qMin(start + 1, len) : start;
328 *pRetVal = start - m_startOffset; // Returns the actually moved distance.
329 m_startOffset = start;
330 } else {
331 if (count > 0) {
332 MoveEndpointByUnit(TextPatternRangeEndpoint_End, unit, count, pRetVal);
333 MoveEndpointByUnit(TextPatternRangeEndpoint_Start, unit, count, pRetVal);
334 } else {
335 MoveEndpointByUnit(TextPatternRangeEndpoint_Start, unit, count, pRetVal);
336 MoveEndpointByUnit(TextPatternRangeEndpoint_End, unit, count, pRetVal);
337 }
338 }
339 return S_OK;
340}
341
342// Copies the value of an end point from one range to another.
343HRESULT QWindowsUiaTextRangeProvider::MoveEndpointByRange(TextPatternRangeEndpoint endpoint,
344 ITextRangeProvider *targetRange,
345 TextPatternRangeEndpoint targetEndpoint)
346{
347 if (!targetRange)
348 return E_INVALIDARG;
349
350 qCDebug(lcQpaUiAutomation) << __FUNCTION__
351 << "endpoint=" << endpoint << "targetRange=" << targetRange << "targetEndpoint=" << targetEndpoint << "this: " << this;
352
353 auto *targetProvider = static_cast<QWindowsUiaTextRangeProvider *>(targetRange);
354
355 int targetPoint = (targetEndpoint == TextPatternRangeEndpoint_Start) ?
356 targetProvider->m_startOffset : targetProvider->m_endOffset;
357
358 // If the moved endpoint crosses the other endpoint, that one is moved too.
359 if (endpoint == TextPatternRangeEndpoint_Start) {
360 m_startOffset = targetPoint;
361 if (m_endOffset < m_startOffset)
362 m_endOffset = m_startOffset;
363 } else {
364 m_endOffset = targetPoint;
365 if (m_endOffset < m_startOffset)
366 m_startOffset = m_endOffset;
367 }
368 return S_OK;
369}
370
371// Moves an endpoint an specific number of units.
372HRESULT QWindowsUiaTextRangeProvider::MoveEndpointByUnit(TextPatternRangeEndpoint endpoint,
373 TextUnit unit, int count,
374 int *pRetVal)
375{
376 qCDebug(lcQpaUiAutomation) << __FUNCTION__
377 << "endpoint=" << endpoint << "unit=" << unit << "count=" << count << "this: " << this;
378
379 if (!pRetVal)
380 return E_INVALIDARG;
381 *pRetVal = 0;
382
383 QAccessibleInterface *accessible = accessibleInterface();
384 if (!accessible)
385 return UIA_E_ELEMENTNOTAVAILABLE;
386
387 QAccessibleTextInterface *textInterface = accessible->textInterface();
388 if (!textInterface)
389 return UIA_E_ELEMENTNOTAVAILABLE;
390
391 int len = textInterface->characterCount();
392
393 if (len < 1)
394 return S_OK;
395
396 if (unit == TextUnit_Character) {
397 if (endpoint == TextPatternRangeEndpoint_Start) {
398 int boundedValue = qBound(0, m_startOffset + count, len);
399 *pRetVal = boundedValue - m_startOffset;
400 m_startOffset = boundedValue;
401 m_endOffset = qBound(m_startOffset, m_endOffset, len);
402 } else {
403 int boundedValue = qBound(0, m_endOffset + count, len);
404 *pRetVal = boundedValue - m_endOffset;
405 m_endOffset = boundedValue;
406 m_startOffset = qBound(0, m_startOffset, m_endOffset);
407 }
408 } else {
409 QString text = textInterface->text(0, len);
410 int moved = 0;
411
412 if (endpoint == TextPatternRangeEndpoint_Start) {
413 if (count > 0) {
414 for (int t = m_startOffset; (t < len - 1) && (moved < count); ++t) {
415 if (!text.isEmpty() && isTextUnitSeparator(unit, text[t]) && !isTextUnitSeparator(unit, text[t + 1])) {
416 m_startOffset = t + 1;
417 ++moved;
418 }
419 }
420 m_endOffset = qBound(m_startOffset, m_endOffset, len);
421 } else {
422 const int start = m_startOffset >= 0 && m_startOffset <= len
423 ? m_startOffset : len;
424 for (int t = start - 1; (t >= 0) && (moved > count); --t) {
425 if (!text.isEmpty() &&!isTextUnitSeparator(unit, text[t]) && ((t == 0) || isTextUnitSeparator(unit, text[t - 1]))) {
426 m_startOffset = t;
427 --moved;
428 }
429 }
430 }
431 } else {
432 if (count > 0) {
433 for (int t = m_endOffset; (t < len) && (moved < count); ++t) {
434 if ((t == len - 1) || (isTextUnitSeparator(unit, text[t]) && ((unit == TextUnit_Word) || !isTextUnitSeparator(unit, text[t + 1])))) {
435 m_endOffset = t + 1;
436 ++moved;
437 }
438 }
439 } else {
440 int end = 0;
441 for (int t = m_endOffset - 2; (t > 0) && (moved > count); --t) {
442 if (isTextUnitSeparator(unit, text[t]) && ((unit == TextUnit_Word) || !isTextUnitSeparator(unit, text[t + 1]))) {
443 end = t + 1;
444 --moved;
445 }
446 }
447 m_endOffset = end;
448 m_startOffset = qBound(0, m_startOffset, m_endOffset);
449 }
450 }
451 *pRetVal = moved;
452 }
453 return S_OK;
454}
455
456HRESULT QWindowsUiaTextRangeProvider::RemoveFromSelection()
457{
458 qCDebug(lcQpaUiAutomation) << __FUNCTION__;
459 // unselects all
460 return unselect();
461}
462
463// Scrolls the range into view.
464HRESULT QWindowsUiaTextRangeProvider::ScrollIntoView(BOOL alignToTop)
465{
466 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << "alignToTop=" << alignToTop << "this: " << this;
467
468 QAccessibleInterface *accessible = accessibleInterface();
469 if (!accessible)
470 return UIA_E_ELEMENTNOTAVAILABLE;
471
472 QAccessibleTextInterface *textInterface = accessible->textInterface();
473 if (!textInterface)
474 return UIA_E_ELEMENTNOTAVAILABLE;
475
476 textInterface->scrollToSubstring(m_startOffset, m_endOffset);
477 return S_OK;
478}
479
480// Selects the range.
481HRESULT QWindowsUiaTextRangeProvider::Select()
482{
483 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
484
485 QAccessibleInterface *accessible = accessibleInterface();
486 if (!accessible)
487 return UIA_E_ELEMENTNOTAVAILABLE;
488
489 QAccessibleTextInterface *textInterface = accessible->textInterface();
490 if (!textInterface)
491 return UIA_E_ELEMENTNOTAVAILABLE;
492
493 // unselects all and adds a new selection
494 unselect();
495 textInterface->addSelection(m_startOffset, m_endOffset);
496 return S_OK;
497}
498
499// Not supported.
500HRESULT QWindowsUiaTextRangeProvider::FindText(BSTR /* text */, BOOL /* backward */,
501 BOOL /* ignoreCase */,
502 ITextRangeProvider **pRetVal)
503{
504 if (!pRetVal)
505 return E_INVALIDARG;
506 *pRetVal = nullptr;
507 return S_OK;
508}
509
510// Removes all selected ranges from the text element.
511HRESULT QWindowsUiaTextRangeProvider::unselect()
512{
513 qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this;
514
515 QAccessibleInterface *accessible = accessibleInterface();
516 if (!accessible)
517 return UIA_E_ELEMENTNOTAVAILABLE;
518
519 QAccessibleTextInterface *textInterface = accessible->textInterface();
520 if (!textInterface)
521 return UIA_E_ELEMENTNOTAVAILABLE;
522
523 int selCount = textInterface->selectionCount();
524
525 for (int i = selCount - 1; i >= 0; --i)
526 textInterface->removeSelection(i);
527 return S_OK;
528}
529
530// helper method to retrieve the value of the given IAccessible2 text attribute,
531// or an empty string if not set
532QString QWindowsUiaTextRangeProvider::valueForIA2Attribute(QAccessibleTextInterface *textInterface,
533 const QString &key)
534{
535 Q_ASSERT(textInterface);
536
537 int startOffset;
538 int endOffset;
539 const QString attributes = textInterface->attributes(m_startOffset, &startOffset, &endOffset);
540 // don't report if attributes don't apply for the whole range
541 if (startOffset > m_startOffset || endOffset < m_endOffset)
542 return {};
543
544 for (auto attr : QStringTokenizer{attributes, u';'})
545 {
546 const QList<QStringView> items = attr.split(u':', Qt::SkipEmptyParts, Qt::CaseSensitive);
547 if (items.count() == 2 && items[0] == key)
548 return items[1].toString();
549 }
550
551 return {};
552}
553
554TextDecorationLineStyle QWindowsUiaTextRangeProvider::uiaLineStyleForIA2LineStyle(const QString &ia2LineStyle)
555{
556 if (ia2LineStyle == QStringLiteral("none"))
557 return TextDecorationLineStyle_None;
558 if (ia2LineStyle == QStringLiteral("single"))
559 return TextDecorationLineStyle_Single;
560 if (ia2LineStyle == QStringLiteral("double"))
561 return TextDecorationLineStyle_Double;
562
563 return TextDecorationLineStyle_Other;
564}
565
566QT_END_NAMESPACE
567
568#endif // QT_CONFIG(accessibility)