| /* |
| * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. |
| * Copyright (C) 2009 Zan Dobersek <zandobersek@gmail.com> |
| * Copyright (C) 2009 Holger Hans Peter Freyther |
| * Copyright (C) 2010 Igalia S.L. |
| * Copyright (C) 2012 ChangSeok Oh <shivamidow@gmail.com> |
| * |
| * 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 "WebCoreSupport/DumpRenderTreeSupportGtk.h" |
| #include <GRefPtrGtk.h> |
| #include <GtkVersioning.h> |
| #include <JavaScriptCore/JSObjectRef.h> |
| #include <JavaScriptCore/JSRetainPtr.h> |
| #include <JavaScriptCore/JSStringRef.h> |
| #include <cstring> |
| #include <gdk/gdk.h> |
| #include <gdk/gdkkeysyms.h> |
| #include <webkit/webkitwebframe.h> |
| #include <webkit/webkitwebview.h> |
| #include <wtf/ASCIICType.h> |
| #include <wtf/Platform.h> |
| #include <wtf/gobject/GOwnPtr.h> |
| #include <wtf/gobject/GUniquePtr.h> |
| #include <wtf/text/CString.h> |
| |
| extern "C" { |
| extern GtkMenu* webkit_web_view_get_context_menu(WebKitWebView*); |
| } |
| |
| 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 unsigned buttonCurrentlyDown; |
| static int clickCount; |
| GdkDragContext* currentDragSourceContext; |
| |
| struct DelayedMessage { |
| GdkEvent* event; |
| gulong delay; |
| }; |
| |
| static DelayedMessage msgQueue[1024]; |
| |
| static unsigned endOfQueue; |
| static unsigned startOfQueue; |
| |
| static const float zoomMultiplierRatio = 1.2f; |
| |
| // WebCore and layout tests assume this value. |
| static const float pixelsPerScrollTick = 40; |
| |
| // 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(); |
| |
| 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, guint modifiers) |
| { |
| 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; |
| |
| event->button.button = gdkButtonNumber; |
| event->button.x = lastMousePositionX; |
| event->button.y = lastMousePositionY; |
| event->button.window = gtk_widget_get_window(GTK_WIDGET(view)); |
| g_object_ref(event->button.window); |
| event->button.device = getDefaultGDKPointerDevice(event->button.window); |
| event->button.state = modifiers | getStateFlags(); |
| event->button.time = GDK_CURRENT_TIME; |
| event->button.axes = 0; |
| |
| 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 getMenuItemTitleCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception) |
| { |
| GtkWidget* widget = GTK_WIDGET(JSObjectGetPrivate(object)); |
| CString label; |
| if (GTK_IS_SEPARATOR_MENU_ITEM(widget)) |
| label = "<separator>"; |
| else |
| label = gtk_menu_item_get_label(GTK_MENU_ITEM(widget)); |
| |
| JSRetainPtr<JSStringRef> itemText(Adopt, JSStringCreateWithUTF8CString(label.data())); |
| return JSValueMakeString(context, itemText.get()); |
| } |
| |
| static bool setMenuItemTitleCallback(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception) |
| { |
| return true; |
| } |
| |
| static JSValueRef menuItemClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| GtkMenuItem* item = GTK_MENU_ITEM(JSObjectGetPrivate(thisObject)); |
| gtk_menu_item_activate(item); |
| return JSValueMakeUndefined(context); |
| } |
| |
| static JSStaticFunction staticMenuItemFunctions[] = { |
| { "click", menuItemClickCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, |
| { 0, 0, 0 } |
| }; |
| |
| static JSStaticValue staticMenuItemValues[] = { |
| { "title", getMenuItemTitleCallback, setMenuItemTitleCallback, kJSPropertyAttributeNone }, |
| { 0, 0, 0, 0 } |
| }; |
| |
| static JSClassRef getMenuItemClass() |
| { |
| static JSClassRef menuItemClass = 0; |
| |
| if (!menuItemClass) { |
| JSClassDefinition classDefinition = { |
| 0, 0, 0, 0, 0, 0, |
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; |
| classDefinition.staticFunctions = staticMenuItemFunctions; |
| classDefinition.staticValues = staticMenuItemValues; |
| |
| menuItemClass = JSClassCreate(&classDefinition); |
| } |
| |
| return menuItemClass; |
| } |
| |
| |
| static JSValueRef contextClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| GdkEvent* pressEvent = gdk_event_new(GDK_BUTTON_PRESS); |
| |
| if (!prepareMouseButtonEvent(pressEvent, 2, 0)) { |
| gdk_event_free(pressEvent); |
| return JSObjectMakeArray(context, 0, 0, 0); |
| } |
| |
| GdkEvent* releaseEvent = gdk_event_copy(pressEvent); |
| sendOrQueueEvent(pressEvent); |
| |
| JSValueRef valueRef = JSObjectMakeArray(context, 0, 0, 0); |
| WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame); |
| GtkMenu* gtkMenu = webkit_web_view_get_context_menu(view); |
| if (gtkMenu) { |
| GUniquePtr<GList> items(gtk_container_get_children(GTK_CONTAINER(gtkMenu))); |
| JSValueRef arrayValues[g_list_length(items.get())]; |
| int index = 0; |
| for (GList* item = g_list_first(items.get()); item; item = g_list_next(item)) { |
| arrayValues[index] = JSObjectMake(context, getMenuItemClass(), item->data); |
| index++; |
| } |
| if (index) |
| valueRef = JSObjectMakeArray(context, index - 1, arrayValues, 0); |
| } |
| |
| releaseEvent->type = GDK_BUTTON_RELEASE; |
| sendOrQueueEvent(releaseEvent); |
| return valueRef; |
| } |
| |
| static gboolean sendClick(gpointer) |
| { |
| GdkEvent* pressEvent = gdk_event_new(GDK_BUTTON_PRESS); |
| |
| if (!prepareMouseButtonEvent(pressEvent, 1, 0)) { |
| gdk_event_free(pressEvent); |
| return FALSE; |
| } |
| |
| GdkEvent* releaseEvent = gdk_event_copy(pressEvent); |
| dispatchEvent(pressEvent); |
| releaseEvent->type = GDK_BUTTON_RELEASE; |
| dispatchEvent(releaseEvent); |
| |
| return FALSE; |
| } |
| |
| static JSValueRef scheduleAsynchronousClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| g_idle_add_full(G_PRIORITY_DEFAULT, sendClick, 0, 0); |
| return JSValueMakeUndefined(context); |
| } |
| |
| static void updateClickCount(int button) |
| { |
| if (lastClickPositionX != lastMousePositionX |
| || lastClickPositionY != lastMousePositionY |
| || lastClickButton != button |
| || timeOffset - lastClickTimeOffset >= 1) |
| clickCount = 1; |
| else |
| clickCount++; |
| } |
| |
| static guint gdkModifierFromJSValue(JSContextRef context, const JSValueRef value) |
| { |
| JSStringRef string = JSValueToStringCopy(context, value, 0); |
| guint gdkModifier = 0; |
| if (JSStringIsEqualToUTF8CString(string, "ctrlKey") |
| || JSStringIsEqualToUTF8CString(string, "addSelectionKey")) |
| gdkModifier = GDK_CONTROL_MASK; |
| else if (JSStringIsEqualToUTF8CString(string, "shiftKey") |
| || JSStringIsEqualToUTF8CString(string, "rangeSelectionKey")) |
| gdkModifier = GDK_SHIFT_MASK; |
| else if (JSStringIsEqualToUTF8CString(string, "altKey")) |
| gdkModifier = GDK_MOD1_MASK; |
| |
| // Currently the metaKey as defined in WebCore/platform/gtk/PlatformMouseEventGtk.cpp |
| // is GDK_META_MASK. This code must be kept in sync with that file. |
| else if (JSStringIsEqualToUTF8CString(string, "metaKey")) |
| gdkModifier = GDK_META_MASK; |
| |
| JSStringRelease(string); |
| return gdkModifier; |
| } |
| |
| static guint gdkModifersFromJSValue(JSContextRef context, const JSValueRef modifiers) |
| { |
| // The value may either be a string with a single modifier or an array of modifiers. |
| if (JSValueIsString(context, modifiers)) |
| return gdkModifierFromJSValue(context, modifiers); |
| |
| JSObjectRef modifiersArray = JSValueToObject(context, modifiers, 0); |
| if (!modifiersArray) |
| return 0; |
| |
| guint gdkModifiers = 0; |
| JSRetainPtr<JSStringRef> lengthProperty(Adopt, JSStringCreateWithUTF8CString("length")); |
| int modifiersCount = JSValueToNumber(context, JSObjectGetProperty(context, modifiersArray, lengthProperty.get(), 0), 0); |
| for (int i = 0; i < modifiersCount; ++i) |
| gdkModifiers |= gdkModifierFromJSValue(context, JSObjectGetPropertyAtIndex(context, modifiersArray, i, 0)); |
| return gdkModifiers; |
| } |
| |
| 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)); |
| } |
| guint modifiers = argumentCount >= 2 ? gdkModifersFromJSValue(context, arguments[1]) : 0; |
| |
| GdkEvent* event = gdk_event_new(GDK_BUTTON_PRESS); |
| if (!prepareMouseButtonEvent(event, button, modifiers)) { |
| gdk_event_free(event); |
| return JSValueMakeUndefined(context); |
| } |
| |
| // If the same mouse button is already in the down position don't send another event as it may confuse Xvfb. |
| if (buttonCurrentlyDown == event->button.button) { |
| gdk_event_free(event); |
| 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; |
| |
| 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)); |
| } |
| guint modifiers = argumentCount >= 2 ? gdkModifersFromJSValue(context, arguments[1]) : 0; |
| |
| GdkEvent* event = gdk_event_new(GDK_BUTTON_RELEASE); |
| if (!prepareMouseButtonEvent(event, button, modifiers)) { |
| gdk_event_free(event); |
| return JSValueMakeUndefined(context); |
| } |
| |
| lastClickPositionX = lastMousePositionX; |
| lastClickPositionY = lastMousePositionY; |
| lastClickButton = buttonCurrentlyDown; |
| lastClickTimeOffset = timeOffset; |
| buttonCurrentlyDown = 0; |
| |
| 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 = gdk_event_new(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)); |
| g_object_ref(event->motion.window); |
| event->button.device = getDefaultGDKPointerDevice(event->motion.window); |
| |
| guint modifiers = argumentCount >= 3 ? gdkModifersFromJSValue(context, arguments[2]) : 0; |
| event->motion.state = modifiers | getStateFlags(); |
| event->motion.axes = 0; |
| |
| 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 mouseScrollByCallback(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)); |
| |
| // Copy behaviour of Qt and EFL - just return in case of (0,0) mouse scroll |
| if (!horizontal && !vertical) |
| return JSValueMakeUndefined(context); |
| |
| GdkEvent* event = gdk_event_new(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)); |
| g_object_ref(event->scroll.window); |
| |
| // GTK+ only supports one tick in each scroll event that is not smooth. For the cases of more than one direction, |
| // and more than one step in a direction, we can only use smooth events, supported from Gtk 3.3.18. |
| #if GTK_CHECK_VERSION(3, 3, 18) |
| if ((horizontal && vertical) || horizontal > 1 || horizontal < -1 || vertical > 1 || vertical < -1) { |
| event->scroll.direction = GDK_SCROLL_SMOOTH; |
| event->scroll.delta_x = -horizontal; |
| event->scroll.delta_y = -vertical; |
| |
| sendOrQueueEvent(event); |
| return JSValueMakeUndefined(context); |
| } |
| #else |
| g_return_val_if_fail((!vertical || !horizontal), JSValueMakeUndefined(context)); |
| #endif |
| |
| if (horizontal < 0) |
| event->scroll.direction = GDK_SCROLL_RIGHT; |
| else if (horizontal > 0) |
| event->scroll.direction = GDK_SCROLL_LEFT; |
| else if (vertical < 0) |
| event->scroll.direction = GDK_SCROLL_DOWN; |
| else if (vertical > 0) |
| event->scroll.direction = GDK_SCROLL_UP; |
| else |
| g_assert_not_reached(); |
| |
| sendOrQueueEvent(event); |
| return JSValueMakeUndefined(context); |
| } |
| |
| static JSValueRef continuousMouseScrollByCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| #if GTK_CHECK_VERSION(3, 3, 18) |
| WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame); |
| if (!view) |
| return JSValueMakeUndefined(context); |
| |
| if (argumentCount < 2) |
| return JSValueMakeUndefined(context); |
| |
| int horizontal = JSValueToNumber(context, arguments[0], exception); |
| g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context)); |
| int vertical = JSValueToNumber(context, arguments[1], exception); |
| g_return_val_if_fail((!exception || !*exception), JSValueMakeUndefined(context)); |
| |
| // We do not yet support continuous scrolling by page. |
| if (argumentCount >= 3 && JSValueToBoolean(context, arguments[2])) |
| return JSValueMakeUndefined(context); |
| |
| GdkEvent* event = gdk_event_new(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)); |
| g_object_ref(event->scroll.window); |
| |
| event->scroll.direction = GDK_SCROLL_SMOOTH; |
| event->scroll.delta_x = -horizontal / pixelsPerScrollTick; |
| event->scroll.delta_y = -vertical / pixelsPerScrollTick; |
| |
| sendOrQueueEvent(event); |
| #endif |
| return JSValueMakeUndefined(context); |
| } |
| |
| static void dragWithFilesDragDataGetCallback(GtkWidget*, GdkDragContext*, GtkSelectionData *data, guint, guint, gpointer userData) |
| { |
| gtk_selection_data_set_uris(data, static_cast<gchar**>(userData)); |
| } |
| |
| static void dragWithFilesDragEndCallback(GtkWidget* widget, GdkDragContext*, gpointer userData) |
| { |
| g_signal_handlers_disconnect_by_func(widget, reinterpret_cast<void*>(dragWithFilesDragEndCallback), userData); |
| g_signal_handlers_disconnect_by_func(widget, reinterpret_cast<void*>(dragWithFilesDragDataGetCallback), userData); |
| g_strfreev(static_cast<gchar**>(userData)); |
| } |
| |
| static JSValueRef beginDragWithFilesCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| if (argumentCount < 1) |
| return JSValueMakeUndefined(context); |
| |
| JSObjectRef filesArray = JSValueToObject(context, arguments[0], exception); |
| ASSERT(!exception || !*exception); |
| |
| const gchar* mainFrameURI = webkit_web_frame_get_uri(mainFrame); |
| GRefPtr<GFile> testFile(adoptGRef(g_file_new_for_uri(mainFrameURI))); |
| GRefPtr<GFile> parentDirectory(g_file_get_parent(testFile.get())); |
| if (!parentDirectory) |
| return JSValueMakeUndefined(context); |
| |
| // If this is an HTTP test, we still need to pass a local file path |
| // to WebCore. Even though the file doesn't exist, this should be fine |
| // for most tests. |
| GUniquePtr<gchar> scheme(g_file_get_uri_scheme(parentDirectory.get())); |
| if (g_str_equal(scheme.get(), "http") || g_str_equal(scheme.get(), "https")) { |
| GUniquePtr<gchar> currentDirectory(g_get_current_dir()); |
| parentDirectory = adoptGRef(g_file_new_for_path(currentDirectory.get())); |
| } |
| |
| JSStringRef lengthProperty = JSStringCreateWithUTF8CString("length"); |
| int filesArrayLength = JSValueToNumber(context, JSObjectGetProperty(context, filesArray, lengthProperty, 0), 0); |
| JSStringRelease(lengthProperty); |
| |
| gchar** draggedFilesURIList = g_new0(gchar*, filesArrayLength + 1); |
| for (int i = 0; i < filesArrayLength; ++i) { |
| JSStringRef filenameString = JSValueToStringCopy(context, |
| JSObjectGetPropertyAtIndex(context, filesArray, i, 0), 0); |
| size_t bufferSize = JSStringGetMaximumUTF8CStringSize(filenameString); |
| GUniquePtr<gchar> filenameBuffer(static_cast<gchar*>(g_malloc(bufferSize))); |
| JSStringGetUTF8CString(filenameString, filenameBuffer.get(), bufferSize); |
| JSStringRelease(filenameString); |
| |
| GRefPtr<GFile> dragFile(g_file_get_child(parentDirectory.get(), filenameBuffer.get())); |
| draggedFilesURIList[i] = g_file_get_uri(dragFile.get()); |
| } |
| |
| GtkWidget* view = GTK_WIDGET(webkit_web_frame_get_web_view(mainFrame)); |
| g_object_connect(G_OBJECT(view), |
| "signal::drag-end", dragWithFilesDragEndCallback, draggedFilesURIList, |
| "signal::drag-data-get", dragWithFilesDragDataGetCallback, draggedFilesURIList, |
| NULL); |
| |
| GdkEvent event; |
| GdkWindow* viewGDKWindow = gtk_widget_get_window(view); |
| 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 = viewGDKWindow; |
| event.motion.device = getDefaultGDKPointerDevice(viewGDKWindow); |
| event.motion.state = GDK_BUTTON1_MASK; |
| |
| int xRoot, yRoot; |
| gdk_window_get_root_coords(viewGDKWindow, lastMousePositionX, lastMousePositionY, &xRoot, &yRoot); |
| event.motion.x_root = xRoot; |
| event.motion.y_root = yRoot; |
| |
| GtkTargetList* targetList = gtk_target_list_new(0, 0); |
| gtk_target_list_add_uri_targets(targetList, 0); |
| gtk_drag_begin(view, targetList, GDK_ACTION_COPY, 1, &event); |
| gtk_target_list_unref(targetList); |
| |
| 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) |
| { |
| DumpRenderTreeSupportGtk::layoutFrame(mainFrame); |
| WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame); |
| if (!view) { |
| gdk_event_free(event); |
| return; |
| } |
| |
| // The widget focus may have been lost in the course of the test, |
| // so force another explicit focus grab here. |
| gtk_widget_grab_focus(GTK_WIDGET(view)); |
| gtk_main_do_event(event); |
| |
| if (!currentDragSourceContext) { |
| gdk_event_free(event); |
| return; |
| } |
| |
| if (event->type == GDK_MOTION_NOTIFY) { |
| // WebKit has called gtk_drag_start(), but because the main loop isn't |
| // running GDK internals don't know that the drag has started yet. Pump |
| // the main loop a little bit so that GDK is in the correct state. |
| while (gtk_events_pending()) |
| gtk_main_iteration(); |
| |
| // Simulate a drag motion on the top-level GDK window. |
| GtkWidget* parentWidget = gtk_widget_get_parent(GTK_WIDGET(view)); |
| GdkWindow* parentWidgetWindow = gtk_widget_get_window(parentWidget); |
| gdk_drag_motion(currentDragSourceContext, parentWidgetWindow, GDK_DRAG_PROTO_XDND, |
| event->motion.x_root, event->motion.y_root, |
| gdk_drag_context_get_selected_action(currentDragSourceContext), |
| gdk_drag_context_get_actions(currentDragSourceContext), |
| GDK_CURRENT_TIME); |
| |
| } else if (currentDragSourceContext && event->type == GDK_BUTTON_RELEASE) { |
| // We've released the mouse button, we should just be able to spin the |
| // event loop here and have GTK+ send the appropriate notifications for |
| // the end of the drag. |
| while (gtk_events_pending()) |
| gtk_main_iteration(); |
| } |
| |
| gdk_event_free(event); |
| } |
| |
| void replaySavedEvents() |
| { |
| // 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 GdkEvent* createKeyPressEvent(JSContextRef context, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| g_return_val_if_fail(argumentCount >= 1, 0); |
| guint modifiers = argumentCount >= 2 ? gdkModifersFromJSValue(context, arguments[1]) : 0; |
| |
| // 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), 0); |
| |
| 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 if (JSStringIsEqualToUTF8CString(character, "insert")) |
| gdkKeySym = GDK_KP_Insert; |
| else if (JSStringIsEqualToUTF8CString(character, "delete")) |
| gdkKeySym = GDK_KP_Delete; |
| else |
| // If we get some other key specified with the numpad location, |
| // crash here, so we add it sooner rather than later. |
| 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, "insert")) |
| gdkKeySym = GDK_Insert; |
| else if (JSStringIsEqualToUTF8CString(character, "delete")) |
| gdkKeySym = GDK_Delete; |
| else if (JSStringIsEqualToUTF8CString(character, "printScreen")) |
| gdkKeySym = GDK_Print; |
| else if (JSStringIsEqualToUTF8CString(character, "menu")) |
| gdkKeySym = GDK_Menu; |
| 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 if (JSStringIsEqualToUTF8CString(character, "leftAlt")) |
| gdkKeySym = GDK_Alt_L; |
| else if (JSStringIsEqualToUTF8CString(character, "leftControl")) |
| gdkKeySym = GDK_Control_L; |
| else if (JSStringIsEqualToUTF8CString(character, "leftShift")) |
| gdkKeySym = GDK_Shift_L; |
| else if (JSStringIsEqualToUTF8CString(character, "rightAlt")) |
| gdkKeySym = GDK_Alt_R; |
| else if (JSStringIsEqualToUTF8CString(character, "rightControl")) |
| gdkKeySym = GDK_Control_R; |
| else if (JSStringIsEqualToUTF8CString(character, "rightShift")) |
| gdkKeySym = GDK_Shift_R; |
| 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)) |
| modifiers |= GDK_SHIFT_MASK; |
| } |
| } |
| } |
| JSStringRelease(character); |
| |
| WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame); |
| g_return_val_if_fail(view, 0); |
| |
| GdkEvent* pressEvent = gdk_event_new(GDK_KEY_PRESS); |
| pressEvent->key.keyval = gdkKeySym; |
| pressEvent->key.state = modifiers; |
| pressEvent->key.window = gtk_widget_get_window(GTK_WIDGET(view)); |
| g_object_ref(pressEvent->key.window); |
| #ifndef GTK_API_VERSION_2 |
| gdk_event_set_device(pressEvent, getDefaultGDKPointerDevice(pressEvent->key.window)); |
| #endif |
| |
| // When synthesizing an event, an invalid hardware_keycode value |
| // can cause it to be badly processed by Gtk+. |
| GOwnPtr<GdkKeymapKey> keys; |
| gint nKeys; |
| if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(), gdkKeySym, &keys.outPtr(), &nKeys)) |
| pressEvent->key.hardware_keycode = keys.get()[0].keycode; |
| |
| return pressEvent; |
| } |
| |
| static void sendKeyDown(GdkEvent* pressEvent) |
| { |
| g_return_if_fail(pressEvent); |
| GdkEvent* releaseEvent = gdk_event_copy(pressEvent); |
| releaseEvent->type = GDK_KEY_RELEASE; |
| |
| dispatchEvent(pressEvent); |
| dispatchEvent(releaseEvent); |
| |
| DumpRenderTreeSupportGtk::deliverAllMutationsIfNecessary(); |
| } |
| |
| static JSValueRef keyDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| GdkEvent* pressEvent = createKeyPressEvent(context, argumentCount, arguments, exception); |
| sendKeyDown(pressEvent); |
| |
| 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 JSValueRef scalePageByCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| if (argumentCount < 3) |
| return JSValueMakeUndefined(context); |
| |
| float scaleFactor = JSValueToNumber(context, arguments[0], exception); |
| float x = JSValueToNumber(context, arguments[1], exception); |
| float y = JSValueToNumber(context, arguments[2], exception); |
| |
| WebKitWebView* view = webkit_web_frame_get_web_view(mainFrame); |
| if (!view) |
| return JSValueMakeUndefined(context); |
| |
| DumpRenderTreeSupportGtk::scalePageBy(view, scaleFactor, x, y); |
| |
| return JSValueMakeUndefined(context); |
| } |
| |
| static gboolean sendAsynchronousKeyDown(gpointer userData) |
| { |
| sendKeyDown(static_cast<GdkEvent*>(userData)); |
| return FALSE; |
| } |
| |
| static JSValueRef scheduleAsynchronousKeyDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| GdkEvent* pressEvent = createKeyPressEvent(context, argumentCount, arguments, exception); |
| if (pressEvent) |
| g_idle_add_full(G_PRIORITY_DEFAULT, sendAsynchronousKeyDown, static_cast<gpointer>(pressEvent), 0); |
| |
| return JSValueMakeUndefined(context); |
| } |
| |
| static JSValueRef clearTouchPointsCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) |
| { |
| return JSValueMakeUndefined(context); |
| } |
| |
| static JSStaticFunction staticFunctions[] = { |
| { "mouseScrollBy", mouseScrollByCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, |
| { "continuousMouseScrollBy", continuousMouseScrollByCallback, 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 }, |
| { "scheduleAsynchronousClick", scheduleAsynchronousClickCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, |
| { "scalePageBy", scalePageByCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, |
| { "scheduleAsynchronousKeyDown", scheduleAsynchronousKeyDownCallback, 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; |
| |
| currentDragSourceContext = 0; |
| } |
| |
| return JSObjectMake(context, getClass(context), 0); |
| } |
| |
| void dragBeginCallback(GtkWidget*, GdkDragContext* context, gpointer) |
| { |
| currentDragSourceContext = context; |
| } |
| |
| void dragEndCallback(GtkWidget*, GdkDragContext* context, gpointer) |
| { |
| currentDragSourceContext = 0; |
| } |
| |
| gboolean dragFailedCallback(GtkWidget*, GdkDragContext* context, gpointer) |
| { |
| // Return TRUE here to disable the stupid GTK+ drag failed animation, |
| // which introduces asynchronous behavior into our drags. |
| return TRUE; |
| } |