1/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2/* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6#include "Accessible-inl.h" 7#include "HyperTextAccessible-inl.h" 8#include "TextLeafAccessible.h" 9 10#include "nsCocoaUtils.h" 11#include "nsObjCExceptions.h" 12 13#import "mozTextAccessible.h" 14 15using namespace mozilla::a11y; 16 17inline bool 18ToNSRange(id aValue, NSRange* aRange) 19{ 20 NS_PRECONDITION(aRange, "aRange is nil"); 21 22 if ([aValue isKindOfClass:[NSValue class]] && 23 strcmp([(NSValue*)aValue objCType], @encode(NSRange)) == 0) { 24 *aRange = [aValue rangeValue]; 25 return true; 26 } 27 28 return false; 29} 30 31inline NSString* 32ToNSString(id aValue) 33{ 34 if ([aValue isKindOfClass:[NSString class]]) { 35 return aValue; 36 } 37 38 return nil; 39} 40 41@interface mozTextAccessible () 42- (NSString*)subrole; 43- (NSString*)selectedText; 44- (NSValue*)selectedTextRange; 45- (NSValue*)visibleCharacterRange; 46- (long)textLength; 47- (BOOL)isReadOnly; 48- (NSNumber*)caretLineNumber; 49- (void)setText:(NSString*)newText; 50- (NSString*)text; 51- (NSString*)stringFromRange:(NSRange*)range; 52@end 53 54@implementation mozTextAccessible 55 56- (BOOL)accessibilityIsIgnored 57{ 58 return ![self getGeckoAccessible] && ![self getProxyAccessible]; 59} 60 61- (NSArray*)accessibilityAttributeNames 62{ 63 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; 64 65 static NSMutableArray* supportedAttributes = nil; 66 if (!supportedAttributes) { 67 // text-specific attributes to supplement the standard one 68 supportedAttributes = [[NSMutableArray alloc] initWithObjects: 69 NSAccessibilitySelectedTextAttribute, // required 70 NSAccessibilitySelectedTextRangeAttribute, // required 71 NSAccessibilityNumberOfCharactersAttribute, // required 72 NSAccessibilityVisibleCharacterRangeAttribute, // required 73 NSAccessibilityInsertionPointLineNumberAttribute, 74 @"AXRequired", 75 @"AXInvalid", 76 nil 77 ]; 78 [supportedAttributes addObjectsFromArray:[super accessibilityAttributeNames]]; 79 } 80 return supportedAttributes; 81 82 NS_OBJC_END_TRY_ABORT_BLOCK_NIL; 83} 84 85- (id)accessibilityAttributeValue:(NSString*)attribute 86{ 87 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; 88 89 if ([attribute isEqualToString:NSAccessibilityNumberOfCharactersAttribute]) 90 return [NSNumber numberWithInt:[self textLength]]; 91 92 if ([attribute isEqualToString:NSAccessibilityInsertionPointLineNumberAttribute]) 93 return [self caretLineNumber]; 94 95 if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) 96 return [self selectedTextRange]; 97 98 if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) 99 return [self selectedText]; 100 101 if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) 102 return @""; 103 104 if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { 105 // Apple's SpeechSynthesisServer expects AXValue to return an AXStaticText 106 // object's AXSelectedText attribute. See bug 674612 for details. 107 // Also if there is no selected text, we return the full text. 108 // See bug 369710 for details. 109 if ([[self role] isEqualToString:NSAccessibilityStaticTextRole]) { 110 NSString* selectedText = [self selectedText]; 111 return (selectedText && [selectedText length]) ? selectedText : [self text]; 112 } 113 114 return [self text]; 115 } 116 117 if (AccessibleWrap* accWrap = [self getGeckoAccessible]) { 118 if ([attribute isEqualToString:@"AXRequired"]) { 119 return [NSNumber numberWithBool:!!(accWrap->State() & states::REQUIRED)]; 120 } 121 122 if ([attribute isEqualToString:@"AXInvalid"]) { 123 return [NSNumber numberWithBool:!!(accWrap->State() & states::INVALID)]; 124 } 125 } else if (ProxyAccessible* proxy = [self getProxyAccessible]) { 126 if ([attribute isEqualToString:@"AXRequired"]) { 127 return [NSNumber numberWithBool:!!(proxy->State() & states::REQUIRED)]; 128 } 129 130 if ([attribute isEqualToString:@"AXInvalid"]) { 131 return [NSNumber numberWithBool:!!(proxy->State() & states::INVALID)]; 132 } 133 } 134 135 if ([attribute isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) 136 return [self visibleCharacterRange]; 137 138 // let mozAccessible handle all other attributes 139 return [super accessibilityAttributeValue:attribute]; 140 141 NS_OBJC_END_TRY_ABORT_BLOCK_NIL; 142} 143 144- (NSArray*)accessibilityParameterizedAttributeNames 145{ 146 static NSArray* supportedParametrizedAttributes = nil; 147 // text specific parametrized attributes 148 if (!supportedParametrizedAttributes) { 149 supportedParametrizedAttributes = [[NSArray alloc] initWithObjects: 150 NSAccessibilityStringForRangeParameterizedAttribute, 151 NSAccessibilityLineForIndexParameterizedAttribute, 152 NSAccessibilityRangeForLineParameterizedAttribute, 153 NSAccessibilityAttributedStringForRangeParameterizedAttribute, 154 NSAccessibilityBoundsForRangeParameterizedAttribute, 155#if DEBUG 156 NSAccessibilityRangeForPositionParameterizedAttribute, 157 NSAccessibilityRangeForIndexParameterizedAttribute, 158 NSAccessibilityRTFForRangeParameterizedAttribute, 159 NSAccessibilityStyleRangeForIndexParameterizedAttribute, 160#endif 161 nil 162 ]; 163 } 164 return supportedParametrizedAttributes; 165} 166 167- (id)accessibilityAttributeValue:(NSString*)attribute forParameter:(id)parameter 168{ 169 AccessibleWrap* accWrap = [self getGeckoAccessible]; 170 ProxyAccessible* proxy = [self getProxyAccessible]; 171 172 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 173 if (!textAcc && !proxy) 174 return nil; 175 176 if ([attribute isEqualToString:NSAccessibilityStringForRangeParameterizedAttribute]) { 177 NSRange range; 178 if (!ToNSRange(parameter, &range)) { 179#if DEBUG 180 NSLog(@"%@: range not set", attribute); 181#endif 182 return @""; 183 } 184 185 return [self stringFromRange:&range]; 186 } 187 188 if ([attribute isEqualToString:NSAccessibilityRangeForLineParameterizedAttribute]) { 189 // XXX: actually get the integer value for the line # 190 return [NSValue valueWithRange:NSMakeRange(0, [self textLength])]; 191 } 192 193 if ([attribute isEqualToString:NSAccessibilityAttributedStringForRangeParameterizedAttribute]) { 194 NSRange range; 195 if (!ToNSRange(parameter, &range)) { 196#if DEBUG 197 NSLog(@"%@: range not set", attribute); 198#endif 199 return @""; 200 } 201 202 return [[[NSAttributedString alloc] initWithString:[self stringFromRange:&range]] autorelease]; 203 } 204 205 if ([attribute isEqualToString:NSAccessibilityLineForIndexParameterizedAttribute]) { 206 // XXX: actually return the line # 207 return [NSNumber numberWithInt:0]; 208 } 209 210 if ([attribute isEqualToString:NSAccessibilityBoundsForRangeParameterizedAttribute]) { 211 NSRange range; 212 if (!ToNSRange(parameter, &range)) { 213#if DEBUG 214 NSLog(@"%@:no range", attribute); 215#endif 216 return nil; 217 } 218 219 int32_t start = range.location; 220 int32_t end = start + range.length; 221 DesktopIntRect bounds; 222 if (textAcc) { 223 bounds = 224 DesktopIntRect::FromUnknownRect(textAcc->TextBounds(start, end)); 225 } else if (proxy) { 226 bounds = 227 DesktopIntRect::FromUnknownRect(proxy->TextBounds(start, end)); 228 } 229 230 return [NSValue valueWithRect:nsCocoaUtils::GeckoRectToCocoaRect(bounds)]; 231 } 232 233#if DEBUG 234 NSLog(@"unhandled attribute:%@ forParameter:%@", attribute, parameter); 235#endif 236 237 return nil; 238} 239 240- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute 241{ 242 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; 243 244 if ([attribute isEqualToString:NSAccessibilityValueAttribute]) 245 return ![self isReadOnly]; 246 247 if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute] || 248 [attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute] || 249 [attribute isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) 250 return YES; 251 252 return [super accessibilityIsAttributeSettable:attribute]; 253 254 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); 255} 256 257- (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute 258{ 259 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 260 261 AccessibleWrap* accWrap = [self getGeckoAccessible]; 262 ProxyAccessible* proxy = [self getProxyAccessible]; 263 264 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 265 if (!textAcc && !proxy) 266 return; 267 268 if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { 269 [self setText:ToNSString(value)]; 270 271 return; 272 } 273 274 if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) { 275 NSString* stringValue = ToNSString(value); 276 if (!stringValue) 277 return; 278 279 int32_t start = 0, end = 0; 280 nsString text; 281 if (textAcc) { 282 textAcc->SelectionBoundsAt(0, &start, &end); 283 textAcc->DeleteText(start, end - start); 284 nsCocoaUtils::GetStringForNSString(stringValue, text); 285 textAcc->InsertText(text, start); 286 } else if (proxy) { 287 nsString data; 288 proxy->SelectionBoundsAt(0, data, &start, &end); 289 proxy->DeleteText(start, end - start); 290 nsCocoaUtils::GetStringForNSString(stringValue, text); 291 proxy->InsertText(text, start); 292 } 293 } 294 295 if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) { 296 NSRange range; 297 if (!ToNSRange(value, &range)) 298 return; 299 300 if (textAcc) { 301 textAcc->SetSelectionBoundsAt(0, range.location, 302 range.location + range.length); 303 } else if (proxy) { 304 proxy->SetSelectionBoundsAt(0, range.location, 305 range.location + range.length); 306 } 307 return; 308 } 309 310 if ([attribute isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) { 311 NSRange range; 312 if (!ToNSRange(value, &range)) 313 return; 314 315 if (textAcc) { 316 textAcc->ScrollSubstringTo(range.location, range.location + range.length, 317 nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE); 318 } else if (proxy) { 319 proxy->ScrollSubstringTo(range.location, range.location + range.length, 320 nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE); 321 } 322 return; 323 } 324 325 [super accessibilitySetValue:value forAttribute:attribute]; 326 327 NS_OBJC_END_TRY_ABORT_BLOCK; 328} 329 330- (NSString*)subrole 331{ 332 if(mRole == roles::PASSWORD_TEXT) 333 return NSAccessibilitySecureTextFieldSubrole; 334 335 return nil; 336} 337 338#pragma mark - 339 340- (BOOL)isReadOnly 341{ 342 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; 343 344 if ([[self role] isEqualToString:NSAccessibilityStaticTextRole]) 345 return YES; 346 347 AccessibleWrap* accWrap = [self getGeckoAccessible]; 348 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 349 if (textAcc) 350 return (accWrap->State() & states::READONLY) == 0; 351 352 if (ProxyAccessible* proxy = [self getProxyAccessible]) 353 return (proxy->State() & states::READONLY) == 0; 354 355 return NO; 356 357 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); 358} 359 360- (NSNumber*)caretLineNumber 361{ 362 AccessibleWrap* accWrap = [self getGeckoAccessible]; 363 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 364 365 int32_t lineNumber = -1; 366 if (textAcc) { 367 lineNumber = textAcc->CaretLineNumber() - 1; 368 } else if (ProxyAccessible* proxy = [self getProxyAccessible]) { 369 lineNumber = proxy->CaretLineNumber() - 1; 370 } 371 372 return (lineNumber >= 0) ? [NSNumber numberWithInt:lineNumber] : nil; 373} 374 375- (void)setText:(NSString*)aNewString 376{ 377 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 378 379 AccessibleWrap* accWrap = [self getGeckoAccessible]; 380 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 381 382 nsString text; 383 nsCocoaUtils::GetStringForNSString(aNewString, text); 384 if (textAcc) { 385 textAcc->ReplaceText(text); 386 } else if (ProxyAccessible* proxy = [self getProxyAccessible]) { 387 proxy->ReplaceText(text); 388 } 389 390 NS_OBJC_END_TRY_ABORT_BLOCK; 391} 392 393- (NSString*)text 394{ 395 AccessibleWrap* accWrap = [self getGeckoAccessible]; 396 ProxyAccessible* proxy = [self getProxyAccessible]; 397 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 398 if (!textAcc && !proxy) 399 return nil; 400 401 // A password text field returns an empty value 402 if (mRole == roles::PASSWORD_TEXT) 403 return @""; 404 405 nsAutoString text; 406 if (textAcc) { 407 textAcc->TextSubstring(0, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT, text); 408 } else if (proxy) { 409 proxy->TextSubstring(0, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT, text); 410 } 411 412 return nsCocoaUtils::ToNSString(text); 413} 414 415- (long)textLength 416{ 417 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; 418 419 AccessibleWrap* accWrap = [self getGeckoAccessible]; 420 ProxyAccessible* proxy = [self getProxyAccessible]; 421 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 422 if (!textAcc && !proxy) 423 return 0; 424 425 return textAcc ? textAcc->CharacterCount() : proxy->CharacterCount(); 426 427 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); 428} 429 430- (long)selectedTextLength 431{ 432 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; 433 434 AccessibleWrap* accWrap = [self getGeckoAccessible]; 435 ProxyAccessible* proxy = [self getProxyAccessible]; 436 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 437 if (!textAcc && !proxy) 438 return 0; 439 440 int32_t start = 0, end = 0; 441 if (textAcc) { 442 textAcc->SelectionBoundsAt(0, &start, &end); 443 } else if (proxy) { 444 nsString data; 445 proxy->SelectionBoundsAt(0, data, &start, &end); 446 } 447 return (end - start); 448 449 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); 450} 451 452- (NSString*)selectedText 453{ 454 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; 455 456 AccessibleWrap* accWrap = [self getGeckoAccessible]; 457 ProxyAccessible* proxy = [self getProxyAccessible]; 458 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 459 if (!textAcc && !proxy) 460 return nil; 461 462 int32_t start = 0, end = 0; 463 nsAutoString selText; 464 if (textAcc) { 465 textAcc->SelectionBoundsAt(0, &start, &end); 466 if (start != end) { 467 textAcc->TextSubstring(start, end, selText); 468 } 469 } else if (proxy) { 470 proxy->SelectionBoundsAt(0, selText, &start, &end); 471 } 472 473 return nsCocoaUtils::ToNSString(selText); 474 475 NS_OBJC_END_TRY_ABORT_BLOCK_NIL; 476} 477 478- (NSValue*)selectedTextRange 479{ 480 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; 481 482 AccessibleWrap* accWrap = [self getGeckoAccessible]; 483 ProxyAccessible* proxy = [self getProxyAccessible]; 484 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 485 486 int32_t start = 0; 487 int32_t end = 0; 488 int32_t count = 0; 489 if (textAcc) { 490 count = textAcc->SelectionCount(); 491 if (count) { 492 textAcc->SelectionBoundsAt(0, &start, &end); 493 return [NSValue valueWithRange:NSMakeRange(start, end - start)]; 494 } 495 496 start = textAcc->CaretOffset(); 497 return [NSValue valueWithRange:NSMakeRange(start != -1 ? start : 0, 0)]; 498 } 499 500 if (proxy) { 501 count = proxy->SelectionCount(); 502 if (count) { 503 nsString data; 504 proxy->SelectionBoundsAt(0, data, &start, &end); 505 return [NSValue valueWithRange:NSMakeRange(start, end - start)]; 506 } 507 508 start = proxy->CaretOffset(); 509 return [NSValue valueWithRange:NSMakeRange(start != -1 ? start : 0, 0)]; 510 } 511 512 return [NSValue valueWithRange:NSMakeRange(0, 0)]; 513 514 NS_OBJC_END_TRY_ABORT_BLOCK_NIL; 515} 516 517- (NSValue*)visibleCharacterRange 518{ 519 // XXX this won't work with Textarea and such as we actually don't give 520 // the visible character range. 521 AccessibleWrap* accWrap = [self getGeckoAccessible]; 522 ProxyAccessible* proxy = [self getProxyAccessible]; 523 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 524 if (!textAcc && !proxy) 525 return 0; 526 527 return [NSValue valueWithRange: 528 NSMakeRange(0, textAcc ? 529 textAcc->CharacterCount() : proxy->CharacterCount())]; 530} 531 532- (void)valueDidChange 533{ 534 NS_OBJC_BEGIN_TRY_ABORT_BLOCK; 535 536 NSAccessibilityPostNotification(GetObjectOrRepresentedView(self), 537 NSAccessibilityValueChangedNotification); 538 539 NS_OBJC_END_TRY_ABORT_BLOCK; 540} 541 542- (void)selectedTextDidChange 543{ 544 NSAccessibilityPostNotification(GetObjectOrRepresentedView(self), 545 NSAccessibilitySelectedTextChangedNotification); 546} 547 548- (NSString*)stringFromRange:(NSRange*)range 549{ 550 NS_PRECONDITION(range, "no range"); 551 552 AccessibleWrap* accWrap = [self getGeckoAccessible]; 553 ProxyAccessible* proxy = [self getProxyAccessible]; 554 HyperTextAccessible* textAcc = accWrap? accWrap->AsHyperText() : nullptr; 555 if (!textAcc && !proxy) 556 return nil; 557 558 nsAutoString text; 559 if (textAcc) { 560 textAcc->TextSubstring(range->location, 561 range->location + range->length, text); 562 } else if (proxy) { 563 proxy->TextSubstring(range->location, 564 range->location + range->length, text); 565 } 566 567 return nsCocoaUtils::ToNSString(text); 568} 569 570@end 571 572@implementation mozTextLeafAccessible 573 574- (NSArray*)accessibilityAttributeNames 575{ 576 static NSMutableArray* supportedAttributes = nil; 577 if (!supportedAttributes) { 578 supportedAttributes = [[super accessibilityAttributeNames] mutableCopy]; 579 [supportedAttributes removeObject:NSAccessibilityChildrenAttribute]; 580 } 581 582 return supportedAttributes; 583} 584 585- (id)accessibilityAttributeValue:(NSString*)attribute 586{ 587 if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) 588 return @""; 589 590 if ([attribute isEqualToString:NSAccessibilityValueAttribute]) 591 return [self text]; 592 593 return [super accessibilityAttributeValue:attribute]; 594} 595 596- (NSString*)text 597{ 598 if (AccessibleWrap* accWrap = [self getGeckoAccessible]) { 599 return nsCocoaUtils::ToNSString(accWrap->AsTextLeaf()->Text()); 600 } 601 602 if (ProxyAccessible* proxy = [self getProxyAccessible]) { 603 nsString text; 604 proxy->Text(&text); 605 return nsCocoaUtils::ToNSString(text); 606 } 607 608 return nil; 609} 610 611- (long)textLength 612{ 613 if (AccessibleWrap* accWrap = [self getGeckoAccessible]) { 614 return accWrap->AsTextLeaf()->Text().Length(); 615 } 616 617 if (ProxyAccessible* proxy = [self getProxyAccessible]) { 618 nsString text; 619 proxy->Text(&text); 620 return text.Length(); 621 } 622 623 return 0; 624} 625 626@end 627