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