2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
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/. */
8 #import "GeckoTextMarker.h"
10 #include "DocAccessible.h"
11 #include "DocAccessibleParent.h"
12 #include "AccAttributes.h"
13 #include "nsCocoaUtils.h"
14 #include "MOXAccessibleBase.h"
15 #include "mozAccessible.h"
17 #include "mozilla/a11y/DocAccessiblePlatformExtParent.h"
22 struct OpaqueGeckoTextMarker {
23 OpaqueGeckoTextMarker(uintptr_t aDoc, uintptr_t aID, int32_t aOffset)
24 : mDoc(aDoc), mID(aID), mOffset(aOffset) {}
25 OpaqueGeckoTextMarker() {}
31 static bool DocumentExists(Accessible* aDoc, uintptr_t aDocPtr) {
32 if (reinterpret_cast<uintptr_t>(aDoc) == aDocPtr) {
36 if (aDoc->IsLocal()) {
37 DocAccessible* docAcc = aDoc->AsLocal()->AsDoc();
38 uint32_t docCount = docAcc->ChildDocumentCount();
39 for (uint32_t i = 0; i < docCount; i++) {
40 if (DocumentExists(docAcc->GetChildDocumentAt(i), aDocPtr)) {
45 DocAccessibleParent* docProxy = aDoc->AsRemote()->AsDoc();
46 size_t docCount = docProxy->ChildDocCount();
47 for (uint32_t i = 0; i < docCount; i++) {
48 if (DocumentExists(docProxy->ChildDocAt(i), aDocPtr)) {
59 GeckoTextMarker::GeckoTextMarker(Accessible* aDoc,
60 AXTextMarkerRef aTextMarker) {
62 OpaqueGeckoTextMarker opaqueMarker;
64 AXTextMarkerGetLength(aTextMarker) == sizeof(OpaqueGeckoTextMarker)) {
65 memcpy(&opaqueMarker, AXTextMarkerGetBytePtr(aTextMarker),
66 sizeof(OpaqueGeckoTextMarker));
67 if (DocumentExists(aDoc, opaqueMarker.mDoc)) {
68 Accessible* doc = reinterpret_cast<Accessible*>(opaqueMarker.mDoc);
69 if (doc->IsRemote()) {
70 mContainer = doc->AsRemote()->AsDoc()->GetAccessible(opaqueMarker.mID);
72 mContainer = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID(
73 reinterpret_cast<void*>(opaqueMarker.mID));
77 mOffset = opaqueMarker.mOffset;
84 GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot,
86 if (aRoot->IsRemote()) {
88 uint64_t containerID = 0;
89 DocAccessibleParent* ipcDoc = aRoot->AsRemote()->Document();
90 Unused << ipcDoc->GetPlatformExtension()->SendOffsetAtIndex(
91 aRoot->AsRemote()->ID(), aIndex, &containerID, &offset);
92 RemoteAccessible* container = ipcDoc->GetAccessible(containerID);
93 return GeckoTextMarker(container, offset);
94 } else if (auto htWrap = static_cast<HyperTextAccessibleWrap*>(
95 aRoot->AsLocal()->AsHyperText())) {
97 HyperTextAccessible* container = nullptr;
98 htWrap->OffsetAtIndex(aIndex, &container, &offset);
99 return GeckoTextMarker(container, offset);
102 return GeckoTextMarker();
105 AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() {
111 if (mContainer->IsRemote()) {
112 doc = mContainer->AsRemote()->Document();
114 doc = mContainer->AsLocal()->Document();
117 uintptr_t identifier =
118 mContainer->IsRemote()
119 ? mContainer->AsRemote()->ID()
120 : reinterpret_cast<uintptr_t>(mContainer->AsLocal()->UniqueID());
122 OpaqueGeckoTextMarker opaqueMarker(reinterpret_cast<uintptr_t>(doc),
123 identifier, mOffset);
124 AXTextMarkerRef cf_text_marker = AXTextMarkerCreate(
125 kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&opaqueMarker),
126 sizeof(OpaqueGeckoTextMarker));
128 return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease];
131 bool GeckoTextMarker::operator<(const GeckoTextMarker& aPoint) const {
132 if (mContainer == aPoint.mContainer) return mOffset < aPoint.mOffset;
134 // Build the chain of parents
135 AutoTArray<Accessible*, 30> parents1, parents2;
136 Accessible* p1 = mContainer;
138 parents1.AppendElement(p1);
142 Accessible* p2 = aPoint.mContainer;
144 parents2.AppendElement(p2);
148 // An empty chain of parents means one of the containers was null.
149 MOZ_ASSERT(parents1.Length() != 0 && parents2.Length() != 0,
150 "have empty chain of parents!");
152 // Find where the parent chain differs
153 uint32_t pos1 = parents1.Length(), pos2 = parents2.Length();
154 for (uint32_t len = std::min(pos1, pos2); len > 0; --len) {
155 Accessible* child1 = parents1.ElementAt(--pos1);
156 Accessible* child2 = parents2.ElementAt(--pos2);
157 if (child1 != child2) {
158 return child1->IndexInParent() < child2->IndexInParent();
163 // If parents1 is a superset of parents2 then mContainer is a
164 // descendant of aPoint.mContainer. The next element down in parents1
165 // is mContainer's ancestor that is the child of aPoint.mContainer.
166 // We compare its end offset in aPoint.mContainer with aPoint.mOffset.
167 Accessible* child = parents1.ElementAt(pos1 - 1);
168 MOZ_ASSERT(child->Parent() == aPoint.mContainer);
169 uint32_t endOffset = child->EndOffset();
170 return endOffset < static_cast<uint32_t>(aPoint.mOffset);
174 // If parents2 is a superset of parents1 then aPoint.mContainer is a
175 // descendant of mContainer. The next element down in parents2
176 // is aPoint.mContainer's ancestor that is the child of mContainer.
177 // We compare its start offset in mContainer with mOffset.
178 Accessible* child = parents2.ElementAt(pos2 - 1);
179 MOZ_ASSERT(child->Parent() == mContainer);
180 uint32_t startOffset = child->StartOffset();
181 return static_cast<uint32_t>(mOffset) <= startOffset;
184 MOZ_ASSERT_UNREACHABLE("Broken tree?!");
188 bool GeckoTextMarker::IsEditableRoot() {
189 uint64_t state = mContainer->IsRemote() ? mContainer->AsRemote()->State()
190 : mContainer->AsLocal()->State();
191 if ((state & states::EDITABLE) == 0) {
195 Accessible* parent = mContainer->Parent();
197 // Not sure when this can happen, but it would technically be an editable
202 state = parent->IsRemote() ? parent->AsRemote()->State()
203 : parent->AsLocal()->State();
205 return (state & states::EDITABLE) == 0;
208 bool GeckoTextMarker::Next() {
209 if (mContainer->IsRemote()) {
210 int32_t nextOffset = 0;
211 uint64_t nextContainerID = 0;
212 DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document();
213 Unused << ipcDoc->GetPlatformExtension()->SendNextClusterAt(
214 mContainer->AsRemote()->ID(), mOffset, &nextContainerID, &nextOffset);
215 RemoteAccessible* nextContainer = ipcDoc->GetAccessible(nextContainerID);
217 nextContainer != mContainer->AsRemote() || nextOffset != mOffset;
218 mContainer = nextContainer;
219 mOffset = nextOffset;
221 } else if (auto htWrap = ContainerAsHyperTextWrap()) {
222 HyperTextAccessible* nextContainer = nullptr;
223 int32_t nextOffset = 0;
224 htWrap->NextClusterAt(mOffset, &nextContainer, &nextOffset);
225 bool moved = nextContainer != htWrap || nextOffset != mOffset;
226 mContainer = nextContainer;
227 mOffset = nextOffset;
234 bool GeckoTextMarker::Previous() {
235 if (mContainer->IsRemote()) {
236 int32_t prevOffset = 0;
237 uint64_t prevContainerID = 0;
238 DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document();
239 Unused << ipcDoc->GetPlatformExtension()->SendPreviousClusterAt(
240 mContainer->AsRemote()->ID(), mOffset, &prevContainerID, &prevOffset);
241 RemoteAccessible* prevContainer = ipcDoc->GetAccessible(prevContainerID);
243 prevContainer != mContainer->AsRemote() || prevOffset != mOffset;
244 mContainer = prevContainer;
245 mOffset = prevOffset;
247 } else if (auto htWrap = ContainerAsHyperTextWrap()) {
248 HyperTextAccessible* prevContainer = nullptr;
249 int32_t prevOffset = 0;
250 htWrap->PreviousClusterAt(mOffset, &prevContainer, &prevOffset);
251 bool moved = prevContainer != htWrap || prevOffset != mOffset;
252 mContainer = prevContainer;
253 mOffset = prevOffset;
260 static uint32_t CharacterCount(Accessible* aContainer) {
261 if (aContainer->IsRemote()) {
262 return aContainer->AsRemote()->CharacterCount();
265 if (aContainer->AsLocal()->IsHyperText()) {
266 return aContainer->AsLocal()->AsHyperText()->CharacterCount();
272 GeckoTextMarkerRange GeckoTextMarker::Range(EWhichRange aRangeType) {
273 MOZ_ASSERT(mContainer);
274 if (mContainer->IsRemote()) {
275 int32_t startOffset = 0, endOffset = 0;
276 uint64_t startContainerID = 0, endContainerID = 0;
277 DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document();
278 bool success = ipcDoc->GetPlatformExtension()->SendRangeAt(
279 mContainer->AsRemote()->ID(), mOffset, aRangeType, &startContainerID,
280 &startOffset, &endContainerID, &endOffset);
282 return GeckoTextMarkerRange(
283 GeckoTextMarker(ipcDoc->GetAccessible(startContainerID), startOffset),
284 GeckoTextMarker(ipcDoc->GetAccessible(endContainerID), endOffset));
286 } else if (auto htWrap = ContainerAsHyperTextWrap()) {
287 int32_t startOffset = 0, endOffset = 0;
288 HyperTextAccessible* startContainer = nullptr;
289 HyperTextAccessible* endContainer = nullptr;
290 htWrap->RangeAt(mOffset, aRangeType, &startContainer, &startOffset,
291 &endContainer, &endOffset);
292 return GeckoTextMarkerRange(GeckoTextMarker(startContainer, startOffset),
293 GeckoTextMarker(endContainer, endOffset));
296 return GeckoTextMarkerRange(GeckoTextMarker(), GeckoTextMarker());
299 Accessible* GeckoTextMarker::Leaf() {
300 MOZ_ASSERT(mContainer);
301 if (mContainer->IsRemote()) {
303 DocAccessibleParent* ipcDoc = mContainer->AsRemote()->Document();
304 Unused << ipcDoc->GetPlatformExtension()->SendLeafAtOffset(
305 mContainer->AsRemote()->ID(), mOffset, &leafID);
306 return ipcDoc->GetAccessible(leafID);
307 } else if (auto htWrap = ContainerAsHyperTextWrap()) {
308 return htWrap->LeafAtOffset(mOffset);
314 // GeckoTextMarkerRange
316 GeckoTextMarkerRange::GeckoTextMarkerRange(
317 Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) {
318 if (!aTextMarkerRange ||
319 CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) {
323 AXTextMarkerRef start_marker(
324 AXTextMarkerRangeCopyStartMarker(aTextMarkerRange));
325 AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange));
327 mStart = GeckoTextMarker(aDoc, start_marker);
328 mEnd = GeckoTextMarker(aDoc, end_marker);
330 CFRelease(start_marker);
331 CFRelease(end_marker);
334 GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) {
335 if (aAccessible->IsHyperText()) {
336 // The accessible is a hypertext. Initialize range to its inner text range.
337 mStart = GeckoTextMarker(aAccessible, 0);
338 mEnd = GeckoTextMarker(aAccessible, (CharacterCount(aAccessible)));
340 // The accessible is not a hypertext (maybe a text leaf?). Initialize range
341 // to its offsets in its container.
342 mStart = GeckoTextMarker(aAccessible->Parent(), 0);
343 mEnd = GeckoTextMarker(aAccessible->Parent(), 0);
344 if (mStart.mContainer->IsRemote()) {
345 DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
346 Unused << ipcDoc->GetPlatformExtension()->SendRangeOfChild(
347 mStart.mContainer->AsRemote()->ID(), aAccessible->AsRemote()->ID(),
348 &mStart.mOffset, &mEnd.mOffset);
349 } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
350 htWrap->RangeOfChild(aAccessible->AsLocal(), &mStart.mOffset,
356 AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() {
361 AXTextMarkerRangeRef cf_text_marker_range =
362 AXTextMarkerRangeCreate(kCFAllocatorDefault, mStart.CreateAXTextMarker(),
363 mEnd.CreateAXTextMarker());
365 return (__bridge AXTextMarkerRangeRef)[(__bridge id)(
366 cf_text_marker_range)autorelease];
369 NSString* GeckoTextMarkerRange::Text() const {
371 if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
372 DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
373 Unused << ipcDoc->GetPlatformExtension()->SendTextForRange(
374 mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
375 mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &text);
376 } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
377 htWrap->TextForRange(text, mStart.mOffset, mEnd.ContainerAsHyperTextWrap(),
380 return nsCocoaUtils::ToNSString(text);
383 static NSColor* ColorFromColor(const Color& aColor) {
384 return [NSColor colorWithCalibratedRed:NS_GET_R(aColor.mValue) / 255.0
385 green:NS_GET_G(aColor.mValue) / 255.0
386 blue:NS_GET_B(aColor.mValue) / 255.0
390 static NSDictionary* StringAttributesFromAttributes(AccAttributes* aAttributes,
391 Accessible* aContainer) {
392 NSMutableDictionary* attrDict =
393 [NSMutableDictionary dictionaryWithCapacity:aAttributes->Count()];
394 NSMutableDictionary* fontAttrDict = [[NSMutableDictionary alloc] init];
395 [attrDict setObject:fontAttrDict forKey:@"AXFont"];
396 for (auto iter : *aAttributes) {
397 if (iter.Name() == nsGkAtoms::backgroundColor) {
398 if (Maybe<Color> value = iter.Value<Color>()) {
399 NSColor* color = ColorFromColor(*value);
400 [attrDict setObject:(__bridge id)color.CGColor
401 forKey:@"AXBackgroundColor"];
403 } else if (iter.Name() == nsGkAtoms::color) {
404 if (Maybe<Color> value = iter.Value<Color>()) {
405 NSColor* color = ColorFromColor(*value);
406 [attrDict setObject:(__bridge id)color.CGColor
407 forKey:@"AXForegroundColor"];
409 } else if (iter.Name() == nsGkAtoms::font_size) {
410 if (Maybe<FontSize> pointSize = iter.Value<FontSize>()) {
411 int32_t fontPixelSize = static_cast<int32_t>(pointSize->mValue * 4 / 3);
412 [fontAttrDict setObject:@(fontPixelSize) forKey:@"AXFontSize"];
414 } else if (iter.Name() == nsGkAtoms::font_family) {
415 nsAutoString fontFamily;
416 iter.ValueAsString(fontFamily);
417 [fontAttrDict setObject:nsCocoaUtils::ToNSString(fontFamily)
418 forKey:@"AXFontFamily"];
419 } else if (iter.Name() == nsGkAtoms::textUnderlineColor) {
420 [attrDict setObject:@1 forKey:@"AXUnderline"];
421 if (Maybe<Color> value = iter.Value<Color>()) {
422 NSColor* color = ColorFromColor(*value);
423 [attrDict setObject:(__bridge id)color.CGColor
424 forKey:@"AXUnderlineColor"];
426 } else if (iter.Name() == nsGkAtoms::invalid) {
427 // XXX: There is currently no attribute for grammar
428 if (auto value = iter.Value<RefPtr<nsAtom>>()) {
429 if (*value == nsGkAtoms::spelling) {
430 [attrDict setObject:@YES
431 forKey:NSAccessibilityMarkedMisspelledTextAttribute];
435 nsAutoString valueStr;
436 iter.ValueAsString(valueStr);
438 iter.NameAsString(keyStr);
439 [attrDict setObject:nsCocoaUtils::ToNSString(valueStr)
440 forKey:nsCocoaUtils::ToNSString(keyStr)];
444 mozAccessible* container = GetNativeFromGeckoAccessible(aContainer);
445 id<MOXAccessible> link =
446 [container moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
447 return [[moxAcc moxRole] isEqualToString:NSAccessibilityLinkRole];
450 [attrDict setObject:link forKey:@"AXLink"];
453 id<MOXAccessible> heading =
454 [container moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
455 return [[moxAcc moxRole] isEqualToString:@"AXHeading"];
458 [attrDict setObject:[heading moxValue] forKey:@"AXHeadingLevel"];
464 NSAttributedString* GeckoTextMarkerRange::AttributedText() const {
465 NSMutableAttributedString* str =
466 [[[NSMutableAttributedString alloc] init] autorelease];
468 if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
469 nsTArray<TextAttributesRun> textAttributesRuns;
470 DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
471 Unused << ipcDoc->GetPlatformExtension()->SendAttributedTextForRange(
472 mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
473 mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &textAttributesRuns);
475 for (size_t i = 0; i < textAttributesRuns.Length(); i++) {
476 AccAttributes* attributes =
477 textAttributesRuns.ElementAt(i).TextAttributes();
478 RemoteAccessible* container =
479 ipcDoc->GetAccessible(textAttributesRuns.ElementAt(i).ContainerID());
481 NSAttributedString* substr = [[[NSAttributedString alloc]
482 initWithString:nsCocoaUtils::ToNSString(
483 textAttributesRuns.ElementAt(i).Text())
484 attributes:StringAttributesFromAttributes(attributes, container)]
487 [str appendAttributedString:substr];
489 } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
490 nsTArray<nsString> texts;
491 nsTArray<LocalAccessible*> containers;
492 nsTArray<RefPtr<AccAttributes>> props;
494 htWrap->AttributedTextForRange(texts, props, containers, mStart.mOffset,
495 mEnd.ContainerAsHyperTextWrap(),
498 MOZ_ASSERT(texts.Length() == props.Length() &&
499 texts.Length() == containers.Length());
501 for (size_t i = 0; i < texts.Length(); i++) {
502 NSAttributedString* substr = [[[NSAttributedString alloc]
503 initWithString:nsCocoaUtils::ToNSString(texts.ElementAt(i))
504 attributes:StringAttributesFromAttributes(
505 props.ElementAt(i), containers.ElementAt(i))]
507 [str appendAttributedString:substr];
514 int32_t GeckoTextMarkerRange::Length() const {
516 if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
517 DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
518 Unused << ipcDoc->GetPlatformExtension()->SendLengthForRange(
519 mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
520 mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &length);
521 } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
522 length = htWrap->LengthForRange(
523 mStart.mOffset, mEnd.ContainerAsHyperTextWrap(), mEnd.mOffset);
529 NSValue* GeckoTextMarkerRange::Bounds() const {
530 LayoutDeviceIntRect rect;
531 if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
532 DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
533 Unused << ipcDoc->GetPlatformExtension()->SendBoundsForRange(
534 mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
535 mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset, &rect);
536 } else if (auto htWrap = mStart.ContainerAsHyperTextWrap()) {
537 rect = htWrap->BoundsForRange(
538 mStart.mOffset, mEnd.ContainerAsHyperTextWrap(), mEnd.mOffset);
541 NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
542 CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
544 NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor,
545 [mainView frame].size.height -
546 static_cast<CGFloat>(rect.y + rect.height) / scaleFactor,
547 static_cast<CGFloat>(rect.width) / scaleFactor,
548 static_cast<CGFloat>(rect.height) / scaleFactor);
550 return [NSValue valueWithRect:r];
553 void GeckoTextMarkerRange::Select() const {
554 if (mStart.mContainer->IsRemote() && mEnd.mContainer->IsRemote()) {
555 DocAccessibleParent* ipcDoc = mStart.mContainer->AsRemote()->Document();
556 Unused << ipcDoc->GetPlatformExtension()->SendSelectRange(
557 mStart.mContainer->AsRemote()->ID(), mStart.mOffset,
558 mEnd.mContainer->AsRemote()->ID(), mEnd.mOffset);
559 } else if (RefPtr<HyperTextAccessibleWrap> htWrap =
560 mStart.ContainerAsHyperTextWrap()) {
561 RefPtr<HyperTextAccessibleWrap> end = mEnd.ContainerAsHyperTextWrap();
562 htWrap->SelectRange(mStart.mOffset, end, mEnd.mOffset);
566 bool GeckoTextMarkerRange::Crop(Accessible* aContainer) {
567 GeckoTextMarker containerStart(aContainer, 0);
568 GeckoTextMarker containerEnd(aContainer, CharacterCount(aContainer));
570 if (mEnd < containerStart || containerEnd < mStart) {
571 // The range ends before the container, or starts after it.
575 if (mStart < containerStart) {
576 // If range start is before container start, adjust range start to
577 // start of container.
578 mStart = containerStart;
581 if (containerEnd < mEnd) {
582 // If range end is after container end, adjust range end to end of