<https://webkit.org/b/98350> [GTK] accessibility/aria-invalid.html times out
[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/gobject/GOwnPtr.h>
34 #include <wtf/text/CString.h>
35
36 namespace WebCore {
37
38 void AXObjectCache::detachWrapper(AccessibilityObject* obj)
39 {
40     webkitAccessibleDetach(WEBKIT_ACCESSIBLE(obj->wrapper()));
41 }
42
43 void AXObjectCache::attachWrapper(AccessibilityObject* obj)
44 {
45     AtkObject* atkObj = ATK_OBJECT(webkitAccessibleNew(obj));
46     obj->setWrapper(atkObj);
47     g_object_unref(atkObj);
48 }
49
50 static AccessibilityObject* getListObject(AccessibilityObject* object)
51 {
52     // Only list boxes and menu lists supported so far.
53     if (!object->isListBox() && !object->isMenuList())
54         return 0;
55
56     // For list boxes the list object is just itself.
57     if (object->isListBox())
58         return object;
59
60     // For menu lists we need to return the first accessible child,
61     // with role MenuListPopupRole, since that's the one holding the list
62     // of items with role MenuListOptionRole.
63     AccessibilityObject::AccessibilityChildrenVector children = object->children();
64     if (!children.size())
65         return 0;
66
67     AccessibilityObject* listObject = children.at(0).get();
68     if (!listObject->isMenuListPopup())
69         return 0;
70
71     return listObject;
72 }
73
74 static void notifyChildrenSelectionChange(AccessibilityObject* object)
75 {
76     // This static variables are needed to keep track of the old
77     // focused object and its associated list object, as per previous
78     // calls to this function, in order to properly decide whether to
79     // emit some signals or not.
80     DEFINE_STATIC_LOCAL(RefPtr<AccessibilityObject>, oldListObject, ());
81     DEFINE_STATIC_LOCAL(RefPtr<AccessibilityObject>, oldFocusedObject, ());
82
83     // Only list boxes and menu lists supported so far.
84     if (!object || !(object->isListBox() || object->isMenuList()))
85         return;
86
87     // Only support HTML select elements so far (ARIA selectors not supported).
88     Node* node = object->node();
89     if (!node || !node->hasTagName(HTMLNames::selectTag))
90         return;
91
92     // Emit signal from the listbox's point of view first.
93     g_signal_emit_by_name(object->wrapper(), "selection-changed");
94
95     // Find the item where the selection change was triggered from.
96     HTMLSelectElement* select = toHTMLSelectElement(node);
97     if (!select)
98         return;
99     int changedItemIndex = select->activeSelectionStartListIndex();
100
101     AccessibilityObject* listObject = getListObject(object);
102     if (!listObject) {
103         oldListObject = 0;
104         return;
105     }
106
107     AccessibilityObject::AccessibilityChildrenVector items = listObject->children();
108     if (changedItemIndex < 0 || changedItemIndex >= static_cast<int>(items.size()))
109         return;
110     AccessibilityObject* item = items.at(changedItemIndex).get();
111
112     // Ensure the current list object is the same than the old one so
113     // further comparisons make sense. Otherwise, just reset
114     // oldFocusedObject so it won't be taken into account.
115     if (oldListObject != listObject)
116         oldFocusedObject = 0;
117
118     AtkObject* axItem = item ? item->wrapper() : 0;
119     AtkObject* axOldFocusedObject = oldFocusedObject ? oldFocusedObject->wrapper() : 0;
120
121     // Old focused object just lost focus, so emit the events.
122     if (axOldFocusedObject && axItem != axOldFocusedObject) {
123         g_signal_emit_by_name(axOldFocusedObject, "focus-event", false);
124         g_signal_emit_by_name(axOldFocusedObject, "state-change", "focused", false);
125     }
126
127     // Emit needed events for the currently (un)selected item.
128     if (axItem) {
129         bool isSelected = item->isSelected();
130         g_signal_emit_by_name(axItem, "state-change", "selected", isSelected);
131         g_signal_emit_by_name(axItem, "focus-event", isSelected);
132         g_signal_emit_by_name(axItem, "state-change", "focused", isSelected);
133     }
134
135     // Update pointers to the previously involved objects.
136     oldListObject = listObject;
137     oldFocusedObject = item;
138 }
139
140 void AXObjectCache::postPlatformNotification(AccessibilityObject* coreObject, AXNotification notification)
141 {
142     AtkObject* axObject = coreObject->wrapper();
143     if (!axObject)
144         return;
145
146     if (notification == AXCheckedStateChanged) {
147         if (!coreObject->isCheckboxOrRadio())
148             return;
149         g_signal_emit_by_name(axObject, "state-change", "checked", coreObject->isChecked());
150     } else if (notification == AXSelectedChildrenChanged || notification == AXMenuListValueChanged) {
151         if (notification == AXMenuListValueChanged && coreObject->isMenuList()) {
152             g_signal_emit_by_name(axObject, "focus-event", true);
153             g_signal_emit_by_name(axObject, "state-change", "focused", true);
154         }
155         notifyChildrenSelectionChange(coreObject);
156     } else if (notification == AXValueChanged) {
157         if (!ATK_IS_VALUE(axObject))
158             return;
159
160         AtkPropertyValues propertyValues;
161         propertyValues.property_name = "accessible-value";
162
163         memset(&propertyValues.new_value,  0, sizeof(GValue));
164         atk_value_get_current_value(ATK_VALUE(axObject), &propertyValues.new_value);
165
166         g_signal_emit_by_name(ATK_OBJECT(axObject), "property-change::accessible-value", &propertyValues, NULL);
167     } else if (notification == AXInvalidStatusChanged)
168         g_signal_emit_by_name(axObject, "state-change", "invalid-entry", coreObject->invalidStatus() != "false");
169 }
170
171 void AXObjectCache::nodeTextChangePlatformNotification(AccessibilityObject* object, AXTextChange textChange, unsigned offset, const String& text)
172 {
173     if (!object || text.isEmpty())
174         return;
175
176     AccessibilityObject* parentObject = object->parentObjectUnignored();
177     if (!parentObject)
178         return;
179
180     AtkObject* wrapper = parentObject->wrapper();
181     if (!wrapper || !ATK_IS_TEXT(wrapper))
182         return;
183
184     Node* node = object->node();
185     if (!node)
186         return;
187
188     // Ensure document's layout is up-to-date before using TextIterator.
189     Document& document = node->document();
190     document.updateLayout();
191
192     // Select the right signal to be emitted
193     CString detail;
194     switch (textChange) {
195     case AXObjectCache::AXTextInserted:
196         detail = "text-insert";
197         break;
198     case AXObjectCache::AXTextDeleted:
199         detail = "text-remove";
200         break;
201     }
202
203     String textToEmit = text;
204     unsigned offsetToEmit = offset;
205
206     // If the object we're emitting the signal from represents a
207     // password field, we will emit the masked text.
208     if (parentObject->isPasswordField()) {
209         String maskedText = parentObject->passwordFieldValue();
210         textToEmit = maskedText.substring(offset, text.length());
211     } else {
212         // Consider previous text objects that might be present for
213         // the current accessibility object to ensure we emit the
214         // right offset (e.g. multiline text areas).
215         RefPtr<Range> range = Range::create(&document, node->parentNode(), 0, node, 0);
216         offsetToEmit = offset + TextIterator::rangeLength(range.get());
217     }
218
219     g_signal_emit_by_name(wrapper, detail.data(), offsetToEmit, textToEmit.length(), textToEmit.utf8().data());
220 }
221
222 void AXObjectCache::frameLoadingEventPlatformNotification(AccessibilityObject* object, AXLoadingEvent loadingEvent)
223 {
224     if (!object)
225         return;
226
227     AtkObject* axObject = object->wrapper();
228     if (!axObject || !ATK_IS_DOCUMENT(axObject))
229         return;
230
231     switch (loadingEvent) {
232     case AXObjectCache::AXLoadingStarted:
233         g_signal_emit_by_name(axObject, "state-change", "busy", true);
234         break;
235     case AXObjectCache::AXLoadingReloaded:
236         g_signal_emit_by_name(axObject, "state-change", "busy", true);
237         g_signal_emit_by_name(axObject, "reload");
238         break;
239     case AXObjectCache::AXLoadingFailed:
240         g_signal_emit_by_name(axObject, "load-stopped");
241         g_signal_emit_by_name(axObject, "state-change", "busy", false);
242         break;
243     case AXObjectCache::AXLoadingFinished:
244         g_signal_emit_by_name(axObject, "load-complete");
245         g_signal_emit_by_name(axObject, "state-change", "busy", false);
246         break;
247     }
248 }
249
250 void AXObjectCache::handleFocusedUIElementChanged(Node* oldFocusedNode, Node* newFocusedNode)
251 {
252     RefPtr<AccessibilityObject> oldObject = getOrCreate(oldFocusedNode);
253     if (oldObject) {
254         g_signal_emit_by_name(oldObject->wrapper(), "focus-event", false);
255         g_signal_emit_by_name(oldObject->wrapper(), "state-change", "focused", false);
256     }
257     RefPtr<AccessibilityObject> newObject = getOrCreate(newFocusedNode);
258     if (newObject) {
259         g_signal_emit_by_name(newObject->wrapper(), "focus-event", true);
260         g_signal_emit_by_name(newObject->wrapper(), "state-change", "focused", true);
261     }
262 }
263
264 void AXObjectCache::handleScrolledToAnchor(const Node*)
265 {
266 }
267
268 } // namespace WebCore
269
270 #endif