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