1 /****************************************************************************
2 **
3 ** Copyright (C) 2018 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 "qwinrtuiatextrangeprovider.h"
44 #include "qwinrtuiametadatacache.h"
45 #include "qwinrtuiamainprovider.h"
46 #include "qwinrtuiautils.h"
47 
48 #include <QtGui/QAccessible>
49 #include <QtGui/QAccessibleInterface>
50 #include <QtCore/QLoggingCategory>
51 #include <QtCore/QString>
52 #include <QtCore/private/qeventdispatcher_winrt_p.h>
53 
54 #include <memory>
55 
56 QT_BEGIN_NAMESPACE
57 
58 using namespace QWinRTUiAutomation;
59 using namespace Microsoft::WRL;
60 using namespace Microsoft::WRL::Wrappers;
61 using namespace ABI::Windows::Foundation;
62 using namespace ABI::Windows::UI::Xaml;
63 using namespace ABI::Windows::UI::Xaml::Automation;
64 using namespace ABI::Windows::UI::Xaml::Automation::Provider;
65 using namespace ABI::Windows::UI::Xaml::Automation::Text;
66 
QWinRTUiaTextRangeProvider(QAccessible::Id id,int startOffset,int endOffset)67 QWinRTUiaTextRangeProvider::QWinRTUiaTextRangeProvider(QAccessible::Id id, int startOffset, int endOffset) :
68     QWinRTUiaBaseProvider(id),
69     m_startOffset(startOffset),
70     m_endOffset(endOffset)
71 {
72     qCDebug(lcQpaUiAutomation) << __FUNCTION__ << startOffset << endOffset;
73 }
74 
~QWinRTUiaTextRangeProvider()75 QWinRTUiaTextRangeProvider::~QWinRTUiaTextRangeProvider()
76 {
77     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
78 }
79 
Clone(ITextRangeProvider ** returnValue)80 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::Clone(ITextRangeProvider **returnValue)
81 {
82     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
83     if (!returnValue)
84         return E_INVALIDARG;
85 
86     ComPtr<QWinRTUiaTextRangeProvider> textRangeProvider = Make<QWinRTUiaTextRangeProvider>(id(), m_startOffset, m_endOffset);
87     textRangeProvider.CopyTo(returnValue);
88     return S_OK;
89 }
90 
91 // Two ranges are considered equal if their start/end points are the same.
Compare(ITextRangeProvider * textRangeProvider,boolean * returnValue)92 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::Compare(ITextRangeProvider *textRangeProvider, boolean *returnValue)
93 {
94     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
95     if (!textRangeProvider || !returnValue)
96         return E_INVALIDARG;
97 
98     QWinRTUiaTextRangeProvider *targetProvider = static_cast<QWinRTUiaTextRangeProvider *>(textRangeProvider);
99     *returnValue = ((targetProvider->m_startOffset == m_startOffset) && (targetProvider->m_endOffset == m_endOffset));
100     return S_OK;
101 }
102 
103 // Compare different endpoinds between two providers.
CompareEndpoints(TextPatternRangeEndpoint endpoint,ITextRangeProvider * textRangeProvider,TextPatternRangeEndpoint targetEndpoint,INT32 * returnValue)104 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::CompareEndpoints(TextPatternRangeEndpoint endpoint, ITextRangeProvider *textRangeProvider, TextPatternRangeEndpoint targetEndpoint, INT32 *returnValue)
105 {
106     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
107 
108     if (!textRangeProvider || !returnValue)
109         return E_INVALIDARG;
110 
111     QWinRTUiaTextRangeProvider *targetProvider = static_cast<QWinRTUiaTextRangeProvider *>(textRangeProvider);
112 
113     int point = (endpoint == TextPatternRangeEndpoint_Start) ? m_startOffset : m_endOffset;
114     int targetPoint = (targetEndpoint == TextPatternRangeEndpoint_Start) ?
115                 targetProvider->m_startOffset : targetProvider->m_endOffset;
116     *returnValue = point - targetPoint;
117     return S_OK;
118 }
119 
120 // Expands/normalizes the range for a given text unit.
ExpandToEnclosingUnit(TextUnit unit)121 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::ExpandToEnclosingUnit(TextUnit unit)
122 {
123     qCDebug(lcQpaUiAutomation) << __FUNCTION__ << "unit=" << unit << "this: " << this;
124 
125     QSharedPointer<QWinRTUiaControlMetadata> metadata = QWinRTUiaMetadataCache::instance()->metadataForId(id());
126 
127     int len = metadata->characterCount();
128     if (len < 1) {
129         m_startOffset = 0;
130         m_endOffset = 0;
131     } else {
132         if (unit == TextUnit_Character) {
133             m_startOffset = qBound(0, m_startOffset, len - 1);
134             m_endOffset = m_startOffset + 1;
135         } else {
136             QString text = metadata->text();
137             for (int t = m_startOffset; t >= 0; --t) {
138                 if (!isTextUnitSeparator(unit, text[t]) && ((t == 0) || isTextUnitSeparator(unit, text[t - 1]))) {
139                     m_startOffset = t;
140                     break;
141                 }
142             }
143             for (int t = m_startOffset; t < len; ++t) {
144                 if ((t == len - 1) || (isTextUnitSeparator(unit, text[t]) && ((unit == TextUnit_Word) || !isTextUnitSeparator(unit, text[t + 1])))) {
145                     m_endOffset = t + 1;
146                     break;
147                 }
148             }
149         }
150     }
151     return S_OK;
152 }
153 
154 // Not supported.
FindAttribute(INT32,IInspectable *,boolean,ITextRangeProvider ** returnValue)155 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::FindAttribute(INT32 /*attributeId*/, IInspectable * /*value*/, boolean /*backward*/, ITextRangeProvider **returnValue)
156 {
157     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
158     if (!returnValue)
159         return E_INVALIDARG;
160     *returnValue = nullptr;
161     return S_OK;
162 }
163 
FindText(HSTRING,boolean,boolean,ITextRangeProvider ** returnValue)164 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::FindText(HSTRING /*text*/, boolean /*backward*/, boolean /*ignoreCase*/, ITextRangeProvider **returnValue)
165 {
166     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
167     if (!returnValue)
168         return E_INVALIDARG;
169     *returnValue = nullptr;
170     return S_OK;
171 }
172 
173 // Returns the value of a given attribute.
GetAttributeValue(INT32 attributeId,IInspectable ** returnValue)174 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::GetAttributeValue(INT32 attributeId, IInspectable **returnValue)
175 {
176     qCDebug(lcQpaUiAutomation) << __FUNCTION__ << "attributeId=" << attributeId;
177 
178     if (!returnValue)
179         return E_INVALIDARG;
180     *returnValue = nullptr;
181 
182     ComPtr<IPropertyValueStatics> propertyValueStatics;
183     if (FAILED(RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Foundation_PropertyValue).Get(), IID_PPV_ARGS(&propertyValueStatics))))
184         return E_FAIL;
185 
186     QSharedPointer<QWinRTUiaControlMetadata> metadata = QWinRTUiaMetadataCache::instance()->metadataForId(id());
187 
188     switch (attributeId) {
189     case AutomationTextAttributesEnum_IsReadOnlyAttribute:
190         return propertyValueStatics->CreateBoolean(metadata->state().readOnly, returnValue);
191     case AutomationTextAttributesEnum_CaretPositionAttribute:
192         if (metadata->cursorPosition() == 0)
193             return propertyValueStatics->CreateInt32(AutomationCaretPosition_BeginningOfLine, returnValue);
194         else if (metadata->cursorPosition() == metadata->characterCount())
195             return propertyValueStatics->CreateInt32(AutomationCaretPosition_EndOfLine, returnValue);
196         else
197             return propertyValueStatics->CreateInt32(AutomationCaretPosition_Unknown, returnValue);
198     default:
199         break;
200     }
201     return E_FAIL;
202 }
203 
204 // Returns an array of bounding rectangles for text lines within the range.
GetBoundingRectangles(UINT32 * returnValueSize,DOUBLE ** returnValue)205 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::GetBoundingRectangles(UINT32 *returnValueSize, DOUBLE **returnValue)
206 {
207     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
208 
209     if (!returnValueSize || !returnValue)
210         return E_INVALIDARG;
211     *returnValueSize = 0;
212     *returnValue = nullptr;
213 
214     auto accid = id();
215     auto startOffset = m_startOffset;
216     auto endOffset = m_endOffset;
217     auto rects = std::make_shared<QVarLengthArray<QRect>>();
218 
219     if (!SUCCEEDED(QEventDispatcherWinRT::runOnMainThread([accid, startOffset, endOffset, rects]() {
220         if (QAccessibleInterface *accessible = accessibleForId(accid)) {
221             if (QAccessibleTextInterface *textInterface = accessible->textInterface()) {
222                 int len = textInterface->characterCount();
223                 if ((startOffset >= 0) && (endOffset <= len) && (startOffset < endOffset)) {
224                     int start, end;
225                     textInterface->textAtOffset(startOffset, QAccessible::LineBoundary, &start, &end);
226                     while ((start >= 0) && (end >= 0)) {
227                         int startRange = qMax(start, startOffset);
228                         int endRange = qMin(end, endOffset);
229                         if (startRange < endRange) {
230                             // Calculates a bounding rectangle for the line and adds it to the list.
231                             const QRect startRect = textInterface->characterRect(startRange);
232                             const QRect endRect = textInterface->characterRect(endRange - 1);
233                             const QRect lineRect(qMin(startRect.x(), endRect.x()),
234                                                  qMin(startRect.y(), endRect.y()),
235                                                  qMax(startRect.x() + startRect.width(), endRect.x() + endRect.width()) - qMin(startRect.x(), endRect.x()),
236                                                  qMax(startRect.y() + startRect.height(), endRect.y() + endRect.height()) - qMin(startRect.y(), endRect.y()));
237                             rects->append(lineRect);
238                         }
239                         if (end >= len) break;
240                         textInterface->textAfterOffset(end + 1, QAccessible::LineBoundary, &start, &end);
241                     }
242                 }
243             }
244         }
245         return S_OK;
246     }))) {
247         return E_FAIL;
248     }
249 
250     DOUBLE *doubleArray = static_cast<DOUBLE *>(CoTaskMemAlloc(4 * rects->size() * sizeof(DOUBLE)));
251     if (!doubleArray)
252         return E_OUTOFMEMORY;
253 
254     DOUBLE *dst = doubleArray;
255     for (auto rect : *rects) {
256         *dst++ = rect.left();
257         *dst++ = rect.top();
258         *dst++ = rect.width();
259         *dst++ = rect.height();
260     }
261     *returnValue = doubleArray;
262     *returnValueSize = 4 * rects->size();
263     return S_OK;
264 }
265 
GetEnclosingElement(IIRawElementProviderSimple ** returnValue)266 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::GetEnclosingElement(IIRawElementProviderSimple **returnValue)
267 {
268     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
269     if (!returnValue)
270         return E_INVALIDARG;
271     return QWinRTUiaMainProvider::rawProviderForAccessibleId(id(), returnValue);
272 }
273 
GetText(INT32 maxLength,HSTRING * returnValue)274 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::GetText(INT32 maxLength, HSTRING *returnValue)
275 {
276     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
277     if (!returnValue)
278         return E_INVALIDARG;
279     *returnValue = nullptr;
280 
281     QSharedPointer<QWinRTUiaControlMetadata> metadata = QWinRTUiaMetadataCache::instance()->metadataForId(id());
282 
283     QString rangeText = metadata->text().mid(m_startOffset, m_endOffset - m_startOffset);
284 
285     if ((maxLength > -1) && (rangeText.size() > maxLength))
286         rangeText.truncate(maxLength);
287     return qHString(rangeText, returnValue);
288 }
289 
Move(TextUnit unit,INT32 count,INT32 * returnValue)290 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::Move(TextUnit unit, INT32 count, INT32 *returnValue)
291 {
292     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
293     if (!returnValue)
294         return E_INVALIDARG;
295     *returnValue = 0;
296 
297     QSharedPointer<QWinRTUiaControlMetadata> metadata = QWinRTUiaMetadataCache::instance()->metadataForId(id());
298 
299     int len = metadata->characterCount();
300     if (len < 1)
301         return S_OK;
302 
303     if (unit == TextUnit_Character) {
304         // Moves the start point, ensuring it lies within the bounds.
305         int start = qBound(0, m_startOffset + count, len - 1);
306         // If range was initially empty, leaves it as is; otherwise, normalizes it to one char.
307         m_endOffset = (m_endOffset > m_startOffset) ? start + 1 : start;
308         *returnValue = start - m_startOffset; // Returns the actually moved distance.
309         m_startOffset = start;
310     } else {
311         if (count > 0) {
312             MoveEndpointByUnit(TextPatternRangeEndpoint_End, unit, count, returnValue);
313             MoveEndpointByUnit(TextPatternRangeEndpoint_Start, unit, count, returnValue);
314         } else {
315             MoveEndpointByUnit(TextPatternRangeEndpoint_Start, unit, count, returnValue);
316             MoveEndpointByUnit(TextPatternRangeEndpoint_End, unit, count, returnValue);
317         }
318     }
319     return S_OK;
320 }
321 
MoveEndpointByUnit(TextPatternRangeEndpoint endpoint,TextUnit unit,INT32 count,INT32 * returnValue)322 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, INT32 count, INT32 *returnValue)
323 {
324     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
325     if (!returnValue)
326         return E_INVALIDARG;
327     *returnValue = 0;
328 
329     QSharedPointer<QWinRTUiaControlMetadata> metadata = QWinRTUiaMetadataCache::instance()->metadataForId(id());
330 
331     int len = metadata->characterCount();
332     if (len < 1)
333         return S_OK;
334 
335     if (unit == TextUnit_Character) {
336         if (endpoint == TextPatternRangeEndpoint_Start) {
337             int boundedValue = qBound(0, m_startOffset + count, len - 1);
338             *returnValue = boundedValue - m_startOffset;
339             m_startOffset = boundedValue;
340             m_endOffset = qBound(m_startOffset, m_endOffset, len);
341         } else {
342             int boundedValue = qBound(0, m_endOffset + count, len);
343             *returnValue = boundedValue - m_endOffset;
344             m_endOffset = boundedValue;
345             m_startOffset = qBound(0, m_startOffset, m_endOffset);
346         }
347     } else {
348         QString text = metadata->text();
349         int moved = 0;
350 
351         if (endpoint == TextPatternRangeEndpoint_Start) {
352             if (count > 0) {
353                 for (int t = m_startOffset; (t < len - 1) && (moved < count); ++t) {
354                     if (isTextUnitSeparator(unit, text[t]) && !isTextUnitSeparator(unit, text[t + 1])) {
355                         m_startOffset = t + 1;
356                         ++moved;
357                     }
358                 }
359                 m_endOffset = qBound(m_startOffset, m_endOffset, len);
360             } else {
361                 for (int t = m_startOffset - 1; (t >= 0) && (moved > count); --t) {
362                     if (!isTextUnitSeparator(unit, text[t]) && ((t == 0) || isTextUnitSeparator(unit, text[t - 1]))) {
363                         m_startOffset = t;
364                         --moved;
365                     }
366                 }
367             }
368         } else {
369             if (count > 0) {
370                 for (int t = m_endOffset; (t < len) && (moved < count); ++t) {
371                     if ((t == len - 1) || (isTextUnitSeparator(unit, text[t]) && ((unit == TextUnit_Word) || !isTextUnitSeparator(unit, text[t + 1])))) {
372                         m_endOffset = t + 1;
373                         ++moved;
374                     }
375                 }
376             } else {
377                 int end = 0;
378                 for (int t = m_endOffset - 2; (t > 0) && (moved > count); --t) {
379                     if (isTextUnitSeparator(unit, text[t]) && ((unit == TextUnit_Word) || !isTextUnitSeparator(unit, text[t + 1]))) {
380                         end = t + 1;
381                         --moved;
382                     }
383                 }
384                 m_endOffset = end;
385                 m_startOffset = qBound(0, m_startOffset, m_endOffset);
386             }
387         }
388         *returnValue = moved;
389     }
390     return S_OK;
391 }
392 
MoveEndpointByRange(TextPatternRangeEndpoint endpoint,ITextRangeProvider * textRangeProvider,TextPatternRangeEndpoint targetEndpoint)393 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::MoveEndpointByRange(TextPatternRangeEndpoint endpoint, ITextRangeProvider *textRangeProvider, TextPatternRangeEndpoint targetEndpoint)
394 {
395     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
396     if (!textRangeProvider)
397         return E_INVALIDARG;
398 
399     QWinRTUiaTextRangeProvider *targetProvider = static_cast<QWinRTUiaTextRangeProvider *>(textRangeProvider);
400 
401     int targetPoint = (targetEndpoint == TextPatternRangeEndpoint_Start) ?
402                 targetProvider->m_startOffset : targetProvider->m_endOffset;
403 
404     // If the moved endpoint crosses the other endpoint, that one is moved too.
405     if (endpoint == TextPatternRangeEndpoint_Start) {
406         m_startOffset = targetPoint;
407         if (m_endOffset < m_startOffset)
408             m_endOffset = m_startOffset;
409     } else {
410         m_endOffset = targetPoint;
411         if (m_endOffset < m_startOffset)
412             m_startOffset = m_endOffset;
413     }
414     return S_OK;
415 }
416 
Select()417 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::Select()
418 {
419     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
420 
421     auto accid = id();
422     auto startOffset = m_startOffset;
423     auto endOffset = m_endOffset;
424 
425     QEventDispatcherWinRT::runOnMainThread([accid, startOffset, endOffset]() {
426         if (QAccessibleInterface *accessible = accessibleForId(accid))
427             if (QAccessibleTextInterface *textInterface = accessible->textInterface()) {
428                 // unselects all and adds a new selection
429                 for (int i = textInterface->selectionCount() - 1; i >= 0; --i)
430                     textInterface->removeSelection(i);
431                 textInterface->addSelection(startOffset, endOffset);
432             }
433         QWinRTUiaMetadataCache::instance()->load(accid);
434         return S_OK;
435     }, 0);
436     return S_OK;
437 }
438 
AddToSelection()439 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::AddToSelection()
440 {
441     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
442     return Select();
443 }
444 
RemoveFromSelection()445 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::RemoveFromSelection()
446 {
447     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
448 
449     auto accid = id();
450 
451     QEventDispatcherWinRT::runOnMainThread([accid]() {
452         if (QAccessibleInterface *accessible = accessibleForId(accid))
453             if (QAccessibleTextInterface *textInterface = accessible->textInterface()) {
454                 // unselects all
455                 for (int i = textInterface->selectionCount() - 1; i >= 0; --i)
456                     textInterface->removeSelection(i);
457             }
458         QWinRTUiaMetadataCache::instance()->load(accid);
459         return S_OK;
460     }, 0);
461     return S_OK;
462 }
463 
ScrollIntoView(boolean)464 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::ScrollIntoView(boolean /*alignToTop*/)
465 {
466     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
467 
468     auto accid = id();
469     auto startOffset = m_startOffset;
470     auto endOffset = m_endOffset;
471 
472     QEventDispatcherWinRT::runOnMainThread([accid, startOffset, endOffset]() {
473         if (QAccessibleInterface *accessible = accessibleForId(accid))
474             if (QAccessibleTextInterface *textInterface = accessible->textInterface()) {
475                 textInterface->scrollToSubstring(startOffset, endOffset);
476             }
477         QWinRTUiaMetadataCache::instance()->load(accid);
478         return S_OK;
479     }, 0);
480     return S_OK;
481 }
482 
483 // Returns an array of children elements embedded within the range.
GetChildren(UINT32 * returnValueSize,IIRawElementProviderSimple *** returnValue)484 HRESULT STDMETHODCALLTYPE QWinRTUiaTextRangeProvider::GetChildren(UINT32 *returnValueSize, IIRawElementProviderSimple ***returnValue)
485 {
486     qCDebug(lcQpaUiAutomation) << __FUNCTION__;
487 
488     if (!returnValue)
489         return E_INVALIDARG;
490     // Not supporting any children.
491     returnValueSize = 0;
492     *returnValue = nullptr;
493     return S_OK;
494 }
495 
496 QT_END_NAMESPACE
497 
498 #endif // QT_CONFIG(accessibility)
499