michael@0: /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "Accessible-inl.h" michael@0: #include "HyperTextAccessible-inl.h" michael@0: #include "TextLeafAccessible.h" michael@0: michael@0: #include "nsCocoaUtils.h" michael@0: #include "nsObjCExceptions.h" michael@0: michael@0: #import "mozTextAccessible.h" michael@0: michael@0: using namespace mozilla::a11y; michael@0: michael@0: inline bool michael@0: ToNSRange(id aValue, NSRange* aRange) michael@0: { michael@0: NS_PRECONDITION(aRange, "aRange is nil"); michael@0: michael@0: if ([aValue isKindOfClass:[NSValue class]] && michael@0: strcmp([(NSValue*)aValue objCType], @encode(NSRange)) == 0) { michael@0: *aRange = [aValue rangeValue]; michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: inline NSString* michael@0: ToNSString(id aValue) michael@0: { michael@0: if ([aValue isKindOfClass:[NSString class]]) { michael@0: return aValue; michael@0: } michael@0: michael@0: return nil; michael@0: } michael@0: michael@0: @interface mozTextAccessible () michael@0: - (NSString*)subrole; michael@0: - (NSString*)selectedText; michael@0: - (NSValue*)selectedTextRange; michael@0: - (NSValue*)visibleCharacterRange; michael@0: - (long)textLength; michael@0: - (BOOL)isReadOnly; michael@0: - (NSNumber*)caretLineNumber; michael@0: - (void)setText:(NSString*)newText; michael@0: - (NSString*)text; michael@0: - (NSString*)stringFromRange:(NSRange*)range; michael@0: @end michael@0: michael@0: @implementation mozTextAccessible michael@0: michael@0: - (id)initWithAccessible:(AccessibleWrap*)accessible michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: if ((self = [super initWithAccessible:accessible])) { michael@0: mGeckoTextAccessible = accessible->AsHyperText(); michael@0: CallQueryInterface(accessible, &mGeckoEditableTextAccessible); michael@0: } michael@0: return self; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: - (BOOL)accessibilityIsIgnored michael@0: { michael@0: return !mGeckoAccessible; michael@0: } michael@0: michael@0: - (NSArray*)accessibilityAttributeNames michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: static NSMutableArray* supportedAttributes = nil; michael@0: if (!supportedAttributes) { michael@0: // text-specific attributes to supplement the standard one michael@0: supportedAttributes = [[NSMutableArray alloc] initWithObjects: michael@0: NSAccessibilitySelectedTextAttribute, // required michael@0: NSAccessibilitySelectedTextRangeAttribute, // required michael@0: NSAccessibilityNumberOfCharactersAttribute, // required michael@0: NSAccessibilityVisibleCharacterRangeAttribute, // required michael@0: NSAccessibilityInsertionPointLineNumberAttribute, michael@0: @"AXRequired", michael@0: @"AXInvalid", michael@0: nil michael@0: ]; michael@0: [supportedAttributes addObjectsFromArray:[super accessibilityAttributeNames]]; michael@0: } michael@0: return supportedAttributes; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: - (id)accessibilityAttributeValue:(NSString*)attribute michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityNumberOfCharactersAttribute]) michael@0: return [NSNumber numberWithInt:[self textLength]]; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityInsertionPointLineNumberAttribute]) michael@0: return [self caretLineNumber]; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) michael@0: return [self selectedTextRange]; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) michael@0: return [self selectedText]; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) michael@0: return @""; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { michael@0: // Apple's SpeechSynthesisServer expects AXValue to return an AXStaticText michael@0: // object's AXSelectedText attribute. See bug 674612 for details. michael@0: // Also if there is no selected text, we return the full text. michael@0: // See bug 369710 for details. michael@0: if ([[self role] isEqualToString:NSAccessibilityStaticTextRole]) { michael@0: NSString* selectedText = [self selectedText]; michael@0: return (selectedText && [selectedText length]) ? selectedText : [self text]; michael@0: } michael@0: michael@0: return [self text]; michael@0: } michael@0: michael@0: if ([attribute isEqualToString:@"AXRequired"]) michael@0: return [NSNumber numberWithBool:!!(mGeckoAccessible->State() & states::REQUIRED)]; michael@0: michael@0: if ([attribute isEqualToString:@"AXInvalid"]) michael@0: return [NSNumber numberWithBool:!!(mGeckoAccessible->State() & states::INVALID)]; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) michael@0: return [self visibleCharacterRange]; michael@0: michael@0: // let mozAccessible handle all other attributes michael@0: return [super accessibilityAttributeValue:attribute]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: - (NSArray*)accessibilityParameterizedAttributeNames michael@0: { michael@0: static NSArray* supportedParametrizedAttributes = nil; michael@0: // text specific parametrized attributes michael@0: if (!supportedParametrizedAttributes) { michael@0: supportedParametrizedAttributes = [[NSArray alloc] initWithObjects: michael@0: NSAccessibilityStringForRangeParameterizedAttribute, michael@0: NSAccessibilityLineForIndexParameterizedAttribute, michael@0: NSAccessibilityRangeForLineParameterizedAttribute, michael@0: NSAccessibilityAttributedStringForRangeParameterizedAttribute, michael@0: NSAccessibilityBoundsForRangeParameterizedAttribute, michael@0: #if DEBUG michael@0: NSAccessibilityRangeForPositionParameterizedAttribute, michael@0: NSAccessibilityRangeForIndexParameterizedAttribute, michael@0: NSAccessibilityRTFForRangeParameterizedAttribute, michael@0: NSAccessibilityStyleRangeForIndexParameterizedAttribute, michael@0: #endif michael@0: nil michael@0: ]; michael@0: } michael@0: return supportedParametrizedAttributes; michael@0: } michael@0: michael@0: - (id)accessibilityAttributeValue:(NSString*)attribute forParameter:(id)parameter michael@0: { michael@0: if (!mGeckoTextAccessible) michael@0: return nil; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityStringForRangeParameterizedAttribute]) { michael@0: NSRange range; michael@0: if (!ToNSRange(parameter, &range)) { michael@0: #if DEBUG michael@0: NSLog(@"%@: range not set", attribute); michael@0: #endif michael@0: return @""; michael@0: } michael@0: michael@0: return [self stringFromRange:&range]; michael@0: } michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityRangeForLineParameterizedAttribute]) { michael@0: // XXX: actually get the integer value for the line # michael@0: return [NSValue valueWithRange:NSMakeRange(0, [self textLength])]; michael@0: } michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityAttributedStringForRangeParameterizedAttribute]) { michael@0: NSRange range; michael@0: if (!ToNSRange(parameter, &range)) { michael@0: #if DEBUG michael@0: NSLog(@"%@: range not set", attribute); michael@0: #endif michael@0: return @""; michael@0: } michael@0: michael@0: return [[[NSAttributedString alloc] initWithString:[self stringFromRange:&range]] autorelease]; michael@0: } michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityLineForIndexParameterizedAttribute]) { michael@0: // XXX: actually return the line # michael@0: return [NSNumber numberWithInt:0]; michael@0: } michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityBoundsForRangeParameterizedAttribute]) { michael@0: NSRange range; michael@0: if (!ToNSRange(parameter, &range)) { michael@0: #if DEBUG michael@0: NSLog(@"%@:no range", attribute); michael@0: #endif michael@0: return nil; michael@0: } michael@0: michael@0: int32_t start = range.location; michael@0: int32_t end = start + range.length; michael@0: nsIntRect bounds = mGeckoTextAccessible->TextBounds(start, end); michael@0: michael@0: return [NSValue valueWithRect:nsCocoaUtils::GeckoRectToCocoaRect(bounds)]; michael@0: } michael@0: michael@0: #if DEBUG michael@0: NSLog(@"unhandled attribute:%@ forParameter:%@", attribute, parameter); michael@0: #endif michael@0: michael@0: return nil; michael@0: } michael@0: michael@0: - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityValueAttribute]) michael@0: return ![self isReadOnly]; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute] || michael@0: [attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute] || michael@0: [attribute isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) michael@0: return YES; michael@0: michael@0: return [super accessibilityIsAttributeSettable:attribute]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); michael@0: } michael@0: michael@0: - (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (!mGeckoTextAccessible) michael@0: return; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { michael@0: [self setText:ToNSString(value)]; michael@0: michael@0: return; michael@0: } michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) { michael@0: NSString* stringValue = ToNSString(value); michael@0: if (!stringValue) michael@0: return; michael@0: michael@0: int32_t start = 0, end = 0; michael@0: mGeckoTextAccessible->SelectionBoundsAt(0, &start, &end); michael@0: mGeckoTextAccessible->DeleteText(start, end - start); michael@0: michael@0: nsString text; michael@0: nsCocoaUtils::GetStringForNSString(stringValue, text); michael@0: mGeckoTextAccessible->InsertText(text, start); michael@0: } michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) { michael@0: NSRange range; michael@0: if (!ToNSRange(value, &range)) michael@0: return; michael@0: michael@0: mGeckoTextAccessible->SetSelectionBoundsAt(0, range.location, michael@0: range.location + range.length); michael@0: return; michael@0: } michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) { michael@0: NSRange range; michael@0: if (!ToNSRange(value, &range)) michael@0: return; michael@0: michael@0: mGeckoTextAccessible->ScrollSubstringTo(range.location, range.location + range.length, michael@0: nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE); michael@0: return; michael@0: } michael@0: michael@0: [super accessibilitySetValue:value forAttribute:attribute]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: - (NSString*)subrole michael@0: { michael@0: if(mRole == roles::PASSWORD_TEXT) michael@0: return NSAccessibilitySecureTextFieldSubrole; michael@0: michael@0: return nil; michael@0: } michael@0: michael@0: - (void)expire michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: mGeckoTextAccessible = nullptr; michael@0: NS_IF_RELEASE(mGeckoEditableTextAccessible); michael@0: [super expire]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: #pragma mark - michael@0: michael@0: - (BOOL)isReadOnly michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: if ([[self role] isEqualToString:NSAccessibilityStaticTextRole]) michael@0: return YES; michael@0: michael@0: if (mGeckoEditableTextAccessible) michael@0: return (mGeckoAccessible->State() & states::READONLY) == 0; michael@0: michael@0: return NO; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); michael@0: } michael@0: michael@0: - (NSNumber*)caretLineNumber michael@0: { michael@0: int32_t lineNumber = mGeckoTextAccessible ? michael@0: mGeckoTextAccessible->CaretLineNumber() - 1 : -1; michael@0: michael@0: return (lineNumber >= 0) ? [NSNumber numberWithInt:lineNumber] : nil; michael@0: } michael@0: michael@0: - (void)setText:(NSString*)aNewString michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: if (mGeckoEditableTextAccessible) { michael@0: nsString text; michael@0: nsCocoaUtils::GetStringForNSString(aNewString, text); michael@0: mGeckoEditableTextAccessible->SetTextContents(text); michael@0: } michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: - (NSString*)text michael@0: { michael@0: if (!mGeckoAccessible || !mGeckoTextAccessible) michael@0: return nil; michael@0: michael@0: // A password text field returns an empty value michael@0: if (mRole == roles::PASSWORD_TEXT) michael@0: return @""; michael@0: michael@0: nsAutoString text; michael@0: mGeckoTextAccessible->TextSubstring(0, michael@0: nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT, michael@0: text); michael@0: return nsCocoaUtils::ToNSString(text); michael@0: } michael@0: michael@0: - (long)textLength michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: if (!mGeckoAccessible || !mGeckoTextAccessible) michael@0: return 0; michael@0: michael@0: return mGeckoTextAccessible ? mGeckoTextAccessible->CharacterCount() : 0; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); michael@0: } michael@0: michael@0: - (long)selectedTextLength michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: if (mGeckoTextAccessible) { michael@0: int32_t start = 0, end = 0; michael@0: mGeckoTextAccessible->SelectionBoundsAt(0, &start, &end); michael@0: return (end - start); michael@0: } michael@0: return 0; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); michael@0: } michael@0: michael@0: - (NSString*)selectedText michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: if (mGeckoTextAccessible) { michael@0: int32_t start = 0, end = 0; michael@0: mGeckoTextAccessible->SelectionBoundsAt(0, &start, &end); michael@0: if (start != end) { michael@0: nsAutoString selText; michael@0: mGeckoTextAccessible->TextSubstring(start, end, selText); michael@0: return nsCocoaUtils::ToNSString(selText); michael@0: } michael@0: } michael@0: return nil; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: - (NSValue*)selectedTextRange michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: if (mGeckoTextAccessible) { michael@0: int32_t start = 0; michael@0: int32_t end = 0; michael@0: int32_t count = mGeckoTextAccessible->SelectionCount(); michael@0: michael@0: if (count) { michael@0: mGeckoTextAccessible->SelectionBoundsAt(0, &start, &end); michael@0: return [NSValue valueWithRange:NSMakeRange(start, end - start)]; michael@0: } michael@0: michael@0: start = mGeckoTextAccessible->CaretOffset(); michael@0: return [NSValue valueWithRange:NSMakeRange(start != -1 ? start : 0, 0)]; michael@0: } michael@0: return [NSValue valueWithRange:NSMakeRange(0, 0)]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: - (NSValue*)visibleCharacterRange michael@0: { michael@0: // XXX this won't work with Textarea and such as we actually don't give michael@0: // the visible character range. michael@0: return [NSValue valueWithRange: michael@0: NSMakeRange(0, mGeckoTextAccessible ? michael@0: mGeckoTextAccessible->CharacterCount() : 0)]; michael@0: } michael@0: michael@0: - (void)valueDidChange michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: NSAccessibilityPostNotification(GetObjectOrRepresentedView(self), michael@0: NSAccessibilityValueChangedNotification); michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: - (void)selectedTextDidChange michael@0: { michael@0: NSAccessibilityPostNotification(GetObjectOrRepresentedView(self), michael@0: NSAccessibilitySelectedTextChangedNotification); michael@0: } michael@0: michael@0: - (NSString*)stringFromRange:(NSRange*)range michael@0: { michael@0: NS_PRECONDITION(mGeckoTextAccessible && range, "no Gecko text accessible or range"); michael@0: michael@0: nsAutoString text; michael@0: mGeckoTextAccessible->TextSubstring(range->location, michael@0: range->location + range->length, text); michael@0: return nsCocoaUtils::ToNSString(text); michael@0: } michael@0: michael@0: @end michael@0: michael@0: @implementation mozTextLeafAccessible michael@0: michael@0: - (NSArray*)accessibilityAttributeNames michael@0: { michael@0: static NSMutableArray* supportedAttributes = nil; michael@0: if (!supportedAttributes) { michael@0: supportedAttributes = [[super accessibilityAttributeNames] mutableCopy]; michael@0: [supportedAttributes removeObject:NSAccessibilityChildrenAttribute]; michael@0: } michael@0: michael@0: return supportedAttributes; michael@0: } michael@0: michael@0: - (id)accessibilityAttributeValue:(NSString*)attribute michael@0: { michael@0: if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) michael@0: return @""; michael@0: michael@0: if ([attribute isEqualToString:NSAccessibilityValueAttribute]) michael@0: return [self text]; michael@0: michael@0: return [super accessibilityAttributeValue:attribute]; michael@0: } michael@0: michael@0: - (NSString*)text michael@0: { michael@0: if (!mGeckoAccessible) michael@0: return nil; michael@0: michael@0: return nsCocoaUtils::ToNSString(mGeckoAccessible->AsTextLeaf()->Text()); michael@0: } michael@0: michael@0: - (long)textLength michael@0: { michael@0: if (!mGeckoAccessible) michael@0: return 0; michael@0: michael@0: return mGeckoAccessible->AsTextLeaf()->Text().Length(); michael@0: } michael@0: michael@0: @end