/* * Copyright (C) 2011 Apple Inc. All Rights Reserved. * Copyright (C) 2012 Igalia S.L. * Copyright (C) 2013 Samsung Electronics. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "AccessibilityUIElement.h" #if HAVE(ACCESSIBILITY) #include "InjectedBundle.h" #include "InjectedBundlePage.h" #include "NotImplemented.h" #include #include #if ATK_CHECK_VERSION(2,11,90) #include #endif #include #include #include #include #include #include #include #include namespace WTR { namespace { #if ATK_CHECK_VERSION(2,11,92) enum RangeLimit { RangeLimitMinimum, RangeLimitMaximum }; #endif enum AtkAttributeType { ObjectAttributeType, TextAttributeType }; enum AttributeDomain { CoreDomain = 0, AtkDomain }; enum AttributesIndex { // Attribute names. InvalidNameIndex = 0, PosInSetIndex, SetSizeIndex, PlaceholderNameIndex, SortNameIndex, // Attribute values. SortAscendingValueIndex, SortDescendingValueIndex, SortUnknownValueIndex, NumberOfAttributes }; // Attribute names & Values (keep on sync with enum AttributesIndex). const String attributesMap[][2] = { // Attribute names. { "AXInvalid", "invalid" }, { "AXARIAPosInSet", "posinset" }, { "AXARIASetSize", "setsize" }, { "AXPlaceholderValue", "placeholder-text" } , { "AXSortDirection", "sort" }, // Attribute values. { "AXAscendingSortDirection", "ascending" }, { "AXDescendingSortDirection", "descending" }, { "AXUnknownSortDirection", "unknown" } }; #if ATK_CHECK_VERSION(2, 11, 3) const char* landmarkStringBanner = "AXLandmarkBanner"; const char* landmarkStringComplementary = "AXLandmarkComplementary"; const char* landmarkStringContentinfo = "AXLandmarkContentInfo"; const char* landmarkStringMain = "AXLandmarkMain"; const char* landmarkStringNavigation = "AXLandmarkNavigation"; const char* landmarkStringSearch = "AXLandmarkSearch"; #endif String jsStringToWTFString(JSStringRef attribute) { size_t bufferSize = JSStringGetMaximumUTF8CStringSize(attribute); GUniquePtr buffer(static_cast(g_malloc(bufferSize))); JSStringGetUTF8CString(attribute, buffer.get(), bufferSize); return String::fromUTF8(buffer.get()); } String coreAttributeToAtkAttribute(JSStringRef attribute) { String attributeString = jsStringToWTFString(attribute); for (int i = 0; i < NumberOfAttributes; ++i) { if (attributesMap[i][CoreDomain] == attributeString) return attributesMap[i][AtkDomain]; } return attributeString; } String atkAttributeValueToCoreAttributeValue(AtkAttributeType type, const String& id, const String& value) { if (type == ObjectAttributeType) { // We need to translate ATK values exposed for 'aria-sort' (e.g. 'ascending') // into those expected by the layout tests (e.g. 'AXAscendingSortDirection'). if (id == attributesMap[SortNameIndex][AtkDomain] && !value.isEmpty()) { if (value == attributesMap[SortAscendingValueIndex][AtkDomain]) return attributesMap[SortAscendingValueIndex][CoreDomain]; if (value == attributesMap[SortDescendingValueIndex][AtkDomain]) return attributesMap[SortDescendingValueIndex][CoreDomain]; return attributesMap[SortUnknownValueIndex][CoreDomain]; } } else if (type == TextAttributeType) { // In case of 'aria-invalid' when the attribute empty or has "false" for ATK // it should not be mapped at all, but layout tests will expect 'false'. if (id == attributesMap[InvalidNameIndex][AtkDomain] && value.isEmpty()) return "false"; } return value; } AtkAttributeSet* getAttributeSet(AtkObject* accessible, AtkAttributeType type) { if (type == ObjectAttributeType) return atk_object_get_attributes(accessible); if (type == TextAttributeType) { if (!ATK_IS_TEXT(accessible)) return nullptr; return atk_text_get_default_attributes(ATK_TEXT(accessible)); } ASSERT_NOT_REACHED(); return nullptr; } String getAttributeSetValueForId(AtkObject* accessible, AtkAttributeType type, String id) { AtkAttributeSet* attributeSet = getAttributeSet(accessible, type); if (!attributeSet) return String(); String attributeValue; for (AtkAttributeSet* attributes = attributeSet; attributes; attributes = attributes->next) { AtkAttribute* atkAttribute = static_cast(attributes->data); if (id == atkAttribute->name) { attributeValue = String::fromUTF8(atkAttribute->value); break; } } atk_attribute_set_free(attributeSet); return atkAttributeValueToCoreAttributeValue(type, id, attributeValue); } String attributeSetToString(AtkAttributeSet* attributeSet, String separator=", ") { if (!attributeSet) return String(); StringBuilder builder; for (AtkAttributeSet* attributes = attributeSet; attributes; attributes = attributes->next) { AtkAttribute* attribute = static_cast(attributes->data); GUniquePtr attributeData(g_strconcat(attribute->name, ":", attribute->value, NULL)); builder.append(attributeData.get()); if (attributes->next) builder.append(separator); } atk_attribute_set_free(attributeSet); return builder.toString(); } String getAtkAttributeSetAsString(AtkObject* accessible, AtkAttributeType type, String separator=", ") { return attributeSetToString(getAttributeSet(accessible, type), separator); } bool checkElementState(PlatformUIElement element, AtkStateType stateType) { if (!ATK_IS_OBJECT(element.get())) return false; GRefPtr stateSet = adoptGRef(atk_object_ref_state_set(ATK_OBJECT(element.get()))); return atk_state_set_contains_state(stateSet.get(), stateType); } JSStringRef indexRangeInTable(PlatformUIElement element, bool isRowRange) { GUniquePtr rangeString(g_strdup("{0, 0}")); #if ATK_CHECK_VERSION(2,11,90) if (!ATK_IS_TABLE_CELL(element.get())) return JSStringCreateWithUTF8CString(rangeString.get()); #else if (!ATK_IS_OBJECT(element.get())) return JSStringCreateWithUTF8CString(rangeString.get()); AtkObject* axTable = atk_object_get_parent(ATK_OBJECT(element.get())); if (!axTable || !ATK_IS_TABLE(axTable)) return JSStringCreateWithUTF8CString(rangeString.get()); // Look for the cell in the table. gint indexInParent = atk_object_get_index_in_parent(ATK_OBJECT(element.get())); if (indexInParent == -1) return JSStringCreateWithUTF8CString(rangeString.get()); #endif gint row = -1; gint column = -1; gint rowSpan = -1; gint columnSpan = -1; #if ATK_CHECK_VERSION(2,11,90) atk_table_cell_get_row_column_span(ATK_TABLE_CELL(element.get()), &row, &column, &rowSpan, &columnSpan); #else row = atk_table_get_row_at_index(ATK_TABLE(axTable), indexInParent); column = atk_table_get_column_at_index(ATK_TABLE(axTable), indexInParent); rowSpan = atk_table_get_row_extent_at(ATK_TABLE(axTable), row, column); columnSpan = atk_table_get_column_extent_at(ATK_TABLE(axTable), row, column); #endif // Get the actual values, if row and columns are valid values. if (row != -1 && column != -1) { int base = 0; int length = 0; if (isRowRange) { base = row; length = rowSpan; } else { base = column; length = columnSpan; } rangeString.reset(g_strdup_printf("{%d, %d}", base, length)); } return JSStringCreateWithUTF8CString(rangeString.get()); } void alterCurrentValue(PlatformUIElement element, int factor) { if (!ATK_IS_VALUE(element.get())) return; #if ATK_CHECK_VERSION(2,11,92) double currentValue; atk_value_get_value_and_text(ATK_VALUE(element.get()), ¤tValue, nullptr); double increment = atk_value_get_increment(ATK_VALUE(element.get())); atk_value_set_value(ATK_VALUE(element.get()), currentValue + factor * increment); #else GValue currentValue = G_VALUE_INIT; atk_value_get_current_value(ATK_VALUE(element.get()), ¤tValue); GValue increment = G_VALUE_INIT; atk_value_get_minimum_increment(ATK_VALUE(element.get()), &increment); GValue newValue = G_VALUE_INIT; g_value_init(&newValue, G_TYPE_FLOAT); g_value_set_float(&newValue, g_value_get_float(¤tValue) + factor * g_value_get_float(&increment)); atk_value_set_current_value(ATK_VALUE(element.get()), &newValue); g_value_unset(&newValue); g_value_unset(&increment); g_value_unset(¤tValue); #endif } gchar* replaceCharactersForResults(gchar* str) { WTF::String uString = WTF::String::fromUTF8(str); // The object replacement character is passed along to ATs so we need to be // able to test for their presence and do so without causing test failures. uString.replace(objectReplacementCharacter, ""); // The presence of newline characters in accessible text of a single object // is appropriate, but it makes test results (especially the accessible tree) // harder to read. uString.replace("\n", "<\\n>"); return g_strdup(uString.utf8().data()); } const gchar* roleToString(AtkObject* object) { AtkRole role = atk_object_get_role(object); #if ATK_CHECK_VERSION(2, 11, 3) if (role == ATK_ROLE_LANDMARK) { String xmlRolesValue = getAttributeSetValueForId(object, ObjectAttributeType, "xml-roles"); if (equalLettersIgnoringASCIICase(xmlRolesValue, "banner")) return landmarkStringBanner; if (equalLettersIgnoringASCIICase(xmlRolesValue, "complementary")) return landmarkStringComplementary; if (equalLettersIgnoringASCIICase(xmlRolesValue, "contentinfo")) return landmarkStringContentinfo; if (equalLettersIgnoringASCIICase(xmlRolesValue, "main")) return landmarkStringMain; if (equalLettersIgnoringASCIICase(xmlRolesValue, "navigation")) return landmarkStringNavigation; if (equalLettersIgnoringASCIICase(xmlRolesValue, "search")) return landmarkStringSearch; } #endif switch (role) { case ATK_ROLE_ALERT: return "AXAlert"; case ATK_ROLE_DIALOG: return "AXDialog"; case ATK_ROLE_CANVAS: return "AXCanvas"; case ATK_ROLE_CAPTION: return "AXCaption"; case ATK_ROLE_CHECK_BOX: return "AXCheckBox"; case ATK_ROLE_COLOR_CHOOSER: return "AXColorWell"; case ATK_ROLE_COLUMN_HEADER: return "AXColumnHeader"; case ATK_ROLE_COMBO_BOX: return "AXComboBox"; case ATK_ROLE_COMMENT: return "AXComment"; case ATK_ROLE_DOCUMENT_FRAME: return "AXDocument"; case ATK_ROLE_DOCUMENT_WEB: return "AXWebArea"; case ATK_ROLE_EMBEDDED: return "AXEmbedded"; case ATK_ROLE_ENTRY: return "AXTextField"; case ATK_ROLE_FOOTER: return "AXFooter"; case ATK_ROLE_FORM: return "AXForm"; case ATK_ROLE_GROUPING: return "AXGroup"; case ATK_ROLE_HEADING: return "AXHeading"; case ATK_ROLE_IMAGE: return "AXImage"; case ATK_ROLE_IMAGE_MAP: return "AXImageMap"; case ATK_ROLE_INVALID: return "AXInvalid"; case ATK_ROLE_LABEL: return "AXLabel"; case ATK_ROLE_LEVEL_BAR: return "AXProgressIndicator"; case ATK_ROLE_LINK: return "AXLink"; case ATK_ROLE_LIST: return "AXList"; case ATK_ROLE_LIST_BOX: return "AXListBox"; case ATK_ROLE_LIST_ITEM: return "AXListItem"; case ATK_ROLE_MENU: return "AXMenu"; case ATK_ROLE_MENU_BAR: return "AXMenuBar"; case ATK_ROLE_MENU_ITEM: return "AXMenuItem"; case ATK_ROLE_PAGE_TAB: return "AXTab"; case ATK_ROLE_PAGE_TAB_LIST: return "AXTabGroup"; case ATK_ROLE_PANEL: return "AXGroup"; case ATK_ROLE_PARAGRAPH: return "AXParagraph"; case ATK_ROLE_PASSWORD_TEXT: return "AXPasswordField"; case ATK_ROLE_PROGRESS_BAR: return "AXProgressIndicator"; case ATK_ROLE_PUSH_BUTTON: return "AXButton"; case ATK_ROLE_RADIO_BUTTON: return "AXRadioButton"; case ATK_ROLE_RADIO_MENU_ITEM: return "AXRadioMenuItem"; case ATK_ROLE_ROW_HEADER: return "AXRowHeader"; case ATK_ROLE_CHECK_MENU_ITEM: return "AXCheckMenuItem"; case ATK_ROLE_RULER: return "AXRuler"; case ATK_ROLE_SCROLL_BAR: return "AXScrollBar"; case ATK_ROLE_SCROLL_PANE: return "AXScrollArea"; case ATK_ROLE_SECTION: return "AXSection"; case ATK_ROLE_SEPARATOR: return "AXSeparator"; case ATK_ROLE_SLIDER: return "AXSlider"; case ATK_ROLE_SPIN_BUTTON: return "AXSpinButton"; case ATK_ROLE_STATUSBAR: return "AXStatusBar"; case ATK_ROLE_TABLE: return "AXTable"; case ATK_ROLE_TABLE_CELL: return "AXCell"; case ATK_ROLE_TABLE_COLUMN_HEADER: return "AXColumnHeader"; case ATK_ROLE_TABLE_ROW: return "AXRow"; case ATK_ROLE_TABLE_ROW_HEADER: return "AXRowHeader"; case ATK_ROLE_TOGGLE_BUTTON: return "AXToggleButton"; case ATK_ROLE_TOOL_BAR: return "AXToolbar"; case ATK_ROLE_TOOL_TIP: return "AXUserInterfaceTooltip"; case ATK_ROLE_TREE: return "AXTree"; case ATK_ROLE_TREE_TABLE: return "AXTreeGrid"; case ATK_ROLE_TREE_ITEM: return "AXTreeItem"; case ATK_ROLE_WINDOW: return "AXWindow"; case ATK_ROLE_UNKNOWN: return "AXUnknown"; #if ATK_CHECK_VERSION(2, 11, 3) case ATK_ROLE_ARTICLE: return "AXArticle"; case ATK_ROLE_AUDIO: return "AXAudio"; case ATK_ROLE_BLOCK_QUOTE: return "AXBlockquote"; case ATK_ROLE_DEFINITION: return "AXDefinition"; case ATK_ROLE_LOG: return "AXLog"; case ATK_ROLE_MARQUEE: return "AXMarquee"; case ATK_ROLE_MATH: return "AXMath"; case ATK_ROLE_TIMER: return "AXTimer"; case ATK_ROLE_VIDEO: return "AXVideo"; #endif #if ATK_CHECK_VERSION(2, 11, 4) case ATK_ROLE_DESCRIPTION_LIST: return "AXDescriptionList"; case ATK_ROLE_DESCRIPTION_TERM: return "AXDescriptionTerm"; case ATK_ROLE_DESCRIPTION_VALUE: return "AXDescriptionValue"; #endif #if ATK_CHECK_VERSION(2, 15, 2) case ATK_ROLE_STATIC: return "AXStatic"; #endif #if ATK_CHECK_VERSION(2, 15, 4) case ATK_ROLE_MATH_FRACTION: return "AXMathFraction"; case ATK_ROLE_MATH_ROOT: return "AXMathRoot"; case ATK_ROLE_SUBSCRIPT: return "AXSubscript"; case ATK_ROLE_SUPERSCRIPT: return "AXSuperscript"; #endif default: // We want to distinguish ATK_ROLE_UNKNOWN from a known AtkRole which // our DRT isn't properly handling. return "FIXME not identified"; } } String selectedText(AtkObject* accessible) { if (!ATK_IS_TEXT(accessible)) return String(); AtkText* text = ATK_TEXT(accessible); gint start, end; g_free(atk_text_get_selection(text, 0, &start, &end)); return atk_text_get_text(text, start, end); } String attributesOfElement(AccessibilityUIElement* element) { StringBuilder builder; builder.append(String::format("%s\n", element->role()->string().utf8().data())); // For the parent we print its role and its name, if available. builder.append("AXParent: "); RefPtr parent = element->parentElement(); AtkObject* atkParent = parent ? parent->platformUIElement().get() : nullptr; if (atkParent) { builder.append(roleToString(atkParent)); const char* parentName = atk_object_get_name(atkParent); if (parentName && g_utf8_strlen(parentName, -1)) builder.append(String::format(": %s", parentName)); } else builder.append("(null)"); builder.append("\n"); builder.append(String::format("AXChildren: %d\n", element->childrenCount())); builder.append(String::format("AXPosition: { %f, %f }\n", element->x(), element->y())); builder.append(String::format("AXSize: { %f, %f }\n", element->width(), element->height())); String title = element->title()->string(); if (!title.isEmpty()) builder.append(String::format("%s\n", title.utf8().data())); String description = element->description()->string(); if (!description.isEmpty()) builder.append(String::format("%s\n", description.utf8().data())); String value = element->stringValue()->string(); if (!value.isEmpty()) builder.append(String::format("%s\n", value.utf8().data())); builder.append(String::format("AXFocusable: %d\n", element->isFocusable())); builder.append(String::format("AXFocused: %d\n", element->isFocused())); builder.append(String::format("AXSelectable: %d\n", element->isSelectable())); builder.append(String::format("AXSelected: %d\n", element->isSelected())); builder.append(String::format("AXMultiSelectable: %d\n", element->isMultiSelectable())); builder.append(String::format("AXEnabled: %d\n", element->isEnabled())); builder.append(String::format("AXExpanded: %d\n", element->isExpanded())); builder.append(String::format("AXRequired: %d\n", element->isRequired())); builder.append(String::format("AXChecked: %d\n", element->isChecked())); String url = element->url()->string(); if (!url.isEmpty()) builder.append(String::format("%s\n", url.utf8().data())); // We append the ATK specific attributes as a single line at the end. builder.append("AXPlatformAttributes: "); builder.append(getAtkAttributeSetAsString(element->platformUIElement().get(), ObjectAttributeType)); return builder.toString(); } static JSRetainPtr createStringWithAttributes(const Vector >& elements) { StringBuilder builder; for (Vector >::const_iterator it = elements.begin(); it != elements.end(); ++it) { builder.append(attributesOfElement(const_cast(it->get()))); builder.append("\n------------\n"); } return JSStringCreateWithUTF8CString(builder.toString().utf8().data()); } static Vector > getRowHeaders(AtkTable* accessible) { Vector > rowHeaders; int rowsCount = atk_table_get_n_rows(accessible); for (int row = 0; row < rowsCount; ++row) rowHeaders.append(AccessibilityUIElement::create(atk_table_get_row_header(accessible, row))); return rowHeaders; } static Vector > getColumnHeaders(AtkTable* accessible) { Vector > columnHeaders; int columnsCount = atk_table_get_n_columns(accessible); for (int column = 0; column < columnsCount; ++column) columnHeaders.append(AccessibilityUIElement::create(atk_table_get_column_header(accessible, column))); return columnHeaders; } static Vector > getVisibleCells(AccessibilityUIElement* element) { Vector > visibleCells; AtkTable* accessible = ATK_TABLE(element->platformUIElement().get()); int rowsCount = atk_table_get_n_rows(accessible); int columnsCount = atk_table_get_n_columns(accessible); for (int row = 0; row < rowsCount; ++row) { for (int column = 0; column < columnsCount; ++column) visibleCells.append(element->cellForColumnAndRow(column, row)); } return visibleCells; } #if ATK_CHECK_VERSION(2,11,90) static Vector> convertGPtrArrayToVector(const GPtrArray* array) { Vector> cells; for (guint i = 0; i < array->len; i++) { if (AtkObject* atkObject = static_cast(g_ptr_array_index(array, i))) cells.append(AccessibilityUIElement::create(atkObject)); } return cells; } static JSValueRef convertToJSObjectArray(const Vector>& children) { WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::singleton().page()->page()); JSContextRef context = WKBundleFrameGetJavaScriptContext(mainFrame); size_t elementCount = children.size(); auto valueElements = std::make_unique(elementCount); for (size_t i = 0; i < elementCount; i++) valueElements[i] = JSObjectMake(context, children[i]->wrapperClass(), children[i].get()); return JSObjectMakeArray(context, elementCount, valueElements.get(), nullptr); } #endif #if ATK_CHECK_VERSION(2,11,92) static double rangeMinMaxValue(AtkValue* atkValue, RangeLimit rangeLimit) { AtkRange* range = atk_value_get_range(atkValue); if (!range) return 0; double rangeValue = 0; switch (rangeLimit) { case RangeLimitMinimum: rangeValue = atk_range_get_lower_limit(range); break; case RangeLimitMaximum: rangeValue = atk_range_get_upper_limit(range); break; }; atk_range_free(range); return rangeValue; } #endif } // namespace AccessibilityUIElement::AccessibilityUIElement(PlatformUIElement element) : m_element(element) { } AccessibilityUIElement::AccessibilityUIElement(const AccessibilityUIElement& other) : JSWrappable() , m_element(other.m_element) { } AccessibilityUIElement::~AccessibilityUIElement() { } bool AccessibilityUIElement::isEqual(AccessibilityUIElement* otherElement) { return m_element == otherElement->platformUIElement(); } void AccessibilityUIElement::getChildren(Vector >& children) { if (!ATK_IS_OBJECT(m_element.get())) return; int count = childrenCount(); for (int i = 0; i < count; i++) { GRefPtr child = adoptGRef(atk_object_ref_accessible_child(ATK_OBJECT(m_element.get()), i)); children.append(AccessibilityUIElement::create(child.get())); } } void AccessibilityUIElement::getChildrenWithRange(Vector >& children, unsigned location, unsigned length) { if (!ATK_IS_OBJECT(m_element.get())) return; unsigned end = location + length; for (unsigned i = location; i < end; i++) { GRefPtr child = adoptGRef(atk_object_ref_accessible_child(ATK_OBJECT(m_element.get()), i)); children.append(AccessibilityUIElement::create(child.get())); } } int AccessibilityUIElement::childrenCount() { if (!ATK_IS_OBJECT(m_element.get())) return 0; return atk_object_get_n_accessible_children(ATK_OBJECT(m_element.get())); } PassRefPtr AccessibilityUIElement::elementAtPoint(int x, int y) { if (!ATK_IS_COMPONENT(m_element.get())) return nullptr; GRefPtr objectAtPoint = adoptGRef(atk_component_ref_accessible_at_point(ATK_COMPONENT(m_element.get()), x, y, ATK_XY_WINDOW)); return AccessibilityUIElement::create(objectAtPoint ? objectAtPoint.get() : m_element.get()); } unsigned AccessibilityUIElement::indexOfChild(AccessibilityUIElement* element) { if (!ATK_IS_OBJECT(m_element.get())) return 0; Vector > children; getChildren(children); unsigned count = children.size(); for (unsigned i = 0; i < count; i++) if (children[i]->isEqual(element)) return i; return 0; } PassRefPtr AccessibilityUIElement::childAtIndex(unsigned index) { if (!ATK_IS_OBJECT(m_element.get())) return nullptr; Vector > children; getChildrenWithRange(children, index, 1); if (children.size() == 1) return children[0]; return nullptr; } static PassRefPtr accessibilityElementAtIndex(AtkObject* element, AtkRelationType relationType, unsigned index) { if (!ATK_IS_OBJECT(element)) return nullptr; AtkRelationSet* relationSet = atk_object_ref_relation_set(element); if (!relationSet) return nullptr; AtkRelation* relation = atk_relation_set_get_relation_by_type(relationSet, relationType); if (!relation) return nullptr; GPtrArray* targetList = atk_relation_get_target(relation); if (!targetList || !targetList->len || index >= targetList->len) return nullptr; AtkObject* target = static_cast(g_ptr_array_index(targetList, index)); g_object_unref(relationSet); return target ? AccessibilityUIElement::create(target) : nullptr; } PassRefPtr AccessibilityUIElement::linkedUIElementAtIndex(unsigned index) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::ariaOwnsElementAtIndex(unsigned index) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::ariaFlowToElementAtIndex(unsigned index) { return accessibilityElementAtIndex(m_element.get(), ATK_RELATION_FLOWS_TO, index); } PassRefPtr AccessibilityUIElement::ariaControlsElementAtIndex(unsigned index) { return accessibilityElementAtIndex(m_element.get(), ATK_RELATION_CONTROLLER_FOR, index); } PassRefPtr AccessibilityUIElement::disclosedRowAtIndex(unsigned index) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::rowAtIndex(unsigned index) { // ATK doesn't have API to get an accessible row by index directly. It does, however, have // API to get cells in the row specified by index. The parent of a cell should be the row. AtkTable* axTable = ATK_TABLE(m_element.get()); unsigned nColumns = columnCount(); for (unsigned col = 0; col < nColumns; col++) { // Find the first cell in this row that only spans one row. if (atk_table_get_row_extent_at(axTable, index, col) == 1) { AtkObject* cell = atk_table_ref_at(axTable, index, col); return cell ? AccessibilityUIElement::create(atk_object_get_parent(cell)) : nullptr; } } return nullptr; } PassRefPtr AccessibilityUIElement::selectedChildAtIndex(unsigned index) const { if (!ATK_SELECTION(m_element.get())) return nullptr; GRefPtr child = adoptGRef(atk_selection_ref_selection(ATK_SELECTION(m_element.get()), index)); return child ? AccessibilityUIElement::create(child.get()) : nullptr; } unsigned AccessibilityUIElement::selectedChildrenCount() const { if (!ATK_IS_SELECTION(m_element.get())) return 0; return atk_selection_get_selection_count(ATK_SELECTION(m_element.get())); } PassRefPtr AccessibilityUIElement::selectedRowAtIndex(unsigned index) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::titleUIElement() { if (!ATK_IS_OBJECT(m_element.get())) return nullptr; AtkRelationSet* set = atk_object_ref_relation_set(ATK_OBJECT(m_element.get())); if (!set) return nullptr; AtkObject* target = nullptr; int count = atk_relation_set_get_n_relations(set); for (int i = 0; i < count; i++) { AtkRelation* relation = atk_relation_set_get_relation(set, i); if (atk_relation_get_relation_type(relation) == ATK_RELATION_LABELLED_BY) { GPtrArray* targetList = atk_relation_get_target(relation); if (targetList->len) target = static_cast(g_ptr_array_index(targetList, 0)); } } g_object_unref(set); return target ? AccessibilityUIElement::create(target) : nullptr; } PassRefPtr AccessibilityUIElement::parentElement() { if (!ATK_IS_OBJECT(m_element.get())) return nullptr; AtkObject* parent = atk_object_get_parent(ATK_OBJECT(m_element.get())); return parent ? AccessibilityUIElement::create(parent) : nullptr; } PassRefPtr AccessibilityUIElement::disclosedByRow() { // FIXME: implement return nullptr; } JSRetainPtr AccessibilityUIElement::attributesOfLinkedUIElements() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::attributesOfDocumentLinks() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::attributesOfChildren() { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); Vector > children; getChildren(children); return createStringWithAttributes(children); } JSRetainPtr AccessibilityUIElement::allAttributes() { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); return JSStringCreateWithUTF8CString(attributesOfElement(this).utf8().data()); } JSRetainPtr AccessibilityUIElement::stringAttributeValue(JSStringRef attribute) { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); String atkAttributeName = coreAttributeToAtkAttribute(attribute); // The value of AXSelectedText is not exposed through any AtkAttribute. if (atkAttributeName == "AXSelectedText") { String string = selectedText(m_element.get()); return JSStringCreateWithUTF8CString(string.utf8().data()); } // Try object attributes before text attributes. String attributeValue = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), ObjectAttributeType, atkAttributeName); // Try text attributes if the requested one was not found and we have an AtkText object. if (attributeValue.isEmpty() && ATK_IS_TEXT(m_element.get())) attributeValue = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), TextAttributeType, atkAttributeName); // Additional check to make sure that the exposure of the state ATK_STATE_INVALID_ENTRY // is consistent with the exposure of aria-invalid as a text attribute, if present. if (atkAttributeName == attributesMap[InvalidNameIndex][AtkDomain]) { bool isInvalidState = checkElementState(m_element.get(), ATK_STATE_INVALID_ENTRY); if (attributeValue.isEmpty()) return JSStringCreateWithUTF8CString(isInvalidState ? "true" : "false"); // If the text attribute was there, check that it's consistent with // what the state says or force the test to fail otherwise. bool isAriaInvalid = attributeValue != "false"; if (isInvalidState != isAriaInvalid) return JSStringCreateWithCharacters(0, 0); } return JSStringCreateWithUTF8CString(attributeValue.utf8().data()); } double AccessibilityUIElement::numberAttributeValue(JSStringRef attribute) { if (!ATK_IS_OBJECT(m_element.get())) return 0; String atkAttributeName = coreAttributeToAtkAttribute(attribute); if (atkAttributeName.isEmpty()) return 0; if (atkAttributeName == "setsize" || atkAttributeName == "posinset") { String attributeValue = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), ObjectAttributeType, atkAttributeName); if (!attributeValue.isEmpty()) return attributeValue.toDouble(); } return 0; } JSValueRef AccessibilityUIElement::uiElementArrayAttributeValue(JSStringRef attribute) const { // FIXME: implement return nullptr; } JSValueRef AccessibilityUIElement::rowHeaders() const { #if ATK_CHECK_VERSION(2,11,90) if (!ATK_IS_TABLE_CELL(m_element.get())) return nullptr; GRefPtr array = adoptGRef(atk_table_cell_get_row_header_cells(ATK_TABLE_CELL(m_element.get()))); if (!array) return nullptr; Vector> rows = convertGPtrArrayToVector(array.get()); return convertToJSObjectArray(rows); #else return nullptr; #endif } JSValueRef AccessibilityUIElement::columnHeaders() const { #if ATK_CHECK_VERSION(2,11,90) if (!ATK_IS_TABLE_CELL(m_element.get()) && !ATK_IS_TABLE(m_element.get())) return nullptr; Vector> columns; if (ATK_IS_TABLE_CELL(m_element.get())) { GRefPtr array = adoptGRef(atk_table_cell_get_column_header_cells(ATK_TABLE_CELL(m_element.get()))); if (!array) return nullptr; columns = convertGPtrArrayToVector(array.get()); } else columns = getColumnHeaders(ATK_TABLE(m_element.get())); return convertToJSObjectArray(columns); #else return nullptr; #endif } PassRefPtr AccessibilityUIElement::uiElementAttributeValue(JSStringRef attribute) const { // FIXME: implement return nullptr; } bool AccessibilityUIElement::boolAttributeValue(JSStringRef attribute) { // FIXME: implement return false; } bool AccessibilityUIElement::isAttributeSettable(JSStringRef attribute) { if (!ATK_IS_OBJECT(m_element.get())) return false; String attributeString = jsStringToWTFString(attribute); if (attributeString != "AXValue") return false; // ATK does not have a single state or property to indicate whether or not the value // of an accessible object can be set. ATs look at several states and properties based // on the type of object. If nothing explicitly indicates the value can or cannot be // set, ATs make role- and interface-based decisions. We'll do something similar here. // This state is expected to be present only for text widgets and contenteditable elements. if (checkElementState(m_element.get(), ATK_STATE_EDITABLE)) return true; #if ATK_CHECK_VERSION(2,11,2) // This state is applicable to checkboxes, radiobuttons, switches, etc. if (checkElementState(m_element.get(), ATK_STATE_CHECKABLE)) return true; #endif #if ATK_CHECK_VERSION(2,15,3) // This state is expected to be present only for controls and only if explicitly set. if (checkElementState(m_element.get(), ATK_STATE_READ_ONLY)) return false; #endif // We expose an object attribute to ATs when there is an author-provided ARIA property // and also when there is a supported ARIA role but no author-provided value. String isReadOnly = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), ObjectAttributeType, "readonly"); if (!isReadOnly.isEmpty()) return isReadOnly == "true" ? false : true; // If we have a native listbox or combobox and the value can be set, the options should // have ATK_STATE_SELECTABLE. AtkRole role = atk_object_get_role(ATK_OBJECT(m_element.get())); if (role == ATK_ROLE_LIST_BOX || role == ATK_ROLE_COMBO_BOX) { if (GRefPtr child = adoptGRef(atk_object_ref_accessible_child(ATK_OBJECT(m_element.get()), 0))) { if (atk_object_get_role(ATK_OBJECT(child.get())) == ATK_ROLE_MENU) child = adoptGRef(atk_object_ref_accessible_child(ATK_OBJECT(child.get()), 0)); return child && checkElementState(child.get(), ATK_STATE_SELECTABLE); } } // If we have a native element which exposes a range whose value can be set, it should // be focusable and have a true range. if (ATK_IS_VALUE(m_element.get()) && checkElementState(m_element.get(), ATK_STATE_FOCUSABLE)) return minValue() != maxValue(); return false; } bool AccessibilityUIElement::isAttributeSupported(JSStringRef attribute) { if (!ATK_IS_OBJECT(m_element.get())) return false; String atkAttributeName = coreAttributeToAtkAttribute(attribute); if (atkAttributeName.isEmpty()) return false; // For now, an attribute is supported whether it's exposed as a object or a text attribute. String attributeValue = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), ObjectAttributeType, atkAttributeName); if (attributeValue.isEmpty()) attributeValue = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), TextAttributeType, atkAttributeName); return !attributeValue.isEmpty(); } JSRetainPtr AccessibilityUIElement::parameterizedAttributeNames() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::role() { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); GUniquePtr roleStringWithPrefix(g_strdup_printf("AXRole: %s", roleToString(ATK_OBJECT(m_element.get())))); return JSStringCreateWithUTF8CString(roleStringWithPrefix.get()); } JSRetainPtr AccessibilityUIElement::subrole() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::roleDescription() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::computedRoleString() { String role = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), ObjectAttributeType, "computed-role"); if (!role.isEmpty()) return JSStringCreateWithUTF8CString(role.utf8().data()); return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::title() { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); const gchar* name = atk_object_get_name(ATK_OBJECT(m_element.get())); GUniquePtr axTitle(g_strdup_printf("AXTitle: %s", name ? name : "")); return JSStringCreateWithUTF8CString(axTitle.get()); } JSRetainPtr AccessibilityUIElement::description() { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); const gchar* description = atk_object_get_description(ATK_OBJECT(m_element.get())); if (!description) return JSStringCreateWithCharacters(0, 0); GUniquePtr axDesc(g_strdup_printf("AXDescription: %s", description)); return JSStringCreateWithUTF8CString(axDesc.get()); } JSRetainPtr AccessibilityUIElement::orientation() const { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); const gchar* axOrientation = nullptr; if (checkElementState(m_element.get(), ATK_STATE_HORIZONTAL)) axOrientation = "AXOrientation: AXHorizontalOrientation"; else if (checkElementState(m_element.get(), ATK_STATE_VERTICAL)) axOrientation = "AXOrientation: AXVerticalOrientation"; if (!axOrientation) return JSStringCreateWithCharacters(0, 0); return JSStringCreateWithUTF8CString(axOrientation); } JSRetainPtr AccessibilityUIElement::stringValue() { if (!ATK_IS_TEXT(m_element.get())) return JSStringCreateWithCharacters(0, 0); GUniquePtr text(atk_text_get_text(ATK_TEXT(m_element.get()), 0, -1)); GUniquePtr textWithReplacedCharacters(replaceCharactersForResults(text.get())); GUniquePtr axValue(g_strdup_printf("AXValue: %s", textWithReplacedCharacters.get())); return JSStringCreateWithUTF8CString(axValue.get()); } JSRetainPtr AccessibilityUIElement::language() { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); const gchar* locale = atk_object_get_object_locale(ATK_OBJECT(m_element.get())); if (!locale) return JSStringCreateWithCharacters(0, 0); GUniquePtr axValue(g_strdup_printf("AXLanguage: %s", locale)); return JSStringCreateWithUTF8CString(axValue.get()); } JSRetainPtr AccessibilityUIElement::helpText() const { if (!ATK_IS_OBJECT(m_element.get())) return JSStringCreateWithCharacters(0, 0); AtkRelationSet* relationSet = atk_object_ref_relation_set(ATK_OBJECT(m_element.get())); if (!relationSet) return JSStringCreateWithCharacters(0, 0); AtkRelation* relation = atk_relation_set_get_relation_by_type(relationSet, ATK_RELATION_DESCRIBED_BY); if (!relation) return JSStringCreateWithCharacters(0, 0); GPtrArray* targetList = atk_relation_get_target(relation); if (!targetList || !targetList->len) return JSStringCreateWithCharacters(0, 0); StringBuilder builder; builder.append("AXHelp: "); for (int targetCount = 0; targetCount < targetList->len; targetCount++) { if (AtkObject* target = static_cast(g_ptr_array_index(targetList, targetCount))) { GUniquePtr text(atk_text_get_text(ATK_TEXT(target), 0, -1)); if (targetCount) builder.append(" "); builder.append(text.get()); } } g_object_unref(relationSet); return JSStringCreateWithUTF8CString(builder.toString().utf8().data()); } double AccessibilityUIElement::x() { if (!ATK_IS_COMPONENT(m_element.get())) return 0; int x; #if ATK_CHECK_VERSION(2,11,90) atk_component_get_extents(ATK_COMPONENT(m_element.get()), &x, nullptr, nullptr, nullptr, ATK_XY_SCREEN); #else atk_component_get_position(ATK_COMPONENT(m_element.get()), &x, nullptr, ATK_XY_SCREEN); #endif return x; } double AccessibilityUIElement::y() { if (!ATK_IS_COMPONENT(m_element.get())) return 0; int y; #if ATK_CHECK_VERSION(2,11,90) atk_component_get_extents(ATK_COMPONENT(m_element.get()), nullptr, &y, nullptr, nullptr, ATK_XY_SCREEN); #else atk_component_get_position(ATK_COMPONENT(m_element.get()), nullptr, &y, ATK_XY_SCREEN); #endif return y; } double AccessibilityUIElement::width() { if (!ATK_IS_COMPONENT(m_element.get())) return 0; int width; #if ATK_CHECK_VERSION(2,11,90) atk_component_get_extents(ATK_COMPONENT(m_element.get()), nullptr, nullptr, &width, nullptr, ATK_XY_WINDOW); #else atk_component_get_size(ATK_COMPONENT(m_element.get()), &width, nullptr); #endif return width; } double AccessibilityUIElement::height() { if (!ATK_IS_COMPONENT(m_element.get())) return 0; int height; #if ATK_CHECK_VERSION(2,11,90) atk_component_get_extents(ATK_COMPONENT(m_element.get()), nullptr, nullptr, nullptr, &height, ATK_XY_WINDOW); #else atk_component_get_size(ATK_COMPONENT(m_element.get()), nullptr, &height); #endif return height; } double AccessibilityUIElement::clickPointX() { if (!ATK_IS_COMPONENT(m_element.get())) return 0; int x, width; #if ATK_CHECK_VERSION(2,11,90) atk_component_get_extents(ATK_COMPONENT(m_element.get()), &x, nullptr, &width, nullptr, ATK_XY_WINDOW); #else atk_component_get_position(ATK_COMPONENT(m_element.get()), &x, nullptr, ATK_XY_WINDOW); atk_component_get_size(ATK_COMPONENT(m_element.get()), &width, nullptr); #endif return x + width / 2.0; } double AccessibilityUIElement::clickPointY() { if (!ATK_IS_COMPONENT(m_element.get())) return 0; int y, height; #if ATK_CHECK_VERSION(2,11,90) atk_component_get_extents(ATK_COMPONENT(m_element.get()), nullptr, &y, nullptr, &height, ATK_XY_WINDOW); #else atk_component_get_position(ATK_COMPONENT(m_element.get()), nullptr, &y, ATK_XY_WINDOW); atk_component_get_size(ATK_COMPONENT(m_element.get()), nullptr, &height); #endif return y + height / 2.0; } double AccessibilityUIElement::intValue() const { if (!ATK_IS_OBJECT(m_element.get())) return 0; if (ATK_IS_VALUE(m_element.get())) { #if ATK_CHECK_VERSION(2,11,92) double value; atk_value_get_value_and_text(ATK_VALUE(m_element.get()), &value, nullptr); return value; #else GValue value = G_VALUE_INIT; atk_value_get_current_value(ATK_VALUE(m_element.get()), &value); if (!G_VALUE_HOLDS_FLOAT(&value)) return 0; return g_value_get_float(&value); #endif } // Consider headings as an special case when returning the "int value" of // an AccessibilityUIElement, so we can reuse some tests to check the level // both for HTML headings and objects with the aria-level attribute. if (atk_object_get_role(ATK_OBJECT(m_element.get())) == ATK_ROLE_HEADING) { String headingLevel = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), ObjectAttributeType, "level"); bool ok; double headingLevelValue = headingLevel.toDouble(&ok); if (ok) return headingLevelValue; } return 0; } double AccessibilityUIElement::minValue() { if (!ATK_IS_VALUE(m_element.get())) return 0; #if ATK_CHECK_VERSION(2,11,92) return rangeMinMaxValue(ATK_VALUE(m_element.get()), RangeLimitMinimum); #else GValue value = G_VALUE_INIT; atk_value_get_minimum_value(ATK_VALUE(m_element.get()), &value); if (!G_VALUE_HOLDS_FLOAT(&value)) return 0; return g_value_get_float(&value); #endif } double AccessibilityUIElement::maxValue() { if (!ATK_IS_VALUE(m_element.get())) return 0; #if ATK_CHECK_VERSION(2,11,92) return rangeMinMaxValue(ATK_VALUE(m_element.get()), RangeLimitMaximum); #else GValue value = G_VALUE_INIT; atk_value_get_maximum_value(ATK_VALUE(m_element.get()), &value); if (!G_VALUE_HOLDS_FLOAT(&value)) return 0; return g_value_get_float(&value); #endif } JSRetainPtr AccessibilityUIElement::valueDescription() { String valueText = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), ObjectAttributeType, "valuetext"); GUniquePtr valueDescription(g_strdup_printf("AXValueDescription: %s", valueText.utf8().data())); return JSStringCreateWithUTF8CString(valueDescription.get()); } int AccessibilityUIElement::insertionPointLineNumber() { // FIXME: implement return -1; } bool AccessibilityUIElement::isPressActionSupported() { if (!ATK_IS_ACTION(m_element.get())) return false; const gchar* actionName = atk_action_get_name(ATK_ACTION(m_element.get()), 0); return equalLettersIgnoringASCIICase(String(actionName), "press") || equalLettersIgnoringASCIICase(String(actionName), "jump"); } bool AccessibilityUIElement::isIncrementActionSupported() { // FIXME: implement return false; } bool AccessibilityUIElement::isDecrementActionSupported() { // FIXME: implement return false; } bool AccessibilityUIElement::isEnabled() { return checkElementState(m_element.get(), ATK_STATE_ENABLED); } bool AccessibilityUIElement::isRequired() const { return checkElementState(m_element.get(), ATK_STATE_REQUIRED); } bool AccessibilityUIElement::isFocused() const { return checkElementState(m_element.get(), ATK_STATE_FOCUSED); } bool AccessibilityUIElement::isSelected() const { return checkElementState(m_element.get(), ATK_STATE_SELECTED); } bool AccessibilityUIElement::isSelectedOptionActive() const { return checkElementState(m_element.get(), ATK_STATE_ACTIVE); } bool AccessibilityUIElement::isExpanded() const { return checkElementState(m_element.get(), ATK_STATE_EXPANDED); } bool AccessibilityUIElement::isChecked() const { return checkElementState(m_element.get(), ATK_STATE_CHECKED); } bool AccessibilityUIElement::isIndeterminate() const { return checkElementState(m_element.get(), ATK_STATE_INDETERMINATE); } int AccessibilityUIElement::hierarchicalLevel() const { // FIXME: implement return 0; } JSRetainPtr AccessibilityUIElement::speak() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } bool AccessibilityUIElement::ariaIsGrabbed() const { // FIXME: implement return false; } JSRetainPtr AccessibilityUIElement::ariaDropEffects() const { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } // parameterized attributes int AccessibilityUIElement::lineForIndex(int index) { if (!ATK_IS_TEXT(m_element.get())) return -1; if (index < 0 || index > atk_text_get_character_count(ATK_TEXT(m_element.get()))) return -1; GUniquePtr text(atk_text_get_text(ATK_TEXT(m_element.get()), 0, index)); int lineNo = 0; for (gchar* offset = text.get(); *offset; ++offset) { if (*offset == '\n') ++lineNo; } return lineNo; } JSRetainPtr AccessibilityUIElement::rangeForLine(int line) { if (!ATK_IS_TEXT(m_element.get())) return JSStringCreateWithCharacters(0, 0); AtkText* text = ATK_TEXT(m_element.get()); gint startOffset = 0, endOffset = 0; for (int i = 0; i <= line; ++i) atk_text_get_string_at_offset(text, endOffset, ATK_TEXT_GRANULARITY_LINE, &startOffset, &endOffset); GUniquePtr range(g_strdup_printf("{%d, %d}", startOffset, endOffset - startOffset)); return JSStringCreateWithUTF8CString(range.get()); } JSRetainPtr AccessibilityUIElement::rangeForPosition(int x, int y) { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::boundsForRange(unsigned location, unsigned length) { if (!ATK_IS_TEXT(m_element.get())) return JSStringCreateWithCharacters(0, 0); AtkTextRectangle rect; atk_text_get_range_extents(ATK_TEXT(m_element.get()), location, location + length, ATK_XY_WINDOW, &rect); GUniquePtr bounds(g_strdup_printf("{%d, %d, %d, %d}", rect.x, rect.y, rect.width, rect.height)); return JSStringCreateWithUTF8CString(bounds.get()); } JSRetainPtr AccessibilityUIElement::stringForRange(unsigned location, unsigned length) { if (!ATK_IS_TEXT(m_element.get())) return JSStringCreateWithCharacters(0, 0); String string = atk_text_get_text(ATK_TEXT(m_element.get()), location, location + length); return JSStringCreateWithUTF8CString(string.utf8().data()); } JSRetainPtr AccessibilityUIElement::attributedStringForRange(unsigned location, unsigned length) { if (!ATK_IS_TEXT(m_element.get())) return JSStringCreateWithCharacters(0, 0); StringBuilder builder; // The default text attributes apply to the entire element. builder.append("\n\tDefault text attributes:\n\t\t"); builder.append(attributeSetToString(getAttributeSet(m_element.get(), TextAttributeType), "\n\t\t")); // The attribute run provides attributes specific to the range of text at the specified offset. AtkAttributeSet* attributeSet; AtkText* text = ATK_TEXT(m_element.get()); gint start = 0, end = 0; for (int i = location; i < location + length; i = end) { AtkAttributeSet* attributeSet = atk_text_get_run_attributes(text, i, &start, &end); GUniquePtr substring(replaceCharactersForResults(atk_text_get_text(text, start, end))); builder.append(String::format("\n\tRange attributes for '%s':\n\t\t", substring.get())); builder.append(attributeSetToString(attributeSet, "\n\t\t")); } atk_attribute_set_free(attributeSet); return JSStringCreateWithUTF8CString(builder.toString().utf8().data()); } bool AccessibilityUIElement::attributedStringRangeIsMisspelled(unsigned location, unsigned length) { // FIXME: implement return false; } unsigned AccessibilityUIElement::uiElementCountForSearchPredicate(JSContextRef context, AccessibilityUIElement* startElement, bool isDirectionNext, JSValueRef searchKey, JSStringRef searchText, bool visibleOnly, bool immediateDescendantsOnly) { // FIXME: implement return 0; } PassRefPtr AccessibilityUIElement::uiElementForSearchPredicate(JSContextRef context, AccessibilityUIElement* startElement, bool isDirectionNext, JSValueRef searchKey, JSStringRef searchText, bool visibleOnly, bool immediateDescendantsOnly) { // FIXME: implement return nullptr; } JSRetainPtr AccessibilityUIElement::selectTextWithCriteria(JSContextRef context, JSStringRef ambiguityResolution, JSValueRef searchStrings, JSStringRef replacementString, JSStringRef activity) { // FIXME: implement return nullptr; } JSRetainPtr AccessibilityUIElement::attributesOfColumnHeaders() { if (!ATK_IS_TABLE(m_element.get())) return JSStringCreateWithCharacters(0, 0); Vector > columnHeaders = getColumnHeaders(ATK_TABLE(m_element.get())); return createStringWithAttributes(columnHeaders); } JSRetainPtr AccessibilityUIElement::attributesOfRowHeaders() { if (!ATK_IS_TABLE(m_element.get())) return JSStringCreateWithCharacters(0, 0); Vector > rowHeaders = getRowHeaders(ATK_TABLE(m_element.get())); return createStringWithAttributes(rowHeaders); } JSRetainPtr AccessibilityUIElement::attributesOfColumns() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::attributesOfRows() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::attributesOfVisibleCells() { if (!ATK_IS_TABLE(m_element.get())) return JSStringCreateWithCharacters(0, 0); Vector > visibleCells = getVisibleCells(this); return createStringWithAttributes(visibleCells); } JSRetainPtr AccessibilityUIElement::attributesOfHeader() { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } int AccessibilityUIElement::rowCount() { if (!ATK_IS_TABLE(m_element.get())) return 0; return atk_table_get_n_rows(ATK_TABLE(m_element.get())); } int AccessibilityUIElement::columnCount() { if (!ATK_IS_TABLE(m_element.get())) return 0; return atk_table_get_n_columns(ATK_TABLE(m_element.get())); } int AccessibilityUIElement::indexInTable() { // FIXME: implement return -1; } JSRetainPtr AccessibilityUIElement::rowIndexRange() { // Range in table for rows. return indexRangeInTable(m_element.get(), true); } JSRetainPtr AccessibilityUIElement::columnIndexRange() { // Range in table for columns. return indexRangeInTable(m_element.get(), false); } PassRefPtr AccessibilityUIElement::cellForColumnAndRow(unsigned col, unsigned row) { if (!ATK_IS_TABLE(m_element.get())) return nullptr; // Adopt the AtkObject representing the cell because // at_table_ref_at() transfers full ownership. GRefPtr foundCell = adoptGRef(atk_table_ref_at(ATK_TABLE(m_element.get()), row, col)); return foundCell ? AccessibilityUIElement::create(foundCell.get()) : nullptr; } PassRefPtr AccessibilityUIElement::horizontalScrollbar() const { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::verticalScrollbar() const { // FIXME: implement return nullptr; } JSRetainPtr AccessibilityUIElement::selectedTextRange() { if (!ATK_IS_TEXT(m_element.get())) return JSStringCreateWithCharacters(0, 0); gint start, end; g_free(atk_text_get_selection(ATK_TEXT(m_element.get()), 0, &start, &end)); GUniquePtr selection(g_strdup_printf("{%d, %d}", start, end - start)); return JSStringCreateWithUTF8CString(selection.get()); } bool AccessibilityUIElement::setSelectedTextRange(unsigned location, unsigned length) { if (!ATK_IS_TEXT(m_element.get())) return false; if (!length) return atk_text_set_caret_offset(ATK_TEXT(m_element.get()), location); return atk_text_set_selection(ATK_TEXT(m_element.get()), 0, location, location + length); } void AccessibilityUIElement::increment() { alterCurrentValue(m_element.get(), 1); } void AccessibilityUIElement::decrement() { alterCurrentValue(m_element.get(), -1); } void AccessibilityUIElement::showMenu() { // FIXME: implement } void AccessibilityUIElement::press() { if (!ATK_IS_ACTION(m_element.get())) return; // Only one action per object is supported so far. atk_action_do_action(ATK_ACTION(m_element.get()), 0); } void AccessibilityUIElement::setSelectedChild(AccessibilityUIElement* element) const { // FIXME: implement } void AccessibilityUIElement::setSelectedChildAtIndex(unsigned index) const { if (!ATK_IS_SELECTION(m_element.get())) return; atk_selection_add_selection(ATK_SELECTION(m_element.get()), index); } void AccessibilityUIElement::removeSelectionAtIndex(unsigned index) const { if (!ATK_IS_SELECTION(m_element.get())) return; atk_selection_remove_selection(ATK_SELECTION(m_element.get()), index); } JSRetainPtr AccessibilityUIElement::accessibilityValue() const { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } JSRetainPtr AccessibilityUIElement::documentEncoding() { if (!ATK_IS_DOCUMENT(m_element.get())) return JSStringCreateWithCharacters(0, 0); AtkRole role = atk_object_get_role(ATK_OBJECT(m_element.get())); if (role != ATK_ROLE_DOCUMENT_FRAME) return JSStringCreateWithCharacters(0, 0); return JSStringCreateWithUTF8CString(atk_document_get_attribute_value(ATK_DOCUMENT(m_element.get()), "Encoding")); } JSRetainPtr AccessibilityUIElement::documentURI() { if (!ATK_IS_DOCUMENT(m_element.get())) return JSStringCreateWithCharacters(0, 0); AtkRole role = atk_object_get_role(ATK_OBJECT(m_element.get())); if (role != ATK_ROLE_DOCUMENT_FRAME) return JSStringCreateWithCharacters(0, 0); return JSStringCreateWithUTF8CString(atk_document_get_attribute_value(ATK_DOCUMENT(m_element.get()), "URI")); } JSRetainPtr AccessibilityUIElement::url() { if (!ATK_IS_HYPERLINK_IMPL(m_element.get())) return JSStringCreateWithCharacters(0, 0); AtkHyperlink* hyperlink = atk_hyperlink_impl_get_hyperlink(ATK_HYPERLINK_IMPL(m_element.get())); GUniquePtr hyperlinkURI(atk_hyperlink_get_uri(hyperlink, 0)); // Build the result string, stripping the absolute URL paths if present. char* localURI = g_strstr_len(hyperlinkURI.get(), -1, "LayoutTests"); String axURL = String::format("AXURL: %s", localURI ? localURI : hyperlinkURI.get()); return JSStringCreateWithUTF8CString(axURL.utf8().data()); } bool AccessibilityUIElement::addNotificationListener(JSValueRef functionCallback) { if (!functionCallback) return false; // Only one notification listener per element. if (m_notificationHandler) return false; m_notificationHandler = AccessibilityNotificationHandler::create(); m_notificationHandler->setPlatformElement(platformUIElement()); m_notificationHandler->setNotificationFunctionCallback(functionCallback); return true; } bool AccessibilityUIElement::removeNotificationListener() { // Programmers should not be trying to remove a listener that's already removed. ASSERT(m_notificationHandler); m_notificationHandler = nullptr; return true; } bool AccessibilityUIElement::isFocusable() const { return checkElementState(m_element.get(), ATK_STATE_FOCUSABLE); } bool AccessibilityUIElement::isSelectable() const { return checkElementState(m_element.get(), ATK_STATE_SELECTABLE); } bool AccessibilityUIElement::isMultiSelectable() const { return checkElementState(m_element.get(), ATK_STATE_MULTISELECTABLE); } bool AccessibilityUIElement::isVisible() const { return checkElementState(m_element.get(), ATK_STATE_VISIBLE); } bool AccessibilityUIElement::isOffScreen() const { // FIXME: implement return false; } bool AccessibilityUIElement::isCollapsed() const { // FIXME: implement return false; } bool AccessibilityUIElement::isIgnored() const { // FIXME: implement return false; } bool AccessibilityUIElement::hasPopup() const { if (!ATK_IS_OBJECT(m_element.get())) return false; String hasPopupValue = getAttributeSetValueForId(ATK_OBJECT(m_element.get()), ObjectAttributeType, "haspopup"); return equalLettersIgnoringASCIICase(hasPopupValue, "true"); } void AccessibilityUIElement::takeFocus() { // FIXME: implement } void AccessibilityUIElement::takeSelection() { // FIXME: implement } void AccessibilityUIElement::addSelection() { // FIXME: implement } void AccessibilityUIElement::removeSelection() { // FIXME: implement } // Text markers PassRefPtr AccessibilityUIElement::lineTextMarkerRangeForTextMarker(AccessibilityTextMarker* textMarker) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::textMarkerRangeForElement(AccessibilityUIElement* element) { // FIXME: implement return nullptr; } int AccessibilityUIElement::textMarkerRangeLength(AccessibilityTextMarkerRange* range) { // FIXME: implement return 0; } PassRefPtr AccessibilityUIElement::previousTextMarker(AccessibilityTextMarker* textMarker) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::nextTextMarker(AccessibilityTextMarker* textMarker) { // FIXME: implement return nullptr; } JSRetainPtr AccessibilityUIElement::stringForTextMarkerRange(AccessibilityTextMarkerRange* markerRange) { // FIXME: implement return JSStringCreateWithCharacters(0, 0); } PassRefPtr AccessibilityUIElement::textMarkerRangeForMarkers(AccessibilityTextMarker* startMarker, AccessibilityTextMarker* endMarker) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::startTextMarkerForTextMarkerRange(AccessibilityTextMarkerRange* range) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::endTextMarkerForTextMarkerRange(AccessibilityTextMarkerRange* range) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::endTextMarkerForBounds(int x, int y, int width, int height) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::startTextMarkerForBounds(int x, int y, int width, int height) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::textMarkerForPoint(int x, int y) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::accessibilityElementForTextMarker(AccessibilityTextMarker* marker) { // FIXME: implement return nullptr; } bool AccessibilityUIElement::attributedStringForTextMarkerRangeContainsAttribute(JSStringRef attribute, AccessibilityTextMarkerRange* range) { // FIXME: implement return false; } int AccessibilityUIElement::indexForTextMarker(AccessibilityTextMarker* marker) { // FIXME: implement return -1; } bool AccessibilityUIElement::isTextMarkerValid(AccessibilityTextMarker* textMarker) { // FIXME: implement return false; } PassRefPtr AccessibilityUIElement::textMarkerForIndex(int textIndex) { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::startTextMarker() { // FIXME: implement return nullptr; } PassRefPtr AccessibilityUIElement::endTextMarker() { // FIXME: implement return nullptr; } bool AccessibilityUIElement::setSelectedVisibleTextRange(AccessibilityTextMarkerRange*) { return false; } void AccessibilityUIElement::scrollToMakeVisible() { // FIXME: implement } void AccessibilityUIElement::scrollToGlobalPoint(int x, int y) { // FIXME: implement } void AccessibilityUIElement::scrollToMakeVisibleWithSubFocus(int x, int y, int width, int height) { // FIXME: implement } JSRetainPtr AccessibilityUIElement::supportedActions() const { // FIXME: implement return nullptr; } JSRetainPtr AccessibilityUIElement::pathDescription() const { notImplemented(); return nullptr; } JSRetainPtr AccessibilityUIElement::mathPostscriptsDescription() const { notImplemented(); return nullptr; } JSRetainPtr AccessibilityUIElement::mathPrescriptsDescription() const { notImplemented(); return nullptr; } JSRetainPtr AccessibilityUIElement::classList() const { notImplemented(); return nullptr; } JSRetainPtr stringAtOffset(PlatformUIElement element, AtkTextBoundary boundary, int offset) { if (!ATK_IS_TEXT(element.get())) return JSStringCreateWithCharacters(0, 0); gint startOffset, endOffset; StringBuilder builder; #if ATK_CHECK_VERSION(2, 10, 0) AtkTextGranularity granularity; switch (boundary) { case ATK_TEXT_BOUNDARY_CHAR: granularity = ATK_TEXT_GRANULARITY_CHAR; break; case ATK_TEXT_BOUNDARY_WORD_START: granularity = ATK_TEXT_GRANULARITY_WORD; break; case ATK_TEXT_BOUNDARY_LINE_START: granularity = ATK_TEXT_GRANULARITY_LINE; break; case ATK_TEXT_BOUNDARY_SENTENCE_START: granularity = ATK_TEXT_GRANULARITY_SENTENCE; break; default: return JSStringCreateWithCharacters(0, 0); } builder.append(atk_text_get_string_at_offset(ATK_TEXT(element.get()), offset, granularity, &startOffset, &endOffset)); #else builder.append(atk_text_get_text_at_offset(ATK_TEXT(element.get()), offset, boundary, &startOffset, &endOffset)); #endif builder.append(String::format(", %i, %i", startOffset, endOffset)); return JSStringCreateWithUTF8CString(builder.toString().utf8().data()); } JSRetainPtr AccessibilityUIElement::characterAtOffset(int offset) { return stringAtOffset(m_element, ATK_TEXT_BOUNDARY_CHAR, offset); } JSRetainPtr AccessibilityUIElement::wordAtOffset(int offset) { return stringAtOffset(m_element, ATK_TEXT_BOUNDARY_WORD_START, offset); } JSRetainPtr AccessibilityUIElement::lineAtOffset(int offset) { return stringAtOffset(m_element, ATK_TEXT_BOUNDARY_LINE_START, offset); } JSRetainPtr AccessibilityUIElement::sentenceAtOffset(int offset) { return stringAtOffset(m_element, ATK_TEXT_BOUNDARY_SENTENCE_START, offset); } } // namespace WTR #endif // HAVE(ACCESSIBILITY)