UiaTextRange.cpp (48148B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "UiaTextRange.h" 8 9 #include "mozilla/a11y/HyperTextAccessibleBase.h" 10 #include "nsAccUtils.h" 11 #include "nsIAccessibleTypes.h" 12 #include "TextLeafRange.h" 13 #include <comdef.h> 14 #include <propvarutil.h> 15 #include <unordered_set> 16 17 namespace mozilla::a11y { 18 19 template <typename T> 20 HRESULT GetAttribute(TEXTATTRIBUTEID aAttributeId, T const& aRangeOrPoint, 21 VARIANT& aRetVal); 22 23 static int CompareVariants(const VARIANT& aFirst, const VARIANT& aSecond) { 24 // MinGW lacks support for VariantCompare, but does support converting to 25 // PROPVARIANT and PropVariantCompareEx. Use this as a workaround for MinGW 26 // builds, but avoid the extra work otherwise. See Bug 1944732. 27 #if defined(__MINGW32__) || defined(__MINGW64__) || defined(__MINGW__) 28 PROPVARIANT firstPropVar; 29 PROPVARIANT secondPropVar; 30 VariantToPropVariant(&aFirst, &firstPropVar); 31 VariantToPropVariant(&aSecond, &secondPropVar); 32 return PropVariantCompareEx(firstPropVar, secondPropVar, PVCU_DEFAULT, 33 PVCHF_DEFAULT); 34 #else 35 return VariantCompare(aFirst, aSecond); 36 #endif 37 } 38 39 // Used internally to safely get a UiaTextRange from a COM pointer provided 40 // to us by a client. 41 // {74B8E664-4578-4B52-9CBC-30A7A8271AE8} 42 static const GUID IID_UiaTextRange = { 43 0x74b8e664, 44 0x4578, 45 0x4b52, 46 {0x9c, 0xbc, 0x30, 0xa7, 0xa8, 0x27, 0x1a, 0xe8}}; 47 48 // Helpers 49 50 static TextLeafPoint GetEndpoint(TextLeafRange& aRange, 51 enum TextPatternRangeEndpoint aEndpoint) { 52 if (aEndpoint == TextPatternRangeEndpoint_Start) { 53 return aRange.Start(); 54 } 55 return aRange.End(); 56 } 57 58 static void RemoveExcludedAccessiblesFromRange(TextLeafRange& aRange) { 59 MOZ_ASSERT(aRange); 60 TextLeafPoint start = aRange.Start(); 61 TextLeafPoint end = aRange.End(); 62 if (start == end) { 63 // The range is collapsed. It doesn't include anything. 64 return; 65 } 66 if (end.mOffset != 0) { 67 // It is theoretically possible for start to be at the exclusive end of a 68 // previous Accessible (i.e. mOffset is its length), so the range doesn't 69 // really encompass that Accessible's text and we should thus exclude that 70 // Accessible. However, that hasn't been seen in practice yet. If it does 71 // occur and cause problems, we should adjust the start point here. 72 return; 73 } 74 // end is at the start of its Accessible. This can happen because we always 75 // search for the start of a character, word, etc. Since the end of a range 76 // is exclusive, the range doesn't include anything in this Accessible. 77 // Move the end back so that it doesn't touch this Accessible at all. This 78 // is important when determining what Accessibles lie within this range 79 // because otherwise, we'd incorrectly consider an Accessible which the range 80 // doesn't actually cover. 81 // Move to the previous character. 82 end = end.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 83 // We want the position immediately after this character in the same 84 // Accessible. 85 ++end.mOffset; 86 if (start <= end) { 87 aRange.SetEnd(end); 88 } 89 } 90 91 static bool IsUiaEmbeddedObject(const Accessible* aAcc) { 92 // "For UI Automation, an embedded object is any element that has non-textual 93 // boundaries such as an image, hyperlink, table, or document type" 94 // https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-textpattern-and-embedded-objects-overview 95 if (aAcc->IsText()) { 96 return false; 97 } 98 switch (aAcc->Role()) { 99 case roles::CONTENT_DELETION: 100 case roles::CONTENT_INSERTION: 101 case roles::EMPHASIS: 102 case roles::LANDMARK: 103 case roles::MARK: 104 case roles::NAVIGATION: 105 case roles::NOTE: 106 case roles::PARAGRAPH: 107 case roles::REGION: 108 case roles::SECTION: 109 case roles::STRONG: 110 case roles::SUBSCRIPT: 111 case roles::SUPERSCRIPT: 112 case roles::TEXT: 113 case roles::TEXT_CONTAINER: 114 return false; 115 default: 116 break; 117 } 118 return true; 119 } 120 121 static NotNull<Accessible*> GetSelectionContainer(TextLeafRange& aRange) { 122 Accessible* acc = aRange.Start().mAcc; 123 MOZ_ASSERT(acc); 124 if (acc->IsTextLeaf()) { 125 if (Accessible* parent = acc->Parent()) { 126 acc = parent; 127 } 128 } 129 if (acc->IsTextField()) { 130 // Gecko uses an independent selection for <input> and <textarea>. 131 return WrapNotNull(acc); 132 } 133 // For everything else (including contentEditable), Gecko uses the document 134 // selection. 135 return WrapNotNull(nsAccUtils::DocumentFor(acc)); 136 } 137 138 static TextLeafPoint NormalizePoint(Accessible* aAcc, int32_t aOffset) { 139 if (!aAcc) { 140 return TextLeafPoint(aAcc, aOffset); 141 } 142 int32_t length = static_cast<int32_t>(nsAccUtils::TextLength(aAcc)); 143 if (aOffset > length) { 144 // This range was created when this leaf contained more characters, but some 145 // characters were since removed. Restrict to the new length. 146 aOffset = length; 147 } 148 return TextLeafPoint(aAcc, aOffset); 149 } 150 151 // UiaTextRange 152 153 UiaTextRange::UiaTextRange(const TextLeafRange& aRange) { 154 MOZ_ASSERT(aRange); 155 SetRange(aRange); 156 } 157 158 void UiaTextRange::SetRange(const TextLeafRange& aRange) { 159 TextLeafPoint start = aRange.Start(); 160 mStartAcc = MsaaAccessible::GetFrom(start.mAcc); 161 MOZ_ASSERT(mStartAcc); 162 mStartOffset = start.mOffset; 163 TextLeafPoint end = aRange.End(); 164 mEndAcc = MsaaAccessible::GetFrom(end.mAcc); 165 MOZ_ASSERT(mEndAcc); 166 mEndOffset = end.mOffset; 167 // Special handling of the insertion point at the end of a line only makes 168 // sense when dealing with the caret, which is a collapsed range. 169 mIsEndOfLineInsertionPoint = start == end && start.mIsEndOfLineInsertionPoint; 170 } 171 172 TextLeafRange UiaTextRange::GetRange() const { 173 // Either Accessible might have been shut down because it was removed from the 174 // tree. In that case, Acc() will return null, resulting in an invalid 175 // TextLeafPoint and thus an invalid TextLeafRange. Any caller is expected to 176 // handle this case. 177 if (mIsEndOfLineInsertionPoint) { 178 MOZ_ASSERT(mStartAcc == mEndAcc && mStartOffset == mEndOffset); 179 TextLeafPoint point = NormalizePoint(mStartAcc->Acc(), mStartOffset); 180 point.mIsEndOfLineInsertionPoint = true; 181 return TextLeafRange(point, point); 182 } 183 return TextLeafRange(NormalizePoint(mStartAcc->Acc(), mStartOffset), 184 NormalizePoint(mEndAcc->Acc(), mEndOffset)); 185 } 186 187 /* static */ 188 TextLeafRange UiaTextRange::GetRangeFrom(ITextRangeProvider* aProvider) { 189 if (aProvider) { 190 RefPtr<UiaTextRange> uiaRange; 191 aProvider->QueryInterface(IID_UiaTextRange, getter_AddRefs(uiaRange)); 192 if (uiaRange) { 193 return uiaRange->GetRange(); 194 } 195 } 196 return TextLeafRange(); 197 } 198 199 /* static */ 200 TextLeafPoint UiaTextRange::FindBoundary(const TextLeafPoint& aOrigin, 201 enum TextUnit aUnit, 202 nsDirection aDirection, 203 bool aIncludeOrigin) { 204 if (aUnit == TextUnit_Page || aUnit == TextUnit_Document) { 205 // The UIA documentation is a little inconsistent regarding the Document 206 // unit: 207 // https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-textpattern-and-embedded-objects-overview 208 // First, it says: 209 // "Objects backed by the same text store as their container are referred to 210 // as "compatible" embedded objects. These objects can be TextPattern 211 // objects themselves and, in this case, their text ranges are comparable to 212 // text ranges obtained from their container. This enables the providers to 213 // expose client information about the individual TextPattern objects as if 214 // they were one, large text provider." 215 // But later, it says: 216 // "For embedded TextPattern objects, the Document unit only recognizes the 217 // content contained within that element." 218 // If ranges are equivalent regardless of what object they were created 219 // from, this doesn't make sense because this would mean that the Document 220 // unit would change depending on where the range was positioned at the 221 // time. Instead, Gecko restricts the range to an editable text control for 222 // ITextProvider::get_DocumentRange, but returns the full document for 223 // TextUnit_Document. This is consistent with Microsoft Word and Chromium. 224 Accessible* doc = nsAccUtils::DocumentFor(aOrigin.mAcc); 225 if (aDirection == eDirPrevious) { 226 return TextLeafPoint(doc, 0); 227 } 228 return TextLeafPoint(doc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); 229 } 230 if (aUnit == TextUnit_Format) { 231 // The UIA documentation says that TextUnit_Format aims to define ranges 232 // that "include all text that shares all the same attributes." 233 // FindTextAttrsStart considers container boundaries to be format boundaries 234 // even if UIA may not. UIA's documentation may consider the next container 235 // to be part of the same format run, since it may have the same attributes. 236 // UIA considers embedded objects to be format boundaries, which is a more 237 // restrictive understanding of boundaries than what Gecko implements here. 238 return aOrigin.FindTextAttrsStart(aDirection, aIncludeOrigin); 239 } 240 AccessibleTextBoundary boundary; 241 switch (aUnit) { 242 case TextUnit_Character: 243 boundary = nsIAccessibleText::BOUNDARY_CLUSTER; 244 break; 245 case TextUnit_Word: 246 boundary = nsIAccessibleText::BOUNDARY_WORD_START; 247 break; 248 case TextUnit_Line: 249 boundary = nsIAccessibleText::BOUNDARY_LINE_START; 250 break; 251 case TextUnit_Paragraph: 252 boundary = nsIAccessibleText::BOUNDARY_PARAGRAPH; 253 break; 254 default: 255 return TextLeafPoint(); 256 } 257 return aOrigin.FindBoundary( 258 boundary, aDirection, 259 aIncludeOrigin ? TextLeafPoint::BoundaryFlags::eIncludeOrigin 260 : TextLeafPoint::BoundaryFlags::eDefaultBoundaryFlags); 261 } 262 263 bool UiaTextRange::MovePoint(TextLeafPoint& aPoint, enum TextUnit aUnit, 264 const int aRequestedCount, int& aActualCount) { 265 aActualCount = 0; 266 const nsDirection direction = aRequestedCount < 0 ? eDirPrevious : eDirNext; 267 while (aActualCount != aRequestedCount) { 268 TextLeafPoint oldPoint = aPoint; 269 aPoint = FindBoundary(aPoint, aUnit, direction); 270 if (!aPoint) { 271 return false; // Unit not supported. 272 } 273 if (aPoint == oldPoint) { 274 break; // Can't go any further. 275 } 276 direction == eDirPrevious ? --aActualCount : ++aActualCount; 277 } 278 return true; 279 } 280 281 void UiaTextRange::SetEndpoint(enum TextPatternRangeEndpoint aEndpoint, 282 const TextLeafPoint& aDest) { 283 // Per the UIA documentation: 284 // https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-moveendpointbyrange#remarks 285 // https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-moveendpointbyunit#remarks 286 // "If the endpoint being moved crosses the other endpoint of the same text 287 // range, that other endpoint is moved also, resulting in a degenerate (empty) 288 // range and ensuring the correct ordering of the endpoints (that is, the 289 // start is always less than or equal to the end)." 290 TextLeafRange origRange = GetRange(); 291 MOZ_ASSERT(origRange); 292 if (aEndpoint == TextPatternRangeEndpoint_Start) { 293 TextLeafPoint end = origRange.End(); 294 if (end < aDest) { 295 end = aDest; 296 } 297 SetRange({aDest, end}); 298 } else { 299 TextLeafPoint start = origRange.Start(); 300 if (aDest < start) { 301 start = aDest; 302 } 303 SetRange({start, aDest}); 304 } 305 } 306 307 // IUnknown 308 IMPL_IUNKNOWN2(UiaTextRange, ITextRangeProvider, UiaTextRange) 309 310 // ITextRangeProvider methods 311 312 STDMETHODIMP 313 UiaTextRange::Clone(__RPC__deref_out_opt ITextRangeProvider** aRetVal) { 314 if (!aRetVal) { 315 return E_INVALIDARG; 316 } 317 TextLeafRange range = GetRange(); 318 if (!range) { 319 return CO_E_OBJNOTCONNECTED; 320 } 321 RefPtr uiaRange = new UiaTextRange(range); 322 uiaRange.forget(aRetVal); 323 return S_OK; 324 } 325 326 STDMETHODIMP 327 UiaTextRange::Compare(__RPC__in_opt ITextRangeProvider* aRange, 328 __RPC__out BOOL* aRetVal) { 329 if (!aRange || !aRetVal) { 330 return E_INVALIDARG; 331 } 332 *aRetVal = GetRange() == GetRangeFrom(aRange); 333 return S_OK; 334 } 335 336 STDMETHODIMP 337 UiaTextRange::CompareEndpoints(enum TextPatternRangeEndpoint aEndpoint, 338 __RPC__in_opt ITextRangeProvider* aTargetRange, 339 enum TextPatternRangeEndpoint aTargetEndpoint, 340 __RPC__out int* aRetVal) { 341 if (!aTargetRange || !aRetVal) { 342 return E_INVALIDARG; 343 } 344 TextLeafRange origRange = GetRange(); 345 if (!origRange) { 346 return CO_E_OBJNOTCONNECTED; 347 } 348 TextLeafPoint origPoint = GetEndpoint(origRange, aEndpoint); 349 TextLeafRange targetRange = GetRangeFrom(aTargetRange); 350 if (!targetRange) { 351 return E_INVALIDARG; 352 } 353 TextLeafPoint targetPoint = GetEndpoint(targetRange, aTargetEndpoint); 354 if (origPoint == targetPoint) { 355 *aRetVal = 0; 356 } else if (origPoint < targetPoint) { 357 *aRetVal = -1; 358 } else { 359 *aRetVal = 1; 360 } 361 return S_OK; 362 } 363 364 STDMETHODIMP 365 UiaTextRange::ExpandToEnclosingUnit(enum TextUnit aUnit) { 366 TextLeafRange range = GetRange(); 367 if (!range) { 368 return CO_E_OBJNOTCONNECTED; 369 } 370 TextLeafPoint origin = range.Start(); 371 TextLeafPoint start = FindBoundary(origin, aUnit, eDirPrevious, 372 /* aIncludeOrigin */ true); 373 if (!start) { 374 return E_FAIL; // Unit not supported. 375 } 376 TextLeafPoint end = FindBoundary(origin, aUnit, eDirNext); 377 SetRange({start, end}); 378 return S_OK; 379 } 380 381 // Search within the text range for the first subrange that has the given 382 // attribute value. The resulting range might span multiple text attribute runs. 383 // If aBackward, start the search from the end of the range. 384 STDMETHODIMP 385 UiaTextRange::FindAttribute(TEXTATTRIBUTEID aAttributeId, VARIANT aVal, 386 BOOL aBackward, 387 __RPC__deref_out_opt ITextRangeProvider** aRetVal) { 388 if (!aRetVal) { 389 return E_INVALIDARG; 390 } 391 *aRetVal = nullptr; 392 TextLeafRange range = GetRange(); 393 if (!range) { 394 return CO_E_OBJNOTCONNECTED; 395 } 396 MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed."); 397 398 VARIANT value{}; 399 400 if (!aBackward) { 401 Maybe<TextLeafPoint> matchingRangeStart{}; 402 // Begin with a range starting at the start of our original range and ending 403 // at the next attribute run start point. 404 TextLeafPoint startPoint = range.Start(); 405 TextLeafPoint endPoint = startPoint; 406 endPoint = endPoint.FindTextAttrsStart(eDirNext); 407 do { 408 // Get the attribute value at the start point. Since we're moving through 409 // text attribute runs, we don't need to check the entire range; this 410 // point's attributes are those of the entire range. 411 GetAttribute(aAttributeId, startPoint, value); 412 // CompareVariants is not valid if types are different. Verify the type 413 // first so the result is well-defined. 414 if (aVal.vt == value.vt && CompareVariants(aVal, value) == 0) { 415 if (!matchingRangeStart) { 416 matchingRangeStart = Some(startPoint); 417 } 418 } else if (matchingRangeStart) { 419 // We fell out of a matching range. We're moving forward, so the 420 // matching range is [matchingRangeStart, startPoint). 421 RefPtr uiaRange = new UiaTextRange( 422 TextLeafRange{matchingRangeStart.value(), startPoint}); 423 uiaRange.forget(aRetVal); 424 return S_OK; 425 } 426 startPoint = endPoint; 427 // Advance only if startPoint != endPoint to avoid infinite loops if 428 // FindTextAttrsStart returns the TextLeafPoint unchanged. This covers 429 // cases like hitting the end of the document. 430 } while ((endPoint = endPoint.FindTextAttrsStart(eDirNext)) && 431 endPoint <= range.End() && startPoint != endPoint); 432 if (matchingRangeStart) { 433 // We found a start point and reached the end of the range. The result is 434 // [matchingRangeStart, stopPoint]. 435 RefPtr uiaRange = new UiaTextRange( 436 TextLeafRange{matchingRangeStart.value(), range.End()}); 437 uiaRange.forget(aRetVal); 438 return S_OK; 439 } 440 } else { 441 Maybe<TextLeafPoint> matchingRangeEnd{}; 442 TextLeafPoint endPoint = range.End(); 443 TextLeafPoint startPoint = endPoint; 444 startPoint = startPoint.FindTextAttrsStart(eDirPrevious); 445 do { 446 GetAttribute(aAttributeId, startPoint, value); 447 if (aVal.vt == value.vt && CompareVariants(aVal, value) == 0) { 448 if (!matchingRangeEnd) { 449 matchingRangeEnd = Some(endPoint); 450 } 451 } else if (matchingRangeEnd) { 452 // We fell out of a matching range. We're moving backward, so the 453 // matching range is [endPoint, matchingRangeEnd). 454 RefPtr uiaRange = 455 new UiaTextRange(TextLeafRange{endPoint, matchingRangeEnd.value()}); 456 uiaRange.forget(aRetVal); 457 return S_OK; 458 } 459 endPoint = startPoint; 460 // Advance only if startPoint != endPoint to avoid infinite loops if 461 // FindTextAttrsStart returns the TextLeafPoint unchanged. This covers 462 // cases like hitting the start of the document. 463 } while ((startPoint = startPoint.FindTextAttrsStart(eDirPrevious)) && 464 range.Start() <= startPoint && startPoint != endPoint); 465 if (matchingRangeEnd) { 466 // We found an end point and reached the start of the range. The result is 467 // [range.Start(), matchingRangeEnd). 468 RefPtr uiaRange = new UiaTextRange( 469 TextLeafRange{range.Start(), matchingRangeEnd.value()}); 470 uiaRange.forget(aRetVal); 471 return S_OK; 472 } 473 } 474 return S_OK; 475 } 476 477 STDMETHODIMP 478 UiaTextRange::FindText(__RPC__in BSTR aText, BOOL aBackward, BOOL aIgnoreCase, 479 __RPC__deref_out_opt ITextRangeProvider** aRetVal) { 480 if (!aRetVal) { 481 return E_INVALIDARG; 482 } 483 *aRetVal = nullptr; 484 TextLeafRange range = GetRange(); 485 if (!range) { 486 return CO_E_OBJNOTCONNECTED; 487 } 488 const TextLeafPoint origStart = range.Start(); 489 MOZ_ASSERT(origStart <= range.End(), "Range must be valid to proceed."); 490 491 // We can't find anything in an empty range. 492 if (origStart == range.End()) { 493 return S_OK; 494 } 495 496 // Iterate over the range's leaf segments and append each leaf's text. Keep 497 // track of the indices in the built string, associating them with the 498 // Accessible pointer whose text begins at that index. 499 nsTArray<std::pair<int32_t, Accessible*>> indexToAcc; 500 nsAutoString rangeText; 501 for (const TextLeafRange leafSegment : range) { 502 const TextLeafPoint segmentStart = leafSegment.Start(); 503 Accessible* startAcc = segmentStart.mAcc; 504 MOZ_ASSERT(startAcc, "Start acc of leaf segment was unexpectedly null."); 505 indexToAcc.EmplaceBack(rangeText.Length(), startAcc); 506 startAcc->AppendTextTo(rangeText, segmentStart.mOffset, 507 leafSegment.End().mOffset - segmentStart.mOffset); 508 } 509 510 // Find the search string's start position in the text of the range, ignoring 511 // case if requested. 512 const nsDependentString searchStr{aText}; 513 const int32_t startIndex = [&]() { 514 if (aIgnoreCase) { 515 ToLowerCase(rangeText); 516 nsAutoString searchStrLower; 517 ToLowerCase(searchStr, searchStrLower); 518 return aBackward ? rangeText.RFind(searchStrLower) 519 : rangeText.Find(searchStrLower); 520 } else { 521 return aBackward ? rangeText.RFind(searchStr) : rangeText.Find(searchStr); 522 } 523 }(); 524 if (startIndex == kNotFound) { 525 return S_OK; 526 } 527 const int32_t endIndex = startIndex + searchStr.Length(); 528 529 // Binary search for the (index, Accessible*) pair where the index is as large 530 // as possible without exceeding the size of the search index. The associated 531 // Accessible* is the Accessible for the resulting TextLeafPoint. 532 auto GetNearestAccLessThanIndex = [&indexToAcc](int32_t aIndex) { 533 MOZ_ASSERT(aIndex >= 0, "Search index is less than 0."); 534 auto itr = 535 std::lower_bound(indexToAcc.begin(), indexToAcc.end(), aIndex, 536 [](const std::pair<int32_t, Accessible*>& aPair, 537 int32_t aIndex) { return aPair.first <= aIndex; }); 538 MOZ_ASSERT(itr != indexToAcc.begin(), 539 "Iterator is unexpectedly at the beginning."); 540 --itr; 541 return itr; 542 }; 543 544 // Get the start offset to use for a given Accessible containing our match. 545 auto getStartOffsetForAcc = [origStart](Accessible* aAcc) { 546 if (aAcc == origStart.mAcc) { 547 // aAcc is in the same leaf in which the origin range starts. The origin 548 // range might start in the middle of the leaf, in which case our gathered 549 // text starts there too. 550 return origStart.mOffset; 551 } 552 return 0; 553 }; 554 555 // Calculate the TextLeafPoint for the start and end of the found text. 556 auto itr = GetNearestAccLessThanIndex(startIndex); 557 Accessible* foundTextStart = itr->second; 558 const int32_t offsetFromStart = 559 startIndex - itr->first + getStartOffsetForAcc(foundTextStart); 560 const TextLeafPoint rangeStart{foundTextStart, offsetFromStart}; 561 562 itr = GetNearestAccLessThanIndex(endIndex); 563 Accessible* foundTextEnd = itr->second; 564 const int32_t offsetFromEndAccStart = 565 endIndex - itr->first + getStartOffsetForAcc(foundTextEnd); 566 const TextLeafPoint rangeEnd{foundTextEnd, offsetFromEndAccStart}; 567 568 TextLeafRange resultRange{rangeStart, rangeEnd}; 569 RefPtr uiaRange = new UiaTextRange(resultRange); 570 uiaRange.forget(aRetVal); 571 return S_OK; 572 } 573 574 template <TEXTATTRIBUTEID Attr> 575 struct AttributeTraits { 576 /* 577 * To define a trait of this type, define the following members on a 578 * specialization of this template struct: 579 * // Define the (Gecko) representation of the attribute type. 580 * using AttrType = <the type associated with the TEXTATTRIBUTEID>; 581 * 582 * // Returns the attribute value at the TextLeafPoint, or Nothing{} if none 583 * // can be calculated. 584 * static Maybe<AttrType> GetValue(TextLeafPoint aPoint); 585 * 586 * // Return the default value specified by the UIA documentation. 587 * static AttrType DefaultValue(); 588 * 589 * // Write the given value to the VARIANT output parameter. This may 590 * // require a non-trivial transformation from Gecko's idea of the value 591 * // into VARIANT form. 592 * static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue); 593 */ 594 }; 595 596 template <TEXTATTRIBUTEID Attr> 597 HRESULT GetAttribute(const TextLeafRange& aRange, VARIANT& aVariant) { 598 // Select the traits of the given TEXTATTRIBUTEID. This helps us choose the 599 // correct functions to call to handle each attribute. 600 using Traits = AttributeTraits<Attr>; 601 using AttrType = typename Traits::AttrType; 602 603 // Get the value at the start point. All other runs in the range must match 604 // this value, otherwise the result is "mixed." 605 const TextLeafPoint end = aRange.End(); 606 TextLeafPoint current = aRange.Start(); 607 Maybe<AttrType> val = Traits::GetValue(current); 608 if (!val) { 609 // Fall back to the UIA-specified default when we don't have an answer. 610 val = Some(Traits::DefaultValue()); 611 } 612 613 // Walk through the range one text attribute run start at a time, poking the 614 // start points to check for the requested attribute. Stop before we hit the 615 // end since the end point is either: 616 // 1. At the start of the one-past-last text attribute run and hence 617 // excluded from the range 618 // 2. After the start of the last text attribute run in the range and hence 619 // tested by that last run's start point 620 while ((current = current.FindTextAttrsStart(eDirNext)) && current < end) { 621 Maybe<AttrType> currentVal = Traits::GetValue(current); 622 if (!currentVal) { 623 // Fall back to the UIA-specified default when we don't have an answer. 624 currentVal = Some(Traits::DefaultValue()); 625 } 626 if (*currentVal != *val) { 627 // If the attribute ever changes, then we need to return "[t]he address 628 // of the value retrieved by the UiaGetReservedMixedAttributeValue 629 // function." 630 aVariant.vt = VT_UNKNOWN; 631 return UiaGetReservedMixedAttributeValue(&aVariant.punkVal); 632 } 633 } 634 635 // Write the value to the VARIANT output parameter. 636 return Traits::WriteToVariant(aVariant, *val); 637 } 638 639 template <TEXTATTRIBUTEID Attr> 640 HRESULT GetAttribute(TextLeafPoint const& aPoint, VARIANT& aVariant) { 641 // Select the traits of the given TEXTATTRIBUTEID. This helps us choose the 642 // correct functions to call to handle each attribute. 643 using Traits = AttributeTraits<Attr>; 644 using AttrType = typename Traits::AttrType; 645 646 // Get the value at the given point. 647 Maybe<AttrType> val = Traits::GetValue(aPoint); 648 if (!val) { 649 // Fall back to the UIA-specified default when we don't have an answer. 650 val = Some(Traits::DefaultValue()); 651 } 652 // Write the value to the VARIANT output parameter. 653 return Traits::WriteToVariant(aVariant, *val); 654 } 655 656 // Dispatch to the proper GetAttribute template specialization for the given 657 // TEXTATTRIBUTEID. T may be a TextLeafPoint or TextLeafRange; this function 658 // will call the appropriate specialization and overload. 659 template <typename T> 660 HRESULT GetAttribute(TEXTATTRIBUTEID aAttributeId, T const& aRangeOrPoint, 661 VARIANT& aRetVal) { 662 switch (aAttributeId) { 663 case UIA_AnnotationTypesAttributeId: 664 return GetAttribute<UIA_AnnotationTypesAttributeId>(aRangeOrPoint, 665 aRetVal); 666 case UIA_FontNameAttributeId: 667 return GetAttribute<UIA_FontNameAttributeId>(aRangeOrPoint, aRetVal); 668 case UIA_FontSizeAttributeId: 669 return GetAttribute<UIA_FontSizeAttributeId>(aRangeOrPoint, aRetVal); 670 case UIA_FontWeightAttributeId: 671 return GetAttribute<UIA_FontWeightAttributeId>(aRangeOrPoint, aRetVal); 672 case UIA_IsHiddenAttributeId: 673 return GetAttribute<UIA_IsHiddenAttributeId>(aRangeOrPoint, aRetVal); 674 case UIA_IsItalicAttributeId: 675 return GetAttribute<UIA_IsItalicAttributeId>(aRangeOrPoint, aRetVal); 676 case UIA_IsReadOnlyAttributeId: 677 return GetAttribute<UIA_IsReadOnlyAttributeId>(aRangeOrPoint, aRetVal); 678 case UIA_StyleIdAttributeId: 679 return GetAttribute<UIA_StyleIdAttributeId>(aRangeOrPoint, aRetVal); 680 case UIA_IsSubscriptAttributeId: 681 return GetAttribute<UIA_IsSubscriptAttributeId>(aRangeOrPoint, aRetVal); 682 case UIA_IsSuperscriptAttributeId: 683 return GetAttribute<UIA_IsSuperscriptAttributeId>(aRangeOrPoint, aRetVal); 684 default: 685 // If the attribute isn't supported, return "[t]he address of the value 686 // retrieved by the UiaGetReservedNotSupportedValue function." 687 aRetVal.vt = VT_UNKNOWN; 688 return UiaGetReservedNotSupportedValue(&aRetVal.punkVal); 689 break; 690 } 691 MOZ_ASSERT_UNREACHABLE("Unhandled UIA Attribute ID"); 692 return S_OK; 693 } 694 695 STDMETHODIMP 696 UiaTextRange::GetAttributeValue(TEXTATTRIBUTEID aAttributeId, 697 __RPC__out VARIANT* aRetVal) { 698 if (!aRetVal) { 699 return E_INVALIDARG; 700 } 701 VariantInit(aRetVal); 702 TextLeafRange range = GetRange(); 703 if (!range) { 704 return CO_E_OBJNOTCONNECTED; 705 } 706 MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed."); 707 return GetAttribute(aAttributeId, range, *aRetVal); 708 } 709 710 STDMETHODIMP 711 UiaTextRange::GetBoundingRectangles(__RPC__deref_out_opt SAFEARRAY** aRetVal) { 712 if (!aRetVal) { 713 return E_INVALIDARG; 714 } 715 *aRetVal = nullptr; 716 TextLeafRange range = GetRange(); 717 if (!range) { 718 return CO_E_OBJNOTCONNECTED; 719 } 720 721 // Get the rectangles for each line. 722 nsTArray<LayoutDeviceIntRect> lineRects = range.LineRects(); 723 // For UIA's purposes, the rectangles of this array are four doubles arranged 724 // in order {left, top, width, height}. 725 SAFEARRAY* rectsVec = SafeArrayCreateVector(VT_R8, 0, lineRects.Length() * 4); 726 if (!rectsVec) { 727 return E_OUTOFMEMORY; 728 } 729 730 // Empty range, return an empty array. 731 if (lineRects.IsEmpty()) { 732 *aRetVal = rectsVec; 733 return S_OK; 734 } 735 736 // Get the double array out of the SAFEARRAY so we can write to it directly. 737 double* safeArrayData = nullptr; 738 HRESULT hr = 739 SafeArrayAccessData(rectsVec, reinterpret_cast<void**>(&safeArrayData)); 740 if (FAILED(hr) || !safeArrayData) { 741 SafeArrayDestroy(rectsVec); 742 return E_FAIL; 743 } 744 745 // Convert the int array to a double array. 746 for (size_t index = 0; index < lineRects.Length(); ++index) { 747 const LayoutDeviceIntRect& lineRect = lineRects[index]; 748 safeArrayData[index * 4 + 0] = static_cast<double>(lineRect.x); 749 safeArrayData[index * 4 + 1] = static_cast<double>(lineRect.y); 750 safeArrayData[index * 4 + 2] = static_cast<double>(lineRect.width); 751 safeArrayData[index * 4 + 3] = static_cast<double>(lineRect.height); 752 } 753 754 // Release the lock on the data. If that fails, bail out. 755 hr = SafeArrayUnaccessData(rectsVec); 756 if (FAILED(hr)) { 757 SafeArrayDestroy(rectsVec); 758 return E_FAIL; 759 } 760 761 *aRetVal = rectsVec; 762 return S_OK; 763 } 764 765 STDMETHODIMP 766 UiaTextRange::GetEnclosingElement( 767 __RPC__deref_out_opt IRawElementProviderSimple** aRetVal) { 768 if (!aRetVal) { 769 return E_INVALIDARG; 770 } 771 *aRetVal = nullptr; 772 TextLeafRange range = GetRange(); 773 if (!range) { 774 return CO_E_OBJNOTCONNECTED; 775 } 776 RemoveExcludedAccessiblesFromRange(range); 777 Accessible* enclosing = 778 range.Start().mAcc->GetClosestCommonInclusiveAncestor(range.End().mAcc); 779 if (!enclosing) { 780 return S_OK; 781 } 782 for (Accessible* acc = enclosing; acc && !acc->IsDoc(); acc = acc->Parent()) { 783 if (nsAccUtils::MustPrune(acc) || 784 // Bug 1950535: Narrator won't report a link correctly when navigating 785 // by character or word if we return a child text leaf. However, if 786 // there is more than a single text leaf, we need to return the child 787 // because it might have semantic significance; e.g. an embedded image. 788 (acc->Role() == roles::LINK && acc->ChildCount() == 1 && 789 acc->FirstChild()->IsText())) { 790 enclosing = acc; 791 break; 792 } 793 } 794 RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(enclosing); 795 uia.forget(aRetVal); 796 return S_OK; 797 } 798 799 STDMETHODIMP 800 UiaTextRange::GetText(int aMaxLength, __RPC__deref_out_opt BSTR* aRetVal) { 801 if (!aRetVal || aMaxLength < -1) { 802 return E_INVALIDARG; 803 } 804 TextLeafRange range = GetRange(); 805 if (!range) { 806 return CO_E_OBJNOTCONNECTED; 807 } 808 nsAutoString text; 809 for (TextLeafRange segment : range) { 810 TextLeafPoint start = segment.Start(); 811 int segmentLength = segment.End().mOffset - start.mOffset; 812 // aMaxLength can be -1 to indicate no maximum. 813 if (aMaxLength >= 0) { 814 const int remaining = aMaxLength - text.Length(); 815 if (segmentLength > remaining) { 816 segmentLength = remaining; 817 } 818 } 819 start.mAcc->AppendTextTo(text, start.mOffset, segmentLength); 820 if (aMaxLength >= 0 && static_cast<int32_t>(text.Length()) >= aMaxLength) { 821 break; 822 } 823 } 824 *aRetVal = ::SysAllocString(text.get()); 825 return S_OK; 826 } 827 828 STDMETHODIMP 829 UiaTextRange::Move(enum TextUnit aUnit, int aCount, __RPC__out int* aRetVal) { 830 if (!aRetVal) { 831 return E_INVALIDARG; 832 } 833 TextLeafRange range = GetRange(); 834 if (!range) { 835 return CO_E_OBJNOTCONNECTED; 836 } 837 TextLeafPoint start = range.Start(); 838 const bool wasCollapsed = start == range.End(); 839 if (!wasCollapsed) { 840 // Per the UIA documentation: 841 // https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-move#remarks 842 // "For a non-degenerate (non-empty) text range, ITextRangeProvider::Move 843 // should normalize and move the text range by performing the following 844 // steps. ... 845 // 2. If necessary, move the resulting text range backward in the document 846 // to the beginning of the requested unit boundary." 847 start = FindBoundary(start, aUnit, eDirPrevious, /* aIncludeOrigin */ true); 848 } 849 if (!MovePoint(start, aUnit, aCount, *aRetVal)) { 850 return E_FAIL; 851 } 852 if (wasCollapsed) { 853 // "For a degenerate text range, ITextRangeProvider::Move should simply move 854 // the text insertion point by the specified number of text units." 855 SetRange({start, start}); 856 } else { 857 // "4. Expand the text range from the degenerate state by moving the ending 858 // endpoint forward by one requested text unit boundary." 859 TextLeafPoint end = FindBoundary(start, aUnit, eDirNext); 860 if (end == start) { 861 // start was already at the last boundary. Move start back to the previous 862 // boundary. 863 start = FindBoundary(start, aUnit, eDirPrevious); 864 // In doing that, we ended up moving 1 less unit. 865 --*aRetVal; 866 } 867 SetRange({start, end}); 868 } 869 return S_OK; 870 } 871 872 STDMETHODIMP 873 UiaTextRange::MoveEndpointByUnit(enum TextPatternRangeEndpoint aEndpoint, 874 enum TextUnit aUnit, int aCount, 875 __RPC__out int* aRetVal) { 876 if (!aRetVal) { 877 return E_INVALIDARG; 878 } 879 TextLeafRange range = GetRange(); 880 if (!range) { 881 return CO_E_OBJNOTCONNECTED; 882 } 883 TextLeafPoint point = GetEndpoint(range, aEndpoint); 884 if (!MovePoint(point, aUnit, aCount, *aRetVal)) { 885 return E_FAIL; 886 } 887 SetEndpoint(aEndpoint, point); 888 return S_OK; 889 } 890 891 STDMETHODIMP 892 UiaTextRange::MoveEndpointByRange( 893 enum TextPatternRangeEndpoint aEndpoint, 894 __RPC__in_opt ITextRangeProvider* aTargetRange, 895 enum TextPatternRangeEndpoint aTargetEndpoint) { 896 if (!aTargetRange) { 897 return E_INVALIDARG; 898 } 899 TextLeafRange origRange = GetRange(); 900 if (!origRange) { 901 return CO_E_OBJNOTCONNECTED; 902 } 903 TextLeafRange targetRange = GetRangeFrom(aTargetRange); 904 if (!targetRange) { 905 return E_INVALIDARG; 906 } 907 TextLeafPoint dest = GetEndpoint(targetRange, aTargetEndpoint); 908 SetEndpoint(aEndpoint, dest); 909 return S_OK; 910 } 911 912 // XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294. 913 MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::Select() { 914 TextLeafRange range = GetRange(); 915 if (!range) { 916 return CO_E_OBJNOTCONNECTED; 917 } 918 if (!range.SetSelection(TextLeafRange::kRemoveAllExistingSelectedRanges, 919 /* aSetFocus */ false)) { 920 return UIA_E_INVALIDOPERATION; 921 } 922 return S_OK; 923 } 924 925 // XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294. 926 MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::AddToSelection() { 927 TextLeafRange range = GetRange(); 928 if (!range) { 929 return CO_E_OBJNOTCONNECTED; 930 } 931 if (!range.SetSelection(-1, /* aSetFocus */ false)) { 932 return UIA_E_INVALIDOPERATION; 933 } 934 return S_OK; 935 } 936 937 // XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294. 938 MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::RemoveFromSelection() { 939 TextLeafRange range = GetRange(); 940 if (!range) { 941 return CO_E_OBJNOTCONNECTED; 942 } 943 NotNull<Accessible*> container = GetSelectionContainer(range); 944 nsTArray<TextLeafRange> ranges; 945 TextLeafRange::GetSelection(container, ranges); 946 auto index = ranges.IndexOf(range); 947 if (index != ranges.NoIndex) { 948 HyperTextAccessibleBase* conHyp = container->AsHyperTextBase(); 949 MOZ_ASSERT(conHyp); 950 conHyp->RemoveFromSelection(index); 951 return S_OK; 952 } 953 // This range isn't in the collection of selected ranges. 954 return UIA_E_INVALIDOPERATION; 955 } 956 957 // XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294. 958 MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP 959 UiaTextRange::ScrollIntoView(BOOL aAlignToTop) { 960 TextLeafRange range = GetRange(); 961 if (!range) { 962 return CO_E_OBJNOTCONNECTED; 963 } 964 range.ScrollIntoView(aAlignToTop 965 ? nsIAccessibleScrollType::SCROLL_TYPE_TOP_LEFT 966 : nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_RIGHT); 967 return S_OK; 968 } 969 970 STDMETHODIMP 971 UiaTextRange::GetChildren(__RPC__deref_out_opt SAFEARRAY** aRetVal) { 972 if (!aRetVal) { 973 return E_INVALIDARG; 974 } 975 *aRetVal = nullptr; 976 TextLeafRange range = GetRange(); 977 if (!range) { 978 return CO_E_OBJNOTCONNECTED; 979 } 980 RemoveExcludedAccessiblesFromRange(range); 981 Accessible* startAcc = range.Start().mAcc; 982 Accessible* endAcc = range.End().mAcc; 983 Accessible* common = startAcc->GetClosestCommonInclusiveAncestor(endAcc); 984 if (!common) { 985 return S_OK; 986 } 987 // Get all the direct children of `common` from `startAcc` through `endAcc`. 988 // Find the index of the direct child containing startAcc. 989 int32_t startIndex = -1; 990 if (startAcc == common) { 991 startIndex = 0; 992 } else { 993 Accessible* child = startAcc; 994 for (;;) { 995 Accessible* parent = child->Parent(); 996 if (parent == common) { 997 startIndex = child->IndexInParent(); 998 break; 999 } 1000 child = parent; 1001 } 1002 MOZ_ASSERT(startIndex >= 0); 1003 } 1004 // Find the index of the direct child containing endAcc. 1005 int32_t endIndex = -1; 1006 if (endAcc == common) { 1007 endIndex = static_cast<int32_t>(common->ChildCount()) - 1; 1008 } else { 1009 Accessible* child = endAcc; 1010 for (;;) { 1011 Accessible* parent = child->Parent(); 1012 if (parent == common) { 1013 endIndex = child->IndexInParent(); 1014 break; 1015 } 1016 child = parent; 1017 } 1018 MOZ_ASSERT(endIndex >= 0); 1019 } 1020 // Now get the children between startIndex and endIndex. 1021 // We guess 30 children because: 1022 // 1. It's unlikely that a client would call GetChildren on a very large range 1023 // because GetChildren is normally only called when reporting content and 1024 // reporting the entire content of a massive range in one hit isn't ideal for 1025 // performance. 1026 // 2. A client is more likely to query the content of a line, paragraph, etc. 1027 // 3. It seems unlikely that there would be more than 30 children in a line or 1028 // paragraph, especially because we're only including children that are 1029 // considered embedded objects by UIA. 1030 AutoTArray<Accessible*, 30> children; 1031 for (int32_t i = startIndex; i <= endIndex; ++i) { 1032 Accessible* child = common->ChildAt(static_cast<uint32_t>(i)); 1033 if (IsUiaEmbeddedObject(child)) { 1034 children.AppendElement(child); 1035 } 1036 } 1037 *aRetVal = AccessibleArrayToUiaArray(children); 1038 return S_OK; 1039 } 1040 1041 /* 1042 * AttributeTraits template specializations 1043 */ 1044 1045 template <> 1046 struct AttributeTraits<UIA_AnnotationTypesAttributeId> { 1047 // Avoiding nsTHashSet here because it has no operator==. 1048 using AttrType = std::unordered_set<int32_t>; 1049 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1050 // Check all of the given annotations. Build a set of the annotations that 1051 // are present at the given TextLeafPoint. 1052 RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes(); 1053 if (!attrs) { 1054 return {}; 1055 } 1056 AttrType annotationsAtPoint{}; 1057 1058 // The "invalid" atom as a key in text attributes could have value 1059 // "spelling", "grammar", or "true". Spelling and grammar map directly to 1060 // UIA. A non-specific "invalid" indicates a generic data validation error, 1061 // and is mapped as such. 1062 if (auto invalid = 1063 attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::invalid)) { 1064 const nsAtom* invalidAtom = invalid->get(); 1065 if (invalidAtom == nsGkAtoms::spelling) { 1066 annotationsAtPoint.insert(AnnotationType_SpellingError); 1067 } else if (invalidAtom == nsGkAtoms::grammar) { 1068 annotationsAtPoint.insert(AnnotationType_GrammarError); 1069 } else if (invalidAtom == nsGkAtoms::_true) { 1070 annotationsAtPoint.insert(AnnotationType_DataValidationError); 1071 } 1072 } 1073 1074 // The presence of the "mark" atom as a key in text attributes indicates a 1075 // highlight at this point. 1076 if (attrs->GetAttribute<bool>(nsGkAtoms::mark)) { 1077 annotationsAtPoint.insert(AnnotationType_Highlighted); 1078 } 1079 1080 return Some(annotationsAtPoint); 1081 } 1082 1083 static AttrType DefaultValue() { 1084 // Per UIA documentation, the default is an empty collection. 1085 return {}; 1086 } 1087 1088 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1089 SAFEARRAY* outputArr = 1090 SafeArrayCreateVector(VT_I4, 0, static_cast<ULONG>(aValue.size())); 1091 if (!outputArr) { 1092 return E_OUTOFMEMORY; 1093 } 1094 1095 // Copy the elements from the unordered_set to the SAFEARRAY. 1096 LONG index = 0; 1097 for (auto value : aValue) { 1098 const HRESULT hr = SafeArrayPutElement(outputArr, &index, &value); 1099 if (FAILED(hr)) { 1100 SafeArrayDestroy(outputArr); 1101 return hr; 1102 } 1103 ++index; 1104 } 1105 1106 aVariant.vt = VT_ARRAY | VT_I4; 1107 aVariant.parray = outputArr; 1108 return S_OK; 1109 } 1110 }; 1111 1112 template <> 1113 struct AttributeTraits<UIA_FontWeightAttributeId> { 1114 using AttrType = int32_t; // LONG, but AccAttributes only accepts int32_t 1115 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1116 RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes(); 1117 if (!attrs) { 1118 return {}; 1119 } 1120 return attrs->GetAttribute<AttrType>(nsGkAtoms::font_weight); 1121 } 1122 1123 static AttrType DefaultValue() { 1124 // See GDI LOGFONT structure and related standards. 1125 return FW_DONTCARE; 1126 } 1127 1128 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1129 aVariant.vt = VT_I4; 1130 aVariant.lVal = aValue; 1131 return S_OK; 1132 } 1133 }; 1134 1135 template <> 1136 struct AttributeTraits<UIA_FontSizeAttributeId> { 1137 using AttrType = FontSize; 1138 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1139 RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes(); 1140 if (!attrs) { 1141 return {}; 1142 } 1143 return attrs->GetAttribute<AttrType>(nsGkAtoms::font_size); 1144 } 1145 1146 static AttrType DefaultValue() { return FontSize{0}; } 1147 1148 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1149 aVariant.vt = VT_I4; 1150 aVariant.lVal = aValue.mValue; 1151 return S_OK; 1152 } 1153 }; 1154 1155 template <> 1156 struct AttributeTraits<UIA_FontNameAttributeId> { 1157 using AttrType = RefPtr<nsAtom>; 1158 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1159 RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes(); 1160 if (!attrs) { 1161 return {}; 1162 } 1163 return attrs->GetAttribute<AttrType>(nsGkAtoms::font_family); 1164 } 1165 1166 static AttrType DefaultValue() { 1167 // Default to the empty string (not null). 1168 return RefPtr<nsAtom>(nsGkAtoms::_empty); 1169 } 1170 1171 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1172 if (!aValue) { 1173 return E_INVALIDARG; 1174 } 1175 BSTR valueBStr = ::SysAllocString(aValue->GetUTF16String()); 1176 if (!valueBStr) { 1177 return E_OUTOFMEMORY; 1178 } 1179 aVariant.vt = VT_BSTR; 1180 aVariant.bstrVal = valueBStr; 1181 return S_OK; 1182 } 1183 }; 1184 1185 template <> 1186 struct AttributeTraits<UIA_IsItalicAttributeId> { 1187 using AttrType = bool; 1188 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1189 RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes(); 1190 if (!attrs) { 1191 return {}; 1192 } 1193 1194 // If the value in the attributes is a RefPtr<nsAtom>, it may be "italic" or 1195 // "normal"; check whether it is "italic". 1196 auto atomResult = 1197 attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::font_style); 1198 if (atomResult) { 1199 MOZ_ASSERT(*atomResult, "Atom must be non-null"); 1200 return Some((*atomResult)->Equals(u"italic"_ns)); 1201 } 1202 // If the FontSlantStyle is not italic, the value is not stored as an nsAtom 1203 // in AccAttributes, so there's no need to check further. 1204 return {}; 1205 } 1206 1207 static AttrType DefaultValue() { return false; } 1208 1209 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1210 aVariant = _variant_t(aValue); 1211 return S_OK; 1212 } 1213 }; 1214 1215 template <> 1216 struct AttributeTraits<UIA_StyleIdAttributeId> { 1217 using AttrType = int32_t; 1218 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1219 Accessible* acc = aPoint.mAcc; 1220 if (!acc || !acc->Parent()) { 1221 return {}; 1222 } 1223 acc = acc->Parent(); 1224 const role role = acc->Role(); 1225 if (role == roles::HEADING) { 1226 switch (acc->GetLevel(true)) { 1227 case 1: 1228 return Some(StyleId_Heading1); 1229 case 2: 1230 return Some(StyleId_Heading2); 1231 case 3: 1232 return Some(StyleId_Heading3); 1233 case 4: 1234 return Some(StyleId_Heading4); 1235 case 5: 1236 return Some(StyleId_Heading5); 1237 case 6: 1238 return Some(StyleId_Heading6); 1239 default: 1240 return {}; 1241 } 1242 } 1243 if (role == roles::BLOCKQUOTE) { 1244 return Some(StyleId_Quote); 1245 } 1246 if (role == roles::EMPHASIS) { 1247 return Some(StyleId_Emphasis); 1248 } 1249 return {}; 1250 } 1251 1252 static AttrType DefaultValue() { return 0; } 1253 1254 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1255 aVariant.vt = VT_I4; 1256 aVariant.lVal = aValue; 1257 return S_OK; 1258 } 1259 }; 1260 1261 template <> 1262 struct AttributeTraits<UIA_IsSubscriptAttributeId> { 1263 using AttrType = bool; 1264 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1265 RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes(); 1266 if (!attrs) { 1267 return {}; 1268 } 1269 auto atomResult = 1270 attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::textPosition); 1271 if (atomResult) { 1272 MOZ_ASSERT(*atomResult, "Atom must be non-null"); 1273 return Some(*atomResult == nsGkAtoms::sub); 1274 } 1275 return {}; 1276 } 1277 1278 static AttrType DefaultValue() { return false; } 1279 1280 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1281 aVariant = _variant_t(aValue); 1282 return S_OK; 1283 } 1284 }; 1285 1286 template <> 1287 struct AttributeTraits<UIA_IsSuperscriptAttributeId> { 1288 using AttrType = bool; 1289 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1290 RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes(); 1291 if (!attrs) { 1292 return {}; 1293 } 1294 auto atomResult = 1295 attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::textPosition); 1296 if (atomResult) { 1297 MOZ_ASSERT(*atomResult, "Atom must be non-null"); 1298 return Some((*atomResult)->Equals(NS_ConvertASCIItoUTF16("super"))); 1299 } 1300 return {}; 1301 } 1302 1303 static AttrType DefaultValue() { return false; } 1304 1305 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1306 aVariant = _variant_t(aValue); 1307 return S_OK; 1308 } 1309 }; 1310 1311 template <> 1312 struct AttributeTraits<UIA_IsHiddenAttributeId> { 1313 using AttrType = bool; 1314 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1315 if (!aPoint.mAcc) { 1316 return {}; 1317 } 1318 const uint64_t state = aPoint.mAcc->State(); 1319 return Some(!!(state & states::INVISIBLE)); 1320 } 1321 1322 static AttrType DefaultValue() { return false; } 1323 1324 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1325 aVariant = _variant_t(aValue); 1326 return S_OK; 1327 } 1328 }; 1329 1330 template <> 1331 struct AttributeTraits<UIA_IsReadOnlyAttributeId> { 1332 using AttrType = bool; 1333 static Maybe<AttrType> GetValue(TextLeafPoint aPoint) { 1334 if (!aPoint.mAcc) { 1335 return {}; 1336 } 1337 Accessible* acc = aPoint.mAcc; 1338 // If the TextLeafPoint we're dealing with is itself a hypertext, don't 1339 // bother checking its parent since this is the Accessible we care about. 1340 if (!acc->IsHyperText()) { 1341 // Check the parent of the leaf, since the leaf itself will never be 1342 // editable, but the parent may. Check for both text fields and 1343 // hypertexts, since we might have something like <input> or a 1344 // contenteditable <span>. 1345 Accessible* parent = acc->Parent(); 1346 if (parent && parent->IsHyperText()) { 1347 acc = parent; 1348 } else { 1349 return Some(true); 1350 } 1351 } 1352 const uint64_t state = acc->State(); 1353 if (state & states::READONLY) { 1354 return Some(true); 1355 } 1356 if (state & states::EDITABLE) { 1357 return Some(false); 1358 } 1359 // Fall back to true if not editable or explicitly marked READONLY. 1360 return Some(true); 1361 } 1362 1363 static AttrType DefaultValue() { 1364 // UIA says the default is false, but we fall back to true in GetValue since 1365 // most things on the web are read-only. 1366 return false; 1367 } 1368 1369 static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) { 1370 aVariant = _variant_t(aValue); 1371 return S_OK; 1372 } 1373 }; 1374 1375 } // namespace mozilla::a11y