fix: avoid unintentional trackpad trigger (closes #4278)
This commit is contained in:
1 parent
a06afd4bd2
commit
0539a3ad49
1 file changed
+132
-110
@@ -48,88 +48,73 @@ class TrackpadEvents {
|
||||
}
|
||||
|
||||
private static func touchEventHandler(_ cgEvent: CGEvent) -> Bool {
|
||||
guard let nsEvent = GestureDetector.convertEvent(cgEvent) else { return false }
|
||||
guard let nsEvent = convertEvent(cgEvent) else { return false } // don't absorb the touch event
|
||||
let touches = nsEvent.allTouches()
|
||||
// sometimes the os sends events with no touches; we ignore these as they could break our gesture logic
|
||||
if touches.count == 0 { return false }
|
||||
if touches.count == 0 {
|
||||
return false
|
||||
}
|
||||
let activeTouches = touches.filter { !$0.isResting && ($0.phase == .began || $0.phase == .moved || $0.phase == .stationary) }
|
||||
// Logger.error("---", "activeTouches:", activeTouches.count, "all:", touches.map { $0.phase.readable })
|
||||
let requiredFingers = Preferences.nextWindowGesture.isThreeFinger() ? 3 : 4
|
||||
// not enough fingers are down
|
||||
if (!App.app.appIsBeingUsed && activeTouches.count != requiredFingers) || (App.app.appIsBeingUsed && activeTouches.count < 2) {
|
||||
DispatchQueue.main.async { ScrollwheelEvents.toggle(false) }
|
||||
TriggerSwipeDetector.reset()
|
||||
NavigationSwipeDetector.reset()
|
||||
return GestureDetector.checkForFingersUp(activeTouches.count, requiredFingers)
|
||||
}
|
||||
// enough fingers are down
|
||||
// Logger.error("---", "activeTouches:", activeTouches.count, "all:", touches.map { $0.phase.readable }, requiredFingers)
|
||||
if App.app.appIsBeingUsed {
|
||||
DispatchQueue.main.async { ScrollwheelEvents.toggle(true) }
|
||||
if !GestureDetector.updateStartPositions(activeTouches, &NavigationSwipeDetector.startPositions) {
|
||||
if let r = NavigationSwipeDetector.check(activeTouches) { return r }
|
||||
handleEventIfAppIsBeingUsed(activeTouches, requiredFingers)
|
||||
return true // absorb the touch event
|
||||
}
|
||||
return handleEventIfAppIsNotBeingUsed(touches, 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()
|
||||
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 {
|
||||
NavigationSwipeDetector.hasDetected(activeTouches)
|
||||
} else {
|
||||
if !GestureDetector.updateStartPositions(activeTouches, &TriggerSwipeDetector.startPositions) {
|
||||
if let r = TriggerSwipeDetector.check(activeTouches) { return r }
|
||||
}
|
||||
// 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 {
|
||||
NonFreshGestureDetector.reset()
|
||||
return false // don't absorb the touch event
|
||||
}
|
||||
if NonFreshGestureDetector.hasDetected(activeTouches, requiredFingers) {
|
||||
return false // don't absorb the touch event
|
||||
}
|
||||
if activeTouches.count != requiredFingers {
|
||||
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 false
|
||||
}
|
||||
}
|
||||
|
||||
class GestureDetector {
|
||||
static func convertEvent(_ cgEvent: CGEvent) -> NSEvent? {
|
||||
var nsEvent: NSEvent?
|
||||
// conversion has to happen on the main-thread, or appkit will crash
|
||||
DispatchQueue.main.sync {
|
||||
nsEvent = NSEvent(cgEvent: cgEvent)
|
||||
}
|
||||
return nsEvent
|
||||
class NonFreshGestureDetector {
|
||||
private static var userHasDoneAnotherGesture = false
|
||||
private static var gestureTracker = GestureTracker()
|
||||
|
||||
/// if the user has already used a gesture-action (e.g. 2-finger scroll), we consider this "session" invalid, until all fingers are released
|
||||
/// This prevents: 4->3 trigger (System swipe already happened), or 2->3 trigger (System scroll already happened)
|
||||
static func hasDetected(_ activeTouches: Set<NSTouch>,_ requiredFingers: Int) -> Bool {
|
||||
guard !userHasDoneAnotherGesture else { return true }
|
||||
let new = gestureTracker.isNewGesture(activeTouches)
|
||||
guard activeTouches.count != requiredFingers && !new else { return false }
|
||||
let distances = gestureTracker.computeDistance(activeTouches)
|
||||
userHasDoneAnotherGesture = distances.contains(where: { abs($0.x) >= TriggerSwipeDetector.MIN_SWIPE_DISTANCE || abs($0.y) >= TriggerSwipeDetector.MIN_SWIPE_DISTANCE })
|
||||
return userHasDoneAnotherGesture
|
||||
}
|
||||
|
||||
static func checkForFingersUp(_ fingersDown: Int, _ requiredFingers: Int) -> Bool {
|
||||
if App.app.appIsBeingUsed && fingersDown < requiredFingers && App.app.shortcutIndex == Preferences.gestureIndex
|
||||
&& !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease {
|
||||
DispatchQueue.main.async { App.app.focusTarget() }
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func updateStartPositions(_ activeTouches: Set<NSTouch>, _ startPositions: inout [String: NSPoint]) -> Bool {
|
||||
// if touches are new, record their startPositions
|
||||
if (activeTouches.contains { startPositions["\($0.identity)"] == nil }) {
|
||||
for touch in activeTouches {
|
||||
startPositions["\(touch.identity)"] = touch.normalizedPosition
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func computeAverageDistance(_ activeTouches: Set<NSTouch>, _ startPositions: [String: NSPoint]) -> NSPoint {
|
||||
var totalDelta = NSPoint(x: 0, y: 0)
|
||||
for touch in activeTouches {
|
||||
totalDelta = totalDelta + (touch.normalizedPosition - startPositions["\(touch.identity)"]!)
|
||||
}
|
||||
return totalDelta / activeTouches.count
|
||||
}
|
||||
|
||||
static func computeDistance(_ activeTouches: Set<NSTouch>, _ startPositions: [String: NSPoint]) -> Array<NSPoint> {
|
||||
var deltas: Array<NSPoint> = []
|
||||
for touch in activeTouches {
|
||||
deltas.append(touch.normalizedPosition - startPositions["\(touch.identity)"]!)
|
||||
}
|
||||
return deltas
|
||||
}
|
||||
|
||||
static func blockOngoingScrolling() {
|
||||
if #available(macOS 10.13, *) {
|
||||
// simulate mouseWheel-stopped to end potential existing scrolling started before AltTab was opened
|
||||
// this avoid the swipe to trigger a scroll on the active window and show AltTab, at the same time
|
||||
CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 1, wheel1: 0, wheel2: 0, wheel3: 0)?
|
||||
.post(tap: .cghidEventTap)
|
||||
}
|
||||
static func reset() {
|
||||
userHasDoneAnotherGesture = false
|
||||
gestureTracker.reset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,37 +126,31 @@ class TriggerSwipeDetector {
|
||||
// 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
|
||||
|
||||
static var startPositions: [String: NSPoint] = [:]
|
||||
static var swipeStillPossible = true
|
||||
private static var gestureTracker = GestureTracker()
|
||||
private static var swipeStillPossible = true
|
||||
|
||||
static func check(_ activeTouches: Set<NSTouch>) -> Bool? {
|
||||
if !App.app.appIsBeingUsed && swipeStillPossible {
|
||||
let distances = GestureDetector.computeDistance(activeTouches, startPositions)
|
||||
for distance in distances {
|
||||
let (absX, absY) = (abs(distance.x), abs(distance.y))
|
||||
let horizontal = Preferences.nextWindowGesture.isHorizontal()
|
||||
if (!(updateSwipeStillPossible(horizontal ? absY : absX) && (horizontal ? absX : absY) >= MIN_SWIPE_DISTANCE )) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
reset()
|
||||
DispatchQueue.main.async {
|
||||
ScrollwheelEvents.toggle(true)
|
||||
performHapticFeedback()
|
||||
App.app.showUiOrCycleSelection(Preferences.gestureIndex, false)
|
||||
}
|
||||
return true
|
||||
static func hasDetected(_ activeTouches: Set<NSTouch>) -> Bool {
|
||||
guard swipeStillPossible && !gestureTracker.isNewGesture(activeTouches) else { return false }
|
||||
let distances = gestureTracker.computeDistance(activeTouches)
|
||||
for distance in distances {
|
||||
let (absX, absY) = (abs(distance.x), abs(distance.y))
|
||||
let horizontal = Preferences.nextWindowGesture.isHorizontal()
|
||||
let distanceInRightDirection = horizontal ? absX : absY
|
||||
let distanceInWrongDirection = horizontal ? absY : absX
|
||||
swipeStillPossible = distanceInWrongDirection < MAX_SWIPE_DISTANCE_IN_WRONG_DIRECTION
|
||||
guard swipeStillPossible && distanceInRightDirection >= MIN_SWIPE_DISTANCE else { return false }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func updateSwipeStillPossible(_ distanceInWrongDirection: Double) -> Bool {
|
||||
swipeStillPossible = distanceInWrongDirection < MAX_SWIPE_DISTANCE_IN_WRONG_DIRECTION
|
||||
return swipeStillPossible
|
||||
reset()
|
||||
DispatchQueue.main.async {
|
||||
ScrollwheelEvents.toggle(true)
|
||||
performHapticFeedback()
|
||||
App.app.showUiOrCycleSelection(Preferences.gestureIndex, false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func reset() {
|
||||
startPositions.removeAll(keepingCapacity: true)
|
||||
gestureTracker.reset()
|
||||
swipeStillPossible = true
|
||||
}
|
||||
}
|
||||
@@ -179,43 +158,86 @@ class TriggerSwipeDetector {
|
||||
class NavigationSwipeDetector {
|
||||
static let MIN_SWIPE_DISTANCE: Double = 0.03 // % of trackpad surface traveled
|
||||
|
||||
static var startPositions: [String: NSPoint] = [:]
|
||||
private static var gestureTracker = GestureTracker()
|
||||
|
||||
static func check(_ activeTouches: Set<NSTouch>) -> Bool? {
|
||||
let averageDistance = GestureDetector.computeAverageDistance(activeTouches, startPositions)
|
||||
static func hasDetected(_ activeTouches: Set<NSTouch>) {
|
||||
guard !gestureTracker.isNewGesture(activeTouches) else { return }
|
||||
let averageDistance = gestureTracker.computeAverageDistance(activeTouches)
|
||||
let (absX, absY) = (abs(averageDistance.x), abs(averageDistance.y))
|
||||
let maxIsX = absX >= absY
|
||||
if (maxIsX ? absX : absY) > MIN_SWIPE_DISTANCE {
|
||||
maxIsX ? resetX(activeTouches) : resetY(activeTouches)
|
||||
let direction: Direction = maxIsX ? (averageDistance.x < 0 ? .left : .right) : (averageDistance.y < 0 ? .down : .up)
|
||||
DispatchQueue.main.async {
|
||||
performHapticFeedback()
|
||||
App.app.cycleSelection(direction, allowWrap: false)
|
||||
}
|
||||
return true
|
||||
guard (maxIsX ? absX : absY) > MIN_SWIPE_DISTANCE else { return }
|
||||
maxIsX ? gestureTracker.resetX(activeTouches) : gestureTracker.resetY(activeTouches)
|
||||
let direction: Direction = maxIsX ? (averageDistance.x < 0 ? .left : .right) : (averageDistance.y < 0 ? .down : .up)
|
||||
DispatchQueue.main.async {
|
||||
performHapticFeedback()
|
||||
App.app.cycleSelection(direction, allowWrap: false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func reset() {
|
||||
gestureTracker.reset()
|
||||
}
|
||||
}
|
||||
|
||||
class GestureTracker {
|
||||
var startPositions = [String: NSPoint]()
|
||||
|
||||
@discardableResult
|
||||
func isNewGesture(_ activeTouches: Set<NSTouch>) -> Bool {
|
||||
// if touches are new, record their startPositions
|
||||
if (activeTouches.contains { startPositions["\($0.identity)"] == nil }) {
|
||||
for touch in activeTouches {
|
||||
startPositions["\(touch.identity)"] = touch.normalizedPosition
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func computeAverageDistance(_ activeTouches: Set<NSTouch>) -> NSPoint {
|
||||
var totalDelta = NSPoint(x: 0, y: 0)
|
||||
for touch in activeTouches {
|
||||
totalDelta = totalDelta + (touch.normalizedPosition - startPositions["\(touch.identity)"]!)
|
||||
}
|
||||
return totalDelta / activeTouches.count
|
||||
}
|
||||
|
||||
func computeDistance(_ activeTouches: Set<NSTouch>) -> Array<NSPoint> {
|
||||
var deltas: Array<NSPoint> = []
|
||||
for touch in activeTouches {
|
||||
deltas.append(touch.normalizedPosition - startPositions["\(touch.identity)"]!)
|
||||
}
|
||||
return deltas
|
||||
}
|
||||
|
||||
func reset() {
|
||||
startPositions.removeAll(keepingCapacity: true)
|
||||
}
|
||||
|
||||
static func resetX(_ activeTouches: Set<NSTouch>) {
|
||||
func resetX(_ activeTouches: Set<NSTouch>) {
|
||||
for touch in activeTouches {
|
||||
startPositions["\(touch.identity)"]!.x = touch.normalizedPosition.x
|
||||
}
|
||||
}
|
||||
|
||||
static func resetY(_ activeTouches: Set<NSTouch>) {
|
||||
func resetY(_ activeTouches: Set<NSTouch>) {
|
||||
for touch in activeTouches {
|
||||
startPositions["\(touch.identity)"]!.y = touch.normalizedPosition.y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performHapticFeedback() {
|
||||
fileprivate func performHapticFeedback() {
|
||||
if Preferences.trackpadHapticFeedbackEnabled {
|
||||
NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .default)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func convertEvent(_ cgEvent: CGEvent) -> NSEvent? {
|
||||
var nsEvent: NSEvent?
|
||||
// conversion has to happen on the main-thread, or appkit will crash
|
||||
DispatchQueue.main.sync {
|
||||
nsEvent = NSEvent(cgEvent: cgEvent)
|
||||
}
|
||||
return nsEvent
|
||||
}
|
||||
Reference in new issue
Block a user