Skip to content

Commit 1689954

Browse files
committed
fix: better gesture recognition (closes #5039)
1 parent e477f8b commit 1689954

File tree

6 files changed

+89
-18
lines changed

6 files changed

+89
-18
lines changed

alt-tab-macos.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
BF0C8A3F32B2177AF407DC7E /* DockEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C870C14C20C936BA4AF40 /* DockEvents.swift */; };
153153
BF0C8A408B8E1C662E457A99 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C86C535CC75FA07FAA09B /* InfoPlist.strings */; };
154154
BF0C8A49891371E4027E0641 /* MacroPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8998862E0E04D60CACBC /* MacroPreferences.swift */; };
155+
BF0C8A5FA46C5E74A071FF4C /* ScrollwheelEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */; };
155156
BF0C8A739A74695E60F16369 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8062E78677398A4A217B /* InfoPlist.strings */; };
156157
BF0C8A95AAD9DADC95887A2D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C895A9AF79A869EE6B108 /* Localizable.strings */; };
157158
BF0C8AC57CE5E9BBEEE2442A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C885D34CDCE2AFC5A94F7 /* InfoPlist.strings */; };
@@ -524,6 +525,7 @@
524525
BF0C8EC400A560FDF99F8C35 /* el */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = el; path = Localizable.strings; sourceTree = "<group>"; };
525526
BF0C8EE17F3ABD7A6D44BFFE /* pl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = pl; path = Localizable.strings; sourceTree = "<group>"; };
526527
BF0C8F58B38F29B2662E6C2D /* PreferencesMigrations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesMigrations.swift; sourceTree = "<group>"; };
528+
BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollwheelEvents.swift; sourceTree = "<group>"; };
527529
BF0C8FB9777EE9BE74EF542B /* zh-hk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = "zh-hk"; path = Localizable.strings; sourceTree = "<group>"; };
528530
BF0C8FC083A2443F3EAF5DB7 /* preferences-blacklist.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "preferences-blacklist.jpg"; sourceTree = "<group>"; };
529531
BF0C8FEE46E9A73F617EF679 /* gu */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = gu; path = Localizable.strings; sourceTree = "<group>"; };
@@ -1890,6 +1892,7 @@
18901892
BF0C8B5D1A9658E64F8ECB56 /* SystemScrollerStyleEvents.swift */,
18911893
BF0C8643F524515224BF1618 /* KeyboardEventsTestable.swift */,
18921894
BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */,
1895+
BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */,
18931896
);
18941897
path = events;
18951898
sourceTree = "<group>";
@@ -2466,6 +2469,7 @@
24662469
BF0C826C7F8C5F68732FD75B /* DebugMenu.swift in Sources */,
24672470
BF0C8E5F4E4D5F7DAEE07C20 /* ThumbnailsPanelBackgroundView.swift in Sources */,
24682471
BF0C8AD478399D2E97AE3FB4 /* CustomRecorderControlTestable.swift in Sources */,
2472+
BF0C8A5FA46C5E74A071FF4C /* ScrollwheelEvents.swift in Sources */,
24692473
);
24702474
runOnlyForDeploymentPostprocessing = 0;
24712475
};

src/api-wrappers/HelperExtensions.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,16 @@ extension NSRunningApplication {
321321
// 250ms is similar to human delay in processing changes on screen
322322
// See https://humanbenchmark.com/tests/reactiontime
323323
let humanPerceptionDelay = DispatchTimeInterval.milliseconds(250)
324+
325+
extension NSTouch.Phase {
326+
var readable: String {
327+
switch self {
328+
case .began: "began"
329+
case .moved: "moved"
330+
case .stationary: "stationary"
331+
case .ended: "ended"
332+
case .cancelled: "cancelled"
333+
default: "unknown"
334+
}
335+
}
336+
}

src/logic/events/MouseEvents.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class MouseEvents {
1010
}
1111

1212
static func toggle(_ enabled: Bool) {
13+
guard enabled != shouldBeEnabled else { return }
1314
shouldBeEnabled = enabled
1415
if let eventTap {
1516
CGEvent.tapEnable(tap: eventTap, enable: enabled)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Cocoa
2+
3+
fileprivate var eventTap: CFMachPort!
4+
fileprivate var shouldBeEnabled: Bool!
5+
6+
class ScrollwheelEvents {
7+
static func observe() {
8+
observe_()
9+
toggle(false)
10+
}
11+
12+
static func toggle(_ enabled: Bool) {
13+
guard enabled != shouldBeEnabled else { return }
14+
shouldBeEnabled = enabled
15+
if let eventTap {
16+
CGEvent.tapEnable(tap: eventTap, enable: enabled)
17+
}
18+
}
19+
}
20+
21+
private func observe_() {
22+
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
23+
eventTap = CGEvent.tapCreate(
24+
tap: .cghidEventTap, // we need raw data
25+
place: .headInsertEventTap,
26+
options: .defaultTap,
27+
eventsOfInterest: NSEvent.EventTypeMask.scrollWheel.rawValue,
28+
callback: handleEvent,
29+
userInfo: nil)
30+
if let eventTap {
31+
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
32+
CFRunLoopAddSource(BackgroundWork.keyboardAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
33+
} else {
34+
App.app.restart()
35+
}
36+
}
37+
38+
private let handleEvent: CGEventTapCallBack = { _, type, cgEvent, _ in
39+
if type.rawValue == NSEvent.EventType.scrollWheel.rawValue {
40+
// block scrolling globally
41+
return nil
42+
} else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) && shouldBeEnabled {
43+
CGEvent.tapEnable(tap: eventTap!, enable: true)
44+
}
45+
return Unmanaged.passUnretained(cgEvent) // focused app will receive the event
46+
}

src/logic/events/TrackpadEvents.swift

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ class TrackpadEvents {
88
static func observe() {
99
observe_()
1010
TrackpadEvents.toggle(Preferences.nextWindowGesture != .disabled)
11+
ScrollwheelEvents.observe()
1112
}
1213

1314
static func toggle(_ enabled: Bool) {
15+
guard enabled != shouldBeEnabled else { return }
1416
shouldBeEnabled = enabled
1517
if let eventTap {
1618
CGEvent.tapEnable(tap: eventTap, enable: enabled)
@@ -48,27 +50,28 @@ private let handleEvent: CGEventTapCallBack = { _, type, cgEvent, _ in
4850

4951
private func touchEventHandler(_ cgEvent: CGEvent) -> Bool {
5052
guard let nsEvent = GestureDetector.convertEvent(cgEvent) else { return false }
51-
// if the finger count doesn't match, we reset tracking data, and may trigger fingersUp
5253
let touches = nsEvent.allTouches()
53-
if touches.count == 0 { return false } // sometimes the os sends events with no touches
54+
// sometimes the os sends events with no touches; we ignore these as they could break our gesture logic
55+
if touches.count == 0 { return false }
5456
let activeTouches = touches.filter { !$0.isResting && ($0.phase == .began || $0.phase == .moved || $0.phase == .stationary) }
57+
// Logger.error("---", "activeTouches:", activeTouches.count, "all:", touches.map { $0.phase.readable })
5558
let requiredFingers = Preferences.nextWindowGesture.isThreeFinger() ? 3 : 4
56-
if (!App.app.appIsBeingUsed && touches.count != requiredFingers) || (App.app.appIsBeingUsed && touches.count < 2) {
59+
// not enough fingers are down
60+
if (!App.app.appIsBeingUsed && activeTouches.count != requiredFingers) || (App.app.appIsBeingUsed && activeTouches.count < 2) {
61+
DispatchQueue.main.async { ScrollwheelEvents.toggle(false) }
5762
TriggerSwipeDetector.reset()
5863
NavigationSwipeDetector.reset()
59-
return GestureDetector.checkForFingersUp(activeTouches, requiredFingers)
64+
return GestureDetector.checkForFingersUp(activeTouches.count, requiredFingers)
6065
}
61-
// when the native using 3-finger swipe to shift Space, macOS will block scrolling in the background
62-
// We imitate this behavior by sending a synthetic scrollWheel event
63-
GestureDetector.blockOngoingScrolling()
64-
// trigger actions if conditions are met
66+
// enough fingers are down
6567
if App.app.appIsBeingUsed {
66-
if !GestureDetector.updateStartPositions(touches, &NavigationSwipeDetector.startPositions) {
67-
if let r = NavigationSwipeDetector.check(touches) { return r }
68+
DispatchQueue.main.async { ScrollwheelEvents.toggle(true) }
69+
if !GestureDetector.updateStartPositions(activeTouches, &NavigationSwipeDetector.startPositions) {
70+
if let r = NavigationSwipeDetector.check(activeTouches) { return r }
6871
}
6972
} else {
70-
if !GestureDetector.updateStartPositions(touches, &TriggerSwipeDetector.startPositions) {
71-
if let r = TriggerSwipeDetector.check(touches) { return r }
73+
if !GestureDetector.updateStartPositions(activeTouches, &TriggerSwipeDetector.startPositions) {
74+
if let r = TriggerSwipeDetector.check(activeTouches) { return r }
7275
}
7376
}
7477
return false
@@ -84,8 +87,8 @@ class GestureDetector {
8487
return nsEvent
8588
}
8689

87-
static func checkForFingersUp(_ touches: Set<NSTouch>, _ requiredFingers: Int) -> Bool {
88-
if App.app.appIsBeingUsed && touches.count < requiredFingers && App.app.shortcutIndex == Preferences.gestureIndex
90+
static func checkForFingersUp(_ fingersDown: Int, _ requiredFingers: Int) -> Bool {
91+
if App.app.appIsBeingUsed && fingersDown < requiredFingers && App.app.shortcutIndex == Preferences.gestureIndex
8992
&& !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease {
9093
DispatchQueue.main.async { App.app.focusTarget() }
9194
return true
@@ -152,7 +155,10 @@ class TriggerSwipeDetector {
152155
}
153156
}
154157
reset()
155-
DispatchQueue.main.async { App.app.showUiOrCycleSelection(Preferences.gestureIndex, false) }
158+
DispatchQueue.main.async {
159+
ScrollwheelEvents.toggle(true)
160+
App.app.showUiOrCycleSelection(Preferences.gestureIndex, false)
161+
}
156162
return true
157163
}
158164
return nil
@@ -174,12 +180,12 @@ class NavigationSwipeDetector {
174180

175181
static var startPositions: [String: NSPoint] = [:]
176182

177-
static func check(_ touches: Set<NSTouch>) -> Bool? {
178-
let averageDistance = GestureDetector.computeAverageDistance(touches, startPositions)
183+
static func check(_ activeTouches: Set<NSTouch>) -> Bool? {
184+
let averageDistance = GestureDetector.computeAverageDistance(activeTouches, startPositions)
179185
let (absX, absY) = (abs(averageDistance.x), abs(averageDistance.y))
180186
let maxIsX = absX >= absY
181187
if (maxIsX ? absX : absY) > MIN_SWIPE_DISTANCE {
182-
maxIsX ? resetX(touches) : resetY(touches)
188+
maxIsX ? resetX(activeTouches) : resetY(activeTouches)
183189
let direction: Direction = maxIsX ? (averageDistance.x < 0 ? .left : .right) : (averageDistance.y < 0 ? .down : .up)
184190
DispatchQueue.main.async { App.app.cycleSelection(direction, allowWrap: false) }
185191
return true

src/ui/App.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class App: AppCenterApplication {
6262
isFirstSummon = true
6363
forceDoNothingOnRelease = false
6464
MouseEvents.toggle(false)
65+
ScrollwheelEvents.toggle(false)
6566
hideThumbnailPanelWithoutChangingKeyWindow()
6667
if !keepPreview {
6768
previewPanel.orderOut(nil)

0 commit comments

Comments
 (0)