From b864bc5802864e61abea0bd7d817468d837509af Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 13 May 2022 23:29:13 +0200 Subject: [PATCH] Bugfix/120/macos doubleclick fix (#122) * (#120) Only set kCGMouseEventClickState = 2 on second click to fire a single doubleclick event after two clicks * (#120) Extend toggleMouse such that it manually handles double clicks by tracking intervals between different click events * (#120) Added explanatory comment on custom doubleclick implementation --- CMakeLists.txt | 2 +- src/macos/mouse.c | 271 +++++++++++++++++++-------------------- src/macos/mouse_utils.h | 83 ++++++++++++ src/macos/mouse_utils.mm | 7 + 4 files changed, 224 insertions(+), 139 deletions(-) create mode 100644 src/macos/mouse_utils.h create mode 100644 src/macos/mouse_utils.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c3f530..a3c9b9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(SOURCE_FILES "src/main.cc" "src/deadbeef_rand.c" "src/MMBitmap.c") if (UNIX AND NOT APPLE) set(SOURCE_FILES "${SOURCE_FILES}" "src/linux/keycode.c" "src/linux/keypress.c" "src/linux/mouse.c" "src/linux/screen.c" "src/linux/screengrab.c" "src/linux/xdisplay.c" "src/linux/highlightwindow.c" "src/linux/window_manager.cc") elseif (UNIX AND APPLE) - set(SOURCE_FILES "${SOURCE_FILES}" "src/macos/keycode.c" "src/macos/keypress.c" "src/macos/mouse.c" "src/macos/screen.c" "src/macos/screengrab.c" "src/macos/highlightwindow.m" "src/macos/window_manager.mm") + set(SOURCE_FILES "${SOURCE_FILES}" "src/macos/keycode.c" "src/macos/keypress.c" "src/macos/mouse.c" "src/macos/mouse_utils.mm" "src/macos/screen.c" "src/macos/screengrab.c" "src/macos/highlightwindow.m" "src/macos/window_manager.mm") elseif (WIN32) set(SOURCE_FILES "${SOURCE_FILES}" "src/win32/keycode.c" "src/win32/keypress.c" "src/win32/mouse.c" "src/win32/screen.c" "src/win32/screengrab.c" "src/win32/highlightwindow.c" "src/win32/window_manager.cc") endif() diff --git a/src/macos/mouse.c b/src/macos/mouse.c index 92abed1..97dbe6e 100644 --- a/src/macos/mouse.c +++ b/src/macos/mouse.c @@ -1,138 +1,155 @@ #include "../mouse.h" +#include "../deadbeef_rand.h" #include "../microsleep.h" +#include "mouse_utils.h" -#include /* For floor() */ #include +#include /* For floor() */ -#if !defined(M_SQRT2) -#define M_SQRT2 1.4142135623730950488016887 /* Fix for MSVC. */ -#endif +static int32_t DEFAULT_DOUBLE_CLICK_INTERVAL_MS = 200; -#define MMMouseToCGEventType(down, button) \ - (down ? MMMouseDownToCGEventType(button) : MMMouseUpToCGEventType(button)) +#define MMMouseToCGEventType(down, button) \ + (down ? MMMouseDownToCGEventType(button) : MMMouseUpToCGEventType(button)) -#define MMMouseDownToCGEventType(button) \ - ((button) == (LEFT_BUTTON) ? kCGEventLeftMouseDown \ - : ((button) == RIGHT_BUTTON ? kCGEventRightMouseDown \ - : kCGEventOtherMouseDown)) +#define MMMouseDownToCGEventType(button) \ + ((button) == (LEFT_BUTTON) \ + ? kCGEventLeftMouseDown \ + : ((button) == RIGHT_BUTTON ? kCGEventRightMouseDown \ + : kCGEventOtherMouseDown)) -#define MMMouseUpToCGEventType(button) \ - ((button) == LEFT_BUTTON ? kCGEventLeftMouseUp \ - : ((button) == RIGHT_BUTTON ? kCGEventRightMouseUp \ - : kCGEventOtherMouseUp)) +#define MMMouseUpToCGEventType(button) \ + ((button) == LEFT_BUTTON \ + ? kCGEventLeftMouseUp \ + : ((button) == RIGHT_BUTTON ? kCGEventRightMouseUp \ + : kCGEventOtherMouseUp)) -#define MMMouseDragToCGEventType(button) \ - ((button) == LEFT_BUTTON ? kCGEventLeftMouseDragged \ - : ((button) == RIGHT_BUTTON ? kCGEventRightMouseDragged \ - : kCGEventOtherMouseDragged)) +#define MMMouseDragToCGEventType(button) \ + ((button) == LEFT_BUTTON \ + ? kCGEventLeftMouseDragged \ + : ((button) == RIGHT_BUTTON ? kCGEventRightMouseDragged \ + : kCGEventOtherMouseDragged)) /** * Calculate the delta for a mouse move and add them to the event. * @param event The mouse move event (by ref). * @param point The new mouse x and y. */ -void calculateDeltas(CGEventRef *event, MMPoint point) -{ - /** - * The next few lines are a workaround for games not detecting mouse moves. - * See this issue for more information: - * https://github.com/octalmage/robotjs/issues/159 - */ - CGEventRef get = CGEventCreate(NULL); - CGPoint mouse = CGEventGetLocation(get); - - // Calculate the deltas. - int64_t deltaX = point.x - mouse.x; - int64_t deltaY = point.y - mouse.y; - - CGEventSetIntegerValueField(*event, kCGMouseEventDeltaX, deltaX); - CGEventSetIntegerValueField(*event, kCGMouseEventDeltaY, deltaY); - - CFRelease(get); +void calculateDeltas(CGEventRef *event, MMPoint point) { + /** + * The next few lines are a workaround for games not detecting mouse moves. + * See this issue for more information: + * https://github.com/octalmage/robotjs/issues/159 + */ + CGEventRef get = CGEventCreate(NULL); + CGPoint mouse = CGEventGetLocation(get); + + // Calculate the deltas. + int64_t deltaX = point.x - mouse.x; + int64_t deltaY = point.y - mouse.y; + + CGEventSetIntegerValueField(*event, kCGMouseEventDeltaX, deltaX); + CGEventSetIntegerValueField(*event, kCGMouseEventDeltaY, deltaY); + + CFRelease(get); } /** * Move the mouse to a specific point. * @param point The coordinates to move the mouse to (x, y). */ -void moveMouse(MMPoint point) -{ - CGPoint position = CGPointMake(point.x, point.y); - // Create an HID hardware event source - CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); - - CGEventRef evt = NULL; - if (CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonLeft)) - { - // Create a left button drag - evt = CGEventCreateMouseEvent(src, kCGEventLeftMouseDragged, position, kCGMouseButtonLeft); - } - else - { - if (CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonRight)) - { - // Create a right button drag - evt = CGEventCreateMouseEvent(src, kCGEventRightMouseDragged, position, kCGMouseButtonLeft); - } - else - { - // Create a mouse move event - evt = CGEventCreateMouseEvent(src, kCGEventMouseMoved, position, kCGMouseButtonLeft); - } - } - - // Post mouse event and release - CGEventPost(kCGHIDEventTap, evt); - if (evt != NULL) - { - CFRelease(evt); +void moveMouse(MMPoint point) { + CGPoint position = CGPointMake(point.x, point.y); + // Create an HID hardware event source + CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + + CGEventRef evt = NULL; + if (CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, + kCGMouseButtonLeft)) { + // Create a left button drag + evt = CGEventCreateMouseEvent(src, kCGEventLeftMouseDragged, position, + kCGMouseButtonLeft); + } else { + if (CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, + kCGMouseButtonRight)) { + // Create a right button drag + evt = CGEventCreateMouseEvent(src, kCGEventRightMouseDragged, position, + kCGMouseButtonLeft); + } else { + // Create a mouse move event + evt = CGEventCreateMouseEvent(src, kCGEventMouseMoved, position, + kCGMouseButtonLeft); } - CFRelease(src); + } + + // Post mouse event and release + CGEventPost(kCGHIDEventTap, evt); + if (evt != NULL) { + CFRelease(evt); + } + CFRelease(src); } -void dragMouse(MMPoint point, const MMMouseButton button) -{ - CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); - const CGEventType dragType = MMMouseDragToCGEventType(button); - CGEventRef drag = CGEventCreateMouseEvent(src, dragType, CGPointFromMMPoint(point), (CGMouseButton)button); - calculateDeltas(&drag, point); +void dragMouse(MMPoint point, const MMMouseButton button) { + CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + const CGEventType dragType = MMMouseDragToCGEventType(button); + CGEventRef drag = CGEventCreateMouseEvent( + src, dragType, CGPointFromMMPoint(point), (CGMouseButton)button); + calculateDeltas(&drag, point); - CGEventPost(kCGHIDEventTap, drag); - CFRelease(drag); - CFRelease(src); + CGEventPost(kCGHIDEventTap, drag); + CFRelease(drag); + CFRelease(src); } -MMPoint getMousePos() -{ - CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); - CGEventRef event = CGEventCreate(src); - CGPoint point = CGEventGetLocation(event); - CFRelease(event); - CFRelease(src); +MMPoint getMousePos() { + CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + CGEventRef event = CGEventCreate(src); + CGPoint point = CGEventGetLocation(event); + CFRelease(event); + CFRelease(src); - return MMPointFromCGPoint(point); + return MMPointFromCGPoint(point); } /** * Press down a button, or release it. * @param down True for down, false for up. * @param button The button to press down or release. + * + * This function ships a manual implementation to handle double clicks by tracking the time interval between mouse events. + * Reason for this is the fact that https://developer.apple.com/documentation/coregraphics/1408790-cgeventsourcesecondssincelasteve?language=objc + * has a bit of latency and will stop working correctly if the time between two consecutive clicks is not long enough. + * + * This implementation captures the current timestamp for up/down events on each of left/middle/right mouse buttons. + * If the interval between two clicks is lower than https://developer.apple.com/documentation/appkit/nsevent/1528384-doubleclickinterval?language=objc + * and both clicks happen at the same position, we alter the mouse event to trigger a double click by setting kCGMouseEventClickState = 2 on the event */ -void toggleMouse(bool down, MMMouseButton button) -{ - const CGPoint currentPos = CGPointFromMMPoint(getMousePos()); - const CGEventType mouseType = MMMouseToCGEventType(down, button); - CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); - CGEventRef event = CGEventCreateMouseEvent(src, mouseType, currentPos, (CGMouseButton)button); - CGEventPost(kCGHIDEventTap, event); - CFRelease(event); - CFRelease(src); +void toggleMouse(bool down, MMMouseButton button) { + static ClickTimer clickTimer = {{0, 0}, {0, 0}, {0, 0}, {0, 0}}; + + MMPoint currentMMPoint= getMousePos(); + + clock_t intervalSinceLastClick = timeSinceLastClick(&clickTimer, down, button, clock()); + + const CGPoint currentPos = CGPointFromMMPoint(currentMMPoint); + const CGEventType mouseType = MMMouseToCGEventType(down, button); + CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + CGEventRef event = CGEventCreateMouseEvent(src, mouseType, currentPos, + (CGMouseButton)button); + double maxInterval = GetDoubleClickTime(); + if (intervalSinceLastClick > 0 && intervalSinceLastClick <= maxInterval && + areSamePoint(currentMMPoint, clickTimer.clickLocation)) { + CGEventSetIntegerValueField(event, kCGMouseEventClickState, 2); + } + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + CFRelease(src); + recordClickTime(&clickTimer, down, button, currentMMPoint); } -void clickMouse(MMMouseButton button) -{ - toggleMouse(true, button); - toggleMouse(false, button); +void clickMouse(MMMouseButton button) { + toggleMouse(true, button); + toggleMouse(false, button); } /** @@ -140,49 +157,27 @@ void clickMouse(MMMouseButton button) * @param button Button to click. */ void doubleClick(MMMouseButton button) { - /* Double click for Mac. */ - const CGPoint currentPos = CGPointFromMMPoint(getMousePos()); - const CGEventType mouseTypeDown = MMMouseToCGEventType(true, button); - const CGEventType mouseTypeUp = MMMouseToCGEventType(false, button); - - CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); - CGEventRef event = CGEventCreateMouseEvent(src, mouseTypeDown, currentPos, - button); - - // First down - CGEventPost(kCGHIDEventTap, event); - - // First up - CGEventSetType(event, mouseTypeUp); - CGEventPost(kCGHIDEventTap, event); - - /* Set event to double click. */ - CGEventSetIntegerValueField(event, kCGMouseEventClickState, 2); - - // Second down - CGEventSetType(event, mouseTypeDown); - CGEventPost(kCGHIDEventTap, event); - - // Second up - CGEventSetType(event, mouseTypeUp); - CGEventPost(kCGHIDEventTap, event); - - CFRelease(event); - CFRelease(src); + double maxDoubleClickTime = GetDoubleClickTime(); + clickMouse(button); + if (maxDoubleClickTime > DEFAULT_DOUBLE_CLICK_INTERVAL_MS) { + microsleep(DEFAULT_DOUBLE_CLICK_INTERVAL_MS); + } else { + microsleep(DEADBEEF_RANDRANGE(1, maxDoubleClickTime)); + } + clickMouse(button); } -void scrollMouse(int x, int y) -{ - /* - * Direction should only be considered based on the scrollDirection. - * This should not interfere. - * Set up the OS specific solution - */ +void scrollMouse(int x, int y) { + /* + * Direction should only be considered based on the scrollDirection. + * This should not interfere. + * Set up the OS specific solution + */ - CGEventRef event; + CGEventRef event; - event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 2, y, x); - CGEventPost(kCGHIDEventTap, event); + event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 2, y, x); + CGEventPost(kCGHIDEventTap, event); - CFRelease(event); + CFRelease(event); } \ No newline at end of file diff --git a/src/macos/mouse_utils.h b/src/macos/mouse_utils.h new file mode 100644 index 0000000..eb9113d --- /dev/null +++ b/src/macos/mouse_utils.h @@ -0,0 +1,83 @@ +#ifndef MOUSE_UTILS_H +#define MOUSE_UTILS_H + +#include "../mouse.h" +#include "../types.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint64_t down; + uint64_t up; +} ClickTime; + +typedef struct { + ClickTime left; + ClickTime middle; + ClickTime right; + MMPoint clickLocation; +} ClickTimer; + +double GetDoubleClickTime(); + +inline bool areSamePoint(MMPoint one, MMPoint other) { + return one.x == other.x && one.y == other.y; +} + +inline void recordClickTime(ClickTimer *timer, bool down, + MMMouseButton button, + MMPoint currentPoint) { + clock_t milli = clock(); + if (button == LEFT_BUTTON) { + if (down) { + timer->left.down = milli; + } else { + timer->left.up = milli; + } + } else if (button == CENTER_BUTTON) { + if (down) { + timer->middle.down = milli; + } else { + timer->middle.up = milli; + } + } else if (button == RIGHT_BUTTON) { + if (down) { + timer->right.down = milli; + } else { + timer->right.up = milli; + } + } + timer->clickLocation = currentPoint; +} + +inline clock_t timeSinceLastClick(ClickTimer *timer, bool down, MMMouseButton button, clock_t currentTime) { + if (button == LEFT_BUTTON) { + if (down) { + return currentTime - timer->left.down; + } else { + return currentTime - timer->left.up; + } + } else if (button == CENTER_BUTTON) { + if (down) { + return currentTime - timer->middle.down; + } else { + return currentTime - timer->middle.up; + } + } else if (button == RIGHT_BUTTON) { + if (down) { + return currentTime - timer->right.down; + } else { + return currentTime - timer->right.up; + } + } + return 0; +} + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/src/macos/mouse_utils.mm b/src/macos/mouse_utils.mm new file mode 100644 index 0000000..dda754f --- /dev/null +++ b/src/macos/mouse_utils.mm @@ -0,0 +1,7 @@ +#include "mouse_utils.h" +#import + +double GetDoubleClickTime() { + double doubleClickInterval = [NSEvent doubleClickInterval]; + return doubleClickInterval * 1000; +} \ No newline at end of file