blob: a04dcfb6a3f7a2341dbf45c047d397235ec9d2ab [file] [log] [blame]
/*
* Copyright (C) 2004, 2005, 2006, 2007 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "WebCoreAXObject.h"
#import "DOMInternal.h"
#import "ColorMac.h"
#import "Document.h"
#import "EventNames.h"
#import "FocusController.h"
#import "Frame.h"
#import "FrameLoader.h"
#import "FrameView.h"
#import "HTMLAreaElement.h"
#import "HTMLCollection.h"
#import "HTMLFrameElementBase.h"
#import "HTMLImageElement.h"
#import "HTMLInputElement.h"
#import "HTMLLabelElement.h"
#import "HTMLMapElement.h"
#import "HTMLNames.h"
#import "HTMLSelectElement.h"
#import "HTMLTextAreaElement.h"
#import "HitTestRequest.h"
#import "HitTestResult.h"
#import "LocalizedStrings.h"
#import "NodeList.h"
#import "Page.h"
#import "RenderImage.h"
#import "RenderListMarker.h"
#import "RenderMenuList.h"
#import "RenderTextControl.h"
#import "RenderTheme.h"
#import "RenderView.h"
#import "RenderWidget.h"
#import "SelectionController.h"
#import "SimpleFontData.h"
#import "TextIterator.h"
#import "WebCoreFrameBridge.h"
#import "WebCoreFrameView.h"
#import "WebCoreObjCExtras.h"
#import "WebCoreViewFactory.h"
#import "htmlediting.h"
#import "kjs_html.h"
#import "visible_units.h"
#include <mach-o/dyld.h>
using namespace WebCore;
using namespace EventNames;
using namespace HTMLNames;
@interface WebCoreAXObject (PrivateWebCoreAXObject)
// forward declarations as needed
- (WebCoreTextMarker*)textMarkerForIndex: (NSNumber*) index lastIndexOK: (BOOL)lastIndexOK;
- (id)doAXLineForTextMarker: (WebCoreTextMarker* ) textMarker;
@end
@implementation WebCoreAXObject
#ifndef BUILDING_ON_TIGER
+ (void)initialize
{
WebCoreObjCFinalizeOnMainThread(self);
}
#endif
-(id)initWithRenderer:(RenderObject*)renderer
{
[super init];
m_renderer = renderer;
#ifndef NDEBUG
m_renderer->setHasAXObject(true);
#endif
return self;
}
-(BOOL)detached
{
return !m_renderer;
}
-(void)detach
{
// Send unregisterUniqueIdForUIElement unconditionally because if it is
// ever accidently not done (via other bugs in our AX implementation) you
// end up with a crash like <rdar://problem/4273149>. It is safe and not
// expensive to send even if the object is not registered.
[[WebCoreViewFactory sharedFactory] unregisterUniqueIdForUIElement:self];
[m_data release];
m_data = 0;
[self removeAXObjectID];
#ifndef NDEBUG
if (m_renderer)
m_renderer->setHasAXObject(false);
#endif
m_renderer = 0;
[self clearChildren];
}
- (void)dealloc
{
[self detach];
[super dealloc];
}
- (void)finalize
{
[self detach];
[super finalize];
}
-(id)data
{
return m_data;
}
-(void)setData:(id)data
{
if (!m_renderer)
return;
[data retain];
[m_data release];
m_data = data;
}
-(HTMLAnchorElement*)anchorElement
{
// return already-known anchor for image areas
if (m_areaElement)
return m_areaElement;
// search up the render tree for a RenderObject with a DOM node. Defer to an earlier continuation, though.
RenderObject* currRenderer;
for (currRenderer = m_renderer; currRenderer && !currRenderer->element(); currRenderer = currRenderer->parent()) {
if (currRenderer->continuation())
return [currRenderer->document()->axObjectCache()->get(currRenderer->continuation()) anchorElement];
}
// bail of none found
if (!currRenderer)
return 0;
// search up the DOM tree for an anchor element
// NOTE: this assumes that any non-image with an anchor is an HTMLAnchorElement
Node* elt = currRenderer->element();
for ( ; elt; elt = elt->parentNode()) {
if (elt->isLink() && elt->renderer() && !elt->renderer()->isImage())
return static_cast<HTMLAnchorElement*>(elt);
}
return 0;
}
-(BOOL)isImageButton
{
return m_renderer->isImage() && m_renderer->element() && m_renderer->element()->hasTagName(inputTag);
}
-(Element*)mouseButtonListener
{
// FIXME: Do the continuation search like anchorElement does
for (EventTargetNode* elt = static_cast<EventTargetNode*>(m_renderer->element()); elt; elt = static_cast<EventTargetNode*>(elt->parentNode())) {
if (elt->getHTMLEventListener(clickEvent) || elt->getHTMLEventListener(mousedownEvent) || elt->getHTMLEventListener(mouseupEvent))
return static_cast<Element*>(elt);
}
return 0;
}
-(Element*)actionElement
{
if (m_renderer->element() && m_renderer->element()->hasTagName(inputTag)) {
HTMLInputElement* input = static_cast<HTMLInputElement*>(m_renderer->element());
if (!input->disabled() && (input->inputType() == HTMLInputElement::CHECKBOX ||
input->inputType() == HTMLInputElement::RADIO ||
input->isTextButton()))
return input;
}
if ([self isImageButton] || m_renderer->isMenuList())
return static_cast<Element*>(m_renderer->element());
Element* elt = [self anchorElement];
if (!elt)
elt = [self mouseButtonListener];
return elt;
}
-(WebCoreAXObject*)firstChild
{
if (!m_renderer || !m_renderer->firstChild())
return nil;
return m_renderer->document()->axObjectCache()->get(m_renderer->firstChild());
}
-(WebCoreAXObject*)lastChild
{
if (!m_renderer || !m_renderer->lastChild())
return nil;
return m_renderer->document()->axObjectCache()->get(m_renderer->lastChild());
}
-(WebCoreAXObject*)previousSibling
{
if (!m_renderer || !m_renderer->previousSibling())
return nil;
return m_renderer->document()->axObjectCache()->get(m_renderer->previousSibling());
}
-(WebCoreAXObject*)nextSibling
{
if (!m_renderer || !m_renderer->nextSibling())
return nil;
return m_renderer->document()->axObjectCache()->get(m_renderer->nextSibling());
}
-(WebCoreAXObject*)parentObject
{
if (m_areaElement)
return m_renderer->document()->axObjectCache()->get(m_renderer);
if (!m_renderer || !m_renderer->parent())
return nil;
return m_renderer->document()->axObjectCache()->get(m_renderer->parent());
}
-(WebCoreAXObject*)parentObjectUnignored
{
WebCoreAXObject* obj = [self parentObject];
if ([obj accessibilityIsIgnored])
return [obj parentObjectUnignored];
return obj;
}
-(void)addChildrenToArray:(NSMutableArray*)array
{
// nothing to add if there is no RenderObject
if (!m_renderer)
return;
// try to add RenderWidget's children, but fall thru if there are none
if (m_renderer->isWidget()) {
RenderWidget* renderWidget = static_cast<RenderWidget*>(m_renderer);
Widget* widget = renderWidget->widget();
if (widget) {
NSArray* childArr = [(widget->getOuterView()) accessibilityAttributeValue: NSAccessibilityChildrenAttribute];
[array addObjectsFromArray: childArr];
return;
}
}
// add all unignored acc children
for (WebCoreAXObject* obj = [self firstChild]; obj; obj = [obj nextSibling]) {
if ([obj accessibilityIsIgnored])
[obj addChildrenToArray: array];
else
[array addObject: obj];
}
// for a RenderImage, add the <area> elements as individual accessibility objects
if (m_renderer->isImage() && !m_areaElement) {
HTMLMapElement* map = static_cast<RenderImage*>(m_renderer)->imageMap();
if (map) {
for (Node* current = map->firstChild(); current; current = current->traverseNextNode(map)) {
// add an <area> element for this child if it has a link
// NOTE: can't cache these because they all have the same renderer, which is the cache key, right?
// plus there may be little reason to since they are being added to the handy array
if (current->isLink()) {
WebCoreAXObject* obj = [[[WebCoreAXObject alloc] initWithRenderer: m_renderer] autorelease];
obj->m_areaElement = static_cast<HTMLAreaElement*>(current);
[array addObject: obj];
}
}
}
}
}
-(BOOL)isWebArea
{
return m_renderer->isRenderView();
}
-(BOOL)isAnchor
{
return m_areaElement || (!m_renderer->isImage() && m_renderer->element() && m_renderer->element()->isLink());
}
-(BOOL)isTextControl
{
return m_renderer->isTextField() || m_renderer->isTextArea();
}
static bool isPasswordFieldElement(Node* node)
{
if (!node || !node->hasTagName(inputTag))
return false;
HTMLInputElement* input = static_cast<HTMLInputElement*>(node);
return input->inputType() == HTMLInputElement::PASSWORD;
}
-(BOOL)isPasswordField
{
return m_renderer && isPasswordFieldElement(m_renderer->element());
}
-(BOOL)isAttachment
{
// widgets are the replaced elements that we represent to AX as attachments
BOOL result = m_renderer && m_renderer->isWidget();
// assert that a widget is a replaced element that is not an image
ASSERT(!result || (m_renderer->isReplaced() && !m_renderer->isImage()));
return result;
}
-(NSView*)attachmentView
{
ASSERT(m_renderer->isReplaced() && m_renderer->isWidget() && !m_renderer->isImage());
RenderWidget* renderWidget = static_cast<RenderWidget*>(m_renderer);
Widget* widget = renderWidget->widget();
if (widget)
return widget->getView();
return nil;
}
static int blockquoteLevel(RenderObject* renderer)
{
int result = 0;
for (Node* node = renderer->element(); node; node = node->parent()) {
if (node->hasTagName(blockquoteTag))
result += 1;
}
return result;
}
static int headingLevel(RenderObject* renderer)
{
if (!renderer->isBlockFlow())
return 0;
Node* node = renderer->element();
if (!node)
return 0;
if (node->hasTagName(h1Tag))
return 1;
if (node->hasTagName(h2Tag))
return 2;
if (node->hasTagName(h3Tag))
return 3;
if (node->hasTagName(h4Tag))
return 4;
if (node->hasTagName(h5Tag))
return 5;
if (node->hasTagName(h6Tag))
return 6;
return 0;
}
-(int)headingLevel
{
return headingLevel(m_renderer);
}
-(BOOL)isHeading
{
return [self headingLevel] != 0;
}
-(NSString*)role
{
if (!m_renderer)
return NSAccessibilityUnknownRole;
if (m_areaElement)
return @"AXLink";
if (m_renderer->element() && m_renderer->element()->isLink()) {
if (m_renderer->isImage())
return @"AXImageMap";
return @"AXLink";
}
if (m_renderer->isListMarker())
return @"AXListMarker";
if (m_renderer->element() && m_renderer->element()->hasTagName(buttonTag))
return NSAccessibilityButtonRole;
if (m_renderer->isText())
return NSAccessibilityStaticTextRole;
if (m_renderer->isImage()) {
if ([self isImageButton])
return NSAccessibilityButtonRole;
return NSAccessibilityImageRole;
}
if ([self isWebArea])
return @"AXWebArea";
if (m_renderer->isTextField())
return NSAccessibilityTextFieldRole;
if (m_renderer->isTextArea())
return NSAccessibilityTextAreaRole;
if (m_renderer->element() && m_renderer->element()->hasTagName(inputTag)) {
HTMLInputElement* input = static_cast<HTMLInputElement*>(m_renderer->element());
if (input->inputType() == HTMLInputElement::CHECKBOX)
return NSAccessibilityCheckBoxRole;
if (input->inputType() == HTMLInputElement::RADIO)
return NSAccessibilityRadioButtonRole;
if (input->isTextButton())
return NSAccessibilityButtonRole;
}
if (m_renderer->isMenuList())
return NSAccessibilityPopUpButtonRole;
if ([self isHeading])
return @"AXHeading";
if (m_renderer->isBlockFlow())
return NSAccessibilityGroupRole;
if ([self isAttachment])
return [[self attachmentView] accessibilityAttributeValue:NSAccessibilityRoleAttribute];
return NSAccessibilityUnknownRole;
}
-(NSString*)subrole
{
if ([self isPasswordField])
return NSAccessibilitySecureTextFieldSubrole;
if ([self isAttachment]) {
NSView* attachmentView = [self attachmentView];
if ([[attachmentView accessibilityAttributeNames] containsObject:NSAccessibilitySubroleAttribute]) {
return [attachmentView accessibilityAttributeValue:NSAccessibilitySubroleAttribute];
}
}
return nil;
}
-(NSString*)roleDescription
{
if (!m_renderer)
return nil;
// attachments have the AXImage role, but a different subrole
if ([self isAttachment])
return [[self attachmentView] accessibilityAttributeValue:NSAccessibilityRoleDescriptionAttribute];
// FIXME 3447564: It would be better to call some AppKit API to get these strings
// (which would be the best way to localize them)
NSString* role = [self role];
if ([role isEqualToString:NSAccessibilityButtonRole])
return NSAccessibilityRoleDescription(NSAccessibilityButtonRole, [self subrole]);
if ([role isEqualToString:NSAccessibilityPopUpButtonRole])
return NSAccessibilityRoleDescription(NSAccessibilityPopUpButtonRole, [self subrole]);
if ([role isEqualToString:NSAccessibilityStaticTextRole])
return NSAccessibilityRoleDescription(NSAccessibilityStaticTextRole, [self subrole]);
if ([role isEqualToString:NSAccessibilityImageRole])
return NSAccessibilityRoleDescription(NSAccessibilityImageRole, [self subrole]);
if ([role isEqualToString:NSAccessibilityGroupRole])
return NSAccessibilityRoleDescription(NSAccessibilityGroupRole, [self subrole]);
if ([role isEqualToString:NSAccessibilityCheckBoxRole])
return NSAccessibilityRoleDescription(NSAccessibilityCheckBoxRole, [self subrole]);
if ([role isEqualToString:NSAccessibilityRadioButtonRole])
return NSAccessibilityRoleDescription(NSAccessibilityRadioButtonRole, [self subrole]);
if ([role isEqualToString:NSAccessibilityTextFieldRole])
return NSAccessibilityRoleDescription(NSAccessibilityTextFieldRole, [self subrole]);
if ([role isEqualToString:NSAccessibilityTextAreaRole])
return NSAccessibilityRoleDescription(NSAccessibilityTextAreaRole, [self subrole]);
if ([role isEqualToString:@"AXWebArea"])
return AXWebAreaText();
if ([role isEqualToString:@"AXLink"])
return AXLinkText();
if ([role isEqualToString:@"AXListMarker"])
return AXListMarkerText();
if ([role isEqualToString:@"AXImageMap"])
return AXImageMapText();
if ([role isEqualToString:@"AXHeading"])
return AXHeadingText();
return NSAccessibilityRoleDescription(NSAccessibilityUnknownRole, nil);
}
-(NSString*)helpText
{
if (!m_renderer)
return nil;
if (m_areaElement) {
const AtomicString& summary = static_cast<Element*>(m_areaElement)->getAttribute(summaryAttr);
if (!summary.isEmpty())
return summary;
const AtomicString& title = static_cast<Element*>(m_areaElement)->getAttribute(titleAttr);
if (!title.isEmpty())
return title;
}
for (RenderObject* curr = m_renderer; curr; curr = curr->parent()) {
if (curr->element() && curr->element()->isHTMLElement()) {
const AtomicString& summary = static_cast<Element*>(curr->element())->getAttribute(summaryAttr);
if (!summary.isEmpty())
return summary;
const AtomicString& title = static_cast<Element*>(curr->element())->getAttribute(titleAttr);
if (!title.isEmpty())
return title;
}
}
return nil;
}
-(NSString*)textUnderElement
{
if (!m_renderer)
return nil;
Node* e = m_renderer->element();
Document* d = m_renderer->document();
if (e && d) {
Frame* p = d->frame();
if (p) {
// catch stale WebCoreAXObject (see <rdar://problem/3960196>)
if (p->document() != d)
return nil;
return plainText(rangeOfContents(e).get());
}
}
// return nil for anonymous text because it is non-trivial to get
// the actual text and, so far, that is not needed
return nil;
}
-(id)value
{
if (!m_renderer || m_areaElement || [self isPasswordField])
return nil;
if (m_renderer->isText())
return [self textUnderElement];
if (m_renderer->isMenuList())
return static_cast<RenderMenuList*>(m_renderer)->text();
if (m_renderer->isListMarker())
return static_cast<RenderListMarker*>(m_renderer)->text();
if ([self isWebArea]) {
if (m_renderer->document()->frame())
return nil;
// FIXME: should use startOfDocument and endOfDocument (or rangeForDocument?) here
VisiblePosition startVisiblePosition = m_renderer->positionForCoordinates(0, 0);
VisiblePosition endVisiblePosition = m_renderer->positionForCoordinates(INT_MAX, INT_MAX);
if (startVisiblePosition.isNull() || endVisiblePosition.isNull())
return nil;
return plainText(makeRange(startVisiblePosition, endVisiblePosition).get());
}
if ([self isAttachment]) {
NSView* attachmentView = [self attachmentView];
if ([[attachmentView accessibilityAttributeNames] containsObject:NSAccessibilityValueAttribute])
return [attachmentView accessibilityAttributeValue:NSAccessibilityValueAttribute];
return nil;
}
if ([self isHeading])
return [NSNumber numberWithInt:[self headingLevel]];
if ([self isTextControl])
return (NSString*)(static_cast<RenderTextControl*>(m_renderer)->text());
if (m_renderer->element() && m_renderer->element()->hasTagName(inputTag)) {
HTMLInputElement* input = static_cast<HTMLInputElement*>(m_renderer->element());
// Checkboxes return their state as an integer. 0 for off, 1 for on.
if (input->inputType() == HTMLInputElement::CHECKBOX ||
input->inputType() == HTMLInputElement::RADIO)
return [NSNumber numberWithInt:input->checked()];
}
// FIXME: We might need to implement a value here for more types
// FIXME: It would be better not to advertise a value at all for the types for which we don't implement one;
// this would require subclassing or making accessibilityAttributeNames do something other than return a
// single static array.
return nil;
}
static HTMLLabelElement* labelForElement(Element* element)
{
RefPtr<NodeList> list = element->document()->getElementsByTagName("label");
unsigned len = list->length();
for (unsigned i = 0; i < len; i++) {
HTMLLabelElement* label = static_cast<HTMLLabelElement*>(list->item(i));
if (label->correspondingControl() == element)
return label;
}
return 0;
}
-(NSString*)title
{
if (!m_renderer || m_areaElement || !m_renderer->element())
return nil;
if (m_renderer->element()->hasTagName(buttonTag))
return [self textUnderElement];
if (m_renderer->element()->hasTagName(inputTag)) {
HTMLInputElement* input = static_cast<HTMLInputElement*>(m_renderer->element());
if (input->isTextButton())
return input->value();
HTMLLabelElement* label = labelForElement(input);
if (label)
return label->innerText();
}
if (m_renderer->element()->isLink() || [self isHeading])
return [self textUnderElement];
if ([self isAttachment]) {
NSView* attachmentView = [self attachmentView];
if ([[attachmentView accessibilityAttributeNames] containsObject:NSAccessibilityTitleAttribute])
return [attachmentView accessibilityAttributeValue:NSAccessibilityTitleAttribute];
}
return nil;
}
- (NSString*)accessibilityDescription
{
if (!m_renderer || m_areaElement)
return nil;
if (m_renderer->isImage()) {
if (m_renderer->element() && m_renderer->element()->isHTMLElement()) {
const AtomicString& alt = static_cast<Element*>(m_renderer->element())->getAttribute(altAttr);
if (alt.isEmpty())
return nil;
return alt;
}
} else if ([self isAttachment]) {
NSView* attachmentView = [self attachmentView];
if ([[attachmentView accessibilityAttributeNames] containsObject:NSAccessibilityDescriptionAttribute])
return [attachmentView accessibilityAttributeValue:NSAccessibilityDescriptionAttribute];
}
if ([self isWebArea]) {
Document *document = m_renderer->document();
Node* owner = document->ownerElement();
if (owner) {
if (owner->hasTagName(frameTag) || owner->hasTagName(iframeTag)) {
HTMLFrameElementBase* frameElement = static_cast<HTMLFrameElementBase*>(owner);
return frameElement->name();
} else if (owner->isHTMLElement()) {
return static_cast<Element*>(owner)->getAttribute(nameAttr);
}
} else {
owner = document->body();
if (owner && owner->isHTMLElement())
return static_cast<Element*>(owner)->getAttribute(nameAttr);
}
}
return nil;
}
static IntRect boundingBoxRect(RenderObject* obj)
{
IntRect rect;
if (obj) {
if (obj->isInlineContinuation())
obj = obj->element()->renderer();
Vector<IntRect> rects;
int x, y;
obj->absolutePosition(x, y);
obj->absoluteRects(rects, x, y);
const size_t n = rects.size();
for (size_t i = 0; i < n; ++i) {
IntRect r = rects[i];
if (!r.isEmpty()) {
if (obj->style()->hasAppearance())
theme()->adjustRepaintRect(obj, r);
rect.unite(r);
}
}
}
return rect;
}
-(NSValue*)position
{
IntRect rect = m_areaElement ? m_areaElement->getRect(m_renderer) : boundingBoxRect(m_renderer);
// The Cocoa accessibility API wants the lower-left corner.
NSPoint point = NSMakePoint(rect.x(), rect.bottom());
if (m_renderer && m_renderer->view() && m_renderer->view()->frameView()) {
NSView* view = m_renderer->view()->frameView()->getDocumentView();
point = [[view window] convertBaseToScreen: [view convertPoint: point toView:nil]];
}
return [NSValue valueWithPoint: point];
}
-(NSValue*)size
{
IntRect rect = m_areaElement ? m_areaElement->getRect(m_renderer) : boundingBoxRect(m_renderer);
return [NSValue valueWithSize: NSMakeSize(rect.width(), rect.height())];
}
// accessibilityShouldUseUniqueId is an AppKit method we override so that
// objects will be given a unique ID, and therefore allow AppKit to know when they
// become obsolete (e.g. when the user navigates to a new web page, making this one
// unrendered but not deallocated because it is in the back/forward cache).
// It is important to call NSAccessibilityUnregisterUniqueIdForUIElement in the
// appropriate place (e.g. dealloc) to remove these non-retained references from
// AppKit's id mapping tables. We do this in detach by calling unregisterUniqueIdForUIElement.
//
// Registering an object is also required for observing notifications. Only registered objects can be observed.
- (BOOL)accessibilityShouldUseUniqueId {
if (!m_renderer)
return NO;
if ([self isWebArea])
return YES;
if ([self isTextControl])
return YES;
return NO;
}
-(BOOL)accessibilityIsIgnored
{
// ignore invisible element
if (!m_renderer || m_renderer->style()->visibility() != VISIBLE)
return YES;
// ignore popup menu items because AppKit does
for (RenderObject* parent = m_renderer->parent(); parent; parent = parent->parent()) {
if (parent->isMenuList())
return YES;
}
// NOTE: BRs always have text boxes now, so the text box check here can be removed
if (m_renderer->isText())
return m_renderer->isBR() || !static_cast<RenderText*>(m_renderer)->firstTextBox();
// delegate to the attachment
if ([self isAttachment])
return [[self attachmentView] accessibilityIsIgnored];
if (m_areaElement || (m_renderer->element() && m_renderer->element()->isLink()))
return NO;
// all controls are accessible
if (m_renderer->element() && m_renderer->element()->isControl())
return NO;
if (m_renderer->isBlockFlow() && m_renderer->childrenInline())
return !static_cast<RenderBlock*>(m_renderer)->firstLineBox() && ![self mouseButtonListener];
// ignore images seemingly used as spacers
if (m_renderer->isImage()) {
// informal standard is to ignore images with zero-length alt strings
Element* elt = static_cast<Element*>(m_renderer->element());
if (elt) {
const AtomicString& alt = elt->getAttribute(altAttr);
if (alt.isEmpty() && !alt.isNull())
return YES;
}
// check for one-dimensional image
if (m_renderer->height() <= 1 || m_renderer->width() <= 1)
return YES;
// check whether rendered image was stretched from one-dimensional file image
RenderImage* image = static_cast<RenderImage*>(m_renderer);
if (image->cachedImage()) {
IntSize imageSize = image->cachedImage()->imageSize();
return (imageSize.height() <= 1 || imageSize.width() <= 1);
}
return NO;
}
return (!m_renderer->isListMarker() && ![self isWebArea]);
}
- (NSArray*)accessibilityAttributeNames
{
if ([self isAttachment])
return [[self attachmentView] accessibilityAttributeNames];
static NSArray* attributes = nil;
static NSArray* anchorAttrs = nil;
static NSArray* webAreaAttrs = nil;
static NSArray* textAttrs = nil;
NSMutableArray* tempArray;
if (attributes == nil) {
attributes = [[NSArray alloc] initWithObjects: NSAccessibilityRoleAttribute,
NSAccessibilitySubroleAttribute,
NSAccessibilityRoleDescriptionAttribute,
NSAccessibilityChildrenAttribute,
NSAccessibilityHelpAttribute,
NSAccessibilityParentAttribute,
NSAccessibilityPositionAttribute,
NSAccessibilitySizeAttribute,
NSAccessibilityTitleAttribute,
NSAccessibilityDescriptionAttribute,
NSAccessibilityValueAttribute,
NSAccessibilityFocusedAttribute,
NSAccessibilityEnabledAttribute,
NSAccessibilityWindowAttribute,
@"AXSelectedTextMarkerRange",
@"AXStartTextMarker",
@"AXEndTextMarker",
@"AXVisited",
nil];
}
if (anchorAttrs == nil) {
tempArray = [[NSMutableArray alloc] initWithArray:attributes];
[tempArray addObject: NSAccessibilityURLAttribute];
anchorAttrs = [[NSArray alloc] initWithArray:tempArray];
[tempArray release];
}
if (webAreaAttrs == nil) {
tempArray = [[NSMutableArray alloc] initWithArray:attributes];
[tempArray addObject: @"AXLinkUIElements"];
[tempArray addObject: @"AXLoaded"];
[tempArray addObject: @"AXLayoutCount"];
webAreaAttrs = [[NSArray alloc] initWithArray:tempArray];
[tempArray release];
}
if (textAttrs == nil) {
tempArray = [[NSMutableArray alloc] initWithArray:attributes];
[tempArray addObject: NSAccessibilityNumberOfCharactersAttribute];
[tempArray addObject: NSAccessibilitySelectedTextAttribute];
[tempArray addObject: NSAccessibilitySelectedTextRangeAttribute];
[tempArray addObject: NSAccessibilityVisibleCharacterRangeAttribute];
[tempArray addObject: NSAccessibilityInsertionPointLineNumberAttribute];
textAttrs = [[NSArray alloc] initWithArray:tempArray];
[tempArray release];
}
if (!m_renderer || [self isPasswordField])
return attributes;
if ([self isWebArea])
return webAreaAttrs;
if ([self isTextControl])
return textAttrs;
if ([self isAnchor] || m_renderer->isImage())
return anchorAttrs;
return attributes;
}
- (NSArray*)accessibilityActionNames
{
static NSArray* actions = nil;
if (actions == nil) {
if ([self actionElement])
actions = [[NSArray alloc] initWithObjects: NSAccessibilityPressAction, nil];
else if ([self isAttachment])
actions = [[[self attachmentView] accessibilityActionNames] retain];
}
return actions;
}
- (NSString*)accessibilityActionDescription:(NSString*)action
{
// we have no custom actions
return NSAccessibilityActionDescription(action);
}
- (void)accessibilityPerformAction:(NSString*)action
{
if ([action isEqualToString:NSAccessibilityPressAction]) {
if ([self isAttachment]) {
[[self attachmentView] accessibilityPerformAction:action];
return;
}
Element* actionElement = [self actionElement];
if (!actionElement)
return;
if (Frame* f = actionElement->document()->frame())
f->loader()->resetMultipleFormSubmissionProtection();
actionElement->accessKeyAction(true);
}
}
- (WebCoreTextMarkerRange*) textMarkerRangeFromMarkers: (WebCoreTextMarker*) textMarker1 andEndMarker:(WebCoreTextMarker*) textMarker2
{
return [[WebCoreViewFactory sharedFactory] textMarkerRangeWithStart:textMarker1 end:textMarker2];
}
- (WebCoreTextMarker*) textMarkerForVisiblePosition: (VisiblePosition)visiblePos
{
if (visiblePos.isNull())
return nil;
if (isPasswordFieldElement(visiblePos.deepEquivalent().node()))
return nil;
return m_renderer->document()->axObjectCache()->textMarkerForVisiblePosition(visiblePos);
}
- (VisiblePosition) visiblePositionForTextMarker: (WebCoreTextMarker*)textMarker
{
return m_renderer->document()->axObjectCache()->visiblePositionForTextMarker(textMarker);
}
- (VisiblePosition) visiblePositionForStartOfTextMarkerRange: (WebCoreTextMarkerRange*)textMarkerRange
{
return [self visiblePositionForTextMarker:[[WebCoreViewFactory sharedFactory] startOfTextMarkerRange:textMarkerRange]];
}
- (VisiblePosition) visiblePositionForEndOfTextMarkerRange: (WebCoreTextMarkerRange*) textMarkerRange
{
return [self visiblePositionForTextMarker:[[WebCoreViewFactory sharedFactory] endOfTextMarkerRange:textMarkerRange]];
}
- (WebCoreTextMarkerRange*) textMarkerRangeFromVisiblePositions: (VisiblePosition) startPosition andEndPos: (VisiblePosition) endPosition
{
WebCoreTextMarker* startTextMarker = [self textMarkerForVisiblePosition: startPosition];
WebCoreTextMarker* endTextMarker = [self textMarkerForVisiblePosition: endPosition];
return [self textMarkerRangeFromMarkers: startTextMarker andEndMarker:endTextMarker];
}
- (WebCoreTextMarkerRange*)textMarkerRange
{
if (!m_renderer)
return nil;
// construct VisiblePositions for start and end
Node* node = m_renderer->element();
VisiblePosition visiblePos1 = VisiblePosition(node, 0, VP_DEFAULT_AFFINITY);
VisiblePosition visiblePos2 = VisiblePosition(node, maxDeepOffset(node), VP_DEFAULT_AFFINITY);
// the VisiblePositions are equal for nodes like buttons, so adjust for that
if (visiblePos1 == visiblePos2) {
visiblePos2 = visiblePos2.next();
if (visiblePos2.isNull())
visiblePos2 = visiblePos1;
}
WebCoreTextMarker* startTextMarker = [self textMarkerForVisiblePosition: visiblePos1];
WebCoreTextMarker* endTextMarker = [self textMarkerForVisiblePosition: visiblePos2];
return [self textMarkerRangeFromMarkers: startTextMarker andEndMarker:endTextMarker];
}
- (RenderObject*)topRenderer
{
return m_renderer->document()->topDocument()->renderer();
}
- (FrameView*)frameView
{
return m_renderer->document()->view();
}
- (FrameView*)topFrameView
{
return m_renderer->document()->topDocument()->renderer()->view()->frameView();
}
- (id)accessibilityAttributeValue:(NSString*)attributeName
{
if (!m_renderer)
return nil;
if ([attributeName isEqualToString: NSAccessibilityRoleAttribute])
return [self role];
if ([attributeName isEqualToString: NSAccessibilitySubroleAttribute])
return [self subrole];
if ([attributeName isEqualToString: NSAccessibilityRoleDescriptionAttribute])
return [self roleDescription];
if ([attributeName isEqualToString: NSAccessibilityParentAttribute]) {
if (m_renderer->isRenderView() && m_renderer->view() && m_renderer->view()->frameView())
return m_renderer->view()->frameView()->getView();
return [self parentObjectUnignored];
}
if ([attributeName isEqualToString: NSAccessibilityChildrenAttribute]) {
if (!m_children) {
m_children = [NSMutableArray arrayWithCapacity: 8];
[m_children retain];
[self addChildrenToArray: m_children];
}
return m_children;
}
if ([self isWebArea]) {
if ([attributeName isEqualToString: @"AXLinkUIElements"]) {
NSMutableArray* links = [NSMutableArray arrayWithCapacity: 32];
RefPtr<HTMLCollection> coll = m_renderer->document()->links();
Node* curr = coll->firstItem();
while (curr) {
RenderObject* obj = curr->renderer();
if (obj) {
WebCoreAXObject* axobj = obj->document()->axObjectCache()->get(obj);
ASSERT([[axobj role] isEqualToString:@"AXLink"]);
if (![axobj accessibilityIsIgnored])
[links addObject: axobj];
}
curr = coll->nextItem();
}
return links;
}
if ([attributeName isEqualToString: @"AXLoaded"])
return [NSNumber numberWithBool: (!m_renderer->document()->tokenizer())];
if ([attributeName isEqualToString: @"AXLayoutCount"])
return [NSNumber numberWithInt: (static_cast<RenderView*>(m_renderer)->frameView()->layoutCount())];
}
if ([self isTextControl]) {
RenderTextControl* textControl = static_cast<RenderTextControl*>(m_renderer);
if ([attributeName isEqualToString: NSAccessibilityNumberOfCharactersAttribute])
return [self isPasswordField] ? nil : [NSNumber numberWithUnsignedInt: textControl->text().length()];
if ([attributeName isEqualToString: NSAccessibilitySelectedTextAttribute]) {
if ([self isPasswordField])
return nil;
NSString* text = textControl->text();
return [text substringWithRange: NSMakeRange(textControl->selectionStart(), textControl->selectionEnd() - textControl->selectionStart())];
}
if ([attributeName isEqualToString: NSAccessibilitySelectedTextRangeAttribute])
return [self isPasswordField] ? nil : [NSValue valueWithRange: NSMakeRange(textControl->selectionStart(), textControl->selectionEnd() - textControl->selectionStart())];
// TODO: Get actual visible range. <rdar://problem/4712101>
if ([attributeName isEqualToString: NSAccessibilityVisibleCharacterRangeAttribute])
return [self isPasswordField] ? nil : [NSValue valueWithRange: NSMakeRange(0, textControl->text().length())];
if ([attributeName isEqualToString: NSAccessibilityInsertionPointLineNumberAttribute]) {
if ([self isPasswordField] || textControl->selectionStart() != textControl->selectionEnd())
return nil;
NSNumber* index = [NSNumber numberWithInt: textControl->selectionStart()];
return [self doAXLineForTextMarker: [self textMarkerForIndex: index lastIndexOK: YES]];
}
}
if ([attributeName isEqualToString: NSAccessibilityURLAttribute]) {
if ([self isAnchor]) {
HTMLAnchorElement* anchor = [self anchorElement];
if (anchor) {
DeprecatedString s = anchor->getAttribute(hrefAttr).deprecatedString();
if (!s.isNull()) {
s = anchor->document()->completeURL(s);
return KURL(s).getNSURL();
}
}
}
else if (m_renderer->isImage() && m_renderer->element() && m_renderer->element()->hasTagName(imgTag)) {
DeprecatedString src = static_cast<HTMLImageElement*>(m_renderer->element())->src().deprecatedString();
if (!src.isNull())
return KURL(src).getNSURL();
}
return nil;
}
if ([attributeName isEqualToString: @"AXVisited"])
return [NSNumber numberWithBool: m_renderer->style()->pseudoState() == PseudoVisited];
if ([attributeName isEqualToString: NSAccessibilityTitleAttribute])
return [self title];
if ([attributeName isEqualToString: NSAccessibilityDescriptionAttribute])
return [self accessibilityDescription];
if ([attributeName isEqualToString: NSAccessibilityValueAttribute])
return [self value];
if ([attributeName isEqualToString: NSAccessibilityHelpAttribute])
return [self helpText];
if ([attributeName isEqualToString: NSAccessibilityFocusedAttribute])
return [NSNumber numberWithBool: (m_renderer->element() && m_renderer->document()->focusedNode() == m_renderer->element())];
if ([attributeName isEqualToString: NSAccessibilityEnabledAttribute])
return [NSNumber numberWithBool: m_renderer->element() ? m_renderer->element()->isEnabled() : YES];
if ([attributeName isEqualToString: NSAccessibilitySizeAttribute])
return [self size];
if ([attributeName isEqualToString: NSAccessibilityPositionAttribute])
return [self position];
if ([attributeName isEqualToString: NSAccessibilityWindowAttribute]) {
if (m_renderer && m_renderer->view() && m_renderer->view()->frameView())
return [m_renderer->view()->frameView()->getView() window];
return nil;
}
if ([attributeName isEqualToString: @"AXSelectedTextMarkerRange"]) {
// get the selection from the document
Selection selection = [self frameView]->frame()->selectionController()->selection();
if (selection.isNone())
return nil;
return (id) [self textMarkerRangeFromVisiblePositions:selection.visibleStart() andEndPos:selection.visibleEnd()];
}
if ([attributeName isEqualToString: @"AXStartTextMarker"])
return (id) [self textMarkerForVisiblePosition: startOfDocument(m_renderer->document())];
if ([attributeName isEqualToString: @"AXEndTextMarker"])
return (id) [self textMarkerForVisiblePosition: endOfDocument(m_renderer->document())];
return nil;
}
- (NSArray* )accessibilityParameterizedAttributeNames
{
if ([self isAttachment])
return nil;
static NSArray* paramAttrs = nil;
static NSArray* textParamAttrs = nil;
if (paramAttrs == nil) {
paramAttrs = [[NSArray alloc] initWithObjects:
@"AXUIElementForTextMarker",
@"AXTextMarkerRangeForUIElement",
@"AXLineForTextMarker",
@"AXTextMarkerRangeForLine",
@"AXStringForTextMarkerRange",
@"AXTextMarkerForPosition",
@"AXBoundsForTextMarkerRange",
@"AXAttributedStringForTextMarkerRange",
@"AXTextMarkerRangeForUnorderedTextMarkers",
@"AXNextTextMarkerForTextMarker",
@"AXPreviousTextMarkerForTextMarker",
@"AXLeftWordTextMarkerRangeForTextMarker",
@"AXRightWordTextMarkerRangeForTextMarker",
@"AXLeftLineTextMarkerRangeForTextMarker",
@"AXRightLineTextMarkerRangeForTextMarker",
@"AXSentenceTextMarkerRangeForTextMarker",
@"AXParagraphTextMarkerRangeForTextMarker",
@"AXNextWordEndTextMarkerForTextMarker",
@"AXPreviousWordStartTextMarkerForTextMarker",
@"AXNextLineEndTextMarkerForTextMarker",
@"AXPreviousLineStartTextMarkerForTextMarker",
@"AXNextSentenceEndTextMarkerForTextMarker",
@"AXPreviousSentenceStartTextMarkerForTextMarker",
@"AXNextParagraphEndTextMarkerForTextMarker",
@"AXPreviousParagraphStartTextMarkerForTextMarker",
@"AXStyleTextMarkerRangeForTextMarker",
@"AXLengthForTextMarkerRange",
nil];
}
if (textParamAttrs == nil) {
NSMutableArray* tempArray = [[NSMutableArray alloc] initWithArray:paramAttrs];
[tempArray addObject: (NSString*)kAXLineForIndexParameterizedAttribute];
[tempArray addObject: (NSString*)kAXRangeForLineParameterizedAttribute];
[tempArray addObject: (NSString*)kAXStringForRangeParameterizedAttribute];
[tempArray addObject: (NSString*)kAXRangeForPositionParameterizedAttribute];
[tempArray addObject: (NSString*)kAXRangeForIndexParameterizedAttribute];
[tempArray addObject: (NSString*)kAXBoundsForRangeParameterizedAttribute];
[tempArray addObject: (NSString*)kAXRTFForRangeParameterizedAttribute];
[tempArray addObject: (NSString*)kAXAttributedStringForRangeParameterizedAttribute];
[tempArray addObject: (NSString*)kAXStyleRangeForIndexParameterizedAttribute];
textParamAttrs = [[NSArray alloc] initWithArray:tempArray];
[tempArray release];
}
if ([self isPasswordField])
return [NSArray array];
if (!m_renderer)
return paramAttrs;
if ([self isTextControl])
return textParamAttrs;
return paramAttrs;
}
- (id)doAXUIElementForTextMarker: (WebCoreTextMarker* ) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
RenderObject* obj = visiblePos.deepEquivalent().node()->renderer();
if (!obj)
return nil;
return obj->document()->axObjectCache()->get(obj);
}
- (id)doAXTextMarkerRangeForUIElement: (id) uiElement
{
return (id)[uiElement textMarkerRange];
}
- (id)doAXLineForTextMarker: (WebCoreTextMarker* ) textMarker
{
unsigned int lineCount = 0;
VisiblePosition savedVisiblePos;
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// move up until we get to the top
// NOTE: BUG This only takes us to the top of the rootEditableElement, not the top of the
// top document.
while (visiblePos.isNotNull() && !(inSameLine(visiblePos, savedVisiblePos))) {
lineCount += 1;
savedVisiblePos = visiblePos;
visiblePos = previousLinePosition(visiblePos, 0);
}
return [NSNumber numberWithUnsignedInt:(lineCount - 1)];
}
- (id)doAXTextMarkerRangeForLine: (NSNumber*) lineNumber
{
unsigned lineCount = [lineNumber unsignedIntValue];
if (lineCount == 0 || !m_renderer)
return nil;
// iterate over the lines
// NOTE: BUG this is wrong when lineNumber is lineCount+1, because nextLinePosition takes you to the
// last offset of the last line
VisiblePosition visiblePos = m_renderer->document()->renderer()->positionForCoordinates(0, 0);
VisiblePosition savedVisiblePos;
while (--lineCount != 0) {
savedVisiblePos = visiblePos;
visiblePos = nextLinePosition(visiblePos, 0);
if (visiblePos.isNull() || visiblePos == savedVisiblePos)
return nil;
}
// make a caret selection for the marker position, then extend it to the line
// NOTE: ignores results of sel.modify because it returns false when
// starting at an empty line. The resulting selection in that case
// will be a caret at visiblePos.
SelectionController selectionController;
selectionController.setSelection(Selection(visiblePos));
selectionController.modify(SelectionController::EXTEND, SelectionController::RIGHT, LineBoundary);
// return a marker range for the selection start to end
VisiblePosition startPosition = selectionController.selection().visibleStart();
VisiblePosition endPosition = selectionController.selection().visibleEnd();
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
static NSString *nsStringForReplacedNode(Node* replacedNode)
{
// we should always be given a rendered node and a replaced node, but be safe
// replaced nodes are either attachments (widgets) or images
if (!replacedNode || !replacedNode->renderer() || !replacedNode->renderer()->isReplaced() || replacedNode->isTextNode()) {
ASSERT_NOT_REACHED();
return nil;
}
// create an AX object, but skip it if it is not supposed to be seen
WebCoreAXObject* obj = replacedNode->renderer()->document()->axObjectCache()->get(replacedNode->renderer());
if ([obj accessibilityIsIgnored])
return nil;
// use the attachmentCharacter to represent the replaced node
const UniChar attachmentChar = NSAttachmentCharacter;
return [NSString stringWithCharacters:&attachmentChar length:1];
}
- (id)doAXStringForTextMarkerRange: (WebCoreTextMarkerRange*) textMarkerRange
{
// extract the start and end VisiblePosition
VisiblePosition startVisiblePosition = [self visiblePositionForStartOfTextMarkerRange: textMarkerRange];
if (startVisiblePosition.isNull())
return nil;
VisiblePosition endVisiblePosition = [self visiblePositionForEndOfTextMarkerRange: textMarkerRange];
if (endVisiblePosition.isNull())
return nil;
NSMutableString* resultString = [[[NSMutableString alloc] init] autorelease];
TextIterator it(makeRange(startVisiblePosition, endVisiblePosition).get());
while (!it.atEnd()) {
// non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
if (it.length() != 0) {
[resultString appendString:[NSString stringWithCharacters:it.characters() length:it.length()]];
} else {
// locate the node and starting offset for this replaced range
int exception = 0;
Node* node = it.range()->startContainer(exception);
ASSERT(node == it.range()->endContainer(exception));
int offset = it.range()->startOffset(exception);
NSString* attachmentString = nsStringForReplacedNode(node->childNode(offset));
// append the replacement string
if (attachmentString)
[resultString appendString:attachmentString];
}
it.advance();
}
return [resultString length] > 0 ? resultString : nil;
}
- (id)doAXTextMarkerForPosition: (NSPoint) point
{
// convert absolute point to view coordinates
FrameView* frameView = [self topFrameView];
NSView* view = frameView->getDocumentView();
RenderObject* renderer = [self topRenderer];
Node* innerNode = 0;
// locate the node containing the point
IntPoint pointResult;
while (1) {
// ask the document layer to hitTest
NSPoint windowCoord = [[view window] convertScreenToBase: point];
IntPoint ourpoint([view convertPoint:windowCoord fromView:nil]);
HitTestRequest request(true, true);
HitTestResult result(ourpoint);
renderer->layer()->hitTest(request, result);
innerNode = result.innerNode();
if (!innerNode || !innerNode->renderer())
return nil;
pointResult = result.localPoint();
// done if hit something other than a widget
renderer = innerNode->renderer();
if (!renderer->isWidget())
break;
// descend into widget (FRAME, IFRAME, OBJECT...)
Widget* widget = static_cast<RenderWidget*>(renderer)->widget();
if (!widget || !widget->isFrameView())
break;
Frame* frame = static_cast<FrameView*>(widget)->frame();
if (!frame)
break;
Document* document = frame->document();
if (!document)
break;
renderer = document->renderer();
frameView = static_cast<FrameView*>(widget);
view = frameView->getDocumentView();
}
// get position within the node
VisiblePosition pos = innerNode->renderer()->positionForPoint(pointResult);
return (id) [self textMarkerForVisiblePosition:pos];
}
- (id)doAXBoundsForTextMarkerRange: (WebCoreTextMarkerRange*) textMarkerRange
{
// extract the start and end VisiblePosition
VisiblePosition startVisiblePosition = [self visiblePositionForStartOfTextMarkerRange: textMarkerRange];
if (startVisiblePosition.isNull())
return nil;
VisiblePosition endVisiblePosition = [self visiblePositionForEndOfTextMarkerRange: textMarkerRange];
if (endVisiblePosition.isNull())
return nil;
IntRect rect1 = startVisiblePosition.caretRect();
IntRect rect2 = endVisiblePosition.caretRect();
// readjust for position at the edge of a line. This is to exclude line rect that doesn't need to be accounted in the range bounds
if (rect2.y() != rect1.y()) {
VisiblePosition endOfFirstLine = endOfLine(startVisiblePosition);
if (startVisiblePosition == endOfFirstLine) {
startVisiblePosition.setAffinity(DOWNSTREAM);
rect1 = startVisiblePosition.caretRect();
}
if (endVisiblePosition == endOfFirstLine) {
endVisiblePosition.setAffinity(UPSTREAM);
rect2 = endVisiblePosition.caretRect();
}
}
IntRect ourrect = rect1;
ourrect.unite(rect2);
// try to use the document view from the first position, so that nested WebAreas work,
// but fall back to the top level doc if we do not find it easily
RenderObject* renderer = startVisiblePosition.deepEquivalent().node()->renderer();
FrameView* frameView = renderer ? renderer->document()->view() : 0;
if (!frameView)
frameView = [self frameView];
NSView *view = frameView->getView();
// if the rectangle spans lines and contains multiple text chars, use the range's bounding box intead
if (rect1.bottom() != rect2.bottom()) {
RefPtr<Range> dataRange = makeRange(startVisiblePosition, endVisiblePosition);
IntRect boundingBox = dataRange->boundingBox();
String rangeString = plainText(dataRange.get());
if (rangeString.length() > 1 && !boundingBox.isEmpty())
ourrect = boundingBox;
}
// convert our rectangle to screen coordinates
NSRect rect = ourrect;
rect = NSOffsetRect(rect, -frameView->contentsX(), -frameView->contentsY());
rect = [view convertRect:rect toView:nil];
rect.origin = [[view window] convertBaseToScreen:rect.origin];
// return the converted rect
return [NSValue valueWithRect:rect];
}
static CGColorRef CreateCGColorIfDifferent(NSColor* nsColor, CGColorRef existingColor)
{
// get color information assuming NSDeviceRGBColorSpace
NSColor* rgbColor = [nsColor colorUsingColorSpaceName:NSDeviceRGBColorSpace];
if (rgbColor == nil)
rgbColor = [NSColor blackColor];
CGFloat components[4];
[rgbColor getRed:&components[0] green:&components[1] blue:&components[2] alpha:&components[3]];
// create a new CGColorRef to return
CGColorSpaceRef cgColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
CGColorRef cgColor = CGColorCreate(cgColorSpace, components);
CGColorSpaceRelease(cgColorSpace);
CFMakeCollectable(cgColor);
// check for match with existing color
if (existingColor && CGColorEqualToColor(cgColor, existingColor))
cgColor = nil;
return cgColor;
}
static void AXAttributeStringSetColor(NSMutableAttributedString* attrString, NSString* attribute, NSColor* color, NSRange range)
{
if (color) {
CGColorRef existingColor = (CGColorRef) [attrString attribute:attribute atIndex:range.location effectiveRange:nil];
CGColorRef cgColor = CreateCGColorIfDifferent(color, existingColor);
if (cgColor) {
[attrString addAttribute:attribute value:(id)cgColor range:range];
CGColorRelease(cgColor);
}
} else
[attrString removeAttribute:attribute range:range];
}
static void AXAttributeStringSetNumber(NSMutableAttributedString* attrString, NSString* attribute, NSNumber* number, NSRange range)
{
if (number)
[attrString addAttribute:attribute value:number range:range];
else
[attrString removeAttribute:attribute range:range];
}
static void AXAttributeStringSetFont(NSMutableAttributedString* attrString, NSString* attribute, NSFont* font, NSRange range)
{
NSDictionary* dict;
if (font) {
dict = [NSDictionary dictionaryWithObjectsAndKeys:
[font fontName] , NSAccessibilityFontNameKey,
[font familyName] , NSAccessibilityFontFamilyKey,
[font displayName] , NSAccessibilityVisibleNameKey,
[NSNumber numberWithFloat:[font pointSize]] , NSAccessibilityFontSizeKey,
nil];
[attrString addAttribute:attribute value:dict range:range];
} else
[attrString removeAttribute:attribute range:range];
}
static void AXAttributeStringSetStyle(NSMutableAttributedString* attrString, RenderObject* renderer, NSRange range)
{
RenderStyle* style = renderer->style();
// set basic font info
AXAttributeStringSetFont(attrString, NSAccessibilityFontTextAttribute, style->font().primaryFont()->getNSFont(), range);
// set basic colors
AXAttributeStringSetColor(attrString, NSAccessibilityForegroundColorTextAttribute, nsColor(style->color()), range);
AXAttributeStringSetColor(attrString, NSAccessibilityBackgroundColorTextAttribute, nsColor(style->backgroundColor()), range);
// set super/sub scripting
EVerticalAlign alignment = style->verticalAlign();
if (alignment == SUB)
AXAttributeStringSetNumber(attrString, NSAccessibilitySuperscriptTextAttribute, [NSNumber numberWithInt:(-1)], range);
else if (alignment == SUPER)
AXAttributeStringSetNumber(attrString, NSAccessibilitySuperscriptTextAttribute, [NSNumber numberWithInt:1], range);
else
[attrString removeAttribute:NSAccessibilitySuperscriptTextAttribute range:range];
// set shadow
if (style->textShadow())
AXAttributeStringSetNumber(attrString, NSAccessibilityShadowTextAttribute, [NSNumber numberWithBool:YES], range);
else
[attrString removeAttribute:NSAccessibilityShadowTextAttribute range:range];
// set underline and strikethrough
int decor = style->textDecorationsInEffect();
if ((decor & UNDERLINE) == 0) {
[attrString removeAttribute:NSAccessibilityUnderlineTextAttribute range:range];
[attrString removeAttribute:NSAccessibilityUnderlineColorTextAttribute range:range];
}
if ((decor & LINE_THROUGH) == 0) {
[attrString removeAttribute:NSAccessibilityStrikethroughTextAttribute range:range];
[attrString removeAttribute:NSAccessibilityStrikethroughColorTextAttribute range:range];
}
if ((decor & (UNDERLINE | LINE_THROUGH)) != 0) {
// find colors using quirk mode approach (strict mode would use current
// color for all but the root line box, which would use getTextDecorationColors)
Color underline, overline, linethrough;
renderer->getTextDecorationColors(decor, underline, overline, linethrough);
if ((decor & UNDERLINE) != 0) {
AXAttributeStringSetNumber(attrString, NSAccessibilityUnderlineTextAttribute, [NSNumber numberWithBool:YES], range);
AXAttributeStringSetColor(attrString, NSAccessibilityUnderlineColorTextAttribute, nsColor(underline), range);
}
if ((decor & LINE_THROUGH) != 0) {
AXAttributeStringSetNumber(attrString, NSAccessibilityStrikethroughTextAttribute, [NSNumber numberWithBool:YES], range);
AXAttributeStringSetColor(attrString, NSAccessibilityStrikethroughColorTextAttribute, nsColor(linethrough), range);
}
}
}
static void AXAttributeStringSetHeadingLevel(NSMutableAttributedString* attrString, RenderObject* renderer, NSRange range)
{
int parentHeadingLevel = headingLevel(renderer->parent());
if (parentHeadingLevel)
[attrString addAttribute:@"AXHeadingLevel" value:[NSNumber numberWithInt:parentHeadingLevel] range:range];
else
[attrString removeAttribute:@"AXHeadingLevel" range:range];
}
static void AXAttributeStringSetBlockquoteLevel(NSMutableAttributedString* attrString, RenderObject* renderer, NSRange range)
{
int quoteLevel = blockquoteLevel(renderer);
if (quoteLevel)
[attrString addAttribute:@"AXBlockQuoteLevel" value:[NSNumber numberWithInt:quoteLevel] range:range];
else
[attrString removeAttribute:@"AXBlockQuoteLevel" range:range];
}
static void AXAttributeStringSetElement(NSMutableAttributedString* attrString, NSString* attribute, id element, NSRange range)
{
if (element) {
// make a serialiazable AX object
AXUIElementRef axElement = [[WebCoreViewFactory sharedFactory] AXUIElementForElement:element];
if (axElement) {
[attrString addAttribute:attribute value:(id)axElement range:range];
CFRelease(axElement);
}
} else
[attrString removeAttribute:attribute range:range];
}
static WebCoreAXObject* AXLinkElementForNode (Node* node)
{
RenderObject* obj = node->renderer();
if (!obj)
return nil;
WebCoreAXObject* axObj = obj->document()->axObjectCache()->get(obj);
HTMLAnchorElement* anchor = [axObj anchorElement];
if (!anchor || !anchor->renderer())
return nil;
return anchor->renderer()->document()->axObjectCache()->get(anchor->renderer());
}
static void AXAttributeStringSetSpelling(NSMutableAttributedString* attrString, Node* node, int offset, NSRange range)
{
Vector<DocumentMarker> markers = node->renderer()->document()->markersForNode(node);
Vector<DocumentMarker>::iterator markerIt = markers.begin();
unsigned endOffset = (unsigned)offset + range.length;
for ( ; markerIt != markers.end(); markerIt++) {
DocumentMarker marker = *markerIt;
if (marker.type != DocumentMarker::Spelling)
continue;
if (marker.endOffset <= (unsigned)offset)
continue;
if (marker.startOffset > endOffset)
break;
// add misspelling attribute for the intersection of the marker and the range
int rStart = range.location + (marker.startOffset - offset);
int rLength = MIN(marker.endOffset, endOffset) - marker.startOffset;
NSRange spellRange = NSMakeRange(rStart, rLength);
AXAttributeStringSetNumber(attrString, NSAccessibilityMisspelledTextAttribute, [NSNumber numberWithBool:YES], spellRange);
if (marker.endOffset > endOffset + 1)
break;
}
}
static void AXAttributedStringAppendText(NSMutableAttributedString* attrString, Node* node, int offset, const UChar* chars, int length)
{
// skip invisible text
if (!node->renderer())
return;
// easier to calculate the range before appending the string
NSRange attrStringRange = NSMakeRange([attrString length], length);
// append the string from this node
[[attrString mutableString] appendString:[NSString stringWithCharacters:chars length:length]];
// add new attributes and remove irrelevant inherited ones
// NOTE: color attributes are handled specially because -[NSMutableAttributedString addAttribute: value: range:] does not merge
// identical colors. Workaround is to not replace an existing color attribute if it matches what we are adding. This also means
// we can not just pre-remove all inherited attributes on the appended string, so we have to remove the irrelevant ones individually.
// remove inherited attachment from prior AXAttributedStringAppendReplaced
[attrString removeAttribute:NSAccessibilityAttachmentTextAttribute range:attrStringRange];
// set new attributes
AXAttributeStringSetStyle(attrString, node->renderer(), attrStringRange);
AXAttributeStringSetHeadingLevel(attrString, node->renderer(), attrStringRange);
AXAttributeStringSetBlockquoteLevel(attrString, node->renderer(), attrStringRange);
AXAttributeStringSetElement(attrString, NSAccessibilityLinkTextAttribute, AXLinkElementForNode(node), attrStringRange);
// do spelling last because it tends to break up the range
AXAttributeStringSetSpelling(attrString, node, offset, attrStringRange);
}
- (id)doAXAttributedStringForTextMarkerRange: (WebCoreTextMarkerRange*) textMarkerRange
{
// extract the start and end VisiblePosition
VisiblePosition startVisiblePosition = [self visiblePositionForStartOfTextMarkerRange: textMarkerRange];
if (startVisiblePosition.isNull())
return nil;
VisiblePosition endVisiblePosition = [self visiblePositionForEndOfTextMarkerRange: textMarkerRange];
if (endVisiblePosition.isNull())
return nil;
// iterate over the range to build the AX attributed string
NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] init];
TextIterator it(makeRange(startVisiblePosition, endVisiblePosition).get());
while (!it.atEnd()) {
// locate the node and starting offset for this range
int exception = 0;
Node* node = it.range()->startContainer(exception);
ASSERT(node == it.range()->endContainer(exception));
int offset = it.range()->startOffset(exception);
// non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
if (it.length() != 0) {
AXAttributedStringAppendText(attrString, node, offset, it.characters(), it.length());
} else {
Node* replacedNode = node->childNode(offset);
NSString *attachmentString = nsStringForReplacedNode(replacedNode);
if (attachmentString) {
NSRange attrStringRange = NSMakeRange([attrString length], [attachmentString length]);
// append the placeholder string
[[attrString mutableString] appendString:attachmentString];
// remove all inherited attributes
[attrString setAttributes:nil range:attrStringRange];
// add the attachment attribute
WebCoreAXObject* obj = replacedNode->renderer()->document()->axObjectCache()->get(replacedNode->renderer());
AXAttributeStringSetElement(attrString, NSAccessibilityAttachmentTextAttribute, obj, attrStringRange);
}
}
it.advance();
}
return [attrString autorelease];
}
- (id)doAXTextMarkerRangeForUnorderedTextMarkers: (NSArray*) markers
{
// get and validate the markers
if ([markers count] < 2)
return nil;
WebCoreTextMarker* textMarker1 = (WebCoreTextMarker*) [markers objectAtIndex:0];
WebCoreTextMarker* textMarker2 = (WebCoreTextMarker*) [markers objectAtIndex:1];
if (![[WebCoreViewFactory sharedFactory] objectIsTextMarker:textMarker1] || ![[WebCoreViewFactory sharedFactory] objectIsTextMarker:textMarker2])
return nil;
// convert to VisiblePosition
VisiblePosition visiblePos1 = [self visiblePositionForTextMarker:textMarker1];
VisiblePosition visiblePos2 = [self visiblePositionForTextMarker:textMarker2];
if (visiblePos1.isNull() || visiblePos2.isNull())
return nil;
WebCoreTextMarker* startTextMarker;
WebCoreTextMarker* endTextMarker;
bool alreadyInOrder;
// upstream is ordered before downstream for the same position
if (visiblePos1 == visiblePos2 && visiblePos2.affinity() == UPSTREAM)
alreadyInOrder = false;
// use selection order to see if the positions are in order
else
alreadyInOrder = Selection(visiblePos1, visiblePos2).isBaseFirst();
if (alreadyInOrder) {
startTextMarker = textMarker1;
endTextMarker = textMarker2;
} else {
startTextMarker = textMarker2;
endTextMarker = textMarker1;
}
return (id) [self textMarkerRangeFromMarkers: startTextMarker andEndMarker:endTextMarker];
}
- (id)doAXNextTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
VisiblePosition nextVisiblePos = visiblePos.next();
if (nextVisiblePos.isNull())
return nil;
return (id) [self textMarkerForVisiblePosition:nextVisiblePos];
}
- (id)doAXPreviousTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
VisiblePosition previousVisiblePos = visiblePos.previous();
if (previousVisiblePos.isNull())
return nil;
return (id) [self textMarkerForVisiblePosition:previousVisiblePos];
}
- (id)doAXLeftWordTextMarkerRangeForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
VisiblePosition startPosition = startOfWord(visiblePos, LeftWordIfOnBoundary);
VisiblePosition endPosition = endOfWord(startPosition);
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
- (id)doAXRightWordTextMarkerRangeForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
VisiblePosition startPosition = startOfWord(visiblePos, RightWordIfOnBoundary);
VisiblePosition endPosition = endOfWord(startPosition);
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
static VisiblePosition updateAXLineStartForVisiblePosition(const VisiblePosition& visiblePosition)
{
// A line in the accessibility sense should include floating objects, such as aligned image, as part of a line.
// So let's update the position to include that.
VisiblePosition tempPosition;
VisiblePosition startPosition = visiblePosition;
Position p;
RenderObject* renderer;
while (true) {
tempPosition = startPosition.previous();
if (tempPosition.isNull())
break;
p = tempPosition.deepEquivalent();
if (!p.node())
break;
renderer = p.node()->renderer();
if (!renderer || renderer->inlineBox(p.offset(), tempPosition.affinity()) || (renderer->isRenderBlock() && p.offset() == 0))
break;
startPosition = tempPosition;
}
return startPosition;
}
- (id)doAXLeftLineTextMarkerRangeForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make a caret selection for the position before marker position (to make sure
// we move off of a line start)
VisiblePosition prevVisiblePos = visiblePos.previous();
if (prevVisiblePos.isNull())
return nil;
VisiblePosition startPosition = startOfLine(prevVisiblePos);
// keep searching for a valid line start position. Unless the textmarker is at the very beginning, there should
// always be a valid line range. However, startOfLine will return null for position next to a floating object,
// since floating object doesn't really belong to any line.
// This check will reposition the marker before the floating object, to ensure we get a line start.
if (startPosition.isNull()) {
while (startPosition.isNull() && prevVisiblePos.isNotNull()) {
prevVisiblePos = prevVisiblePos.previous();
startPosition = startOfLine(prevVisiblePos);
}
} else
startPosition = updateAXLineStartForVisiblePosition(startPosition);
VisiblePosition endPosition = endOfLine(prevVisiblePos);
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
- (id)doAXRightLineTextMarkerRangeForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make sure we move off of a line end
VisiblePosition nextVisiblePos = visiblePos.next();
if (nextVisiblePos.isNull())
return nil;
VisiblePosition startPosition = startOfLine(nextVisiblePos);
// fetch for a valid line start position
if (startPosition.isNull() ) {
startPosition = visiblePos;
nextVisiblePos = nextVisiblePos.next();
} else
startPosition = updateAXLineStartForVisiblePosition(startPosition);
VisiblePosition endPosition = endOfLine(nextVisiblePos);
// as long as the position hasn't reached the end of the doc, keep searching for a valid line end position
// Unless the textmarker is at the very end, there should always be a valid line range. However, endOfLine will
// return null for position by a floating object, since floating object doesn't really belong to any line.
// This check will reposition the marker after the floating object, to ensure we get a line end.
while (endPosition.isNull() && nextVisiblePos.isNotNull()) {
nextVisiblePos = nextVisiblePos.next();
endPosition = endOfLine(nextVisiblePos);
}
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
- (id)doAXSentenceTextMarkerRangeForTextMarker: (WebCoreTextMarker*) textMarker
{
// NOTE: BUG FO 2 IMPLEMENT (currently returns incorrect answer)
// Related? <rdar://problem/3927736> Text selection broken in 8A336
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
VisiblePosition startPosition = startOfSentence(visiblePos);
VisiblePosition endPosition = endOfSentence(startPosition);
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
- (id)doAXParagraphTextMarkerRangeForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
VisiblePosition startPosition = startOfParagraph(visiblePos);
VisiblePosition endPosition = endOfParagraph(startPosition);
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
- (id)doAXNextWordEndTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make sure we move off of a word end
visiblePos = visiblePos.next();
if (visiblePos.isNull())
return nil;
VisiblePosition endPosition = endOfWord(visiblePos, LeftWordIfOnBoundary);
return (id) [self textMarkerForVisiblePosition:endPosition];
}
- (id)doAXPreviousWordStartTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make sure we move off of a word start
visiblePos = visiblePos.previous();
if (visiblePos.isNull())
return nil;
VisiblePosition startPosition = startOfWord(visiblePos, RightWordIfOnBoundary);
return (id) [self textMarkerForVisiblePosition:startPosition];
}
- (id)doAXNextLineEndTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// to make sure we move off of a line end
VisiblePosition nextVisiblePos = visiblePos.next();
if (nextVisiblePos.isNull())
return nil;
VisiblePosition endPosition = endOfLine(nextVisiblePos);
// as long as the position hasn't reached the end of the doc, keep searching for a valid line end position
// There are cases like when the position is next to a floating object that'll return null for end of line. This code will avoid returning null.
while (endPosition.isNull() && nextVisiblePos.isNotNull()) {
nextVisiblePos = nextVisiblePos.next();
endPosition = endOfLine(nextVisiblePos);
}
return (id) [self textMarkerForVisiblePosition: endPosition];
}
- (id)doAXPreviousLineStartTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make sure we move off of a line start
VisiblePosition prevVisiblePos = visiblePos.previous();
if (prevVisiblePos.isNull())
return nil;
VisiblePosition startPosition = startOfLine(prevVisiblePos);
// as long as the position hasn't reached the beginning of the doc, keep searching for a valid line start position
// There are cases like when the position is next to a floating object that'll return null for start of line. This code will avoid returning null.
if (startPosition.isNull()) {
while (startPosition.isNull() && prevVisiblePos.isNotNull()) {
prevVisiblePos = prevVisiblePos.previous();
startPosition = startOfLine(prevVisiblePos);
}
} else
startPosition = updateAXLineStartForVisiblePosition(startPosition);
return (id) [self textMarkerForVisiblePosition: startPosition];
}
- (id)doAXNextSentenceEndTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
// NOTE: BUG FO 2 IMPLEMENT (currently returns incorrect answer)
// Related? <rdar://problem/3927736> Text selection broken in 8A336
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make sure we move off of a sentence end
VisiblePosition nextVisiblePos = visiblePos.next();
if (nextVisiblePos.isNull())
return nil;
// an empty line is considered a sentence. If it's skipped, then the sentence parser will not
// see this empty line. Instead, return the end position of the empty line.
VisiblePosition endPosition;
String lineString = plainText(makeRange(startOfLine(visiblePos), endOfLine(visiblePos)).get());
if (lineString.isEmpty())
endPosition = nextVisiblePos;
else
endPosition = endOfSentence(nextVisiblePos);
return (id) [self textMarkerForVisiblePosition: endPosition];
}
- (id)doAXPreviousSentenceStartTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
// NOTE: BUG FO 2 IMPLEMENT (currently returns incorrect answer)
// Related? <rdar://problem/3927736> Text selection broken in 8A336
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make sure we move off of a sentence start
VisiblePosition previousVisiblePos = visiblePos.previous();
if (previousVisiblePos.isNull())
return nil;
// treat empty line as a separate sentence.
VisiblePosition startPosition;
String lineString = plainText(makeRange(startOfLine(previousVisiblePos), endOfLine(previousVisiblePos)).get());
if (lineString.isEmpty())
startPosition = previousVisiblePos;
else
startPosition = startOfSentence(previousVisiblePos);
return (id) [self textMarkerForVisiblePosition: startPosition];
}
- (id)doAXNextParagraphEndTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make sure we move off of a paragraph end
visiblePos = visiblePos.next();
if (visiblePos.isNull())
return nil;
VisiblePosition endPosition = endOfParagraph(visiblePos);
return (id) [self textMarkerForVisiblePosition: endPosition];
}
- (id)doAXPreviousParagraphStartTextMarkerForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
// make sure we move off of a paragraph start
visiblePos = visiblePos.previous();
if (visiblePos.isNull())
return nil;
VisiblePosition startPosition = startOfParagraph(visiblePos);
return (id) [self textMarkerForVisiblePosition: startPosition];
}
static VisiblePosition startOfStyleRange (const VisiblePosition visiblePos)
{
RenderObject* renderer = visiblePos.deepEquivalent().node()->renderer();
RenderObject* startRenderer = renderer;
RenderStyle* style = renderer->style();
// traverse backward by renderer to look for style change
for (RenderObject* r = renderer->previousInPreOrder(); r; r = r->previousInPreOrder()) {
// skip non-leaf nodes
if (r->firstChild())
continue;
// stop at style change
if (r->style() != style)
break;
// remember match
startRenderer = r;
}
return VisiblePosition(startRenderer->node(), 0, VP_DEFAULT_AFFINITY);
}
static VisiblePosition endOfStyleRange (const VisiblePosition visiblePos)
{
RenderObject* renderer = visiblePos.deepEquivalent().node()->renderer();
RenderObject* endRenderer = renderer;
RenderStyle* style = renderer->style();
// traverse forward by renderer to look for style change
for (RenderObject* r = renderer->nextInPreOrder(); r; r = r->nextInPreOrder()) {
// skip non-leaf nodes
if (r->firstChild())
continue;
// stop at style change
if (r->style() != style)
break;
// remember match
endRenderer = r;
}
return VisiblePosition(endRenderer->node(), maxDeepOffset(endRenderer->node()), VP_DEFAULT_AFFINITY);
}
- (id)doAXStyleTextMarkerRangeForTextMarker: (WebCoreTextMarker*) textMarker
{
VisiblePosition visiblePos = [self visiblePositionForTextMarker:textMarker];
if (visiblePos.isNull())
return nil;
VisiblePosition startPosition = startOfStyleRange(visiblePos);
VisiblePosition endPosition = endOfStyleRange(visiblePos);
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
- (id)doAXLengthForTextMarkerRange: (WebCoreTextMarkerRange*) textMarkerRange
{
// NOTE: BUG Multi-byte support
CFStringRef string = (CFStringRef) [self doAXStringForTextMarkerRange: textMarkerRange];
if (!string)
return nil;
return [NSNumber numberWithInt:CFStringGetLength(string)];
}
// NOTE: Consider providing this utility method as AX API
- (WebCoreTextMarker*)textMarkerForIndex: (NSNumber*) index lastIndexOK: (BOOL)lastIndexOK
{
ASSERT(m_renderer->isTextField() || m_renderer->isTextArea());
RenderTextControl* textControl = static_cast<RenderTextControl*>(m_renderer);
unsigned int indexValue = [index unsignedIntValue];
// lastIndexOK specifies whether the position after the last character is acceptable
if (indexValue >= textControl->text().length()) {
if (!lastIndexOK || indexValue > textControl->text().length())
return nil;
}
VisiblePosition position = textControl->visiblePositionForIndex(indexValue);
position.setAffinity(DOWNSTREAM);
return [self textMarkerForVisiblePosition: position];
}
// NOTE: Consider providing this utility method as AX API
- (NSNumber*)indexForTextMarker: (WebCoreTextMarker*) marker
{
ASSERT(m_renderer->isTextField() || m_renderer->isTextArea());
RenderTextControl* textControl = static_cast<RenderTextControl*>(m_renderer);
VisiblePosition position = [self visiblePositionForTextMarker: marker];
Node* node = position.deepEquivalent().node();
if (!node)
return nil;
for (RenderObject* renderer = node->renderer(); renderer && renderer->element(); renderer = renderer->parent()) {
if (renderer == textControl)
return [NSNumber numberWithInt: textControl->indexForVisiblePosition(position)];
}
return nil;
}
// NOTE: Consider providing this utility method as AX API
- (WebCoreTextMarkerRange*)textMarkerRangeForRange: (NSRange) range
{
ASSERT(m_renderer->isTextField() || m_renderer->isTextArea());
RenderTextControl* textControl = static_cast<RenderTextControl*>(m_renderer);
if (range.location + range.length > textControl->text().length())
return nil;
VisiblePosition startPosition = textControl->visiblePositionForIndex(range.location);
startPosition.setAffinity(DOWNSTREAM);
VisiblePosition endPosition = textControl->visiblePositionForIndex(range.location + range.length);
return [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
// NOTE: Consider providing this utility method as AX API
- (NSValue*)rangeForTextMarkerRange: (WebCoreTextMarkerRange*) textMarkerRange
{
WebCoreTextMarker* textMarker1 = [[WebCoreViewFactory sharedFactory] startOfTextMarkerRange:textMarkerRange];
WebCoreTextMarker* textMarker2 = [[WebCoreViewFactory sharedFactory] endOfTextMarkerRange:textMarkerRange];
NSNumber* index1 = [self indexForTextMarker: textMarker1];
NSNumber* index2 = [self indexForTextMarker: textMarker2];
if (!index1 || !index2 || [index1 unsignedIntValue] > [index2 unsignedIntValue])
return nil;
return [NSValue valueWithRange: NSMakeRange([index1 unsignedIntValue], [index2 unsignedIntValue] - [index1 unsignedIntValue])];
}
// Given an indexed character, the line number of the text associated with this accessibility
// object that contains the character.
- (id)doAXLineForIndex: (NSNumber*) index
{
return [self doAXLineForTextMarker: [self textMarkerForIndex: index lastIndexOK: NO]];
}
// Given a line number, the range of characters of the text associated with this accessibility
// object that contains the line number.
- (id)doAXRangeForLine: (NSNumber*) lineNumber
{
ASSERT(m_renderer->isTextField() || m_renderer->isTextArea());
RenderTextControl* textControl = static_cast<RenderTextControl*>(m_renderer);
// iterate to the specified line
VisiblePosition visiblePos = textControl->visiblePositionForIndex(0);
VisiblePosition savedVisiblePos;
for (unsigned lineCount = [lineNumber unsignedIntValue]; lineCount != 0; lineCount -= 1) {
savedVisiblePos = visiblePos;
visiblePos = nextLinePosition(visiblePos, 0);
if (visiblePos.isNull() || visiblePos == savedVisiblePos)
return nil;
}
// make a caret selection for the marker position, then extend it to the line
// NOTE: ignores results of selectionController.modify because it returns false when
// starting at an empty line. The resulting selection in that case
// will be a caret at visiblePos.
SelectionController selectionController;
selectionController.setSelection(Selection(visiblePos));
selectionController.modify(SelectionController::EXTEND, SelectionController::LEFT, LineBoundary);
selectionController.modify(SelectionController::EXTEND, SelectionController::RIGHT, LineBoundary);
// calculate the indices for the selection start and end
VisiblePosition startPosition = selectionController.selection().visibleStart();
VisiblePosition endPosition = selectionController.selection().visibleEnd();
int index1 = textControl->indexForVisiblePosition(startPosition);
int index2 = textControl->indexForVisiblePosition(endPosition);
// add one to the end index for a line break not caused by soft line wrap (to match AppKit)
if (endPosition.affinity() == DOWNSTREAM && endPosition.next().isNotNull())
index2 += 1;
// return nil rather than an zero-length range (to match AppKit)
if (index1 == index2)
return nil;
return [NSValue valueWithRange: NSMakeRange(index1, index2 - index1)];
}
// A substring of the text associated with this accessibility object that is
// specified by the given character range.
- (id)doAXStringForRange: (NSRange) range
{
if ([self isPasswordField])
return nil;
if (range.length == 0)
return @"";
ASSERT(m_renderer->isTextField() || m_renderer->isTextArea());
RenderTextControl* textControl = static_cast<RenderTextControl*>(m_renderer);
String text = textControl->text();
if (range.location + range.length > text.length())
return nil;
return text.substring(range.location, range.length);
}
// The composed character range in the text associated with this accessibility object that
// is specified by the given screen coordinates. This parameterized attribute returns the
// complete range of characters (including surrogate pairs of multi-byte glyphs) at the given
// screen coordinates.
// NOTE: This varies from AppKit when the point is below the last line. AppKit returns an
// an error in that case. We return textControl->text().length(), 1. Does this matter?
- (id)doAXRangeForPosition: (NSPoint) point
{
NSNumber* index = [self indexForTextMarker: [self doAXTextMarkerForPosition: point]];
if (!index)
return nil;
return [NSValue valueWithRange: NSMakeRange([index unsignedIntValue], 1)];
}
// The composed character range in the text associated with this accessibility object that
// is specified by the given index value. This parameterized attribute returns the complete
// range of characters (including surrogate pairs of multi-byte glyphs) at the given index.
- (id)doAXRangeForIndex: (NSNumber*) number
{
ASSERT(m_renderer->isTextField() || m_renderer->isTextArea());
RenderTextControl* textControl = static_cast<RenderTextControl*>(m_renderer);
String text = textControl->text();
if (!text.length() || [number unsignedIntValue] > text.length() - 1)
return nil;
return [NSValue valueWithRange: NSMakeRange([number unsignedIntValue], 1)];
}
// The bounding rectangle of the text associated with this accessibility object that is
// specified by the given range. This is the bounding rectangle a sighted user would see
// on the display screen, in pixels.
- (id)doAXBoundsForRange: (NSRange) range
{
return [self doAXBoundsForTextMarkerRange: [self textMarkerRangeForRange:range]];
}
// The CFAttributedStringType representation of the text associated with this accessibility
// object that is specified by the given range.
- (id)doAXAttributedStringForRange: (NSRange) range
{
return [self doAXAttributedStringForTextMarkerRange: [self textMarkerRangeForRange:range]];
}
// The RTF representation of the text associated with this accessibility object that is
// specified by the given range.
- (id)doAXRTFForRange: (NSRange) range
{
NSAttributedString* attrString = [self doAXAttributedStringForRange: range];
return [attrString RTFFromRange: NSMakeRange(0, [attrString length]) documentAttributes: nil];
}
// Given a character index, the range of text associated with this accessibility object
// over which the style in effect at that character index applies.
- (id)doAXStyleRangeForIndex: (NSNumber*) index
{
WebCoreTextMarkerRange* textMarkerRange = [self doAXStyleTextMarkerRangeForTextMarker: [self textMarkerForIndex: index lastIndexOK: NO]];
return [self rangeForTextMarkerRange: textMarkerRange];
}
- (id)accessibilityAttributeValue:(NSString*)attribute forParameter:(id)parameter
{
WebCoreTextMarker* textMarker = nil;
WebCoreTextMarkerRange* textMarkerRange = nil;
NSNumber* number = nil;
NSArray* array = nil;
WebCoreAXObject* uiElement = nil;
NSPoint point = NSZeroPoint;
bool pointSet = false;
NSRange range = {0, 0};
bool rangeSet = false;
// basic parameter validation
if (!m_renderer || !attribute || !parameter)
return nil;
// common parameter type check/casting. Nil checks in handlers catch wrong type case.
// NOTE: This assumes nil is not a valid parameter, because it is indistinguishable from
// a parameter of the wrong type.
if ([[WebCoreViewFactory sharedFactory] objectIsTextMarker:parameter])
textMarker = (WebCoreTextMarker*) parameter;
else if ([[WebCoreViewFactory sharedFactory] objectIsTextMarkerRange:parameter])
textMarkerRange = (WebCoreTextMarkerRange*) parameter;
else if ([parameter isKindOfClass:[WebCoreAXObject self]])
uiElement = (WebCoreAXObject*) parameter;
else if ([parameter isKindOfClass:[NSNumber self]])
number = parameter;
else if ([parameter isKindOfClass:[NSArray self]])
array = parameter;
else if ([parameter isKindOfClass:[NSValue self]] && strcmp([(NSValue*)parameter objCType], @encode(NSPoint)) == 0) {
pointSet = true;
point = [(NSValue*)parameter pointValue];
} else if ([parameter isKindOfClass:[NSValue self]] && strcmp([(NSValue*)parameter objCType], @encode(NSRange)) == 0) {
rangeSet = true;
range = [(NSValue*)parameter rangeValue];
} else {
// got a parameter of a type we never use
// NOTE: No ASSERT_NOT_REACHED because this can happen accidentally
// while using accesstool (e.g.), forcing you to start over
return nil;
}
// dispatch
if ([attribute isEqualToString: @"AXUIElementForTextMarker"])
return [self doAXUIElementForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXTextMarkerRangeForUIElement"])
return [self doAXTextMarkerRangeForUIElement: uiElement];
if ([attribute isEqualToString: @"AXLineForTextMarker"])
return [self doAXLineForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXTextMarkerRangeForLine"])
return [self doAXTextMarkerRangeForLine: number];
if ([attribute isEqualToString: @"AXStringForTextMarkerRange"])
return [self doAXStringForTextMarkerRange: textMarkerRange];
if ([attribute isEqualToString: @"AXTextMarkerForPosition"])
return pointSet ? [self doAXTextMarkerForPosition: point] : nil;
if ([attribute isEqualToString: @"AXBoundsForTextMarkerRange"])
return [self doAXBoundsForTextMarkerRange: textMarkerRange];
if ([attribute isEqualToString: @"AXAttributedStringForTextMarkerRange"])
return [self doAXAttributedStringForTextMarkerRange: textMarkerRange];
if ([attribute isEqualToString: @"AXTextMarkerRangeForUnorderedTextMarkers"])
return [self doAXTextMarkerRangeForUnorderedTextMarkers: array];
if ([attribute isEqualToString: @"AXNextTextMarkerForTextMarker"])
return [self doAXNextTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXPreviousTextMarkerForTextMarker"])
return [self doAXPreviousTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXLeftWordTextMarkerRangeForTextMarker"])
return [self doAXLeftWordTextMarkerRangeForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXRightWordTextMarkerRangeForTextMarker"])
return [self doAXRightWordTextMarkerRangeForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXLeftLineTextMarkerRangeForTextMarker"])
return [self doAXLeftLineTextMarkerRangeForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXRightLineTextMarkerRangeForTextMarker"])
return [self doAXRightLineTextMarkerRangeForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXSentenceTextMarkerRangeForTextMarker"])
return [self doAXSentenceTextMarkerRangeForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXParagraphTextMarkerRangeForTextMarker"])
return [self doAXParagraphTextMarkerRangeForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXNextWordEndTextMarkerForTextMarker"])
return [self doAXNextWordEndTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXPreviousWordStartTextMarkerForTextMarker"])
return [self doAXPreviousWordStartTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXNextLineEndTextMarkerForTextMarker"])
return [self doAXNextLineEndTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXPreviousLineStartTextMarkerForTextMarker"])
return [self doAXPreviousLineStartTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXNextSentenceEndTextMarkerForTextMarker"])
return [self doAXNextSentenceEndTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXPreviousSentenceStartTextMarkerForTextMarker"])
return [self doAXPreviousSentenceStartTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXNextParagraphEndTextMarkerForTextMarker"])
return [self doAXNextParagraphEndTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXPreviousParagraphStartTextMarkerForTextMarker"])
return [self doAXPreviousParagraphStartTextMarkerForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXStyleTextMarkerRangeForTextMarker"])
return [self doAXStyleTextMarkerRangeForTextMarker: textMarker];
if ([attribute isEqualToString: @"AXLengthForTextMarkerRange"])
return [self doAXLengthForTextMarkerRange: textMarkerRange];
if ([self isTextControl]) {
if ([attribute isEqualToString: (NSString*)kAXLineForIndexParameterizedAttribute])
return [self doAXLineForIndex: number];
if ([attribute isEqualToString: (NSString*)kAXRangeForLineParameterizedAttribute])
return [self doAXRangeForLine: number];
if ([attribute isEqualToString: (NSString*)kAXStringForRangeParameterizedAttribute])
return rangeSet ? [self doAXStringForRange: range] : nil;
if ([attribute isEqualToString: (NSString*)kAXRangeForPositionParameterizedAttribute])
return pointSet ? [self doAXRangeForPosition: point] : nil;
if ([attribute isEqualToString: (NSString*)kAXRangeForIndexParameterizedAttribute])
return [self doAXRangeForIndex: number];
if ([attribute isEqualToString: (NSString*)kAXBoundsForRangeParameterizedAttribute])
return rangeSet ? [self doAXBoundsForRange: range] : nil;
if ([attribute isEqualToString: (NSString*)kAXRTFForRangeParameterizedAttribute])
return rangeSet ? [self doAXRTFForRange: range] : nil;
if ([attribute isEqualToString: (NSString*)kAXAttributedStringForRangeParameterizedAttribute])
return rangeSet ? [self doAXAttributedStringForRange: range] : nil;
if ([attribute isEqualToString: (NSString*)kAXStyleRangeForIndexParameterizedAttribute])
return [self doAXStyleRangeForIndex: number];
}
return nil;
}
- (id)accessibilityHitTest:(NSPoint)point
{
if (!m_renderer)
return NSAccessibilityUnignoredAncestor(self);
HitTestRequest request(true, true);
HitTestResult result = HitTestResult(IntPoint(point));
m_renderer->layer()->hitTest(request, result);
if (!result.innerNode())
return NSAccessibilityUnignoredAncestor(self);
Node* node = result.innerNode()->shadowAncestorNode();
RenderObject* obj = node->renderer();
if (!obj)
return NSAccessibilityUnignoredAncestor(self);
return NSAccessibilityUnignoredAncestor(obj->document()->axObjectCache()->get(obj));
}
- (RenderObject*)rendererForView:(NSView*)view
{
// check for WebKit NSView that lets us find its bridge
WebCoreFrameBridge* bridge = nil;
if ([view conformsToProtocol:@protocol(WebCoreBridgeHolder)]) {
NSView<WebCoreBridgeHolder>* bridgeHolder = (NSView<WebCoreBridgeHolder>*)view;
bridge = [bridgeHolder webCoreBridge];
}
Frame* frame = [bridge _frame];
if (!frame)
return nil;