| /* |
| * 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; |
| |
| Document* document = frame->document(); |
| if (!document) |
| return nil; |
| |
| Node* node = document->ownerElement(); |
| if (!node) |
| return nil; |
| |
| return node->renderer(); |
| } |
| |
| // _accessibilityParentForSubview is called by AppKit when moving up the tree |
| // we override it so that we can return our WebCoreAXObject parent of an AppKit AX object |
| - (id)_accessibilityParentForSubview:(NSView*)subview |
| { |
| RenderObject* renderer = [self rendererForView:subview]; |
| if (!renderer) |
| return nil; |
| |
| WebCoreAXObject* obj = renderer->document()->axObjectCache()->get(renderer); |
| return [obj parentObjectUnignored]; |
| } |
| |
| - (id)accessibilityFocusedUIElement |
| { |
| // get the focused node in the page |
| Page* page = m_renderer->document()->page(); |
| if (!page) |
| return nil; |
| |
| Document* focusedDocument = page->focusController()->focusedOrMainFrame()->document(); |
| Node* focusedNode = focusedDocument->focusedNode(); |
| if (!focusedNode || !focusedNode->renderer()) |
| focusedNode = focusedDocument; |
| |
| WebCoreAXObject* obj = focusedNode->renderer()->document()->axObjectCache()->get(focusedNode->renderer()); |
| |
| // the HTML element, for example, is focusable but has an AX object that is ignored |
| if ([obj accessibilityIsIgnored]) |
| obj = [obj parentObjectUnignored]; |
| |
| return obj; |
| } |
| |
| - (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 |
| Selection newSelection = Selection(startVisiblePosition, endVisiblePosition); |
| m_renderer->document()->frame()->selectionController()->setSelection(newSelection); |
| } |
| |
| - (BOOL)canSetFocusAttribute |
| { |
| // NOTE: It would be more accurate to ask the document whether setFocusedNode() would |
| // do anything. For example, it setFocusedNode() will do nothing if the current focused |
| // node will not relinquish the focus. |
| if (!m_renderer->element() || !m_renderer->element()->isEnabled()) |
| return NO; |
| |
| NSString* role = [self role]; |
| if ([role isEqualToString:@"AXLink"] || |
| [role isEqualToString:NSAccessibilityTextFieldRole] || |
| [role isEqualToString:NSAccessibilityTextAreaRole] || |
| [role isEqualToString:NSAccessibilityButtonRole] || |
| [role isEqualToString:NSAccessibilityPopUpButtonRole] || |
| [role isEqualToString:NSAccessibilityCheckBoxRole] || |
| [role isEqualToString:NSAccessibilityRadioButtonRole]) |
| return YES; |
| |
| return NO; |
| } |
| |
| - (BOOL)canSetValueAttribute |
| { |
| return [self isTextControl]; |
| } |
| |
| - (BOOL)canSetTextRangeAttributes |
| { |
| return [self isTextControl]; |
| } |
| |
| - (BOOL)accessibilityIsAttributeSettable:(NSString*)attributeName |
| { |
| if ([attributeName isEqualToString: @"AXSelectedTextMarkerRange"]) |
| return YES; |
| |
| if ([attributeName isEqualToString: NSAccessibilityFocusedAttribute]) |
| return [self canSetFocusAttribute]; |
| |
| if ([attributeName isEqualToString: NSAccessibilityValueAttribute]) |
| return [self canSetValueAttribute]; |
| |
| if ([attributeName isEqualToString: NSAccessibilitySelectedTextAttribute] || |
| [attributeName isEqualToString: NSAccessibilitySelectedTextRangeAttribute] || |
| [attributeName isEqualToString: NSAccessibilityVisibleCharacterRangeAttribute]) |
| return [self canSetTextRangeAttributes]; |
| |
| return NO; |
| } |
| |
| - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attributeName |
| { |
| WebCoreTextMarkerRange* textMarkerRange = nil; |
| NSNumber* number = nil; |
| NSString* string = nil; |
| NSRange range = {0, 0}; |
| |
| // decode the parameter |
| if ([[WebCoreViewFactory sharedFactory] objectIsTextMarkerRange:value]) |
| textMarkerRange = (WebCoreTextMarkerRange*) value; |
| |
| else if ([value isKindOfClass:[NSNumber self]]) |
| number = value; |
| |
| else if ([value isKindOfClass:[NSString self]]) |
| string = value; |
| |
| else if ([value isKindOfClass:[NSValue self]]) |
| range = [value rangeValue]; |
| |
| // handle the command |
| if ([attributeName isEqualToString: @"AXSelectedTextMarkerRange"]) { |
| ASSERT(textMarkerRange); |
| [self doSetAXSelectedTextMarkerRange:textMarkerRange]; |
| |
| } else if ([attributeName isEqualToString: NSAccessibilityFocusedAttribute]) { |
| ASSERT(number); |
| if ([self canSetFocusAttribute] && number) { |
| if ([number intValue] == 0) |
| m_renderer->document()->setFocusedNode(0); |
| else { |
| if (m_renderer->element()->isElementNode()) |
| static_cast<Element*>(m_renderer->element())->focus(); |
| else |
| m_renderer->document()->setFocusedNode(m_renderer->element()); |
| } |
| } |
| } else if ([attributeName isEqualToString: NSAccessibilityValueAttribute]) { |
| if (!string) |
| return; |
| if (m_renderer->isTextField()) { |
| HTMLInputElement* input = static_cast<HTMLInputElement*>(m_renderer->element()); |
| input->setValue(string); |
| } else if (m_renderer->isTextArea()) { |
| HTMLTextAreaElement* textArea = static_cast<HTMLTextAreaElement*>(m_renderer->element()); |
| textArea->setValue(string); |
| } |
| } else if ([self isTextControl]) { |
| RenderTextControl* textControl = static_cast<RenderTextControl*>(m_renderer); |
| if ([attributeName isEqualToString: NSAccessibilitySelectedTextAttribute]) { |
| // TODO: set selected text (ReplaceSelectionCommand). <rdar://problem/4712125> |
| } else if ([attributeName isEqualToString: NSAccessibilitySelectedTextRangeAttribute]) { |
| textControl->setSelectionRange(range.location, range.location + range.length); |
| } else if ([attributeName isEqualToString: NSAccessibilityVisibleCharacterRangeAttribute]) { |
| // TODO: make range visible (scrollRectToVisible). <rdar://problem/4712101> |
| } |
| } |
| } |
| |
| - (WebCoreAXObject*)observableObject |
| { |
| for (RenderObject* renderer = m_renderer; renderer && renderer->element(); renderer = renderer->parent()) { |
| if (renderer->isTextField() || renderer->isTextArea()) |
| return renderer->document()->axObjectCache()->get(renderer); |
| } |
| |
| return nil; |
| } |
| |
| - (void)childrenChanged |
| { |
| [self clearChildren]; |
| |
| if ([self accessibilityIsIgnored]) |
| [[self parentObject] childrenChanged]; |
| } |
| |
| - (void)clearChildren |
| { |
| [m_children release]; |
| m_children = nil; |
| } |
| |
| -(AXID)axObjectID |
| { |
| return m_id; |
| } |
| |
| -(void)setAXObjectID:(AXID) axObjectID |
| { |
| m_id = axObjectID; |
| } |
| |
| - (void)removeAXObjectID |
| { |
| if (!m_id) |
| return; |
| |
| m_renderer->document()->axObjectCache()->removeAXID(self); |
| } |
| |
| @end |