[ATK] Defer the emision of AtkObject::children-changed signal after layout is done
[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 "WebKitAccessible.h"
33 #include <wtf/NeverDestroyed.h>
34 #include <wtf/glib/GRefPtr.h>
35 #include <wtf/text/CString.h>
36
37 namespace WebCore {
38
39 static AtkObject* wrapperParent(WebKitAccessible* wrapper)
40 {
41     // Look for the right object to emit the signal from, but using the implementation
42     // of atk_object_get_parent from AtkObject class (which uses a cached pointer if set)
43     // since the accessibility hierarchy in WebCore will no longer be navigable.
44     gpointer webkitAccessibleClass = g_type_class_peek_parent(WEBKIT_ACCESSIBLE_GET_CLASS(wrapper));
45     gpointer atkObjectClass = g_type_class_peek_parent(webkitAccessibleClass);
46     AtkObject* atkParent = ATK_OBJECT_CLASS(atkObjectClass)->get_parent(ATK_OBJECT(wrapper));
47     // We don't want to emit any signal from an object outside WebKit's world.
48     return WEBKIT_IS_ACCESSIBLE(atkParent) ? atkParent : nullptr;
49 }
50
51 void AXObjectCache::detachWrapper(AccessibilityObject* obj, AccessibilityDetachmentType detachmentType)
52 {
53     auto* wrapper = obj->wrapper();
54     ASSERT(wrapper);
55
56     // If an object is being detached NOT because of the AXObjectCache being destroyed,
57     // then it's being removed from the accessibility tree and we should emit a signal.
58     if (detachmentType != AccessibilityDetachmentType::CacheDestroyed && obj->document() && wrapperParent(wrapper))
59         m_deferredDetachedWrapperList.add(wrapper);
60
61     webkitAccessibleDetach(WEBKIT_ACCESSIBLE(wrapper));
62 }
63
64 void AXObjectCache::attachWrapper(AccessibilityObject* obj)
65 {
66     GRefPtr<WebKitAccessible> wrapper = adoptGRef(webkitAccessibleNew(obj));
67     obj->setWrapper(wrapper.get());
68
69     // If an object is being attached and we are not in the middle of a layout update, then
70     // we should report ATs by emitting the children-changed::add signal from the parent.
71     Document* document = obj->document();
72     if (!document || document->childNeedsStyleRecalc())
73         return;
74
75     // Don't emit the signal when the actual object being added is not going to be exposed.
76     if (obj->accessibilityIsIgnoredByDefault())
77         return;
78
79     // Don't emit the signal if the object being added is not -- or not yet -- rendered,
80     // which can occur in nested iframes. In these instances we don't want to ignore the
81     // child. But if an assistive technology is listening, AT-SPI2 will attempt to create
82     // and cache the state set for the child upon emission of the signal. If the object
83     // has not yet been rendered, this will result in a crash.
84     if (!obj->renderer())
85         return;
86
87     m_deferredAttachedWrapperObjectList.add(obj);
88 }
89
90 void AXObjectCache::platformPerformDeferredCacheUpdate()
91 {
92     for (auto& coreObject : m_deferredAttachedWrapperObjectList) {
93         auto* wrapper = coreObject->wrapper();
94         if (!wrapper)
95             continue;
96
97         // Don't emit the signal for objects whose parents won't be exposed directly.
98         auto* coreParent = coreObject->parentObjectUnignored();
99         if (!coreParent || coreParent->accessibilityIsIgnoredByDefault())
100             continue;
101
102         // Look for the right object to emit the signal from.
103         auto* atkParent = coreParent->wrapper();
104         if (!atkParent)
105             continue;
106
107         size_t index = coreParent->children(false).find(coreObject);
108         g_signal_emit_by_name(atkParent, "children-changed::add", index != notFound ? index : -1, wrapper);
109     }
110     m_deferredAttachedWrapperObjectList.clear();
111
112     for (auto& wrapper : m_deferredDetachedWrapperList) {
113         if (auto* atkParent = wrapperParent(wrapper.get())) {
114             // The accessibility hierarchy is already invalid, so the parent-children relationships
115             // in the AccessibilityObject tree are not there anymore, so we can't know the offset.
116             g_signal_emit_by_name(atkParent, "children-changed::remove", -1, wrapper.get());
117         }
118     }
119     m_deferredDetachedWrapperList.clear();
120 }
121
122 static AccessibilityObject* getListObject(AccessibilityObject* object)
123 {
124     // Only list boxes and menu lists supported so far.
125     if (!object->isListBox() && !object->isMenuList())
126         return 0;
127
128     // For list boxes the list object is just itself.
129     if (object->isListBox())
130         return object;
131
132     // For menu lists we need to return the first accessible child,
133     // with role MenuListPopupRole, since that's the one holding the list
134     // of items with role MenuListOptionRole.
135     const AccessibilityObject::AccessibilityChildrenVector& children = object->children();
136     if (!children.size())
137         return 0;
138
139     AccessibilityObject* listObject = children.at(0).get();
140     if (!listObject->isMenuListPopup())
141         return 0;
142
143     return listObject;
144 }
145
146 static void notifyChildrenSelectionChange(AccessibilityObject* object)
147 {
148     // This static variables are needed to keep track of the old
149     // focused object and its associated list object, as per previous
150     // calls to this function, in order to properly decide whether to
151     // emit some signals or not.
152     static NeverDestroyed<RefPtr<AccessibilityObject>> oldListObject;
153     static NeverDestroyed<RefPtr<AccessibilityObject>> oldFocusedObject;
154
155     // Only list boxes and menu lists supported so far.
156     if (!object || !(object->isListBox() || object->isMenuList()))
157         return;
158
159     // Only support HTML select elements so far (ARIA selectors not supported).
160     Node* node = object->node();
161     if (!is<HTMLSelectElement>(node))
162         return;
163
164     // Emit signal from the listbox's point of view first.
165     g_signal_emit_by_name(object->wrapper(), "selection-changed");
166
167     // Find the item where the selection change was triggered from.
168     HTMLSelectElement& select = downcast<HTMLSelectElement>(*node);
169     int changedItemIndex = select.activeSelectionStartListIndex();
170
171     AccessibilityObject* listObject = getListObject(object);
172     if (!listObject) {
173         oldListObject.get() = nullptr;
174         return;
175     }
176
177     const AccessibilityObject::AccessibilityChildrenVector& items = listObject->children();
178     if (changedItemIndex < 0 || changedItemIndex >= static_cast<int>(items.size()))
179         return;
180     AccessibilityObject* item = items.at(changedItemIndex).get();
181
182     // Ensure the current list object is the same than the old one so
183     // further comparisons make sense. Otherwise, just reset
184     // oldFocusedObject so it won't be taken into account.
185     if (oldListObject.get() != listObject)
186         oldFocusedObject.get() = nullptr;
187
188     WebKitAccessible* axItem = item ? item->wrapper() : nullptr;
189     WebKitAccessible* axOldFocusedObject = oldFocusedObject.get() ? oldFocusedObject.get()->wrapper() : nullptr;
190
191     // Old focused object just lost focus, so emit the events.
192     if (axOldFocusedObject && axItem != axOldFocusedObject) {
193         g_signal_emit_by_name(axOldFocusedObject, "focus-event", false);
194         atk_object_notify_state_change(ATK_OBJECT(axOldFocusedObject), ATK_STATE_FOCUSED, false);
195     }
196
197     // Emit needed events for the currently (un)selected item.
198     if (axItem) {
199         bool isSelected = item->isSelected();
200         atk_object_notify_state_change(ATK_OBJECT(axItem), ATK_STATE_SELECTED, isSelected);
201         // When the selection changes in a collapsed widget such as a combo box
202         // whose child menu is not showing, that collapsed widget retains focus.
203         if (!object->isCollapsed()) {
204             g_signal_emit_by_name(axItem, "focus-event", isSelected);
205             atk_object_notify_state_change(ATK_OBJECT(axItem), ATK_STATE_FOCUSED, isSelected);
206         }
207     }
208
209     // Update pointers to the previously involved objects.
210     oldListObject.get() = listObject;
211     oldFocusedObject.get() = item;
212 }
213
214 void AXObjectCache::postPlatformNotification(AccessibilityObject* coreObject, AXNotification notification)
215 {
216     auto* axObject = ATK_OBJECT(coreObject->wrapper());
217     if (!axObject)
218         return;
219
220     switch (notification) {
221     case AXCheckedStateChanged:
222         if (!coreObject->isCheckboxOrRadio() && !coreObject->isSwitch())
223             return;
224         atk_object_notify_state_change(axObject, ATK_STATE_CHECKED, coreObject->isChecked());
225         break;
226
227     case AXSelectedChildrenChanged:
228     case AXMenuListValueChanged:
229         // Accessible focus claims should not be made if the associated widget is not focused.
230         if (notification == AXMenuListValueChanged && coreObject->isMenuList() && coreObject->isFocused()) {
231             g_signal_emit_by_name(axObject, "focus-event", true);
232             atk_object_notify_state_change(axObject, ATK_STATE_FOCUSED, true);
233         }
234         notifyChildrenSelectionChange(coreObject);
235         break;
236
237     case AXValueChanged:
238         if (ATK_IS_VALUE(axObject)) {
239             AtkPropertyValues propertyValues;
240             propertyValues.property_name = "accessible-value";
241
242             memset(&propertyValues.new_value,  0, sizeof(GValue));
243 #if ATK_CHECK_VERSION(2,11,92)
244             double value;
245             atk_value_get_value_and_text(ATK_VALUE(axObject), &value, nullptr);
246             g_value_set_double(g_value_init(&propertyValues.new_value, G_TYPE_DOUBLE), value);
247 #else
248             atk_value_get_current_value(ATK_VALUE(axObject), &propertyValues.new_value);
249 #endif
250
251             g_signal_emit_by_name(axObject, "property-change::accessible-value", &propertyValues, NULL);
252         }
253         break;
254
255     case AXInvalidStatusChanged:
256         atk_object_notify_state_change(axObject, ATK_STATE_INVALID_ENTRY, coreObject->invalidStatus() != "false");
257         break;
258
259     case AXElementBusyChanged:
260         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, coreObject->isBusy());
261         break;
262
263     case AXCurrentChanged:
264         atk_object_notify_state_change(axObject, ATK_STATE_ACTIVE, coreObject->currentState() != AccessibilityCurrentState::False);
265         break;
266
267     case AXRowExpanded:
268         atk_object_notify_state_change(axObject, ATK_STATE_EXPANDED, true);
269         break;
270
271     case AXRowCollapsed:
272         atk_object_notify_state_change(axObject, ATK_STATE_EXPANDED, false);
273         break;
274
275     case AXExpandedChanged:
276         atk_object_notify_state_change(axObject, ATK_STATE_EXPANDED, coreObject->isExpanded());
277         break;
278
279     case AXDisabledStateChanged: {
280         bool enabledState = coreObject->isEnabled();
281         atk_object_notify_state_change(axObject, ATK_STATE_ENABLED, enabledState);
282         atk_object_notify_state_change(axObject, ATK_STATE_SENSITIVE, enabledState);
283         break;
284     }
285
286     case AXPressedStateChanged:
287         atk_object_notify_state_change(axObject, ATK_STATE_PRESSED, coreObject->isPressed());
288         break;
289
290     case AXReadOnlyStatusChanged:
291 #if ATK_CHECK_VERSION(2,15,3)
292         atk_object_notify_state_change(axObject, ATK_STATE_READ_ONLY, !coreObject->canSetValueAttribute());
293 #endif
294         break;
295
296     case AXRequiredStatusChanged:
297         atk_object_notify_state_change(axObject, ATK_STATE_REQUIRED, coreObject->isRequired());
298         break;
299
300     case AXActiveDescendantChanged:
301         if (AccessibilityObject* descendant = coreObject->activeDescendant())
302             platformHandleFocusedUIElementChanged(nullptr, descendant->node());
303         break;
304
305     default:
306         break;
307     }
308 }
309
310 void AXObjectCache::nodeTextChangePlatformNotification(AccessibilityObject* object, AXTextChange textChange, unsigned offset, const String& text)
311 {
312     if (!object || text.isEmpty())
313         return;
314
315     AccessibilityObject* parentObject = object->isNonNativeTextControl() ? object : object->parentObjectUnignored();
316     if (!parentObject)
317         return;
318
319     auto* wrapper = parentObject->wrapper();
320     if (!wrapper || !ATK_IS_TEXT(wrapper))
321         return;
322
323     Node* node = object->node();
324     if (!node)
325         return;
326
327     // Ensure document's layout is up-to-date before using TextIterator.
328     Document& document = node->document();
329     document.updateLayout();
330
331     // Select the right signal to be emitted
332     CString detail;
333     switch (textChange) {
334     case AXTextInserted:
335         detail = "text-insert";
336         break;
337     case AXTextDeleted:
338         detail = "text-remove";
339         break;
340     case AXTextAttributesChanged:
341         detail = "text-attributes-changed";
342         break;
343     }
344
345     String textToEmit = text;
346     unsigned offsetToEmit = offset;
347
348     // If the object we're emitting the signal from represents a
349     // password field, we will emit the masked text.
350     if (parentObject->isPasswordField()) {
351         String maskedText = parentObject->passwordFieldValue();
352         textToEmit = maskedText.substring(offset, text.length());
353     } else {
354         // Consider previous text objects that might be present for
355         // the current accessibility object to ensure we emit the
356         // right offset (e.g. multiline text areas).
357         auto range = Range::create(document, node->parentNode(), 0, node, 0);
358         offsetToEmit = offset + TextIterator::rangeLength(range.ptr());
359     }
360
361     g_signal_emit_by_name(wrapper, detail.data(), offsetToEmit, textToEmit.length(), textToEmit.utf8().data());
362 }
363
364 void AXObjectCache::frameLoadingEventPlatformNotification(AccessibilityObject* object, AXLoadingEvent loadingEvent)
365 {
366     if (!object)
367         return;
368
369     auto* axObject = ATK_OBJECT(object->wrapper());
370     if (!axObject || !ATK_IS_DOCUMENT(axObject))
371         return;
372
373     switch (loadingEvent) {
374     case AXObjectCache::AXLoadingStarted:
375         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, true);
376         break;
377     case AXObjectCache::AXLoadingReloaded:
378         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, true);
379         g_signal_emit_by_name(axObject, "reload");
380         break;
381     case AXObjectCache::AXLoadingFailed:
382         g_signal_emit_by_name(axObject, "load-stopped");
383         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, false);
384         break;
385     case AXObjectCache::AXLoadingFinished:
386         g_signal_emit_by_name(axObject, "load-complete");
387         atk_object_notify_state_change(axObject, ATK_STATE_BUSY, false);
388         break;
389     }
390 }
391
392 void AXObjectCache::platformHandleFocusedUIElementChanged(Node* oldFocusedNode, Node* newFocusedNode)
393 {
394     RefPtr<AccessibilityObject> oldObject = getOrCreate(oldFocusedNode);
395     if (oldObject) {
396         auto* axObject = oldObject->wrapper();
397         g_signal_emit_by_name(axObject, "focus-event", false);
398         atk_object_notify_state_change(ATK_OBJECT(axObject), ATK_STATE_FOCUSED, false);
399     }
400     RefPtr<AccessibilityObject> newObject = getOrCreate(newFocusedNode);
401     if (newObject) {
402         auto* axObject = newObject->wrapper();
403         g_signal_emit_by_name(axObject, "focus-event", true);
404         atk_object_notify_state_change(ATK_OBJECT(axObject), ATK_STATE_FOCUSED, true);
405     }
406 }
407
408 void AXObjectCache::handleScrolledToAnchor(const Node*)
409 {
410 }
411
412 } // namespace WebCore
413
414 #endif