fix: better handle palm/fingers resting on the trackpad (closes #5191)

This commit is contained in:
lwouis committed 2026-01-15 00:08:34 +01:00
1 parent a0c9feefca
commit 9ed8a199b0
2 files changed
+32 -20

No files matched your search

+31 -19
View File
@@ -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<NSTouch>, _ requiredFingers: Int) {
if activeTouches.count == 0 {
NavigationSwipeDetector.reset()
private static func handleEventIfAppIsBeingUsed(_ fingersDown: Int, _ activeTouches: Set<NSTouch>, _ 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<NSTouch>, _ activeTouches: Set<NSTouch>, _ requiredFingers: Int) -> Bool {
if touches.count <= 1 {
private static func handleEventIfAppIsNotBeingUsed(_ fingersDown: Int, _ activeTouches: Set<NSTouch>, _ 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<NSTouch>) -> Bool {
static func hasDetected(_ fingersDown: Int, _ activeTouches: Set<NSTouch>) -> 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))
+1 -1
View File
@@ -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)