From 772131bf4c0b1efa27228d9eb699c1387a440a05 Mon Sep 17 00:00:00 2001 From: NullPointerDepressiveDisorder <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:10:43 -0800 Subject: [PATCH 1/2] Enhance mouse event handling for middle mouse button - Set isMiddleMouseDown synchronously in startDrag, endDrag, and cancelDrag methods to prevent race conditions during rapid drag cycles. - Ensure proper state management by synchronously updating isMiddleMouseDown before dispatching asynchronous tasks. - Update MenuBarController to ensure UI mutations occur on the main actor, improving thread safety for UI updates. --- MiddleDrag/Core/MouseEventGenerator.swift | 21 +++++++++++++++++---- MiddleDrag/UI/MenuBarController.swift | 6 +++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/MiddleDrag/Core/MouseEventGenerator.swift b/MiddleDrag/Core/MouseEventGenerator.swift index 97f6e8f..fb00fdd 100644 --- a/MiddleDrag/Core/MouseEventGenerator.swift +++ b/MiddleDrag/Core/MouseEventGenerator.swift @@ -63,10 +63,15 @@ final class MouseEventGenerator: @unchecked Sendable { previousDeltaX = 0 previousDeltaY = 0 + // CRITICAL: Set isMiddleMouseDown SYNCHRONOUSLY before dispatching + // This prevents a race condition where endDrag() could be called before + // the async block runs, causing endDrag() to see isMiddleMouseDown=false + // and skip sending the mouse up event, leaving the button stuck down. + isMiddleMouseDown = true + // Now do the async part for sending the mouse down event eventQueue.async { [weak self] in guard let self = self else { return } - self.isMiddleMouseDown = true self.sendMiddleMouseDown(at: quartzPos) } } @@ -211,10 +216,14 @@ final class MouseEventGenerator: @unchecked Sendable { func endDrag() { guard isMiddleMouseDown else { return } + // CRITICAL: Set isMiddleMouseDown = false SYNCHRONOUSLY to match startDrag + // This prevents race conditions with rapid start/end cycles and ensures + // updateDrag() stops processing immediately + isMiddleMouseDown = false + eventQueue.async { [weak self] in guard let self = self else { return } - self.isMiddleMouseDown = false // Reset smoothing state self.previousDeltaX = 0 self.previousDeltaY = 0 @@ -289,12 +298,16 @@ final class MouseEventGenerator: @unchecked Sendable { func cancelDrag() { guard isMiddleMouseDown else { return } - // Asynchronously end the drag - this won't block the event queue + // CRITICAL: Set isMiddleMouseDown = false SYNCHRONOUSLY to match startDrag + // This prevents race conditions with rapid cancel/start cycles and ensures + // updateDrag() stops processing immediately + isMiddleMouseDown = false + + // Asynchronously send the mouse up event and clean up state // The cleanup will happen on the event queue, ensuring proper sequencing // with other operations like performClick() eventQueue.async { [weak self] in guard let self = self else { return } - self.isMiddleMouseDown = false // Reset smoothing state self.previousDeltaX = 0 self.previousDeltaY = 0 diff --git a/MiddleDrag/UI/MenuBarController.swift b/MiddleDrag/UI/MenuBarController.swift index b5c3038..ccc4d06 100644 --- a/MiddleDrag/UI/MenuBarController.swift +++ b/MiddleDrag/UI/MenuBarController.swift @@ -52,7 +52,10 @@ class MenuBarController: NSObject { context.duration = 0.2 button.animator().alphaValue = 0.7 }, completionHandler: { - button.alphaValue = 1.0 + // Ensure mutation happens on the main actor + Task { @MainActor in + button.alphaValue = 1.0 + } }) } @@ -642,3 +645,4 @@ extension Notification.Name { static let preferencesChanged = Notification.Name("MiddleDragPreferencesChanged") static let launchAtLoginChanged = Notification.Name("MiddleDragLaunchAtLoginChanged") } + From b7abe3575c33766b0d01867b40946ddd12241c46 Mon Sep 17 00:00:00 2001 From: NullPointerDepressiveDisorder <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:18:55 -0800 Subject: [PATCH 2/2] Refactor mouse event handling to eliminate race conditions - Updated the MouseEventGenerator to set the isMiddleMouseDown flag and send the mouse-down event synchronously, preventing potential race conditions during drag operations. - Improved comments to clarify the critical nature of synchronous execution for both flag setting and event dispatching. --- MiddleDrag/Core/MouseEventGenerator.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/MiddleDrag/Core/MouseEventGenerator.swift b/MiddleDrag/Core/MouseEventGenerator.swift index fb00fdd..6584abb 100644 --- a/MiddleDrag/Core/MouseEventGenerator.swift +++ b/MiddleDrag/Core/MouseEventGenerator.swift @@ -63,17 +63,15 @@ final class MouseEventGenerator: @unchecked Sendable { previousDeltaX = 0 previousDeltaY = 0 - // CRITICAL: Set isMiddleMouseDown SYNCHRONOUSLY before dispatching - // This prevents a race condition where endDrag() could be called before - // the async block runs, causing endDrag() to see isMiddleMouseDown=false - // and skip sending the mouse up event, leaving the button stuck down. + // CRITICAL: Both flag AND mouse-down event must be set/sent SYNCHRONOUSLY. + // This prevents two race conditions: + // 1. endDrag() seeing isMiddleMouseDown=false (original sticky bug) + // 2. updateDrag() sending drag events before mouse-down reaches macOS + // + // sendMiddleMouseDown() just creates and posts a CGEvent, which is thread-safe + // and takes microseconds. No need for async dispatch here. isMiddleMouseDown = true - - // Now do the async part for sending the mouse down event - eventQueue.async { [weak self] in - guard let self = self else { return } - self.sendMiddleMouseDown(at: quartzPos) - } + sendMiddleMouseDown(at: quartzPos) } /// Magic number to identify our own events (0x4D44 = 'MD')