/* * Copyright (C) 2008 Nuanti Ltd. * * 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 "AXObjectCache.h" #if HAVE(ACCESSIBILITY) #include "AccessibilityObject.h" #include "AccessibilityRenderObject.h" #include "Document.h" #include "Element.h" #include "HTMLSelectElement.h" #include "Range.h" #include "TextIterator.h" #include "WebKitAccessibleWrapperAtk.h" #include #include #include namespace WebCore { void AXObjectCache::detachWrapper(AccessibilityObject* obj, AccessibilityDetachmentType detachmentType) { AtkObject* wrapper = obj->wrapper(); ASSERT(wrapper); // If an object is being detached NOT because of the AXObjectCache being destroyed, // then it's being removed from the accessibility tree and we should emit a signal. if (detachmentType != CacheDestroyed) { if (obj->document()) { // Look for the right object to emit the signal from, but using the implementation // of atk_object_get_parent from AtkObject class (which uses a cached pointer if set) // since the accessibility hierarchy in WebCore will no longer be navigable. gpointer webkitAccessibleClass = g_type_class_peek_parent(WEBKIT_ACCESSIBLE_GET_CLASS(wrapper)); gpointer atkObjectClass = g_type_class_peek_parent(webkitAccessibleClass); AtkObject* atkParent = ATK_OBJECT_CLASS(atkObjectClass)->get_parent(ATK_OBJECT(wrapper)); // We don't want to emit any signal from an object outside WebKit's world. if (WEBKIT_IS_ACCESSIBLE(atkParent)) { // The accessibility hierarchy is already invalid, so the parent-children relationships // in the AccessibilityObject tree are not there anymore, so we can't know the offset. g_signal_emit_by_name(atkParent, "children-changed::remove", -1, wrapper); } } } webkitAccessibleDetach(WEBKIT_ACCESSIBLE(wrapper)); } void AXObjectCache::attachWrapper(AccessibilityObject* obj) { AtkObject* atkObj = ATK_OBJECT(webkitAccessibleNew(obj)); obj->setWrapper(atkObj); g_object_unref(atkObj); // If an object is being attached and we are not in the middle of a layout update, then // we should report ATs by emitting the children-changed::add signal from the parent. Document* document = obj->document(); if (!document || document->childNeedsStyleRecalc()) return; // Don't emit the signal when the actual object being added is not going to be exposed. if (obj->accessibilityIsIgnoredByDefault()) return; // Don't emit the signal if the object being added is not -- or not yet -- rendered, // which can occur in nested iframes. In these instances we don't want to ignore the // child. But if an assistive technology is listening, AT-SPI2 will attempt to create // and cache the state set for the child upon emission of the signal. If the object // has not yet been rendered, this will result in a crash. if (!obj->renderer()) return; // Don't emit the signal for objects whose parents won't be exposed directly. AccessibilityObject* coreParent = obj->parentObjectUnignored(); if (!coreParent || coreParent->accessibilityIsIgnoredByDefault()) return; // Look for the right object to emit the signal from. AtkObject* atkParent = coreParent->wrapper(); if (!atkParent) return; size_t index = coreParent->children(false).find(obj); g_signal_emit_by_name(atkParent, "children-changed::add", index, atkObj); } static AccessibilityObject* getListObject(AccessibilityObject* object) { // Only list boxes and menu lists supported so far. if (!object->isListBox() && !object->isMenuList()) return 0; // For list boxes the list object is just itself. if (object->isListBox()) return object; // For menu lists we need to return the first accessible child, // with role MenuListPopupRole, since that's the one holding the list // of items with role MenuListOptionRole. const AccessibilityObject::AccessibilityChildrenVector& children = object->children(); if (!children.size()) return 0; AccessibilityObject* listObject = children.at(0).get(); if (!listObject->isMenuListPopup()) return 0; return listObject; } static void notifyChildrenSelectionChange(AccessibilityObject* object) { // This static variables are needed to keep track of the old // focused object and its associated list object, as per previous // calls to this function, in order to properly decide whether to // emit some signals or not. static NeverDestroyed> oldListObject; static NeverDestroyed> oldFocusedObject; // Only list boxes and menu lists supported so far. if (!object || !(object->isListBox() || object->isMenuList())) return; // Only support HTML select elements so far (ARIA selectors not supported). Node* node = object->node(); if (!is(node)) return; // Emit signal from the listbox's point of view first. g_signal_emit_by_name(object->wrapper(), "selection-changed"); // Find the item where the selection change was triggered from. HTMLSelectElement& select = downcast(*node); int changedItemIndex = select.activeSelectionStartListIndex(); AccessibilityObject* listObject = getListObject(object); if (!listObject) { oldListObject.get() = nullptr; return; } const AccessibilityObject::AccessibilityChildrenVector& items = listObject->children(); if (changedItemIndex < 0 || changedItemIndex >= static_cast(items.size())) return; AccessibilityObject* item = items.at(changedItemIndex).get(); // Ensure the current list object is the same than the old one so // further comparisons make sense. Otherwise, just reset // oldFocusedObject so it won't be taken into account. if (oldListObject.get() != listObject) oldFocusedObject.get() = nullptr; AtkObject* axItem = item ? item->wrapper() : nullptr; AtkObject* axOldFocusedObject = oldFocusedObject.get() ? oldFocusedObject.get()->wrapper() : nullptr; // Old focused object just lost focus, so emit the events. if (axOldFocusedObject && axItem != axOldFocusedObject) { g_signal_emit_by_name(axOldFocusedObject, "focus-event", false); atk_object_notify_state_change(axOldFocusedObject, ATK_STATE_FOCUSED, false); } // Emit needed events for the currently (un)selected item. if (axItem) { bool isSelected = item->isSelected(); atk_object_notify_state_change(axItem, ATK_STATE_SELECTED, isSelected); // When the selection changes in a collapsed widget such as a combo box // whose child menu is not showing, that collapsed widget retains focus. if (!object->isCollapsed()) { g_signal_emit_by_name(axItem, "focus-event", isSelected); atk_object_notify_state_change(axItem, ATK_STATE_FOCUSED, isSelected); } } // Update pointers to the previously involved objects. oldListObject.get() = listObject; oldFocusedObject.get() = item; } void AXObjectCache::postPlatformNotification(AccessibilityObject* coreObject, AXNotification notification) { AtkObject* axObject = coreObject->wrapper(); if (!axObject) return; switch (notification) { case AXCheckedStateChanged: if (!coreObject->isCheckboxOrRadio() && !coreObject->isSwitch()) return; atk_object_notify_state_change(axObject, ATK_STATE_CHECKED, coreObject->isChecked()); break; case AXSelectedChildrenChanged: case AXMenuListValueChanged: // Accessible focus claims should not be made if the associated widget is not focused. if (notification == AXMenuListValueChanged && coreObject->isMenuList() && coreObject->isFocused()) { g_signal_emit_by_name(axObject, "focus-event", true); atk_object_notify_state_change(axObject, ATK_STATE_FOCUSED, true); } notifyChildrenSelectionChange(coreObject); break; case AXValueChanged: if (ATK_IS_VALUE(axObject)) { AtkPropertyValues propertyValues; propertyValues.property_name = "accessible-value"; memset(&propertyValues.new_value, 0, sizeof(GValue)); #if ATK_CHECK_VERSION(2,11,92) double value; atk_value_get_value_and_text(ATK_VALUE(axObject), &value, nullptr); g_value_set_double(g_value_init(&propertyValues.new_value, G_TYPE_DOUBLE), value); #else atk_value_get_current_value(ATK_VALUE(axObject), &propertyValues.new_value); #endif g_signal_emit_by_name(ATK_OBJECT(axObject), "property-change::accessible-value", &propertyValues, NULL); } break; case AXInvalidStatusChanged: atk_object_notify_state_change(axObject, ATK_STATE_INVALID_ENTRY, coreObject->invalidStatus() != "false"); break; default: break; } } void AXObjectCache::nodeTextChangePlatformNotification(AccessibilityObject* object, AXTextChange textChange, unsigned offset, const String& text) { if (!object || text.isEmpty()) return; AccessibilityObject* parentObject = object->isNonNativeTextControl() ? object : object->parentObjectUnignored(); if (!parentObject) return; AtkObject* wrapper = parentObject->wrapper(); if (!wrapper || !ATK_IS_TEXT(wrapper)) return; Node* node = object->node(); if (!node) return; // Ensure document's layout is up-to-date before using TextIterator. Document& document = node->document(); document.updateLayout(); // Select the right signal to be emitted CString detail; switch (textChange) { case AXTextInserted: detail = "text-insert"; break; case AXTextDeleted: detail = "text-remove"; break; case AXTextAttributesChanged: detail = "text-attributes-changed"; break; } String textToEmit = text; unsigned offsetToEmit = offset; // If the object we're emitting the signal from represents a // password field, we will emit the masked text. if (parentObject->isPasswordField()) { String maskedText = parentObject->passwordFieldValue(); textToEmit = maskedText.substring(offset, text.length()); } else { // Consider previous text objects that might be present for // the current accessibility object to ensure we emit the // right offset (e.g. multiline text areas). RefPtr range = Range::create(document, node->parentNode(), 0, node, 0); offsetToEmit = offset + TextIterator::rangeLength(range.get()); } g_signal_emit_by_name(wrapper, detail.data(), offsetToEmit, textToEmit.length(), textToEmit.utf8().data()); } void AXObjectCache::frameLoadingEventPlatformNotification(AccessibilityObject* object, AXLoadingEvent loadingEvent) { if (!object) return; AtkObject* axObject = object->wrapper(); if (!axObject || !ATK_IS_DOCUMENT(axObject)) return; switch (loadingEvent) { case AXObjectCache::AXLoadingStarted: atk_object_notify_state_change(axObject, ATK_STATE_BUSY, true); break; case AXObjectCache::AXLoadingReloaded: atk_object_notify_state_change(axObject, ATK_STATE_BUSY, true); g_signal_emit_by_name(axObject, "reload"); break; case AXObjectCache::AXLoadingFailed: g_signal_emit_by_name(axObject, "load-stopped"); atk_object_notify_state_change(axObject, ATK_STATE_BUSY, false); break; case AXObjectCache::AXLoadingFinished: g_signal_emit_by_name(axObject, "load-complete"); atk_object_notify_state_change(axObject, ATK_STATE_BUSY, false); break; } } void AXObjectCache::platformHandleFocusedUIElementChanged(Node* oldFocusedNode, Node* newFocusedNode) { RefPtr oldObject = getOrCreate(oldFocusedNode); if (oldObject) { g_signal_emit_by_name(oldObject->wrapper(), "focus-event", false); atk_object_notify_state_change(oldObject->wrapper(), ATK_STATE_FOCUSED, false); } RefPtr newObject = getOrCreate(newFocusedNode); if (newObject) { g_signal_emit_by_name(newObject->wrapper(), "focus-event", true); atk_object_notify_state_change(newObject->wrapper(), ATK_STATE_FOCUSED, true); } } void AXObjectCache::handleScrolledToAnchor(const Node*) { } } // namespace WebCore #endif