[ATK] Missing WTR AccessibilityUIElement::addNotificationListener implementation
[WebKit-https.git] / Tools / WebKitTestRunner / InjectedBundle / atk / AccessibilityNotificationHandlerAtk.cpp
1 /*
2  * Copyright (C) 2013 Samsung Electronics Inc. All rights reserved.
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 "AccessibilityNotificationHandlerAtk.h"
22
23 #include "InjectedBundle.h"
24 #include "InjectedBundlePage.h"
25 #include "JSWrapper.h"
26 #include <WebKit2/WKBundleFrame.h>
27 #include <WebKit2/WKBundlePage.h>
28 #include <WebKit2/WKBundlePagePrivate.h>
29 #include <wtf/HashMap.h>
30 #include <wtf/gobject/GOwnPtr.h>
31 #include <wtf/text/CString.h>
32 #include <wtf/text/WTFString.h>
33
34 namespace WTR {
35
36 namespace {
37
38 typedef HashMap<AtkObject*, AccessibilityNotificationHandler*> NotificationHandlersMap;
39
40 unsigned stateChangeListenerId = 0;
41 unsigned focusEventListenerId = 0;
42 unsigned activeDescendantChangedListenerId = 0;
43 unsigned childrenChangedListenerId = 0;
44 unsigned propertyChangedListenerId = 0;
45 unsigned visibleDataChangedListenerId = 0;
46 NotificationHandlersMap notificationHandlers;
47 AccessibilityNotificationHandler* globalNotificationHandler = 0;
48 bool loggingAccessibilityEvents = false;
49
50 void printAccessibilityEvent(AtkObject* accessible, const char* signalName, const char* signalValue)
51 {
52     // Do not handle state-change:defunct signals, as the AtkObject
53     // associated to them will not be valid at this point already.
54     if (!signalName || !g_strcmp0(signalName, "state-change:defunct"))
55         return;
56
57     if (!accessible || !ATK_IS_OBJECT(accessible))
58         return;
59
60     const char* objectName = atk_object_get_name(accessible);
61     AtkRole objectRole = atk_object_get_role(accessible);
62
63     // Try to always provide a name to be logged for the object.
64     if (!objectName || *objectName == '\0')
65         objectName = "(No name)";
66
67     GOwnPtr<char> signalNameAndValue(signalValue ? g_strdup_printf("%s = %s", signalName, signalValue) : g_strdup(signalName));
68     GOwnPtr<char> accessibilityEventString(g_strdup_printf("Accessibility object emitted \"%s\" / Name: \"%s\" / Role: %d\n", signalNameAndValue.get(), objectName, objectRole));
69     InjectedBundle::shared().outputText(String::fromUTF8(accessibilityEventString.get()));
70 }
71
72 gboolean axObjectEventListener(GSignalInvocationHint* signalHint, unsigned numParamValues, const GValue* paramValues, gpointer data)
73 {
74     // At least we should receive the instance emitting the signal.
75     if (!numParamValues)
76         return true;
77
78     AtkObject* accessible = ATK_OBJECT(g_value_get_object(&paramValues[0]));
79     if (!accessible || !ATK_IS_OBJECT(accessible))
80         return true;
81
82     GSignalQuery signalQuery;
83     GOwnPtr<char> signalName;
84     GOwnPtr<char> signalValue;
85     const char* notificationName = 0;
86
87     g_signal_query(signalHint->signal_id, &signalQuery);
88
89     if (!g_strcmp0(signalQuery.signal_name, "state-change")) {
90         signalName.set(g_strdup_printf("state-change:%s", g_value_get_string(&paramValues[1])));
91         signalValue.set(g_strdup_printf("%d", g_value_get_boolean(&paramValues[2])));
92         if (!g_strcmp0(g_value_get_string(&paramValues[1]), "checked"))
93             notificationName = "CheckedStateChanged";
94         else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "invalid-entry"))
95             notificationName = "AXInvalidStatusChanged";
96         else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "layout-complete"))
97             notificationName = "AXLayoutComplete";
98     } else if (!g_strcmp0(signalQuery.signal_name, "focus-event")) {
99         signalName.set(g_strdup("focus-event"));
100         signalValue.set(g_strdup_printf("%d", g_value_get_boolean(&paramValues[1])));
101         if (g_value_get_boolean(&paramValues[1]))
102             notificationName = "AXFocusedUIElementChanged";
103     } else if (!g_strcmp0(signalQuery.signal_name, "children-changed")) {
104         signalName.set(g_strdup("children-changed"));
105         signalValue.set(g_strdup_printf("%d", g_value_get_uint(&paramValues[1])));
106     } else if (!g_strcmp0(signalQuery.signal_name, "property-change")) {
107         signalName.set(g_strdup_printf("property-change:%s", g_quark_to_string(signalHint->detail)));
108         if (!g_strcmp0(g_quark_to_string(signalHint->detail), "accessible-value"))
109             notificationName = "AXValueChanged";
110     } else
111         signalName.set(g_strdup(signalQuery.signal_name));
112
113     if (loggingAccessibilityEvents)
114         printAccessibilityEvent(accessible, signalName.get(), signalValue.get());
115
116 #if PLATFORM(GTK)
117     WKBundlePageRef page = InjectedBundle::shared().page()->page();
118     WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(page);
119     JSContextRef jsContext = WKBundleFrameGetJavaScriptContext(mainFrame);
120 #else
121     JSContextRef jsContext = 0;
122 #endif
123     if (!jsContext)
124         return true;
125
126     if (notificationName) {
127         JSRetainPtr<JSStringRef> jsNotificationEventName(Adopt, JSStringCreateWithUTF8CString(notificationName));
128         JSValueRef notificationNameArgument = JSValueMakeString(jsContext, jsNotificationEventName.get());
129         NotificationHandlersMap::iterator elementNotificationHandler = notificationHandlers.find(accessible);
130         if (elementNotificationHandler != notificationHandlers.end()) {
131             // Listener for one element just gets one argument, the notification name.
132             JSObjectCallAsFunction(jsContext, const_cast<JSObjectRef>(elementNotificationHandler->value->notificationFunctionCallback()), 0, 1, &notificationNameArgument, 0);
133         }
134
135         if (globalNotificationHandler) {
136             // A global listener gets the element and the notification name as arguments.
137             JSValueRef arguments[2];
138             arguments[0] = toJS(jsContext, WTF::getPtr(WTR::AccessibilityUIElement::create(accessible)));
139             arguments[1] = notificationNameArgument;
140             JSObjectCallAsFunction(jsContext, const_cast<JSObjectRef>(globalNotificationHandler->notificationFunctionCallback()), 0, 2, arguments, 0);
141         }
142     }
143
144     return true;
145 }
146
147 } // namespace
148
149 AccessibilityNotificationHandler::AccessibilityNotificationHandler()
150     : m_platformElement(0)
151     , m_notificationFunctionCallback(0)
152 {
153 }
154
155 AccessibilityNotificationHandler::~AccessibilityNotificationHandler()
156 {
157     removeAccessibilityNotificationHandler();
158     disconnectAccessibilityCallbacks();
159 }
160
161 void AccessibilityNotificationHandler::logAccessibilityEvents()
162 {
163     connectAccessibilityCallbacks();
164     loggingAccessibilityEvents = true;
165 }
166
167 void AccessibilityNotificationHandler::setNotificationFunctionCallback(JSValueRef notificationFunctionCallback)
168 {
169     if (!notificationFunctionCallback) {
170         removeAccessibilityNotificationHandler();
171         disconnectAccessibilityCallbacks();
172         return;
173     }
174
175     m_notificationFunctionCallback = notificationFunctionCallback;
176
177 #if PLATFORM(GTK)
178     WKBundlePageRef page = InjectedBundle::shared().page()->page();
179     WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(page);
180     JSContextRef jsContext = WKBundleFrameGetJavaScriptContext(mainFrame);
181 #else
182     JSContextRef jsContext = 0;
183 #endif
184     if (!jsContext)
185         return;
186
187     connectAccessibilityCallbacks();
188
189     JSValueProtect(jsContext, m_notificationFunctionCallback);
190     // Check if this notification handler is related to a specific element.
191     if (m_platformElement) {
192         NotificationHandlersMap::iterator currentNotificationHandler = notificationHandlers.find(m_platformElement.get());
193         if (currentNotificationHandler != notificationHandlers.end()) {
194             ASSERT(currentNotificationHandler->value->platformElement());
195             JSValueUnprotect(jsContext, currentNotificationHandler->value->notificationFunctionCallback());
196             notificationHandlers.remove(currentNotificationHandler->value->platformElement().get());
197         }
198         notificationHandlers.add(m_platformElement.get(), this);
199     } else {
200         if (globalNotificationHandler)
201             JSValueUnprotect(jsContext, globalNotificationHandler->notificationFunctionCallback());
202         globalNotificationHandler = this;
203     }
204 }
205
206 void AccessibilityNotificationHandler::removeAccessibilityNotificationHandler()
207 {
208 #if PLATFORM(GTK)
209     WKBundlePageRef page = InjectedBundle::shared().page()->page();
210     WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(page);
211     JSContextRef jsContext = WKBundleFrameGetJavaScriptContext(mainFrame);
212 #else
213     JSContextRef jsContext = 0;
214 #endif
215     if (!jsContext)
216         return;
217
218     if (globalNotificationHandler == this) {
219         JSValueUnprotect(jsContext, globalNotificationHandler->notificationFunctionCallback());
220         globalNotificationHandler = 0;
221     } else if (m_platformElement.get()) {
222         NotificationHandlersMap::iterator removeNotificationHandler = notificationHandlers.find(m_platformElement.get());
223         if (removeNotificationHandler != notificationHandlers.end()) {
224             JSValueUnprotect(jsContext, removeNotificationHandler->value->notificationFunctionCallback());
225             notificationHandlers.remove(removeNotificationHandler);
226         }
227     }
228 }
229
230 void AccessibilityNotificationHandler::connectAccessibilityCallbacks()
231 {
232     // Ensure no callbacks are connected before.
233     if (!disconnectAccessibilityCallbacks())
234         return;
235
236     // Add global listeners for AtkObject's signals.
237     stateChangeListenerId = atk_add_global_event_listener(axObjectEventListener, "ATK:AtkObject:state-change");
238     focusEventListenerId = atk_add_global_event_listener(axObjectEventListener, "ATK:AtkObject:focus-event");
239     activeDescendantChangedListenerId = atk_add_global_event_listener(axObjectEventListener, "ATK:AtkObject:active-descendant-changed");
240     childrenChangedListenerId = atk_add_global_event_listener(axObjectEventListener, "ATK:AtkObject:children-changed");
241     propertyChangedListenerId = atk_add_global_event_listener(axObjectEventListener, "ATK:AtkObject:property-change");
242     visibleDataChangedListenerId = atk_add_global_event_listener(axObjectEventListener, "ATK:AtkObject:visible-data-changed");
243 }
244
245 bool AccessibilityNotificationHandler::disconnectAccessibilityCallbacks()
246 {
247     // Only disconnect if logging is off and there is no notification handler.
248     if (loggingAccessibilityEvents || !notificationHandlers.isEmpty() || globalNotificationHandler)
249         return false;
250
251     // AtkObject signals.
252     if (stateChangeListenerId) {
253         atk_remove_global_event_listener(stateChangeListenerId);
254         stateChangeListenerId = 0;
255     }
256     if (focusEventListenerId) {
257         atk_remove_global_event_listener(focusEventListenerId);
258         focusEventListenerId = 0;
259     }
260     if (activeDescendantChangedListenerId) {
261         atk_remove_global_event_listener(activeDescendantChangedListenerId);
262         activeDescendantChangedListenerId = 0;
263     }
264     if (childrenChangedListenerId) {
265         atk_remove_global_event_listener(childrenChangedListenerId);
266         childrenChangedListenerId = 0;
267     }
268     if (propertyChangedListenerId) {
269         atk_remove_global_event_listener(propertyChangedListenerId);
270         propertyChangedListenerId = 0;
271     }
272     if (visibleDataChangedListenerId) {
273         atk_remove_global_event_listener(visibleDataChangedListenerId);
274         visibleDataChangedListenerId = 0;
275     }
276
277     return true;
278 }
279
280 } // namespace WTR