/* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2009 Zan Dobersek * Copyright (C) 2009 Holger Hans Peter Freyther * Copyright (C) 2010 Igalia S.L. * Copyright (C) 2011 ProFUSION Embedded Systems * Copyright (C) 2011, 2012 Samsung Electronics * * 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 "DumpRenderTreeChrome.h" #include "JSStringUtils.h" #include "NotImplemented.h" #include "WebCoreSupport/DumpRenderTreeSupportEfl.h" #include "ewk_private.h" #include #include #include #include #include #include #include #include static bool gDragMode; static int gTimeOffset = 0; static int gLastMousePositionX; static int gLastMousePositionY; static int gLastClickPositionX; static int gLastClickPositionY; static int gLastClickTimeOffset; static int gLastClickButton; static int gButtonCurrentlyDown; static int gClickCount; static const float zoomMultiplierRatio = 1.2f; // Key event location code defined in DOM Level 3. enum KeyLocationCode { DomKeyLocationStandard, DomKeyLocationLeft, DomKeyLocationRight, DomKeyLocationNumpad }; enum EvasKeyModifier { EvasKeyModifierNone = 0, EvasKeyModifierControl = 1 << 0, EvasKeyModifierShift = 1 << 1, EvasKeyModifierAlt = 1 << 2, EvasKeyModifierMeta = 1 << 3 }; enum EvasMouseButton { EvasMouseButtonNone, EvasMouseButtonLeft, EvasMouseButtonMiddle, EvasMouseButtonRight }; enum EvasMouseEvent { EvasMouseEventNone = 0, EvasMouseEventDown = 1 << 0, EvasMouseEventUp = 1 << 1, EvasMouseEventMove = 1 << 2, EvasMouseEventScrollUp = 1 << 3, EvasMouseEventScrollDown = 1 << 4, EvasMouseEventScrollLeft = 1 << 5, EvasMouseEventScrollRight = 1 << 6, EvasMouseEventClick = EvasMouseEventMove | EvasMouseEventDown | EvasMouseEventUp, }; enum ZoomEvent { ZoomIn, ZoomOut }; struct KeyEventInfo { KeyEventInfo(const CString& keyName, EvasKeyModifier modifiers) : keyName(keyName) , modifiers(modifiers) { } const CString keyName; EvasKeyModifier modifiers; }; static void setEvasModifiers(Evas* evas, EvasKeyModifier modifiers) { static const char* modifierNames[] = { "Control", "Shift", "Alt", "Super" }; for (unsigned modifier = 0; modifier < 4; ++modifier) { if (modifiers & (1 << modifier)) evas_key_modifier_on(evas, modifierNames[modifier]); else evas_key_modifier_off(evas, modifierNames[modifier]); } } static EvasMouseButton translateMouseButtonNumber(int eventSenderButtonNumber) { static const EvasMouseButton translationTable[] = { EvasMouseButtonLeft, EvasMouseButtonMiddle, EvasMouseButtonRight, EvasMouseButtonMiddle // fast/events/mouse-click-events expects the 4th button to be treated as the middle button }; static const unsigned translationTableSize = sizeof(translationTable) / sizeof(translationTable[0]); if (eventSenderButtonNumber < translationTableSize) return translationTable[eventSenderButtonNumber]; return EvasMouseButtonLeft; } static void sendMouseEvent(Evas* evas, EvasMouseEvent event, int buttonNumber, EvasKeyModifier modifiers) { unsigned timeStamp = 0; DumpRenderTreeSupportEfl::layoutFrame(browser->mainFrame()); setEvasModifiers(evas, modifiers); if (event & EvasMouseEventMove) evas_event_feed_mouse_move(evas, gLastMousePositionX, gLastMousePositionY, timeStamp++, 0); if (event & EvasMouseEventDown) evas_event_feed_mouse_down(evas, buttonNumber, EVAS_BUTTON_NONE, timeStamp++, 0); if (event & EvasMouseEventUp) evas_event_feed_mouse_up(evas, buttonNumber, EVAS_BUTTON_NONE, timeStamp++, 0); const bool horizontal = !!(event & EvasMouseEventScrollLeft | event & EvasMouseEventScrollRight); const bool vertical = !!(event & EvasMouseEventScrollUp | event & EvasMouseEventScrollDown); if (vertical && horizontal) { evas_event_feed_mouse_wheel(evas, 0, (event & EvasMouseEventScrollUp) ? 10 : -10, timeStamp, 0); evas_event_feed_mouse_wheel(evas, 1, (event & EvasMouseEventScrollLeft) ? 10 : -10, timeStamp, 0); } else if (vertical) evas_event_feed_mouse_wheel(evas, 0, (event & EvasMouseEventScrollUp) ? 10 : -10, timeStamp, 0); else if (horizontal) evas_event_feed_mouse_wheel(evas, 1, (event & EvasMouseEventScrollLeft) ? 10 : -10, timeStamp, 0); setEvasModifiers(evas, EvasKeyModifierNone); } static Eina_Bool sendClick(void*) { sendMouseEvent(evas_object_evas_get(browser->mainFrame()), EvasMouseEventClick, EvasMouseButtonLeft, EvasKeyModifierNone); return ECORE_CALLBACK_CANCEL; } static JSValueRef scheduleAsynchronousClickCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { ecore_idler_add(sendClick, 0); return JSValueMakeUndefined(context); } static void updateClickCount(int button) { if (gLastClickPositionX != gLastMousePositionX || gLastClickPositionY != gLastMousePositionY || gLastClickButton != button || gTimeOffset - gLastClickTimeOffset >= 1) gClickCount = 1; else gClickCount++; } static EvasKeyModifier modifierFromJSValue(JSContextRef context, const JSValueRef value) { JSRetainPtr jsKeyValue(Adopt, JSValueToStringCopy(context, value, 0)); if (equals(jsKeyValue, "ctrlKey") || equals(jsKeyValue, "addSelectionKey")) return EvasKeyModifierControl; if (equals(jsKeyValue, "shiftKey") || equals(jsKeyValue, "rangeSelectionKey")) return EvasKeyModifierShift; if (equals(jsKeyValue, "altKey")) return EvasKeyModifierAlt; if (equals(jsKeyValue, "metaKey")) return EvasKeyModifierMeta; return EvasKeyModifierNone; } static EvasKeyModifier modifiersFromJSValue(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 modifierFromJSValue(context, modifiers); JSObjectRef modifiersArray = JSValueToObject(context, modifiers, 0); if (!modifiersArray) return EvasKeyModifierNone; unsigned modifier = 0; int modifiersCount = JSValueToNumber(context, JSObjectGetProperty(context, modifiersArray, JSStringCreateWithUTF8CString("length"), 0), 0); for (int i = 0; i < modifiersCount; ++i) modifier |= static_cast(modifierFromJSValue(context, JSObjectGetPropertyAtIndex(context, modifiersArray, i, 0))); return static_cast(modifier); } 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(JSValueToNumber(context, arguments[0], exception)); if (exception && *exception) return JSValueMakeUndefined(context); } button = translateMouseButtonNumber(button); // If the same mouse button is already in the down position don't send another event as it may confuse Xvfb. if (gButtonCurrentlyDown == button) return JSValueMakeUndefined(context); updateClickCount(button); EvasKeyModifier modifiers = argumentCount >= 2 ? modifiersFromJSValue(context, arguments[1]) : EvasKeyModifierNone; sendMouseEvent(evas_object_evas_get(browser->mainFrame()), EvasMouseEventDown, button, modifiers); gButtonCurrentlyDown = button; return JSValueMakeUndefined(context); } 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(JSValueToNumber(context, arguments[0], exception)); if (exception && *exception) return JSValueMakeUndefined(context); } gLastClickPositionX = gLastMousePositionX; gLastClickPositionY = gLastMousePositionY; gLastClickButton = gButtonCurrentlyDown; gLastClickTimeOffset = gTimeOffset; gButtonCurrentlyDown = 0; EvasKeyModifier modifiers = argumentCount >= 2 ? modifiersFromJSValue(context, arguments[1]) : EvasKeyModifierNone; sendMouseEvent(evas_object_evas_get(browser->mainFrame()), EvasMouseEventUp, translateMouseButtonNumber(button), modifiers); return JSValueMakeUndefined(context); } static JSValueRef mouseMoveToCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { if (argumentCount < 2) return JSValueMakeUndefined(context); gLastMousePositionX = static_cast(JSValueToNumber(context, arguments[0], exception)); if (exception && *exception) return JSValueMakeUndefined(context); gLastMousePositionY = static_cast(JSValueToNumber(context, arguments[1], exception)); if (exception && *exception) return JSValueMakeUndefined(context); sendMouseEvent(evas_object_evas_get(browser->mainFrame()), EvasMouseEventMove, EvasMouseButtonNone, EvasKeyModifierNone); return JSValueMakeUndefined(context); } static EvasMouseEvent evasMouseEventFromHorizontalAndVerticalOffsets(int horizontalOffset, int verticalOffset) { unsigned mouseEvent = 0; if (verticalOffset > 0) mouseEvent |= EvasMouseEventScrollUp; else if (verticalOffset < 0) mouseEvent |= EvasMouseEventScrollDown; if (horizontalOffset > 0) mouseEvent |= EvasMouseEventScrollRight; else if (horizontalOffset < 0) mouseEvent |= EvasMouseEventScrollLeft; return static_cast(mouseEvent); } static JSValueRef mouseScrollByCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { if (argumentCount < 2) return JSValueMakeUndefined(context); const int horizontal = static_cast(JSValueToNumber(context, arguments[0], exception)); if (exception && *exception) return JSValueMakeUndefined(context); const int vertical = static_cast(JSValueToNumber(context, arguments[1], exception)); if (exception && *exception) return JSValueMakeUndefined(context); sendMouseEvent(evas_object_evas_get(browser->mainFrame()), evasMouseEventFromHorizontalAndVerticalOffsets(horizontal, vertical), EvasMouseButtonNone, EvasKeyModifierNone); return JSValueMakeUndefined(context); } static JSValueRef continuousMouseScrollByCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { return JSValueMakeUndefined(context); } static const CString keyPadNameFromJSValue(JSStringRef character) { if (equals(character, "leftArrow")) return "KP_Left"; if (equals(character, "rightArrow")) return "KP_Right"; if (equals(character, "upArrow")) return "KP_Up"; if (equals(character, "downArrow")) return "KP_Down"; if (equals(character, "pageUp")) return "KP_Prior"; if (equals(character, "pageDown")) return "KP_Next"; if (equals(character, "home")) return "KP_Home"; if (equals(character, "end")) return "KP_End"; if (equals(character, "insert")) return "KP_Insert"; if (equals(character, "delete")) return "KP_Delete"; return character->ustring().utf8(); } static const CString keyNameFromJSValue(JSStringRef character) { if (equals(character, "leftArrow")) return "Left"; if (equals(character, "rightArrow")) return "Right"; if (equals(character, "upArrow")) return "Up"; if (equals(character, "downArrow")) return "Down"; if (equals(character, "pageUp")) return "Prior"; if (equals(character, "pageDown")) return "Next"; if (equals(character, "home")) return "Home"; if (equals(character, "end")) return "End"; if (equals(character, "insert")) return "Insert"; if (equals(character, "delete")) return "Delete"; if (equals(character, "printScreen")) return "Print"; if (equals(character, "menu")) return "Menu"; if (equals(character, "F1")) return "F1"; if (equals(character, "F2")) return "F2"; if (equals(character, "F3")) return "F3"; if (equals(character, "F4")) return "F4"; if (equals(character, "F5")) return "F5"; if (equals(character, "F6")) return "F6"; if (equals(character, "F7")) return "F7"; if (equals(character, "F8")) return "F8"; if (equals(character, "F9")) return "F9"; if (equals(character, "F10")) return "F10"; if (equals(character, "F11")) return "F11"; if (equals(character, "F12")) return "F12"; int charCode = JSStringGetCharactersPtr(character)[0]; if (charCode == '\n' || charCode == '\r') return "Return"; if (charCode == '\t') return "Tab"; if (charCode == '\x8') return "BackSpace"; return character->ustring().utf8(); } static KeyEventInfo* createKeyEventInfo(JSContextRef context, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { if (!ewk_frame_view_get(browser->mainFrame())) return 0; if (argumentCount < 1) return 0; // handle location argument. int location = DomKeyLocationStandard; if (argumentCount > 2) location = static_cast(JSValueToNumber(context, arguments[2], exception)); JSRetainPtr character(Adopt, JSValueToStringCopy(context, arguments[0], exception)); if (exception && *exception) return 0; EvasKeyModifier modifiers = EvasKeyModifierNone; if (argumentCount >= 2) modifiers = modifiersFromJSValue(context, arguments[1]); const CString keyName = (location == DomKeyLocationNumpad) ? keyPadNameFromJSValue(character.get()) : keyNameFromJSValue(character.get()); return new KeyEventInfo(keyName, modifiers); } static void sendKeyDown(Evas* evas, KeyEventInfo* keyEventInfo) { if (!keyEventInfo) return; const char* keyName = keyEventInfo->keyName.data(); EvasKeyModifier modifiers = keyEventInfo->modifiers; DumpRenderTreeSupportEfl::layoutFrame(browser->mainFrame()); ASSERT(evas); setEvasModifiers(evas, modifiers); evas_event_feed_key_down(evas, keyName, keyName, keyName, 0, 0, 0); evas_event_feed_key_up(evas, keyName, keyName, keyName, 0, 1, 0); setEvasModifiers(evas, EvasKeyModifierNone); DumpRenderTreeSupportEfl::deliverAllMutationsIfNecessary(); } static JSValueRef keyDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { OwnPtr keyEventInfo = adoptPtr(createKeyEventInfo(context, argumentCount, arguments, exception)); sendKeyDown(evas_object_evas_get(browser->mainFrame()), keyEventInfo.get()); 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); Evas_Object* view = ewk_frame_view_get(browser->mainFrame()); if (!view) return JSValueMakeUndefined(context); float scaleFactor = JSValueToNumber(context, arguments[0], exception); float x = JSValueToNumber(context, arguments[1], exception); float y = JSValueToNumber(context, arguments[2], exception); ewk_view_scale_set(view, scaleFactor, x, y); return JSValueMakeUndefined(context); } static void textZoom(ZoomEvent zoomEvent) { Evas_Object* view = ewk_frame_view_get(browser->mainFrame()); if (!view) return; float zoomFactor = ewk_view_text_zoom_get(view); if (zoomEvent == ZoomIn) zoomFactor *= zoomMultiplierRatio; else zoomFactor /= zoomMultiplierRatio; ewk_view_text_zoom_set(view, zoomFactor); } static void pageZoom(ZoomEvent zoomEvent) { Evas_Object* view = ewk_frame_view_get(browser->mainFrame()); if (!view) return; float zoomFactor = ewk_view_page_zoom_get(view); if (zoomEvent == ZoomIn) zoomFactor *= zoomMultiplierRatio; else zoomFactor /= zoomMultiplierRatio; ewk_view_page_zoom_set(view, zoomFactor); } static JSValueRef textZoomInCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { textZoom(ZoomIn); return JSValueMakeUndefined(context); } static JSValueRef textZoomOutCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { textZoom(ZoomOut); return JSValueMakeUndefined(context); } static JSValueRef zoomPageInCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { pageZoom(ZoomIn); return JSValueMakeUndefined(context); } static JSValueRef zoomPageOutCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { pageZoom(ZoomOut); return JSValueMakeUndefined(context); } static Eina_Bool sendAsynchronousKeyDown(void* userData) { OwnPtr keyEventInfo = adoptPtr(static_cast(userData)); sendKeyDown(evas_object_evas_get(browser->mainFrame()), keyEventInfo.get()); return ECORE_CALLBACK_CANCEL; } static JSValueRef scheduleAsynchronousKeyDownCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { KeyEventInfo* keyEventInfo = createKeyEventInfo(context, argumentCount, arguments, exception); ecore_idler_add(sendAsynchronousKeyDown, static_cast(keyEventInfo)); return JSValueMakeUndefined(context); } static JSStaticFunction staticFunctions[] = { { "mouseScrollBy", mouseScrollByCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "continuousMouseScrollBy", continuousMouseScrollByCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "mouseDown", mouseDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "mouseUp", mouseUpCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "mouseMoveTo", mouseMoveToCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "keyDown", keyDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "scheduleAsynchronousClick", scheduleAsynchronousClickCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "scheduleAsynchronousKeyDown", scheduleAsynchronousKeyDownCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "scalePageBy", scalePageByCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "textZoomIn", textZoomInCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "textZoomOut", textZoomOutCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "zoomPageIn", zoomPageInCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { "zoomPageOut", zoomPageOutCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { 0, 0, 0 } }; static JSStaticValue staticValues[] = { { 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) { gDragMode = 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. gTimeOffset += 1000; gLastMousePositionX = gLastMousePositionY = 0; gLastClickPositionX = gLastClickPositionY = 0; gLastClickTimeOffset = 0; gLastClickButton = 0; gButtonCurrentlyDown = 0; gClickCount = 0; } return JSObjectMake(context, getClass(context), 0); }