blob: 3f7c4a6c5127140700009fae921bac955311a24c [file] [log] [blame]
/*
* Copyright (C) 2004, 2005 Apple Computer, 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.
*/
#include "config.h"
#import "KWQAccObject.h"
// need this until accesstool supports arrays of markers
#define MARKERARRAY_SELF_TEST 0
#include <mach-o/dyld.h>
#import "KWQAccObjectCache.h"
#import <kxmlcore/Assertions.h>
#import "KWQFoundationExtras.h"
#import "Widget.h"
#import "WebCoreFrameBridge.h"
#import "WebCoreFrameView.h"
#import "WebCoreViewFactory.h"
#import "DocumentImpl.h"
#import "HTMLInputElementImpl.h"
#import "HTMLCollectionImpl.h"
#import "html_inlineimpl.h"
#import "html_imageimpl.h"
#import "PlatformString.h"
#import "dom2_eventsimpl.h"
#import "dom2_range.h"
#import "FrameView.h"
#import "MacFrame.h"
#import "render_canvas.h"
#import "render_image.h"
#import "render_list.h"
#import "render_object.h"
#import "render_style.h"
#import "RenderText.h"
#import "render_theme.h"
#import "SelectionController.h"
#import "kjs_html.h"
#import "text_granularity.h"
#import "VisiblePosition.h"
#import "visible_text.h"
#import "visible_units.h"
#import "DOMInternal.h"
#import "EventNames.h"
#import "htmlnames.h"
using namespace WebCore;
using namespace EventNames;
using namespace HTMLNames;
// FIXME: This will eventually need to really localize.
#define UI_STRING(string, comment) ((NSString *)[NSString stringWithUTF8String:(string)])
@implementation KWQAccObject
-(id)initWithRenderer:(RenderObject*)renderer
{
[super init];
m_renderer = renderer;
return self;
}
-(BOOL)detached
{
return !m_renderer;
}
// accessibilityShouldUseUniqueId is an AppKit method we override so that the canvas
// 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.
- (BOOL)accessibilityShouldUseUniqueId {
return m_renderer && m_renderer->isCanvas();
}
-(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 removeAccObjectID];
m_renderer = 0;
[self clearChildren];
}
- (void)dealloc
{
[self detach];
[super dealloc];
}
-(id)data
{
return m_data;
}
-(void)setData:(id)data
{
if (!m_renderer)
return;
[data retain];
[m_data release];
m_data = data;
}
-(HTMLAnchorElementImpl*)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()->getAccObjectCache()->accObject(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 HTMLAnchorElementImpl
NodeImpl* elt = currRenderer->element();
for ( ; elt; elt = elt->parentNode()) {
if (elt->isLink() && elt->renderer() && !elt->renderer()->isImage())
return static_cast<HTMLAnchorElementImpl*>(elt);
}
return 0;
}
-(BOOL)isImageButton
{
return m_renderer->isImage() && static_cast<RenderImage*>(m_renderer)->isImageButton();
}
-(ElementImpl *)mouseButtonListener
{
// FIXME: Do the continuation search like anchorElement does
for (NodeImpl *elt = m_renderer->element(); elt; elt = elt->parentNode()) {
if (elt->getHTMLEventListener(clickEvent) || elt->getHTMLEventListener(mousedownEvent) || elt->getHTMLEventListener(mouseupEvent))
return static_cast<ElementImpl*>(elt);
}
return NULL;
}
-(ElementImpl *)actionElement
{
if (m_renderer->element() && m_renderer->element()->hasTagName(inputTag)) {
HTMLInputElementImpl* input = static_cast<HTMLInputElementImpl*>(m_renderer->element());
if (!input->disabled() && (input->inputType() == HTMLInputElementImpl::CHECKBOX ||
input->inputType() == HTMLInputElementImpl::RADIO ||
input->isTextButton()))
return input;
}
if ([self isImageButton])
return static_cast<ElementImpl*>(m_renderer->element());
ElementImpl * elt = [self anchorElement];
if (!elt)
elt = [self mouseButtonListener];
return elt;
}
-(KWQAccObject*)firstChild
{
if (!m_renderer || !m_renderer->firstChild())
return nil;
return m_renderer->document()->getAccObjectCache()->accObject(m_renderer->firstChild());
}
-(KWQAccObject*)lastChild
{
if (!m_renderer || !m_renderer->lastChild())
return nil;
return m_renderer->document()->getAccObjectCache()->accObject(m_renderer->lastChild());
}
-(KWQAccObject*)previousSibling
{
if (!m_renderer || !m_renderer->previousSibling())
return nil;
return m_renderer->document()->getAccObjectCache()->accObject(m_renderer->previousSibling());
}
-(KWQAccObject*)nextSibling
{
if (!m_renderer || !m_renderer->nextSibling())
return nil;
return m_renderer->document()->getAccObjectCache()->accObject(m_renderer->nextSibling());
}
-(KWQAccObject*)parentObject
{
if (m_areaElement)
return m_renderer->document()->getAccObjectCache()->accObject(m_renderer);
if (!m_renderer || !m_renderer->parent())
return nil;
return m_renderer->document()->getAccObjectCache()->accObject(m_renderer->parent());
}
-(KWQAccObject*)parentObjectUnignored
{
KWQAccObject* obj = [self parentObject];
if ([obj accessibilityIsIgnored])
return [obj parentObjectUnignored];
else
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 (KWQAccObject* 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) {
HTMLMapElementImpl* map = static_cast<RenderImage*>(m_renderer)->imageMap();
if (map) {
for (NodeImpl *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()) {
KWQAccObject* obj = [[[KWQAccObject alloc] initWithRenderer: m_renderer] autorelease];
obj->m_areaElement = static_cast<HTMLAreaElementImpl*>(current);
[array addObject: obj];
}
}
}
}
}
-(BOOL)isAttachment
{
// widgets are the replaced elements that we represent to AX as attachments
BOOL result = 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;
}
-(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 (m_renderer->isCanvas())
return @"AXWebArea";
if (m_renderer->element() && m_renderer->element()->hasTagName(inputTag)) {
HTMLInputElementImpl* input = static_cast<HTMLInputElementImpl*>(m_renderer->element());
if (input->inputType() == HTMLInputElementImpl::CHECKBOX)
return NSAccessibilityCheckBoxRole;
if (input->inputType() == HTMLInputElementImpl::RADIO)
return NSAccessibilityRadioButtonRole;
if (input->isTextButton())
return NSAccessibilityButtonRole;
}
if (m_renderer->isBlockFlow())
return NSAccessibilityGroupRole;
if ([self isAttachment])
return [[self attachmentView] accessibilityAttributeValue:NSAccessibilityRoleAttribute];
return NSAccessibilityUnknownRole;
}
-(NSString*)subrole
{
if ([self isAttachment])
return [[self 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 3517227: These need to be localized (UI_STRING here is a dummy macro)
// 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, nil);
if ([role isEqualToString:NSAccessibilityStaticTextRole])
return NSAccessibilityRoleDescription(NSAccessibilityStaticTextRole, nil);
if ([role isEqualToString:NSAccessibilityImageRole])
return NSAccessibilityRoleDescription(NSAccessibilityImageRole, nil);
if ([role isEqualToString:NSAccessibilityGroupRole])
return NSAccessibilityRoleDescription(NSAccessibilityGroupRole, nil);
if ([role isEqualToString:NSAccessibilityCheckBoxRole])
return NSAccessibilityRoleDescription(NSAccessibilityCheckBoxRole, nil);
if ([role isEqualToString:NSAccessibilityRadioButtonRole])
return NSAccessibilityRoleDescription(NSAccessibilityRadioButtonRole, nil);
if ([role isEqualToString:@"AXWebArea"])
return UI_STRING("web area", "accessibility role description for web area");
if ([role isEqualToString:@"AXLink"])
return UI_STRING("link", "accessibility role description for link");
if ([role isEqualToString:@"AXListMarker"])
return UI_STRING("list marker", "accessibility role description for list marker");
if ([role isEqualToString:@"AXImageMap"])
return UI_STRING("image map", "accessibility role description for image map");
return NSAccessibilityRoleDescription(NSAccessibilityUnknownRole, nil);
}
-(NSString*)helpText
{
if (!m_renderer)
return nil;
if (m_areaElement) {
QString summary = static_cast<ElementImpl*>(m_areaElement)->getAttribute(summaryAttr).qstring();
if (!summary.isEmpty())
return summary.getNSString();
QString title = static_cast<ElementImpl*>(m_areaElement)->getAttribute(titleAttr).qstring();
if (!title.isEmpty())
return title.getNSString();
}
for (RenderObject* curr = m_renderer; curr; curr = curr->parent()) {
if (curr->element() && curr->element()->isHTMLElement()) {
QString summary = static_cast<ElementImpl*>(curr->element())->getAttribute(summaryAttr).qstring();
if (!summary.isEmpty())
return summary.getNSString();
QString title = static_cast<ElementImpl*>(curr->element())->getAttribute(titleAttr).qstring();
if (!title.isEmpty())
return title.getNSString();
}
}
return nil;
}
-(NSString*)textUnderElement
{
if (!m_renderer)
return nil;
NodeImpl* e = m_renderer->element();
DocumentImpl* d = m_renderer->document();
if (e && d) {
Frame* p = d->frame();
if (p) {
// catch stale KWQAccObject (see <rdar://problem/3960196>)
if (p->document() != d)
return nil;
return plainText(rangeOfContents(e).get()).getNSString();
}
}
return nil;
}
-(id)value
{
if (!m_renderer || m_areaElement)
return nil;
if (m_renderer->isText())
return [self textUnderElement];
if (m_renderer->isListMarker())
return static_cast<RenderListMarker*>(m_renderer)->text().getNSString();
if (m_renderer->isCanvas()) {
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 (LONG_MAX, LONG_MAX);
if (startVisiblePosition.isNull() || endVisiblePosition.isNull())
return nil;
QString qString = plainText(makeRange(startVisiblePosition, endVisiblePosition).get());
// transform it to a CFString and return that
return (id)qString.getCFString();
}
if ([self isAttachment])
return [[self attachmentView] accessibilityAttributeValue:NSAccessibilityValueAttribute];
if (m_renderer->element() && m_renderer->element()->hasTagName(inputTag)) {
HTMLInputElementImpl* input = static_cast<HTMLInputElementImpl*>(m_renderer->element());
if (input->inputType() == HTMLInputElementImpl::CHECKBOX ||
input->inputType() == HTMLInputElementImpl::RADIO)
// Checkboxes return their state as an integer. 0 for off, 1 for on.
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;
}
-(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)) {
HTMLInputElementImpl* input = static_cast<HTMLInputElementImpl*>(m_renderer->element());
if (input->isTextButton())
return input->value();
}
if (m_renderer->element()->isLink())
return [self textUnderElement];
if ([self isAttachment])
return [[self 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()) {
QString alt = static_cast<ElementImpl*>(m_renderer->element())->getAttribute(altAttr).qstring();
return !alt.isEmpty() ? alt.getNSString() : nil;
}
} else if ([self isAttachment])
return [[self attachmentView] accessibilityAttributeValue:NSAccessibilityTitleAttribute];
return nil;
}
static IntRect boundingBoxRect(RenderObject* obj)
{
IntRect rect;
if (obj) {
if (obj->isInlineContinuation())
obj = obj->element()->renderer();
QValueList<IntRect> rects;
int x = 0, y = 0;
obj->absolutePosition(x, y);
obj->absoluteRects(rects, x, y);
for (QValueList<IntRect>::ConstIterator it = rects.begin(); it != rects.end(); ++it) {
IntRect r = *it;
if (!r.isEmpty()) {
if (obj->style()->hasAppearance())
theme()->adjustRepaintRect(obj, r);
if (rect.isEmpty())
rect = r;
else
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->canvas() && m_renderer->canvas()->view()) {
NSView* view = m_renderer->canvas()->view()->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())];
}
-(BOOL)accessibilityIsIgnored
{
if (!m_renderer || m_renderer->style()->visibility() != khtml::VISIBLE)
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 must be examined.
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];
return (!m_renderer->isListMarker() && !m_renderer->isCanvas() &&
!m_renderer->isImage() &&
!(m_renderer->element() && m_renderer->element()->isHTMLElement() &&
m_renderer->element()->hasTagName(buttonTag)));
}
- (NSArray *)accessibilityAttributeNames
{
static NSArray* attributes = nil;
static NSArray* anchorAttrs = nil;
static NSArray* webAreaAttrs = nil;
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) {
anchorAttrs = [[NSArray alloc] initWithObjects: NSAccessibilityRoleAttribute,
NSAccessibilityRoleDescriptionAttribute,
NSAccessibilityChildrenAttribute,
NSAccessibilityHelpAttribute,
NSAccessibilityParentAttribute,
NSAccessibilityPositionAttribute,
NSAccessibilitySizeAttribute,
NSAccessibilityTitleAttribute,
NSAccessibilityValueAttribute,
NSAccessibilityFocusedAttribute,
NSAccessibilityEnabledAttribute,
NSAccessibilityWindowAttribute,
@"AXURL",
@"AXSelectedTextMarkerRange",
@"AXStartTextMarker",
@"AXEndTextMarker",
@"AXVisited",
nil];
}
if (webAreaAttrs == nil) {
webAreaAttrs = [[NSArray alloc] initWithObjects: NSAccessibilityRoleAttribute,
NSAccessibilityRoleDescriptionAttribute,
NSAccessibilityChildrenAttribute,
NSAccessibilityHelpAttribute,
NSAccessibilityParentAttribute,
NSAccessibilityPositionAttribute,
NSAccessibilitySizeAttribute,
NSAccessibilityTitleAttribute,
NSAccessibilityValueAttribute,
NSAccessibilityFocusedAttribute,
NSAccessibilityEnabledAttribute,
NSAccessibilityWindowAttribute,
@"AXLinkUIElements",
@"AXLoaded",
@"AXLayoutCount",
@"AXSelectedTextMarkerRange",
@"AXStartTextMarker",
@"AXEndTextMarker",
@"AXVisited",
nil];
}
if (m_renderer && m_renderer->isCanvas())
return webAreaAttrs;
if (m_areaElement || (m_renderer && !m_renderer->isImage() && m_renderer->element() && m_renderer->element()->isLink()))
return anchorAttrs;
return attributes;
}
- (NSArray*)accessibilityActionNames
{
static NSArray* actions = nil;
if ([self actionElement]) {
if (actions == nil)
actions = [[NSArray alloc] initWithObjects: NSAccessibilityPressAction, nil];
return actions;
}
return nil;
}
- (NSString *)accessibilityActionDescription:(NSString *)action
{
// we have no custom actions
return NSAccessibilityActionDescription(action);
}
- (void)accessibilityPerformAction:(NSString *)action
{
if ([action isEqualToString:NSAccessibilityPressAction]) {
ElementImpl *actionElement = [self actionElement];
if (!actionElement)
return;
DocumentImpl* d = actionElement->getDocument();
if (d) {
Frame* p = d->frame();
if (p) {
Mac(p)->prepareForUserAction();
}
}
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;
return m_renderer->document()->getAccObjectCache()->textMarkerForVisiblePosition(visiblePos);
}
- (VisiblePosition) visiblePositionForTextMarker: (WebCoreTextMarker *)textMarker
{
return m_renderer->document()->getAccObjectCache()->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;
WebCoreTextMarker *startTextMarker = [self textMarkerForVisiblePosition: VisiblePosition(m_renderer->element(), m_renderer->caretMinOffset(), khtml::VP_DEFAULT_AFFINITY)];
WebCoreTextMarker *endTextMarker = [self textMarkerForVisiblePosition: VisiblePosition(m_renderer->element(), m_renderer->caretMaxRenderedOffset(), khtml::VP_DEFAULT_AFFINITY)];
return [self textMarkerRangeFromMarkers: startTextMarker andEndMarker:endTextMarker];
}
- (DocumentImpl *)topDocument
{
return m_renderer->document()->topDocument();
}
- (RenderObject *)topRenderer
{
return m_renderer->document()->topDocument()->renderer();
}
- (FrameView *)topView
{
return m_renderer->document()->topDocument()->renderer()->canvas()->view();
}
- (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->isCanvas() && m_renderer->canvas() && m_renderer->canvas()->view())
return m_renderer->canvas()->view()->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 (m_renderer->isCanvas()) {
if ([attributeName isEqualToString: @"AXLinkUIElements"]) {
NSMutableArray *links = [NSMutableArray arrayWithCapacity: 32];
HTMLCollectionImpl *coll = new HTMLCollectionImpl(m_renderer->document(), HTMLCollectionImpl::DOC_LINKS);
coll->ref();
NodeImpl *curr = coll->firstItem();
while (curr) {
RenderObject *obj = curr->renderer();
if (obj) {
KWQAccObject *axobj = obj->document()->getAccObjectCache()->accObject(obj);
ASSERT([[axobj role] isEqualToString:@"AXLink"]);
if (![axobj accessibilityIsIgnored])
[links addObject: axobj];
}
curr = coll->nextItem();
}
coll->deref();
return links;
}
if ([attributeName isEqualToString: @"AXLoaded"])
return [NSNumber numberWithBool: (!m_renderer->document()->tokenizer())];
if ([attributeName isEqualToString: @"AXLayoutCount"])
return [NSNumber numberWithInt: (static_cast<RenderCanvas*>(m_renderer)->view()->layoutCount())];
}
if ([attributeName isEqualToString: @"AXURL"] &&
(m_areaElement || (!m_renderer->isImage() && m_renderer->element() && m_renderer->element()->isLink()))) {
HTMLAnchorElementImpl* anchor = [self anchorElement];
if (anchor) {
QString s = anchor->getAttribute(hrefAttr).qstring();
if (!s.isNull()) {
s = anchor->getDocument()->completeURL(s);
return s.getNSString();
}
}
}
if ([attributeName isEqualToString: @"AXVisited"]) {
return [NSNumber numberWithBool: m_renderer->style()->pseudoState() == khtml::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()->focusNode() == 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->canvas() && m_renderer->canvas()->view())
return [m_renderer->canvas()->view()->getView() window];
return nil;
}
if ([attributeName isEqualToString: @"AXSelectedTextMarkerRange"]) {
// get the selection from the document part
// NOTE: BUG support nested WebAreas, like in <http://webcourses.niu.edu/>
// (there is a web archive of this page attached to <rdar://problem/3888973>)
// Trouble is we need to know which document view to ask.
SelectionController sel = [self topView]->frame()->selection();
if (sel.isNone()) {
sel = m_renderer->document()->renderer()->canvas()->view()->frame()->selection();
if (sel.isNone())
return nil;
}
// return a marker range for the selection start to end
VisiblePosition startPosition = VisiblePosition(sel.start(), sel.affinity());
VisiblePosition endPosition = VisiblePosition(sel.end(), sel.affinity());
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
if ([attributeName isEqualToString: @"AXStartTextMarker"]) {
// FIXME: should use startOfDocument here
VisiblePosition startPos = [self topRenderer]->positionForCoordinates (0, 0);
return (id) [self textMarkerForVisiblePosition: startPos];
}
if ([attributeName isEqualToString: @"AXEndTextMarker"]) {
// FIXME: should use endOfDocument here
VisiblePosition endPos = [self topRenderer]->positionForCoordinates (LONG_MAX, LONG_MAX);
return (id) [self textMarkerForVisiblePosition: endPos];
}
return nil;
}
- (NSArray *)accessibilityParameterizedAttributeNames
{
static NSArray* paramAttributes = nil;
if (paramAttributes == nil) {
paramAttributes = [[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",
@"AXLengthForTextMarkerRange",
nil];
}
return paramAttributes;
}
- (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()->getAccObjectCache()->accObject(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 is not correct in non-editable elements when the position is on the
// first line, but not at the first offset, because previousLinePosition takes you to the
// first offset, the same as if you had started on the second line. In editable elements,
// previousLinePosition returns nil, so the count is accurate.
// NOTE: BUG This only takes us to the top of the rootEditableElement, not the top of the
// top document.
while (visiblePos.isNotNull() && visiblePos != savedVisiblePos) {
lineCount += 1;
savedVisiblePos = visiblePos;
visiblePos = previousLinePosition(visiblePos, 0);
}
return [NSNumber numberWithUnsignedInt:lineCount];
}
- (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 = [self topRenderer]->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 sel = SelectionController(visiblePos, visiblePos);
(void)sel.modify(SelectionController::EXTEND, SelectionController::RIGHT, khtml::LINE_BOUNDARY);
// return a marker range for the selection start to end
VisiblePosition startPosition = VisiblePosition(sel.start(), sel.affinity());
VisiblePosition endPosition = VisiblePosition(sel.end(), sel.affinity());
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
- (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;
// get the visible text in the range
QString qString = plainText(makeRange(startVisiblePosition, endVisiblePosition).get());
// transform it to a CFString and return that
return (id)qString.getCFString();
}
- (id)doAXTextMarkerForPosition: (NSPoint) point
{
// convert absolute point to view coordinates
FrameView *docView = [self topView];
NSView *view = docView->getView();
RenderObject *renderer = [self topRenderer];
NodeImpl *innerNode = NULL;
NSPoint ourpoint;
// locate the node containing the point
while (1) {
// ask the document layer to hitTest
NSPoint windowCoord = [[view window] convertScreenToBase: point];
ourpoint = [view convertPoint:windowCoord fromView:nil];
ourpoint.x += docView->contentsX();
ourpoint.y += docView->contentsY();
RenderObject::NodeInfo nodeInfo(true, true);
renderer->layer()->hitTest(nodeInfo, (int)ourpoint.x, (int)ourpoint.y);
innerNode = nodeInfo.innerNode();
if (!innerNode || !innerNode->renderer())
return nil;
// 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;
DocumentImpl *document = frame->document();
if (!document)
break;
renderer = document->renderer();
docView = static_cast<FrameView *>(widget);
view = docView->getDocumentView();
}
// get position within the node
VisiblePosition pos = innerNode->renderer()->positionForCoordinates ((int)ourpoint.x, (int)ourpoint.y);
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;
// use the SelectionController class to help calculate the corresponding rectangle
IntRect rect1 = SelectionController(startVisiblePosition, startVisiblePosition).caretRect();
IntRect rect2 = SelectionController(endVisiblePosition, endVisiblePosition).caretRect();
IntRect ourrect = rect1;
ourrect.unite(rect2);
// try to use the document view from the selection, so that nested WebAreas work,
// but fall back to the top level doc if we do not find it easily
FrameView *docView = NULL;
RenderObject * renderer = startVisiblePosition.deepEquivalent().node()->renderer();
if (renderer) {
DocumentImpl* doc = renderer->document();
if (doc)
docView = doc->view();
}
if (!docView)
docView = [self topView];
NSView * view = docView->getView();
// if the selection spans lines, the rectangle is to extend
// across the width of the view
if (rect1.bottom() != rect2.bottom()) {
ourrect.setX(static_cast<int>([view frame].origin.x));
ourrect.setWidth(static_cast<int>([view frame].size.width));
}
// convert our rectangle to screen coordinates
NSRect rect = ourrect;
rect = NSOffsetRect(rect, -docView->contentsX(), -docView->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];
float 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 != nil) {
CGColorRef existingColor = (CGColorRef) [attrString attribute:attribute atIndex:range.location effectiveRange:nil];
CGColorRef cgColor = CreateCGColorIfDifferent(color, existingColor);
if (cgColor != NULL) {
[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 != nil)
[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 != nil) {
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().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 == khtml::SUB)
AXAttributeStringSetNumber(attrString, NSAccessibilitySuperscriptTextAttribute, [NSNumber numberWithInt:(-1)], range);
else if (alignment == khtml::SUPER)
AXAttributeStringSetNumber(attrString, NSAccessibilitySuperscriptTextAttribute, [NSNumber numberWithInt:1], range);
else
[attrString removeAttribute:NSAccessibilitySuperscriptTextAttribute range:range];
// set shadow
if (style->textShadow() != nil)
AXAttributeStringSetNumber(attrString, NSAccessibilityShadowTextAttribute, [NSNumber numberWithBool:YES], range);
else
[attrString removeAttribute:NSAccessibilityShadowTextAttribute range:range];
// set underline and strikethrough
int decor = style->textDecorationsInEffect();
if ((decor & khtml::UNDERLINE) == 0) {
[attrString removeAttribute:NSAccessibilityUnderlineTextAttribute range:range];
[attrString removeAttribute:NSAccessibilityUnderlineColorTextAttribute range:range];
}
if ((decor & khtml::LINE_THROUGH) == 0) {
[attrString removeAttribute:NSAccessibilityStrikethroughTextAttribute range:range];
[attrString removeAttribute:NSAccessibilityStrikethroughColorTextAttribute range:range];
}
if ((decor & (khtml::UNDERLINE | khtml::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 & khtml::UNDERLINE) != 0) {
AXAttributeStringSetNumber(attrString, NSAccessibilityUnderlineTextAttribute, [NSNumber numberWithBool:YES], range);
AXAttributeStringSetColor(attrString, NSAccessibilityUnderlineColorTextAttribute, nsColor(underline), range);
}
if ((decor & khtml::LINE_THROUGH) != 0) {
AXAttributeStringSetNumber(attrString, NSAccessibilityStrikethroughTextAttribute, [NSNumber numberWithBool:YES], range);
AXAttributeStringSetColor(attrString, NSAccessibilityStrikethroughColorTextAttribute, nsColor(linethrough), range);
}
}
}
static void AXAttributeStringSetElement(NSMutableAttributedString *attrString, NSString *attribute, id element, NSRange range)
{
if (element != nil) {
// make a serialiazable AX object
AXUIElementRef axElement = [[WebCoreViewFactory sharedFactory] AXUIElementForElement:element];
if (axElement != NULL) {
[attrString addAttribute:attribute value:(id)axElement range:range];
CFRelease(axElement);
}
} else {
[attrString removeAttribute:attribute range:range];
}
}
static KWQAccObject *AXLinkElementForNode (NodeImpl *node)
{
RenderObject *obj = node->renderer();
if (!obj)
return nil;
KWQAccObject *axObj = obj->document()->getAccObjectCache()->accObject(obj);
HTMLAnchorElementImpl* anchor = [axObj anchorElement];
if (!anchor || !anchor->renderer())
return nil;
return anchor->renderer()->document()->getAccObjectCache()->accObject(anchor->renderer());
}
static void AXAttributedStringAppendText (NSMutableAttributedString *attrString, NodeImpl *nodeImpl, const QChar *chars, int length)
{
// skip invisible text
if (!nodeImpl->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:(const UniChar*)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, nodeImpl->renderer(), attrStringRange);
AXAttributeStringSetElement(attrString, NSAccessibilityLinkTextAttribute, AXLinkElementForNode(nodeImpl), attrStringRange);
}
static void AXAttributedStringAppendReplaced (NSMutableAttributedString *attrString, NodeImpl *replacedNode)
{
static const UniChar attachmentChar = NSAttachmentCharacter;
// we should always be given a rendered node, but be safe
if (!replacedNode || !replacedNode->renderer()) {
ASSERT_NOT_REACHED();
return;
}
// we should always be given a replaced node, but be safe
// replaced nodes are either attachments (widgets) or images
if (!replacedNode->renderer()->isReplaced()) {
ASSERT_NOT_REACHED();
return;
}
// create an AX object, but skip it if it is not supposed to be seen
KWQAccObject *obj = replacedNode->renderer()->document()->getAccObjectCache()->accObject(replacedNode->renderer());
if ([obj accessibilityIsIgnored])
return;
// easier to calculate the range before appending the string
NSRange attrStringRange = NSMakeRange([attrString length], 1);
// append the placeholder string
[[attrString mutableString] appendString:[NSString stringWithCharacters:&attachmentChar length:1]];
// remove all inherited attributes
[attrString setAttributes:nil range:attrStringRange];
// add the attachment attribute
AXAttributeStringSetElement(attrString, NSAccessibilityAttachmentTextAttribute, obj, 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 for this range
int exception = 0;
NodeImpl *nodeImpl = it.range()->startContainer(exception);
ASSERT(nodeImpl == it.range()->endContainer(exception));
// non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
if (it.length() != 0) {
AXAttributedStringAppendText (attrString, nodeImpl, it.characters(), it.length());
} else {
AXAttributedStringAppendReplaced (attrString, nodeImpl->childNode(it.range()->startOffset(exception)));
}
it.advance();
}
return [attrString autorelease];
}
- (id)doAXTextMarkerRangeForUnorderedTextMarkers: (NSArray *) markers
{
#if MARKERARRAY_SELF_TEST
WebCoreTextMarkerRange *tmr = [self getSelectedTextMarkerRange];
WebCoreTextMarker *tm1 = AXTextMarkerRangeCopyEndMarker(tmr);
WebCoreTextMarker *tm2 = AXTextMarkerRangeCopyStartMarker(tmr);
markers = [NSArray arrayWithObjects: (id) tm1, (id) tm2, nil];
#endif
// 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;
// use the SelectionController class to do the ordering
// NOTE: Perhaps we could add a SelectionController method to indicate direction, based on m_baseIsStart
WebCoreTextMarker *startTextMarker;
WebCoreTextMarker *endTextMarker;
SelectionController sel(visiblePos1, visiblePos2);
if (sel.base() == sel.start()) {
startTextMarker = textMarker1;
endTextMarker = textMarker2;
} else {
startTextMarker = textMarker2;
endTextMarker = textMarker1;
}
// return a range based on the SelectionController verdict
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, khtml::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, khtml::RightWordIfOnBoundary);
VisiblePosition endPosition = endOfWord(startPosition);
return (id) [self textMarkerRangeFromVisiblePositions:startPosition andEndPos:endPosition];
}
- (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);
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);
VisiblePosition 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, khtml::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, khtml::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);
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);
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
visiblePos = visiblePos.next();
if (visiblePos.isNull())
return nil;
VisiblePosition endPosition = endOfSentence(visiblePos);
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
visiblePos = visiblePos.previous();
if (visiblePos.isNull())
return nil;
VisiblePosition startPosition = startOfSentence(visiblePos);
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];
}
- (id)doAXLengthForTextMarkerRange: (WebCoreTextMarkerRange *) textMarkerRange
{
// NOTE: BUG Multi-byte support
CFStringRef string = (CFStringRef) [self doAXStringForTextMarkerRange: textMarkerRange];
if (!string)
return nil;
return [NSNumber numberWithInt:CFStringGetLength(string)];
}
- (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter
{
WebCoreTextMarker * textMarker = nil;
WebCoreTextMarkerRange *textMarkerRange = nil;
NSNumber * number = nil;
NSArray * array = nil;
KWQAccObject * uiElement = nil;
NSPoint point = {0.0, 0.0};
bool pointSet = 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:[KWQAccObject self]])
uiElement = (KWQAccObject *) 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 {
// 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: @"AXLengthForTextMarkerRange"])
return [self doAXLengthForTextMarkerRange: textMarkerRange];
return nil;
}
- (id)accessibilityHitTest:(NSPoint)point
{
if (!m_renderer)
return NSAccessibilityUnignoredAncestor(self);
RenderObject::NodeInfo nodeInfo(true, true);
m_renderer->layer()->hitTest(nodeInfo, (int)point.x, (int)point.y);
if (!nodeInfo.innerNode())
return NSAccessibilityUnignoredAncestor(self);
RenderObject* obj = nodeInfo.innerNode()->renderer();
if (!obj)
return NSAccessibilityUnignoredAncestor(self);
KWQAccObject * accObject = obj->document()->getAccObjectCache()->accObject(obj);
return NSAccessibilityUnignoredAncestor(accObject);
}
- (RenderObject *) rendererForView:(NSView *)view
{
// check for WebCore NSView that lets us find its widget
Frame* docPart = m_renderer->document()->frame();
if (docPart) {
DOMElement *domElement = [Mac(docPart)->bridge() elementForView:view];
if (domElement)
return [domElement _elementImpl]->renderer();
}
// 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];
}
MacFrame *frame = [bridge impl];
if (!frame)
return NULL;
DocumentImpl *document = frame->document();
if (!document)
return NULL;
NodeImpl* node = document->ownerElement();
if (!node)
return NULL;
return node->renderer();
}
// _accessibilityParentForSubview is called by AppKit when moving up the tree
// we override it so that we can return our KWQAccObject parent of an AppKit AX object
- (id)_accessibilityParentForSubview:(NSView *)subview
{
RenderObject *renderer = [self rendererForView:subview];
if (!renderer)
return nil;
KWQAccObject* obj = renderer->document()->getAccObjectCache()->accObject(renderer);
return [obj parentObjectUnignored];
}
- (id)accessibilityFocusedUIElement
{
// NOTE: BUG support nested WebAreas
NodeImpl *focusNode = m_renderer->document()->focusNode();
if (!focusNode || !focusNode->renderer())
return nil;
KWQAccObject* obj = focusNode->renderer()->document()->getAccObjectCache()->accObject(focusNode->renderer());
// the HTML element, for example, is focusable but has an AX object that is ignored
if ([obj accessibilityIsIgnored])
obj = [obj parentObjectUnignored];
return obj;
}
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attributeName
{
if ([attributeName isEqualToString: @"AXSelectedTextMarkerRangeAttribute"])
return YES;
if ([attributeName isEqualToString: NSAccessibilityFocusedAttribute]) {
if ([[self role] isEqualToString:@"AXLink"] ||
(([[self role] isEqualToString:NSAccessibilityCheckBoxRole] ||
([[self role] isEqualToString:NSAccessibilityRadioButtonRole])) &&
m_renderer->element()->isEnabled()))
return YES;
}
return NO;
}
- (void)doSetAXSelectedTextMarkerRange: (WebCoreTextMarkerRange *)textMarkerRange
{
// extract the start and end VisiblePosition
VisiblePosition startVisiblePosition = [self visiblePositionForStartOfTextMarkerRange: textMarkerRange];
if (startVisiblePosition.isNull())
return;
VisiblePosition endVisiblePosition = [self visiblePositionForEndOfTextMarkerRange: textMarkerRange];
if (endVisiblePosition.isNull())
return;
// make selection and tell the document to use it
// NOTE: BUG support nested WebAreas
SelectionController sel = SelectionController(startVisiblePosition, endVisiblePosition);
[self topDocument]->frame()->setSelection(sel);
}
- (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attributeName;
{
WebCoreTextMarkerRange *textMarkerRange = nil;
NSNumber * number = nil;
// decode the parameter
if ([[WebCoreViewFactory sharedFactory] objectIsTextMarkerRange:value])
textMarkerRange = (WebCoreTextMarkerRange *) value;
else if ([value isKindOfClass:[NSNumber self]])
number = value;
// handle the command
if ([attributeName isEqualToString: @"AXSelectedTextMarkerRange"]) {
ASSERT(textMarkerRange);
[self doSetAXSelectedTextMarkerRange:textMarkerRange];
} else if ([attributeName isEqualToString: NSAccessibilityFocusedAttribute]) {
ASSERT(number);
if ([[self role] isEqualToString:@"AXLink"] ||
(([[self role] isEqualToString:NSAccessibilityCheckBoxRole] ||
([[self role] isEqualToString:NSAccessibilityRadioButtonRole])) &&
m_renderer->element()->isEnabled())) {
if ([number intValue] != 0)
m_renderer->document()->setFocusNode(m_renderer->element());
else
m_renderer->document()->setFocusNode(0);
}
}
}
- (void)childrenChanged
{
[self clearChildren];
if ([self accessibilityIsIgnored])
[[self parentObject] childrenChanged];
}
- (void)clearChildren
{
[m_children release];
m_children = nil;
}
-(KWQAccObjectID)accObjectID
{
return m_accObjectID;
}
-(void)setAccObjectID:(KWQAccObjectID) accObjectID
{
m_accObjectID = accObjectID;
}
- (void)removeAccObjectID
{
if (!m_accObjectID)
return;
m_renderer->document()->getAccObjectCache()->removeAccObjectID(self);
}
@end