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 10#include "mozilla/Preferences.h" 11 12#import "MOXTextMarkerDelegate.h" 13 14using namespace mozilla::a11y; 15 16#define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug" 17 18static nsTHashMap<nsUint64HashKey, MOXTextMarkerDelegate*> sDelegates; 19 20@implementation MOXTextMarkerDelegate 21 22+ (id)getOrCreateForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc { 23 MOZ_ASSERT(!aDoc.IsNull()); 24 25 MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc.Bits()); 26 if (!delegate) { 27 delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc]; 28 sDelegates.InsertOrUpdate(aDoc.Bits(), delegate); 29 [delegate retain]; 30 } 31 32 return delegate; 33} 34 35+ (void)destroyForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc { 36 MOZ_ASSERT(!aDoc.IsNull()); 37 38 MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc.Bits()); 39 if (delegate) { 40 sDelegates.Remove(aDoc.Bits()); 41 [delegate release]; 42 } 43} 44 45- (id)initWithDoc:(AccessibleOrProxy)aDoc { 46 MOZ_ASSERT(!aDoc.IsNull(), "Cannot init MOXTextDelegate with null"); 47 if ((self = [super init])) { 48 mGeckoDocAccessible = aDoc; 49 } 50 51 return self; 52} 53 54- (void)dealloc { 55 [self invalidateSelection]; 56 [super dealloc]; 57} 58 59- (void)setSelectionFrom:(AccessibleOrProxy)startContainer 60 at:(int32_t)startOffset 61 to:(AccessibleOrProxy)endContainer 62 at:(int32_t)endOffset { 63 GeckoTextMarkerRange selection(GeckoTextMarker(startContainer, startOffset), 64 GeckoTextMarker(endContainer, endOffset)); 65 66 // We store it as an AXTextMarkerRange because it is a safe 67 // way to keep a weak reference - when we need to use the 68 // range we can convert it back to a GeckoTextMarkerRange 69 // and check that it's valid. 70 mSelection = [selection.CreateAXTextMarkerRange() retain]; 71} 72 73- (void)setCaretOffset:(mozilla::a11y::AccessibleOrProxy)container 74 at:(int32_t)offset { 75 GeckoTextMarker caretMarker(container, offset); 76 77 mPrevCaret = mCaret; 78 mCaret = [caretMarker.CreateAXTextMarker() retain]; 79} 80 81// This returns an info object to pass with AX SelectedTextChanged events. 82// It uses the current and previous caret position to make decisions 83// regarding which attributes to add to the info object. 84- (NSDictionary*)selectionChangeInfo { 85 GeckoTextMarkerRange selectedGeckoRange = 86 GeckoTextMarkerRange(mGeckoDocAccessible, mSelection); 87 int32_t stateChangeType = selectedGeckoRange.mStart == selectedGeckoRange.mEnd 88 ? AXTextStateChangeTypeSelectionMove 89 : AXTextStateChangeTypeSelectionExtend; 90 91 // This is the base info object, includes the selected marker range and 92 // the change type depending on the collapsed state of the selection. 93 NSMutableDictionary* info = [[@{ 94 @"AXSelectedTextMarkerRange" : selectedGeckoRange.IsValid() ? mSelection 95 : [NSNull null], 96 @"AXTextStateChangeType" : @(stateChangeType), 97 } mutableCopy] autorelease]; 98 99 GeckoTextMarker caretMarker(mGeckoDocAccessible, mCaret); 100 GeckoTextMarker prevCaretMarker(mGeckoDocAccessible, mPrevCaret); 101 if (!caretMarker.IsValid()) { 102 // If the current caret is invalid, stop here and return base info. 103 return info; 104 } 105 106 mozAccessible* caretEditable = 107 [GetNativeFromGeckoAccessible(caretMarker.mContainer) 108 moxEditableAncestor]; 109 110 if (!caretEditable && stateChangeType == AXTextStateChangeTypeSelectionMove) { 111 // If we are not in an editable, VO expects AXTextStateSync to be present 112 // and true. 113 info[@"AXTextStateSync"] = @YES; 114 } 115 116 if (!prevCaretMarker.IsValid() || caretMarker == prevCaretMarker) { 117 // If we have no stored previous marker, stop here. 118 return info; 119 } 120 121 mozAccessible* prevCaretEditable = 122 [GetNativeFromGeckoAccessible(prevCaretMarker.mContainer) 123 moxEditableAncestor]; 124 125 if (prevCaretEditable != caretEditable) { 126 // If the caret goes in or out of an editable, consider the 127 // move direction "discontiguous". 128 info[@"AXTextSelectionDirection"] = 129 @(AXTextSelectionDirectionDiscontiguous); 130 if ([[caretEditable moxFocused] boolValue]) { 131 // If the caret is in a new focused editable, VO expects this attribute to 132 // be present and to be true. 133 info[@"AXTextSelectionChangedFocus"] = @YES; 134 } 135 136 return info; 137 } 138 139 bool isForward = prevCaretMarker < caretMarker; 140 uint32_t deltaLength = 141 GeckoTextMarkerRange(isForward ? prevCaretMarker : caretMarker, 142 isForward ? caretMarker : prevCaretMarker) 143 .Length(); 144 145 // Determine selection direction with marker comparison. 146 // If the delta between the two markers is more than one, consider it 147 // a word. Not accurate, but good enough for VO. 148 [info addEntriesFromDictionary:@{ 149 @"AXTextSelectionDirection" : isForward 150 ? @(AXTextSelectionDirectionNext) 151 : @(AXTextSelectionDirectionPrevious), 152 @"AXTextSelectionGranularity" : deltaLength == 1 153 ? @(AXTextSelectionGranularityCharacter) 154 : @(AXTextSelectionGranularityWord) 155 }]; 156 157 return info; 158} 159 160- (void)invalidateSelection { 161 [mSelection release]; 162 [mCaret release]; 163 [mPrevCaret release]; 164 mSelection = nil; 165} 166 167- (mozilla::a11y::GeckoTextMarkerRange)selection { 168 return mozilla::a11y::GeckoTextMarkerRange(mGeckoDocAccessible, mSelection); 169} 170 171- (id)moxStartTextMarker { 172 GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0); 173 return geckoTextPoint.CreateAXTextMarker(); 174} 175 176- (id)moxEndTextMarker { 177 uint32_t characterCount = 178 mGeckoDocAccessible.IsProxy() 179 ? mGeckoDocAccessible.AsProxy()->CharacterCount() 180 : mGeckoDocAccessible.AsAccessible() 181 ->Document() 182 ->AsHyperText() 183 ->CharacterCount(); 184 GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, characterCount); 185 return geckoTextPoint.CreateAXTextMarker(); 186} 187 188- (id)moxSelectedTextMarkerRange { 189 return mSelection && 190 GeckoTextMarkerRange(mGeckoDocAccessible, mSelection).IsValid() 191 ? mSelection 192 : nil; 193} 194 195- (NSString*)moxStringForTextMarkerRange:(id)textMarkerRange { 196 mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, 197 textMarkerRange); 198 if (!range.IsValid()) { 199 return @""; 200 } 201 202 return range.Text(); 203} 204 205- (NSNumber*)moxLengthForTextMarkerRange:(id)textMarkerRange { 206 return @([[self moxStringForTextMarkerRange:textMarkerRange] length]); 207} 208 209- (id)moxTextMarkerRangeForUnorderedTextMarkers:(NSArray*)textMarkers { 210 if ([textMarkers count] != 2) { 211 // Don't allow anything but a two member array. 212 return nil; 213 } 214 215 GeckoTextMarker p1(mGeckoDocAccessible, textMarkers[0]); 216 GeckoTextMarker p2(mGeckoDocAccessible, textMarkers[1]); 217 218 if (!p1.IsValid() || !p2.IsValid()) { 219 // If either marker is invalid, return nil. 220 return nil; 221 } 222 223 bool ordered = p1 < p2; 224 GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1); 225 226 return range.CreateAXTextMarkerRange(); 227} 228 229- (id)moxStartTextMarkerForTextMarkerRange:(id)textMarkerRange { 230 mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, 231 textMarkerRange); 232 233 return range.IsValid() ? range.mStart.CreateAXTextMarker() : nil; 234} 235 236- (id)moxEndTextMarkerForTextMarkerRange:(id)textMarkerRange { 237 mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, 238 textMarkerRange); 239 240 return range.IsValid() ? range.mEnd.CreateAXTextMarker() : nil; 241} 242 243- (id)moxLeftWordTextMarkerRangeForTextMarker:(id)textMarker { 244 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 245 if (!geckoTextMarker.IsValid()) { 246 return nil; 247 } 248 249 return geckoTextMarker.Range(EWhichRange::eLeftWord) 250 .CreateAXTextMarkerRange(); 251} 252 253- (id)moxRightWordTextMarkerRangeForTextMarker:(id)textMarker { 254 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 255 if (!geckoTextMarker.IsValid()) { 256 return nil; 257 } 258 259 return geckoTextMarker.Range(EWhichRange::eRightWord) 260 .CreateAXTextMarkerRange(); 261} 262 263- (id)moxLineTextMarkerRangeForTextMarker:(id)textMarker { 264 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 265 if (!geckoTextMarker.IsValid()) { 266 return nil; 267 } 268 269 return geckoTextMarker.Range(EWhichRange::eLine).CreateAXTextMarkerRange(); 270} 271 272- (id)moxLeftLineTextMarkerRangeForTextMarker:(id)textMarker { 273 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 274 if (!geckoTextMarker.IsValid()) { 275 return nil; 276 } 277 278 return geckoTextMarker.Range(EWhichRange::eLeftLine) 279 .CreateAXTextMarkerRange(); 280} 281 282- (id)moxRightLineTextMarkerRangeForTextMarker:(id)textMarker { 283 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 284 if (!geckoTextMarker.IsValid()) { 285 return nil; 286 } 287 288 return geckoTextMarker.Range(EWhichRange::eRightLine) 289 .CreateAXTextMarkerRange(); 290} 291 292- (id)moxParagraphTextMarkerRangeForTextMarker:(id)textMarker { 293 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 294 if (!geckoTextMarker.IsValid()) { 295 return nil; 296 } 297 298 return geckoTextMarker.Range(EWhichRange::eParagraph) 299 .CreateAXTextMarkerRange(); 300} 301 302// override 303- (id)moxStyleTextMarkerRangeForTextMarker:(id)textMarker { 304 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 305 if (!geckoTextMarker.IsValid()) { 306 return nil; 307 } 308 309 return geckoTextMarker.Range(EWhichRange::eStyle).CreateAXTextMarkerRange(); 310} 311 312- (id)moxNextTextMarkerForTextMarker:(id)textMarker { 313 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 314 if (!geckoTextMarker.IsValid()) { 315 return nil; 316 } 317 318 if (!geckoTextMarker.Next()) { 319 return nil; 320 } 321 322 return geckoTextMarker.CreateAXTextMarker(); 323} 324 325- (id)moxPreviousTextMarkerForTextMarker:(id)textMarker { 326 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 327 if (!geckoTextMarker.IsValid()) { 328 return nil; 329 } 330 331 if (!geckoTextMarker.Previous()) { 332 return nil; 333 } 334 335 return geckoTextMarker.CreateAXTextMarker(); 336} 337 338- (NSAttributedString*)moxAttributedStringForTextMarkerRange: 339 (id)textMarkerRange { 340 mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, 341 textMarkerRange); 342 if (!range.IsValid()) { 343 return nil; 344 } 345 346 return range.AttributedText(); 347} 348 349- (NSValue*)moxBoundsForTextMarkerRange:(id)textMarkerRange { 350 mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, 351 textMarkerRange); 352 if (!range.IsValid()) { 353 return nil; 354 } 355 356 return range.Bounds(); 357} 358 359- (NSNumber*)moxIndexForTextMarker:(id)textMarker { 360 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 361 if (!geckoTextMarker.IsValid()) { 362 return nil; 363 } 364 365 GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0), 366 geckoTextMarker); 367 368 return @(range.Length()); 369} 370 371- (id)moxTextMarkerForIndex:(NSNumber*)index { 372 GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex( 373 mGeckoDocAccessible, [index integerValue]); 374 if (!geckoTextMarker.IsValid()) { 375 return nil; 376 } 377 378 return geckoTextMarker.CreateAXTextMarker(); 379} 380 381- (id)moxUIElementForTextMarker:(id)textMarker { 382 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 383 if (!geckoTextMarker.IsValid()) { 384 return nil; 385 } 386 387 AccessibleOrProxy leaf = geckoTextMarker.Leaf(); 388 if (leaf.IsNull()) { 389 return nil; 390 } 391 392 return GetNativeFromGeckoAccessible(leaf); 393} 394 395- (id)moxTextMarkerRangeForUIElement:(id)element { 396 if (![element isKindOfClass:[mozAccessible class]]) { 397 return nil; 398 } 399 400 GeckoTextMarkerRange range([element geckoAccessible]); 401 return range.CreateAXTextMarkerRange(); 402} 403 404- (NSString*)moxMozDebugDescriptionForTextMarker:(id)textMarker { 405 if (!Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) { 406 return nil; 407 } 408 409 GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker); 410 if (!geckoTextMarker.IsValid()) { 411 return @"<GeckoTextMarker 0x0 [0]>"; 412 } 413 414 return [NSString stringWithFormat:@"<GeckoTextMarker 0x%lx [%d]>", 415 geckoTextMarker.mContainer.Bits(), 416 geckoTextMarker.mOffset]; 417} 418 419- (NSString*)moxMozDebugDescriptionForTextMarkerRange:(id)textMarkerRange { 420 if (!Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) { 421 return nil; 422 } 423 424 mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, 425 textMarkerRange); 426 if (!range.IsValid()) { 427 return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>"; 428 } 429 430 return [NSString 431 stringWithFormat:@"<GeckoTextMarkerRange 0x%lx [%d] - 0x%lx [%d]>", 432 range.mStart.mContainer.Bits(), range.mStart.mOffset, 433 range.mEnd.mContainer.Bits(), range.mEnd.mOffset]; 434} 435 436- (void)moxSetSelectedTextMarkerRange:(id)textMarkerRange { 437 mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, 438 textMarkerRange); 439 if (range.IsValid()) { 440 range.Select(); 441 } 442} 443 444@end 445