diff options
Diffstat (limited to 'Source/WebCore/accessibility/AccessibilityObject.cpp')
-rw-r--r-- | Source/WebCore/accessibility/AccessibilityObject.cpp | 1071 |
1 files changed, 1071 insertions, 0 deletions
diff --git a/Source/WebCore/accessibility/AccessibilityObject.cpp b/Source/WebCore/accessibility/AccessibilityObject.cpp new file mode 100644 index 0000000..511bd3f --- /dev/null +++ b/Source/WebCore/accessibility/AccessibilityObject.cpp @@ -0,0 +1,1071 @@ +/* + * Copyright (C) 2008, 2009 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "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 OR ITS CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "AccessibilityObject.h" + +#include "AXObjectCache.h" +#include "AccessibilityRenderObject.h" +#include "CharacterNames.h" +#include "FloatRect.h" +#include "FocusController.h" +#include "Frame.h" +#include "FrameLoader.h" +#include "LocalizedStrings.h" +#include "NodeList.h" +#include "NotImplemented.h" +#include "Page.h" +#include "RenderImage.h" +#include "RenderListItem.h" +#include "RenderListMarker.h" +#include "RenderMenuList.h" +#include "RenderTextControl.h" +#include "RenderTheme.h" +#include "RenderView.h" +#include "RenderWidget.h" +#include "SelectionController.h" +#include "TextIterator.h" +#include "htmlediting.h" +#include "visible_units.h" +#include <wtf/StdLibExtras.h> + +using namespace std; + +namespace WebCore { + +using namespace HTMLNames; + +AccessibilityObject::AccessibilityObject() + : m_id(0) + , m_haveChildren(false) + , m_role(UnknownRole) +#if PLATFORM(GTK) + , m_wrapper(0) +#endif +{ +} + +AccessibilityObject::~AccessibilityObject() +{ + ASSERT(isDetached()); +} + +void AccessibilityObject::detach() +{ +#if HAVE(ACCESSIBILITY) + setWrapper(0); +#endif +} + +AccessibilityObject* AccessibilityObject::parentObjectUnignored() const +{ + AccessibilityObject* parent; + for (parent = parentObject(); parent && parent->accessibilityIsIgnored(); parent = parent->parentObject()) { + } + + return parent; +} + +AccessibilityObject* AccessibilityObject::firstAccessibleObjectFromNode(const Node* node) +{ + ASSERT(AXObjectCache::accessibilityEnabled()); + + if (!node) + return 0; + + Document* document = node->document(); + if (!document) + return 0; + + AXObjectCache* cache = document->axObjectCache(); + + AccessibilityObject* accessibleObject = cache->getOrCreate(node->renderer()); + while (accessibleObject && accessibleObject->accessibilityIsIgnored()) { + node = node->traverseNextNode(); + + while (node && !node->renderer()) + node = node->traverseNextSibling(); + + if (!node) + return 0; + + accessibleObject = cache->getOrCreate(node->renderer()); + } + + return accessibleObject; +} + +bool AccessibilityObject::isARIAInput(AccessibilityRole ariaRole) +{ + return ariaRole == RadioButtonRole || ariaRole == CheckBoxRole || ariaRole == TextFieldRole; +} + +bool AccessibilityObject::isARIAControl(AccessibilityRole ariaRole) +{ + return isARIAInput(ariaRole) || ariaRole == TextAreaRole || ariaRole == ButtonRole + || ariaRole == ComboBoxRole || ariaRole == SliderRole; +} + +IntPoint AccessibilityObject::clickPoint() const +{ + IntRect rect = elementRect(); + return IntPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() / 2); +} + +bool AccessibilityObject::press() const +{ + Element* actionElem = actionElement(); + if (!actionElem) + return false; + if (Frame* f = actionElem->document()->frame()) + f->loader()->resetMultipleFormSubmissionProtection(); + actionElem->accessKeyAction(true); + return true; +} + +String AccessibilityObject::language() const +{ + const AtomicString& lang = getAttribute(langAttr); + if (!lang.isEmpty()) + return lang; + + AccessibilityObject* parent = parentObject(); + + // as a last resort, fall back to the content language specified in the meta tag + if (!parent) { + Document* doc = document(); + if (doc) + return doc->contentLanguage(); + return nullAtom; + } + + return parent->language(); +} + +VisiblePositionRange AccessibilityObject::visiblePositionRangeForUnorderedPositions(const VisiblePosition& visiblePos1, const VisiblePosition& visiblePos2) const +{ + if (visiblePos1.isNull() || visiblePos2.isNull()) + return VisiblePositionRange(); + + VisiblePosition startPos; + VisiblePosition endPos; + 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 = VisibleSelection(visiblePos1, visiblePos2).isBaseFirst(); + + if (alreadyInOrder) { + startPos = visiblePos1; + endPos = visiblePos2; + } else { + startPos = visiblePos2; + endPos = visiblePos1; + } + + return VisiblePositionRange(startPos, endPos); +} + +VisiblePositionRange AccessibilityObject::positionOfLeftWord(const VisiblePosition& visiblePos) const +{ + VisiblePosition startPosition = startOfWord(visiblePos, LeftWordIfOnBoundary); + VisiblePosition endPosition = endOfWord(startPosition); + return VisiblePositionRange(startPosition, endPosition); +} + +VisiblePositionRange AccessibilityObject::positionOfRightWord(const VisiblePosition& visiblePos) const +{ + VisiblePosition startPosition = startOfWord(visiblePos, RightWordIfOnBoundary); + VisiblePosition endPosition = endOfWord(startPosition); + return VisiblePositionRange(startPosition, 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->isRenderBlock() && !p.deprecatedEditingOffset())) + break; + InlineBox* box; + int ignoredCaretOffset; + p.getInlineBoxAndOffset(tempPosition.affinity(), box, ignoredCaretOffset); + if (box) + break; + startPosition = tempPosition; + } + + return startPosition; +} + +VisiblePositionRange AccessibilityObject::leftLineVisiblePositionRange(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePositionRange(); + + // 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 VisiblePositionRange(); + + VisiblePosition startPosition = startOfLine(prevVisiblePos); + + // keep searching for a valid line start position. Unless the VisiblePosition 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 VisiblePositionRange(startPosition, endPosition); +} + +VisiblePositionRange AccessibilityObject::rightLineVisiblePositionRange(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePositionRange(); + + // make sure we move off of a line end + VisiblePosition nextVisiblePos = visiblePos.next(); + if (nextVisiblePos.isNull()) + return VisiblePositionRange(); + + 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 VisiblePosition 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 VisiblePositionRange(startPosition, endPosition); +} + +VisiblePositionRange AccessibilityObject::sentenceForPosition(const VisiblePosition& visiblePos) const +{ + // FIXME: FO 2 IMPLEMENT (currently returns incorrect answer) + // Related? <rdar://problem/3927736> Text selection broken in 8A336 + VisiblePosition startPosition = startOfSentence(visiblePos); + VisiblePosition endPosition = endOfSentence(startPosition); + return VisiblePositionRange(startPosition, endPosition); +} + +VisiblePositionRange AccessibilityObject::paragraphForPosition(const VisiblePosition& visiblePos) const +{ + VisiblePosition startPosition = startOfParagraph(visiblePos); + VisiblePosition endPosition = endOfParagraph(startPosition); + return VisiblePositionRange(startPosition, endPosition); +} + +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 lastDeepEditingPositionForNode(endRenderer->node()); +} + +VisiblePositionRange AccessibilityObject::styleRangeForPosition(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePositionRange(); + + return VisiblePositionRange(startOfStyleRange(visiblePos), endOfStyleRange(visiblePos)); +} + +// NOTE: Consider providing this utility method as AX API +VisiblePositionRange AccessibilityObject::visiblePositionRangeForRange(const PlainTextRange& range) const +{ + unsigned textLength = getLengthForTextRange(); + if (range.start + range.length > textLength) + return VisiblePositionRange(); + + VisiblePosition startPosition = visiblePositionForIndex(range.start); + startPosition.setAffinity(DOWNSTREAM); + VisiblePosition endPosition = visiblePositionForIndex(range.start + range.length); + return VisiblePositionRange(startPosition, endPosition); +} + +static bool replacedNodeNeedsCharacter(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()) + return false; + + // create an AX object, but skip it if it is not supposed to be seen + AccessibilityObject* object = replacedNode->renderer()->document()->axObjectCache()->getOrCreate(replacedNode->renderer()); + if (object->accessibilityIsIgnored()) + return false; + + return true; +} + +// Finds a RenderListItem parent give a node. +static RenderListItem* renderListItemContainerForNode(Node* node) +{ + for (; node; node = node->parentNode()) { + RenderBoxModelObject* renderer = node->renderBoxModelObject(); + if (renderer && renderer->isListItem()) + return toRenderListItem(renderer); + } + return 0; +} + +// Returns the text associated with a list marker if this node is contained within a list item. +String AccessibilityObject::listMarkerTextForNodeAndPosition(Node* node, const VisiblePosition& visiblePositionStart) const +{ + // If the range does not contain the start of the line, the list marker text should not be included. + if (!isStartOfLine(visiblePositionStart)) + return String(); + + RenderListItem* listItem = renderListItemContainerForNode(node); + if (!listItem) + return String(); + + // If this is in a list item, we need to manually add the text for the list marker + // because a RenderListMarker does not have a Node equivalent and thus does not appear + // when iterating text. + const String& markerText = listItem->markerText(); + if (markerText.isEmpty()) + return String(); + + // Append text, plus the period that follows the text. + // FIXME: Not all list marker styles are followed by a period, but this + // sounds much better when there is a synthesized pause because of a period. + Vector<UChar> resultVector; + resultVector.append(markerText.characters(), markerText.length()); + resultVector.append('.'); + resultVector.append(' '); + + return String::adopt(resultVector); +} + +String AccessibilityObject::stringForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const +{ + if (visiblePositionRange.isNull()) + return String(); + + Vector<UChar> resultVector; + RefPtr<Range> range = makeRange(visiblePositionRange.start, visiblePositionRange.end); + for (TextIterator it(range.get()); !it.atEnd(); it.advance()) { + // non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX) + if (it.length()) { + // Add a textual representation for list marker text + String listMarkerText = listMarkerTextForNodeAndPosition(it.node(), visiblePositionRange.start); + if (!listMarkerText.isEmpty()) + resultVector.append(listMarkerText.characters(), listMarkerText.length()); + + resultVector.append(it.characters(), 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); + + if (replacedNodeNeedsCharacter(node->childNode(offset))) + resultVector.append(objectReplacementCharacter); + } + } + + return String::adopt(resultVector); +} + +int AccessibilityObject::lengthForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const +{ + // FIXME: Multi-byte support + if (visiblePositionRange.isNull()) + return -1; + + int length = 0; + RefPtr<Range> range = makeRange(visiblePositionRange.start, visiblePositionRange.end); + for (TextIterator it(range.get()); !it.atEnd(); it.advance()) { + // non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX) + if (it.length()) + 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); + + if (replacedNodeNeedsCharacter(node->childNode(offset))) + length++; + } + } + + return length; +} + +VisiblePosition AccessibilityObject::nextWordEnd(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePosition(); + + // make sure we move off of a word end + VisiblePosition nextVisiblePos = visiblePos.next(); + if (nextVisiblePos.isNull()) + return VisiblePosition(); + + return endOfWord(nextVisiblePos, LeftWordIfOnBoundary); +} + +VisiblePosition AccessibilityObject::previousWordStart(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePosition(); + + // make sure we move off of a word start + VisiblePosition prevVisiblePos = visiblePos.previous(); + if (prevVisiblePos.isNull()) + return VisiblePosition(); + + return startOfWord(prevVisiblePos, RightWordIfOnBoundary); +} + +VisiblePosition AccessibilityObject::nextLineEndPosition(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePosition(); + + // to make sure we move off of a line end + VisiblePosition nextVisiblePos = visiblePos.next(); + if (nextVisiblePos.isNull()) + return VisiblePosition(); + + 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 endPosition; +} + +VisiblePosition AccessibilityObject::previousLineStartPosition(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePosition(); + + // make sure we move off of a line start + VisiblePosition prevVisiblePos = visiblePos.previous(); + if (prevVisiblePos.isNull()) + return VisiblePosition(); + + 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 startPosition; +} + +VisiblePosition AccessibilityObject::nextSentenceEndPosition(const VisiblePosition& visiblePos) const +{ + // FIXME: FO 2 IMPLEMENT (currently returns incorrect answer) + // Related? <rdar://problem/3927736> Text selection broken in 8A336 + if (visiblePos.isNull()) + return VisiblePosition(); + + // make sure we move off of a sentence end + VisiblePosition nextVisiblePos = visiblePos.next(); + if (nextVisiblePos.isNull()) + return VisiblePosition(); + + // 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(nextVisiblePos), endOfLine(nextVisiblePos)).get()); + if (lineString.isEmpty()) + endPosition = nextVisiblePos; + else + endPosition = endOfSentence(nextVisiblePos); + + return endPosition; +} + +VisiblePosition AccessibilityObject::previousSentenceStartPosition(const VisiblePosition& visiblePos) const +{ + // FIXME: FO 2 IMPLEMENT (currently returns incorrect answer) + // Related? <rdar://problem/3927736> Text selection broken in 8A336 + if (visiblePos.isNull()) + return VisiblePosition(); + + // make sure we move off of a sentence start + VisiblePosition previousVisiblePos = visiblePos.previous(); + if (previousVisiblePos.isNull()) + return VisiblePosition(); + + // 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 startPosition; +} + +VisiblePosition AccessibilityObject::nextParagraphEndPosition(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePosition(); + + // make sure we move off of a paragraph end + VisiblePosition nextPos = visiblePos.next(); + if (nextPos.isNull()) + return VisiblePosition(); + + return endOfParagraph(nextPos); +} + +VisiblePosition AccessibilityObject::previousParagraphStartPosition(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return VisiblePosition(); + + // make sure we move off of a paragraph start + VisiblePosition previousPos = visiblePos.previous(); + if (previousPos.isNull()) + return VisiblePosition(); + + return startOfParagraph(previousPos); +} + +AccessibilityObject* AccessibilityObject::accessibilityObjectForPosition(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return 0; + + RenderObject* obj = visiblePos.deepEquivalent().node()->renderer(); + if (!obj) + return 0; + + return obj->document()->axObjectCache()->getOrCreate(obj); +} + +int AccessibilityObject::lineForPosition(const VisiblePosition& visiblePos) const +{ + if (visiblePos.isNull()) + return 0; + + unsigned lineCount = 0; + VisiblePosition currentVisiblePos = visiblePos; + VisiblePosition savedVisiblePos; + + // move up until we get to the top + // FIXME: This only takes us to the top of the rootEditableElement, not the top of the + // top document. + while (currentVisiblePos.isNotNull() && !(inSameLine(currentVisiblePos, savedVisiblePos))) { + ++lineCount; + savedVisiblePos = currentVisiblePos; + VisiblePosition prevVisiblePos = previousLinePosition(currentVisiblePos, 0); + currentVisiblePos = prevVisiblePos; + } + + return lineCount - 1; +} + +// NOTE: Consider providing this utility method as AX API +PlainTextRange AccessibilityObject::plainTextRangeForVisiblePositionRange(const VisiblePositionRange& positionRange) const +{ + int index1 = index(positionRange.start); + int index2 = index(positionRange.end); + if (index1 < 0 || index2 < 0 || index1 > index2) + return PlainTextRange(); + + return PlainTextRange(index1, index2 - index1); +} + +// 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? +PlainTextRange AccessibilityObject::doAXRangeForPosition(const IntPoint& point) const +{ + int i = index(visiblePositionForPoint(point)); + if (i < 0) + return PlainTextRange(); + + return PlainTextRange(i, 1); +} + +// Given a character index, the range of text associated with this accessibility object +// over which the style in effect at that character index applies. +PlainTextRange AccessibilityObject::doAXStyleRangeForIndex(unsigned index) const +{ + VisiblePositionRange range = styleRangeForPosition(visiblePositionForIndex(index, false)); + return plainTextRangeForVisiblePositionRange(range); +} + +// Given an indexed character, the line number of the text associated with this accessibility +// object that contains the character. +unsigned AccessibilityObject::doAXLineForIndex(unsigned index) +{ + return lineForPosition(visiblePositionForIndex(index, false)); +} + +FrameView* AccessibilityObject::documentFrameView() const +{ + const AccessibilityObject* object = this; + while (object && !object->isAccessibilityRenderObject()) + object = object->parentObject(); + + if (!object) + return 0; + + return object->documentFrameView(); +} + +void AccessibilityObject::updateChildrenIfNecessary() +{ + if (!hasChildren()) + addChildren(); +} + +void AccessibilityObject::clearChildren() +{ + m_children.clear(); + m_haveChildren = false; +} + +AccessibilityObject* AccessibilityObject::anchorElementForNode(Node* node) +{ + RenderObject* obj = node->renderer(); + if (!obj) + return 0; + + RefPtr<AccessibilityObject> axObj = obj->document()->axObjectCache()->getOrCreate(obj); + Element* anchor = axObj->anchorElement(); + if (!anchor) + return 0; + + RenderObject* anchorRenderer = anchor->renderer(); + if (!anchorRenderer) + return 0; + + return anchorRenderer->document()->axObjectCache()->getOrCreate(anchorRenderer); +} + +void AccessibilityObject::ariaTreeRows(AccessibilityChildrenVector& result) +{ + AccessibilityChildrenVector axChildren = children(); + unsigned count = axChildren.size(); + for (unsigned k = 0; k < count; ++k) { + AccessibilityObject* obj = axChildren[k].get(); + + // Add tree items as the rows. + if (obj->roleValue() == TreeItemRole) + result.append(obj); + + // Now see if this item also has rows hiding inside of it. + obj->ariaTreeRows(result); + } +} + +void AccessibilityObject::ariaTreeItemContent(AccessibilityChildrenVector& result) +{ + // The ARIA tree item content are the item that are not other tree items or their containing groups. + AccessibilityChildrenVector axChildren = children(); + unsigned count = axChildren.size(); + for (unsigned k = 0; k < count; ++k) { + AccessibilityObject* obj = axChildren[k].get(); + AccessibilityRole role = obj->roleValue(); + if (role == TreeItemRole || role == GroupRole) + continue; + + result.append(obj); + } +} + +void AccessibilityObject::ariaTreeItemDisclosedRows(AccessibilityChildrenVector& result) +{ + AccessibilityChildrenVector axChildren = children(); + unsigned count = axChildren.size(); + for (unsigned k = 0; k < count; ++k) { + AccessibilityObject* obj = axChildren[k].get(); + + // Add tree items as the rows. + if (obj->roleValue() == TreeItemRole) + result.append(obj); + // If it's not a tree item, then descend into the group to find more tree items. + else + obj->ariaTreeRows(result); + } +} + +const String& AccessibilityObject::actionVerb() const +{ + // FIXME: Need to add verbs for select elements. + DEFINE_STATIC_LOCAL(const String, buttonAction, (AXButtonActionVerb())); + DEFINE_STATIC_LOCAL(const String, textFieldAction, (AXTextFieldActionVerb())); + DEFINE_STATIC_LOCAL(const String, radioButtonAction, (AXRadioButtonActionVerb())); + DEFINE_STATIC_LOCAL(const String, checkedCheckBoxAction, (AXCheckedCheckBoxActionVerb())); + DEFINE_STATIC_LOCAL(const String, uncheckedCheckBoxAction, (AXUncheckedCheckBoxActionVerb())); + DEFINE_STATIC_LOCAL(const String, linkAction, (AXLinkActionVerb())); + DEFINE_STATIC_LOCAL(const String, menuListAction, (AXMenuListActionVerb())); + DEFINE_STATIC_LOCAL(const String, menuListPopupAction, (AXMenuListPopupActionVerb())); + DEFINE_STATIC_LOCAL(const String, noAction, ()); + + switch (roleValue()) { + case ButtonRole: + return buttonAction; + case TextFieldRole: + case TextAreaRole: + return textFieldAction; + case RadioButtonRole: + return radioButtonAction; + case CheckBoxRole: + return isChecked() ? checkedCheckBoxAction : uncheckedCheckBoxAction; + case LinkRole: + case WebCoreLinkRole: + return linkAction; + case PopUpButtonRole: + return menuListAction; + case MenuListPopupRole: + return menuListPopupAction; + default: + return noAction; + } +} + +bool AccessibilityObject::ariaIsMultiline() const +{ + return equalIgnoringCase(getAttribute(aria_multilineAttr), "true"); +} + +const AtomicString& AccessibilityObject::invalidStatus() const +{ + DEFINE_STATIC_LOCAL(const AtomicString, invalidStatusFalse, ("false")); + + // aria-invalid can return false (default), grammer, spelling, or true. + const AtomicString& ariaInvalid = getAttribute(aria_invalidAttr); + + // If empty or not present, it should return false. + if (ariaInvalid.isEmpty()) + return invalidStatusFalse; + + return ariaInvalid; +} + +const AtomicString& AccessibilityObject::getAttribute(const QualifiedName& attribute) const +{ + Node* elementNode = node(); + if (!elementNode) + return nullAtom; + + if (!elementNode->isElementNode()) + return nullAtom; + + Element* element = static_cast<Element*>(elementNode); + return element->fastGetAttribute(attribute); +} + +// Lacking concrete evidence of orientation, horizontal means width > height. vertical is height > width; +AccessibilityOrientation AccessibilityObject::orientation() const +{ + IntRect bounds = elementRect(); + if (bounds.size().width() > bounds.size().height()) + return AccessibilityOrientationHorizontal; + if (bounds.size().height() > bounds.size().width()) + return AccessibilityOrientationVertical; + + // A tie goes to horizontal. + return AccessibilityOrientationHorizontal; +} + +typedef HashMap<String, AccessibilityRole, CaseFoldingHash> ARIARoleMap; + +struct RoleEntry { + String ariaRole; + AccessibilityRole webcoreRole; +}; + +static ARIARoleMap* createARIARoleMap() +{ + const RoleEntry roles[] = { + { "alert", ApplicationAlertRole }, + { "alertdialog", ApplicationAlertDialogRole }, + { "application", LandmarkApplicationRole }, + { "article", DocumentArticleRole }, + { "banner", LandmarkBannerRole }, + { "button", ButtonRole }, + { "checkbox", CheckBoxRole }, + { "complementary", LandmarkComplementaryRole }, + { "contentinfo", LandmarkContentInfoRole }, + { "dialog", ApplicationDialogRole }, + { "directory", DirectoryRole }, + { "grid", TableRole }, + { "gridcell", CellRole }, + { "columnheader", ColumnHeaderRole }, + { "combobox", ComboBoxRole }, + { "definition", DefinitionListDefinitionRole }, + { "document", DocumentRole }, + { "rowheader", RowHeaderRole }, + { "group", GroupRole }, + { "heading", HeadingRole }, + { "img", ImageRole }, + { "link", WebCoreLinkRole }, + { "list", ListRole }, + { "listitem", ListItemRole }, + { "listbox", ListBoxRole }, + { "log", ApplicationLogRole }, + // "option" isn't here because it may map to different roles depending on the parent element's role + { "main", LandmarkMainRole }, + { "marquee", ApplicationMarqueeRole }, + { "math", DocumentMathRole }, + { "menu", MenuRole }, + { "menubar", MenuBarRole }, + // "menuitem" isn't here because it may map to different roles depending on the parent element's role + { "menuitemcheckbox", MenuItemRole }, + { "menuitemradio", MenuItemRole }, + { "note", DocumentNoteRole }, + { "navigation", LandmarkNavigationRole }, + { "option", ListBoxOptionRole }, + { "presentation", PresentationalRole }, + { "progressbar", ProgressIndicatorRole }, + { "radio", RadioButtonRole }, + { "radiogroup", RadioGroupRole }, + { "region", DocumentRegionRole }, + { "row", RowRole }, + { "range", SliderRole }, + { "scrollbar", ScrollBarRole }, + { "search", LandmarkSearchRole }, + { "separator", SplitterRole }, + { "slider", SliderRole }, + { "spinbutton", ProgressIndicatorRole }, + { "status", ApplicationStatusRole }, + { "tab", TabRole }, + { "tablist", TabListRole }, + { "tabpanel", TabPanelRole }, + { "text", StaticTextRole }, + { "textbox", TextAreaRole }, + { "timer", ApplicationTimerRole }, + { "toolbar", ToolbarRole }, + { "tooltip", UserInterfaceTooltipRole }, + { "tree", TreeRole }, + { "treegrid", TreeGridRole }, + { "treeitem", TreeItemRole } + }; + ARIARoleMap* roleMap = new ARIARoleMap; + + for (size_t i = 0; i < WTF_ARRAY_LENGTH(roles); ++i) + roleMap->set(roles[i].ariaRole, roles[i].webcoreRole); + return roleMap; +} + +AccessibilityRole AccessibilityObject::ariaRoleToWebCoreRole(const String& value) +{ + ASSERT(!value.isEmpty()); + static const ARIARoleMap* roleMap = createARIARoleMap(); + return roleMap->get(value); +} + +const AtomicString& AccessibilityObject::placeholderValue() const +{ + const AtomicString& placeholder = getAttribute(placeholderAttr); + if (!placeholder.isEmpty()) + return placeholder; + + return nullAtom; +} + +bool AccessibilityObject::isInsideARIALiveRegion() const +{ + if (supportsARIALiveRegion()) + return true; + + for (AccessibilityObject* axParent = parentObject(); axParent; axParent = axParent->parentObject()) { + if (axParent->supportsARIALiveRegion()) + return true; + } + + return false; +} + +bool AccessibilityObject::supportsARIAAttributes() const +{ + return supportsARIALiveRegion() || supportsARIADragging() || supportsARIADropping() || supportsARIAFlowTo() || supportsARIAOwns(); +} + +bool AccessibilityObject::supportsARIALiveRegion() const +{ + const AtomicString& liveRegion = ariaLiveRegionStatus(); + return equalIgnoringCase(liveRegion, "polite") || equalIgnoringCase(liveRegion, "assertive"); +} + +AccessibilityObject* AccessibilityObject::elementAccessibilityHitTest(const IntPoint& point) const +{ + // Send the hit test back into the sub-frame if necessary. + if (isAttachment()) { + Widget* widget = widgetForAttachmentView(); + if (widget && widget->isFrameView()) + return axObjectCache()->getOrCreate(static_cast<ScrollView*>(widget))->accessibilityHitTest(point); + } + + return const_cast<AccessibilityObject*>(this); +} + +AXObjectCache* AccessibilityObject::axObjectCache() const +{ + Document* doc = document(); + if (doc) + return doc->axObjectCache(); + return 0; +} + +AccessibilityObject* AccessibilityObject::focusedUIElement() const +{ + Document* doc = document(); + if (!doc) + return 0; + + Page* page = doc->page(); + if (!page) + return 0; + + return AXObjectCache::focusedUIElementForPage(page); +} + +bool AccessibilityObject::supportsARIAExpanded() const +{ + return !getAttribute(aria_expandedAttr).isEmpty(); +} + +bool AccessibilityObject::isExpanded() const +{ + if (equalIgnoringCase(getAttribute(aria_expandedAttr), "true")) + return true; + + return false; +} + +AccessibilityButtonState AccessibilityObject::checkboxOrRadioValue() const +{ + // If this is a real checkbox or radio button, AccessibilityRenderObject will handle. + // If it's an ARIA checkbox or radio, the aria-checked attribute should be used. + + const AtomicString& result = getAttribute(aria_checkedAttr); + if (equalIgnoringCase(result, "true")) + return ButtonStateOn; + if (equalIgnoringCase(result, "mixed")) + return ButtonStateMixed; + + return ButtonStateOff; +} + +} // namespace WebCore |