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
37namespace WebCore {
38
39static 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
51void 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
64void 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
90void 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
122static 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
146static 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
214void 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
310void 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
364void 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
392void 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
408void AXObjectCache::handleScrolledToAnchor(const Node*)
409{
410}
411
412} // namespace WebCore
413
414#endif
415