WebKitTools/DumpRenderTree/gtk/EventSender.cpp
changeset 0 4f2f89ce4247
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebKitTools/DumpRenderTree/gtk/EventSender.cpp	Fri Sep 17 09:02:29 2010 +0300
@@ -0,0 +1,637 @@
+/*
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2009 Zan Dobersek <zandobersek@gmail.com>
+ * Copyright (C) 2009 Holger Hans Peter Freyther
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1.  Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ * 2.  Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ *     its contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "EventSender.h"
+
+#include "DumpRenderTree.h"
+
+#include <JavaScriptCore/JSObjectRef.h>
+#include <JavaScriptCore/JSRetainPtr.h>
+#include <JavaScriptCore/JSStringRef.h>
+#include <webkit/webkitwebframe.h>
+#include <webkit/webkitwebview.h>
+#include <wtf/ASCIICType.h>
+#include <wtf/Platform.h>
+
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+#include <string.h>
+
+// FIXME: Implement support for synthesizing drop events.
+
+extern "C" {
+    extern void webkit_web_frame_layout(WebKitWebFrame* frame);
+}
+
+static bool dragMode;
+static int timeOffset = 0;
+
+static int lastMousePositionX;
+static int lastMousePositionY;
+static int lastClickPositionX;
+static int lastClickPositionY;
+static int lastClickTimeOffset;
+static int lastClickButton;
+static int buttonCurrentlyDown;
+static int clickCount;
+
+struct DelayedMessage {
+    GdkEvent event;
+    gulong delay;
+};
+
+static DelayedMessage msgQueue[1024];
+
+static unsigned endOfQueue;
+static unsigned startOfQueue;
+
+static const float zoomMultiplierRatio = 1.2f;
+
+// Key event location code defined in DOM Level 3.
+enum KeyLocationCode {
+    DOM_KEY_LOCATION_STANDARD      = 0x00,
+    DOM_KEY_LOCATION_LEFT          = 0x01,
+    DOM_KEY_LOCATION_RIGHT         = 0x02,
+    DOM_KEY_LOCATION_NUMPAD        = 0x03
+};
+
+static void sendOrQueueEvent(GdkEvent, bool = true);
+static void dispatchEvent(GdkEvent event);
+static guint getStateFlags();
+
+#if !GTK_CHECK_VERSION(2, 17, 3)
+static void gdk_window_get_root_coords(GdkWindow* window, gint x, gint y, gint* rootX, gint* rootY)
+{
+    gdk_window_get_root_origin(window, rootX, rootY);
+    *rootX = *rootX + x;
+    *rootY = *rootY + y;
+}
+#endif
+
+#if !GTK_CHECK_VERSION(2, 14, 0)
+static GdkWindow* gtk_widget_get_window(GtkWidget* widget)
+{
+    g_return_val_if_fail(GTK_IS_WIDGET(widget), 0);
+    return widget->window;
+}
+#endif
+
+static JSValueRef getDragModeCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception)
+{
+    return JSValueMakeBoolean(context, dragMode);
+}
+
+static bool setDragModeCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception)
+{
+    dragMode = JSValueToBoolean(context, value);
+    return true;
+}
+
+static JSValueRef leapForwardCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    if (argumentCount > 0) {
+        msgQueue[endOfQueue].delay = JSValueToNumber(context, arguments[0], exception);
+        timeOffset += msgQueue[endOfQueue].delay;
+        ASSERT(!exception || !*exception);
+    }
+
+    return JSValueMakeUndefined(context);
+}
+
+bool prepareMouseButtonEvent(GdkEvent* event, int eventSenderButtonNumber)
+{
+    WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
+    if (!view)
+        return false;
+
+    // The logic for mapping EventSender button numbers to GDK button
+    // numbers originates from the Windows EventSender.
+    int gdkButtonNumber = 3;
+    if (eventSenderButtonNumber >= 0 && eventSenderButtonNumber <= 2)
+        gdkButtonNumber = eventSenderButtonNumber + 1;
+
+    // fast/events/mouse-click-events expects the 4th button
+    // to be event.button = 1, so send a middle-button event.
+    else if (eventSenderButtonNumber == 3)
+        gdkButtonNumber = 2;
+
+    memset(event, 0, sizeof(event));
+    event->button.button = gdkButtonNumber;
+    event->button.x = lastMousePositionX;
+    event->button.y = lastMousePositionY;
+    event->button.window = gtk_widget_get_window(GTK_WIDGET(view));
+    event->button.device = gdk_device_get_core_pointer();
+    event->button.state = getStateFlags();
+    event->button.time = GDK_CURRENT_TIME;
+
+    int xRoot, yRoot;
+    gdk_window_get_root_coords(gtk_widget_get_window(GTK_WIDGET(view)), lastMousePositionX, lastMousePositionY, &xRoot, &yRoot);
+    event->button.x_root = xRoot;
+    event->button.y_root = yRoot;
+
+    return true;
+}
+
+static JSValueRef contextClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    GdkEvent event;
+    if (!prepareMouseButtonEvent(&event, 2))
+        return JSValueMakeUndefined(context);
+
+    event.type = GDK_BUTTON_PRESS;
+    sendOrQueueEvent(event);
+    event.type = GDK_BUTTON_RELEASE;
+    sendOrQueueEvent(event);
+
+    return JSValueMakeUndefined(context);
+}
+
+static void updateClickCount(int button)
+{
+    if (lastClickPositionX != lastMousePositionX
+        || lastClickPositionY != lastMousePositionY
+        || lastClickButton != button
+        || timeOffset - lastClickTimeOffset >= 1)
+        clickCount = 1;
+    else
+        clickCount++;
+}
+
+static JSValueRef mouseDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    int button = 0;
+    if (argumentCount == 1) {
+        button = static_cast<int>(JSValueToNumber(context, arguments[0], exception));
+        g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
+    }
+
+    GdkEvent event;
+    if (!prepareMouseButtonEvent(&event, button))
+        return JSValueMakeUndefined(context);
+
+    buttonCurrentlyDown = event.button.button;
+
+    // Normally GDK will send both GDK_BUTTON_PRESS and GDK_2BUTTON_PRESS for
+    // the second button press during double-clicks. WebKit GTK+ selectively
+    // ignores the first GDK_BUTTON_PRESS of that pair using gdk_event_peek.
+    // Since our events aren't ever going onto the GDK event queue, WebKit won't
+    // be able to filter out the first GDK_BUTTON_PRESS, so we just don't send
+    // it here. Eventually this code should probably figure out a way to get all
+    // appropriate events onto the event queue and this work-around should be
+    // removed.
+    updateClickCount(event.button.button);
+    if (clickCount == 2)
+        event.type = GDK_2BUTTON_PRESS;
+    else if (clickCount == 3)
+        event.type = GDK_3BUTTON_PRESS;
+    else
+        event.type = GDK_BUTTON_PRESS;
+
+    sendOrQueueEvent(event);
+    return JSValueMakeUndefined(context);
+}
+
+static guint getStateFlags()
+{
+    if (buttonCurrentlyDown == 1)
+        return GDK_BUTTON1_MASK;
+    if (buttonCurrentlyDown == 2)
+        return GDK_BUTTON2_MASK;
+    if (buttonCurrentlyDown == 3)
+        return GDK_BUTTON3_MASK;
+    return 0;
+}
+
+static JSValueRef mouseUpCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    int button = 0;
+    if (argumentCount == 1) {
+        button = static_cast<int>(JSValueToNumber(context, arguments[0], exception));
+        g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
+    }
+
+    GdkEvent event;
+    if (!prepareMouseButtonEvent(&event, button))
+        return JSValueMakeUndefined(context);
+
+    lastClickPositionX = lastMousePositionX;
+    lastClickPositionY = lastMousePositionY;
+    lastClickButton = buttonCurrentlyDown;
+    lastClickTimeOffset = timeOffset;
+    buttonCurrentlyDown = 0;
+
+    event.type = GDK_BUTTON_RELEASE;
+    sendOrQueueEvent(event);
+    return JSValueMakeUndefined(context);
+}
+
+static JSValueRef mouseMoveToCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
+    if (!view)
+        return JSValueMakeUndefined(context);
+
+    if (argumentCount < 2)
+        return JSValueMakeUndefined(context);
+
+    lastMousePositionX = (int)JSValueToNumber(context, arguments[0], exception);
+    g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
+    lastMousePositionY = (int)JSValueToNumber(context, arguments[1], exception);
+    g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
+
+    GdkEvent event;
+    memset(&event, 0, sizeof(event));
+    event.type = GDK_MOTION_NOTIFY;
+    event.motion.x = lastMousePositionX;
+    event.motion.y = lastMousePositionY;
+
+    event.motion.time = GDK_CURRENT_TIME;
+    event.motion.window = gtk_widget_get_window(GTK_WIDGET(view));
+    event.motion.device = gdk_device_get_core_pointer();
+    event.motion.state = getStateFlags();
+
+    int xRoot, yRoot;
+    gdk_window_get_root_coords(gtk_widget_get_window(GTK_WIDGET(view)), lastMousePositionX, lastMousePositionY, &xRoot, &yRoot);
+    event.motion.x_root = xRoot;
+    event.motion.y_root = yRoot;
+
+    sendOrQueueEvent(event, false);
+    return JSValueMakeUndefined(context);
+}
+
+static JSValueRef mouseWheelToCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
+    if (!view)
+        return JSValueMakeUndefined(context);
+
+    if (argumentCount < 2)
+        return JSValueMakeUndefined(context);
+
+    int horizontal = (int)JSValueToNumber(context, arguments[0], exception);
+    g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
+    int vertical = (int)JSValueToNumber(context, arguments[1], exception);
+    g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
+
+    // GTK+ doesn't support multiple direction scrolls in the same event!
+    g_return_val_if_fail((!vertical || !horizontal), JSValueMakeUndefined(context));
+
+    GdkEvent event;
+    event.type = GDK_SCROLL;
+    event.scroll.x = lastMousePositionX;
+    event.scroll.y = lastMousePositionY;
+    event.scroll.time = GDK_CURRENT_TIME;
+    event.scroll.window = gtk_widget_get_window(GTK_WIDGET(view));
+
+    if (horizontal < 0)
+        event.scroll.direction = GDK_SCROLL_LEFT;
+    else if (horizontal > 0)
+        event.scroll.direction = GDK_SCROLL_RIGHT;
+    else if (vertical < 0)
+        event.scroll.direction = GDK_SCROLL_UP;
+    else if (vertical > 0)
+        event.scroll.direction = GDK_SCROLL_DOWN;
+    else
+        g_assert_not_reached();
+
+    sendOrQueueEvent(event);
+    return JSValueMakeUndefined(context);
+}
+
+static JSValueRef beginDragWithFilesCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    if (argumentCount < 1)
+        return JSValueMakeUndefined(context);
+
+    // FIXME: Implement this completely once WebCore has complete drag and drop support
+    return JSValueMakeUndefined(context);
+}
+
+static void sendOrQueueEvent(GdkEvent event, bool shouldReplaySavedEvents)
+{
+    // Mouse move events are queued if the previous event was queued or if a
+    // delay was set up by leapForward().
+    if ((dragMode && buttonCurrentlyDown) || endOfQueue != startOfQueue || msgQueue[endOfQueue].delay) {
+        msgQueue[endOfQueue++].event = event;
+
+        if (shouldReplaySavedEvents)
+            replaySavedEvents();
+
+        return;
+    }
+
+    dispatchEvent(event);
+}
+
+static void dispatchEvent(GdkEvent event)
+{
+    webkit_web_frame_layout(mainFrame);
+    WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
+    if (!view)
+        return;
+
+    gtk_main_do_event(&event);
+}
+
+void replaySavedEvents()
+{
+    // FIXME: Eventually we may need to have more sophisticated logic to
+    // track drag-and-drop operations.
+
+    // First send all the events that are ready to be sent
+    while (startOfQueue < endOfQueue) {
+        if (msgQueue[startOfQueue].delay) {
+            g_usleep(msgQueue[startOfQueue].delay * 1000);
+            msgQueue[startOfQueue].delay = 0;
+        }
+
+        dispatchEvent(msgQueue[startOfQueue++].event);
+    }
+
+    startOfQueue = 0;
+    endOfQueue = 0;
+}
+
+static JSValueRef keyDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    if (argumentCount < 1)
+        return JSValueMakeUndefined(context);
+
+    static const JSStringRef lengthProperty = JSStringCreateWithUTF8CString("length");
+
+    webkit_web_frame_layout(mainFrame);
+
+    // handle modifier keys.
+    int state = 0;
+    if (argumentCount > 1) {
+        JSObjectRef modifiersArray = JSValueToObject(context, arguments[1], exception);
+        if (modifiersArray) {
+            for (int i = 0; i < JSValueToNumber(context, JSObjectGetProperty(context, modifiersArray, lengthProperty, 0), 0); ++i) {
+                JSValueRef value = JSObjectGetPropertyAtIndex(context, modifiersArray, i, 0);
+                JSStringRef string = JSValueToStringCopy(context, value, 0);
+                if (JSStringIsEqualToUTF8CString(string, "ctrlKey"))
+                    state |= GDK_CONTROL_MASK;
+                else if (JSStringIsEqualToUTF8CString(string, "shiftKey"))
+                    state |= GDK_SHIFT_MASK;
+                else if (JSStringIsEqualToUTF8CString(string, "altKey"))
+                    state |= GDK_MOD1_MASK;
+
+                JSStringRelease(string);
+            }
+        }
+    }
+
+    // handle location argument.
+    int location = DOM_KEY_LOCATION_STANDARD;
+    if (argumentCount > 2)
+        location = (int)JSValueToNumber(context, arguments[2], exception);
+
+    JSStringRef character = JSValueToStringCopy(context, arguments[0], exception);
+    g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
+    int gdkKeySym = GDK_VoidSymbol;
+    if (location == DOM_KEY_LOCATION_NUMPAD) {
+        if (JSStringIsEqualToUTF8CString(character, "leftArrow"))
+            gdkKeySym = GDK_KP_Left;
+        else if (JSStringIsEqualToUTF8CString(character, "rightArrow"))
+            gdkKeySym = GDK_KP_Right;
+        else if (JSStringIsEqualToUTF8CString(character, "upArrow"))
+            gdkKeySym = GDK_KP_Up;
+        else if (JSStringIsEqualToUTF8CString(character, "downArrow"))
+            gdkKeySym = GDK_KP_Down;
+        else if (JSStringIsEqualToUTF8CString(character, "pageUp"))
+            gdkKeySym = GDK_KP_Page_Up;
+        else if (JSStringIsEqualToUTF8CString(character, "pageDown"))
+            gdkKeySym = GDK_KP_Page_Down;
+        else if (JSStringIsEqualToUTF8CString(character, "home"))
+            gdkKeySym = GDK_KP_Home;
+        else if (JSStringIsEqualToUTF8CString(character, "end"))
+            gdkKeySym = GDK_KP_End;
+        else
+            // Assume we only get arrow/pgUp/pgDn/home/end keys with
+            // location=NUMPAD for now.
+            g_assert_not_reached();
+    } else {
+        if (JSStringIsEqualToUTF8CString(character, "leftArrow"))
+            gdkKeySym = GDK_Left;
+        else if (JSStringIsEqualToUTF8CString(character, "rightArrow"))
+            gdkKeySym = GDK_Right;
+        else if (JSStringIsEqualToUTF8CString(character, "upArrow"))
+            gdkKeySym = GDK_Up;
+        else if (JSStringIsEqualToUTF8CString(character, "downArrow"))
+            gdkKeySym = GDK_Down;
+        else if (JSStringIsEqualToUTF8CString(character, "pageUp"))
+            gdkKeySym = GDK_Page_Up;
+        else if (JSStringIsEqualToUTF8CString(character, "pageDown"))
+            gdkKeySym = GDK_Page_Down;
+        else if (JSStringIsEqualToUTF8CString(character, "home"))
+            gdkKeySym = GDK_Home;
+        else if (JSStringIsEqualToUTF8CString(character, "end"))
+            gdkKeySym = GDK_End;
+        else if (JSStringIsEqualToUTF8CString(character, "delete"))
+            gdkKeySym = GDK_Delete;
+        else if (JSStringIsEqualToUTF8CString(character, "F1"))
+            gdkKeySym = GDK_F1;
+        else if (JSStringIsEqualToUTF8CString(character, "F2"))
+            gdkKeySym = GDK_F2;
+        else if (JSStringIsEqualToUTF8CString(character, "F3"))
+            gdkKeySym = GDK_F3;
+        else if (JSStringIsEqualToUTF8CString(character, "F4"))
+            gdkKeySym = GDK_F4;
+        else if (JSStringIsEqualToUTF8CString(character, "F5"))
+            gdkKeySym = GDK_F5;
+        else if (JSStringIsEqualToUTF8CString(character, "F6"))
+            gdkKeySym = GDK_F6;
+        else if (JSStringIsEqualToUTF8CString(character, "F7"))
+            gdkKeySym = GDK_F7;
+        else if (JSStringIsEqualToUTF8CString(character, "F8"))
+            gdkKeySym = GDK_F8;
+        else if (JSStringIsEqualToUTF8CString(character, "F9"))
+            gdkKeySym = GDK_F9;
+        else if (JSStringIsEqualToUTF8CString(character, "F10"))
+            gdkKeySym = GDK_F10;
+        else if (JSStringIsEqualToUTF8CString(character, "F11"))
+            gdkKeySym = GDK_F11;
+        else if (JSStringIsEqualToUTF8CString(character, "F12"))
+            gdkKeySym = GDK_F12;
+        else {
+            int charCode = JSStringGetCharactersPtr(character)[0];
+            if (charCode == '\n' || charCode == '\r')
+                gdkKeySym = GDK_Return;
+            else if (charCode == '\t')
+                gdkKeySym = GDK_Tab;
+            else if (charCode == '\x8')
+                gdkKeySym = GDK_BackSpace;
+            else {
+                gdkKeySym = gdk_unicode_to_keyval(charCode);
+                if (WTF::isASCIIUpper(charCode))
+                    state |= GDK_SHIFT_MASK;
+            }
+        }
+    }
+    JSStringRelease(character);
+
+    WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
+    if (!view)
+        return JSValueMakeUndefined(context);
+
+    // create and send the event
+    GdkEvent event;
+    memset(&event, 0, sizeof(event));
+    event.key.keyval = gdkKeySym;
+    event.key.state = state;
+    event.key.window = gtk_widget_get_window(GTK_WIDGET(view));
+
+    // When synthesizing an event, an invalid hardware_keycode value
+    // can cause it to be badly processed by Gtk+.
+    GdkKeymapKey* keys;
+    gint n_keys;
+    if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(), gdkKeySym, &keys, &n_keys)) {
+        event.key.hardware_keycode = keys[0].keycode;
+        g_free(keys);
+    }
+
+    event.key.type = GDK_KEY_PRESS;
+    dispatchEvent(event);
+
+    event.key.type = GDK_KEY_RELEASE;
+    dispatchEvent(event);
+
+    return JSValueMakeUndefined(context);
+}
+
+static void zoomIn(gboolean fullContentsZoom)
+{
+    WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
+    if (!view)
+        return;
+
+    webkit_web_view_set_full_content_zoom(view, fullContentsZoom);
+    gfloat currentZoom = webkit_web_view_get_zoom_level(view);
+    webkit_web_view_set_zoom_level(view, currentZoom * zoomMultiplierRatio);
+}
+
+static void zoomOut(gboolean fullContentsZoom)
+{
+    WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
+    if (!view)
+        return;
+
+    webkit_web_view_set_full_content_zoom(view, fullContentsZoom);
+    gfloat currentZoom = webkit_web_view_get_zoom_level(view);
+    webkit_web_view_set_zoom_level(view, currentZoom / zoomMultiplierRatio);
+}
+
+static JSValueRef textZoomInCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    zoomIn(FALSE);
+    return JSValueMakeUndefined(context);
+}
+
+static JSValueRef textZoomOutCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    zoomOut(FALSE);
+    return JSValueMakeUndefined(context);
+}
+
+static JSValueRef zoomPageInCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    zoomIn(TRUE);
+    return JSValueMakeUndefined(context);
+}
+
+static JSValueRef zoomPageOutCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    zoomOut(TRUE);
+    return JSValueMakeUndefined(context);
+}
+
+static JSStaticFunction staticFunctions[] = {
+    { "mouseWheelTo", mouseWheelToCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "contextClick", contextClickCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "mouseDown", mouseDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "mouseUp", mouseUpCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "mouseMoveTo", mouseMoveToCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "beginDragWithFiles", beginDragWithFilesCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "leapForward", leapForwardCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "keyDown", keyDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "textZoomIn", textZoomInCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "textZoomOut", textZoomOutCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "zoomPageIn", zoomPageInCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { "zoomPageOut", zoomPageOutCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+    { 0, 0, 0 }
+};
+
+static JSStaticValue staticValues[] = {
+    { "dragMode", getDragModeCallback, setDragModeCallback, kJSPropertyAttributeNone },
+    { 0, 0, 0, 0 }
+};
+
+static JSClassRef getClass(JSContextRef context)
+{
+    static JSClassRef eventSenderClass = 0;
+
+    if (!eventSenderClass) {
+        JSClassDefinition classDefinition = {
+                0, 0, 0, 0, 0, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        classDefinition.staticFunctions = staticFunctions;
+        classDefinition.staticValues = staticValues;
+
+        eventSenderClass = JSClassCreate(&classDefinition);
+    }
+
+    return eventSenderClass;
+}
+
+JSObjectRef makeEventSender(JSContextRef context, bool isTopFrame)
+{
+    if (isTopFrame) {
+        dragMode = true;
+
+        // Fly forward in time one second when the main frame loads. This will
+        // ensure that when a test begins clicking in the same location as
+        // a previous test, those clicks won't be interpreted as continuations
+        // of the previous test's click sequences.
+        timeOffset += 1000;
+
+        lastMousePositionX = lastMousePositionY = 0;
+        lastClickPositionX = lastClickPositionY = 0;
+        lastClickTimeOffset = 0;
+        lastClickButton = 0;
+        buttonCurrentlyDown = 0;
+        clickCount = 0;
+
+        endOfQueue = 0;
+        startOfQueue = 0;
+    }
+
+    return JSObjectMake(context, getClass(context), 0);
+}