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