fix: better gesture recognition (closes #5039)
This commit is contained in:
1 parent
e477f8b3da
commit
168995493f
6 files changed
+89
-18
No files matched your search
@@ -152,6 +152,7 @@
|
||||
BF0C8A3F32B2177AF407DC7E /* DockEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C870C14C20C936BA4AF40 /* DockEvents.swift */; };
|
||||
BF0C8A408B8E1C662E457A99 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C86C535CC75FA07FAA09B /* InfoPlist.strings */; };
|
||||
BF0C8A49891371E4027E0641 /* MacroPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8998862E0E04D60CACBC /* MacroPreferences.swift */; };
|
||||
BF0C8A5FA46C5E74A071FF4C /* ScrollwheelEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */; };
|
||||
BF0C8A739A74695E60F16369 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8062E78677398A4A217B /* InfoPlist.strings */; };
|
||||
BF0C8A95AAD9DADC95887A2D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C895A9AF79A869EE6B108 /* Localizable.strings */; };
|
||||
BF0C8AC57CE5E9BBEEE2442A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C885D34CDCE2AFC5A94F7 /* InfoPlist.strings */; };
|
||||
@@ -524,6 +525,7 @@
|
||||
BF0C8EC400A560FDF99F8C35 /* el */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = el; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
BF0C8EE17F3ABD7A6D44BFFE /* pl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = pl; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
BF0C8F58B38F29B2662E6C2D /* PreferencesMigrations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesMigrations.swift; sourceTree = "<group>"; };
|
||||
BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollwheelEvents.swift; sourceTree = "<group>"; };
|
||||
BF0C8FB9777EE9BE74EF542B /* zh-hk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = "zh-hk"; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
BF0C8FC083A2443F3EAF5DB7 /* preferences-blacklist.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "preferences-blacklist.jpg"; sourceTree = "<group>"; };
|
||||
BF0C8FEE46E9A73F617EF679 /* gu */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = gu; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -1890,6 +1892,7 @@
|
||||
BF0C8B5D1A9658E64F8ECB56 /* SystemScrollerStyleEvents.swift */,
|
||||
BF0C8643F524515224BF1618 /* KeyboardEventsTestable.swift */,
|
||||
BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */,
|
||||
BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */,
|
||||
);
|
||||
path = events;
|
||||
sourceTree = "<group>";
|
||||
@@ -2466,6 +2469,7 @@
|
||||
BF0C826C7F8C5F68732FD75B /* DebugMenu.swift in Sources */,
|
||||
BF0C8E5F4E4D5F7DAEE07C20 /* ThumbnailsPanelBackgroundView.swift in Sources */,
|
||||
BF0C8AD478399D2E97AE3FB4 /* CustomRecorderControlTestable.swift in Sources */,
|
||||
BF0C8A5FA46C5E74A071FF4C /* ScrollwheelEvents.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -321,3 +321,16 @@ extension NSRunningApplication {
|
||||
// 250ms is similar to human delay in processing changes on screen
|
||||
// See https://humanbenchmark.com/tests/reactiontime
|
||||
let humanPerceptionDelay = DispatchTimeInterval.milliseconds(250)
|
||||
|
||||
extension NSTouch.Phase {
|
||||
var readable: String {
|
||||
switch self {
|
||||
case .began: "began"
|
||||
case .moved: "moved"
|
||||
case .stationary: "stationary"
|
||||
case .ended: "ended"
|
||||
case .cancelled: "cancelled"
|
||||
default: "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ class MouseEvents {
|
||||
}
|
||||
|
||||
static func toggle(_ enabled: Bool) {
|
||||
guard enabled != shouldBeEnabled else { return }
|
||||
shouldBeEnabled = enabled
|
||||
if let eventTap {
|
||||
CGEvent.tapEnable(tap: eventTap, enable: enabled)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import Cocoa
|
||||
|
||||
fileprivate var eventTap: CFMachPort!
|
||||
fileprivate var shouldBeEnabled: Bool!
|
||||
|
||||
class ScrollwheelEvents {
|
||||
static func observe() {
|
||||
observe_()
|
||||
toggle(false)
|
||||
}
|
||||
|
||||
static func toggle(_ enabled: Bool) {
|
||||
guard enabled != shouldBeEnabled else { return }
|
||||
shouldBeEnabled = enabled
|
||||
if let eventTap {
|
||||
CGEvent.tapEnable(tap: eventTap, enable: enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func observe_() {
|
||||
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
|
||||
eventTap = CGEvent.tapCreate(
|
||||
tap: .cghidEventTap, // we need raw data
|
||||
place: .headInsertEventTap,
|
||||
options: .defaultTap,
|
||||
eventsOfInterest: NSEvent.EventTypeMask.scrollWheel.rawValue,
|
||||
callback: handleEvent,
|
||||
userInfo: nil)
|
||||
if let eventTap {
|
||||
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
|
||||
CFRunLoopAddSource(BackgroundWork.keyboardAndTrackpadEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
} else {
|
||||
App.app.restart()
|
||||
}
|
||||
}
|
||||
|
||||
private let handleEvent: CGEventTapCallBack = { _, type, cgEvent, _ in
|
||||
if type.rawValue == NSEvent.EventType.scrollWheel.rawValue {
|
||||
// block scrolling globally
|
||||
return nil
|
||||
} else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) && shouldBeEnabled {
|
||||
CGEvent.tapEnable(tap: eventTap!, enable: true)
|
||||
}
|
||||
return Unmanaged.passUnretained(cgEvent) // focused app will receive the event
|
||||
}
|
||||
@@ -8,9 +8,11 @@ class TrackpadEvents {
|
||||
static func observe() {
|
||||
observe_()
|
||||
TrackpadEvents.toggle(Preferences.nextWindowGesture != .disabled)
|
||||
ScrollwheelEvents.observe()
|
||||
}
|
||||
|
||||
static func toggle(_ enabled: Bool) {
|
||||
guard enabled != shouldBeEnabled else { return }
|
||||
shouldBeEnabled = enabled
|
||||
if let eventTap {
|
||||
CGEvent.tapEnable(tap: eventTap, enable: enabled)
|
||||
@@ -48,27 +50,28 @@ private let handleEvent: CGEventTapCallBack = { _, type, cgEvent, _ in
|
||||
|
||||
private func touchEventHandler(_ cgEvent: CGEvent) -> Bool {
|
||||
guard let nsEvent = GestureDetector.convertEvent(cgEvent) else { return false }
|
||||
// if the finger count doesn't match, we reset tracking data, and may trigger fingersUp
|
||||
let touches = nsEvent.allTouches()
|
||||
if touches.count == 0 { return false } // sometimes the os sends events with no touches
|
||||
// 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("---", "activeTouches:", activeTouches.count, "all:", touches.map { $0.phase.readable })
|
||||
let requiredFingers = Preferences.nextWindowGesture.isThreeFinger() ? 3 : 4
|
||||
if (!App.app.appIsBeingUsed && touches.count != requiredFingers) || (App.app.appIsBeingUsed && touches.count < 2) {
|
||||
// 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, requiredFingers)
|
||||
return GestureDetector.checkForFingersUp(activeTouches.count, requiredFingers)
|
||||
}
|
||||
// when the native using 3-finger swipe to shift Space, macOS will block scrolling in the background
|
||||
// We imitate this behavior by sending a synthetic scrollWheel event
|
||||
GestureDetector.blockOngoingScrolling()
|
||||
// trigger actions if conditions are met
|
||||
// enough fingers are down
|
||||
if App.app.appIsBeingUsed {
|
||||
if !GestureDetector.updateStartPositions(touches, &NavigationSwipeDetector.startPositions) {
|
||||
if let r = NavigationSwipeDetector.check(touches) { return r }
|
||||
DispatchQueue.main.async { ScrollwheelEvents.toggle(true) }
|
||||
if !GestureDetector.updateStartPositions(activeTouches, &NavigationSwipeDetector.startPositions) {
|
||||
if let r = NavigationSwipeDetector.check(activeTouches) { return r }
|
||||
}
|
||||
} else {
|
||||
if !GestureDetector.updateStartPositions(touches, &TriggerSwipeDetector.startPositions) {
|
||||
if let r = TriggerSwipeDetector.check(touches) { return r }
|
||||
if !GestureDetector.updateStartPositions(activeTouches, &TriggerSwipeDetector.startPositions) {
|
||||
if let r = TriggerSwipeDetector.check(activeTouches) { return r }
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -84,8 +87,8 @@ class GestureDetector {
|
||||
return nsEvent
|
||||
}
|
||||
|
||||
static func checkForFingersUp(_ touches: Set<NSTouch>, _ requiredFingers: Int) -> Bool {
|
||||
if App.app.appIsBeingUsed && touches.count < requiredFingers && App.app.shortcutIndex == Preferences.gestureIndex
|
||||
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
|
||||
@@ -152,7 +155,10 @@ class TriggerSwipeDetector {
|
||||
}
|
||||
}
|
||||
reset()
|
||||
DispatchQueue.main.async { App.app.showUiOrCycleSelection(Preferences.gestureIndex, false) }
|
||||
DispatchQueue.main.async {
|
||||
ScrollwheelEvents.toggle(true)
|
||||
App.app.showUiOrCycleSelection(Preferences.gestureIndex, false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return nil
|
||||
@@ -174,12 +180,12 @@ class NavigationSwipeDetector {
|
||||
|
||||
static var startPositions: [String: NSPoint] = [:]
|
||||
|
||||
static func check(_ touches: Set<NSTouch>) -> Bool? {
|
||||
let averageDistance = GestureDetector.computeAverageDistance(touches, startPositions)
|
||||
static func check(_ activeTouches: Set<NSTouch>) -> Bool? {
|
||||
let averageDistance = GestureDetector.computeAverageDistance(activeTouches, startPositions)
|
||||
let (absX, absY) = (abs(averageDistance.x), abs(averageDistance.y))
|
||||
let maxIsX = absX >= absY
|
||||
if (maxIsX ? absX : absY) > MIN_SWIPE_DISTANCE {
|
||||
maxIsX ? resetX(touches) : resetY(touches)
|
||||
maxIsX ? resetX(activeTouches) : resetY(activeTouches)
|
||||
let direction: Direction = maxIsX ? (averageDistance.x < 0 ? .left : .right) : (averageDistance.y < 0 ? .down : .up)
|
||||
DispatchQueue.main.async { App.app.cycleSelection(direction, allowWrap: false) }
|
||||
return true
|
||||
|
||||
@@ -62,6 +62,7 @@ class App: AppCenterApplication {
|
||||
isFirstSummon = true
|
||||
forceDoNothingOnRelease = false
|
||||
MouseEvents.toggle(false)
|
||||
ScrollwheelEvents.toggle(false)
|
||||
hideThumbnailPanelWithoutChangingKeyWindow()
|
||||
if !keepPreview {
|
||||
previewPanel.orderOut(nil)
|
||||
|
||||
Reference in new issue
Block a user