MOXTextMarkerDelegate.mm (16606B)
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 <Cocoa/Cocoa.h> 9 #include "DocAccessible.h" 10 11 #import "MOXTextMarkerDelegate.h" 12 13 #include "mozAccessible.h" 14 #include "mozilla/Preferences.h" 15 #include "nsISelectionListener.h" 16 17 using namespace mozilla::a11y; 18 19 #define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug" 20 21 MOZ_RUNINIT static nsTHashMap<nsPtrHashKey<mozilla::a11y::Accessible>, 22 MOXTextMarkerDelegate*> 23 sDelegates; 24 25 @implementation MOXTextMarkerDelegate 26 27 + (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc { 28 MOZ_ASSERT(aDoc); 29 30 MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc); 31 if (!delegate) { 32 delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc]; 33 sDelegates.InsertOrUpdate(aDoc, delegate); 34 [delegate retain]; 35 } 36 37 return delegate; 38 } 39 40 + (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc { 41 MOZ_ASSERT(aDoc); 42 43 MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc); 44 if (delegate) { 45 sDelegates.Remove(aDoc); 46 [delegate release]; 47 } 48 } 49 50 - (id)initWithDoc:(Accessible*)aDoc { 51 MOZ_ASSERT(aDoc, "Cannot init MOXTextDelegate with null"); 52 if ((self = [super init])) { 53 mGeckoDocAccessible = aDoc; 54 } 55 56 mCaretMoveGranularity = nsISelectionListener::NO_AMOUNT; 57 58 return self; 59 } 60 61 - (void)dealloc { 62 [self invalidateSelection]; 63 [super dealloc]; 64 } 65 66 - (void)setSelectionFrom:(Accessible*)startContainer 67 at:(int32_t)startOffset 68 to:(Accessible*)endContainer 69 at:(int32_t)endOffset { 70 GeckoTextMarkerRange selection(GeckoTextMarker(startContainer, startOffset), 71 GeckoTextMarker(endContainer, endOffset)); 72 73 // We store it as an AXTextMarkerRange because it is a safe 74 // way to keep a weak reference - when we need to use the 75 // range we can convert it back to a GeckoTextMarkerRange 76 // and check that it's valid. 77 mSelection = selection.CreateAXTextMarkerRange(); 78 CFRetain(mSelection); 79 } 80 81 - (void)setCaretOffset:(mozilla::a11y::Accessible*)container 82 at:(int32_t)offset 83 moveGranularity:(int32_t)granularity { 84 GeckoTextMarker caretMarker(container, offset); 85 86 mPrevCaret = mCaret; 87 mCaret = caretMarker.CreateAXTextMarker(); 88 mCaretMoveGranularity = granularity; 89 90 CFRetain(mCaret); 91 } 92 93 mozAccessible* GetEditableNativeFromGeckoAccessible(Accessible* aAcc) { 94 // The gecko accessible may not have a native accessible so we need 95 // to walk up the parent chain to find the nearest one. 96 // This happens when caching is enabled and the text marker's accessible 97 // may be a text leaf that is pruned from the platform. 98 for (Accessible* acc = aAcc; acc; acc = acc->Parent()) { 99 if (mozAccessible* mozAcc = GetNativeFromGeckoAccessible(acc)) { 100 return [mozAcc moxEditableAncestor]; 101 } 102 } 103 104 return nil; 105 } 106 107 // This returns an info object to pass with AX SelectedTextChanged events. 108 // It uses the current and previous caret position to make decisions 109 // regarding which attributes to add to the info object. 110 - (NSDictionary*)selectionChangeInfo { 111 GeckoTextMarkerRange selectedGeckoRange = 112 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 113 mGeckoDocAccessible, mSelection); 114 115 int32_t stateChangeType = 116 selectedGeckoRange.Start() == selectedGeckoRange.End() 117 ? AXTextStateChangeTypeSelectionMove 118 : AXTextStateChangeTypeSelectionExtend; 119 120 // This is the base info object, includes the selected marker range and 121 // the change type depending on the collapsed state of the selection. 122 NSMutableDictionary* info = [[@{ 123 @"AXSelectedTextMarkerRange" : selectedGeckoRange.IsValid() 124 ? (__bridge id)mSelection 125 : [NSNull null], 126 @"AXTextStateChangeType" : @(stateChangeType), 127 } mutableCopy] autorelease]; 128 129 GeckoTextMarker caretMarker = 130 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mCaret); 131 GeckoTextMarker prevCaretMarker = 132 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mPrevCaret); 133 134 if (!caretMarker.IsValid()) { 135 // If the current caret is invalid, stop here and return base info. 136 return info; 137 } 138 139 mozAccessible* caretEditable = 140 GetEditableNativeFromGeckoAccessible(caretMarker.Acc()); 141 142 if (!caretEditable && stateChangeType == AXTextStateChangeTypeSelectionMove) { 143 // If we are not in an editable, VO expects AXTextStateSync to be present 144 // and true. 145 info[@"AXTextStateSync"] = @YES; 146 } 147 148 if (!prevCaretMarker.IsValid() || caretMarker == prevCaretMarker) { 149 // If we have no stored previous marker, stop here. 150 return info; 151 } 152 153 mozAccessible* prevCaretEditable = 154 GetEditableNativeFromGeckoAccessible(prevCaretMarker.Acc()); 155 156 if (prevCaretEditable != caretEditable) { 157 // If the caret goes in or out of an editable, consider the 158 // move direction "discontiguous". 159 info[@"AXTextSelectionDirection"] = 160 @(AXTextSelectionDirectionDiscontiguous); 161 if ([[caretEditable moxFocused] boolValue]) { 162 // If the caret is in a new focused editable, VO expects this attribute to 163 // be present and to be true. 164 info[@"AXTextSelectionChangedFocus"] = @YES; 165 } 166 167 return info; 168 } 169 170 bool isForward = prevCaretMarker < caretMarker; 171 int direction = isForward ? AXTextSelectionDirectionNext 172 : AXTextSelectionDirectionPrevious; 173 174 int32_t granularity = AXTextSelectionGranularityUnknown; 175 switch (mCaretMoveGranularity) { 176 case nsISelectionListener::CHARACTER_AMOUNT: 177 case nsISelectionListener::CLUSTER_AMOUNT: 178 granularity = AXTextSelectionGranularityCharacter; 179 break; 180 case nsISelectionListener::WORD_AMOUNT: 181 case nsISelectionListener::WORDNOSPACE_AMOUNT: 182 granularity = AXTextSelectionGranularityWord; 183 break; 184 case nsISelectionListener::LINE_AMOUNT: 185 granularity = AXTextSelectionGranularityLine; 186 break; 187 case nsISelectionListener::BEGINLINE_AMOUNT: 188 direction = AXTextSelectionDirectionBeginning; 189 granularity = AXTextSelectionGranularityLine; 190 break; 191 case nsISelectionListener::ENDLINE_AMOUNT: 192 direction = AXTextSelectionDirectionEnd; 193 granularity = AXTextSelectionGranularityLine; 194 break; 195 case nsISelectionListener::PARAGRAPH_AMOUNT: 196 granularity = AXTextSelectionGranularityParagraph; 197 break; 198 default: 199 break; 200 } 201 202 // Determine selection direction with marker comparison. 203 // If the delta between the two markers is more than one, consider it 204 // a word. Not accurate, but good enough for VO. 205 [info addEntriesFromDictionary:@{ 206 @"AXTextSelectionDirection" : @(direction), 207 @"AXTextSelectionGranularity" : @(granularity) 208 }]; 209 210 return info; 211 } 212 213 - (void)invalidateSelection { 214 CFRelease(mSelection); 215 CFRelease(mCaret); 216 CFRelease(mPrevCaret); 217 mSelection = nil; 218 } 219 220 - (mozilla::a11y::GeckoTextMarkerRange)selection { 221 return mozilla::a11y::GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 222 mGeckoDocAccessible, mSelection); 223 } 224 225 - (AXTextMarkerRef)moxStartTextMarker { 226 GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0); 227 return geckoTextPoint.CreateAXTextMarker(); 228 } 229 230 - (AXTextMarkerRef)moxEndTextMarker { 231 GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 232 nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); 233 return geckoTextPoint.CreateAXTextMarker(); 234 } 235 236 - (AXTextMarkerRangeRef)moxSelectedTextMarkerRange { 237 return mSelection && GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 238 mGeckoDocAccessible, mSelection) 239 .IsValid() 240 ? mSelection 241 : nil; 242 } 243 244 - (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { 245 mozilla::a11y::GeckoTextMarkerRange range = 246 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 247 mGeckoDocAccessible, textMarkerRange); 248 if (!range.IsValid()) { 249 return @""; 250 } 251 252 return range.Text(); 253 } 254 255 - (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { 256 mozilla::a11y::GeckoTextMarkerRange range = 257 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 258 mGeckoDocAccessible, textMarkerRange); 259 if (!range.IsValid()) { 260 return @0; 261 } 262 263 return @(range.Length()); 264 } 265 266 - (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers: 267 (NSArray*)textMarkers { 268 if ([textMarkers count] != 2) { 269 // Don't allow anything but a two member array. 270 return nil; 271 } 272 273 GeckoTextMarker p1 = GeckoTextMarker::MarkerFromAXTextMarker( 274 mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[0]); 275 GeckoTextMarker p2 = GeckoTextMarker::MarkerFromAXTextMarker( 276 mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[1]); 277 278 if (!p1.IsValid() || !p2.IsValid()) { 279 // If either marker is invalid, return nil. 280 return nil; 281 } 282 283 bool ordered = p1 < p2; 284 GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1); 285 286 return range.CreateAXTextMarkerRange(); 287 } 288 289 - (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange: 290 (AXTextMarkerRangeRef)textMarkerRange { 291 mozilla::a11y::GeckoTextMarkerRange range = 292 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 293 mGeckoDocAccessible, textMarkerRange); 294 295 return range.IsValid() ? range.Start().CreateAXTextMarker() : nil; 296 } 297 298 - (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange: 299 (AXTextMarkerRangeRef)textMarkerRange { 300 mozilla::a11y::GeckoTextMarkerRange range = 301 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 302 mGeckoDocAccessible, textMarkerRange); 303 304 return range.IsValid() ? range.End().CreateAXTextMarker() : nil; 305 } 306 307 - (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker: 308 (AXTextMarkerRef)textMarker { 309 GeckoTextMarker geckoTextMarker = 310 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 311 if (!geckoTextMarker.IsValid()) { 312 return nil; 313 } 314 315 return geckoTextMarker.LeftWordRange().CreateAXTextMarkerRange(); 316 } 317 318 - (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker: 319 (AXTextMarkerRef)textMarker { 320 GeckoTextMarker geckoTextMarker = 321 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 322 if (!geckoTextMarker.IsValid()) { 323 return nil; 324 } 325 326 return geckoTextMarker.RightWordRange().CreateAXTextMarkerRange(); 327 } 328 329 - (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker: 330 (AXTextMarkerRef)textMarker { 331 GeckoTextMarker geckoTextMarker = 332 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 333 if (!geckoTextMarker.IsValid()) { 334 return nil; 335 } 336 337 return geckoTextMarker.LineRange().CreateAXTextMarkerRange(); 338 } 339 340 - (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker: 341 (AXTextMarkerRef)textMarker { 342 GeckoTextMarker geckoTextMarker = 343 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 344 if (!geckoTextMarker.IsValid()) { 345 return nil; 346 } 347 348 return geckoTextMarker.LeftLineRange().CreateAXTextMarkerRange(); 349 } 350 351 - (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker: 352 (AXTextMarkerRef)textMarker { 353 GeckoTextMarker geckoTextMarker = 354 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 355 if (!geckoTextMarker.IsValid()) { 356 return nil; 357 } 358 359 return geckoTextMarker.RightLineRange().CreateAXTextMarkerRange(); 360 } 361 362 - (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker: 363 (AXTextMarkerRef)textMarker { 364 GeckoTextMarker geckoTextMarker = 365 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 366 if (!geckoTextMarker.IsValid()) { 367 return nil; 368 } 369 370 return geckoTextMarker.ParagraphRange().CreateAXTextMarkerRange(); 371 } 372 373 // override 374 - (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker: 375 (AXTextMarkerRef)textMarker { 376 GeckoTextMarker geckoTextMarker = 377 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 378 if (!geckoTextMarker.IsValid()) { 379 return nil; 380 } 381 382 return geckoTextMarker.StyleRange().CreateAXTextMarkerRange(); 383 } 384 385 - (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker { 386 GeckoTextMarker geckoTextMarker = 387 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 388 if (!geckoTextMarker.IsValid()) { 389 return nil; 390 } 391 392 if (!geckoTextMarker.Next()) { 393 return nil; 394 } 395 396 return geckoTextMarker.CreateAXTextMarker(); 397 } 398 399 - (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker: 400 (AXTextMarkerRef)textMarker { 401 GeckoTextMarker geckoTextMarker = 402 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 403 if (!geckoTextMarker.IsValid()) { 404 return nil; 405 } 406 407 if (!geckoTextMarker.Previous()) { 408 return nil; 409 } 410 411 return geckoTextMarker.CreateAXTextMarker(); 412 } 413 414 - (NSAttributedString*)moxAttributedStringForTextMarkerRange: 415 (AXTextMarkerRangeRef)textMarkerRange { 416 mozilla::a11y::GeckoTextMarkerRange range = 417 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 418 mGeckoDocAccessible, textMarkerRange); 419 if (!range.IsValid()) { 420 return nil; 421 } 422 423 return range.AttributedText(); 424 } 425 426 - (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { 427 mozilla::a11y::GeckoTextMarkerRange range = 428 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 429 mGeckoDocAccessible, textMarkerRange); 430 if (!range.IsValid()) { 431 return nil; 432 } 433 434 return range.Bounds(); 435 } 436 437 - (NSNumber*)moxIndexForTextMarker:(AXTextMarkerRef)textMarker { 438 GeckoTextMarker geckoTextMarker = 439 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 440 if (!geckoTextMarker.IsValid()) { 441 return nil; 442 } 443 444 GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0), 445 geckoTextMarker); 446 447 return @(range.Length()); 448 } 449 450 - (AXTextMarkerRef)moxTextMarkerForIndex:(NSNumber*)index { 451 GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex( 452 mGeckoDocAccessible, [index integerValue]); 453 if (!geckoTextMarker.IsValid()) { 454 return nil; 455 } 456 457 return geckoTextMarker.CreateAXTextMarker(); 458 } 459 460 - (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker { 461 GeckoTextMarker geckoTextMarker = 462 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 463 if (!geckoTextMarker.IsValid()) { 464 return nil; 465 } 466 467 Accessible* leaf = geckoTextMarker.Leaf(); 468 if (!leaf) { 469 return nil; 470 } 471 472 return GetNativeFromGeckoAccessible(leaf); 473 } 474 475 - (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element { 476 if (![element isKindOfClass:[mozAccessible class]]) { 477 return nil; 478 } 479 480 GeckoTextMarkerRange range((Accessible*)[element geckoAccessible]); 481 return range.CreateAXTextMarkerRange(); 482 } 483 484 - (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker { 485 if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) { 486 return nil; 487 } 488 489 GeckoTextMarker geckoTextMarker = 490 GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker); 491 if (!geckoTextMarker.IsValid()) { 492 return @"<GeckoTextMarker 0x0 [0]>"; 493 } 494 495 return [NSString stringWithFormat:@"<GeckoTextMarker %p [%d]>", 496 geckoTextMarker.Acc(), 497 geckoTextMarker.Offset()]; 498 } 499 500 - (NSString*)moxMozDebugDescriptionForTextMarkerRange: 501 (AXTextMarkerRangeRef)textMarkerRange { 502 if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) { 503 return nil; 504 } 505 506 mozilla::a11y::GeckoTextMarkerRange range = 507 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 508 mGeckoDocAccessible, textMarkerRange); 509 if (!range.IsValid()) { 510 return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>"; 511 } 512 513 return [NSString stringWithFormat:@"<GeckoTextMarkerRange %p [%d] - %p [%d]>", 514 range.Start().Acc(), range.Start().Offset(), 515 range.End().Acc(), range.End().Offset()]; 516 } 517 518 - (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange { 519 mozilla::a11y::GeckoTextMarkerRange range = 520 GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange( 521 mGeckoDocAccessible, textMarkerRange); 522 if (range.IsValid()) { 523 range.Select(); 524 } 525 } 526 527 @end