1/* 2 * Copyright (C) 2008 Nuanti Ltd. 3 * 4 * This library is free software; you can redistribute it and/or 5 * modify it under the terms of the GNU Library General Public 6 * License as published by the Free Software Foundation; either 7 * version 2 of the License, or (at your option) any later version. 8 * 9 * This library is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 * Library General Public License for more details. 13 * 14 * You should have received a copy of the GNU Library General Public License 15 * along with this library; see the file COPYING.LIB. If not, write to 16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 17 * Boston, MA 02110-1301, USA. 18 */ 19 20#include "config.h" 21#include "AXObjectCache.h" 22 23#if HAVE(ACCESSIBILITY) 24 25#include "AccessibilityObject.h" 26#include "AccessibilityRenderObject.h" 27#include "Document.h" 28#include "Element.h" 29#include "HTMLSelectElement.h" 30#include "Range.h" 31#include "TextIterator.h" 32#include "WebKitAccessibleWrapperAtk.h" 33#include <wtf/NeverDestroyed.h> 34#include <wtf/gobject/GRefPtr.h> 35#include <wtf/text/CString.h> 36 37namespace WebCore { 38 39void AXObjectCache::detachWrapper(AccessibilityObject* obj, AccessibilityDetachmentType detachmentType) 40{ 41 AtkObject* wrapper = obj->wrapper(); 42 ASSERT(wrapper); 43 44 // If an object is being detached NOT because of the AXObjectCache being destroyed, 45 // then it's being removed from the accessibility tree and we should emit a signal. 46 if (detachmentType != CacheDestroyed) { 47 if (obj->document()) { 48 // Look for the right object to emit the signal from, but using the implementation 49 // of atk_object_get_parent from AtkObject class (which uses a cached pointer if set) 50 // since the accessibility hierarchy in WebCore will no longer be navigable. 51 gpointer webkitAccessibleClass = g_type_class_peek_parent(WEBKIT_ACCESSIBLE_GET_CLASS(wrapper)); 52 gpointer atkObjectClass = g_type_class_peek_parent(webkitAccessibleClass); 53 AtkObject* atkParent = ATK_OBJECT_CLASS(atkObjectClass)->get_parent(ATK_OBJECT(wrapper)); 54 55 // We don't want to emit any signal from an object outside WebKit's world. 56 if (WEBKIT_IS_ACCESSIBLE(atkParent)) { 57 // The accessibility hierarchy is already invalid, so the parent-children relationships 58 // in the AccessibilityObject tree are not there anymore, so we can't know the offset. 59 g_signal_emit_by_name(atkParent, "children-changed::remove", -1, wrapper); 60 } 61 } 62 } 63 64 webkitAccessibleDetach(WEBKIT_ACCESSIBLE(wrapper)); 65} 66 67void AXObjectCache::attachWrapper(AccessibilityObject* obj) 68{ 69 AtkObject* atkObj = ATK_OBJECT(webkitAccessibleNew(obj)); 70 obj->setWrapper(atkObj); 71 g_object_unref(atkObj); 72 73 // If an object is being attached and we are not in the middle of a layout update, then 74 // we should report ATs by emitting the children-changed::add signal from the parent. 75 Document* document = obj->document(); 76 if (!document || document->childNeedsStyleRecalc()) 77 return; 78 79 // Don't emit the signal when the actual object being added is not going to be exposed. 80 if (obj->accessibilityIsIgnoredByDefault()) 81 return; 82 83 // Don't emit the signal for objects whose parents won't be exposed directly. 84 AccessibilityObject* coreParent = obj->parentObjectUnignored(); 85 if (!coreParent || coreParent->accessibilityIsIgnoredByDefault()) 86 return; 87 88 // Look for the right object to emit the signal from. 89 AtkObject* atkParent = coreParent->wrapper(); 90 if (!atkParent) 91 return; 92 93 size_t index = coreParent->children(false).find(obj); 94 g_signal_emit_by_name(atkParent, "children-changed::add", index, atkObj); 95} 96 97static AccessibilityObject* getListObject(AccessibilityObject* object) 98{ 99 // Only list boxes and menu lists supported so far. 100 if (!object->isListBox() && !object->isMenuList()) 101 return 0; 102 103 // For list boxes the list object is just itself. 104 if (object->isListBox()) 105 return object; 106 107 // For menu lists we need to return the first accessible child, 108 // with role MenuListPopupRole, since that's the one holding the list 109 // of items with role MenuListOptionRole. 110 const AccessibilityObject::AccessibilityChildrenVector& children = object->children(); 111 if (!children.size()) 112 return 0; 113 114 AccessibilityObject* listObject = children.at(0).get(); 115 if (!listObject->isMenuListPopup()) 116 return 0; 117 118 return listObject; 119} 120 121static void notifyChildrenSelectionChange(AccessibilityObject* object) 122{ 123 // This static variables are needed to keep track of the old 124 // focused object and its associated list object, as per previous 125 // calls to this function, in order to properly decide whether to 126 // emit some signals or not. 127 static NeverDestroyed<RefPtr<AccessibilityObject>> oldListObject; 128 static NeverDestroyed<RefPtr<AccessibilityObject>> oldFocusedObject; 129 130 // Only list boxes and menu lists supported so far. 131 if (!object || !(object->isListBox() || object->isMenuList())) 132 return; 133 134 // Only support HTML select elements so far (ARIA selectors not supported). 135 Node* node = object->node(); 136 if (!node || !isHTMLSelectElement(node)) 137 return; 138 139 // Emit signal from the listbox's point of view first. 140 g_signal_emit_by_name(object->wrapper(), "selection-changed"); 141 142 // Find the item where the selection change was triggered from. 143 HTMLSelectElement* select = toHTMLSelectElement(node); 144 if (!select) 145 return; 146 int changedItemIndex = select->activeSelectionStartListIndex(); 147 148 AccessibilityObject* listObject = getListObject(object); 149 if (!listObject) { 150 oldListObject.get() = 0; 151 return; 152 } 153 154 const AccessibilityObject::AccessibilityChildrenVector& items = listObject->children(); 155 if (changedItemIndex < 0 || changedItemIndex >= static_cast<int>(items.size())) 156 return; 157 AccessibilityObject* item = items.at(changedItemIndex).get(); 158 159 // Ensure the current list object is the same than the old one so 160 // further comparisons make sense. Otherwise, just reset 161 // oldFocusedObject so it won't be taken into account. 162 if (oldListObject.get() != listObject) 163 oldFocusedObject.get() = 0; 164 165 AtkObject* axItem = item ? item->wrapper() : 0; 166 AtkObject* axOldFocusedObject = oldFocusedObject.get() ? oldFocusedObject.get()->wrapper() : 0; 167 168 // Old focused object just lost focus, so emit the events. 169 if (axOldFocusedObject && axItem != axOldFocusedObject) { 170 g_signal_emit_by_name(axOldFocusedObject, "focus-event", false); 171 atk_object_notify_state_change(axOldFocusedObject, ATK_STATE_FOCUSED, false); 172 } 173 174 // Emit needed events for the currently (un)selected item. 175 if (axItem) { 176 bool isSelected = item->isSelected(); 177 atk_object_notify_state_change(axItem, ATK_STATE_SELECTED, isSelected); 178 g_signal_emit_by_name(axItem, "focus-event", isSelected); 179 atk_object_notify_state_change(axItem, ATK_STATE_FOCUSED, isSelected); 180 } 181 182 // Update pointers to the previously involved objects. 183 oldListObject.get() = listObject; 184 oldFocusedObject.get() = item; 185} 186 187void AXObjectCache::postPlatformNotification(AccessibilityObject* coreObject, AXNotification notification) 188{ 189 AtkObject* axObject = coreObject->wrapper(); 190 if (!axObject) 191 return; 192 193 switch (notification) { 194 case AXCheckedStateChanged: 195 if (!coreObject->isCheckboxOrRadio()) 196 return; 197 atk_object_notify_state_change(axObject, ATK_STATE_CHECKED, coreObject->isChecked()); 198 break; 199 200 case AXSelectedChildrenChanged: 201 case AXMenuListValueChanged: 202 if (notification == AXMenuListValueChanged && coreObject->isMenuList()) { 203 g_signal_emit_by_name(axObject, "focus-event", true); 204 atk_object_notify_state_change(axObject, ATK_STATE_FOCUSED, true); 205 } 206 notifyChildrenSelectionChange(coreObject); 207 break; 208 209 case AXValueChanged: 210 if (ATK_IS_VALUE(axObject)) { 211 AtkPropertyValues propertyValues; 212 propertyValues.property_name = "accessible-value"; 213 214 memset(&propertyValues.new_value, 0, sizeof(GValue)); 215#if ATK_CHECK_VERSION(2,11,92) 216 double value; 217 atk_value_get_value_and_text(ATK_VALUE(axObject), &value, nullptr); 218 g_value_set_double(g_value_init(&propertyValues.new_value, G_TYPE_DOUBLE), value); 219#else 220 atk_value_get_current_value(ATK_VALUE(axObject), &propertyValues.new_value); 221#endif 222 223 g_signal_emit_by_name(ATK_OBJECT(axObject), "property-change::accessible-value", &propertyValues, NULL); 224 } 225 break; 226 227 case AXInvalidStatusChanged: 228 atk_object_notify_state_change(axObject, ATK_STATE_INVALID_ENTRY, coreObject->invalidStatus() != "false"); 229 break; 230 231 default: 232 break; 233 } 234} 235 236void AXObjectCache::nodeTextChangePlatformNotification(AccessibilityObject* object, AXTextChange textChange, unsigned offset, const String& text) 237{ 238 if (!object || text.isEmpty()) 239 return; 240 241 AccessibilityObject* parentObject = object->parentObjectUnignored(); 242 if (!parentObject) 243 return; 244 245 AtkObject* wrapper = parentObject->wrapper(); 246 if (!wrapper || !ATK_IS_TEXT(wrapper)) 247 return; 248 249 Node* node = object->node(); 250 if (!node) 251 return; 252 253 // Ensure document's layout is up-to-date before using TextIterator. 254 Document& document = node->document(); 255 document.updateLayout(); 256 257 // Select the right signal to be emitted 258 CString detail; 259 switch (textChange) { 260 case AXObjectCache::AXTextInserted: 261 detail = "text-insert"; 262 break; 263 case AXObjectCache::AXTextDeleted: 264 detail = "text-remove"; 265 break; 266 } 267 268 String textToEmit = text; 269 unsigned offsetToEmit = offset; 270 271 // If the object we're emitting the signal from represents a 272 // password field, we will emit the masked text. 273 if (parentObject->isPasswordField()) { 274 String maskedText = parentObject->passwordFieldValue(); 275 textToEmit = maskedText.substring(offset, text.length()); 276 } else { 277 // Consider previous text objects that might be present for 278 // the current accessibility object to ensure we emit the 279 // right offset (e.g. multiline text areas). 280 RefPtr<Range> range = Range::create(document, node->parentNode(), 0, node, 0); 281 offsetToEmit = offset + TextIterator::rangeLength(range.get()); 282 } 283 284 g_signal_emit_by_name(wrapper, detail.data(), offsetToEmit, textToEmit.length(), textToEmit.utf8().data()); 285} 286 287void AXObjectCache::frameLoadingEventPlatformNotification(AccessibilityObject* object, AXLoadingEvent loadingEvent) 288{ 289 if (!object) 290 return; 291 292 AtkObject* axObject = object->wrapper(); 293 if (!axObject || !ATK_IS_DOCUMENT(axObject)) 294 return; 295 296 switch (loadingEvent) { 297 case AXObjectCache::AXLoadingStarted: 298 atk_object_notify_state_change(axObject, ATK_STATE_BUSY, true); 299 break; 300 case AXObjectCache::AXLoadingReloaded: 301 atk_object_notify_state_change(axObject, ATK_STATE_BUSY, true); 302 g_signal_emit_by_name(axObject, "reload"); 303 break; 304 case AXObjectCache::AXLoadingFailed: 305 g_signal_emit_by_name(axObject, "load-stopped"); 306 atk_object_notify_state_change(axObject, ATK_STATE_BUSY, false); 307 break; 308 case AXObjectCache::AXLoadingFinished: 309 g_signal_emit_by_name(axObject, "load-complete"); 310 atk_object_notify_state_change(axObject, ATK_STATE_BUSY, false); 311 break; 312 } 313} 314 315void AXObjectCache::platformHandleFocusedUIElementChanged(Node* oldFocusedNode, Node* newFocusedNode) 316{ 317 RefPtr<AccessibilityObject> oldObject = getOrCreate(oldFocusedNode); 318 if (oldObject) { 319 g_signal_emit_by_name(oldObject->wrapper(), "focus-event", false); 320 atk_object_notify_state_change(oldObject->wrapper(), ATK_STATE_FOCUSED, false); 321 } 322 RefPtr<AccessibilityObject> newObject = getOrCreate(newFocusedNode); 323 if (newObject) { 324 g_signal_emit_by_name(newObject->wrapper(), "focus-event", true); 325 atk_object_notify_state_change(newObject->wrapper(), ATK_STATE_FOCUSED, true); 326 } 327} 328 329void AXObjectCache::handleScrolledToAnchor(const Node*) 330{ 331} 332 333} // namespace WebCore 334 335#endif 336