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