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#if HAVE(ACCESSIBILITY)
24
25#include "InjectedBundle.h"
26#include "InjectedBundlePage.h"
27#include "JSWrapper.h"
28#include <WebKit/WKBundleFrame.h>
29#include <WebKit/WKBundlePage.h>
30#include <WebKit/WKBundlePagePrivate.h>
31#include <wtf/HashMap.h>
32#include <wtf/Vector.h>
33#include <wtf/glib/GUniquePtr.h>
34#include <wtf/text/CString.h>
35#include <wtf/text/WTFString.h>
36
37namespace WTR {
38
39namespace {
40
41typedef HashMap<AtkObject*, AccessibilityNotificationHandler*> NotificationHandlersMap;
42
43static WTF::Vector<unsigned>& listenerIds()
44{
45 static NeverDestroyed<WTF::Vector<unsigned>> ids;
46 return ids.get();
47}
48
49static NotificationHandlersMap& notificationHandlers()
50{
51 static NeverDestroyed<NotificationHandlersMap> map;
52 return map.get();
53}
54
55AccessibilityNotificationHandler* globalNotificationHandler = nullptr;
56
57gboolean axObjectEventListener(GSignalInvocationHint* signalHint, unsigned numParamValues, const GValue* paramValues, gpointer data)
58{
59 // At least we should receive the instance emitting the signal.
60 if (!numParamValues)
61 return true;
62
63 AtkObject* accessible = ATK_OBJECT(g_value_get_object(&paramValues[0]));
64 if (!accessible || !ATK_IS_OBJECT(accessible))
65 return true;
66
67#if PLATFORM(GTK)
68 WKBundlePageRef page = InjectedBundle::singleton().page()->page();
69 WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(page);
70 JSContextRef jsContext = WKBundleFrameGetJavaScriptContext(mainFrame);
71#else
72 JSContextRef jsContext = nullptr;
73#endif
74
75 GSignalQuery signalQuery;
76 const char* notificationName = nullptr;
77 Vector<JSValueRef> extraArgs;
78
79 g_signal_query(signalHint->signal_id, &signalQuery);
80
81 if (!g_strcmp0(signalQuery.signal_name, "state-change")) {
82 if (!g_strcmp0(g_value_get_string(&paramValues[1]), "checked"))
83 notificationName = "CheckedStateChanged";
84 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "invalid-entry"))
85 notificationName = "AXInvalidStatusChanged";
86 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "active"))
87 notificationName = "ActiveStateChanged";
88 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "busy"))
89 notificationName = "AXElementBusyChanged";
90 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "enabled"))
91 notificationName = "AXDisabledStateChanged";
92 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "expanded"))
93 notificationName = "AXExpandedChanged";
94 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "pressed"))
95 notificationName = "AXPressedStateChanged";
96 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "read-only"))
97 notificationName = "AXReadOnlyStatusChanged";
98 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "required"))
99 notificationName = "AXRequiredStatusChanged";
100 else if (!g_strcmp0(g_value_get_string(&paramValues[1]), "sensitive"))
101 notificationName = "AXSensitiveStateChanged";
102 else
103 return true;
104 GUniquePtr<char> signalValue(g_strdup_printf("%d", g_value_get_boolean(&paramValues[2])));
105 JSRetainPtr<JSStringRef> jsSignalValue(Adopt, JSStringCreateWithUTF8CString(signalValue.get()));
106 extraArgs.append(JSValueMakeString(jsContext, jsSignalValue.get()));
107 } else if (!g_strcmp0(signalQuery.signal_name, "focus-event")) {
108 if (g_value_get_boolean(&paramValues[1]))
109 notificationName = "AXFocusedUIElementChanged";
110 } else if (!g_strcmp0(signalQuery.signal_name, "selection-changed")) {
111 notificationName = "AXSelectedChildrenChanged";
112 } else if (!g_strcmp0(signalQuery.signal_name, "children-changed")) {
113 const gchar* childrenChangedDetail = g_quark_to_string(signalHint->detail);
114 notificationName = !g_strcmp0(childrenChangedDetail, "add") ? "AXChildrenAdded" : "AXChildrenRemoved";
115 gpointer child = g_value_get_pointer(&paramValues[2]);
116 if (ATK_IS_OBJECT(child))
117 extraArgs.append(toJS(jsContext, WTF::getPtr(WTR::AccessibilityUIElement::create(ATK_OBJECT(child)))));
118 } else if (!g_strcmp0(signalQuery.signal_name, "property-change")) {
119 if (!g_strcmp0(g_quark_to_string(signalHint->detail), "accessible-value"))
120 notificationName = "AXValueChanged";
121 } else if (!g_strcmp0(signalQuery.signal_name, "load-complete"))
122 notificationName = "AXLoadComplete";
123 else if (!g_strcmp0(signalQuery.signal_name, "text-caret-moved")) {
124 notificationName = "AXTextCaretMoved";
125 GUniquePtr<char> signalValue(g_strdup_printf("%d", g_value_get_int(&paramValues[1])));
126 JSRetainPtr<JSStringRef> jsSignalValue(Adopt, JSStringCreateWithUTF8CString(signalValue.get()));
127 extraArgs.append(JSValueMakeString(jsContext, jsSignalValue.get()));
128 } else if (!g_strcmp0(signalQuery.signal_name, "text-insert") || !g_strcmp0(signalQuery.signal_name, "text-remove"))
129 notificationName = "AXTextChanged";
130
131 if (!jsContext)
132 return true;
133
134 if (notificationName) {
135 JSRetainPtr<JSStringRef> jsNotificationEventName(Adopt, JSStringCreateWithUTF8CString(notificationName));
136 JSValueRef notificationNameArgument = JSValueMakeString(jsContext, jsNotificationEventName.get());
137 NotificationHandlersMap::iterator elementNotificationHandler = notificationHandlers().find(accessible);
138 JSValueRef arguments[5]; // this dimension must be >= 2 + max(extraArgs.size())
139 arguments[0] = toJS(jsContext, WTF::getPtr(WTR::AccessibilityUIElement::create(accessible)));
140 arguments[1] = notificationNameArgument;
141 size_t numOfExtraArgs = extraArgs.size();
142 for (size_t i = 0; i < numOfExtraArgs; i++)
143 arguments[i + 2] = extraArgs[i];
144 if (elementNotificationHandler != notificationHandlers().end()) {
145 // Listener for one element. As arguments, it gets the notification name
146 // and sometimes extra arguments.
147 JSObjectCallAsFunction(jsContext,
148 const_cast<JSObjectRef>(elementNotificationHandler->value->notificationFunctionCallback()),
149 0, numOfExtraArgs + 1, arguments + 1, 0);
150 }
151
152 if (globalNotificationHandler) {
153 // A global listener gets additionally the element as the first argument.
154 JSObjectCallAsFunction(jsContext,
155 const_cast<JSObjectRef>(globalNotificationHandler->notificationFunctionCallback()),
156 0, numOfExtraArgs + 2, arguments, 0);
157 }
158 }
159
160 return true;
161}
162
163} // namespace
164
165AccessibilityNotificationHandler::AccessibilityNotificationHandler()
166 : m_platformElement(0)
167 , m_notificationFunctionCallback(0)
168{
169}
170
171AccessibilityNotificationHandler::~AccessibilityNotificationHandler()
172{
173 removeAccessibilityNotificationHandler();
174 disconnectAccessibilityCallbacks();
175}
176
177void AccessibilityNotificationHandler::setNotificationFunctionCallback(JSValueRef notificationFunctionCallback)
178{
179 if (!notificationFunctionCallback) {
180 removeAccessibilityNotificationHandler();
181 disconnectAccessibilityCallbacks();
182 return;
183 }
184
185 m_notificationFunctionCallback = notificationFunctionCallback;
186
187#if PLATFORM(GTK)
188 WKBundlePageRef page = InjectedBundle::singleton().page()->page();
189 WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(page);
190 JSContextRef jsContext = WKBundleFrameGetJavaScriptContext(mainFrame);
191#else
192 JSContextRef jsContext = nullptr;
193#endif
194 if (!jsContext)
195 return;
196
197 connectAccessibilityCallbacks();
198
199 JSValueProtect(jsContext, m_notificationFunctionCallback);
200 // Check if this notification handler is related to a specific element.
201 if (m_platformElement) {
202 NotificationHandlersMap::iterator currentNotificationHandler = notificationHandlers().find(m_platformElement.get());
203 if (currentNotificationHandler != notificationHandlers().end()) {
204 ASSERT(currentNotificationHandler->value->platformElement());
205 JSValueUnprotect(jsContext, currentNotificationHandler->value->notificationFunctionCallback());
206 notificationHandlers().remove(currentNotificationHandler->value->platformElement().get());
207 }
208 notificationHandlers().add(m_platformElement.get(), this);
209 } else {
210 if (globalNotificationHandler)
211 JSValueUnprotect(jsContext, globalNotificationHandler->notificationFunctionCallback());
212 globalNotificationHandler = this;
213 }
214}
215
216void AccessibilityNotificationHandler::removeAccessibilityNotificationHandler()
217{
218#if PLATFORM(GTK)
219 WKBundlePageRef page = InjectedBundle::singleton().page()->page();
220 WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(page);
221 JSContextRef jsContext = WKBundleFrameGetJavaScriptContext(mainFrame);
222#else
223 JSContextRef jsContext = nullptr;
224#endif
225 if (!jsContext)
226 return;
227
228 if (globalNotificationHandler == this) {
229 JSValueUnprotect(jsContext, globalNotificationHandler->notificationFunctionCallback());
230 globalNotificationHandler = nullptr;
231 } else if (m_platformElement.get()) {
232 NotificationHandlersMap::iterator removeNotificationHandler = notificationHandlers().find(m_platformElement.get());
233 if (removeNotificationHandler != notificationHandlers().end()) {
234 JSValueUnprotect(jsContext, removeNotificationHandler->value->notificationFunctionCallback());
235 notificationHandlers().remove(removeNotificationHandler);
236 }
237 }
238}
239
240void AccessibilityNotificationHandler::connectAccessibilityCallbacks()
241{
242 // Ensure no callbacks are connected before.
243 if (!disconnectAccessibilityCallbacks())
244 return;
245
246 const char* signalNames[] = {
247 "ATK:AtkObject:state-change",
248 "ATK:AtkObject:focus-event",
249 "ATK:AtkObject:active-descendant-changed",
250 "ATK:AtkObject:children-changed",
251 "ATK:AtkObject:property-change",
252 "ATK:AtkObject:visible-data-changed",
253 "ATK:AtkDocument:load-complete",
254 "ATK:AtkSelection:selection-changed",
255 "ATK:AtkText:text-caret-moved",
256 "ATK:AtkText:text-insert",
257 "ATK:AtkText:text-remove",
258 0
259 };
260
261 // Register atk interfaces, otherwise add_global may fail.
262 GObject* dummyObject = (GObject*)g_object_new(G_TYPE_OBJECT, NULL, NULL);
263 g_object_unref(atk_no_op_object_new(dummyObject));
264 g_object_unref(dummyObject);
265
266 // Add global listeners for AtkObject's signals.
267 for (const char** signalName = signalNames; *signalName; signalName++) {
268 unsigned id = atk_add_global_event_listener(axObjectEventListener, *signalName);
269 if (!id) {
270 String message = makeString("atk_add_global_event_listener failed for signal ", *signalName, '\n');
271 InjectedBundle::singleton().outputText(message);
272 continue;
273 }
274
275 listenerIds().append(id);
276 }
277}
278
279bool AccessibilityNotificationHandler::disconnectAccessibilityCallbacks()
280{
281 // Only disconnect if there is no notification handler.
282 if (!notificationHandlers().isEmpty() || globalNotificationHandler)
283 return false;
284
285 // AtkObject signals.
286 for (size_t i = 0; i < listenerIds().size(); i++) {
287 ASSERT(listenerIds()[i]);
288 atk_remove_global_event_listener(listenerIds()[i]);
289 }
290 listenerIds().clear();
291 return true;
292}
293
294} // namespace WTR
295
296#endif // HAVE(ACCESSIBILITY)
297