GeckoTextMarker.mm (19043B)
1 /* clang-format off */ 2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 3 /* clang-format on */ 4 /* This Source Code Form is subject to the terms of the Mozilla Public 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 7 8 #import "GeckoTextMarker.h" 9 10 #import "MacUtils.h" 11 12 #include "AccAttributes.h" 13 #include "DocAccessible.h" 14 #include "DocAccessibleParent.h" 15 #include "nsCocoaUtils.h" 16 #include "HyperTextAccessible.h" 17 #include "States.h" 18 #include "nsAccUtils.h" 19 20 namespace mozilla { 21 namespace a11y { 22 23 struct TextMarkerData { 24 TextMarkerData(uintptr_t aDoc, uintptr_t aID, int32_t aOffset) 25 : mDoc(aDoc), mID(aID), mOffset(aOffset) {} 26 TextMarkerData() {} 27 uintptr_t mDoc; 28 uintptr_t mID; 29 int32_t mOffset; 30 }; 31 32 // GeckoTextMarker 33 34 GeckoTextMarker::GeckoTextMarker(Accessible* aAcc, int32_t aOffset) { 35 HyperTextAccessibleBase* ht = aAcc->AsHyperTextBase(); 36 if (ht && aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT && 37 aOffset <= static_cast<int32_t>(ht->CharacterCount())) { 38 mPoint = aAcc->AsHyperTextBase()->ToTextLeafPoint(aOffset); 39 } else { 40 mPoint = TextLeafPoint(aAcc, aOffset); 41 } 42 } 43 44 GeckoTextMarker GeckoTextMarker::MarkerFromAXTextMarker( 45 Accessible* aDoc, AXTextMarkerRef aTextMarker) { 46 MOZ_ASSERT(aDoc); 47 if (!aTextMarker) { 48 return GeckoTextMarker(); 49 } 50 51 if (AXTextMarkerGetLength(aTextMarker) != sizeof(TextMarkerData)) { 52 MOZ_ASSERT_UNREACHABLE("Malformed AXTextMarkerRef"); 53 return GeckoTextMarker(); 54 } 55 56 TextMarkerData markerData; 57 memcpy(&markerData, AXTextMarkerGetBytePtr(aTextMarker), 58 sizeof(TextMarkerData)); 59 60 if (!utils::DocumentExists(aDoc, markerData.mDoc)) { 61 return GeckoTextMarker(); 62 } 63 64 Accessible* doc = reinterpret_cast<Accessible*>(markerData.mDoc); 65 MOZ_ASSERT(doc->IsDoc()); 66 int32_t offset = markerData.mOffset; 67 Accessible* acc = nullptr; 68 if (doc->IsRemote()) { 69 acc = doc->AsRemote()->AsDoc()->GetAccessible(markerData.mID); 70 } else { 71 acc = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID( 72 reinterpret_cast<void*>(markerData.mID)); 73 } 74 75 if (!acc) { 76 return GeckoTextMarker(); 77 } 78 79 return GeckoTextMarker(acc, offset); 80 } 81 82 GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot, 83 int32_t aIndex) { 84 TextLeafRange range( 85 TextLeafPoint(aRoot, 0), 86 TextLeafPoint(aRoot, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT)); 87 int32_t index = aIndex; 88 // Iterate through all segments until we exhausted the index sum 89 // so we can find the segment the index lives in. 90 for (TextLeafRange segment : range) { 91 if (segment.Start().mAcc->IsMenuPopup() && 92 (segment.Start().mAcc->State() & states::INVISIBLE)) { 93 // XXX: Invisible XUL menu popups are in our tree and we need to skip 94 // them. 95 continue; 96 } 97 98 if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) { 99 // XXX: MacOS expects bullets to be in the range's text, but not in 100 // the calculated length! 101 continue; 102 } 103 104 index -= segment.End().mOffset - segment.Start().mOffset; 105 if (index <= 0) { 106 // The index is in the current segment. 107 return GeckoTextMarker(segment.Start().mAcc, 108 segment.End().mOffset + index); 109 } 110 } 111 112 return GeckoTextMarker(); 113 } 114 115 AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() { 116 if (!IsValid()) { 117 return nil; 118 } 119 120 Accessible* doc = nsAccUtils::DocumentFor(mPoint.mAcc); 121 TextMarkerData markerData(reinterpret_cast<uintptr_t>(doc), mPoint.mAcc->ID(), 122 mPoint.mOffset); 123 AXTextMarkerRef cf_text_marker = AXTextMarkerCreate( 124 kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&markerData), 125 sizeof(TextMarkerData)); 126 127 return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease]; 128 } 129 130 bool GeckoTextMarker::Next() { 131 TextLeafPoint next = 132 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext, 133 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); 134 135 if (next && next != mPoint) { 136 mPoint = next; 137 return true; 138 } 139 140 return false; 141 } 142 143 bool GeckoTextMarker::Previous() { 144 TextLeafPoint prev = 145 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 146 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); 147 if (prev && mPoint != prev) { 148 mPoint = prev; 149 return true; 150 } 151 152 return false; 153 } 154 155 /** 156 * Return true if the given point is inside editable content. 157 */ 158 static bool IsPointInEditable(const TextLeafPoint& aPoint) { 159 if (aPoint.mAcc) { 160 if (aPoint.mAcc->State() & states::EDITABLE) { 161 return true; 162 } 163 164 Accessible* parent = aPoint.mAcc->Parent(); 165 if (parent && (parent->State() & states::EDITABLE)) { 166 return true; 167 } 168 } 169 170 return false; 171 } 172 173 GeckoTextMarkerRange GeckoTextMarker::LeftWordRange() const { 174 bool includeCurrentInStart = !mPoint.IsParagraphStart(true); 175 if (includeCurrentInStart) { 176 TextLeafPoint prevChar = 177 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 178 if (!prevChar.IsSpace()) { 179 includeCurrentInStart = false; 180 } 181 } 182 183 TextLeafPoint start = mPoint.FindBoundary( 184 nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious, 185 includeCurrentInStart 186 ? (TextLeafPoint::BoundaryFlags::eIncludeOrigin | 187 TextLeafPoint::BoundaryFlags::eStopInEditable | 188 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker) 189 : (TextLeafPoint::BoundaryFlags::eStopInEditable | 190 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker)); 191 192 TextLeafPoint end; 193 if (start == mPoint) { 194 end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirPrevious, 195 TextLeafPoint::BoundaryFlags::eStopInEditable); 196 } 197 198 if (start != mPoint || end == start) { 199 end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext, 200 TextLeafPoint::BoundaryFlags::eStopInEditable); 201 if (end < mPoint && IsPointInEditable(end) && !IsPointInEditable(mPoint)) { 202 start = end; 203 end = mPoint; 204 } 205 } 206 207 return GeckoTextMarkerRange(start < end ? start : end, 208 start < end ? end : start); 209 } 210 211 GeckoTextMarkerRange GeckoTextMarker::RightWordRange() const { 212 TextLeafPoint prevChar = 213 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 214 TextLeafPoint::BoundaryFlags::eStopInEditable); 215 216 if (prevChar != mPoint && mPoint.IsParagraphStart(true)) { 217 return GeckoTextMarkerRange(mPoint, mPoint); 218 } 219 220 TextLeafPoint end = 221 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext, 222 TextLeafPoint::BoundaryFlags::eStopInEditable); 223 224 if (end == mPoint) { 225 // No word to the right of this point. 226 return GeckoTextMarkerRange(mPoint, mPoint); 227 } 228 229 TextLeafPoint start = 230 end.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious, 231 TextLeafPoint::BoundaryFlags::eStopInEditable); 232 233 if (start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext, 234 TextLeafPoint::BoundaryFlags::eStopInEditable) < 235 mPoint) { 236 // Word end is inside of an input to the left of this. 237 return GeckoTextMarkerRange(mPoint, mPoint); 238 } 239 240 if (mPoint < start) { 241 end = start; 242 start = mPoint; 243 } 244 245 return GeckoTextMarkerRange(start < end ? start : end, 246 start < end ? end : start); 247 } 248 249 GeckoTextMarkerRange GeckoTextMarker::LineRange() const { 250 TextLeafPoint start = mPoint.FindBoundary( 251 nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious, 252 TextLeafPoint::BoundaryFlags::eStopInEditable | 253 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker | 254 TextLeafPoint::BoundaryFlags::eIncludeOrigin); 255 // If this is a blank line containing only a line feed, the start boundary 256 // is the same as the end boundary. We do not want to walk to the end of the 257 // next line. 258 TextLeafPoint end = 259 start.IsLineFeedChar() 260 ? start 261 : start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext, 262 TextLeafPoint::BoundaryFlags::eStopInEditable); 263 264 return GeckoTextMarkerRange(start, end); 265 } 266 267 GeckoTextMarkerRange GeckoTextMarker::LeftLineRange() const { 268 TextLeafPoint start = mPoint.FindBoundary( 269 nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious, 270 TextLeafPoint::BoundaryFlags::eStopInEditable | 271 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); 272 TextLeafPoint end = 273 start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext, 274 TextLeafPoint::BoundaryFlags::eStopInEditable); 275 276 return GeckoTextMarkerRange(start, end); 277 } 278 279 GeckoTextMarkerRange GeckoTextMarker::RightLineRange() const { 280 TextLeafPoint end = 281 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext, 282 TextLeafPoint::BoundaryFlags::eStopInEditable); 283 TextLeafPoint start = 284 end.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious, 285 TextLeafPoint::BoundaryFlags::eStopInEditable); 286 287 return GeckoTextMarkerRange(start, end); 288 } 289 290 GeckoTextMarkerRange GeckoTextMarker::ParagraphRange() const { 291 // XXX: WebKit gets trapped in inputs. Maybe we shouldn't? 292 TextLeafPoint end = 293 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirNext, 294 TextLeafPoint::BoundaryFlags::eStopInEditable); 295 TextLeafPoint start = 296 end.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirPrevious, 297 TextLeafPoint::BoundaryFlags::eStopInEditable); 298 299 return GeckoTextMarkerRange(start, end); 300 } 301 302 GeckoTextMarkerRange GeckoTextMarker::StyleRange() const { 303 if (mPoint.mOffset == 0) { 304 // If the marker is on the boundary between two leafs, MacOS expects the 305 // previous leaf. 306 TextLeafPoint prev = mPoint.FindBoundary( 307 nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 308 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); 309 if (prev != mPoint) { 310 return GeckoTextMarker(prev).StyleRange(); 311 } 312 } 313 314 TextLeafPoint start(mPoint.mAcc, 0); 315 TextLeafPoint end(mPoint.mAcc, nsAccUtils::TextLength(mPoint.mAcc)); 316 return GeckoTextMarkerRange(start, end); 317 } 318 319 Accessible* GeckoTextMarker::Leaf() { 320 MOZ_ASSERT(mPoint.mAcc); 321 Accessible* acc = mPoint.mAcc; 322 if (mPoint.mOffset == 0) { 323 // If the marker is on the boundary between two leafs, MacOS expects the 324 // previous leaf. 325 TextLeafPoint prev = mPoint.FindBoundary( 326 nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, 327 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker); 328 acc = prev.mAcc; 329 } 330 331 Accessible* parent = acc->Parent(); 332 return parent && nsAccUtils::MustPrune(parent) ? parent : acc; 333 } 334 335 // GeckoTextMarkerRange 336 337 GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) { 338 mRange = TextLeafRange( 339 TextLeafPoint(aAccessible, 0), 340 TextLeafPoint(aAccessible, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT)); 341 } 342 343 GeckoTextMarkerRange GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 344 Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) { 345 if (!aTextMarkerRange || 346 CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) { 347 return GeckoTextMarkerRange(); 348 } 349 350 AXTextMarkerRef start_marker( 351 AXTextMarkerRangeCopyStartMarker(aTextMarkerRange)); 352 AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange)); 353 354 GeckoTextMarker start = 355 GeckoTextMarker::MarkerFromAXTextMarker(aDoc, start_marker); 356 GeckoTextMarker end = 357 GeckoTextMarker::MarkerFromAXTextMarker(aDoc, end_marker); 358 359 CFRelease(start_marker); 360 CFRelease(end_marker); 361 362 return GeckoTextMarkerRange(start, end); 363 } 364 365 AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() { 366 if (!IsValid()) { 367 return nil; 368 } 369 370 GeckoTextMarker start = GeckoTextMarker(mRange.Start()); 371 GeckoTextMarker end = GeckoTextMarker(mRange.End()); 372 373 AXTextMarkerRangeRef cf_text_marker_range = 374 AXTextMarkerRangeCreate(kCFAllocatorDefault, start.CreateAXTextMarker(), 375 end.CreateAXTextMarker()); 376 377 return (__bridge AXTextMarkerRangeRef)[(__bridge id)( 378 cf_text_marker_range)autorelease]; 379 } 380 381 NSString* GeckoTextMarkerRange::Text() const { 382 if (mRange.Start() == mRange.End()) { 383 return @""; 384 } 385 386 if ((mRange.Start().mAcc == mRange.End().mAcc) && 387 (mRange.Start().mAcc->ChildCount() == 0) && 388 (mRange.Start().mAcc->State() & states::EDITABLE)) { 389 return @""; 390 } 391 392 nsAutoString text; 393 TextLeafPoint prev = mRange.Start().FindBoundary( 394 nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 395 TextLeafRange range = 396 prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER 397 ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End()) 398 : mRange; 399 400 for (TextLeafRange segment : range) { 401 TextLeafPoint start = segment.Start(); 402 if (start.mAcc->IsMenuPopup() && 403 (start.mAcc->State() & states::INVISIBLE)) { 404 // XXX: Invisible XUL menu popups are in our tree and we need to skip 405 // them. 406 continue; 407 } 408 if (start.mAcc->IsTextField() && start.mAcc->ChildCount() == 0) { 409 continue; 410 } 411 412 start.mAcc->AppendTextTo(text, start.mOffset, 413 segment.End().mOffset - start.mOffset); 414 } 415 416 return nsCocoaUtils::ToNSString(text); 417 } 418 419 static void AppendTextToAttributedString( 420 NSMutableAttributedString* aAttributedString, Accessible* aAccessible, 421 const nsString& aString, AccAttributes* aAttributes) { 422 NSAttributedString* substr = [[[NSAttributedString alloc] 423 initWithString:nsCocoaUtils::ToNSString(aString) 424 attributes:utils::StringAttributesFromAccAttributes( 425 aAttributes, aAccessible)] autorelease]; 426 427 [aAttributedString appendAttributedString:substr]; 428 } 429 430 static RefPtr<AccAttributes> GetTextAttributes(TextLeafPoint aPoint) { 431 RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes(); 432 if (!attrs) { 433 // If we can't fetch text attributes for the given point, return null. 434 // We avoid creating a new AccAttributes here because our AttributedText() 435 // code below relies on this null return value to indicate we're dealing 436 // with a non-text control. 437 return nullptr; 438 } 439 // Mac expects some object properties to be exposed as text attributes. We 440 // add these here rather than in utils::StringAttributesFromAccAttributes so 441 // we can use AccAttributes::Equal to determine whether we need to start a new 442 // run, rather than needing additional special case comparisons. 443 for (Accessible* ancestor = aPoint.mAcc->Parent(); 444 ancestor && !ancestor->IsDoc(); ancestor = ancestor->Parent()) { 445 if (ancestor->Role() == roles::MARK) { 446 attrs->SetAttribute(nsGkAtoms::mark, true); 447 } 448 } 449 return attrs; 450 } 451 452 NSAttributedString* GeckoTextMarkerRange::AttributedText() const { 453 NSMutableAttributedString* str = 454 [[[NSMutableAttributedString alloc] init] autorelease]; 455 456 if (mRange.Start() == mRange.End()) { 457 return str; 458 } 459 460 if ((mRange.Start().mAcc == mRange.End().mAcc) && 461 (mRange.Start().mAcc->ChildCount() == 0) && 462 (mRange.Start().mAcc->IsTextField())) { 463 return str; 464 } 465 466 TextLeafPoint prev = mRange.Start().FindBoundary( 467 nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 468 TextLeafRange range = 469 prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER 470 ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End()) 471 : mRange; 472 473 nsAutoString text; 474 TextLeafPoint start = range.Start(); 475 const TextLeafPoint stop = range.End(); 476 RefPtr<AccAttributes> currentRun = GetTextAttributes(start); 477 Accessible* runAcc = start.mAcc; 478 do { 479 TextLeafPoint attributesNext = start.FindTextAttrsStart(eDirNext, false); 480 if (stop < attributesNext) { 481 attributesNext = stop; 482 } 483 if (start.mAcc->IsMenuPopup() && 484 (start.mAcc->State() & states::INVISIBLE)) { 485 // XXX: Invisible XUL menu popups are in our tree and we need to skip 486 // them. 487 start = attributesNext; 488 continue; 489 } 490 RefPtr<AccAttributes> attributes = GetTextAttributes(start); 491 if (!currentRun || !attributes || !attributes->Equal(currentRun)) { 492 // If currentRun is null this is a non-text control and we will 493 // append a run with no text or attributes, just an AXAttachment 494 // referencing this accessible. 495 AppendTextToAttributedString(str, runAcc, text, currentRun); 496 text.Truncate(); 497 currentRun = attributes; 498 runAcc = start.mAcc; 499 } 500 for (TextLeafRange segment : TextLeafRange(start, attributesNext)) { 501 TextLeafPoint segStart = segment.Start(); 502 segStart.mAcc->AppendTextTo(text, segStart.mOffset, 503 segment.End().mOffset - segStart.mOffset); 504 } 505 start = attributesNext; 506 } while (start != stop); 507 508 if (!text.IsEmpty()) { 509 AppendTextToAttributedString(str, runAcc, text, currentRun); 510 } 511 512 return str; 513 } 514 515 int32_t GeckoTextMarkerRange::Length() const { 516 int32_t length = 0; 517 for (TextLeafRange segment : mRange) { 518 if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) { 519 // XXX: MacOS expects bullets to be in the range's text, but not in 520 // the calculated length! 521 continue; 522 } 523 length += segment.End().mOffset - segment.Start().mOffset; 524 } 525 526 return length; 527 } 528 529 NSValue* GeckoTextMarkerRange::Bounds() const { 530 LayoutDeviceIntRect rect = mRange ? mRange.Bounds() : LayoutDeviceIntRect(); 531 // We need to find the NSScreen that this range belongs to. Because we 532 // are not guaranteed to get a native accessible for the range's start or 533 // end acc (whitespace, for ex. has no native acc) we fetch the doc 534 // and then use its native acc to retrieve the screen. 535 Accessible* acc = nsAccUtils::DocumentFor(mRange.Start().mAcc); 536 NSScreen* screen = 537 utils::GetNSScreenForAcc(GetNativeFromGeckoAccessible(acc)); 538 CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(screen); 539 540 // Regardless of screen selected above, VO is only happy if we use the 541 // main screen height for Y coordinate conversion. This is consistent with 542 // moxHitTest and moxFrame. 543 NSScreen* mainView = [[NSScreen screens] objectAtIndex:0]; 544 NSRect r = 545 NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor, 546 [mainView frame].size.height - 547 static_cast<CGFloat>(rect.y + rect.height) / scaleFactor, 548 static_cast<CGFloat>(rect.width) / scaleFactor, 549 static_cast<CGFloat>(rect.height) / scaleFactor); 550 551 return [NSValue valueWithRect:r]; 552 } 553 554 void GeckoTextMarkerRange::Select() const { mRange.SetSelection(0); } 555 556 } // namespace a11y 557 } // namespace mozilla