b845115e6f29885a28771fc222fa1eada1a7e759
[WebKit-https.git] / Source / WebCore / accessibility / atk / AXObjectCacheAtk.cpp
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
37 namespace WebCore {
38
39 void 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
67 void 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 if the object being added is not -- or not yet -- rendered,
84     // which can occur in nested iframes. In these instances we don't want to ignore the
85     // child. But if an assistive technology is listening, AT-SPI2 will attempt to create
86     // and cache the state set for the child upon emission of the signal. If the object
87     // has not yet been rendered, this will result in a crash.
88     if (!obj->renderer())
89         return;
90
91     // Don't emit the signal for objects whose parents won't be exposed directly.
92     AccessibilityObject* coreParent = obj->parentObjectUnignored();
93     if (!coreParent || coreParent->accessibilityIsIgnoredByDefault())
94         return;
95
96     // Look for the right object to emit the signal from.
97     AtkObject* atkParent = coreParent->wrapper();
98     if (!atkParent)
99         return;
100
101     size_t index = coreParent->children(false).find(obj);
102     g_signal_emit_by_name(atkParent, "children-changed::add", index, atkObj);
103 }
104
105 static AccessibilityObject* getListObject(AccessibilityObject* object)
106 {
107     // Only list boxes and menu lists supported so far.
108     if (!object->isListBox() && !object->isMenuList())
109         return 0;
110
111     // For list boxes the list object is just itself.
112     if (object->isListBox())
113         return object;
114
115     // For menu lists we need to return the first accessible child,
116     // with role MenuListPopupRole, since that's the one holding the list
117     // of items with role MenuListOptionRole.
118     const AccessibilityObject::AccessibilityChildrenVector& children = object->children();
119     if (!children.size())
120         return 0;
121
122     AccessibilityObject* listObject = children.at(0).get();
123     if (!listObject->isMenuListPopup())
124         return 0;
125
126     return listObject;
127 }
128
129 static void notifyChildrenSelectionChange(AccessibilityObject* object)
130 {
131     // This static variables are needed to keep track of the old
132     // focused object and its associated list object, as per previous
133     // calls to this function, in order to properly decide whether to
134     // emit some signals or not.
135     static NeverDestroyed<RefPtr<AccessibilityObject>> oldListObject;
136     static NeverDestroyed<RefPtr<AccessibilityObject>> oldFocusedObject;
137
138     // Only list boxes and menu lists supported so far.
139     if (!object || !(object->isListBox() || object->isMenuList()))
140         return;
141
142     // Only support HTML select elements so far (ARIA selectors not supported).
143     Node* node = object->node();
144     if (!is<HTMLSelectElement>(node))
145         return;
146
147     // Emit signal from the listbox's point of view first.
148     g_signal_emit_by_name(object->wrapper(), "selection-changed");
149
150     // Find the item where the selection change was triggered from.
151     HTMLSelectElement& select = downcast<HTMLSelectElement>(*node);
152     int changedItemIndex = select.activeSelectionStartListIndex();
153
154     AccessibilityObject* listObject = getListObject(object);
155     if (!listObject) {
156         oldListObject.get() = 0;
157         return;
158     }
159
160     const AccessibilityObject::AccessibilityChildrenVector& items = listObject->children();
161     if (changedItemIndex < 0 || changedItemIndex >= static_cast<int>(items.size()))
162         return;
163     AccessibilityObject* item = items.at(changedItemIndex).get();
164
165     // Ensure the current list object is the same than the old one so
166     // further comparisons make sense. Otherwise, just reset
167     // oldFocusedObject so it won't be taken into account.
168     if (oldListObject.get() != listObject)
169         oldFocusedObject.get() = 0;
170
171     AtkObject* axItem = item ? item->wrapper() : nullptr;
172     AtkObject* axOldFocusedObject = oldFocusedObject.get() ? oldFocusedObject.get()->wrapper() : nullptr;
173
174     // Old focused object just lost focus, so emit the events.
175     if (axOldFocusedObject && axItem != axOldFocusedObject) {
176         g_signal_emit_by_name(axOldFocusedObject, "focus-event", false);
177         atk_object_notify_state_change(axOldFocusedObject, ATK_STATE_FOCUSED, false);
178     }
179
180     // Emit needed events for the currently (un)selected item.
181     if (axItem) {
182         bool isSelected = item->isSelected();
183         atk_object_notify_state_change(axItem, ATK_STATE_SELECTED, isSelected);
184         // When the selection changes in a collapsed widget such as a combo box
185         // whose child menu is not showing, that collapsed widget retains focus.
186         if (!object->isCollapsed()) {
187             g_signal_emit_by_name(axItem, "focus-event", isSelected);
188             atk_object_notify_state_change(axItem, ATK_STATE_FOCUSED, isSelected);
189         }
190     }
191
192     // Update pointers to the previously involved objects.
193     oldListObject.get() = listObject;
194     oldFocusedObject.get() = item;
195 }
196
197 void AXObjectCache::postPlatformNotification(AccessibilityObject* coreObject, AXNotification notification)
198 {
199     AtkObject* axObject = coreObject->wrapper();
200     if (!axObject)
201         return;
202
203     switch (notification) {
204     case AXCheckedStateChanged:
205         if (!coreObject->isCheckboxOrRadio() && !coreObject->isSwitch())
206             return;
207         atk_object_notify_state_change(axObject, ATK_STATE_CHECKED, coreObject->isChecked());
208         break;
209
210     case AXSelectedChildrenChanged:
211     case AXMenuListValueChanged:
212         // Accessible focus claims should not be made if the associated widget is not focused.
213         if (notification == AXMenuListValueChanged && coreObject->isMenuList() && coreObject->isFocused()) {
214             g_signal_emit_by_name(axObject, "focus-event", true);
215             atk_object_notify_state_change(axObject, ATK_STATE_FOCUSED, true);
216         }
217         notifyChildrenSelectionChange(coreObject);
218         break;
219
220     case AXValueChanged:
221         if (ATK_IS_VALUE(axObject)) {
222             AtkPropertyValues propertyValues;
223             propertyValues.property_name = "accessible-value";
224
225             memset(&propertyValues.new_value,  0, sizeof(GValue));
226 #if ATK_CHECK_VERSION(2,11,92)
227             double value;
228             atk_value_get_value_and_text(ATK_VALUE(axObject), &value, nullptr);
229             g_value_set_double(g_value_init(&propertyValues.new_value, G_TYPE_DOUBLE), value);
230 #else
231             atk_value_get_current_value(ATK_VALUE(axObject), &propertyValues.new_value);
232 #endif
233
234             g_signal_emit_by_name(ATK_OBJECT(axObject), "property-change::accessible-value", &propertyValues, NULL);
235         }
236         break;
237
238     case AXInvalidStatusChanged:
239         atk_object_notify_state_change(axObject, ATK_STATE_INVALID_ENTRY, coreObject->invalidStatus() != "false");
240         break;
241
242     default:
243         break;
244     }
245 }
246
247 void AXObjectCache::nodeTextChangePlatformNotification(AccessibilityObject* object, AXTextChange textChange, unsigned offset, const String& text)
248 {
249     if (!object || text.isEmpty())
250         return;
251
252     AccessibilityObject* parentObject = object->parentObjectUnignored();
253     if (!parentObject)
254         return;
255
256     AtkObject* wrapper = parentObject->wrapper();
257     if (!wrapper || !ATK_IS_TEXT(wrapper))
258         return;
259
260     Node* node = object->node();
261     if (!node)
262         return;
263
264     // Ensure document's layout is up-to-date before using TextIterator.
265     Document& document = node->document();
266     document.updateLayout();
267
268     // Select the right signal to be emitted
269     CString detail;
270     switch (textChange) {
271     case AXTextInserted:
272         detail = "text-insert";
273         break;
274     case AXTextDeleted:
275         detail = "text-remove";
276         break;
277     case AXTextAttributesChanged:
278         detail = "text-attributes-changed";
279         break;
280     }
281
282     String textToEmit = text;
283     unsigned offsetToEmit = offset;
284
285     // If the object we're emitting the signal from represents a
286     // password field, we will emit the masked text.
287     if (parentObject->isPasswordField()) {
288         String maskedText = parentObject->passwordFieldValue();
289         textToEmit = maskedText.substring(offset, text.length());
290     } else {
291         // Consider previous text objects that might be present for
292         // the current accessibility object to ensure we emit the
293         // right offset (e.g. multiline text areas).
294         RefPtr<Range> range = Range::create(document, node->parentNode(), 0, node, 0);
295         offsetToEmit = offset + TextIterator::rangeLength(range.get());
296     }
297
298     g_signal_emit_by_name(wrapper, detail.data(), offsetToEmit, textToEmit.length(), textToEmit.utf8().data());
299 }
300
301 void AXObjectCache::frameLoadingEventPlatformNotification(AccessibilityObject* object, AXLoadingEvent loadingEvent)
302 {
303     if (!object)
304         return;
305
306     AtkObject* axObject = object->wrapper();
307     if (!axObject || !ATK_IS_DOCUMENT(axObject))
308         return;
309
310     switch (loadingEvent) {
311     case AXObjectCache::AXLoadingStarted:
312         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, true);
313         break;
314     case AXObjectCache::AXLoadingReloaded:
315         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, true);
316         g_signal_emit_by_name(axObject, "reload");
317         break;
318     case AXObjectCache::AXLoadingFailed:
319         g_signal_emit_by_name(axObject, "load-stopped");
320         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, false);
321         break;
322     case AXObjectCache::AXLoadingFinished:
323         g_signal_emit_by_name(axObject, "load-complete");
324         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, false);
325         break;
326     }
327 }
328
329 void AXObjectCache::platformHandleFocusedUIElementChanged(Node* oldFocusedNode, Node* newFocusedNode)
330 {
331     RefPtr<AccessibilityObject> oldObject = getOrCreate(oldFocusedNode);
332     if (oldObject) {
333         g_signal_emit_by_name(oldObject->wrapper(), "focus-event", false);
334         atk_object_notify_state_change(oldObject->wrapper(), ATK_STATE_FOCUSED, false);
335     }
336     RefPtr<AccessibilityObject> newObject = getOrCreate(newFocusedNode);
337     if (newObject) {
338         g_signal_emit_by_name(newObject->wrapper(), "focus-event", true);
339         atk_object_notify_state_change(newObject->wrapper(), ATK_STATE_FOCUSED, true);
340     }
341 }
342
343 void AXObjectCache::handleScrolledToAnchor(const Node*)
344 {
345 }
346
347 } // namespace WebCore
348
349 #endif