/* * Copyright (C) 2008 Nuanti Ltd. * Copyright (C) 2009 Igalia S.L. * Copyright (C) 2009 Jan Alonzo * * 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 "AccessibilityObjectWrapperAtk.h" #if HAVE(ACCESSIBILITY) #include "AXObjectCache.h" #include "AccessibilityList.h" #include "AccessibilityListBox.h" #include "AccessibilityListBoxOption.h" #include "AccessibilityRenderObject.h" #include "AccessibilityTable.h" #include "AccessibilityTableCell.h" #include "AccessibilityTableColumn.h" #include "AccessibilityTableRow.h" #include "Document.h" #include "DocumentType.h" #include "Editor.h" #include "Frame.h" #include "FrameView.h" #include "GOwnPtr.h" #include "HostWindow.h" #include "HTMLNames.h" #include "HTMLTableCaptionElement.h" #include "HTMLTableElement.h" #include "InlineTextBox.h" #include "IntRect.h" #include "NotImplemented.h" #include "RenderListMarker.h" #include "RenderText.h" #include "TextEncoding.h" #include #include #include #include #include #include #include using namespace WebCore; static AccessibilityObject* fallbackObject() { static AXObjectCache* fallbackCache = new AXObjectCache; static AccessibilityObject* object = 0; if (!object) { // FIXME: using fallbackCache->getOrCreate(ListBoxOptionRole) is a hack object = fallbackCache->getOrCreate(ListBoxOptionRole); object->ref(); } return object; } // Used to provide const char* returns. static const char* returnString(const String& str) { static CString returnedString; returnedString = str.utf8(); return returnedString.data(); } static AccessibilityObject* core(WebKitAccessible* accessible) { if (!accessible) return 0; return accessible->m_object; } static AccessibilityObject* core(AtkObject* object) { if (!WEBKIT_IS_ACCESSIBLE(object)) return 0; return core(WEBKIT_ACCESSIBLE(object)); } static AccessibilityObject* core(AtkAction* action) { return core(ATK_OBJECT(action)); } static AccessibilityObject* core(AtkSelection* selection) { return core(ATK_OBJECT(selection)); } static AccessibilityObject* core(AtkText* text) { return core(ATK_OBJECT(text)); } static AccessibilityObject* core(AtkEditableText* text) { return core(ATK_OBJECT(text)); } static AccessibilityObject* core(AtkComponent* component) { return core(ATK_OBJECT(component)); } static AccessibilityObject* core(AtkImage* image) { return core(ATK_OBJECT(image)); } static AccessibilityObject* core(AtkTable* table) { return core(ATK_OBJECT(table)); } static AccessibilityObject* core(AtkDocument* document) { return core(ATK_OBJECT(document)); } static gchar* webkit_accessible_text_get_text(AtkText* text, gint startOffset, gint endOffset); static const gchar* webkit_accessible_get_name(AtkObject* object) { AccessibilityObject* coreObject = core(object); if (!coreObject->isAccessibilityRenderObject()) return returnString(coreObject->stringValue()); AccessibilityRenderObject* renderObject = static_cast(coreObject); if (coreObject->isControl()) { AccessibilityObject* label = renderObject->correspondingLabelForControlElement(); if (label) { AtkObject* atkObject = label->wrapper(); if (ATK_IS_TEXT(atkObject)) return webkit_accessible_text_get_text(ATK_TEXT(atkObject), 0, -1); } // Try text under the node String textUnder = renderObject->textUnderElement(); if (textUnder.length()) return returnString(textUnder); } if (renderObject->isImage() || renderObject->isInputImage()) { Node* node = renderObject->renderer()->node(); if (node && node->isHTMLElement()) { // Get the attribute rather than altText String so as not to fall back on title. String alt = static_cast(node)->getAttribute(HTMLNames::altAttr); if (!alt.isEmpty()) return returnString(alt); } } return returnString(coreObject->stringValue()); } static const gchar* webkit_accessible_get_description(AtkObject* object) { AccessibilityObject* coreObject = core(object); Node* node = 0; if (coreObject->isAccessibilityRenderObject()) node = static_cast(coreObject)->renderer()->node(); if (!node || !node->isHTMLElement() || coreObject->ariaRoleAttribute() != UnknownRole) return returnString(coreObject->accessibilityDescription()); // atk_table_get_summary returns an AtkObject. We have no summary object, so expose summary here. if (coreObject->roleValue() == TableRole) { String summary = static_cast(node)->summary(); if (!summary.isEmpty()) return returnString(summary); } // The title attribute should be reliably available as the object's descripton. // We do not want to fall back on other attributes in its absence. See bug 25524. String title = static_cast(node)->title(); if (!title.isEmpty()) return returnString(title); return returnString(coreObject->accessibilityDescription()); } static void setAtkRelationSetFromCoreObject(AccessibilityObject* coreObject, AtkRelationSet* relationSet) { AccessibilityRenderObject* accObject = static_cast(coreObject); if (accObject->isControl()) { AccessibilityObject* label = accObject->correspondingLabelForControlElement(); if (label) atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_LABELLED_BY, label->wrapper()); } else { AccessibilityObject* control = accObject->correspondingControlForLabelElement(); if (control) atk_relation_set_add_relation_by_type(relationSet, ATK_RELATION_LABEL_FOR, control->wrapper()); } } static gpointer webkit_accessible_parent_class = 0; static AtkObject* atkParentOfWebView(AtkObject* object) { AccessibilityObject* coreParent = core(object)->parentObjectUnignored(); // The top level web view 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 (!coreParent && core(object)->isWebArea()) { HostWindow* hostWindow = core(object)->document()->view()->hostWindow(); if (hostWindow) { PlatformPageClient webView = hostWindow->platformPageClient(); if (webView) { GtkWidget* webViewParent = gtk_widget_get_parent(webView); if (webViewParent) return gtk_widget_get_accessible(webViewParent); } } } if (!coreParent) return 0; return coreParent->wrapper(); } static AtkObject* webkit_accessible_get_parent(AtkObject* object) { AccessibilityObject* coreParent = core(object)->parentObjectUnignored(); if (!coreParent && core(object)->isWebArea()) return atkParentOfWebView(object); if (!coreParent) return 0; return coreParent->wrapper(); } static gint webkit_accessible_get_n_children(AtkObject* object) { return core(object)->children().size(); } static AtkObject* webkit_accessible_ref_child(AtkObject* object, gint index) { AccessibilityObject* coreObject = core(object); AccessibilityObject::AccessibilityChildrenVector children = coreObject->children(); if (index < 0 || static_cast(index) >= children.size()) return 0; AccessibilityObject* coreChild = children.at(index).get(); if (!coreChild) return 0; AtkObject* child = coreChild->wrapper(); atk_object_set_parent(child, object); g_object_ref(child); return child; } static gint webkit_accessible_get_index_in_parent(AtkObject* object) { AccessibilityObject* coreObject = core(object); AccessibilityObject* parent = coreObject->parentObjectUnignored(); if (!parent && core(object)->isWebArea()) { AtkObject* atkParent = atkParentOfWebView(object); if (!atkParent) return -1; unsigned count = atk_object_get_n_accessible_children(atkParent); for (unsigned i = 0; i < count; ++i) { AtkObject* child = atk_object_ref_accessible_child(atkParent, i); bool childIsObject = child == object; g_object_unref(child); if (childIsObject) return i; } } AccessibilityObject::AccessibilityChildrenVector children = parent->children(); unsigned count = children.size(); for (unsigned i = 0; i < count; ++i) { if (children[i] == coreObject) return i; } return -1; } static AtkAttributeSet* addAttributeToSet(AtkAttributeSet* attributeSet, const char* name, const char* value) { AtkAttribute* attribute = static_cast(g_malloc(sizeof(AtkAttribute))); attribute->name = g_strdup(name); attribute->value = g_strdup(value); attributeSet = g_slist_prepend(attributeSet, attribute); return attributeSet; } static AtkAttributeSet* webkit_accessible_get_attributes(AtkObject* object) { AtkAttributeSet* attributeSet = 0; attributeSet = addAttributeToSet(attributeSet, "toolkit", "WebKitGtk"); int headingLevel = core(object)->headingLevel(); if (headingLevel) { String value = String::number(headingLevel); attributeSet = addAttributeToSet(attributeSet, "level", value.utf8().data()); } return attributeSet; } static AtkRole atkRole(AccessibilityRole role) { switch (role) { case UnknownRole: return ATK_ROLE_UNKNOWN; case ButtonRole: return ATK_ROLE_PUSH_BUTTON; case RadioButtonRole: return ATK_ROLE_RADIO_BUTTON; case CheckBoxRole: return ATK_ROLE_CHECK_BOX; case SliderRole: return ATK_ROLE_SLIDER; case TabGroupRole: return ATK_ROLE_PAGE_TAB_LIST; case TextFieldRole: case TextAreaRole: return ATK_ROLE_ENTRY; case StaticTextRole: return ATK_ROLE_TEXT; case OutlineRole: return ATK_ROLE_TREE; case MenuBarRole: return ATK_ROLE_MENU_BAR; case MenuListPopupRole: case MenuRole: return ATK_ROLE_MENU; case MenuListOptionRole: case MenuItemRole: return ATK_ROLE_MENU_ITEM; case ColumnRole: //return ATK_ROLE_TABLE_COLUMN_HEADER; // Is this right? return ATK_ROLE_UNKNOWN; // Matches Mozilla case RowRole: //return ATK_ROLE_TABLE_ROW_HEADER; // Is this right? return ATK_ROLE_LIST_ITEM; // Matches Mozilla case ToolbarRole: return ATK_ROLE_TOOL_BAR; case BusyIndicatorRole: return ATK_ROLE_PROGRESS_BAR; // Is this right? case ProgressIndicatorRole: //return ATK_ROLE_SPIN_BUTTON; // Some confusion about this role in AccessibilityRenderObject.cpp return ATK_ROLE_PROGRESS_BAR; case WindowRole: return ATK_ROLE_WINDOW; case PopUpButtonRole: case ComboBoxRole: return ATK_ROLE_COMBO_BOX; case SplitGroupRole: return ATK_ROLE_SPLIT_PANE; case SplitterRole: return ATK_ROLE_SEPARATOR; case ColorWellRole: return ATK_ROLE_COLOR_CHOOSER; case ListRole: return ATK_ROLE_LIST; case ScrollBarRole: return ATK_ROLE_SCROLL_BAR; case GridRole: // Is this right? case TableRole: return ATK_ROLE_TABLE; case ApplicationRole: return ATK_ROLE_APPLICATION; case GroupRole: case RadioGroupRole: return ATK_ROLE_PANEL; case CellRole: return ATK_ROLE_TABLE_CELL; case LinkRole: case WebCoreLinkRole: case ImageMapLinkRole: return ATK_ROLE_LINK; case ImageMapRole: case ImageRole: return ATK_ROLE_IMAGE; case ListMarkerRole: return ATK_ROLE_TEXT; case WebAreaRole: //return ATK_ROLE_HTML_CONTAINER; // Is this right? return ATK_ROLE_DOCUMENT_FRAME; case HeadingRole: return ATK_ROLE_HEADING; case ListBoxRole: return ATK_ROLE_LIST; case ListItemRole: case ListBoxOptionRole: return ATK_ROLE_LIST_ITEM; default: return ATK_ROLE_UNKNOWN; } } static AtkRole webkit_accessible_get_role(AtkObject* object) { AccessibilityObject* axObject = core(object); if (!axObject) return ATK_ROLE_UNKNOWN; // WebCore does not know about paragraph role, label role, or section role if (axObject->isAccessibilityRenderObject()) { Node* node = static_cast(axObject)->renderer()->node(); if (node) { if (node->hasTagName(HTMLNames::pTag)) return ATK_ROLE_PARAGRAPH; if (node->hasTagName(HTMLNames::labelTag)) return ATK_ROLE_LABEL; if (node->hasTagName(HTMLNames::divTag)) return ATK_ROLE_SECTION; if (node->hasTagName(HTMLNames::formTag)) return ATK_ROLE_FORM; } } // Note: Why doesn't WebCore have a password field for this if (axObject->isPasswordField()) return ATK_ROLE_PASSWORD_TEXT; return atkRole(axObject->roleValue()); } 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 (coreObject->isChecked()) atk_state_set_add_state(stateSet, ATK_STATE_CHECKED); // FIXME: isReadOnly does not seem to do the right thing for // controls, so check explicitly for them. In addition, because // isReadOnly is false for listBoxOptions, we need to add one // more check so that we do not present them as being "editable". if ((!coreObject->isReadOnly() || (coreObject->isControl() && coreObject->canSetValueAttribute())) && !isListBoxOption) 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->canSetFocusAttribute()) atk_state_set_add_state(stateSet, ATK_STATE_FOCUSABLE); if (coreObject->isFocused()) atk_state_set_add_state(stateSet, ATK_STATE_FOCUSED); // TODO: ATK_STATE_HORIZONTAL if (coreObject->isIndeterminate()) atk_state_set_add_state(stateSet, ATK_STATE_INDETERMINATE); 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); // TODO: ATK_STATE_SELECTABLE_TEXT if (coreObject->canSetSelectedAttribute()) { atk_state_set_add_state(stateSet, ATK_STATE_SELECTABLE); // Items in focusable lists in Gtk 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 in Gtk 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 within GTK+ 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() == TextFieldRole) atk_state_set_add_state(stateSet, ATK_STATE_SINGLE_LINE); else if (coreObject->roleValue() == TextAreaRole) atk_state_set_add_state(stateSet, ATK_STATE_MULTI_LINE); // TODO: ATK_STATE_SENSITIVE // TODO: ATK_STATE_VERTICAL if (coreObject->isVisited()) atk_state_set_add_state(stateSet, ATK_STATE_VISITED); } static AtkStateSet* webkit_accessible_ref_state_set(AtkObject* object) { AtkStateSet* stateSet = ATK_OBJECT_CLASS(webkit_accessible_parent_class)->ref_state_set(object); AccessibilityObject* coreObject = core(object); if (coreObject == fallbackObject()) { atk_state_set_add_state(stateSet, ATK_STATE_DEFUNCT); return stateSet; } setAtkStateSetFromCoreObject(coreObject, stateSet); return stateSet; } static AtkRelationSet* webkit_accessible_ref_relation_set(AtkObject* object) { AtkRelationSet* relationSet = ATK_OBJECT_CLASS(webkit_accessible_parent_class)->ref_relation_set(object); AccessibilityObject* coreObject = core(object); setAtkRelationSetFromCoreObject(coreObject, relationSet); return relationSet; } static void webkit_accessible_init(AtkObject* object, gpointer data) { if (ATK_OBJECT_CLASS(webkit_accessible_parent_class)->initialize) ATK_OBJECT_CLASS(webkit_accessible_parent_class)->initialize(object, data); WEBKIT_ACCESSIBLE(object)->m_object = reinterpret_cast(data); } static void webkit_accessible_finalize(GObject* object) { // This is a good time to clear the return buffer. returnString(String()); G_OBJECT_CLASS(webkit_accessible_parent_class)->finalize(object); } static void webkit_accessible_class_init(AtkObjectClass* klass) { GObjectClass* gobjectClass = G_OBJECT_CLASS(klass); webkit_accessible_parent_class = g_type_class_peek_parent(klass); gobjectClass->finalize = webkit_accessible_finalize; klass->initialize = webkit_accessible_init; klass->get_name = webkit_accessible_get_name; klass->get_description = webkit_accessible_get_description; klass->get_parent = webkit_accessible_get_parent; klass->get_n_children = webkit_accessible_get_n_children; klass->ref_child = webkit_accessible_ref_child; klass->get_role = webkit_accessible_get_role; klass->ref_state_set = webkit_accessible_ref_state_set; klass->get_index_in_parent = webkit_accessible_get_index_in_parent; klass->get_attributes = webkit_accessible_get_attributes; klass->ref_relation_set = webkit_accessible_ref_relation_set; } GType webkit_accessible_get_type(void) { static volatile gsize type_volatile = 0; if (g_once_init_enter(&type_volatile)) { static const GTypeInfo tinfo = { sizeof(WebKitAccessibleClass), (GBaseInitFunc) 0, (GBaseFinalizeFunc) 0, (GClassInitFunc) webkit_accessible_class_init, (GClassFinalizeFunc) 0, 0, /* class data */ sizeof(WebKitAccessible), /* instance size */ 0, /* nb preallocs */ (GInstanceInitFunc) 0, 0 /* value table */ }; GType type = g_type_register_static(ATK_TYPE_OBJECT, "WebKitAccessible", &tinfo, GTypeFlags(0)); g_once_init_leave(&type_volatile, type); } return type_volatile; } static gboolean webkit_accessible_action_do_action(AtkAction* action, gint i) { g_return_val_if_fail(i == 0, FALSE); return core(action)->performDefaultAction(); } static gint webkit_accessible_action_get_n_actions(AtkAction* action) { return 1; } static const gchar* webkit_accessible_action_get_description(AtkAction* action, gint i) { g_return_val_if_fail(i == 0, 0); // TODO: Need a way to provide/localize action descriptions. notImplemented(); return ""; } static const gchar* webkit_accessible_action_get_keybinding(AtkAction* action, gint i) { g_return_val_if_fail(i == 0, 0); // FIXME: Construct a proper keybinding string. return returnString(core(action)->accessKey().string()); } static const gchar* webkit_accessible_action_get_name(AtkAction* action, gint i) { g_return_val_if_fail(i == 0, 0); return returnString(core(action)->actionVerb()); } static void atk_action_interface_init(AtkActionIface* iface) { iface->do_action = webkit_accessible_action_do_action; iface->get_n_actions = webkit_accessible_action_get_n_actions; iface->get_description = webkit_accessible_action_get_description; iface->get_keybinding = webkit_accessible_action_get_keybinding; iface->get_name = webkit_accessible_action_get_name; } // Selection (for controls) static AccessibilityObject* optionFromList(AtkSelection* selection, gint i) { AccessibilityObject* coreSelection = core(selection); if (!coreSelection || i < 0) return 0; AccessibilityRenderObject::AccessibilityChildrenVector options = core(selection)->children(); if (i < static_cast(options.size())) return options.at(i).get(); return 0; } static AccessibilityObject* optionFromSelection(AtkSelection* selection, gint i) { // i is the ith selection as opposed to the ith child. AccessibilityObject* coreSelection = core(selection); if (!coreSelection || i < 0) return 0; AccessibilityRenderObject::AccessibilityChildrenVector selectedItems; if (coreSelection->isListBox()) static_cast(coreSelection)->selectedChildren(selectedItems); // TODO: Combo boxes if (i < static_cast(selectedItems.size())) return selectedItems.at(i).get(); return 0; } static gboolean webkit_accessible_selection_add_selection(AtkSelection* selection, gint i) { AccessibilityObject* option = optionFromList(selection, i); if (option && core(selection)->isListBox()) { AccessibilityListBoxOption* listBoxOption = static_cast(option); listBoxOption->setSelected(true); return listBoxOption->isSelected(); } return false; } static gboolean webkit_accessible_selection_clear_selection(AtkSelection* selection) { AccessibilityObject* coreSelection = core(selection); if (!coreSelection) return false; AccessibilityRenderObject::AccessibilityChildrenVector selectedItems; if (coreSelection->isListBox()) { // Set the list of selected items to an empty list; then verify that it worked. AccessibilityListBox* listBox = static_cast(coreSelection); listBox->setSelectedChildren(selectedItems); listBox->selectedChildren(selectedItems); return selectedItems.size() == 0; } return false; } static AtkObject* webkit_accessible_selection_ref_selection(AtkSelection* selection, gint i) { AccessibilityObject* option = optionFromSelection(selection, i); if (option) { AtkObject* child = option->wrapper(); g_object_ref(child); return child; } return 0; } static gint webkit_accessible_selection_get_selection_count(AtkSelection* selection) { AccessibilityObject* coreSelection = core(selection); if (coreSelection && coreSelection->isListBox()) { AccessibilityRenderObject::AccessibilityChildrenVector selectedItems; static_cast(coreSelection)->selectedChildren(selectedItems); return static_cast(selectedItems.size()); } return 0; } static gboolean webkit_accessible_selection_is_child_selected(AtkSelection* selection, gint i) { AccessibilityObject* option = optionFromList(selection, i); if (option && core(selection)->isListBox()) return static_cast(option)->isSelected(); return false; } static gboolean webkit_accessible_selection_remove_selection(AtkSelection* selection, gint i) { // TODO: This is only getting called if i == 0. What is preventing the rest? AccessibilityObject* option = optionFromSelection(selection, i); if (option && core(selection)->isListBox()) { AccessibilityListBoxOption* listBoxOption = static_cast(option); listBoxOption->setSelected(false); return !listBoxOption->isSelected(); } return false; } static gboolean webkit_accessible_selection_select_all_selection(AtkSelection* selection) { AccessibilityObject* coreSelection = core(selection); if (!coreSelection || !coreSelection->isMultiSelectable()) return false; AccessibilityRenderObject::AccessibilityChildrenVector children = coreSelection->children(); if (coreSelection->isListBox()) { AccessibilityListBox* listBox = static_cast(coreSelection); listBox->setSelectedChildren(children); AccessibilityRenderObject::AccessibilityChildrenVector selectedItems; listBox->selectedChildren(selectedItems); return selectedItems.size() == children.size(); } return false; } static void atk_selection_interface_init(AtkSelectionIface* iface) { iface->add_selection = webkit_accessible_selection_add_selection; iface->clear_selection = webkit_accessible_selection_clear_selection; iface->ref_selection = webkit_accessible_selection_ref_selection; iface->get_selection_count = webkit_accessible_selection_get_selection_count; iface->is_child_selected = webkit_accessible_selection_is_child_selected; iface->remove_selection = webkit_accessible_selection_remove_selection; iface->select_all_selection = webkit_accessible_selection_select_all_selection; } // Text static gchar* utf8Substr(const gchar* string, gint start, gint end) { ASSERT(string); glong strLen = g_utf8_strlen(string, -1); if (start > strLen || end > strLen) return 0; gchar* startPtr = g_utf8_offset_to_pointer(string, start); gsize lenInBytes = g_utf8_offset_to_pointer(string, end) - startPtr + 1; gchar* output = static_cast(g_malloc0(lenInBytes + 1)); return g_utf8_strncpy(output, startPtr, end - start + 1); } // This function is not completely general, is it's tied to the // internals of WebCore's text presentation. static gchar* convertUniCharToUTF8(const UChar* characters, gint length, int from, int to) { CString stringUTF8 = UTF8Encoding().encode(characters, length, QuestionMarksForUnencodables); gchar* utf8String = utf8Substr(stringUTF8.data(), from, to); if (!g_utf8_validate(utf8String, -1, 0)) { g_free(utf8String); return 0; } gsize len = strlen(utf8String); GString* ret = g_string_new_len(0, len); gchar* ptr = utf8String; // WebCore introduces line breaks in the text that do not reflect // the layout you see on the screen, replace them with spaces while (len > 0) { gint index, start; pango_find_paragraph_boundary(ptr, len, &index, &start); g_string_append_len(ret, ptr, index); if (index == start) break; g_string_append_c(ret, ' '); ptr += start; len -= start; } g_free(utf8String); return g_string_free(ret, FALSE); } gchar* textForObject(AccessibilityRenderObject* accObject) { GString* str = g_string_new(0); // For text controls, we can get the text line by line. if (accObject->isTextControl()) { unsigned textLength = accObject->textLength(); int lineNumber = 0; PlainTextRange range = accObject->doAXRangeForLine(lineNumber); while (range.length) { // When a line of text wraps in a text area, the final space is removed. if (range.start + range.length < textLength) range.length -= 1; String lineText = accObject->doAXStringForRange(range); g_string_append(str, lineText.utf8().data()); g_string_append(str, "\n"); range = accObject->doAXRangeForLine(++lineNumber); } } else if (accObject->renderer()) { // For RenderBlocks, piece together the text from the RenderText objects they contain. for (RenderObject* obj = accObject->renderer()->firstChild(); obj; obj = obj->nextSibling()) { if (obj->isBR()) { g_string_append(str, "\n"); continue; } RenderText* renderText; if (obj->isText()) renderText = toRenderText(obj); else if (obj->firstChild() && obj->firstChild()->isText()) { // Handle RenderInlines (and any other similiar RenderObjects). renderText = toRenderText(obj->firstChild()); } else continue; InlineTextBox* box = renderText->firstTextBox(); while (box) { gchar* text = convertUniCharToUTF8(renderText->characters(), renderText->textLength(), box->start(), box->end()); g_string_append(str, text); // Newline chars in the source result in separate text boxes, so check // before adding a newline in the layout. See bug 25415 comment #78. // If the next sibling is a BR, we'll add the newline when we examine that child. if (!box->nextOnLineExists() && (!obj->nextSibling() || !obj->nextSibling()->isBR())) g_string_append(str, "\n"); box = box->nextTextBox(); } } } return g_string_free(str, FALSE); } static gchar* webkit_accessible_text_get_text(AtkText* text, gint startOffset, gint endOffset) { AccessibilityObject* coreObject = core(text); String ret; unsigned start = startOffset; if (endOffset == -1) { endOffset = coreObject->stringValue().length(); if (!endOffset) endOffset = coreObject->textUnderElement().length(); } int length = endOffset - startOffset; if (coreObject->isTextControl()) ret = coreObject->doAXStringForRange(PlainTextRange(start, length)); else ret = coreObject->textUnderElement().substring(start, length); if (!ret.length()) { // This can happen at least with anonymous RenderBlocks (e.g. body text amongst paragraphs) ret = String(textForObject(static_cast(coreObject))); if (!endOffset) endOffset = ret.length(); ret = ret.substring(start, endOffset - startOffset); } // Prefix a item number/bullet if needed if (coreObject->roleValue() == ListItemRole) { RenderObject* objRenderer = static_cast(coreObject)->renderer(); RenderObject* markerRenderer = objRenderer ? objRenderer->firstChild() : 0; if (markerRenderer && markerRenderer->isListMarker()) { String markerTxt = toRenderListMarker(markerRenderer)->text(); ret = markerTxt.length() > 0 ? markerTxt + " " + ret : ret; } } return g_strdup(ret.utf8().data()); } static GailTextUtil* getGailTextUtilForAtk(AtkText* textObject) { gpointer data = g_object_get_data(G_OBJECT(textObject), "webkit-accessible-gail-text-util"); if (data) return static_cast(data); GailTextUtil* gailTextUtil = gail_text_util_new(); gail_text_util_text_setup(gailTextUtil, webkit_accessible_text_get_text(textObject, 0, -1)); g_object_set_data_full(G_OBJECT(textObject), "webkit-accessible-gail-text-util", gailTextUtil, g_object_unref); return gailTextUtil; } static PangoLayout* getPangoLayoutForAtk(AtkText* textObject) { AccessibilityObject* coreObject = core(textObject); HostWindow* hostWindow = coreObject->document()->view()->hostWindow(); if (!hostWindow) return 0; PlatformPageClient webView = hostWindow->platformPageClient(); if (!webView) return 0; AccessibilityRenderObject* accObject = static_cast(coreObject); if (!accObject) return 0; // Create a string with the layout as it appears on the screen PangoLayout* layout = gtk_widget_create_pango_layout(static_cast(webView), textForObject(accObject)); g_object_set_data_full(G_OBJECT(textObject), "webkit-accessible-pango-layout", layout, g_object_unref); return layout; } static gchar* webkit_accessible_text_get_text_after_offset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) { return gail_text_util_get_text(getGailTextUtilForAtk(text), getPangoLayoutForAtk(text), GAIL_AFTER_OFFSET, boundaryType, offset, startOffset, endOffset); } static gchar* webkit_accessible_text_get_text_at_offset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) { return gail_text_util_get_text(getGailTextUtilForAtk(text), getPangoLayoutForAtk(text), GAIL_AT_OFFSET, boundaryType, offset, startOffset, endOffset); } static gchar* webkit_accessible_text_get_text_before_offset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) { return gail_text_util_get_text(getGailTextUtilForAtk(text), getPangoLayoutForAtk(text), GAIL_BEFORE_OFFSET, boundaryType, offset, startOffset, endOffset); } static gunichar webkit_accessible_text_get_character_at_offset(AtkText* text, gint offset) { notImplemented(); return 0; } static gint webkit_accessible_text_get_caret_offset(AtkText* text) { // coreObject is the unignored object whose offset the caller is requesting. // focusedObject is the object with the caret. It is likely ignored -- unless it's a link. AccessibilityObject* coreObject = core(text); Node* focusedNode = coreObject->selection().end().node(); if (!focusedNode) return 0; RenderObject* focusedRenderer = focusedNode->renderer(); AccessibilityObject* focusedObject = coreObject->document()->axObjectCache()->getOrCreate(focusedRenderer); int offset; // Don't ignore links if the offset is being requested for a link. objectAndOffsetUnignored(focusedObject, offset, !coreObject->isLink()); // TODO: Verify this for RTL text. return offset; } static AtkAttributeSet* getAttributeSetForAccessibilityObject(const AccessibilityObject* object) { if (!object->isAccessibilityRenderObject()) return 0; RenderObject* renderer = static_cast(object)->renderer(); RenderStyle* style = renderer->style(); AtkAttributeSet* result = 0; GOwnPtr buffer(g_strdup_printf("%i", style->fontSize())); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_SIZE), buffer.get()); Color bgColor = style->visitedDependentColor(CSSPropertyBackgroundColor); if (bgColor.isValid()) { buffer.set(g_strdup_printf("%i,%i,%i", bgColor.red(), bgColor.green(), bgColor.blue())); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_BG_COLOR), buffer.get()); } Color fgColor = style->visitedDependentColor(CSSPropertyColor); if (fgColor.isValid()) { buffer.set(g_strdup_printf("%i,%i,%i", fgColor.red(), fgColor.green(), fgColor.blue())); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_FG_COLOR), buffer.get()); } int baselinePosition; bool includeRise = true; switch (style->verticalAlign()) { case SUB: baselinePosition = -1 * renderer->baselinePosition(true); break; case SUPER: baselinePosition = renderer->baselinePosition(true); break; case BASELINE: baselinePosition = 0; break; default: includeRise = false; break; } if (includeRise) { buffer.set(g_strdup_printf("%i", baselinePosition)); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_RISE), buffer.get()); } int indentation = style->textIndent().calcValue(object->size().width()); if (indentation != undefinedLength) { buffer.set(g_strdup_printf("%i", indentation)); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_INDENT), buffer.get()); } String fontFamilyName = style->font().family().family().string(); if (fontFamilyName.left(8) == "-webkit-") fontFamilyName = fontFamilyName.substring(8); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_FAMILY_NAME), fontFamilyName.utf8().data()); int fontWeight = -1; switch (style->font().weight()) { case FontWeight100: fontWeight = 100; break; case FontWeight200: fontWeight = 200; break; case FontWeight300: fontWeight = 300; break; case FontWeight400: fontWeight = 400; break; case FontWeight500: fontWeight = 500; break; case FontWeight600: fontWeight = 600; break; case FontWeight700: fontWeight = 700; break; case FontWeight800: fontWeight = 800; break; case FontWeight900: fontWeight = 900; } if (fontWeight > 0) { buffer.set(g_strdup_printf("%i", fontWeight)); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_WEIGHT), buffer.get()); } switch (style->textAlign()) { case TAAUTO: break; case LEFT: case WEBKIT_LEFT: result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "left"); break; case RIGHT: case WEBKIT_RIGHT: result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "right"); break; case CENTER: case WEBKIT_CENTER: result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "center"); break; case JUSTIFY: result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "fill"); } result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_UNDERLINE), (style->textDecoration() & UNDERLINE) ? "single" : "none"); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_STYLE), style->font().italic() ? "italic" : "normal"); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_STRIKETHROUGH), (style->textDecoration() & LINE_THROUGH) ? "true" : "false"); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_INVISIBLE), (style->visibility() == HIDDEN) ? "true" : "false"); result = addAttributeToSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_EDITABLE), object->isReadOnly() ? "false" : "true"); return result; } static gint compareAttribute(const AtkAttribute* a, const AtkAttribute* b) { return g_strcmp0(a->name, b->name) || g_strcmp0(a->value, b->value); } // Returns an AtkAttributeSet with the elements of a1 which are either // not present or different in a2. Neither a1 nor a2 should be used // after calling this function. static AtkAttributeSet* attributeSetDifference(AtkAttributeSet* a1, AtkAttributeSet* a2) { if (!a2) return a1; AtkAttributeSet* i = a1; AtkAttributeSet* found; AtkAttributeSet* toDelete = 0; while (i) { found = g_slist_find_custom(a2, i->data, (GCompareFunc)compareAttribute); if (found) { AtkAttributeSet* t = i->next; toDelete = g_slist_prepend(toDelete, i->data); a1 = g_slist_delete_link(a1, i); i = t; } else i = i->next; } atk_attribute_set_free(a2); atk_attribute_set_free(toDelete); return a1; } static guint accessibilityObjectLength(const AccessibilityObject* object) { GOwnPtr text(webkit_accessible_text_get_text(ATK_TEXT(object->wrapper()), 0, -1)); return g_utf8_strlen(text.get(), -1); } static const AccessibilityObject* getAccessibilityObjectForOffset(const AccessibilityObject* object, guint offset, gint* startOffset, gint* endOffset) { const AccessibilityObject* result; guint length = accessibilityObjectLength(object); if (length > offset) { *startOffset = 0; *endOffset = length; result = object; } else { *startOffset = -1; *endOffset = -1; result = 0; } if (!object->firstChild()) return result; AccessibilityObject* child = object->firstChild(); guint currentOffset = 0; guint childPosition = 0; while (child && currentOffset <= offset) { guint childLength = accessibilityObjectLength(child); currentOffset = childLength + childPosition; if (currentOffset > offset) { gint childStartOffset; gint childEndOffset; const AccessibilityObject* grandChild = getAccessibilityObjectForOffset(child, offset-childPosition, &childStartOffset, &childEndOffset); if (childStartOffset >= 0) { *startOffset = childStartOffset + childPosition; *endOffset = childEndOffset + childPosition; result = grandChild; } } else { childPosition += childLength; child = child->nextSibling(); } } return result; } static AtkAttributeSet* getRunAttributesFromAccesibilityObject(const AccessibilityObject* element, gint offset, gint* startOffset, gint* endOffset) { const AccessibilityObject *child = getAccessibilityObjectForOffset(element, offset, startOffset, endOffset); if (!child) { *startOffset = -1; *endOffset = -1; return 0; } AtkAttributeSet* defaultAttributes = getAttributeSetForAccessibilityObject(element); AtkAttributeSet* childAttributes = getAttributeSetForAccessibilityObject(child); return attributeSetDifference(childAttributes, defaultAttributes); } static AtkAttributeSet* webkit_accessible_text_get_run_attributes(AtkText* text, gint offset, gint* startOffset, gint* endOffset) { AccessibilityObject* coreObject = core(text); AtkAttributeSet* result; if (!coreObject) { *startOffset = 0; *endOffset = atk_text_get_character_count(text); return 0; } if (offset == -1) offset = atk_text_get_caret_offset(text); result = getRunAttributesFromAccesibilityObject(coreObject, offset, startOffset, endOffset); if (*startOffset < 0) { *startOffset = offset; *endOffset = offset; } return result; } static AtkAttributeSet* webkit_accessible_text_get_default_attributes(AtkText* text) { AccessibilityObject* coreObject = core(text); if (!coreObject || !coreObject->isAccessibilityRenderObject()) return 0; return getAttributeSetForAccessibilityObject(coreObject); } static IntRect textExtents(AtkText* text, gint startOffset, gint length, AtkCoordType coords) { gchar* textContent = webkit_accessible_text_get_text(text, startOffset, -1); gint textLength = g_utf8_strlen(textContent, -1); // The first case (endOffset of -1) should work, but seems broken for all Gtk+ apps. gint rangeLength = length; if (rangeLength < 0 || rangeLength > textLength) rangeLength = textLength; AccessibilityObject* coreObject = core(text); IntRect extents = coreObject->doAXBoundsForRange(PlainTextRange(startOffset, rangeLength)); switch(coords) { case ATK_XY_SCREEN: extents = coreObject->document()->view()->contentsToScreen(extents); break; case ATK_XY_WINDOW: // No-op break; } return extents; } static void webkit_accessible_text_get_character_extents(AtkText* text, gint offset, gint* x, gint* y, gint* width, gint* height, AtkCoordType coords) { IntRect extents = textExtents(text, offset, 1, coords); *x = extents.x(); *y = extents.y(); *width = extents.width(); *height = extents.height(); } static void webkit_accessible_text_get_range_extents(AtkText* text, gint startOffset, gint endOffset, AtkCoordType coords, AtkTextRectangle* rect) { IntRect extents = textExtents(text, startOffset, endOffset - startOffset + 1, coords); rect->x = extents.x(); rect->y = extents.y(); rect->width = extents.width(); rect->height = extents.height(); } static gint webkit_accessible_text_get_character_count(AtkText* text) { AccessibilityObject* coreObject = core(text); if (coreObject->isTextControl()) return coreObject->textLength(); else return coreObject->textUnderElement().length(); } static gint webkit_accessible_text_get_offset_at_point(AtkText* text, gint x, gint y, AtkCoordType coords) { // FIXME: Use the AtkCoordType // TODO: Is it correct to ignore range.length? IntPoint pos(x, y); PlainTextRange range = core(text)->doAXRangeForPosition(pos); return range.start; } static bool selectionBelongsToObject(AccessibilityObject* coreObject, VisibleSelection& selection) { if (!coreObject->isAccessibilityRenderObject()) return false; Node* node = static_cast(coreObject)->renderer()->node(); return node == selection.base().containerNode(); } static gint webkit_accessible_text_get_n_selections(AtkText* text) { AccessibilityObject* coreObject = core(text); VisibleSelection selection = coreObject->selection(); // We don't support multiple selections for now, so there's only // two possibilities // Also, we don't want to do anything if the selection does not // belong to the currently selected object. We have to check since // there's no way to get the selection for a given object, only // the global one (the API is a bit confusing) return !selectionBelongsToObject(coreObject, selection) || selection.isNone() ? 0 : 1; } static gchar* webkit_accessible_text_get_selection(AtkText* text, gint selection_num, gint* start_offset, gint* end_offset) { AccessibilityObject* coreObject = core(text); VisibleSelection selection = coreObject->selection(); // WebCore does not support multiple selection, so anything but 0 does not make sense for now. // Also, we don't want to do anything if the selection does not // belong to the currently selected object. We have to check since // there's no way to get the selection for a given object, only // the global one (the API is a bit confusing) if (selection_num != 0 || !selectionBelongsToObject(coreObject, selection)) { *start_offset = *end_offset = 0; return 0; } *start_offset = selection.start().offsetInContainerNode(); *end_offset = selection.end().offsetInContainerNode(); return webkit_accessible_text_get_text(text, *start_offset, *end_offset); } static gboolean webkit_accessible_text_add_selection(AtkText* text, gint start_offset, gint end_offset) { notImplemented(); return FALSE; } static gboolean webkit_accessible_text_remove_selection(AtkText* text, gint selection_num) { notImplemented(); return FALSE; } static gboolean webkit_accessible_text_set_selection(AtkText* text, gint selection_num, gint start_offset, gint end_offset) { notImplemented(); return FALSE; } static gboolean webkit_accessible_text_set_caret_offset(AtkText* text, gint offset) { AccessibilityObject* coreObject = core(text); // FIXME: We need to reimplement visiblePositionRangeForRange here // because the actual function checks the offset is within the // boundaries of text().length(), but text() only works for text // controls... VisiblePosition startPosition = coreObject->visiblePositionForIndex(offset); startPosition.setAffinity(DOWNSTREAM); VisiblePosition endPosition = coreObject->visiblePositionForIndex(offset); VisiblePositionRange range = VisiblePositionRange(startPosition, endPosition); coreObject->setSelectedVisiblePositionRange(range); return TRUE; } static void atk_text_interface_init(AtkTextIface* iface) { iface->get_text = webkit_accessible_text_get_text; iface->get_text_after_offset = webkit_accessible_text_get_text_after_offset; iface->get_text_at_offset = webkit_accessible_text_get_text_at_offset; iface->get_character_at_offset = webkit_accessible_text_get_character_at_offset; iface->get_text_before_offset = webkit_accessible_text_get_text_before_offset; iface->get_caret_offset = webkit_accessible_text_get_caret_offset; iface->get_run_attributes = webkit_accessible_text_get_run_attributes; iface->get_default_attributes = webkit_accessible_text_get_default_attributes; iface->get_character_extents = webkit_accessible_text_get_character_extents; iface->get_range_extents = webkit_accessible_text_get_range_extents; iface->get_character_count = webkit_accessible_text_get_character_count; iface->get_offset_at_point = webkit_accessible_text_get_offset_at_point; iface->get_n_selections = webkit_accessible_text_get_n_selections; iface->get_selection = webkit_accessible_text_get_selection; // set methods iface->add_selection = webkit_accessible_text_add_selection; iface->remove_selection = webkit_accessible_text_remove_selection; iface->set_selection = webkit_accessible_text_set_selection; iface->set_caret_offset = webkit_accessible_text_set_caret_offset; } // EditableText static gboolean webkit_accessible_editable_text_set_run_attributes(AtkEditableText* text, AtkAttributeSet* attrib_set, gint start_offset, gint end_offset) { notImplemented(); return FALSE; } static void webkit_accessible_editable_text_set_text_contents(AtkEditableText* text, const gchar* string) { // FIXME: string nullcheck? core(text)->setValue(String::fromUTF8(string)); } static void webkit_accessible_editable_text_insert_text(AtkEditableText* text, const gchar* string, gint length, gint* position) { // FIXME: string nullcheck? AccessibilityObject* coreObject = core(text); // FIXME: Not implemented in WebCore //coreObject->setSelectedTextRange(PlainTextRange(*position, 0)); //coreObject->setSelectedText(String::fromUTF8(string)); if (!coreObject->document() || !coreObject->document()->frame()) return; coreObject->setSelectedVisiblePositionRange(coreObject->visiblePositionRangeForRange(PlainTextRange(*position, 0))); coreObject->setFocused(true); // FIXME: We should set position to the actual inserted text length, which may be less than that requested. if (coreObject->document()->frame()->editor()->insertTextWithoutSendingTextEvent(String::fromUTF8(string), false, 0)) *position += length; } static void webkit_accessible_editable_text_copy_text(AtkEditableText* text, gint start_pos, gint end_pos) { notImplemented(); } static void webkit_accessible_editable_text_cut_text(AtkEditableText* text, gint start_pos, gint end_pos) { notImplemented(); } static void webkit_accessible_editable_text_delete_text(AtkEditableText* text, gint start_pos, gint end_pos) { AccessibilityObject* coreObject = core(text); // FIXME: Not implemented in WebCore //coreObject->setSelectedTextRange(PlainTextRange(start_pos, end_pos - start_pos)); //coreObject->setSelectedText(String()); if (!coreObject->document() || !coreObject->document()->frame()) return; coreObject->setSelectedVisiblePositionRange(coreObject->visiblePositionRangeForRange(PlainTextRange(start_pos, end_pos - start_pos))); coreObject->setFocused(true); coreObject->document()->frame()->editor()->performDelete(); } static void webkit_accessible_editable_text_paste_text(AtkEditableText* text, gint position) { notImplemented(); } static void atk_editable_text_interface_init(AtkEditableTextIface* iface) { iface->set_run_attributes = webkit_accessible_editable_text_set_run_attributes; iface->set_text_contents = webkit_accessible_editable_text_set_text_contents; iface->insert_text = webkit_accessible_editable_text_insert_text; iface->copy_text = webkit_accessible_editable_text_copy_text; iface->cut_text = webkit_accessible_editable_text_cut_text; iface->delete_text = webkit_accessible_editable_text_delete_text; iface->paste_text = webkit_accessible_editable_text_paste_text; } static void contentsToAtk(AccessibilityObject* coreObject, AtkCoordType coordType, IntRect rect, gint* x, gint* y, gint* width = 0, gint* height = 0) { FrameView* frameView = coreObject->documentFrameView(); if (frameView) { switch (coordType) { case ATK_XY_WINDOW: rect = frameView->contentsToWindow(rect); break; case ATK_XY_SCREEN: rect = frameView->contentsToScreen(rect); break; } } if (x) *x = rect.x(); if (y) *y = rect.y(); if (width) *width = rect.width(); if (height) *height = rect.height(); } static IntPoint atkToContents(AccessibilityObject* coreObject, AtkCoordType coordType, gint x, gint y) { IntPoint pos(x, y); FrameView* frameView = coreObject->documentFrameView(); if (frameView) { switch (coordType) { case ATK_XY_SCREEN: return frameView->screenToContents(pos); case ATK_XY_WINDOW: return frameView->windowToContents(pos); } } return pos; } static AtkObject* webkit_accessible_component_ref_accessible_at_point(AtkComponent* component, gint x, gint y, AtkCoordType coordType) { IntPoint pos = atkToContents(core(component), coordType, x, y); AccessibilityObject* target = core(component)->doAccessibilityHitTest(pos); if (!target) return 0; g_object_ref(target->wrapper()); return target->wrapper(); } static void webkit_accessible_component_get_extents(AtkComponent* component, gint* x, gint* y, gint* width, gint* height, AtkCoordType coordType) { IntRect rect = core(component)->elementRect(); contentsToAtk(core(component), coordType, rect, x, y, width, height); } static gboolean webkit_accessible_component_grab_focus(AtkComponent* component) { core(component)->setFocused(true); return core(component)->isFocused(); } static void atk_component_interface_init(AtkComponentIface* iface) { iface->ref_accessible_at_point = webkit_accessible_component_ref_accessible_at_point; iface->get_extents = webkit_accessible_component_get_extents; iface->grab_focus = webkit_accessible_component_grab_focus; } // Image static void webkit_accessible_image_get_image_position(AtkImage* image, gint* x, gint* y, AtkCoordType coordType) { IntRect rect = core(image)->elementRect(); contentsToAtk(core(image), coordType, rect, x, y); } static const gchar* webkit_accessible_image_get_image_description(AtkImage* image) { return returnString(core(image)->accessibilityDescription()); } static void webkit_accessible_image_get_image_size(AtkImage* image, gint* width, gint* height) { IntSize size = core(image)->size(); if (width) *width = size.width(); if (height) *height = size.height(); } static void atk_image_interface_init(AtkImageIface* iface) { iface->get_image_position = webkit_accessible_image_get_image_position; iface->get_image_description = webkit_accessible_image_get_image_description; iface->get_image_size = webkit_accessible_image_get_image_size; } // Table static AccessibilityTableCell* cell(AtkTable* table, guint row, guint column) { AccessibilityObject* accTable = core(table); if (accTable->isAccessibilityRenderObject()) return static_cast(accTable)->cellForColumnAndRow(column, row); return 0; } static gint cellIndex(AccessibilityTableCell* axCell, AccessibilityTable* axTable) { // Calculate the cell's index as if we had a traditional Gtk+ table in // which cells are all direct children of the table, arranged row-first. AccessibilityObject::AccessibilityChildrenVector allCells; axTable->cells(allCells); AccessibilityObject::AccessibilityChildrenVector::iterator position; position = std::find(allCells.begin(), allCells.end(), axCell); if (position == allCells.end()) return -1; return position - allCells.begin(); } static AccessibilityTableCell* cellAtIndex(AtkTable* table, gint index) { AccessibilityObject* accTable = core(table); if (accTable->isAccessibilityRenderObject()) { AccessibilityObject::AccessibilityChildrenVector allCells; static_cast(accTable)->cells(allCells); if (0 <= index && static_cast(index) < allCells.size()) { AccessibilityObject* accCell = allCells.at(index).get(); return static_cast(accCell); } } return 0; } static AtkObject* webkit_accessible_table_ref_at(AtkTable* table, gint row, gint column) { AccessibilityTableCell* axCell = cell(table, row, column); if (!axCell) return 0; return axCell->wrapper(); } static gint webkit_accessible_table_get_index_at(AtkTable* table, gint row, gint column) { AccessibilityTableCell* axCell = cell(table, row, column); AccessibilityTable* axTable = static_cast(core(table)); return cellIndex(axCell, axTable); } static gint webkit_accessible_table_get_column_at_index(AtkTable* table, gint index) { AccessibilityTableCell* axCell = cellAtIndex(table, index); if (axCell){ pair columnRange; axCell->columnIndexRange(columnRange); return columnRange.first; } return -1; } static gint webkit_accessible_table_get_row_at_index(AtkTable* table, gint index) { AccessibilityTableCell* axCell = cellAtIndex(table, index); if (axCell){ pair rowRange; axCell->rowIndexRange(rowRange); return rowRange.first; } return -1; } static gint webkit_accessible_table_get_n_columns(AtkTable* table) { AccessibilityObject* accTable = core(table); if (accTable->isAccessibilityRenderObject()) return static_cast(accTable)->columnCount(); return 0; } static gint webkit_accessible_table_get_n_rows(AtkTable* table) { AccessibilityObject* accTable = core(table); if (accTable->isAccessibilityRenderObject()) return static_cast(accTable)->rowCount(); return 0; } static gint webkit_accessible_table_get_column_extent_at(AtkTable* table, gint row, gint column) { AccessibilityTableCell* axCell = cell(table, row, column); if (axCell) { pair columnRange; axCell->columnIndexRange(columnRange); return columnRange.second; } return 0; } static gint webkit_accessible_table_get_row_extent_at(AtkTable* table, gint row, gint column) { AccessibilityTableCell* axCell = cell(table, row, column); if (axCell) { pair rowRange; axCell->rowIndexRange(rowRange); return rowRange.second; } return 0; } static AtkObject* webkit_accessible_table_get_column_header(AtkTable* table, gint column) { AccessibilityObject* accTable = core(table); if (accTable->isAccessibilityRenderObject()) { AccessibilityObject::AccessibilityChildrenVector allColumnHeaders; static_cast(accTable)->columnHeaders(allColumnHeaders); unsigned columnCount = allColumnHeaders.size(); for (unsigned k = 0; k < columnCount; ++k) { pair columnRange; AccessibilityTableCell* cell = static_cast(allColumnHeaders.at(k).get()); cell->columnIndexRange(columnRange); if (columnRange.first <= column && column < columnRange.first + columnRange.second) return allColumnHeaders[k]->wrapper(); } } return 0; } static AtkObject* webkit_accessible_table_get_row_header(AtkTable* table, gint row) { AccessibilityObject* accTable = core(table); if (accTable->isAccessibilityRenderObject()) { AccessibilityObject::AccessibilityChildrenVector allRowHeaders; static_cast(accTable)->rowHeaders(allRowHeaders); unsigned rowCount = allRowHeaders.size(); for (unsigned k = 0; k < rowCount; ++k) { pair rowRange; AccessibilityTableCell* cell = static_cast(allRowHeaders.at(k).get()); cell->rowIndexRange(rowRange); if (rowRange.first <= row && row < rowRange.first + rowRange.second) return allRowHeaders[k]->wrapper(); } } return 0; } static AtkObject* webkit_accessible_table_get_caption(AtkTable* table) { AccessibilityObject* accTable = core(table); if (accTable->isAccessibilityRenderObject()) { Node* node = static_cast(accTable)->renderer()->node(); if (node && node->hasTagName(HTMLNames::tableTag)) { HTMLTableCaptionElement* caption = static_cast(node)->caption(); if (caption) return AccessibilityObject::firstAccessibleObjectFromNode(caption->renderer()->node())->wrapper(); } } return 0; } static const gchar* webkit_accessible_table_get_column_description(AtkTable* table, gint column) { AtkObject* columnHeader = atk_table_get_column_header(table, column); if (columnHeader && ATK_IS_TEXT(columnHeader)) return webkit_accessible_text_get_text(ATK_TEXT(columnHeader), 0, -1); return 0; } static const gchar* webkit_accessible_table_get_row_description(AtkTable* table, gint row) { AtkObject* rowHeader = atk_table_get_row_header(table, row); if (rowHeader && ATK_IS_TEXT(rowHeader)) return webkit_accessible_text_get_text(ATK_TEXT(rowHeader), 0, -1); return 0; } static void atk_table_interface_init(AtkTableIface* iface) { iface->ref_at = webkit_accessible_table_ref_at; iface->get_index_at = webkit_accessible_table_get_index_at; iface->get_column_at_index = webkit_accessible_table_get_column_at_index; iface->get_row_at_index = webkit_accessible_table_get_row_at_index; iface->get_n_columns = webkit_accessible_table_get_n_columns; iface->get_n_rows = webkit_accessible_table_get_n_rows; iface->get_column_extent_at = webkit_accessible_table_get_column_extent_at; iface->get_row_extent_at = webkit_accessible_table_get_row_extent_at; iface->get_column_header = webkit_accessible_table_get_column_header; iface->get_row_header = webkit_accessible_table_get_row_header; iface->get_caption = webkit_accessible_table_get_caption; iface->get_column_description = webkit_accessible_table_get_column_description; iface->get_row_description = webkit_accessible_table_get_row_description; } static const gchar* documentAttributeValue(AtkDocument* document, const gchar* attribute) { Document* coreDocument = core(document)->document(); if (!coreDocument) return 0; String value = String(); if (!g_ascii_strcasecmp(attribute, "DocType") && coreDocument->doctype()) value = coreDocument->doctype()->name(); else if (!g_ascii_strcasecmp(attribute, "Encoding")) value = coreDocument->charset(); else if (!g_ascii_strcasecmp(attribute, "URI")) value = coreDocument->documentURI(); if (!value.isEmpty()) return returnString(value); return 0; } static const gchar* webkit_accessible_document_get_attribute_value(AtkDocument* document, const gchar* attribute) { return documentAttributeValue(document, attribute); } static AtkAttributeSet* webkit_accessible_document_get_attributes(AtkDocument* document) { AtkAttributeSet* attributeSet = 0; const gchar* attributes [] = {"DocType", "Encoding", "URI"}; for (unsigned i = 0; i < G_N_ELEMENTS(attributes); i++) { const gchar* value = documentAttributeValue(document, attributes[i]); if (value) attributeSet = addAttributeToSet(attributeSet, attributes[i], value); } return attributeSet; } static const gchar* webkit_accessible_document_get_locale(AtkDocument* document) { // TODO: Should we fall back on lang xml:lang when the following comes up empty? String language = core(document)->language(); if (!language.isEmpty()) return returnString(language); return 0; } static void atk_document_interface_init(AtkDocumentIface* iface) { iface->get_document_attribute_value = webkit_accessible_document_get_attribute_value; iface->get_document_attributes = webkit_accessible_document_get_attributes; iface->get_document_locale = webkit_accessible_document_get_locale; } static const GInterfaceInfo AtkInterfacesInitFunctions[] = { {(GInterfaceInitFunc)atk_action_interface_init, (GInterfaceFinalizeFunc) 0, 0}, {(GInterfaceInitFunc)atk_selection_interface_init, (GInterfaceFinalizeFunc) 0, 0}, {(GInterfaceInitFunc)atk_editable_text_interface_init, (GInterfaceFinalizeFunc) 0, 0}, {(GInterfaceInitFunc)atk_text_interface_init, (GInterfaceFinalizeFunc) 0, 0}, {(GInterfaceInitFunc)atk_component_interface_init, (GInterfaceFinalizeFunc) 0, 0}, {(GInterfaceInitFunc)atk_image_interface_init, (GInterfaceFinalizeFunc) 0, 0}, {(GInterfaceInitFunc)atk_table_interface_init, (GInterfaceFinalizeFunc) 0, 0}, {(GInterfaceInitFunc)atk_document_interface_init, (GInterfaceFinalizeFunc) 0, 0} }; enum WAIType { WAI_ACTION, WAI_SELECTION, WAI_EDITABLE_TEXT, WAI_TEXT, WAI_COMPONENT, WAI_IMAGE, WAI_TABLE, WAI_DOCUMENT }; static GType GetAtkInterfaceTypeFromWAIType(WAIType type) { switch (type) { case WAI_ACTION: return ATK_TYPE_ACTION; case WAI_SELECTION: return ATK_TYPE_SELECTION; case WAI_EDITABLE_TEXT: return ATK_TYPE_EDITABLE_TEXT; case WAI_TEXT: return ATK_TYPE_TEXT; case WAI_COMPONENT: return ATK_TYPE_COMPONENT; case WAI_IMAGE: return ATK_TYPE_IMAGE; case WAI_TABLE: return ATK_TYPE_TABLE; case WAI_DOCUMENT: return ATK_TYPE_DOCUMENT; } return G_TYPE_INVALID; } static guint16 getInterfaceMaskFromObject(AccessibilityObject* coreObject) { guint16 interfaceMask = 0; // Component interface is always supported interfaceMask |= 1 << WAI_COMPONENT; // Action if (!coreObject->actionVerb().isEmpty()) interfaceMask |= 1 << WAI_ACTION; // Selection if (coreObject->isListBox()) interfaceMask |= 1 << WAI_SELECTION; // Text & Editable Text AccessibilityRole role = coreObject->roleValue(); if (role == StaticTextRole) interfaceMask |= 1 << WAI_TEXT; else if (coreObject->isAccessibilityRenderObject()) if (coreObject->isTextControl()) { interfaceMask |= 1 << WAI_TEXT; if (!coreObject->isReadOnly()) interfaceMask |= 1 << WAI_EDITABLE_TEXT; } else if (role != TableRole && static_cast(coreObject)->renderer()->childrenInline()) interfaceMask |= 1 << WAI_TEXT; // Image if (coreObject->isImage()) interfaceMask |= 1 << WAI_IMAGE; // Table if (role == TableRole) interfaceMask |= 1 << WAI_TABLE; // Document if (role == WebAreaRole) interfaceMask |= 1 << WAI_DOCUMENT; return interfaceMask; } static const char* getUniqueAccessibilityTypeName(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 getAccessibilityTypeFromObject(AccessibilityObject* coreObject) { static const GTypeInfo typeInfo = { sizeof(WebKitAccessibleClass), (GBaseInitFunc) 0, (GBaseFinalizeFunc) 0, (GClassInitFunc) 0, (GClassFinalizeFunc) 0, 0, /* class data */ sizeof(WebKitAccessible), /* instance size */ 0, /* nb preallocs */ (GInstanceInitFunc) 0, 0 /* value table */ }; guint16 interfaceMask = getInterfaceMaskFromObject(coreObject); const char* atkTypeName = getUniqueAccessibilityTypeName(interfaceMask); GType type = g_type_from_name(atkTypeName); if (type) return type; type = g_type_register_static(WEBKIT_TYPE_ACCESSIBLE, atkTypeName, &typeInfo, GTypeFlags(0)); for (guint i = 0; i < G_N_ELEMENTS(AtkInterfacesInitFunctions); i++) { if (interfaceMask & (1 << i)) g_type_add_interface_static(type, GetAtkInterfaceTypeFromWAIType(static_cast(i)), &AtkInterfacesInitFunctions[i]); } return type; } WebKitAccessible* webkit_accessible_new(AccessibilityObject* coreObject) { GType type = getAccessibilityTypeFromObject(coreObject); AtkObject* object = static_cast(g_object_new(type, 0)); atk_object_initialize(object, coreObject); return WEBKIT_ACCESSIBLE(object); } AccessibilityObject* webkit_accessible_get_accessibility_object(WebKitAccessible* accessible) { return accessible->m_object; } void webkit_accessible_detach(WebKitAccessible* accessible) { ASSERT(accessible->m_object); // We replace the WebCore AccessibilityObject with a fallback object that // provides default implementations to avoid repetitive null-checking after // detachment. accessible->m_object = fallbackObject(); } AtkObject* webkit_accessible_get_focused_element(WebKitAccessible* accessible) { if (!accessible->m_object) return 0; RefPtr focusedObj = accessible->m_object->focusedUIElement(); if (!focusedObj) return 0; return focusedObj->wrapper(); } AccessibilityObject* objectAndOffsetUnignored(AccessibilityObject* coreObject, int& offset, bool ignoreLinks) { Node* endNode = static_cast(coreObject)->renderer()->node(); int endOffset = coreObject->selection().end().computeOffsetInContainerNode(); // Indication that something bogus has transpired. offset = -1; AccessibilityObject* realObject = coreObject; if (realObject->accessibilityIsIgnored()) realObject = realObject->parentObjectUnignored(); if (ignoreLinks && realObject->isLink()) realObject = realObject->parentObjectUnignored(); Node* node = static_cast(realObject)->renderer()->node(); if (node) { RefPtr range = rangeOfContents(node); if (range->ownerDocument() == node->document()) { ExceptionCode ec = 0; range->setEndBefore(endNode, ec); if (range->boundaryPointsValid()) offset = range->text().length() + endOffset; } } return realObject; } #endif // HAVE(ACCESSIBILITY)