2010-06-16 Martin Robinson <mrobinson@igalia.com>
[WebKit-https.git] / WebKitTools / 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  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1.  Redistributions of source code must retain the above copyright
11  *     notice, this list of conditions and the following disclaimer.
12  * 2.  Redistributions in binary form must reproduce the above copyright
13  *     notice, this list of conditions and the following disclaimer in the
14  *     documentation and/or other materials provided with the distribution.
15  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16  *     its contributors may be used to endorse or promote products derived
17  *     from this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 #include "config.h"
32 #include "EventSender.h"
33
34 #include "DumpRenderTree.h"
35
36 #include <JavaScriptCore/JSObjectRef.h>
37 #include <JavaScriptCore/JSRetainPtr.h>
38 #include <JavaScriptCore/JSStringRef.h>
39 #include <webkit/webkitwebframe.h>
40 #include <webkit/webkitwebview.h>
41 #include <wtf/ASCIICType.h>
42 #include <wtf/Platform.h>
43
44 #include <gdk/gdk.h>
45 #include <gdk/gdkkeysyms.h>
46 #include <string.h>
47
48 // FIXME: Implement support for synthesizing drop events.
49
50 extern "C" {
51     extern void webkit_web_frame_layout(WebKitWebFrame* frame);
52 }
53
54 static bool dragMode;
55 static int timeOffset = 0;
56
57 static int lastMousePositionX;
58 static int lastMousePositionY;
59 static int lastClickPositionX;
60 static int lastClickPositionY;
61 static int lastClickTimeOffset;
62 static int lastClickButton;
63 static int buttonCurrentlyDown;
64 static int clickCount;
65
66 struct DelayedMessage {
67     GdkEvent event;
68     gulong delay;
69 };
70
71 static DelayedMessage msgQueue[1024];
72
73 static unsigned endOfQueue;
74 static unsigned startOfQueue;
75
76 static const float zoomMultiplierRatio = 1.2f;
77
78 // Key event location code defined in DOM Level 3.
79 enum KeyLocationCode {
80     DOM_KEY_LOCATION_STANDARD      = 0x00,
81     DOM_KEY_LOCATION_LEFT          = 0x01,
82     DOM_KEY_LOCATION_RIGHT         = 0x02,
83     DOM_KEY_LOCATION_NUMPAD        = 0x03
84 };
85
86 static void sendOrQueueEvent(GdkEvent, bool = true);
87 static void dispatchEvent(GdkEvent event);
88 static guint getStateFlags();
89
90 static JSValueRef getDragModeCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception)
91 {
92     return JSValueMakeBoolean(context, dragMode);
93 }
94
95 static bool setDragModeCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception)
96 {
97     dragMode = JSValueToBoolean(context, value);
98     return true;
99 }
100
101 static JSValueRef leapForwardCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
102 {
103     if (argumentCount > 0) {
104         msgQueue[endOfQueue].delay = JSValueToNumber(context, arguments[0], exception);
105         timeOffset += msgQueue[endOfQueue].delay;
106         ASSERT(!exception || !*exception);
107     }
108
109     return JSValueMakeUndefined(context);
110 }
111
112 #if !GTK_CHECK_VERSION(2,17,3)
113 static void getRootCoords(GtkWidget* view, int* rootX, int* rootY)
114 {
115     GtkWidget* window = gtk_widget_get_toplevel(GTK_WIDGET(view));
116     int tmpX, tmpY;
117
118     gtk_widget_translate_coordinates(view, window, lastMousePositionX, lastMousePositionY, &tmpX, &tmpY);
119
120     gdk_window_get_origin(window->window, rootX, rootY);
121
122     *rootX += tmpX;
123     *rootY += tmpY;
124 }
125 #endif
126
127 bool prepareMouseButtonEvent(GdkEvent* event, int eventSenderButtonNumber)
128 {
129     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
130     if (!view)
131         return false;
132
133     // The logic for mapping EventSender button numbers to GDK button
134     // numbers originates from the Windows EventSender.
135     int gdkButtonNumber = 3;
136     if (eventSenderButtonNumber >= 0 && eventSenderButtonNumber <= 2)
137         gdkButtonNumber = eventSenderButtonNumber + 1;
138
139     // fast/events/mouse-click-events expects the 4th button
140     // to be event.button = 1, so send a middle-button event.
141     else if (eventSenderButtonNumber == 3)
142         gdkButtonNumber = 2;
143
144     memset(event, 0, sizeof(event));
145     event->button.button = gdkButtonNumber;
146     event->button.x = lastMousePositionX;
147     event->button.y = lastMousePositionY;
148     event->button.window = GTK_WIDGET(view)->window;
149     event->button.device = gdk_device_get_core_pointer();
150     event->button.state = getStateFlags();
151     event->button.time = GDK_CURRENT_TIME;
152
153     int xRoot, yRoot;
154 #if GTK_CHECK_VERSION(2, 17, 3)
155     gdk_window_get_root_coords(GTK_WIDGET(view)->window, lastMousePositionX, lastMousePositionY, &xRoot, &yRoot);
156 #else
157     getRootCoords(GTK_WIDGET(view), &xRoot, &yRoot);
158 #endif
159     event->button.x_root = xRoot;
160     event->button.y_root = yRoot;
161
162     return true;
163 }
164
165 static JSValueRef contextClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
166 {
167     GdkEvent event;
168     if (!prepareMouseButtonEvent(&event, 2))
169         return JSValueMakeUndefined(context);
170
171     event.type = GDK_BUTTON_PRESS;
172     sendOrQueueEvent(event);
173     event.type = GDK_BUTTON_RELEASE;
174     sendOrQueueEvent(event);
175
176     return JSValueMakeUndefined(context);
177 }
178
179 static void updateClickCount(int button)
180 {
181     if (lastClickPositionX != lastMousePositionX
182         || lastClickPositionY != lastMousePositionY
183         || lastClickButton != button
184         || timeOffset - lastClickTimeOffset >= 1)
185         clickCount = 1;
186     else
187         clickCount++;
188 }
189
190 static JSValueRef mouseDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
191 {
192     int button = 0;
193     if (argumentCount == 1) {
194         button = static_cast<int>(JSValueToNumber(context, arguments[0], exception));
195         g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
196     }
197
198     GdkEvent event;
199     if (!prepareMouseButtonEvent(&event, button))
200         return JSValueMakeUndefined(context);
201
202     buttonCurrentlyDown = event.button.button;
203
204     // Normally GDK will send both GDK_BUTTON_PRESS and GDK_2BUTTON_PRESS for
205     // the second button press during double-clicks. WebKit GTK+ selectively
206     // ignores the first GDK_BUTTON_PRESS of that pair using gdk_event_peek.
207     // Since our events aren't ever going onto the GDK event queue, WebKit won't
208     // be able to filter out the first GDK_BUTTON_PRESS, so we just don't send
209     // it here. Eventually this code should probably figure out a way to get all
210     // appropriate events onto the event queue and this work-around should be
211     // removed.
212     updateClickCount(event.button.button);
213     if (clickCount == 2)
214         event.type = GDK_2BUTTON_PRESS;
215     else if (clickCount == 3)
216         event.type = GDK_3BUTTON_PRESS;
217     else
218         event.type = GDK_BUTTON_PRESS;
219
220     sendOrQueueEvent(event);
221     return JSValueMakeUndefined(context);
222 }
223
224 static guint getStateFlags()
225 {
226     if (buttonCurrentlyDown == 1)
227         return GDK_BUTTON1_MASK;
228     if (buttonCurrentlyDown == 2)
229         return GDK_BUTTON2_MASK;
230     if (buttonCurrentlyDown == 3)
231         return GDK_BUTTON3_MASK;
232     return 0;
233 }
234
235 static JSValueRef mouseUpCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
236 {
237     int button = 0;
238     if (argumentCount == 1) {
239         button = static_cast<int>(JSValueToNumber(context, arguments[0], exception));
240         g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
241     }
242
243     GdkEvent event;
244     if (!prepareMouseButtonEvent(&event, button))
245         return JSValueMakeUndefined(context);
246
247     lastClickPositionX = lastMousePositionX;
248     lastClickPositionY = lastMousePositionY;
249     lastClickButton = buttonCurrentlyDown;
250     lastClickTimeOffset = timeOffset;
251     buttonCurrentlyDown = 0;
252
253     event.type = GDK_BUTTON_RELEASE;
254     sendOrQueueEvent(event);
255     return JSValueMakeUndefined(context);
256 }
257
258 static JSValueRef mouseMoveToCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
259 {
260     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
261     if (!view)
262         return JSValueMakeUndefined(context);
263
264     if (argumentCount < 2)
265         return JSValueMakeUndefined(context);
266
267     lastMousePositionX = (int)JSValueToNumber(context, arguments[0], exception);
268     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
269     lastMousePositionY = (int)JSValueToNumber(context, arguments[1], exception);
270     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
271
272     GdkEvent event;
273     memset(&event, 0, sizeof(event));
274     event.type = GDK_MOTION_NOTIFY;
275     event.motion.x = lastMousePositionX;
276     event.motion.y = lastMousePositionY;
277
278     event.motion.time = GDK_CURRENT_TIME;
279     event.motion.window = GTK_WIDGET(view)->window;
280     event.motion.device = gdk_device_get_core_pointer();
281     event.motion.state = getStateFlags();
282
283     int xRoot, yRoot;
284 #if GTK_CHECK_VERSION(2,17,3)
285     gdk_window_get_root_coords(GTK_WIDGET(view)->window, lastMousePositionX, lastMousePositionY, &xRoot, &yRoot);
286 #else
287     getRootCoords(GTK_WIDGET(view), &xRoot, &yRoot);
288 #endif
289     event.motion.x_root = xRoot;
290     event.motion.y_root = yRoot;
291
292     sendOrQueueEvent(event, false);
293     return JSValueMakeUndefined(context);
294 }
295
296 static JSValueRef mouseWheelToCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
297 {
298     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
299     if (!view)
300         return JSValueMakeUndefined(context);
301
302     if (argumentCount < 2)
303         return JSValueMakeUndefined(context);
304
305     int horizontal = (int)JSValueToNumber(context, arguments[0], exception);
306     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
307     int vertical = (int)JSValueToNumber(context, arguments[1], exception);
308     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
309
310     // GTK+ doesn't support multiple direction scrolls in the same event!
311     g_return_val_if_fail((!vertical || !horizontal), JSValueMakeUndefined(context));
312
313     GdkEvent event;
314     event.type = GDK_SCROLL;
315     event.scroll.x = lastMousePositionX;
316     event.scroll.y = lastMousePositionY;
317     event.scroll.time = GDK_CURRENT_TIME;
318     event.scroll.window = GTK_WIDGET(view)->window;
319
320     if (horizontal < 0)
321         event.scroll.direction = GDK_SCROLL_LEFT;
322     else if (horizontal > 0)
323         event.scroll.direction = GDK_SCROLL_RIGHT;
324     else if (vertical < 0)
325         event.scroll.direction = GDK_SCROLL_UP;
326     else if (vertical > 0)
327         event.scroll.direction = GDK_SCROLL_DOWN;
328     else
329         g_assert_not_reached();
330
331     sendOrQueueEvent(event);
332     return JSValueMakeUndefined(context);
333 }
334
335 static JSValueRef beginDragWithFilesCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
336 {
337     if (argumentCount < 1)
338         return JSValueMakeUndefined(context);
339
340     // FIXME: Implement this completely once WebCore has complete drag and drop support
341     return JSValueMakeUndefined(context);
342 }
343
344 static void sendOrQueueEvent(GdkEvent event, bool shouldReplaySavedEvents)
345 {
346     // Mouse move events are queued if the previous event was queued or if a
347     // delay was set up by leapForward().
348     if ((dragMode && buttonCurrentlyDown) || endOfQueue != startOfQueue || msgQueue[endOfQueue].delay) {
349         msgQueue[endOfQueue++].event = event;
350
351         if (shouldReplaySavedEvents)
352             replaySavedEvents();
353
354         return;
355     }
356
357     dispatchEvent(event);
358 }
359
360 static void dispatchEvent(GdkEvent event)
361 {
362     webkit_web_frame_layout(mainFrame);
363     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
364     if (!view)
365         return;
366
367     gtk_main_do_event(&event);
368 }
369
370 void replaySavedEvents()
371 {
372     // FIXME: Eventually we may need to have more sophisticated logic to
373     // track drag-and-drop operations.
374
375     // First send all the events that are ready to be sent
376     while (startOfQueue < endOfQueue) {
377         if (msgQueue[startOfQueue].delay) {
378             g_usleep(msgQueue[startOfQueue].delay * 1000);
379             msgQueue[startOfQueue].delay = 0;
380         }
381
382         dispatchEvent(msgQueue[startOfQueue++].event);
383     }
384
385     startOfQueue = 0;
386     endOfQueue = 0;
387 }
388
389 static JSValueRef keyDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
390 {
391     if (argumentCount < 1)
392         return JSValueMakeUndefined(context);
393
394     static const JSStringRef lengthProperty = JSStringCreateWithUTF8CString("length");
395
396     webkit_web_frame_layout(mainFrame);
397
398     // handle modifier keys.
399     int state = 0;
400     if (argumentCount > 1) {
401         JSObjectRef modifiersArray = JSValueToObject(context, arguments[1], exception);
402         if (modifiersArray) {
403             for (int i = 0; i < JSValueToNumber(context, JSObjectGetProperty(context, modifiersArray, lengthProperty, 0), 0); ++i) {
404                 JSValueRef value = JSObjectGetPropertyAtIndex(context, modifiersArray, i, 0);
405                 JSStringRef string = JSValueToStringCopy(context, value, 0);
406                 if (JSStringIsEqualToUTF8CString(string, "ctrlKey"))
407                     state |= GDK_CONTROL_MASK;
408                 else if (JSStringIsEqualToUTF8CString(string, "shiftKey"))
409                     state |= GDK_SHIFT_MASK;
410                 else if (JSStringIsEqualToUTF8CString(string, "altKey"))
411                     state |= GDK_MOD1_MASK;
412
413                 JSStringRelease(string);
414             }
415         }
416     }
417
418     // handle location argument.
419     int location = DOM_KEY_LOCATION_STANDARD;
420     if (argumentCount > 2)
421         location = (int)JSValueToNumber(context, arguments[2], exception);
422
423     JSStringRef character = JSValueToStringCopy(context, arguments[0], exception);
424     g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
425     int gdkKeySym = GDK_VoidSymbol;
426     if (location == DOM_KEY_LOCATION_NUMPAD) {
427         if (JSStringIsEqualToUTF8CString(character, "leftArrow"))
428             gdkKeySym = GDK_KP_Left;
429         else if (JSStringIsEqualToUTF8CString(character, "rightArrow"))
430             gdkKeySym = GDK_KP_Right;
431         else if (JSStringIsEqualToUTF8CString(character, "upArrow"))
432             gdkKeySym = GDK_KP_Up;
433         else if (JSStringIsEqualToUTF8CString(character, "downArrow"))
434             gdkKeySym = GDK_KP_Down;
435         else if (JSStringIsEqualToUTF8CString(character, "pageUp"))
436             gdkKeySym = GDK_KP_Page_Up;
437         else if (JSStringIsEqualToUTF8CString(character, "pageDown"))
438             gdkKeySym = GDK_KP_Page_Down;
439         else if (JSStringIsEqualToUTF8CString(character, "home"))
440             gdkKeySym = GDK_KP_Home;
441         else if (JSStringIsEqualToUTF8CString(character, "end"))
442             gdkKeySym = GDK_KP_End;
443         else
444             // Assume we only get arrow/pgUp/pgDn/home/end keys with
445             // location=NUMPAD for now.
446             g_assert_not_reached();
447     } else {
448         if (JSStringIsEqualToUTF8CString(character, "leftArrow"))
449             gdkKeySym = GDK_Left;
450         else if (JSStringIsEqualToUTF8CString(character, "rightArrow"))
451             gdkKeySym = GDK_Right;
452         else if (JSStringIsEqualToUTF8CString(character, "upArrow"))
453             gdkKeySym = GDK_Up;
454         else if (JSStringIsEqualToUTF8CString(character, "downArrow"))
455             gdkKeySym = GDK_Down;
456         else if (JSStringIsEqualToUTF8CString(character, "pageUp"))
457             gdkKeySym = GDK_Page_Up;
458         else if (JSStringIsEqualToUTF8CString(character, "pageDown"))
459             gdkKeySym = GDK_Page_Down;
460         else if (JSStringIsEqualToUTF8CString(character, "home"))
461             gdkKeySym = GDK_Home;
462         else if (JSStringIsEqualToUTF8CString(character, "end"))
463             gdkKeySym = GDK_End;
464         else if (JSStringIsEqualToUTF8CString(character, "delete"))
465             gdkKeySym = GDK_Delete;
466         else if (JSStringIsEqualToUTF8CString(character, "F1"))
467             gdkKeySym = GDK_F1;
468         else if (JSStringIsEqualToUTF8CString(character, "F2"))
469             gdkKeySym = GDK_F2;
470         else if (JSStringIsEqualToUTF8CString(character, "F3"))
471             gdkKeySym = GDK_F3;
472         else if (JSStringIsEqualToUTF8CString(character, "F4"))
473             gdkKeySym = GDK_F4;
474         else if (JSStringIsEqualToUTF8CString(character, "F5"))
475             gdkKeySym = GDK_F5;
476         else if (JSStringIsEqualToUTF8CString(character, "F6"))
477             gdkKeySym = GDK_F6;
478         else if (JSStringIsEqualToUTF8CString(character, "F7"))
479             gdkKeySym = GDK_F7;
480         else if (JSStringIsEqualToUTF8CString(character, "F8"))
481             gdkKeySym = GDK_F8;
482         else if (JSStringIsEqualToUTF8CString(character, "F9"))
483             gdkKeySym = GDK_F9;
484         else if (JSStringIsEqualToUTF8CString(character, "F10"))
485             gdkKeySym = GDK_F10;
486         else if (JSStringIsEqualToUTF8CString(character, "F11"))
487             gdkKeySym = GDK_F11;
488         else if (JSStringIsEqualToUTF8CString(character, "F12"))
489             gdkKeySym = GDK_F12;
490         else {
491             int charCode = JSStringGetCharactersPtr(character)[0];
492             if (charCode == '\n' || charCode == '\r')
493                 gdkKeySym = GDK_Return;
494             else if (charCode == '\t')
495                 gdkKeySym = GDK_Tab;
496             else if (charCode == '\x8')
497                 gdkKeySym = GDK_BackSpace;
498             else {
499                 gdkKeySym = gdk_unicode_to_keyval(charCode);
500                 if (WTF::isASCIIUpper(charCode))
501                     state |= GDK_SHIFT_MASK;
502             }
503         }
504     }
505     JSStringRelease(character);
506
507     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
508     if (!view)
509         return JSValueMakeUndefined(context);
510
511     // create and send the event
512     GdkEvent event;
513     memset(&event, 0, sizeof(event));
514     event.key.keyval = gdkKeySym;
515     event.key.state = state;
516     event.key.window = GTK_WIDGET(view)->window;
517
518     // When synthesizing an event, an invalid hardware_keycode value
519     // can cause it to be badly processed by Gtk+.
520     GdkKeymapKey* keys;
521     gint n_keys;
522     if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(), gdkKeySym, &keys, &n_keys)) {
523         event.key.hardware_keycode = keys[0].keycode;
524         g_free(keys);
525     }
526
527     event.key.type = GDK_KEY_PRESS;
528     dispatchEvent(event);
529
530     event.key.type = GDK_KEY_RELEASE;
531     dispatchEvent(event);
532
533     return JSValueMakeUndefined(context);
534 }
535
536 static void zoomIn(gboolean fullContentsZoom)
537 {
538     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
539     if (!view)
540         return;
541
542     webkit_web_view_set_full_content_zoom(view, fullContentsZoom);
543     gfloat currentZoom = webkit_web_view_get_zoom_level(view);
544     webkit_web_view_set_zoom_level(view, currentZoom * zoomMultiplierRatio);
545 }
546
547 static void zoomOut(gboolean fullContentsZoom)
548 {
549     WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
550     if (!view)
551         return;
552
553     webkit_web_view_set_full_content_zoom(view, fullContentsZoom);
554     gfloat currentZoom = webkit_web_view_get_zoom_level(view);
555     webkit_web_view_set_zoom_level(view, currentZoom / zoomMultiplierRatio);
556 }
557
558 static JSValueRef textZoomInCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
559 {
560     zoomIn(FALSE);
561     return JSValueMakeUndefined(context);
562 }
563
564 static JSValueRef textZoomOutCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
565 {
566     zoomOut(FALSE);
567     return JSValueMakeUndefined(context);
568 }
569
570 static JSValueRef zoomPageInCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
571 {
572     zoomIn(TRUE);
573     return JSValueMakeUndefined(context);
574 }
575
576 static JSValueRef zoomPageOutCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
577 {
578     zoomOut(TRUE);
579     return JSValueMakeUndefined(context);
580 }
581
582 static JSStaticFunction staticFunctions[] = {
583     { "mouseWheelTo", mouseWheelToCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
584     { "contextClick", contextClickCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
585     { "mouseDown", mouseDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
586     { "mouseUp", mouseUpCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
587     { "mouseMoveTo", mouseMoveToCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
588     { "beginDragWithFiles", beginDragWithFilesCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
589     { "leapForward", leapForwardCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
590     { "keyDown", keyDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
591     { "textZoomIn", textZoomInCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
592     { "textZoomOut", textZoomOutCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
593     { "zoomPageIn", zoomPageInCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
594     { "zoomPageOut", zoomPageOutCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
595     { 0, 0, 0 }
596 };
597
598 static JSStaticValue staticValues[] = {
599     { "dragMode", getDragModeCallback, setDragModeCallback, kJSPropertyAttributeNone },
600     { 0, 0, 0, 0 }
601 };
602
603 static JSClassRef getClass(JSContextRef context)
604 {
605     static JSClassRef eventSenderClass = 0;
606
607     if (!eventSenderClass) {
608         JSClassDefinition classDefinition = {
609                 0, 0, 0, 0, 0, 0,
610                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
611         classDefinition.staticFunctions = staticFunctions;
612         classDefinition.staticValues = staticValues;
613
614         eventSenderClass = JSClassCreate(&classDefinition);
615     }
616
617     return eventSenderClass;
618 }
619
620 JSObjectRef makeEventSender(JSContextRef context, bool isTopFrame)
621 {
622     if (isTopFrame) {
623         dragMode = true;
624
625         // Fly forward in time one second when the main frame loads. This will
626         // ensure that when a test begins clicking in the same location as
627         // a previous test, those clicks won't be interpreted as continuations
628         // of the previous test's click sequences.
629         timeOffset += 1000;
630
631         lastMousePositionX = lastMousePositionY = 0;
632         lastClickPositionX = lastClickPositionY = 0;
633         lastClickTimeOffset = 0;
634         lastClickButton = 0;
635         buttonCurrentlyDown = 0;
636         clickCount = 0;
637
638         endOfQueue = 0;
639         startOfQueue = 0;
640     }
641
642     return JSObjectMake(context, getClass(context), 0);
643 }