diff --git a/alt-tab-macos.xcodeproj/project.pbxproj b/alt-tab-macos.xcodeproj/project.pbxproj index 54c7e780..f6a2adb1 100644 --- a/alt-tab-macos.xcodeproj/project.pbxproj +++ b/alt-tab-macos.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ BF0C827C61136E83F43C6436 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8913DCD0FE4C494055B6 /* InfoPlist.strings */; }; BF0C828D53D429C2B8B22391 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C883EBA8FDA9CB61C91FD /* Localizable.strings */; }; BF0C829583ED47E458528F3A /* accessibility@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = BF0C889529FF19620EDAC295 /* accessibility@2x.png */; }; + BF0C82E9AB495AAAA182764A /* CursorEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C81DD40D5AAD9B7C09028 /* CursorEvents.swift */; }; BF0C832C5BF4C8E9C95F7767 /* MissionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C82E4DE5A28800F1DA6E6 /* MissionControl.swift */; }; BF0C83578F7292F307E30751 /* PopupButtonLikeSystemSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8A479D2B7FB80531F037 /* PopupButtonLikeSystemSettings.swift */; }; BF0C836CC0D9CEE5026B8EF4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C83121AAE4437993B4FF3 /* InfoPlist.strings */; }; @@ -387,6 +388,7 @@ BF0C81BB57B297B2432C4824 /* test-release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "test-release.xcconfig"; sourceTree = ""; }; BF0C81BF699036148BA17B7C /* jv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = jv; path = Localizable.strings; sourceTree = ""; }; BF0C81D602ED642A48D50B78 /* notarytool */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = notarytool; sourceTree = ""; }; + BF0C81DD40D5AAD9B7C09028 /* CursorEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorEvents.swift; sourceTree = ""; }; BF0C82009DCCA86287730F13 /* th */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = th; path = InfoPlist.strings; sourceTree = ""; }; BF0C821029C1CBAC09232B0B /* nn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nn; path = InfoPlist.strings; sourceTree = ""; }; BF0C822D3BEDAB1F1C9F775E /* ScreensEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreensEvents.swift; sourceTree = ""; }; @@ -1898,6 +1900,7 @@ BF0C8643F524515224BF1618 /* KeyboardEventsTestable.swift */, BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */, BF0C8FAF34022E23582797F3 /* ScrollwheelEvents.swift */, + BF0C81DD40D5AAD9B7C09028 /* CursorEvents.swift */, ); path = events; sourceTree = ""; @@ -2476,6 +2479,7 @@ BF0C8E5F4E4D5F7DAEE07C20 /* ThumbnailsPanelBackgroundView.swift in Sources */, BF0C8AD478399D2E97AE3FB4 /* CustomRecorderControlTestable.swift in Sources */, BF0C8A5FA46C5E74A071FF4C /* ScrollwheelEvents.swift in Sources */, + BF0C82E9AB495AAAA182764A /* CursorEvents.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/api-wrappers/HelperExtensions.swift b/src/api-wrappers/HelperExtensions.swift index c57f7e56..7a7fafbe 100644 --- a/src/api-wrappers/HelperExtensions.swift +++ b/src/api-wrappers/HelperExtensions.swift @@ -381,3 +381,17 @@ extension String.StringInterpolation { } } } + +extension CGEvent { + func toNSEvent() -> NSEvent? { + if Thread.isMainThread { + return NSEvent(cgEvent: self) + } + // conversion has to happen on the main-thread, or appkit will crash + var nsEvent: NSEvent? + DispatchQueue.main.sync { + nsEvent = NSEvent(cgEvent: self) + } + return nsEvent + } +} diff --git a/src/logic/events/CursorEvents.swift b/src/logic/events/CursorEvents.swift new file mode 100644 index 00000000..3a824872 --- /dev/null +++ b/src/logic/events/CursorEvents.swift @@ -0,0 +1,69 @@ +import Cocoa +import Carbon.HIToolbox.Events + +class CursorEvents { + private static var eventTap: CFMachPort! + private static var shouldBeEnabled: Bool! + static var deadZoneInitialPosition: CGPoint? + static var isAllowedToMouseHover = true + + static func observe() { + observe_() + } + + static func toggle(_ enabled: Bool) { + guard enabled != shouldBeEnabled else { return } + shouldBeEnabled = enabled + if !enabled { + deadZoneInitialPosition = nil + } + if let eventTap { + CGEvent.tapEnable(tap: eventTap, enable: enabled) + } + } + + private static func observe_() { + let eventMask = [CGEventType.mouseMoved].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) }) + // CGEvent.tapCreate returns nil if ensureAccessibilityCheckboxIsChecked() didn't pass + eventTap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .defaultTap, + eventsOfInterest: eventMask, + callback: handleEvent, + userInfo: nil) + if let eventTap { + toggle(false) + let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0) + // we run on main-thread directly since all we do is check NSEvent data, which we must do on main-thread + CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes) + } else { + App.app.restart() + } + } + + private static let handleEvent: CGEventTapCallBack = { _, type, cgEvent, _ in + if type == .mouseMoved { + updateDeadzoneSituation(cgEvent) + } else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) && shouldBeEnabled { + CGEvent.tapEnable(tap: eventTap!, enable: true) + } + return Unmanaged.passUnretained(cgEvent) // focused app will receive the event + } + + /// when using the trackpad, the user may swipe with a slight mistake. This will create a small cursor movement + /// we ignore those, as they are not intended. Intended movements will be larger and not ignored + private static func updateDeadzoneSituation(_ cgEvent: CGEvent) { + guard let event = cgEvent.toNSEvent() else { return } + guard let deadZoneInitialPosition else { + deadZoneInitialPosition = event.locationInWindow + isAllowedToMouseHover = false + return + } + let deltaX = event.locationInWindow.x - deadZoneInitialPosition.x + let deltaY = event.locationInWindow.y - deadZoneInitialPosition.y + let d = hypot(deltaX, deltaY) + Logger.error { d } + isAllowedToMouseHover = d > 25 + } +} diff --git a/src/logic/events/MouseEvents.swift b/src/logic/events/MouseEvents.swift index dcc072d4..9074960b 100644 --- a/src/logic/events/MouseEvents.swift +++ b/src/logic/events/MouseEvents.swift @@ -28,7 +28,7 @@ class MouseEvents { callback: handleEvent, userInfo: nil) if let eventTap { - MouseEvents.toggle(false) + toggle(false) let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0) // we run on main-thread directly since all we do is check UI coordinates, which we must do on main-thread CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes) diff --git a/src/logic/events/TrackpadEvents.swift b/src/logic/events/TrackpadEvents.swift index 33f9f02e..61664b61 100644 --- a/src/logic/events/TrackpadEvents.swift +++ b/src/logic/events/TrackpadEvents.swift @@ -4,6 +4,7 @@ import Cocoa class TrackpadEvents { private static var eventTap: CFMachPort! private static var shouldBeEnabled: Bool! + private static var cursorMovedDistance = CGFloat(0.0) static func observe() { observe_() @@ -48,15 +49,15 @@ class TrackpadEvents { } private static func touchEventHandler(_ cgEvent: CGEvent) -> Bool { - guard let nsEvent = convertEvent(cgEvent) else { return false } // don't absorb the touch event + 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) } let requiredFingers = Preferences.nextWindowGesture.isThreeFinger() ? 3 : 4 - // Logger.error("---", "activeTouches:", activeTouches.count, "all:", touches.map { $0.phase.readable }, requiredFingers) if App.app.appIsBeingUsed { handleEventIfAppIsBeingUsed(activeTouches, requiredFingers) return true // absorb the touch event @@ -74,6 +75,7 @@ class TrackpadEvents { } } } else if activeTouches.count > 1 { + CursorEvents.deadZoneInitialPosition = nil NavigationSwipeDetector.hasDetected(activeTouches) } else { // if activeTouches.count == 1, ignore (finger is in pointer-mode) @@ -232,12 +234,3 @@ fileprivate func performHapticFeedback() { 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 -} diff --git a/src/ui/App.swift b/src/ui/App.swift index 4a084913..efce462c 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -63,6 +63,7 @@ class App: AppCenterApplication { isFirstSummon = true forceDoNothingOnRelease = false MouseEvents.toggle(false) + CursorEvents.toggle(false) ScrollwheelEvents.toggle(false) hideThumbnailPanelWithoutChangingKeyWindow() if !keepPreview { @@ -314,6 +315,7 @@ extension App: NSApplicationDelegate { self.feedbackWindow = FeedbackWindow() KeyboardEvents.addEventHandlers() MouseEvents.observe() + CursorEvents.observe() TrackpadEvents.observe() CliEvents.observe() // login item and plist updates can be done a bit later, to accelerate launch diff --git a/src/ui/main-window/ThumbnailsPanel.swift b/src/ui/main-window/ThumbnailsPanel.swift index db9ff034..0efba52d 100644 --- a/src/ui/main-window/ThumbnailsPanel.swift +++ b/src/ui/main-window/ThumbnailsPanel.swift @@ -71,6 +71,7 @@ class ThumbnailsPanel: NSPanel { alphaValue = 1 makeKeyAndOrderFront(nil) MouseEvents.toggle(true) + CursorEvents.toggle(true) thumbnailsView.scrollView.flashScrollers() } diff --git a/src/ui/main-window/ThumbnailsView.swift b/src/ui/main-window/ThumbnailsView.swift index de932ab0..75761d3b 100644 --- a/src/ui/main-window/ThumbnailsView.swift +++ b/src/ui/main-window/ThumbnailsView.swift @@ -290,7 +290,7 @@ class ScrollView: NSScrollView { } override func mouseMoved(with event: NSEvent) { - guard let documentView, !isCurrentlyScrolling && !ScrollwheelEvents.shouldBeEnabled else { return } + guard let documentView, !isCurrentlyScrolling && CursorEvents.isAllowedToMouseHover else { return } let location = documentView.convert(App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream, from: nil) let newTarget = findTarget(location) guard newTarget !== previousTarget else { return }