diff --git a/src/logic/events/TrackpadEvents.swift b/src/logic/events/TrackpadEvents.swift index 61664b61..7fb92109 100644 --- a/src/logic/events/TrackpadEvents.swift +++ b/src/logic/events/TrackpadEvents.swift @@ -20,6 +20,14 @@ class TrackpadEvents { } } + static func reset() { + ScrollwheelEvents.toggle(false) + NavigationSwipeDetector.reset() + NonFreshGestureDetector.reset() + // no need to call TriggerSwipeDetector.reset; it does it itself when triggering + TriggerSwipeDetector.maxFingersDownDuringTrigger = 0 + } + private static func observe_() { // CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass eventTap = CGEvent.tapCreate( @@ -51,39 +59,40 @@ class TrackpadEvents { private static func touchEventHandler(_ cgEvent: CGEvent) -> Bool { guard let nsEvent = cgEvent.toNSEvent() else { return false } // don't absorb the touch event let touches = nsEvent.allTouches() - // Logger.error { (touches.count, touches.map { $0.phase.readable}) } - // sometimes the os sends events with no touches; we ignore these as they could break our gesture logic - if touches.count == 0 { - return false - } - let activeTouches = touches.filter { !$0.isResting && ($0.phase == .began || $0.phase == .moved || $0.phase == .stationary) } + // Logger.error { (touches.count, touches.map { $0.phase.readable }) } + // macOS often sends faulty events with no touches between valid events; we ignore these as they would break our gesture logic + guard touches.count > 0 else { return false } + // isResting seems to always return false. It's not doing its job to detect resting thumb/palm/finger + // on macOS, the finger contact surface is not exposed in NSTouch, so we can't detect big contact == palm, for example + // the closest thing we can do to detect resting inputs is remove touches which have .phase == .stationary + let activeTouches = touches.filter { !$0.isResting && ($0.phase == .began || $0.phase == .moved) } + let fingersDown = touches.count { $0.phase == .began || $0.phase == .moved || $0.phase == .stationary } let requiredFingers = Preferences.nextWindowGesture.isThreeFinger() ? 3 : 4 if App.app.appIsBeingUsed { - handleEventIfAppIsBeingUsed(activeTouches, requiredFingers) + handleEventIfAppIsBeingUsed(fingersDown, activeTouches, requiredFingers) return true // absorb the touch event } - return handleEventIfAppIsNotBeingUsed(touches, activeTouches, requiredFingers) // absorb or not the touch event, depending on the situation + return handleEventIfAppIsNotBeingUsed(fingersDown, activeTouches, requiredFingers) // absorb or not the touch event, depending on the situation } - private static func handleEventIfAppIsBeingUsed(_ activeTouches: Set, _ requiredFingers: Int) { - if activeTouches.count == 0 { - NavigationSwipeDetector.reset() + private static func handleEventIfAppIsBeingUsed(_ fingersDown: Int, _ activeTouches: Set, _ requiredFingers: Int) { + if fingersDown <= TriggerSwipeDetector.maxFingersDownDuringTrigger - requiredFingers { if App.app.shortcutIndex == Preferences.gestureIndex && !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease { DispatchQueue.main.async { - ScrollwheelEvents.toggle(false) App.app.focusTarget() } } - } else if activeTouches.count > 1 { + return + } + if activeTouches.count > 1 { CursorEvents.deadZoneInitialPosition = nil NavigationSwipeDetector.hasDetected(activeTouches) - } else { - // if activeTouches.count == 1, ignore (finger is in pointer-mode) } + // if activeTouches.count == 1, ignore (finger is in pointer-mode) } - private static func handleEventIfAppIsNotBeingUsed(_ touches: Set, _ activeTouches: Set, _ requiredFingers: Int) -> Bool { - if touches.count <= 1 { + private static func handleEventIfAppIsNotBeingUsed(_ fingersDown: Int, _ activeTouches: Set, _ requiredFingers: Int) -> Bool { + if fingersDown <= 1 { NonFreshGestureDetector.reset() return false // don't absorb the touch event } @@ -94,7 +103,7 @@ class TrackpadEvents { TriggerSwipeDetector.reset() return false // don't absorb the touch event } else { - return TriggerSwipeDetector.hasDetected(activeTouches) // absorb or not the touch event, depending on the situation + return TriggerSwipeDetector.hasDetected(fingersDown, activeTouches) // absorb or not the touch event, depending on the situation } } } @@ -127,12 +136,15 @@ class TriggerSwipeDetector { // when the native using 3-finger swipe to shift Space, macOS will prevent a swipe until the fingers are raised, // if the user moves too much in the vertical direction. We imitate this behavior static let MAX_SWIPE_DISTANCE_IN_WRONG_DIRECTION: Double = 0.1 // % of trackpad surface traveled + // if there are extra non-active fingers during the gesture trigger, we remember it so we can ignore those extra fingers to detect intent to focus on release + static var maxFingersDownDuringTrigger = 0 private static var gestureTracker = GestureTracker() private static var swipeStillPossible = true - static func hasDetected(_ activeTouches: Set) -> Bool { + static func hasDetected(_ fingersDown: Int, _ activeTouches: Set) -> Bool { guard swipeStillPossible && !gestureTracker.isNewGesture(activeTouches) else { return false } + maxFingersDownDuringTrigger = max(maxFingersDownDuringTrigger, fingersDown) let distances = gestureTracker.computeDistance(activeTouches) for distance in distances { let (absX, absY) = (abs(distance.x), abs(distance.y)) diff --git a/src/ui/App.swift b/src/ui/App.swift index efce462c..1f824014 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -64,7 +64,7 @@ class App: AppCenterApplication { forceDoNothingOnRelease = false MouseEvents.toggle(false) CursorEvents.toggle(false) - ScrollwheelEvents.toggle(false) + TrackpadEvents.reset() hideThumbnailPanelWithoutChangingKeyWindow() if !keepPreview { previewPanel.orderOut(nil)