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 |
415 | |