blob: f42928c1d9ae79afc7a5326be220f56dd6b3dddc [file] [log] [blame]
/*
* 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>
// TODO: Currently drag and drop related code is left out and
// should be merged once we have drag and drop support in WebCore.
extern "C" {
extern void webkit_web_frame_layout(WebKitWebFrame* frame);
}
static bool down = false;
static bool currentEventButton = 1;
static bool dragMode = true;
static bool replayingSavedEvents = false;
static int lastMousePositionX;
static int lastMousePositionY;
static int lastClickPositionX;
static int lastClickPositionY;
static int clickCount = 0;
struct DelayedMessage {
GdkEvent event;
gulong delay;
gboolean isDragEvent;
};
static DelayedMessage msgQueue[1024];
static unsigned endOfQueue;
static unsigned startOfQueue;
static const float zoomMultiplierRatio = 1.2f;
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)
{
// FIXME: Add proper support for forward leaps
return JSValueMakeUndefined(context);
}
static JSValueRef contextClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
{
webkit_web_frame_layout(mainFrame);
WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
if (!view)
return JSValueMakeUndefined(context);
GdkEvent event;
memset(&event, 0, sizeof(event));
event.button.button = 3;
event.button.x = lastMousePositionX;
event.button.y = lastMousePositionY;
event.button.window = GTK_WIDGET(view)->window;
gboolean return_val;
down = true;
event.type = GDK_BUTTON_PRESS;
g_signal_emit_by_name(view, "button_press_event", &event, &return_val);
down = false;
event.type = GDK_BUTTON_RELEASE;
g_signal_emit_by_name(view, "button_release_event", &event, &return_val);
return JSValueMakeUndefined(context);
}
static void updateClickCount(int /* button */)
{
// FIXME: take the last clicked button number and the time of last click into account.
if (lastClickPositionX != lastMousePositionX || lastClickPositionY != lastMousePositionY)
clickCount = 1;
else
clickCount++;
}
#if !GTK_CHECK_VERSION(2,17,3)
static void getRootCoords(GtkWidget* view, int* rootX, int* rootY)
{
GtkWidget* window = gtk_widget_get_toplevel(GTK_WIDGET(view));
int tmpX, tmpY;
gtk_widget_translate_coordinates(view, window, lastMousePositionX, lastMousePositionY, &tmpX, &tmpY);
gdk_window_get_origin(window->window, rootX, rootY);
*rootX += tmpX;
*rootY += tmpY;
}
#endif
static JSValueRef mouseDownCallback(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);
down = true;
GdkEvent event;
memset(&event, 0, sizeof(event));
event.type = GDK_BUTTON_PRESS;
event.button.button = 1;
if (argumentCount == 1) {
event.button.button = (int)JSValueToNumber(context, arguments[0], exception) + 1;
g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
}
currentEventButton = event.button.button;
event.button.x = lastMousePositionX;
event.button.y = lastMousePositionY;
event.button.window = GTK_WIDGET(view)->window;
event.button.time = GDK_CURRENT_TIME;
event.button.device = gdk_device_get_core_pointer();
int x_root, y_root;
#if GTK_CHECK_VERSION(2,17,3)
gdk_window_get_root_coords(GTK_WIDGET(view)->window, lastMousePositionX, lastMousePositionY, &x_root, &y_root);
#else
getRootCoords(GTK_WIDGET(view), &x_root, &y_root);
#endif
event.button.x_root = x_root;
event.button.y_root = y_root;
updateClickCount(1);
if (!msgQueue[endOfQueue].delay) {
webkit_web_frame_layout(mainFrame);
gboolean return_val;
g_signal_emit_by_name(view, "button_press_event", &event, &return_val);
if (clickCount == 2) {
event.type = GDK_2BUTTON_PRESS;
g_signal_emit_by_name(view, "button_press_event", &event, &return_val);
}
} else {
// replaySavedEvents should have the required logic to make leapForward delays work
msgQueue[endOfQueue++].event = event;
replaySavedEvents();
}
return JSValueMakeUndefined(context);
}
static JSValueRef mouseUpCallback(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);
down = false;
GdkEvent event;
memset(&event, 0, sizeof(event));
event.type = GDK_BUTTON_RELEASE;
event.button.button = 1;
if (argumentCount == 1) {
event.button.button = (int)JSValueToNumber(context, arguments[0], exception) + 1;
g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
}
currentEventButton = event.button.button;
event.button.x = lastMousePositionX;
event.button.y = lastMousePositionY;
event.button.window = GTK_WIDGET(view)->window;
event.button.time = GDK_CURRENT_TIME;
event.button.device = gdk_device_get_core_pointer();
int x_root, y_root;
#if GTK_CHECK_VERSION(2,17,3)
gdk_window_get_root_coords(GTK_WIDGET(view)->window, lastMousePositionX, lastMousePositionY, &x_root, &y_root);
#else
getRootCoords(GTK_WIDGET(view), &x_root, &y_root);
#endif
event.button.x_root = x_root;
event.button.y_root = y_root;
if ((dragMode && !replayingSavedEvents) || msgQueue[endOfQueue].delay) {
msgQueue[endOfQueue].event = event;
msgQueue[endOfQueue++].isDragEvent = true;
replaySavedEvents();
} else {
webkit_web_frame_layout(mainFrame);
gboolean return_val;
g_signal_emit_by_name(view, "button_release_event", &event, &return_val);
}
lastClickPositionX = lastMousePositionX;
lastClickPositionY = lastMousePositionY;
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(view)->window;
event.motion.device = gdk_device_get_core_pointer();
int x_root, y_root;
#if GTK_CHECK_VERSION(2,17,3)
gdk_window_get_root_coords(GTK_WIDGET(view)->window, lastMousePositionX, lastMousePositionY, &x_root, &y_root);
#else
getRootCoords(GTK_WIDGET(view), &x_root, &y_root);
#endif
event.motion.x_root = x_root;
event.motion.y_root = y_root;
if (down) {
if (currentEventButton == 1)
event.motion.state = GDK_BUTTON1_MASK;
else if (currentEventButton == 2)
event.motion.state = GDK_BUTTON2_MASK;
else if (currentEventButton == 3)
event.motion.state = GDK_BUTTON3_MASK;
} else
event.motion.state = 0;
if (dragMode && down && !replayingSavedEvents) {
msgQueue[endOfQueue].event = event;
msgQueue[endOfQueue++].isDragEvent = true;
} else {
webkit_web_frame_layout(mainFrame);
gboolean return_val;
g_signal_emit_by_name(view, "motion_notify_event", &event, &return_val);
}
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(view)->window;
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();
if (dragMode && down && !replayingSavedEvents) {
msgQueue[endOfQueue].event = event;
msgQueue[endOfQueue++].isDragEvent = true;
} else {
webkit_web_frame_layout(mainFrame);
gtk_main_do_event(&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);
}
void replaySavedEvents()
{
// FIXME: This doesn't deal with forward leaps, but it should.
WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame);
if (!view)
return;
replayingSavedEvents = true;
for (unsigned queuePos = 0; queuePos < endOfQueue; queuePos++) {
GdkEvent event = msgQueue[queuePos].event;
gboolean return_val;
switch (event.type) {
case GDK_BUTTON_RELEASE:
g_signal_emit_by_name(view, "button_release_event", &event, &return_val);
break;
case GDK_BUTTON_PRESS:
g_signal_emit_by_name(view, "button_press_event", &event, &return_val);
break;
case GDK_MOTION_NOTIFY:
g_signal_emit_by_name(view, "motion_notify_event", &event, &return_val);
break;
default:
continue;
}
startOfQueue++;
}
int numQueuedMessages = endOfQueue - startOfQueue;
if (!numQueuedMessages) {
startOfQueue = 0;
endOfQueue = 0;
replayingSavedEvents = false;
return;
}
startOfQueue = 0;
endOfQueue = 0;
replayingSavedEvents = false;
}
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);
}
}
}
JSStringRef character = JSValueToStringCopy(context, arguments[0], exception);
g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context));
int gdkKeySym;
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_BackSpace;
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(view)->window;
gboolean return_val;
event.key.type = GDK_KEY_PRESS;
g_signal_emit_by_name(view, "key-press-event", &event.key, &return_val);
event.key.type = GDK_KEY_RELEASE;
g_signal_emit_by_name(view, "key-release-event", &event.key, &return_val);
return JSValueMakeUndefined(context);
}
static JSValueRef textZoomInCallback(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);
gfloat currentZoom = webkit_web_view_get_zoom_level(view);
webkit_web_view_set_zoom_level(view, currentZoom * zoomMultiplierRatio);
return JSValueMakeUndefined(context);
}
static JSValueRef textZoomOutCallback(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);
gfloat currentZoom = webkit_web_view_get_zoom_level(view);
webkit_web_view_set_zoom_level(view, currentZoom / zoomMultiplierRatio);
return JSValueMakeUndefined(context);
}
static JSValueRef zoomPageInCallback(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);
webkit_web_view_zoom_in(view);
return JSValueMakeUndefined(context);
}
static JSValueRef zoomPageOutCallback(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);
webkit_web_view_zoom_out(view);
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)
{
down = false;
dragMode = true;
lastMousePositionX = lastMousePositionY = 0;
lastClickPositionX = lastClickPositionY = 0;
if (!replayingSavedEvents) {
// This function can be called in the middle of a test, even
// while replaying saved events. Resetting these while doing that
// can break things.
endOfQueue = 0;
startOfQueue = 0;
}
return JSObjectMake(context, getClass(context), 0);
}