blob: 125c192bc0ce3c44b10141035b621345a896eef6 [file] [log] [blame]
/*
* Copyright (C) 2008 Nuanti Ltd.
* Copyright (C) 2009 Jan Alonzo
* Copyright (C) 2009, 2010, 2011, 2012, 2019 Igalia S.L.
* Copyright (C) 2013 Samsung Electronics
*
* Portions from Mozilla a11y, copyright as follows:
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is
* Sun Microsystems, Inc.
* Portions created by the Initial Developer are Copyright (C) 2002
* the Initial Developer. All Rights Reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "WebKitAccessible.h"
#if ENABLE(ACCESSIBILITY)
#include "AXObjectCache.h"
#include "AccessibilityList.h"
#include "AccessibilityListBoxOption.h"
#include "AccessibilityTable.h"
#include "AccessibilityTableCell.h"
#include "AccessibilityTableRow.h"
#include "Document.h"
#include "Editing.h"
#include "Frame.h"
#include "FrameView.h"
#include "HTMLNames.h"
#include "HTMLTableElement.h"
#include "HostWindow.h"
#include "RenderAncestorIterator.h"
#include "RenderBlock.h"
#include "RenderObject.h"
#include "SVGElement.h"
#include "Settings.h"
#include "TextIterator.h"
#include "VisibleUnits.h"
#include "WebKitAccessibleHyperlink.h"
#include "WebKitAccessibleInterfaceAction.h"
#include "WebKitAccessibleInterfaceComponent.h"
#include "WebKitAccessibleInterfaceDocument.h"
#include "WebKitAccessibleInterfaceEditableText.h"
#include "WebKitAccessibleInterfaceHyperlinkImpl.h"
#include "WebKitAccessibleInterfaceHypertext.h"
#include "WebKitAccessibleInterfaceImage.h"
#include "WebKitAccessibleInterfaceSelection.h"
#include "WebKitAccessibleInterfaceTable.h"
#include "WebKitAccessibleInterfaceTableCell.h"
#include "WebKitAccessibleInterfaceText.h"
#include "WebKitAccessibleInterfaceValue.h"
#include "WebKitAccessibleUtil.h"
#include <glib/gprintf.h>
#include <wtf/glib/WTFGType.h>
#include <wtf/text/CString.h>
using namespace WebCore;
struct _WebKitAccessiblePrivate {
AccessibilityObject* object;
// Cached data for AtkObject.
CString accessibleName;
CString accessibleDescription;
// Cached data for AtkAction.
CString actionName;
CString actionKeyBinding;
// Cached data for AtkDocument.
CString documentLocale;
CString documentType;
CString documentEncoding;
CString documentURI;
// Cached data for AtkImage.
CString imageDescription;
};
WEBKIT_DEFINE_TYPE(WebKitAccessible, webkit_accessible, ATK_TYPE_OBJECT)
static AccessibilityObject* fallbackObject()
{
static AccessibilityObject* object = &AccessibilityListBoxOption::create().leakRef();
return object;
}
static const gchar* webkitAccessibleGetName(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, nullptr);
Vector<AccessibilityText> textOrder;
accessible->priv->object->accessibilityText(textOrder);
for (const auto& text : textOrder) {
// FIXME: This check is here because AccessibilityNodeObject::titleElementText()
// appends an empty String for the LabelByElementText source when there is a
// titleUIElement(). Removing this check makes some fieldsets lose their name.
if (text.text.isEmpty())
continue;
// WebCore Accessibility should provide us with the text alternative computation
// in the order defined by that spec. So take the first thing that our platform
// does not expose via the AtkObject description.
if (text.textSource != AccessibilityTextSource::Help && text.textSource != AccessibilityTextSource::Summary)
return webkitAccessibleCacheAndReturnAtkProperty(accessible, AtkCachedAccessibleName, text.text.utf8());
}
return webkitAccessibleCacheAndReturnAtkProperty(accessible, AtkCachedAccessibleName, "");
}
static const gchar* webkitAccessibleGetDescription(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, nullptr);
Vector<AccessibilityText> textOrder;
accessible->priv->object->accessibilityText(textOrder);
bool nameTextAvailable = false;
for (const auto& text : textOrder) {
// WebCore Accessibility should provide us with the text alternative computation
// in the order defined by that spec. So take the first thing that our platform
// does not expose via the AtkObject name.
if (text.textSource == AccessibilityTextSource::Help || text.textSource == AccessibilityTextSource::Summary)
return webkitAccessibleCacheAndReturnAtkProperty(accessible, AtkCachedAccessibleDescription, text.text.utf8());
// If there is no other text alternative, the title tag contents will have been
// used for the AtkObject name. We don't want to duplicate it here.
if (text.textSource == AccessibilityTextSource::TitleTag && nameTextAvailable)
return webkitAccessibleCacheAndReturnAtkProperty(accessible, AtkCachedAccessibleDescription, text.text.utf8());
nameTextAvailable = true;
}
return webkitAccessibleCacheAndReturnAtkProperty(accessible, AtkCachedAccessibleDescription, "");
}
static void removeAtkRelationByType(AtkRelationSet* relationSet, AtkRelationType relationType)
{
int count = atk_relation_set_get_n_relations(relationSet);
for (int i = 0; i < count; i++) {
AtkRelation* relation = atk_relation_set_get_relation(relationSet, i);
if (atk_relation_get_relation_type(relation) == relationType) {
atk_relation_set_remove(relationSet, relation);
break;
}
}
}
static void setAtkRelationSetFromCoreObject(AccessibilityObject* coreObject, AtkRelationSet* relationSet)
{
// Elements with aria-labelledby should have the labelled-by relation as per the ARIA AAM spec.
// Controls with a label element and fieldsets with a legend element should also use this relation
// as per the HTML AAM spec. The reciprocal label-for relation should also be used.
removeAtkRelationByType(relationSet, ATK_RELATION_LABELLED_BY);
removeAtkRelationByType(relationSet, ATK_RELATION_LABEL_FOR);
if (coreObject->isControl()) {
if (AccessibilityObject* label = coreObject->correspondingLabelForControlElement())
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_LABELLED_BY, ATK_OBJECT(label->wrapper()));
} else if (coreObject->isFieldset()) {
if (AccessibilityObject* label = coreObject->titleUIElement())
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_LABELLED_BY, ATK_OBJECT(label->wrapper()));
} else if (coreObject->roleValue() == AccessibilityRole::Legend) {
if (RenderBlock* renderFieldset = ancestorsOfType<RenderBlock>(*coreObject->renderer()).first()) {
if (renderFieldset->isFieldset()) {
AccessibilityObject* fieldset = coreObject->axObjectCache()->getOrCreate(renderFieldset);
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_LABEL_FOR, ATK_OBJECT(fieldset->wrapper()));
}
}
} else if (AccessibilityObject* control = coreObject->correspondingControlForLabelElement())
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_LABEL_FOR, ATK_OBJECT(control->wrapper()));
else {
AccessibilityObject::AccessibilityChildrenVector ariaLabelledByElements;
coreObject->ariaLabelledByElements(ariaLabelledByElements);
for (const auto& accessibilityObject : ariaLabelledByElements)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_LABELLED_BY, ATK_OBJECT(accessibilityObject->wrapper()));
}
// Elements referenced by aria-labelledby should have the label-for relation as per the ARIA AAM spec.
AccessibilityObject::AccessibilityChildrenVector labels;
coreObject->ariaLabelledByReferencingElements(labels);
for (const auto& accessibilityObject : labels)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_LABEL_FOR, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements with aria-flowto should have the flows-to relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_FLOWS_TO);
AccessibilityObject::AccessibilityChildrenVector ariaFlowToElements;
coreObject->ariaFlowToElements(ariaFlowToElements);
for (const auto& accessibilityObject : ariaFlowToElements)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_FLOWS_TO, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements referenced by aria-flowto should have the flows-from relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_FLOWS_FROM);
AccessibilityObject::AccessibilityChildrenVector flowFrom;
coreObject->ariaFlowToReferencingElements(flowFrom);
for (const auto& accessibilityObject : flowFrom)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_FLOWS_FROM, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements with aria-describedby should have the described-by relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_DESCRIBED_BY);
AccessibilityObject::AccessibilityChildrenVector ariaDescribedByElements;
coreObject->ariaDescribedByElements(ariaDescribedByElements);
for (const auto& accessibilityObject : ariaDescribedByElements)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_DESCRIBED_BY, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements referenced by aria-describedby should have the description-for relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_DESCRIPTION_FOR);
AccessibilityObject::AccessibilityChildrenVector describers;
coreObject->ariaDescribedByReferencingElements(describers);
for (const auto& accessibilityObject : describers)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_DESCRIPTION_FOR, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements with aria-controls should have the controller-for relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_CONTROLLER_FOR);
AccessibilityObject::AccessibilityChildrenVector ariaControls;
coreObject->ariaControlsElements(ariaControls);
for (const auto& accessibilityObject : ariaControls)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_CONTROLLER_FOR, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements referenced by aria-controls should have the controlled-by relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_CONTROLLED_BY);
AccessibilityObject::AccessibilityChildrenVector controllers;
coreObject->ariaControlsReferencingElements(controllers);
for (const auto& accessibilityObject : controllers)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_CONTROLLED_BY, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements with aria-owns should have the node-parent-of relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_NODE_PARENT_OF);
AccessibilityObject::AccessibilityChildrenVector ariaOwns;
coreObject->ariaOwnsElements(ariaOwns);
for (const auto& accessibilityObject : ariaOwns)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_NODE_PARENT_OF, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements referenced by aria-owns should have the node-child-of relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_NODE_CHILD_OF);
AccessibilityObject::AccessibilityChildrenVector owners;
coreObject->ariaOwnsReferencingElements(owners);
for (const auto& accessibilityObject : owners)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_NODE_CHILD_OF, ATK_OBJECT(accessibilityObject->wrapper()));
#if ATK_CHECK_VERSION(2, 25, 2)
// Elements with aria-details should have the details relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_DETAILS);
AccessibilityObject::AccessibilityChildrenVector ariaDetails;
coreObject->ariaDetailsElements(ariaDetails);
for (const auto& accessibilityObject : ariaDetails)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_DETAILS, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements referenced by aria-details should have the details-for relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_DETAILS_FOR);
AccessibilityObject::AccessibilityChildrenVector details;
coreObject->ariaDetailsReferencingElements(details);
for (const auto& accessibilityObject : details)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_DETAILS_FOR, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements with aria-errormessage should have the error-message relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_ERROR_MESSAGE);
AccessibilityObject::AccessibilityChildrenVector ariaErrorMessage;
coreObject->ariaErrorMessageElements(ariaErrorMessage);
for (const auto& accessibilityObject : ariaErrorMessage)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_ERROR_MESSAGE, ATK_OBJECT(accessibilityObject->wrapper()));
// Elements referenced by aria-errormessage should have the error-for relation as per the ARIA AAM spec.
removeAtkRelationByType(relationSet, ATK_RELATION_ERROR_FOR);
AccessibilityObject::AccessibilityChildrenVector errors;
coreObject->ariaErrorMessageReferencingElements(errors);
for (const auto& accessibilityObject : errors)
atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_ERROR_FOR, ATK_OBJECT(accessibilityObject->wrapper()));
#endif
}
static bool isRootObject(AccessibilityObject* coreObject)
{
// The root accessible object in WebCore is always an object with
// the ScrolledArea role with one child with the WebArea role.
if (!coreObject || !coreObject->isScrollView())
return false;
AccessibilityObject* firstChild = coreObject->firstChild();
return firstChild && firstChild->isWebArea();
}
static AtkObject* webkitAccessibleGetParent(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, nullptr);
// Check first if the parent has been already set.
AtkObject* accessibleParent = ATK_OBJECT_CLASS(webkit_accessible_parent_class)->get_parent(object);
if (accessibleParent)
return accessibleParent;
// Parent not set yet, so try to find it in the hierarchy.
auto* coreObject = accessible->priv->object;
auto* coreParent = coreObject->parentObjectUnignored();
if (!coreParent && isRootObject(coreObject)) {
// The top level object claims to not have a parent. This makes it
// impossible for assistive technologies to ascend the accessible
// hierarchy all the way to the application. (Bug 30489)
if (!coreObject->document())
return nullptr;
}
return coreParent ? ATK_OBJECT(coreParent->wrapper()) : nullptr;
}
static gint webkitAccessibleGetNChildren(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, 0);
return accessible->priv->object->children().size();
}
static AtkObject* webkitAccessibleRefChild(AtkObject* object, gint index)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, nullptr);
if (index < 0)
return nullptr;
const auto& children = accessible->priv->object->children();
if (static_cast<size_t>(index) >= children.size())
return nullptr;
auto& coreChild = children[index];
if (!coreChild)
return nullptr;
auto* child = coreChild->wrapper();
if (!child)
return nullptr;
atk_object_set_parent(ATK_OBJECT(child), object);
return ATK_OBJECT(g_object_ref(child));
}
static gint webkitAccessibleGetIndexInParent(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, -1);
auto* coreObject = accessible->priv->object;
auto* parent = coreObject->parentObjectUnignored();
if (!parent && isRootObject(coreObject)) {
if (!coreObject->document())
return -1;
auto* atkParent = parent ? ATK_OBJECT(parent->wrapper()) : nullptr;
if (!atkParent)
return -1;
unsigned count = atk_object_get_n_accessible_children(atkParent);
for (unsigned i = 0; i < count; ++i) {
GRefPtr<AtkObject> child = adoptGRef(atk_object_ref_accessible_child(atkParent, i));
if (child.get() == object)
return i;
}
}
if (!parent)
return -1;
size_t index = parent->children().find(coreObject);
return (index == WTF::notFound) ? -1 : index;
}
static AtkAttributeSet* webkitAccessibleGetAttributes(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, nullptr);
AtkAttributeSet* attributeSet = nullptr;
#if PLATFORM(GTK)
attributeSet = addToAtkAttributeSet(attributeSet, "toolkit", "WebKitGtk");
#elif PLATFORM(WPE)
attributeSet = addToAtkAttributeSet(attributeSet, "toolkit", "WPEWebKit");
#endif
auto* coreObject = accessible->priv->object;
// Hack needed for WebKit2 tests because obtaining an element by its ID
// cannot be done from the UIProcess. Assistive technologies have no need
// for this information.
Element* element = coreObject->element() ? coreObject->element() : coreObject->actionElement();
if (element) {
String tagName = element->tagName();
if (!tagName.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "tag", tagName.convertToASCIILowercase().utf8().data());
String id = element->getIdAttribute().string();
if (!id.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "html-id", id.utf8().data());
}
int level = coreObject->isHeading() ? coreObject->headingLevel() : coreObject->hierarchicalLevel();
if (level) {
String value = String::number(level);
attributeSet = addToAtkAttributeSet(attributeSet, "level", value.utf8().data());
}
if (coreObject->roleValue() == AccessibilityRole::MathElement) {
if (coreObject->isMathMultiscriptObject(AccessibilityMathMultiscriptObjectType::PreSuperscript) || coreObject->isMathMultiscriptObject(AccessibilityMathMultiscriptObjectType::PreSubscript))
attributeSet = addToAtkAttributeSet(attributeSet, "multiscript-type", "pre");
else if (coreObject->isMathMultiscriptObject(AccessibilityMathMultiscriptObjectType::PostSuperscript) || coreObject->isMathMultiscriptObject(AccessibilityMathMultiscriptObjectType::PostSubscript))
attributeSet = addToAtkAttributeSet(attributeSet, "multiscript-type", "post");
}
if (is<AccessibilityTable>(*coreObject) && downcast<AccessibilityTable>(*coreObject).isExposable()) {
auto& table = downcast<AccessibilityTable>(*coreObject);
int rowCount = table.axRowCount();
if (rowCount)
attributeSet = addToAtkAttributeSet(attributeSet, "rowcount", String::number(rowCount).utf8().data());
int columnCount = table.axColumnCount();
if (columnCount)
attributeSet = addToAtkAttributeSet(attributeSet, "colcount", String::number(columnCount).utf8().data());
} else if (is<AccessibilityTableRow>(*coreObject)) {
auto& row = downcast<AccessibilityTableRow>(*coreObject);
int rowIndex = row.axRowIndex();
if (rowIndex != -1)
attributeSet = addToAtkAttributeSet(attributeSet, "rowindex", String::number(rowIndex).utf8().data());
} else if (is<AccessibilityTableCell>(*coreObject)) {
auto& cell = downcast<AccessibilityTableCell>(*coreObject);
int rowIndex = cell.axRowIndex();
if (rowIndex != -1)
attributeSet = addToAtkAttributeSet(attributeSet, "rowindex", String::number(rowIndex).utf8().data());
int columnIndex = cell.axColumnIndex();
if (columnIndex != -1)
attributeSet = addToAtkAttributeSet(attributeSet, "colindex", String::number(columnIndex).utf8().data());
int rowSpan = cell.axRowSpan();
if (rowSpan != -1)
attributeSet = addToAtkAttributeSet(attributeSet, "rowspan", String::number(rowSpan).utf8().data());
int columnSpan = cell.axColumnSpan();
if (columnSpan != -1)
attributeSet = addToAtkAttributeSet(attributeSet, "colspan", String::number(columnSpan).utf8().data());
}
String placeholder = coreObject->placeholderValue();
if (!placeholder.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "placeholder-text", placeholder.utf8().data());
if (coreObject->supportsAutoComplete())
attributeSet = addToAtkAttributeSet(attributeSet, "autocomplete", coreObject->autoCompleteValue().utf8().data());
if (coreObject->supportsHasPopup())
attributeSet = addToAtkAttributeSet(attributeSet, "haspopup", coreObject->popupValue().utf8().data());
if (coreObject->supportsCurrent())
attributeSet = addToAtkAttributeSet(attributeSet, "current", coreObject->currentValue().utf8().data());
// The Core AAM states that an explicitly-set value should be exposed, including "none".
if (coreObject->hasAttribute(HTMLNames::aria_sortAttr)) {
switch (coreObject->sortDirection()) {
case AccessibilitySortDirection::Invalid:
break;
case AccessibilitySortDirection::Ascending:
attributeSet = addToAtkAttributeSet(attributeSet, "sort", "ascending");
break;
case AccessibilitySortDirection::Descending:
attributeSet = addToAtkAttributeSet(attributeSet, "sort", "descending");
break;
case AccessibilitySortDirection::Other:
attributeSet = addToAtkAttributeSet(attributeSet, "sort", "other");
break;
case AccessibilitySortDirection::None:
attributeSet = addToAtkAttributeSet(attributeSet, "sort", "none");
}
}
if (coreObject->supportsPosInSet())
attributeSet = addToAtkAttributeSet(attributeSet, "posinset", String::number(coreObject->posInSet()).utf8().data());
if (coreObject->supportsSetSize())
attributeSet = addToAtkAttributeSet(attributeSet, "setsize", String::number(coreObject->setSize()).utf8().data());
String isReadOnly = coreObject->readOnlyValue();
if (!isReadOnly.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "readonly", isReadOnly.utf8().data());
String valueDescription = coreObject->valueDescription();
if (!valueDescription.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "valuetext", valueDescription.utf8().data());
// According to the W3C Core Accessibility API Mappings 1.1, section 5.4.1 General Rules:
// "User agents must expose the WAI-ARIA role string if the API supports a mechanism to do so."
// In the case of ATK, the mechanism to do so is an object attribute pair (xml-roles:"string").
// We cannot use the computedRoleString for this purpose because it is not limited to elements
// with ARIA roles, and it might not contain the actual ARIA role value (e.g. DPub ARIA).
String roleString = coreObject->getAttribute(HTMLNames::roleAttr);
if (!roleString.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "xml-roles", roleString.utf8().data());
String computedRoleString = coreObject->computedRoleString();
if (!computedRoleString.isEmpty()) {
attributeSet = addToAtkAttributeSet(attributeSet, "computed-role", computedRoleString.utf8().data());
// The HTML AAM maps several elements to ARIA landmark roles. In order for the type of landmark
// to be obtainable in the same fashion as an ARIA landmark, fall back on the computedRoleString.
// We also want to do this for the style-format-group element types so that the type of format
// group it is doesn't get lost to a generic platform role.
if (coreObject->ariaRoleAttribute() == AccessibilityRole::Unknown
&& (coreObject->isLandmark() || coreObject->isStyleFormatGroup()))
attributeSet = addToAtkAttributeSet(attributeSet, "xml-roles", computedRoleString.utf8().data());
}
String roleDescription = coreObject->roleDescription();
if (!roleDescription.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "roledescription", roleDescription.utf8().data());
// We need to expose the live region attributes even if the live region is currently disabled/off.
if (auto liveContainer = coreObject->liveRegionAncestor(false)) {
String liveStatus = liveContainer->liveRegionStatus();
String relevant = liveContainer->liveRegionRelevant();
bool isAtom = liveContainer->liveRegionAtomic();
String liveRole = roleString.isEmpty() ? computedRoleString : roleString;
// According to the Core AAM, we need to expose the above properties with "container-" prefixed
// object attributes regardless of whether the container is this object, or an ancestor of it.
attributeSet = addToAtkAttributeSet(attributeSet, "container-live", liveStatus.utf8().data());
attributeSet = addToAtkAttributeSet(attributeSet, "container-relevant", relevant.utf8().data());
if (isAtom)
attributeSet = addToAtkAttributeSet(attributeSet, "container-atomic", "true");
if (!liveRole.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "container-live-role", liveRole.utf8().data());
// According to the Core AAM, if this object is the live region (rather than its descendant),
// we must expose the above properties on the object without a "container-" prefix.
if (liveContainer == coreObject) {
attributeSet = addToAtkAttributeSet(attributeSet, "live", liveStatus.utf8().data());
attributeSet = addToAtkAttributeSet(attributeSet, "relevant", relevant.utf8().data());
if (isAtom)
attributeSet = addToAtkAttributeSet(attributeSet, "atomic", "true");
} else if (!isAtom && coreObject->liveRegionAtomic())
attributeSet = addToAtkAttributeSet(attributeSet, "atomic", "true");
}
// The Core AAM states the author-provided value should be exposed as-is.
String dropEffect = coreObject->getAttribute(HTMLNames::aria_dropeffectAttr);
if (!dropEffect.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "dropeffect", dropEffect.utf8().data());
if (coreObject->isARIAGrabbed())
attributeSet = addToAtkAttributeSet(attributeSet, "grabbed", "true");
else if (coreObject->supportsARIADragging())
attributeSet = addToAtkAttributeSet(attributeSet, "grabbed", "false");
// The Core AAM states the author-provided value should be exposed as-is.
const AtomString& keyShortcuts = coreObject->keyShortcutsValue();
if (!keyShortcuts.isEmpty())
attributeSet = addToAtkAttributeSet(attributeSet, "keyshortcuts", keyShortcuts.string().utf8().data());
return attributeSet;
}
static AtkRole atkRole(AccessibilityObject* coreObject)
{
switch (coreObject->roleValue()) {
case AccessibilityRole::ApplicationAlert:
return ATK_ROLE_ALERT;
case AccessibilityRole::ApplicationAlertDialog:
case AccessibilityRole::ApplicationDialog:
return ATK_ROLE_DIALOG;
case AccessibilityRole::ApplicationStatus:
return ATK_ROLE_STATUSBAR;
case AccessibilityRole::Unknown:
return ATK_ROLE_UNKNOWN;
case AccessibilityRole::Audio:
return ATK_ROLE_AUDIO;
case AccessibilityRole::Video:
return ATK_ROLE_VIDEO;
case AccessibilityRole::Button:
return ATK_ROLE_PUSH_BUTTON;
case AccessibilityRole::Switch:
case AccessibilityRole::ToggleButton:
return ATK_ROLE_TOGGLE_BUTTON;
case AccessibilityRole::RadioButton:
return ATK_ROLE_RADIO_BUTTON;
case AccessibilityRole::CheckBox:
return ATK_ROLE_CHECK_BOX;
case AccessibilityRole::Slider:
return ATK_ROLE_SLIDER;
case AccessibilityRole::TabGroup:
case AccessibilityRole::TabList:
return ATK_ROLE_PAGE_TAB_LIST;
case AccessibilityRole::TextField:
case AccessibilityRole::TextArea:
case AccessibilityRole::SearchField:
return ATK_ROLE_ENTRY;
case AccessibilityRole::StaticText:
return ATK_ROLE_STATIC;
case AccessibilityRole::Outline:
case AccessibilityRole::Tree:
return ATK_ROLE_TREE;
case AccessibilityRole::TreeItem:
return ATK_ROLE_TREE_ITEM;
case AccessibilityRole::MenuBar:
return ATK_ROLE_MENU_BAR;
case AccessibilityRole::MenuListPopup:
case AccessibilityRole::Menu:
return ATK_ROLE_MENU;
case AccessibilityRole::MenuListOption:
case AccessibilityRole::MenuItem:
case AccessibilityRole::MenuButton:
return ATK_ROLE_MENU_ITEM;
case AccessibilityRole::MenuItemCheckbox:
return ATK_ROLE_CHECK_MENU_ITEM;
case AccessibilityRole::MenuItemRadio:
return ATK_ROLE_RADIO_MENU_ITEM;
case AccessibilityRole::Column:
// return ATK_ROLE_TABLE_COLUMN_HEADER; // Is this right?
return ATK_ROLE_UNKNOWN; // Matches Mozilla
case AccessibilityRole::Row:
return ATK_ROLE_TABLE_ROW;
case AccessibilityRole::Toolbar:
return ATK_ROLE_TOOL_BAR;
case AccessibilityRole::Meter:
return ATK_ROLE_LEVEL_BAR;
case AccessibilityRole::BusyIndicator:
case AccessibilityRole::ProgressIndicator:
return ATK_ROLE_PROGRESS_BAR;
case AccessibilityRole::Window:
return ATK_ROLE_WINDOW;
case AccessibilityRole::PopUpButton:
return coreObject->hasPopup() ? ATK_ROLE_PUSH_BUTTON : ATK_ROLE_COMBO_BOX;
case AccessibilityRole::ComboBox:
return ATK_ROLE_COMBO_BOX;
case AccessibilityRole::SplitGroup:
return ATK_ROLE_SPLIT_PANE;
case AccessibilityRole::Splitter:
return ATK_ROLE_SEPARATOR;
#if PLATFORM(GTK)
case AccessibilityRole::ColorWell:
// ATK_ROLE_COLOR_CHOOSER is defined as a dialog (i.e. it's what appears when you push the button).
return ATK_ROLE_PUSH_BUTTON;
#endif
case AccessibilityRole::List:
return ATK_ROLE_LIST;
case AccessibilityRole::ScrollBar:
return ATK_ROLE_SCROLL_BAR;
case AccessibilityRole::ScrollArea:
case AccessibilityRole::TabPanel:
return ATK_ROLE_SCROLL_PANE;
case AccessibilityRole::Grid:
case AccessibilityRole::Table:
return ATK_ROLE_TABLE;
case AccessibilityRole::TreeGrid:
return ATK_ROLE_TREE_TABLE;
case AccessibilityRole::Application:
return ATK_ROLE_APPLICATION;
case AccessibilityRole::ApplicationGroup:
case AccessibilityRole::Feed:
case AccessibilityRole::Figure:
case AccessibilityRole::GraphicsObject:
case AccessibilityRole::Group:
case AccessibilityRole::RadioGroup:
case AccessibilityRole::SVGRoot:
return ATK_ROLE_PANEL;
case AccessibilityRole::RowHeader:
return ATK_ROLE_ROW_HEADER;
case AccessibilityRole::ColumnHeader:
return ATK_ROLE_COLUMN_HEADER;
case AccessibilityRole::Caption:
return ATK_ROLE_CAPTION;
case AccessibilityRole::Cell:
case AccessibilityRole::GridCell:
return coreObject->inheritsPresentationalRole() ? ATK_ROLE_SECTION : ATK_ROLE_TABLE_CELL;
case AccessibilityRole::Link:
case AccessibilityRole::WebCoreLink:
case AccessibilityRole::ImageMapLink:
return ATK_ROLE_LINK;
case AccessibilityRole::ImageMap:
return ATK_ROLE_IMAGE_MAP;
case AccessibilityRole::GraphicsSymbol:
case AccessibilityRole::Image:
return ATK_ROLE_IMAGE;
case AccessibilityRole::ListMarker:
return ATK_ROLE_TEXT;
case AccessibilityRole::DocumentArticle:
return ATK_ROLE_ARTICLE;
case AccessibilityRole::Document:
case AccessibilityRole::GraphicsDocument:
return ATK_ROLE_DOCUMENT_FRAME;
case AccessibilityRole::DocumentNote:
return ATK_ROLE_COMMENT;
case AccessibilityRole::Heading:
return ATK_ROLE_HEADING;
case AccessibilityRole::ListBox:
// https://rawgit.com/w3c/aria/master/core-aam/core-aam.html#role-map-listbox
return coreObject->isDescendantOfRole(AccessibilityRole::ComboBox) ? ATK_ROLE_MENU : ATK_ROLE_LIST_BOX;
case AccessibilityRole::ListItem:
return coreObject->inheritsPresentationalRole() ? ATK_ROLE_SECTION : ATK_ROLE_LIST_ITEM;
case AccessibilityRole::ListBoxOption:
return coreObject->isDescendantOfRole(AccessibilityRole::ComboBox) ? ATK_ROLE_MENU_ITEM : ATK_ROLE_LIST_ITEM;
case AccessibilityRole::Paragraph:
return ATK_ROLE_PARAGRAPH;
case AccessibilityRole::Label:
case AccessibilityRole::Legend:
return ATK_ROLE_LABEL;
case AccessibilityRole::Blockquote:
return ATK_ROLE_BLOCK_QUOTE;
#if ATK_CHECK_VERSION(2, 25, 2)
case AccessibilityRole::Footnote:
return ATK_ROLE_FOOTNOTE;
#endif
case AccessibilityRole::ApplicationTextGroup:
case AccessibilityRole::Div:
case AccessibilityRole::Pre:
case AccessibilityRole::SVGText:
case AccessibilityRole::TextGroup:
return ATK_ROLE_SECTION;
case AccessibilityRole::Footer:
return ATK_ROLE_FOOTER;
case AccessibilityRole::Form:
if (coreObject->ariaRoleAttribute() != AccessibilityRole::Unknown)
return ATK_ROLE_LANDMARK;
return ATK_ROLE_FORM;
case AccessibilityRole::Canvas:
return ATK_ROLE_CANVAS;
case AccessibilityRole::HorizontalRule:
return ATK_ROLE_SEPARATOR;
case AccessibilityRole::SpinButton:
return ATK_ROLE_SPIN_BUTTON;
case AccessibilityRole::Tab:
return ATK_ROLE_PAGE_TAB;
case AccessibilityRole::UserInterfaceTooltip:
return ATK_ROLE_TOOL_TIP;
case AccessibilityRole::WebArea:
return ATK_ROLE_DOCUMENT_WEB;
case AccessibilityRole::WebApplication:
return ATK_ROLE_EMBEDDED;
case AccessibilityRole::ApplicationLog:
return ATK_ROLE_LOG;
case AccessibilityRole::ApplicationMarquee:
return ATK_ROLE_MARQUEE;
case AccessibilityRole::ApplicationTimer:
return ATK_ROLE_TIMER;
case AccessibilityRole::Definition:
return ATK_ROLE_DEFINITION;
case AccessibilityRole::DocumentMath:
return ATK_ROLE_MATH;
case AccessibilityRole::MathElement:
if (coreObject->isMathRow())
return ATK_ROLE_PANEL;
if (coreObject->isMathTable())
return ATK_ROLE_TABLE;
if (coreObject->isMathTableRow())
return ATK_ROLE_TABLE_ROW;
if (coreObject->isMathTableCell())
return ATK_ROLE_TABLE_CELL;
if (coreObject->isMathSubscriptSuperscript() || coreObject->isMathMultiscript())
return ATK_ROLE_SECTION;
if (coreObject->isMathFraction())
return ATK_ROLE_MATH_FRACTION;
if (coreObject->isMathSquareRoot() || coreObject->isMathRoot())
return ATK_ROLE_MATH_ROOT;
if (coreObject->isMathScriptObject(AccessibilityMathScriptObjectType::Subscript)
|| coreObject->isMathMultiscriptObject(AccessibilityMathMultiscriptObjectType::PreSubscript) || coreObject->isMathMultiscriptObject(AccessibilityMathMultiscriptObjectType::PostSubscript))
return ATK_ROLE_SUBSCRIPT;
if (coreObject->isMathScriptObject(AccessibilityMathScriptObjectType::Superscript)
|| coreObject->isMathMultiscriptObject(AccessibilityMathMultiscriptObjectType::PreSuperscript) || coreObject->isMathMultiscriptObject(AccessibilityMathMultiscriptObjectType::PostSuperscript))
return ATK_ROLE_SUPERSCRIPT;
if (coreObject->isMathToken())
return ATK_ROLE_STATIC;
return ATK_ROLE_UNKNOWN;
case AccessibilityRole::LandmarkBanner:
case AccessibilityRole::LandmarkComplementary:
case AccessibilityRole::LandmarkContentInfo:
case AccessibilityRole::LandmarkDocRegion:
case AccessibilityRole::LandmarkMain:
case AccessibilityRole::LandmarkNavigation:
case AccessibilityRole::LandmarkRegion:
case AccessibilityRole::LandmarkSearch:
return ATK_ROLE_LANDMARK;
case AccessibilityRole::DescriptionList:
return ATK_ROLE_DESCRIPTION_LIST;
case AccessibilityRole::Term:
case AccessibilityRole::DescriptionListTerm:
return ATK_ROLE_DESCRIPTION_TERM;
case AccessibilityRole::DescriptionListDetail:
return ATK_ROLE_DESCRIPTION_VALUE;
case AccessibilityRole::Deletion:
#if ATK_CHECK_VERSION(2, 33, 3)
return ATK_ROLE_CONTENT_DELETION;
#else
return ATK_ROLE_STATIC;
#endif
case AccessibilityRole::Insertion:
#if ATK_CHECK_VERSION(2, 33, 3)
return ATK_ROLE_CONTENT_INSERTION;
#else
return ATK_ROLE_STATIC;
#endif
case AccessibilityRole::Subscript:
return ATK_ROLE_SUBSCRIPT;
case AccessibilityRole::Superscript:
return ATK_ROLE_SUPERSCRIPT;
case AccessibilityRole::Inline:
case AccessibilityRole::SVGTextPath:
case AccessibilityRole::SVGTSpan:
case AccessibilityRole::Time:
return ATK_ROLE_STATIC;
default:
return ATK_ROLE_UNKNOWN;
}
}
static AtkRole webkitAccessibleGetRole(AtkObject* object)
{
// ATK_ROLE_UNKNOWN should only be applied in cases where there is a valid
// WebCore accessible object for which the platform role mapping is unknown.
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, ATK_ROLE_INVALID);
// Note: Why doesn't WebCore have a password field for this
if (accessible->priv->object->isPasswordField())
return ATK_ROLE_PASSWORD_TEXT;
return atkRole(accessible->priv->object);
}
static bool isTextWithCaret(AccessibilityObject* coreObject)
{
if (!coreObject || !coreObject->isAccessibilityRenderObject())
return false;
Document* document = coreObject->document();
if (!document)
return false;
Frame* frame = document->frame();
if (!frame)
return false;
if (!frame->settings().caretBrowsingEnabled())
return false;
// Check text objects and paragraphs only.
auto* axObject = coreObject->wrapper();
AtkRole role = axObject ? atk_object_get_role(ATK_OBJECT(axObject)) : ATK_ROLE_INVALID;
if (role != ATK_ROLE_TEXT && role != ATK_ROLE_PARAGRAPH)
return false;
// Finally, check whether the caret is set in the current object.
VisibleSelection selection = coreObject->selection();
if (!selection.isCaret())
return false;
return selectionBelongsToObject(coreObject, selection);
}
static void setAtkStateSetFromCoreObject(AccessibilityObject* coreObject, AtkStateSet* stateSet)
{
AccessibilityObject* parent = coreObject->parentObject();
bool isListBoxOption = parent && parent->isListBox();
// Please keep the state list in alphabetical order
if ((isListBoxOption && coreObject->isSelectedOptionActive())
|| coreObject->currentState() != AccessibilityCurrentState::False)
atk_state_set_add_state(stateSet, ATK_STATE_ACTIVE);
if (coreObject->isBusy())
atk_state_set_add_state(stateSet, ATK_STATE_BUSY);
if (coreObject->supportsChecked() && coreObject->canSetValueAttribute())
atk_state_set_add_state(stateSet, ATK_STATE_CHECKABLE);
if (coreObject->isChecked())
atk_state_set_add_state(stateSet, ATK_STATE_CHECKED);
if ((coreObject->isTextControl() || coreObject->isNonNativeTextControl()) && coreObject->canSetValueAttribute())
atk_state_set_add_state(stateSet, ATK_STATE_EDITABLE);
// FIXME: Put both ENABLED and SENSITIVE together here for now
if (coreObject->isEnabled()) {
atk_state_set_add_state(stateSet, ATK_STATE_ENABLED);
atk_state_set_add_state(stateSet, ATK_STATE_SENSITIVE);
}
if (coreObject->canSetExpandedAttribute())
atk_state_set_add_state(stateSet, ATK_STATE_EXPANDABLE);
if (coreObject->isExpanded())
atk_state_set_add_state(stateSet, ATK_STATE_EXPANDED);
if (coreObject->canSetFocusAttribute())
atk_state_set_add_state(stateSet, ATK_STATE_FOCUSABLE);
// According to the Core AAM, if the element which is focused has a valid aria-activedescendant,
// we should not expose the focused state on the element which is actually focused, but instead
// on its active descendant.
if ((coreObject->isFocused() && !coreObject->activeDescendant()) || isTextWithCaret(coreObject))
atk_state_set_add_state(stateSet, ATK_STATE_FOCUSED);
else if (coreObject->isActiveDescendantOfFocusedContainer()) {
atk_state_set_add_state(stateSet, ATK_STATE_FOCUSABLE);
atk_state_set_add_state(stateSet, ATK_STATE_FOCUSED);
}
if (coreObject->orientation() == AccessibilityOrientation::Horizontal)
atk_state_set_add_state(stateSet, ATK_STATE_HORIZONTAL);
else if (coreObject->orientation() == AccessibilityOrientation::Vertical)
atk_state_set_add_state(stateSet, ATK_STATE_VERTICAL);
if (coreObject->hasPopup())
atk_state_set_add_state(stateSet, ATK_STATE_HAS_POPUP);
if (coreObject->isIndeterminate())
atk_state_set_add_state(stateSet, ATK_STATE_INDETERMINATE);
else if (coreObject->isCheckboxOrRadio() || coreObject->isMenuItem() || coreObject->isToggleButton()) {
if (coreObject->checkboxOrRadioValue() == AccessibilityButtonState::Mixed)
atk_state_set_add_state(stateSet, ATK_STATE_INDETERMINATE);
}
if (coreObject->isModalNode())
atk_state_set_add_state(stateSet, ATK_STATE_MODAL);
if (coreObject->invalidStatus() != "false")
atk_state_set_add_state(stateSet, ATK_STATE_INVALID_ENTRY);
if (coreObject->isMultiSelectable())
atk_state_set_add_state(stateSet, ATK_STATE_MULTISELECTABLE);
// TODO: ATK_STATE_OPAQUE
if (coreObject->isPressed())
atk_state_set_add_state(stateSet, ATK_STATE_PRESSED);
if (!coreObject->canSetValueAttribute() && (coreObject->supportsReadOnly()))
atk_state_set_add_state(stateSet, ATK_STATE_READ_ONLY);
if (coreObject->isRequired())
atk_state_set_add_state(stateSet, ATK_STATE_REQUIRED);
// TODO: ATK_STATE_SELECTABLE_TEXT
if (coreObject->canSetSelectedAttribute()) {
atk_state_set_add_state(stateSet, ATK_STATE_SELECTABLE);
// Items in focusable lists have both STATE_SELECT{ABLE,ED}
// and STATE_FOCUS{ABLE,ED}. We'll fake the latter based on
// the former.
if (isListBoxOption)
atk_state_set_add_state(stateSet, ATK_STATE_FOCUSABLE);
}
if (coreObject->isSelected()) {
atk_state_set_add_state(stateSet, ATK_STATE_SELECTED);
// Items in focusable lists have both STATE_SELECT{ABLE,ED}
// and STATE_FOCUS{ABLE,ED}. We'll fake the latter based on the
// former.
if (isListBoxOption)
atk_state_set_add_state(stateSet, ATK_STATE_FOCUSED);
}
// FIXME: Group both SHOWING and VISIBLE here for now
// Not sure how to handle this in WebKit, see bug
// http://bugzilla.gnome.org/show_bug.cgi?id=509650 for other
// issues with SHOWING vs VISIBLE.
if (!coreObject->isOffScreen()) {
atk_state_set_add_state(stateSet, ATK_STATE_SHOWING);
atk_state_set_add_state(stateSet, ATK_STATE_VISIBLE);
}
// Mutually exclusive, so we group these two
if (coreObject->roleValue() == AccessibilityRole::TextArea || coreObject->ariaIsMultiline())
atk_state_set_add_state(stateSet, ATK_STATE_MULTI_LINE);
else if (coreObject->roleValue() == AccessibilityRole::TextField || coreObject->roleValue() == AccessibilityRole::SearchField)
atk_state_set_add_state(stateSet, ATK_STATE_SINGLE_LINE);
// TODO: ATK_STATE_SENSITIVE
if (coreObject->supportsAutoComplete() && coreObject->autoCompleteValue() != "none")
atk_state_set_add_state(stateSet, ATK_STATE_SUPPORTS_AUTOCOMPLETION);
if (coreObject->isVisited())
atk_state_set_add_state(stateSet, ATK_STATE_VISITED);
}
static AtkStateSet* webkitAccessibleRefStateSet(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
AtkStateSet* stateSet = ATK_OBJECT_CLASS(webkit_accessible_parent_class)->ref_state_set(object);
// Make sure the layout is updated to really know whether the object
// is defunct or not, so we can return the proper state.
accessible->priv->object->updateBackingStore();
if (accessible->priv->object == fallbackObject()) {
atk_state_set_add_state(stateSet, ATK_STATE_DEFUNCT);
return stateSet;
}
// Text objects must be focusable.
AtkRole role = atk_object_get_role(object);
if (role == ATK_ROLE_TEXT || role == ATK_ROLE_PARAGRAPH)
atk_state_set_add_state(stateSet, ATK_STATE_FOCUSABLE);
setAtkStateSetFromCoreObject(accessible->priv->object, stateSet);
return stateSet;
}
static AtkRelationSet* webkitAccessibleRefRelationSet(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, nullptr);
AtkRelationSet* relationSet = ATK_OBJECT_CLASS(webkit_accessible_parent_class)->ref_relation_set(object);
setAtkRelationSetFromCoreObject(accessible->priv->object, relationSet);
return relationSet;
}
static void webkitAccessibleInit(AtkObject* object, gpointer data)
{
if (ATK_OBJECT_CLASS(webkit_accessible_parent_class)->initialize)
ATK_OBJECT_CLASS(webkit_accessible_parent_class)->initialize(object, data);
WebKitAccessible* accessible = WEBKIT_ACCESSIBLE(object);
accessible->priv->object = reinterpret_cast<AccessibilityObject*>(data);
}
static const gchar* webkitAccessibleGetObjectLocale(AtkObject* object)
{
auto* accessible = WEBKIT_ACCESSIBLE(object);
returnValIfWebKitAccessibleIsInvalid(accessible, nullptr);
if (ATK_IS_DOCUMENT(object)) {
// TODO: Should we fall back on lang xml:lang when the following comes up empty?
String language = accessible->priv->object->language();
if (!language.isEmpty())
return webkitAccessibleCacheAndReturnAtkProperty(accessible, AtkCachedDocumentLocale, language.utf8());
} else if (ATK_IS_TEXT(object)) {
const gchar* locale = nullptr;
AtkAttributeSet* textAttributes = atk_text_get_default_attributes(ATK_TEXT(object));
for (auto* attributes = textAttributes; attributes; attributes = attributes->next) {
auto* atkAttribute = static_cast<AtkAttribute*>(attributes->data);
if (!strcmp(atkAttribute->name, atk_text_attribute_get_name(ATK_TEXT_ATTR_LANGUAGE))) {
locale = webkitAccessibleCacheAndReturnAtkProperty(accessible, AtkCachedDocumentLocale, atkAttribute->value);
break;
}
}
atk_attribute_set_free(textAttributes);
return locale;
}
return nullptr;
}
static void webkit_accessible_class_init(WebKitAccessibleClass* klass)
{
auto* atkObjectClass = ATK_OBJECT_CLASS(klass);
atkObjectClass->initialize = webkitAccessibleInit;
atkObjectClass->get_name = webkitAccessibleGetName;
atkObjectClass->get_description = webkitAccessibleGetDescription;
atkObjectClass->get_parent = webkitAccessibleGetParent;
atkObjectClass->get_n_children = webkitAccessibleGetNChildren;
atkObjectClass->ref_child = webkitAccessibleRefChild;
atkObjectClass->get_role = webkitAccessibleGetRole;
atkObjectClass->ref_state_set = webkitAccessibleRefStateSet;
atkObjectClass->get_index_in_parent = webkitAccessibleGetIndexInParent;
atkObjectClass->get_attributes = webkitAccessibleGetAttributes;
atkObjectClass->ref_relation_set = webkitAccessibleRefRelationSet;
atkObjectClass->get_object_locale = webkitAccessibleGetObjectLocale;
}
static const GInterfaceInfo atkInterfacesInitFunctions[] = {
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleActionInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleSelectionInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleEditableTextInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleTextInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleComponentInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleImageInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleTableInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleTableCellInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleHypertextInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleHyperlinkImplInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleDocumentInterfaceInit)), nullptr, nullptr},
{reinterpret_cast<GInterfaceInitFunc>(reinterpret_cast<GCallback>(webkitAccessibleValueInterfaceInit)), nullptr, nullptr}
};
enum WAIType {
WAIAction,
WAISelection,
WAIEditableText,
WAIText,
WAIComponent,
WAIImage,
WAITable,
WAITableCell,
WAIHypertext,
WAIHyperlink,
WAIDocument,
WAIValue,
};
static GType atkInterfaceTypeFromWAIType(WAIType type)
{
switch (type) {
case WAIAction:
return ATK_TYPE_ACTION;
case WAISelection:
return ATK_TYPE_SELECTION;
case WAIEditableText:
return ATK_TYPE_EDITABLE_TEXT;
case WAIText:
return ATK_TYPE_TEXT;
case WAIComponent:
return ATK_TYPE_COMPONENT;
case WAIImage:
return ATK_TYPE_IMAGE;
case WAITable:
return ATK_TYPE_TABLE;
case WAITableCell:
return ATK_TYPE_TABLE_CELL;
case WAIHypertext:
return ATK_TYPE_HYPERTEXT;
case WAIHyperlink:
return ATK_TYPE_HYPERLINK_IMPL;
case WAIDocument:
return ATK_TYPE_DOCUMENT;
case WAIValue:
return ATK_TYPE_VALUE;
}
return G_TYPE_INVALID;
}
static bool roleIsTextType(AccessibilityRole role)
{
return role == AccessibilityRole::Paragraph
|| role == AccessibilityRole::Heading
|| role == AccessibilityRole::Div
|| role == AccessibilityRole::Cell
|| role == AccessibilityRole::Link
|| role == AccessibilityRole::WebCoreLink
|| role == AccessibilityRole::ListItem
|| role == AccessibilityRole::Pre
|| role == AccessibilityRole::GridCell
|| role == AccessibilityRole::TextGroup
|| role == AccessibilityRole::ApplicationTextGroup
|| role == AccessibilityRole::ApplicationGroup;
}
static guint16 interfaceMaskFromObject(AXCoreObject* coreObject)
{
guint16 interfaceMask = 0;
// Component interface is always supported
interfaceMask |= 1 << WAIComponent;
AccessibilityRole role = coreObject->roleValue();
// Action
// As the implementation of the AtkAction interface is a very
// basic one (just relays in executing the default action for each
// object, and only supports having one action per object), it is
// better just to implement this interface for every instance of
// the WebKitAccessible class and let WebCore decide what to do.
interfaceMask |= 1 << WAIAction;
// Selection
if (coreObject->canHaveSelectedChildren() || coreObject->isMenuList())
interfaceMask |= 1 << WAISelection;
// Get renderer if available.
RenderObject* renderer = nullptr;
if (coreObject->isAccessibilityRenderObject())
renderer = coreObject->renderer();
// Hyperlink (links and embedded objects).
if (coreObject->isLink() || (renderer && renderer->isReplaced()))
interfaceMask |= 1 << WAIHyperlink;
// Text, Editable Text & Hypertext
if (role == AccessibilityRole::StaticText || coreObject->isMenuListOption())
interfaceMask |= 1 << WAIText;
else if (coreObject->isTextControl() || coreObject->isNonNativeTextControl()) {
interfaceMask |= 1 << WAIText;
if (coreObject->canSetValueAttribute())
interfaceMask |= 1 << WAIEditableText;
} else if (!coreObject->isWebArea()) {
if (role != AccessibilityRole::Table) {
interfaceMask |= 1 << WAIHypertext;
if ((renderer && renderer->childrenInline()) || roleIsTextType(role) || coreObject->isMathToken())
interfaceMask |= 1 << WAIText;
}
// Add the TEXT interface for list items whose
// first accessible child has a text renderer
if (role == AccessibilityRole::ListItem) {
const auto& children = coreObject->children();
if (!children.isEmpty())
interfaceMask |= interfaceMaskFromObject(children[0].get());
}
}
// Image
if (coreObject->isImage())
interfaceMask |= 1 << WAIImage;
// Table
if (coreObject->isTable())
interfaceMask |= 1 << WAITable;
if (role == AccessibilityRole::Cell || role == AccessibilityRole::GridCell || role == AccessibilityRole::ColumnHeader || role == AccessibilityRole::RowHeader)
interfaceMask |= 1 << WAITableCell;
// Document
if (role == AccessibilityRole::WebArea)
interfaceMask |= 1 << WAIDocument;
// Value
if (coreObject->supportsRangeValue())
interfaceMask |= 1 << WAIValue;
#if ENABLE(INPUT_TYPE_COLOR)
// Color type.
if (role == AccessibilityRole::ColorWell)
interfaceMask |= 1 << WAIText;
#endif
return interfaceMask;
}
static const char* uniqueAccessibilityTypeName(guint16 interfaceMask)
{
#define WAI_TYPE_NAME_LEN (30) // Enough for prefix + 5 hex characters (max).
static char name[WAI_TYPE_NAME_LEN + 1];
g_sprintf(name, "WAIType%x", interfaceMask);
name[WAI_TYPE_NAME_LEN] = '\0';
return name;
}
static GType accessibilityTypeFromObject(AccessibilityObject* coreObject)
{
static const GTypeInfo typeInfo = {
sizeof(WebKitAccessibleClass),
nullptr, // GBaseInitFunc
nullptr, // GBaseFinalizeFunc
nullptr, // GClassInitFunc
nullptr, // GClassFinalizeFunc
nullptr, // class data
sizeof(WebKitAccessible), // instance size
0, // nb preallocs
nullptr, // GInstanceInitFunc
nullptr // value table
};
guint16 interfaceMask = interfaceMaskFromObject(coreObject);
const char* atkTypeName = uniqueAccessibilityTypeName(interfaceMask);
if (GType type = g_type_from_name(atkTypeName))
return type;
GType type = g_type_register_static(WEBKIT_TYPE_ACCESSIBLE, atkTypeName, &typeInfo, static_cast<GTypeFlags>(0));
for (unsigned i = 0; i < G_N_ELEMENTS(atkInterfacesInitFunctions); ++i) {
if (interfaceMask & (1 << i)) {
g_type_add_interface_static(type,
atkInterfaceTypeFromWAIType(static_cast<WAIType>(i)),
&atkInterfacesInitFunctions[i]);
}
}
return type;
}
WebKitAccessible* webkitAccessibleNew(AccessibilityObject* coreObject)
{
auto* object = ATK_OBJECT(g_object_new(accessibilityTypeFromObject(coreObject), nullptr));
atk_object_initialize(object, coreObject);
return WEBKIT_ACCESSIBLE(object);
}
AccessibilityObject& webkitAccessibleGetAccessibilityObject(WebKitAccessible* accessible)
{
ASSERT(WEBKIT_IS_ACCESSIBLE(accessible));
return *accessible->priv->object;
}
void webkitAccessibleDetach(WebKitAccessible* accessible)
{
ASSERT(WEBKIT_IS_ACCESSIBLE(accessible));
ASSERT(accessible->priv->object != fallbackObject());
if (accessible->priv->object->roleValue() == AccessibilityRole::WebArea)
atk_object_notify_state_change(ATK_OBJECT(accessible), ATK_STATE_DEFUNCT, TRUE);
// We replace the WebCore AccessibilityObject with a fallback object that
// provides default implementations to avoid repetitive null-checking after
// detachment.
accessible->priv->object = fallbackObject();
}
bool webkitAccessibleIsDetached(WebKitAccessible* accessible)
{
ASSERT(WEBKIT_IS_ACCESSIBLE(accessible));
return accessible->priv->object == fallbackObject();
}
const char* webkitAccessibleCacheAndReturnAtkProperty(WebKitAccessible* accessible, AtkCachedProperty property, CString&& value)
{
ASSERT(WEBKIT_IS_ACCESSIBLE(accessible));
WebKitAccessiblePrivate* priv = accessible->priv;
CString* propertyPtr = nullptr;
switch (property) {
case AtkCachedAccessibleName:
propertyPtr = &priv->accessibleName;
break;
case AtkCachedAccessibleDescription:
propertyPtr = &priv->accessibleDescription;
break;
case AtkCachedActionName:
propertyPtr = &priv->actionName;
break;
case AtkCachedActionKeyBinding:
propertyPtr = &priv->actionKeyBinding;
break;
case AtkCachedDocumentLocale:
propertyPtr = &priv->documentLocale;
break;
case AtkCachedDocumentType:
propertyPtr = &priv->documentType;
break;
case AtkCachedDocumentEncoding:
propertyPtr = &priv->documentEncoding;
break;
case AtkCachedDocumentURI:
propertyPtr = &priv->documentURI;
break;
case AtkCachedImageDescription:
propertyPtr = &priv->imageDescription;
break;
default:
ASSERT_NOT_REACHED();
}
// Don't invalidate old memory if not stricly needed, since other
// callers might be still holding on to it.
if (*propertyPtr != value)
*propertyPtr = WTFMove(value);
return (*propertyPtr).data();
}
#endif // ENABLE(ACCESSIBILITY)