diff options
Diffstat (limited to 'Source/WebCore/accessibility/AXObjectCache.cpp')
-rw-r--r-- | Source/WebCore/accessibility/AXObjectCache.cpp | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/Source/WebCore/accessibility/AXObjectCache.cpp b/Source/WebCore/accessibility/AXObjectCache.cpp new file mode 100644 index 0000000..d0d19f7 --- /dev/null +++ b/Source/WebCore/accessibility/AXObjectCache.cpp @@ -0,0 +1,632 @@ +/* + * Copyright (C) 2008, 2009, 2010 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 "AXObjectCache.h" + +#include "AccessibilityARIAGrid.h" +#include "AccessibilityARIAGridCell.h" +#include "AccessibilityARIAGridRow.h" +#include "AccessibilityImageMapLink.h" +#include "AccessibilityList.h" +#include "AccessibilityListBox.h" +#include "AccessibilityListBoxOption.h" +#include "AccessibilityMediaControls.h" +#include "AccessibilityMenuList.h" +#include "AccessibilityMenuListOption.h" +#include "AccessibilityMenuListPopup.h" +#include "AccessibilityProgressIndicator.h" +#include "AccessibilityRenderObject.h" +#include "AccessibilityScrollView.h" +#include "AccessibilityScrollbar.h" +#include "AccessibilitySlider.h" +#include "AccessibilityTable.h" +#include "AccessibilityTableCell.h" +#include "AccessibilityTableColumn.h" +#include "AccessibilityTableHeaderContainer.h" +#include "AccessibilityTableRow.h" +#include "Document.h" +#include "FocusController.h" +#include "Frame.h" +#include "HTMLAreaElement.h" +#include "HTMLImageElement.h" +#include "HTMLNames.h" +#if ENABLE(VIDEO) +#include "MediaControlElements.h" +#endif +#include "InputElement.h" +#include "Page.h" +#include "RenderListBox.h" +#include "RenderMenuList.h" +#include "RenderProgress.h" +#include "RenderSlider.h" +#include "RenderTable.h" +#include "RenderTableCell.h" +#include "RenderTableRow.h" +#include "RenderView.h" +#include "ScrollView.h" + +#include <wtf/PassRefPtr.h> + +namespace WebCore { + +using namespace HTMLNames; + +bool AXObjectCache::gAccessibilityEnabled = false; +bool AXObjectCache::gAccessibilityEnhancedUserInterfaceEnabled = false; + +AXObjectCache::AXObjectCache(const Document* doc) + : m_notificationPostTimer(this, &AXObjectCache::notificationPostTimerFired) +{ + m_document = const_cast<Document*>(doc); +} + +AXObjectCache::~AXObjectCache() +{ + HashMap<AXID, RefPtr<AccessibilityObject> >::iterator end = m_objects.end(); + for (HashMap<AXID, RefPtr<AccessibilityObject> >::iterator it = m_objects.begin(); it != end; ++it) { + AccessibilityObject* obj = (*it).second.get(); + detachWrapper(obj); + obj->detach(); + removeAXID(obj); + } +} + +AccessibilityObject* AXObjectCache::focusedImageMapUIElement(HTMLAreaElement* areaElement) +{ + // Find the corresponding accessibility object for the HTMLAreaElement. This should be + // in the list of children for its corresponding image. + if (!areaElement) + return 0; + + HTMLImageElement* imageElement = areaElement->imageElement(); + if (!imageElement) + return 0; + + AccessibilityObject* axRenderImage = areaElement->document()->axObjectCache()->getOrCreate(imageElement->renderer()); + if (!axRenderImage) + return 0; + + AccessibilityObject::AccessibilityChildrenVector imageChildren = axRenderImage->children(); + unsigned count = imageChildren.size(); + for (unsigned k = 0; k < count; ++k) { + AccessibilityObject* child = imageChildren[k].get(); + if (!child->isImageMapLink()) + continue; + + if (static_cast<AccessibilityImageMapLink*>(child)->areaElement() == areaElement) + return child; + } + + return 0; +} + +AccessibilityObject* AXObjectCache::focusedUIElementForPage(const Page* page) +{ + // get the focused node in the page + Document* focusedDocument = page->focusController()->focusedOrMainFrame()->document(); + Node* focusedNode = focusedDocument->focusedNode(); + if (!focusedNode) + focusedNode = focusedDocument; + + if (focusedNode->hasTagName(areaTag)) + return focusedImageMapUIElement(static_cast<HTMLAreaElement*>(focusedNode)); + + RenderObject* focusedNodeRenderer = focusedNode->renderer(); + if (!focusedNodeRenderer) + return 0; + + AccessibilityObject* obj = focusedNodeRenderer->document()->axObjectCache()->getOrCreate(focusedNodeRenderer); + + if (obj->shouldFocusActiveDescendant()) { + if (AccessibilityObject* descendant = obj->activeDescendant()) + obj = descendant; + } + + // the HTML element, for example, is focusable but has an AX object that is ignored + if (obj->accessibilityIsIgnored()) + obj = obj->parentObjectUnignored(); + + return obj; +} + +AccessibilityObject* AXObjectCache::get(Widget* widget) +{ + if (!widget) + return 0; + + AXID axID = m_widgetObjectMapping.get(widget); + ASSERT(!HashTraits<AXID>::isDeletedValue(axID)); + if (!axID) + return 0; + + return m_objects.get(axID).get(); +} + +AccessibilityObject* AXObjectCache::get(RenderObject* renderer) +{ + if (!renderer) + return 0; + + AXID axID = m_renderObjectMapping.get(renderer); + ASSERT(!HashTraits<AXID>::isDeletedValue(axID)); + if (!axID) + return 0; + + return m_objects.get(axID).get(); +} + +// FIXME: This probably belongs on Node. +// FIXME: This should take a const char*, but one caller passes nullAtom. +bool nodeHasRole(Node* node, const String& role) +{ + if (!node || !node->isElementNode()) + return false; + + return equalIgnoringCase(static_cast<Element*>(node)->getAttribute(roleAttr), role); +} + +static PassRefPtr<AccessibilityObject> createFromRenderer(RenderObject* renderer) +{ + // FIXME: How could renderer->node() ever not be an Element? + Node* node = renderer->node(); + + // If the node is aria role="list" or the aria role is empty and its a + // ul/ol/dl type (it shouldn't be a list if aria says otherwise). + if (node && ((nodeHasRole(node, "list") || nodeHasRole(node, "directory")) + || (nodeHasRole(node, nullAtom) && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(dlTag))))) + return AccessibilityList::create(renderer); + + // aria tables + if (nodeHasRole(node, "grid") || nodeHasRole(node, "treegrid")) + return AccessibilityARIAGrid::create(renderer); + if (nodeHasRole(node, "row")) + return AccessibilityARIAGridRow::create(renderer); + if (nodeHasRole(node, "gridcell") || nodeHasRole(node, "columnheader") || nodeHasRole(node, "rowheader")) + return AccessibilityARIAGridCell::create(renderer); + +#if ENABLE(VIDEO) + // media controls + if (node && node->isMediaControlElement()) + return AccessibilityMediaControl::create(renderer); +#endif + + if (renderer->isBoxModelObject()) { + RenderBoxModelObject* cssBox = toRenderBoxModelObject(renderer); + if (cssBox->isListBox()) + return AccessibilityListBox::create(toRenderListBox(cssBox)); + if (cssBox->isMenuList()) + return AccessibilityMenuList::create(toRenderMenuList(cssBox)); + + // standard tables + if (cssBox->isTable()) + return AccessibilityTable::create(toRenderTable(cssBox)); + if (cssBox->isTableRow()) + return AccessibilityTableRow::create(toRenderTableRow(cssBox)); + if (cssBox->isTableCell()) + return AccessibilityTableCell::create(toRenderTableCell(cssBox)); + +#if ENABLE(PROGRESS_TAG) + // progress bar + if (cssBox->isProgress()) + return AccessibilityProgressIndicator::create(toRenderProgress(cssBox)); +#endif + + // input type=range + if (cssBox->isSlider()) + return AccessibilitySlider::create(toRenderSlider(cssBox)); + } + + return AccessibilityRenderObject::create(renderer); +} + +AccessibilityObject* AXObjectCache::getOrCreate(Widget* widget) +{ + if (!widget) + return 0; + + if (AccessibilityObject* obj = get(widget)) + return obj; + + RefPtr<AccessibilityObject> newObj = 0; + if (widget->isFrameView()) + newObj = AccessibilityScrollView::create(static_cast<ScrollView*>(widget)); + else if (widget->isScrollbar()) + newObj = AccessibilityScrollbar::create(static_cast<Scrollbar*>(widget)); + + getAXID(newObj.get()); + + m_widgetObjectMapping.set(widget, newObj->axObjectID()); + m_objects.set(newObj->axObjectID(), newObj); + attachWrapper(newObj.get()); + return newObj.get(); +} + +AccessibilityObject* AXObjectCache::getOrCreate(RenderObject* renderer) +{ + if (!renderer) + return 0; + + if (AccessibilityObject* obj = get(renderer)) + return obj; + + RefPtr<AccessibilityObject> newObj = createFromRenderer(renderer); + + getAXID(newObj.get()); + + m_renderObjectMapping.set(renderer, newObj->axObjectID()); + m_objects.set(newObj->axObjectID(), newObj); + attachWrapper(newObj.get()); + return newObj.get(); +} + +AccessibilityObject* AXObjectCache::rootObject() +{ + return getOrCreate(m_document->view()); +} + +AccessibilityObject* AXObjectCache::getOrCreate(AccessibilityRole role) +{ + RefPtr<AccessibilityObject> obj = 0; + + // will be filled in... + switch (role) { + case ListBoxOptionRole: + obj = AccessibilityListBoxOption::create(); + break; + case ImageMapLinkRole: + obj = AccessibilityImageMapLink::create(); + break; + case ColumnRole: + obj = AccessibilityTableColumn::create(); + break; + case TableHeaderContainerRole: + obj = AccessibilityTableHeaderContainer::create(); + break; + case SliderThumbRole: + obj = AccessibilitySliderThumb::create(); + break; + case MenuListPopupRole: + obj = AccessibilityMenuListPopup::create(); + break; + case MenuListOptionRole: + obj = AccessibilityMenuListOption::create(); + break; + default: + obj = 0; + } + + if (obj) + getAXID(obj.get()); + else + return 0; + + m_objects.set(obj->axObjectID(), obj); + attachWrapper(obj.get()); + return obj.get(); +} + +void AXObjectCache::remove(AXID axID) +{ + if (!axID) + return; + + // first fetch object to operate some cleanup functions on it + AccessibilityObject* obj = m_objects.get(axID).get(); + if (!obj) + return; + + detachWrapper(obj); + obj->detach(); + removeAXID(obj); + + // finally remove the object + if (!m_objects.take(axID)) + return; + + ASSERT(m_objects.size() >= m_idsInUse.size()); +} + +void AXObjectCache::remove(RenderObject* renderer) +{ + if (!renderer) + return; + + AXID axID = m_renderObjectMapping.get(renderer); + remove(axID); + m_renderObjectMapping.remove(renderer); +} + +void AXObjectCache::remove(Widget* view) +{ + if (!view) + return; + + AXID axID = m_widgetObjectMapping.get(view); + remove(axID); + m_widgetObjectMapping.remove(view); +} + + +#if !PLATFORM(WIN) || OS(WINCE) +AXID AXObjectCache::platformGenerateAXID() const +{ + static AXID lastUsedID = 0; + + // Generate a new ID. + AXID objID = lastUsedID; + do { + ++objID; + } while (!objID || HashTraits<AXID>::isDeletedValue(objID) || m_idsInUse.contains(objID)); + + lastUsedID = objID; + + return objID; +} +#endif + +AXID AXObjectCache::getAXID(AccessibilityObject* obj) +{ + // check for already-assigned ID + AXID objID = obj->axObjectID(); + if (objID) { + ASSERT(m_idsInUse.contains(objID)); + return objID; + } + + objID = platformGenerateAXID(); + + m_idsInUse.add(objID); + obj->setAXObjectID(objID); + + return objID; +} + +void AXObjectCache::removeAXID(AccessibilityObject* object) +{ + if (!object) + return; + + AXID objID = object->axObjectID(); + if (!objID) + return; + ASSERT(!HashTraits<AXID>::isDeletedValue(objID)); + ASSERT(m_idsInUse.contains(objID)); + object->setAXObjectID(0); + m_idsInUse.remove(objID); +} + +#if HAVE(ACCESSIBILITY) +void AXObjectCache::contentChanged(RenderObject* renderer) +{ + AccessibilityObject* object = getOrCreate(renderer); + if (object) + object->contentChanged(); +} +#endif + +void AXObjectCache::childrenChanged(RenderObject* renderer) +{ + if (!renderer) + return; + + AXID axID = m_renderObjectMapping.get(renderer); + if (!axID) + return; + + AccessibilityObject* obj = m_objects.get(axID).get(); + if (obj) + obj->childrenChanged(); +} + +void AXObjectCache::notificationPostTimerFired(Timer<AXObjectCache>*) +{ + m_notificationPostTimer.stop(); + + unsigned i = 0, count = m_notificationsToPost.size(); + for (i = 0; i < count; ++i) { + AccessibilityObject* obj = m_notificationsToPost[i].first.get(); +#ifndef NDEBUG + // Make sure none of the render views are in the process of being layed out. + // Notifications should only be sent after the renderer has finished + if (obj->isAccessibilityRenderObject()) { + AccessibilityRenderObject* renderObj = static_cast<AccessibilityRenderObject*>(obj); + RenderObject* renderer = renderObj->renderer(); + if (renderer && renderer->view()) + ASSERT(!renderer->view()->layoutState()); + } +#endif + + postPlatformNotification(obj, m_notificationsToPost[i].second); + } + + m_notificationsToPost.clear(); +} + +#if HAVE(ACCESSIBILITY) +void AXObjectCache::postNotification(RenderObject* renderer, AXNotification notification, bool postToElement, PostType postType) +{ + // Notifications for text input objects are sent to that object. + // All others are sent to the top WebArea. + if (!renderer) + return; + + // Get an accessibility object that already exists. One should not be created here + // because a render update may be in progress and creating an AX object can re-trigger a layout + RefPtr<AccessibilityObject> object = get(renderer); + while (!object && renderer) { + renderer = renderer->parent(); + object = get(renderer); + } + + if (!renderer) + return; + + postNotification(object.get(), renderer->document(), notification, postToElement, postType); +} + +void AXObjectCache::postNotification(AccessibilityObject* object, Document* document, AXNotification notification, bool postToElement, PostType postType) +{ + if (object && !postToElement) + object = object->observableObject(); + + if (!object && document) + object = get(document->renderer()); + + if (!object) + return; + + if (postType == PostAsynchronously) { + m_notificationsToPost.append(make_pair(object, notification)); + if (!m_notificationPostTimer.isActive()) + m_notificationPostTimer.startOneShot(0); + } else + postPlatformNotification(object, notification); +} + +void AXObjectCache::selectedChildrenChanged(RenderObject* renderer) +{ + // postToElement is false so that you can pass in any child of an element and it will go up the parent tree + // to find the container which should send out the notification. + postNotification(renderer, AXSelectedChildrenChanged, false); +} + +void AXObjectCache::nodeTextChangeNotification(RenderObject* renderer, AXTextChange textChange, unsigned offset, unsigned count) +{ + if (!renderer) + return; + + // Delegate on the right platform + AccessibilityObject* obj = getOrCreate(renderer); + nodeTextChangePlatformNotification(obj, textChange, offset, count); +} +#endif + +#if HAVE(ACCESSIBILITY) + +void AXObjectCache::handleScrollbarUpdate(ScrollView* view) +{ + if (!view) + return; + + // We don't want to create a scroll view from this method, only update an existing one. + AccessibilityObject* scrollViewObject = get(view); + if (scrollViewObject) + scrollViewObject->updateChildrenIfNecessary(); +} + +void AXObjectCache::handleAriaExpandedChange(RenderObject *renderer) +{ + if (!renderer) + return; + AccessibilityObject* obj = getOrCreate(renderer); + if (obj) + obj->handleAriaExpandedChanged(); +} + +void AXObjectCache::handleActiveDescendantChanged(RenderObject* renderer) +{ + if (!renderer) + return; + AccessibilityObject* obj = getOrCreate(renderer); + if (obj) + obj->handleActiveDescendantChanged(); +} + +void AXObjectCache::handleAriaRoleChanged(RenderObject* renderer) +{ + if (!renderer) + return; + AccessibilityObject* obj = getOrCreate(renderer); + if (obj && obj->isAccessibilityRenderObject()) + static_cast<AccessibilityRenderObject*>(obj)->updateAccessibilityRole(); +} +#endif + +VisiblePosition AXObjectCache::visiblePositionForTextMarkerData(TextMarkerData& textMarkerData) +{ + if (!isNodeInUse(textMarkerData.node)) + return VisiblePosition(); + + VisiblePosition visiblePos = VisiblePosition(textMarkerData.node, textMarkerData.offset, textMarkerData.affinity); + Position deepPos = visiblePos.deepEquivalent(); + if (deepPos.isNull()) + return VisiblePosition(); + + RenderObject* renderer = deepPos.node()->renderer(); + if (!renderer) + return VisiblePosition(); + + AXObjectCache* cache = renderer->document()->axObjectCache(); + if (!cache->isIDinUse(textMarkerData.axID)) + return VisiblePosition(); + + if (deepPos.node() != textMarkerData.node || deepPos.deprecatedEditingOffset() != textMarkerData.offset) + return VisiblePosition(); + + return visiblePos; +} + +void AXObjectCache::textMarkerDataForVisiblePosition(TextMarkerData& textMarkerData, const VisiblePosition& visiblePos) +{ + // This memory must be bzero'd so instances of TextMarkerData can be tested for byte-equivalence. + // This also allows callers to check for failure by looking at textMarkerData upon return. + memset(&textMarkerData, 0, sizeof(TextMarkerData)); + + if (visiblePos.isNull()) + return; + + Position deepPos = visiblePos.deepEquivalent(); + Node* domNode = deepPos.node(); + ASSERT(domNode); + if (!domNode) + return; + + if (domNode->isHTMLElement()) { + InputElement* inputElement = toInputElement(static_cast<Element*>(domNode)); + if (inputElement && inputElement->isPasswordField()) + return; + } + + // locate the renderer, which must exist for a visible dom node + RenderObject* renderer = domNode->renderer(); + ASSERT(renderer); + + // find or create an accessibility object for this renderer + AXObjectCache* cache = renderer->document()->axObjectCache(); + RefPtr<AccessibilityObject> obj = cache->getOrCreate(renderer); + + textMarkerData.axID = obj.get()->axObjectID(); + textMarkerData.node = domNode; + textMarkerData.offset = deepPos.deprecatedEditingOffset(); + textMarkerData.affinity = visiblePos.affinity(); + + cache->setNodeInUse(domNode); +} + +} // namespace WebCore |