3c6364856f319dc3742ac5ae4c05312115c4f708
[WebKit-https.git] / Tools / DumpRenderTree / gtk / EventSender.cpp
1 /*
2  * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
3  * Copyright (C) 2009 Zan Dobersek <zandobersek@gmail.com>
4  * Copyright (C) 2009 Holger Hans Peter Freyther
5  * Copyright (C) 2010 Igalia S.L.
6  * Copyright (C) 2012 ChangSeok Oh <shivamidow@gmail.com>
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  *
12  * 1.  Redistributions of source code must retain the above copyright
13  *     notice, this list of conditions and the following disclaimer.
14  * 2.  Redistributions in binary form must reproduce the above copyright
15  *     notice, this list of conditions and the following disclaimer in the
16  *     documentation and/or other materials provided with the distribution.
17  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
18  *     its contributors may be used to endorse or promote products derived
19  *     from this software without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
22  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
25  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
28  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32
33 #include "config.h"
34 #include "EventSender.h"
35
36 #include "DumpRenderTree.h"
37 #include "WebCoreSupport/DumpRenderTreeSupportGtk.h"
38 #include <GOwnPtrGtk.h>
39 #include <GRefPtrGtk.h>
40 #include <GtkVersioning.h>
41 #include <JavaScriptCore/JSObjectRef.h>
42 #include <JavaScriptCore/JSRetainPtr.h>
43 #include <JavaScriptCore/JSStringRef.h>
44 #include <cstring>
45 #include <gdk/gdk.h>
46 #include <gdk/gdkkeysyms.h>
47 #include <webkit/webkitwebframe.h>
48 #include <webkit/webkitwebview.h>
49 #include <wtf/ASCIICType.h>
50 #include <wtf/Platform.h>
51 #include <wtf/text/CString.h>
52
53 extern "C" {
54     extern GtkMenu* webkit_web_view_get_context_menu(WebKitWebView*);
55 }
56
57 static bool dragMode;
58 static int timeOffset = 0;
59
60 static int lastMousePositionX;
61 static int lastMousePositionY;
62 static int lastClickPositionX;
63 static int lastClickPositionY;
64 static int lastClickTimeOffset;
65 static int lastClickButton;
66 static unsigned buttonCurrentlyDown;
67 static int clickCount;
68 GdkDragContext* currentDragSourceContext;
69
70 struct DelayedMessage {
71     GdkEvent* event;
72     gulong delay;
73 };
74
75 static DelayedMessage msgQueue[1024];
76
77 static unsigned endOfQueue;
78 static unsigned startOfQueue;
79
80 static const float zoomMultiplierRatio = 1.2f;
81
82 // WebCore and layout tests assume this value.
83 static const float pixelsPerScrollTick = 40;
84
85 // Key event location code defined in DOM Level 3.
86 enum KeyLocationCode {
87     DOM_KEY_LOCATION_STANDARD      = 0x00,
88     DOM_KEY_LOCATION_LEFT          = 0x01,
89     DOM_KEY_LOCATION_RIGHT         = 0x02,
90     DOM_KEY_LOCATION_NUMPAD        = 0x03
91 };
92
93 static void sendOrQueueEvent(GdkEvent*, bool = true);
94 static void dispatchEvent(GdkEvent* event);
95 static guint getStateFlags();
96
97 static JSValueRef getDragModeCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception)
98 {
99     return JSValueMakeBoolean(context, dragMode);
100 }
101
102 static bool setDragModeCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception)
103 {
104     dragMode = JSValueToBoolean(context, value);
105     return true;
106 }
107
108 static JSValueRef leapForwardCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
109 {
110     if (argumentCount > 0) {
111         msgQueue[endOfQueue].delay = JSValueToNumber(context, arguments[0], exception);
112         timeOffset += msgQueue[endOfQueue].delay;
113         ASSERT(!exception || !*exception);
114     }
115
116     return JSValueMakeUndefined(context);
117 }
118
119 bool prepareMouseButtonEvent(GdkEvent* event, int eventSenderButtonNumber, guint modifiers)
120 {
121     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
122     if (!view)
123         return false;
124
125     // The logic for mapping EventSender button numbers to GDK button
126     // numbers originates from the Windows EventSender.
127     int gdkButtonNumber = 3;
128     if (eventSenderButtonNumber >= 0 && eventSenderButtonNumber <= 2)
129         gdkButtonNumber = eventSenderButtonNumber + 1;
130
131     // fast/events/mouse-click-events expects the 4th button
132     // to be event->button = 1, so send a middle-button event.
133     else if (eventSenderButtonNumber == 3)
134         gdkButtonNumber = 2;
135
136     event->button.button = gdkButtonNumber;
137     event->button.x = lastMousePositionX;
138     event->button.y = lastMousePositionY;
139     event->button.window = gtk_widget_get_window(GTK_WIDGET(view));
140     g_object_ref(event->button.window);
141     event->button.device = getDefaultGDKPointerDevice(event->button.window);
142     event->button.state = modifiers | getStateFlags();
143     event->button.time = GDK_CURRENT_TIME;
144     event->button.axes = 0;
145
146     int xRoot, yRoot;
147     gdk_window_get_root_coords(gtk_widget_get_window(GTK_WIDGET(view)), lastMousePositionX, lastMousePositionY, &xRoot, &yRoot);
148     event->button.x_root = xRoot;
149     event->button.y_root = yRoot;
150
151     return true;
152 }
153
154 static JSValueRef getMenuItemTitleCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception)
155 {
156     GtkWidget* widget = GTK_WIDGET(JSObjectGetPrivate(object));
157     CString label;
158     if (GTK_IS_SEPARATOR_MENU_ITEM(widget))
159         label = "<separator>";
160     else
161         label = gtk_menu_item_get_label(GTK_MENU_ITEM(widget));
162
163     JSRetainPtr<JSStringRef> itemText(Adopt, JSStringCreateWithUTF8CString(label.data()));
164     return JSValueMakeString(context, itemText.get());
165 }
166
167 static bool setMenuItemTitleCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception)
168 {
169     return true;
170 }
171
172 static JSValueRef menuItemClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
173 {
174     GtkMenuItem* item = GTK_MENU_ITEM(JSObjectGetPrivate(thisObject));
175     gtk_menu_item_activate(item);
176     return JSValueMakeUndefined(context);
177 }
178
179 static JSStaticFunction staticMenuItemFunctions[] = {
180     { "click", menuItemClickCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
181     { 0, 0, 0 }
182 };
183
184 static JSStaticValue staticMenuItemValues[] = {
185     { "title", getMenuItemTitleCallback, setMenuItemTitleCallback, kJSPropertyAttributeNone },
186     { 0, 0, 0, 0 }
187 };
188
189 static JSClassRef getMenuItemClass()
190 {
191     static JSClassRef menuItemClass = 0;
192
193     if (!menuItemClass) {
194         JSClassDefinition classDefinition = {
195                 0, 0, 0, 0, 0, 0,
196                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
197         classDefinition.staticFunctions = staticMenuItemFunctions;
198         classDefinition.staticValues = staticMenuItemValues;
199
200         menuItemClass = JSClassCreate(&classDefinition);
201     }
202
203     return menuItemClass;
204 }
205
206
207 static JSValueRef contextClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
208 {
209     GdkEvent* pressEvent = gdk_event_new(GDK_BUTTON_PRESS);
210
211     if (!prepareMouseButtonEvent(pressEvent, 2, 0)) {
212         gdk_event_free(pressEvent);
213         return JSObjectMakeArray(context, 0, 0, 0);
214     }
215
216     GdkEvent* releaseEvent = gdk_event_copy(pressEvent);
217     sendOrQueueEvent(pressEvent);
218
219     JSValueRef valueRef = JSObjectMakeArray(context, 0, 0, 0);
220     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
221     GtkMenu* gtkMenu = webkit_web_view_get_context_menu(view);
222     if (gtkMenu) {
223         GOwnPtr<GList> items(gtk_container_get_children(GTK_CONTAINER(gtkMenu)));
224         JSValueRef arrayValues[g_list_length(items.get())];
225         int index = 0;
226         for (GList* item = g_list_first(items.get()); item; item = g_list_next(item)) {
227             arrayValues[index] = JSObjectMake(context, getMenuItemClass(), item->data);
228             index++;
229         }
230         if (index)
231             valueRef = JSObjectMakeArray(context, index - 1, arrayValues, 0);
232     }
233
234     releaseEvent->type = GDK_BUTTON_RELEASE;
235     sendOrQueueEvent(releaseEvent);
236     return valueRef;
237 }
238
239 static gboolean sendClick(gpointer)
240 {
241     GdkEvent* pressEvent = gdk_event_new(GDK_BUTTON_PRESS);
242
243     if (!prepareMouseButtonEvent(pressEvent, 1, 0)) {
244         gdk_event_free(pressEvent);
245         return FALSE;
246     }
247
248     GdkEvent* releaseEvent = gdk_event_copy(pressEvent);
249     dispatchEvent(pressEvent);
250     releaseEvent->type = GDK_BUTTON_RELEASE;
251     dispatchEvent(releaseEvent);
252
253     return FALSE;
254 }
255
256 static JSValueRef scheduleAsynchronousClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
257 {
258     g_idle_add_full(G_PRIORITY_DEFAULT, sendClick, 0, 0);
259     return JSValueMakeUndefined(context);
260 }
261
262 static void updateClickCount(int button)
263 {
264     if (lastClickPositionX != lastMousePositionX
265         || lastClickPositionY != lastMousePositionY
266         || lastClickButton != button
267         || timeOffset - lastClickTimeOffset >= 1)
268         clickCount = 1;
269     else
270         clickCount++;
271 }
272
273 static guint gdkModifierFromJSValue(JSContextRef context, const JSValueRef value)
274 {
275     JSStringRef string = JSValueToStringCopy(context, value, 0);
276     guint gdkModifier = 0;
277     if (JSStringIsEqualToUTF8CString(string, "ctrlKey")
278         || JSStringIsEqualToUTF8CString(string, "addSelectionKey"))
279         gdkModifier = GDK_CONTROL_MASK;
280     else if (JSStringIsEqualToUTF8CString(string, "shiftKey")
281              || JSStringIsEqualToUTF8CString(string, "rangeSelectionKey"))
282         gdkModifier = GDK_SHIFT_MASK;
283     else if (JSStringIsEqualToUTF8CString(string, "altKey"))
284         gdkModifier = GDK_MOD1_MASK;
285     
286     // Currently the metaKey as defined in WebCore/platform/gtk/PlatformMouseEventGtk.cpp
287     // is GDK_META_MASK. This code must be kept in sync with that file.
288     else if (JSStringIsEqualToUTF8CString(string, "metaKey"))
289         gdkModifier = GDK_META_MASK;
290     
291     JSStringRelease(string);
292     return gdkModifier;
293 }
294
295 static guint gdkModifersFromJSValue(JSContextRef context, const JSValueRef modifiers)
296 {
297     // The value may either be a string with a single modifier or an array of modifiers.
298     if (JSValueIsString(context, modifiers))
299         return gdkModifierFromJSValue(context, modifiers);
300
301     JSObjectRef modifiersArray = JSValueToObject(context, modifiers, 0);
302     if (!modifiersArray)
303         return 0;
304
305     guint gdkModifiers = 0;
306     JSRetainPtr<JSStringRef> lengthProperty(Adopt, JSStringCreateWithUTF8CString("length"));
307     int modifiersCount = JSValueToNumber(context, JSObjectGetProperty(context, modifiersArray, lengthProperty.get(), 0), 0);
308     for (int i = 0; i < modifiersCount; ++i)
309         gdkModifiers |= gdkModifierFromJSValue(context, JSObjectGetPropertyAtIndex(context, modifiersArray, i, 0));
310     return gdkModifiers;
311 }
312
313 static JSValueRef mouseDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
314 {
315     int button = 0;
316     if (argumentCount == 1) {
317         button = static_cast<int>(JSValueToNumber(context, arguments[0], exception));
318         g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
319     }
320     guint modifiers = argumentCount >= 2 ? gdkModifersFromJSValue(context, arguments[1]) : 0;
321
322     GdkEvent* event = gdk_event_new(GDK_BUTTON_PRESS);
323     if (!prepareMouseButtonEvent(event, button, modifiers)) {
324         gdk_event_free(event);
325         return JSValueMakeUndefined(context);
326     }
327
328     // If the same mouse button is already in the down position don't send another event as it may confuse Xvfb.
329     if (buttonCurrentlyDown == event->button.button) {
330         gdk_event_free(event);
331         return JSValueMakeUndefined(context);
332     }
333
334     buttonCurrentlyDown = event->button.button;
335
336     // Normally GDK will send both GDK_BUTTON_PRESS and GDK_2BUTTON_PRESS for
337     // the second button press during double-clicks. WebKit GTK+ selectively
338     // ignores the first GDK_BUTTON_PRESS of that pair using gdk_event_peek.
339     // Since our events aren't ever going onto the GDK event queue, WebKit won't
340     // be able to filter out the first GDK_BUTTON_PRESS, so we just don't send
341     // it here. Eventually this code should probably figure out a way to get all
342     // appropriate events onto the event queue and this work-around should be
343     // removed.
344     updateClickCount(event->button.button);
345     if (clickCount == 2)
346         event->type = GDK_2BUTTON_PRESS;
347     else if (clickCount == 3)
348         event->type = GDK_3BUTTON_PRESS;
349
350     sendOrQueueEvent(event);
351     return JSValueMakeUndefined(context);
352 }
353
354 static guint getStateFlags()
355 {
356     if (buttonCurrentlyDown == 1)
357         return GDK_BUTTON1_MASK;
358     if (buttonCurrentlyDown == 2)
359         return GDK_BUTTON2_MASK;
360     if (buttonCurrentlyDown == 3)
361         return GDK_BUTTON3_MASK;
362     return 0;
363 }
364
365 static JSValueRef mouseUpCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
366 {
367     int button = 0;
368     if (argumentCount == 1) {
369         button = static_cast<int>(JSValueToNumber(context, arguments[0], exception));
370         g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
371     }
372     guint modifiers = argumentCount >= 2 ? gdkModifersFromJSValue(context, arguments[1]) : 0;
373
374     GdkEvent* event = gdk_event_new(GDK_BUTTON_RELEASE);
375     if (!prepareMouseButtonEvent(event, button, modifiers)) {
376         gdk_event_free(event);
377         return JSValueMakeUndefined(context);
378     }
379
380     lastClickPositionX = lastMousePositionX;
381     lastClickPositionY = lastMousePositionY;
382     lastClickButton = buttonCurrentlyDown;
383     lastClickTimeOffset = timeOffset;
384     buttonCurrentlyDown = 0;
385
386     sendOrQueueEvent(event);
387     return JSValueMakeUndefined(context);
388 }
389
390 static JSValueRef mouseMoveToCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
391 {
392     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
393     if (!view)
394         return JSValueMakeUndefined(context);
395
396     if (argumentCount < 2)
397         return JSValueMakeUndefined(context);
398
399     lastMousePositionX = (int)JSValueToNumber(context, arguments[0], exception);
400     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
401     lastMousePositionY = (int)JSValueToNumber(context, arguments[1], exception);
402     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
403
404     GdkEvent* event = gdk_event_new(GDK_MOTION_NOTIFY);
405     event->motion.x = lastMousePositionX;
406     event->motion.y = lastMousePositionY;
407
408     event->motion.time = GDK_CURRENT_TIME;
409     event->motion.window = gtk_widget_get_window(GTK_WIDGET(view));
410     g_object_ref(event->motion.window);
411     event->button.device = getDefaultGDKPointerDevice(event->motion.window);
412
413     guint modifiers = argumentCount >= 3 ? gdkModifersFromJSValue(context, arguments[2]) : 0;
414     event->motion.state = modifiers | getStateFlags();
415     event->motion.axes = 0;
416
417     int xRoot, yRoot;
418     gdk_window_get_root_coords(gtk_widget_get_window(GTK_WIDGET(view)), lastMousePositionX, lastMousePositionY, &xRoot, &yRoot);
419     event->motion.x_root = xRoot;
420     event->motion.y_root = yRoot;
421
422     sendOrQueueEvent(event, false);
423     return JSValueMakeUndefined(context);
424 }
425
426 static JSValueRef mouseScrollByCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
427 {
428     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
429     if (!view)
430         return JSValueMakeUndefined(context);
431
432     if (argumentCount < 2)
433         return JSValueMakeUndefined(context);
434
435     int horizontal = (int)JSValueToNumber(context, arguments[0], exception);
436     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
437     int vertical = (int)JSValueToNumber(context, arguments[1], exception);
438     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
439
440     // Copy behaviour of Qt and EFL - just return in case of (0,0) mouse scroll
441     if (!horizontal && !vertical)
442         return JSValueMakeUndefined(context);
443
444     GdkEvent* event = gdk_event_new(GDK_SCROLL);
445     event->scroll.x = lastMousePositionX;
446     event->scroll.y = lastMousePositionY;
447     event->scroll.time = GDK_CURRENT_TIME;
448     event->scroll.window = gtk_widget_get_window(GTK_WIDGET(view));
449     g_object_ref(event->scroll.window);
450
451     // GTK+ only supports one tick in each scroll event that is not smooth. For the cases of more than one direction,
452     // and more than one step in a direction, we can only use smooth events, supported from Gtk 3.3.18.
453 #if GTK_CHECK_VERSION(3, 3, 18)
454     if ((horizontal && vertical) || horizontal > 1 || horizontal < -1 || vertical > 1 || vertical < -1) {
455         event->scroll.direction = GDK_SCROLL_SMOOTH;
456         event->scroll.delta_x = -horizontal;
457         event->scroll.delta_y = -vertical;
458
459         sendOrQueueEvent(event);
460         return JSValueMakeUndefined(context);
461     }
462 #else
463     g_return_val_if_fail((!vertical || !horizontal), JSValueMakeUndefined(context));
464 #endif
465
466     if (horizontal < 0)
467         event->scroll.direction = GDK_SCROLL_RIGHT;
468     else if (horizontal > 0)
469         event->scroll.direction = GDK_SCROLL_LEFT;
470     else if (vertical < 0)
471         event->scroll.direction = GDK_SCROLL_DOWN;
472     else if (vertical > 0)
473         event->scroll.direction = GDK_SCROLL_UP;
474     else
475         g_assert_not_reached();
476
477     sendOrQueueEvent(event);
478     return JSValueMakeUndefined(context);
479 }
480
481 static JSValueRef continuousMouseScrollByCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
482 {
483 #if GTK_CHECK_VERSION(3, 3, 18)
484     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
485     if (!view)
486         return JSValueMakeUndefined(context);
487
488     if (argumentCount < 2)
489         return JSValueMakeUndefined(context);
490
491     int horizontal = JSValueToNumber(context, arguments[0], exception);
492     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
493     int vertical = JSValueToNumber(context, arguments[1], exception);
494     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
495
496     // We do not yet support continuous scrolling by page.
497     if (argumentCount >= 3 && JSValueToBoolean(context, arguments[2]))
498         return JSValueMakeUndefined(context);
499
500     GdkEvent* event = gdk_event_new(GDK_SCROLL);
501     event->scroll.x = lastMousePositionX;
502     event->scroll.y = lastMousePositionY;
503     event->scroll.time = GDK_CURRENT_TIME;
504     event->scroll.window = gtk_widget_get_window(GTK_WIDGET(view));
505     g_object_ref(event->scroll.window);
506
507     event->scroll.direction = GDK_SCROLL_SMOOTH;
508     event->scroll.delta_x = -horizontal / pixelsPerScrollTick;
509     event->scroll.delta_y = -vertical / pixelsPerScrollTick;
510
511     sendOrQueueEvent(event);
512 #endif
513     return JSValueMakeUndefined(context);
514 }
515
516 static void dragWithFilesDragDataGetCallback(GtkWidget*, GdkDragContext*, GtkSelectionData *data, guint, guint, gpointer userData)
517 {
518     gtk_selection_data_set_uris(data, static_cast<gchar**>(userData));
519 }
520
521 static void dragWithFilesDragEndCallback(GtkWidget* widget, GdkDragContext*, gpointer userData)
522 {
523     g_signal_handlers_disconnect_by_func(widget, reinterpret_cast<void*>(dragWithFilesDragEndCallback), userData);
524     g_signal_handlers_disconnect_by_func(widget, reinterpret_cast<void*>(dragWithFilesDragDataGetCallback), userData);
525     g_strfreev(static_cast<gchar**>(userData));
526 }
527
528 static JSValueRef beginDragWithFilesCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
529 {
530     if (argumentCount < 1)
531         return JSValueMakeUndefined(context);
532
533     JSObjectRef filesArray = JSValueToObject(context, arguments[0], exception);
534     ASSERT(!exception || !*exception);
535
536     const gchar* mainFrameURI = webkit_web_frame_get_uri(mainFrame);
537     GRefPtr<GFile> testFile(adoptGRef(g_file_new_for_uri(mainFrameURI)));
538     GRefPtr<GFile> parentDirectory(g_file_get_parent(testFile.get()));
539     if (!parentDirectory)
540         return JSValueMakeUndefined(context);
541
542     // If this is an HTTP test, we still need to pass a local file path
543     // to WebCore. Even though the file doesn't exist, this should be fine
544     // for most tests.
545     GOwnPtr<gchar> scheme(g_file_get_uri_scheme(parentDirectory.get()));
546     if (g_str_equal(scheme.get(), "http") || g_str_equal(scheme.get(), "https")) {
547         GOwnPtr<gchar> currentDirectory(g_get_current_dir());
548         parentDirectory = adoptGRef(g_file_new_for_path(currentDirectory.get()));
549     }
550
551     JSStringRef lengthProperty = JSStringCreateWithUTF8CString("length");
552     int filesArrayLength = JSValueToNumber(context, JSObjectGetProperty(context, filesArray, lengthProperty, 0), 0);
553     JSStringRelease(lengthProperty);
554
555     gchar** draggedFilesURIList = g_new0(gchar*, filesArrayLength + 1);
556     for (int i = 0; i < filesArrayLength; ++i) {
557         JSStringRef filenameString = JSValueToStringCopy(context,
558                                                          JSObjectGetPropertyAtIndex(context, filesArray, i, 0), 0);
559         size_t bufferSize = JSStringGetMaximumUTF8CStringSize(filenameString);
560         GOwnPtr<gchar> filenameBuffer(static_cast<gchar*>(g_malloc(bufferSize)));
561         JSStringGetUTF8CString(filenameString, filenameBuffer.get(), bufferSize);
562         JSStringRelease(filenameString);
563
564         GRefPtr<GFile> dragFile(g_file_get_child(parentDirectory.get(), filenameBuffer.get()));
565         draggedFilesURIList[i] = g_file_get_uri(dragFile.get());
566     }
567
568     GtkWidget* view = GTK_WIDGET(webkit_web_frame_get_web_view(mainFrame));
569     g_object_connect(G_OBJECT(view),
570         "signal::drag-end", dragWithFilesDragEndCallback, draggedFilesURIList,
571         "signal::drag-data-get", dragWithFilesDragDataGetCallback, draggedFilesURIList,
572         NULL);
573
574     GdkEvent event;
575     GdkWindow* viewGDKWindow = gtk_widget_get_window(view);
576     memset(&event, 0, sizeof(event));
577     event.type = GDK_MOTION_NOTIFY;
578     event.motion.x = lastMousePositionX;
579     event.motion.y = lastMousePositionY;
580     event.motion.time = GDK_CURRENT_TIME;
581     event.motion.window = viewGDKWindow;
582     event.motion.device = getDefaultGDKPointerDevice(viewGDKWindow);
583     event.motion.state = GDK_BUTTON1_MASK;
584
585     int xRoot, yRoot;
586     gdk_window_get_root_coords(viewGDKWindow, lastMousePositionX, lastMousePositionY, &xRoot, &yRoot);
587     event.motion.x_root = xRoot;
588     event.motion.y_root = yRoot;
589
590     GtkTargetList* targetList = gtk_target_list_new(0, 0);
591     gtk_target_list_add_uri_targets(targetList, 0);
592     gtk_drag_begin(view, targetList, GDK_ACTION_COPY, 1, &event);
593     gtk_target_list_unref(targetList);
594
595     return JSValueMakeUndefined(context);
596 }
597
598 static void sendOrQueueEvent(GdkEvent* event, bool shouldReplaySavedEvents)
599 {
600     // Mouse move events are queued if the previous event was queued or if a
601     // delay was set up by leapForward().
602     if ((dragMode && buttonCurrentlyDown) || endOfQueue != startOfQueue || msgQueue[endOfQueue].delay) {
603         msgQueue[endOfQueue++].event = event;
604
605         if (shouldReplaySavedEvents)
606             replaySavedEvents();
607
608         return;
609     }
610
611     dispatchEvent(event);
612 }
613
614 static void dispatchEvent(GdkEvent* event)
615 {
616     DumpRenderTreeSupportGtk::layoutFrame(mainFrame);
617     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
618     if (!view) {
619         gdk_event_free(event);
620         return;
621     }
622
623     // The widget focus may have been lost in the course of the test,
624     // so force another explicit focus grab here.
625     gtk_widget_grab_focus(GTK_WIDGET(view));
626     gtk_main_do_event(event);
627
628     if (!currentDragSourceContext) {
629         gdk_event_free(event);
630         return;
631     }
632
633     if (event->type == GDK_MOTION_NOTIFY) {
634         // WebKit has called gtk_drag_start(), but because the main loop isn't
635         // running GDK internals don't know that the drag has started yet. Pump
636         // the main loop a little bit so that GDK is in the correct state.
637         while (gtk_events_pending())
638             gtk_main_iteration();
639
640         // Simulate a drag motion on the top-level GDK window.
641         GtkWidget* parentWidget = gtk_widget_get_parent(GTK_WIDGET(view));
642         GdkWindow* parentWidgetWindow = gtk_widget_get_window(parentWidget);
643         gdk_drag_motion(currentDragSourceContext, parentWidgetWindow, GDK_DRAG_PROTO_XDND,
644             event->motion.x_root, event->motion.y_root,
645             gdk_drag_context_get_selected_action(currentDragSourceContext),
646             gdk_drag_context_get_actions(currentDragSourceContext),
647             GDK_CURRENT_TIME);
648
649     } else if (currentDragSourceContext && event->type == GDK_BUTTON_RELEASE) {
650         // We've released the mouse button, we should just be able to spin the
651         // event loop here and have GTK+ send the appropriate notifications for
652         // the end of the drag.
653         while (gtk_events_pending())
654             gtk_main_iteration();
655     }
656
657     gdk_event_free(event);
658 }
659
660 void replaySavedEvents()
661 {
662     // First send all the events that are ready to be sent
663     while (startOfQueue < endOfQueue) {
664         if (msgQueue[startOfQueue].delay) {
665             g_usleep(msgQueue[startOfQueue].delay * 1000);
666             msgQueue[startOfQueue].delay = 0;
667         }
668
669         dispatchEvent(msgQueue[startOfQueue++].event);
670     }
671
672     startOfQueue = 0;
673     endOfQueue = 0;
674 }
675
676 static GdkEvent* createKeyPressEvent(JSContextRef context, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
677 {
678     g_return_val_if_fail(argumentCount >= 1, 0);
679     guint modifiers = argumentCount >= 2 ? gdkModifersFromJSValue(context, arguments[1]) : 0;
680
681     // handle location argument.
682     int location = DOM_KEY_LOCATION_STANDARD;
683     if (argumentCount > 2)
684         location = (int)JSValueToNumber(context, arguments[2], exception);
685
686     JSStringRef character = JSValueToStringCopy(context, arguments[0], exception);
687     g_return_val_if_fail((!exception || !*exception), 0);
688
689     int gdkKeySym = GDK_VoidSymbol;
690     if (location == DOM_KEY_LOCATION_NUMPAD) {
691         if (JSStringIsEqualToUTF8CString(character, "leftArrow"))
692             gdkKeySym = GDK_KP_Left;
693         else if (JSStringIsEqualToUTF8CString(character, "rightArrow"))
694             gdkKeySym = GDK_KP_Right;
695         else if (JSStringIsEqualToUTF8CString(character, "upArrow"))
696             gdkKeySym = GDK_KP_Up;
697         else if (JSStringIsEqualToUTF8CString(character, "downArrow"))
698             gdkKeySym = GDK_KP_Down;
699         else if (JSStringIsEqualToUTF8CString(character, "pageUp"))
700             gdkKeySym = GDK_KP_Page_Up;
701         else if (JSStringIsEqualToUTF8CString(character, "pageDown"))
702             gdkKeySym = GDK_KP_Page_Down;
703         else if (JSStringIsEqualToUTF8CString(character, "home"))
704             gdkKeySym = GDK_KP_Home;
705         else if (JSStringIsEqualToUTF8CString(character, "end"))
706             gdkKeySym = GDK_KP_End;
707         else if (JSStringIsEqualToUTF8CString(character, "insert"))
708             gdkKeySym = GDK_KP_Insert;
709         else if (JSStringIsEqualToUTF8CString(character, "delete"))
710             gdkKeySym = GDK_KP_Delete;
711         else
712             // If we get some other key specified with the numpad location,
713             // crash here, so we add it sooner rather than later.
714             g_assert_not_reached();
715     } else {
716         if (JSStringIsEqualToUTF8CString(character, "leftArrow"))
717             gdkKeySym = GDK_Left;
718         else if (JSStringIsEqualToUTF8CString(character, "rightArrow"))
719             gdkKeySym = GDK_Right;
720         else if (JSStringIsEqualToUTF8CString(character, "upArrow"))
721             gdkKeySym = GDK_Up;
722         else if (JSStringIsEqualToUTF8CString(character, "downArrow"))
723             gdkKeySym = GDK_Down;
724         else if (JSStringIsEqualToUTF8CString(character, "pageUp"))
725             gdkKeySym = GDK_Page_Up;
726         else if (JSStringIsEqualToUTF8CString(character, "pageDown"))
727             gdkKeySym = GDK_Page_Down;
728         else if (JSStringIsEqualToUTF8CString(character, "home"))
729             gdkKeySym = GDK_Home;
730         else if (JSStringIsEqualToUTF8CString(character, "end"))
731             gdkKeySym = GDK_End;
732         else if (JSStringIsEqualToUTF8CString(character, "insert"))
733             gdkKeySym = GDK_Insert;
734         else if (JSStringIsEqualToUTF8CString(character, "delete"))
735             gdkKeySym = GDK_Delete;
736         else if (JSStringIsEqualToUTF8CString(character, "printScreen"))
737             gdkKeySym = GDK_Print;
738         else if (JSStringIsEqualToUTF8CString(character, "menu"))
739             gdkKeySym = GDK_Menu;
740         else if (JSStringIsEqualToUTF8CString(character, "F1"))
741             gdkKeySym = GDK_F1;
742         else if (JSStringIsEqualToUTF8CString(character, "F2"))
743             gdkKeySym = GDK_F2;
744         else if (JSStringIsEqualToUTF8CString(character, "F3"))
745             gdkKeySym = GDK_F3;
746         else if (JSStringIsEqualToUTF8CString(character, "F4"))
747             gdkKeySym = GDK_F4;
748         else if (JSStringIsEqualToUTF8CString(character, "F5"))
749             gdkKeySym = GDK_F5;
750         else if (JSStringIsEqualToUTF8CString(character, "F6"))
751             gdkKeySym = GDK_F6;
752         else if (JSStringIsEqualToUTF8CString(character, "F7"))
753             gdkKeySym = GDK_F7;
754         else if (JSStringIsEqualToUTF8CString(character, "F8"))
755             gdkKeySym = GDK_F8;
756         else if (JSStringIsEqualToUTF8CString(character, "F9"))
757             gdkKeySym = GDK_F9;
758         else if (JSStringIsEqualToUTF8CString(character, "F10"))
759             gdkKeySym = GDK_F10;
760         else if (JSStringIsEqualToUTF8CString(character, "F11"))
761             gdkKeySym = GDK_F11;
762         else if (JSStringIsEqualToUTF8CString(character, "F12"))
763             gdkKeySym = GDK_F12;
764         else if (JSStringIsEqualToUTF8CString(character, "leftAlt"))
765             gdkKeySym = GDK_Alt_L;
766         else if (JSStringIsEqualToUTF8CString(character, "leftControl"))
767             gdkKeySym = GDK_Control_L;
768         else if (JSStringIsEqualToUTF8CString(character, "leftShift"))
769             gdkKeySym = GDK_Shift_L;
770         else if (JSStringIsEqualToUTF8CString(character, "rightAlt"))
771             gdkKeySym = GDK_Alt_R;
772         else if (JSStringIsEqualToUTF8CString(character, "rightControl"))
773             gdkKeySym = GDK_Control_R;
774         else if (JSStringIsEqualToUTF8CString(character, "rightShift"))
775             gdkKeySym = GDK_Shift_R;
776         else {
777             int charCode = JSStringGetCharactersPtr(character)[0];
778             if (charCode == '\n' || charCode == '\r')
779                 gdkKeySym = GDK_Return;
780             else if (charCode == '\t')
781                 gdkKeySym = GDK_Tab;
782             else if (charCode == '\x8')
783                 gdkKeySym = GDK_BackSpace;
784             else {
785                 gdkKeySym = gdk_unicode_to_keyval(charCode);
786                 if (WTF::isASCIIUpper(charCode))
787                     modifiers |= GDK_SHIFT_MASK;
788             }
789         }
790     }
791     JSStringRelease(character);
792
793     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
794     g_return_val_if_fail(view, 0);
795
796     GdkEvent* pressEvent = gdk_event_new(GDK_KEY_PRESS);
797     pressEvent->key.keyval = gdkKeySym;
798     pressEvent->key.state = modifiers;
799     pressEvent->key.window = gtk_widget_get_window(GTK_WIDGET(view));
800     g_object_ref(pressEvent->key.window);
801 #ifndef GTK_API_VERSION_2
802     gdk_event_set_device(pressEvent, getDefaultGDKPointerDevice(pressEvent->key.window));
803 #endif
804
805     // When synthesizing an event, an invalid hardware_keycode value
806     // can cause it to be badly processed by Gtk+.
807     GOwnPtr<GdkKeymapKey> keys;
808     gint nKeys;
809     if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(), gdkKeySym, &keys.outPtr(), &nKeys))
810         pressEvent->key.hardware_keycode = keys.get()[0].keycode;
811
812     return pressEvent;
813 }
814
815 static void sendKeyDown(GdkEvent* pressEvent)
816 {
817     g_return_if_fail(pressEvent);
818     GdkEvent* releaseEvent = gdk_event_copy(pressEvent);
819     releaseEvent->type = GDK_KEY_RELEASE;
820
821     dispatchEvent(pressEvent);
822     dispatchEvent(releaseEvent);
823
824     DumpRenderTreeSupportGtk::deliverAllMutationsIfNecessary();
825 }
826
827 static JSValueRef keyDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
828 {
829     GdkEvent* pressEvent = createKeyPressEvent(context, argumentCount, arguments, exception);
830     sendKeyDown(pressEvent);
831
832     return JSValueMakeUndefined(context);
833 }
834
835 static void zoomIn(gboolean fullContentsZoom)
836 {
837     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
838     if (!view)
839         return;
840
841     webkit_web_view_set_full_content_zoom(view, fullContentsZoom);
842     gfloat currentZoom = webkit_web_view_get_zoom_level(view);
843     webkit_web_view_set_zoom_level(view, currentZoom * zoomMultiplierRatio);
844 }
845
846 static void zoomOut(gboolean fullContentsZoom)
847 {
848     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
849     if (!view)
850         return;
851
852     webkit_web_view_set_full_content_zoom(view, fullContentsZoom);
853     gfloat currentZoom = webkit_web_view_get_zoom_level(view);
854     webkit_web_view_set_zoom_level(view, currentZoom / zoomMultiplierRatio);
855 }
856
857 static JSValueRef textZoomInCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
858 {
859     zoomIn(FALSE);
860     return JSValueMakeUndefined(context);
861 }
862
863 static JSValueRef textZoomOutCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
864 {
865     zoomOut(FALSE);
866     return JSValueMakeUndefined(context);
867 }
868
869 static JSValueRef zoomPageInCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
870 {
871     zoomIn(TRUE);
872     return JSValueMakeUndefined(context);
873 }
874
875 static JSValueRef zoomPageOutCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
876 {
877     zoomOut(TRUE);
878     return JSValueMakeUndefined(context);
879 }
880
881 static JSValueRef scalePageByCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
882 {
883     if (argumentCount < 3)
884         return JSValueMakeUndefined(context);
885
886     float scaleFactor = JSValueToNumber(context, arguments[0], exception);
887     float x = JSValueToNumber(context, arguments[1], exception);
888     float y = JSValueToNumber(context, arguments[2], exception);
889
890     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
891     if (!view)
892         return JSValueMakeUndefined(context);
893
894     DumpRenderTreeSupportGtk::scalePageBy(view, scaleFactor, x, y);
895
896     return JSValueMakeUndefined(context);
897 }
898
899 static gboolean sendAsynchronousKeyDown(gpointer userData)
900 {
901     sendKeyDown(static_cast<GdkEvent*>(userData));
902     return FALSE;
903 }
904
905 static JSValueRef scheduleAsynchronousKeyDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
906 {
907     GdkEvent* pressEvent = createKeyPressEvent(context, argumentCount, arguments, exception);
908     if (pressEvent)
909         g_idle_add_full(G_PRIORITY_DEFAULT, sendAsynchronousKeyDown, static_cast<gpointer>(pressEvent), 0);
910
911     return JSValueMakeUndefined(context);
912 }
913
914 static JSStaticFunction staticFunctions[] = {
915     { "mouseScrollBy", mouseScrollByCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
916     { "continuousMouseScrollBy", continuousMouseScrollByCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
917     { "contextClick", contextClickCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
918     { "mouseDown", mouseDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
919     { "mouseUp", mouseUpCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
920     { "mouseMoveTo", mouseMoveToCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
921     { "beginDragWithFiles", beginDragWithFilesCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
922     { "leapForward", leapForwardCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
923     { "keyDown", keyDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
924     { "textZoomIn", textZoomInCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
925     { "textZoomOut", textZoomOutCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
926     { "zoomPageIn", zoomPageInCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
927     { "zoomPageOut", zoomPageOutCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
928     { "scheduleAsynchronousClick", scheduleAsynchronousClickCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
929     { "scalePageBy", scalePageByCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
930     { "scheduleAsynchronousKeyDown", scheduleAsynchronousKeyDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
931
932     { 0, 0, 0 }
933 };
934
935 static JSStaticValue staticValues[] = {
936     { "dragMode", getDragModeCallback, setDragModeCallback, kJSPropertyAttributeNone },
937     { 0, 0, 0, 0 }
938 };
939
940 static JSClassRef getClass(JSContextRef context)
941 {
942     static JSClassRef eventSenderClass = 0;
943
944     if (!eventSenderClass) {
945         JSClassDefinition classDefinition = {
946                 0, 0, 0, 0, 0, 0,
947                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
948         classDefinition.staticFunctions = staticFunctions;
949         classDefinition.staticValues = staticValues;
950
951         eventSenderClass = JSClassCreate(&classDefinition);
952     }
953
954     return eventSenderClass;
955 }
956
957 JSObjectRef makeEventSender(JSContextRef context, bool isTopFrame)
958 {
959     if (isTopFrame) {
960         dragMode = true;
961
962         // Fly forward in time one second when the main frame loads. This will
963         // ensure that when a test begins clicking in the same location as
964         // a previous test, those clicks won't be interpreted as continuations
965         // of the previous test's click sequences.
966         timeOffset += 1000;
967
968         lastMousePositionX = lastMousePositionY = 0;
969         lastClickPositionX = lastClickPositionY = 0;
970         lastClickTimeOffset = 0;
971         lastClickButton = 0;
972         buttonCurrentlyDown = 0;
973         clickCount = 0;
974
975         endOfQueue = 0;
976         startOfQueue = 0;
977
978         currentDragSourceContext = 0;
979     }
980
981     return JSObjectMake(context, getClass(context), 0);
982 }
983
984 void dragBeginCallback(GtkWidget*, GdkDragContext* context, gpointer)
985 {
986     currentDragSourceContext = context;
987 }
988
989 void dragEndCallback(GtkWidget*, GdkDragContext* context, gpointer)
990 {
991     currentDragSourceContext = 0;
992 }
993
994 gboolean dragFailedCallback(GtkWidget*, GdkDragContext* context, gpointer)
995 {
996     // Return TRUE here to disable the stupid GTK+ drag failed animation,
997     // which introduces asynchronous behavior into our drags.
998     return TRUE;
999 }