Signed-off-by: Elaine Lefler elaineclefler@gmail.com ---
v2: Splitting into smaller patches as per feedback.
I'm afraid this is also a fairly large patch, but there's no way to reduce it without leaving dead code. --- dlls/winemac.drv/Makefile.in | 3 +- dlls/winemac.drv/cocoa_wintab.h | 13 ++ dlls/winemac.drv/cocoa_wintab.m | 219 ++++++++++++++++++++++++++++++++ dlls/winemac.drv/event.c | 3 + dlls/winemac.drv/macdrv.h | 1 + dlls/winemac.drv/macdrv_cocoa.h | 11 ++ dlls/winemac.drv/wintab.c | 74 ++++++++++- 7 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 dlls/winemac.drv/cocoa_wintab.m
diff --git a/dlls/winemac.drv/Makefile.in b/dlls/winemac.drv/Makefile.in index 47b7c68feb3..5fc8f4152fe 100644 --- a/dlls/winemac.drv/Makefile.in +++ b/dlls/winemac.drv/Makefile.in @@ -33,6 +33,7 @@ OBJC_SRCS = \ cocoa_main.m \ cocoa_opengl.m \ cocoa_status_item.m \ - cocoa_window.m + cocoa_window.m \ + cocoa_wintab.m
RC_SRCS = winemac.rc diff --git a/dlls/winemac.drv/cocoa_wintab.h b/dlls/winemac.drv/cocoa_wintab.h index 1b9867c9d34..0b85bd2ad0d 100644 --- a/dlls/winemac.drv/cocoa_wintab.h +++ b/dlls/winemac.drv/cocoa_wintab.h @@ -23,6 +23,13 @@
#include <math.h>
+#ifdef __OBJC__ +/* Necessary hack because this header must be included on both the Wine side + * and from Objective-C */ +typedef BOOL OBJC_BOOL; +#define BOOL WIN_BOOL +#endif + #include "windef.h" #include "wintab.h"
@@ -48,4 +55,10 @@ /* Shared device context */ extern LOGCONTEXTW macdrv_tablet_ctx DECLSPEC_HIDDEN;
+/* Window to which tablet events are delivered */ +extern void* macdrv_tablet_window DECLSPEC_HIDDEN; + +/* Objective-C function to start tablet events */ +void CDECL macdrv_start_tablet_monitor(void) DECLSPEC_HIDDEN; + #endif /* !defined(__WINE_MACDRV_COCOA_WINTAB_H) */ diff --git a/dlls/winemac.drv/cocoa_wintab.m b/dlls/winemac.drv/cocoa_wintab.m new file mode 100644 index 00000000000..da0c2e91df4 --- /dev/null +++ b/dlls/winemac.drv/cocoa_wintab.m @@ -0,0 +1,219 @@ +/* + * MACDRV Cocoa wintab implementations + * + * Copyright 2022 Elaine Lefler + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "config.h" + +#include "cocoa_app.h" +#include "cocoa_event.h" +#include "cocoa_window.h" +#include "cocoa_wintab.h" + +static UINT packet_id = 0; +static UINT current_cursor = MACDRV_CURSOR_CURSOR; + +static CFMachPortRef event_tap; + +static const CGEventMask MOUSE_DOWN_EVENTS = + CGEventMaskBit(kCGEventLeftMouseDown) + | CGEventMaskBit(kCGEventRightMouseDown) + | CGEventMaskBit(kCGEventOtherMouseDown); +static const CGEventMask MOUSE_UP_EVENTS = + CGEventMaskBit(kCGEventLeftMouseUp) + | CGEventMaskBit(kCGEventRightMouseUp) + | CGEventMaskBit(kCGEventOtherMouseUp); +static const CGEventMask MOUSE_DRAG_EVENTS = + CGEventMaskBit(kCGEventLeftMouseDragged) + | CGEventMaskBit(kCGEventRightMouseDragged) + | CGEventMaskBit(kCGEventOtherMouseDragged); + +static const CGEventMask MOUSE_EVENTS = + CGEventMaskBit(kCGEventMouseMoved) + | MOUSE_DOWN_EVENTS | MOUSE_UP_EVENTS | MOUSE_DRAG_EVENTS; + +static const CGEventMask TABLET_EVENTS = + CGEventMaskBit(kCGEventTabletPointer) + | CGEventMaskBit(kCGEventTabletProximity); + +static UINT cursor_from_nx(NSPointingDeviceType device_type) +{ + switch (device_type) + { + case NX_TABLET_POINTER_PEN: + return MACDRV_CURSOR_PEN; + case NX_TABLET_POINTER_ERASER: + return MACDRV_CURSOR_ERASER; + default: + /* Squash Unknown into Cursor */ + return MACDRV_CURSOR_CURSOR; + } +} + +static void packet_from_cgevent(PACKET* pkt, CGEventRef event) +{ + static const double ALTI_VECTOR_SCALE = 0.89879404629916700; /* cos(26) */ + static const double IN_VECTOR_ANGLE_SCALE = .073519026192766368; /* (cos(19) / cos(26) - 1) * sqrt(2) */ + + double azimuth, altitude; + double angle_scale, max_length, length; + + CGPoint location = CGEventGetUnflippedLocation(event); + double tilt_x = CGEventGetDoubleValueField(event, kCGTabletEventTiltX); + double tilt_y = CGEventGetDoubleValueField(event, kCGTabletEventTiltY); + uint64_t time_ns = CGEventGetTimestamp(event); + pkt->pkStatus = (current_cursor == MACDRV_CURSOR_ERASER ? TPS_INVERT : 0); + pkt->pkTime = [[WineApplicationController sharedController] ticksForEventTime:time_ns / (double)NSEC_PER_SEC]; + pkt->pkSerialNumber = ++packet_id; + + pkt->pkCursor = current_cursor; + pkt->pkButtons = CGEventGetIntegerValueField(event, kCGTabletEventPointButtons); + + location = cgpoint_win_from_mac(location); + + /* y should be y-1 because of the way "unflipped" location is calculated. + * We don't -1 the extents because macOS doesn't map the tablet that way. */ + pkt->pkX = (location.x - (double)macdrv_tablet_ctx.lcSysOrgX) / macdrv_tablet_ctx.lcSysExtX * TABLET_WIDTH; + pkt->pkY = (location.y - 1 - (double)macdrv_tablet_ctx.lcSysOrgY) / macdrv_tablet_ctx.lcSysExtY * TABLET_HEIGHT; + pkt->pkZ = CGEventGetIntegerValueField(event, kCGTabletEventPointZ); + + pkt->pkNormalPressure = CGEventGetDoubleValueField(event, kCGTabletEventPointPressure) * MAX_PRESSURE; + pkt->pkTangentPressure = CGEventGetDoubleValueField(event, kCGTabletEventTangentialPressure) * MAX_PRESSURE; + + /* Find the angle around the Z axis. Note that 0 is up and angles move + * clockwise. Swapping x and y gives the correct angle. */ + azimuth = atan2(tilt_x, tilt_y); + /* Adjust to 0..360 range */ + if (azimuth < 0.) + azimuth += 2 * M_PI; + + /* With the pen resting on its barrel and oriented horizontally or + * vertically, a real tablet reads an altitude of 26. This corresponds to a + * tilt of 1. Spinning it to a 45 degree angle allows reading a little bit + * lower, down to 19. However, the X/Y tilt appears to report values that + * are out of range for wintab. Cap the vector at a value that smoothly + * transitions between 1 and the maximum length based on the azimuth. */ + angle_scale = fmin(fabs(cos(azimuth)), fabs(sin(azimuth))); + max_length = 1. + IN_VECTOR_ANGLE_SCALE * angle_scale; + + /* Since the pen moves in a circular path we can calculate the altitude as + * the arccosine of the X/Y vector. Multiply it by cos(26) such that a + * vector of length 1 maps to 26 degrees. */ + length = fmin(max_length, sqrt(tilt_x * tilt_x + tilt_y * tilt_y)); + altitude = acos(length * ALTI_VECTOR_SCALE); + + pkt->pkOrientation.orAzimuth = MAKE_ANGLE_RAD(azimuth); + /* Altitude is negative on the eraser */ + pkt->pkOrientation.orAltitude = MAKE_ANGLE_RAD(!(pkt->pkStatus & TPS_INVERT) ? altitude : -altitude); + /* Rotation is the same as twist */ + pkt->pkOrientation.orTwist = MAKE_ANGLE_DEG(CGEventGetDoubleValueField(event, kCGTabletEventRotation)); +} + +static CGEventRef tablet_event_cb(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon) +{ + CGEventMask type_mask; + BOOL is_proximity; + + macdrv_event* out_event; + + /* Tap can be disabled for various reasons, make sure it comes back */ + if (type == kCGEventTapDisabledByTimeout + || type == kCGEventTapDisabledByUserInput) + { + CGEventTapEnable(event_tap, YES); + return event; + } + + type_mask = CGEventMaskBit(type); + is_proximity = (type == kCGEventTabletProximity); + + if (!(type_mask & TABLET_EVENTS)) + { + int64_t subtype = CGEventGetIntegerValueField(event, kCGMouseEventSubtype); + if (subtype == kCGEventMouseSubtypeTabletProximity) + is_proximity = YES; + else if (subtype != kCGEventMouseSubtypeTabletPoint) + /* Not a tablet event */ + return event; + } + + /*NSLog(@"%@", [NSEvent eventWithCGEvent:event]);*/ + out_event = macdrv_create_event(TABLET_EVENT, (WineWindow*)macdrv_tablet_window); + + if (is_proximity) + { + /* Cursor type only appears during proximity events */ + current_cursor = cursor_from_nx(CGEventGetIntegerValueField(event, kCGTabletProximityEventPointerType)); + + if (CGEventGetIntegerValueField(event, kCGTabletProximityEventEnterProximity) != 0) + out_event->tablet_event.type = TABLET_EVENT_PROXIMITY_ENTER; + else + out_event->tablet_event.type = TABLET_EVENT_PROXIMITY_LEAVE; + } + else + { + PACKET* event_packet = calloc(1, sizeof(PACKET)); + packet_from_cgevent(event_packet, event); + out_event->tablet_event.type = TABLET_EVENT_POINT; + out_event->tablet_event.packet = event_packet; + } + + [[(WineWindow*)macdrv_tablet_window queue] postEvent:out_event]; + macdrv_release_event(out_event); + return event; +} + +static void* macdrv_tablet_main(void* _) +{ + CFRunLoopSourceRef source; + event_tap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap, + kCGEventTapOptionListenOnly, MOUSE_EVENTS | TABLET_EVENTS, tablet_event_cb, NULL); + + if (!event_tap) + return NULL; + + source = CFMachPortCreateRunLoopSource(NULL, event_tap, 0); + if (!source) + { + CFRelease(event_tap); + return NULL; + } + + CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes); + CFRelease(source); + + CFRunLoopRun(); + + CFRelease(event_tap); + return NULL; +} + +void CDECL macdrv_start_tablet_monitor(void) +{ + /* Install a global monitor for relevant events. This ensures we can deliver + * the full device area and event frequency that the tablet offers. It's + * essential to run the monitor in its own thread, because CoreGraphics + * blocks event handling until it's done. Scheduling on a busy thread would + * cause the mouse to lag. */ + static pthread_t thread; + if (!thread) + { + if (pthread_create(&thread, NULL, macdrv_tablet_main, NULL) == 0) + pthread_detach(thread); + } +} diff --git a/dlls/winemac.drv/event.c b/dlls/winemac.drv/event.c index f197af0808e..22caf0cf2a7 100644 --- a/dlls/winemac.drv/event.c +++ b/dlls/winemac.drv/event.c @@ -308,6 +308,9 @@ void macdrv_handle_event(const macdrv_event *event) case WINDOW_RESTORE_REQUESTED: macdrv_window_restore_requested(hwnd, event); break; + case TABLET_EVENT: + macdrv_tablet_event(event); + break; default: TRACE(" ignoring\n"); break; diff --git a/dlls/winemac.drv/macdrv.h b/dlls/winemac.drv/macdrv.h index 4e4524722af..3eecb26a01e 100644 --- a/dlls/winemac.drv/macdrv.h +++ b/dlls/winemac.drv/macdrv.h @@ -232,6 +232,7 @@ extern DWORD CDECL macdrv_MsgWaitForMultipleObjectsEx(DWORD count, const HANDLE extern void macdrv_window_drag_begin(HWND hwnd, const macdrv_event *event) DECLSPEC_HIDDEN; extern void macdrv_window_drag_end(HWND hwnd) DECLSPEC_HIDDEN; extern void macdrv_reassert_window_position(HWND hwnd) DECLSPEC_HIDDEN; +extern void macdrv_tablet_event(const macdrv_event *event) DECLSPEC_HIDDEN; extern BOOL query_resize_size(HWND hwnd, macdrv_query *query) DECLSPEC_HIDDEN; extern BOOL query_resize_start(HWND hwnd) DECLSPEC_HIDDEN; extern BOOL query_min_max_info(HWND hwnd) DECLSPEC_HIDDEN; diff --git a/dlls/winemac.drv/macdrv_cocoa.h b/dlls/winemac.drv/macdrv_cocoa.h index 94f9fbcfa17..c7f87888fdc 100644 --- a/dlls/winemac.drv/macdrv_cocoa.h +++ b/dlls/winemac.drv/macdrv_cocoa.h @@ -351,6 +351,7 @@ extern int macdrv_set_display_mode(const struct macdrv_display* display, WINDOW_MINIMIZE_REQUESTED, WINDOW_RESIZE_ENDED, WINDOW_RESTORE_REQUESTED, + TABLET_EVENT, NUM_EVENT_TYPES };
@@ -361,6 +362,12 @@ extern int macdrv_set_display_mode(const struct macdrv_display* display, QUIT_REASON_SHUTDOWN, };
+enum { + TABLET_EVENT_POINT, + TABLET_EVENT_PROXIMITY_ENTER, + TABLET_EVENT_PROXIMITY_LEAVE, +}; + typedef uint64_t macdrv_event_mask;
typedef struct macdrv_event { @@ -455,6 +462,10 @@ extern int macdrv_set_display_mode(const struct macdrv_display* display, int keep_frame; CGRect frame; } window_restore_requested; + struct { + int type; + void *packet; + } tablet_event; }; } macdrv_event;
diff --git a/dlls/winemac.drv/wintab.c b/dlls/winemac.drv/wintab.c index bd6d6b783e0..11979afba59 100644 --- a/dlls/winemac.drv/wintab.c +++ b/dlls/winemac.drv/wintab.c @@ -38,6 +38,8 @@ static LOGCONTEXTW macdrv_system_ctx;
void* macdrv_tablet_window; static HWND macdrv_tablet_hwnd; +static PACKET current_packet; +static int last_event_type = -1;
static size_t registered_hwnd_count = 0; static size_t registered_hwnd_cap = 0; @@ -553,10 +555,80 @@ int CDECL macdrv_AttachEventQueueToTablet(HWND hOwner) if (registered_hwnd_count > 1) FIXME("Multiple contexts are not correctly supported. All events are delivered to all contexts.\n");
+ macdrv_start_tablet_monitor(); return 0; }
+/*********************************************************************** + * macdrv_tablet_event + * + * Handler for TABLET_EVENT events. + */ +void macdrv_tablet_event(const macdrv_event* event) +{ + int i; + PACKET* old_pkt = ¤t_packet; + PACKET* new_pkt = event->tablet_event.packet; + int event_type = event->tablet_event.type; + BOOL is_proximity = (event_type != TABLET_EVENT_POINT); + + if (is_proximity) + { + /* Wacom's documentation very confusingly states: + * > The high-order word is non-zero when the cursor is leaving or + * > entering hardware proximity. + * What it actually means is that the high-order word is 1 when entering + * proximity or 0 when leaving proximity. + * + * The low and high order words are always the same. The only way to + * enter/leave context proximity without entering/leaving device + * proximity is to have multiple contexts. Macdrv has no awareness of + * contexts so this would have to be handled by wintab.dll. */ + BOOL is_enter = (event_type == TABLET_EVENT_PROXIMITY_ENTER); + LPARAM l_param = MAKELPARAM(is_enter, is_enter); + + /* We can get duplicate events due to NSEventTypeMouseMoved + + * NSEventTypeTabletProximity both being delivered. Squash them here. */ + if (last_event_type == event_type) + return; + + last_event_type = event_type; + + for (i = 0; i < registered_hwnd_count; i++) + { + HWND hwnd = registered_hwnd[i]; + SendMessageW(macdrv_tablet_hwnd, WT_PROXIMITY, (WPARAM)hwnd, l_param); + } + } + else + { + last_event_type = TABLET_EVENT_POINT; + + /* Calculate which values changed */ + new_pkt->pkChanged = ((new_pkt->pkStatus != old_pkt->pkStatus) * PK_STATUS) + | ((new_pkt->pkTime != old_pkt->pkTime) * PK_TIME) + | PK_SERIAL_NUMBER + | ((new_pkt->pkButtons != old_pkt->pkButtons) * PK_BUTTONS) + | ((new_pkt->pkX != old_pkt->pkX) * PK_X) + | ((new_pkt->pkY != old_pkt->pkY) * PK_Y) + | ((new_pkt->pkZ != old_pkt->pkZ) * PK_Z) + | ((new_pkt->pkNormalPressure != old_pkt->pkNormalPressure) * PK_NORMAL_PRESSURE) + | ((new_pkt->pkTangentPressure != old_pkt->pkTangentPressure) * PK_TANGENT_PRESSURE) + | ((memcmp(&new_pkt->pkOrientation, &old_pkt->pkOrientation, sizeof(new_pkt->pkOrientation)) != 0) * PK_ORIENTATION); + + *old_pkt = *new_pkt; + free(new_pkt); + + for (i = 0; i < registered_hwnd_count; i++) + { + HWND hwnd = registered_hwnd[i]; + SendMessageW(macdrv_tablet_hwnd, WT_PACKET, (WPARAM)current_packet.pkSerialNumber, (LPARAM)hwnd); + } + } +} + int CDECL macdrv_GetCurrentPacket(LPPACKET packet) { - return 0; + *packet = current_packet; + return 1; }