Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swipe gestures support PoC #2355

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions alt-tab-macos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
4807A6C623A9CD190052A53E /* SkyLight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4807A6C523A9CD190052A53E /* SkyLight.framework */; };
48F3E16224EC0D8800A1C64B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BABDA79F8EF76E3ACDD5F /* Localizable.strings */; };
5883F0A829A5182C0071DB65 /* TrackpadEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5883F0A729A5182C0071DB65 /* TrackpadEvents.swift */; };
76D02BB22BFE7C9E0056008D /* Pods_alt_tab_macos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0712B3BEA2B3780398C0999 /* Pods_alt_tab_macos.framework */; };
BF0C8052AE41B1B10E42BFCE /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C875C983226CB16DBD90F /* InfoPlist.strings */; };
BF0C807D26E465F467B066F7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C849EBA5A547912BD8BB9 /* Localizable.strings */; };
Expand Down Expand Up @@ -171,6 +172,7 @@
/* Begin PBXFileReference section */
4807A6C523A9CD190052A53E /* SkyLight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SkyLight.framework; path = ../../../../System/Library/PrivateFrameworks/SkyLight.framework; sourceTree = "<group>"; };
481FE54624D2D387001032F1 /* alt-tab-macos-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "alt-tab-macos-Bridging-Header.h"; sourceTree = "<group>"; };
5883F0A729A5182C0071DB65 /* TrackpadEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadEvents.swift; sourceTree = "<group>"; };
59DD4BE63D42EEFF1182BA7F /* Pods-alt-tab-macos.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-alt-tab-macos.release.xcconfig"; path = "Target Support Files/Pods-alt-tab-macos/Pods-alt-tab-macos.release.xcconfig"; sourceTree = "<group>"; };
672F94799CC7C90282AEB3EA /* Pods-alt-tab-macos.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-alt-tab-macos.debug.xcconfig"; path = "Target Support Files/Pods-alt-tab-macos/Pods-alt-tab-macos.debug.xcconfig"; sourceTree = "<group>"; };
BF0C80A05C11C8ADC366EC4A /* lb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = lb; path = InfoPlist.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1271,6 +1273,7 @@
D04BA76AEE37D6656EB80126 /* WorkspaceEvents.swift */,
D04BA5D9B4EBEB9E4333AE39 /* UserDefaultsEvents.swift */,
D04BAF320F4D43F0DDFE063E /* MouseEvents.swift */,
5883F0A729A5182C0071DB65 /* TrackpadEvents.swift */,
);
path = events;
sourceTree = "<group>";
Expand Down Expand Up @@ -1643,6 +1646,7 @@
D04BA7B8D599E1A7A27FF5AE /* AcknowledgmentsTab.swift in Sources */,
D04BA8757C9F20F25EB50785 /* TextField.swift in Sources */,
D04BAE8B16A06A10E2FA94DE /* AccessibilityEvents.swift in Sources */,
5883F0A829A5182C0071DB65 /* TrackpadEvents.swift in Sources */,
D04BAAAF5CFA991D3B078DB8 /* KeyboardEvents.swift in Sources */,
D04BA1766FBCB2E941D081A5 /* WorkspaceEvents.swift in Sources */,
D04BA0AE9865276FF8EF5DEB /* UserDefaultsEvents.swift in Sources */,
Expand Down
11 changes: 8 additions & 3 deletions src/logic/ATShortcut.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ class ATShortcut {
// contains at least
return modifiers == (modifiers | shortcut.carbonModifierFlags)
}
let holdModifiers = ControlsTab.shortcuts[Preferences.indexToName("holdShortcut", App.app.shortcutIndex)]!.shortcut.carbonModifierFlags
// contains exactly or exactly + holdShortcut modifiers
return modifiers == shortcut.carbonModifierFlags || modifiers == (shortcut.carbonModifierFlags | holdModifiers)
if modifiers == shortcut.carbonModifierFlags {
return true
}
guard let atShortcut = ControlsTab.shortcuts[Preferences.indexToName("holdShortcut", App.app.shortcutIndex)] else {
return false
}
let holdModifiers = atShortcut.shortcut.carbonModifierFlags
return modifiers == (shortcut.carbonModifierFlags | holdModifiers)
}

func shouldTrigger() -> Bool {
Expand Down
34 changes: 27 additions & 7 deletions src/logic/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Preferences {
"nextWindowShortcut3": "",
"nextWindowShortcut4": "",
"nextWindowShortcut5": "",
"gesture": GesturePreference.none.rawValue,
"focusWindowShortcut": "Space",
"previousWindowShortcut": "⇧",
"cancelShortcut": "⎋",
Expand All @@ -36,16 +37,19 @@ class Preferences {
"showMinimizedWindows3": ShowHowPreference.show.rawValue,
"showMinimizedWindows4": ShowHowPreference.show.rawValue,
"showMinimizedWindows5": ShowHowPreference.show.rawValue,
"showMinimizedWindows6": ShowHowPreference.show.rawValue,
"showHiddenWindows": ShowHowPreference.show.rawValue,
"showHiddenWindows2": ShowHowPreference.show.rawValue,
"showHiddenWindows3": ShowHowPreference.show.rawValue,
"showHiddenWindows4": ShowHowPreference.show.rawValue,
"showHiddenWindows5": ShowHowPreference.show.rawValue,
"showHiddenWindows6": ShowHowPreference.show.rawValue,
"showFullscreenWindows": ShowHowPreference.show.rawValue,
"showFullscreenWindows2": ShowHowPreference.show.rawValue,
"showFullscreenWindows3": ShowHowPreference.show.rawValue,
"showFullscreenWindows4": ShowHowPreference.show.rawValue,
"showFullscreenWindows5": ShowHowPreference.show.rawValue,
"showFullscreenWindows6": ShowHowPreference.show.rawValue,
"showTabsAsWindows": "false",
"hideColoredCircles": "false",
"windowDisplayDelay": "0",
Expand All @@ -58,16 +62,19 @@ class Preferences {
"appsToShow3": AppsToShowPreference.all.rawValue,
"appsToShow4": AppsToShowPreference.all.rawValue,
"appsToShow5": AppsToShowPreference.all.rawValue,
"appsToShow6": AppsToShowPreference.all.rawValue,
"spacesToShow": SpacesToShowPreference.all.rawValue,
"spacesToShow2": SpacesToShowPreference.all.rawValue,
"spacesToShow3": SpacesToShowPreference.all.rawValue,
"spacesToShow4": SpacesToShowPreference.all.rawValue,
"spacesToShow5": SpacesToShowPreference.all.rawValue,
"spacesToShow6": SpacesToShowPreference.all.rawValue,
"screensToShow": ScreensToShowPreference.all.rawValue,
"screensToShow2": ScreensToShowPreference.all.rawValue,
"screensToShow3": ScreensToShowPreference.all.rawValue,
"screensToShow4": ScreensToShowPreference.all.rawValue,
"screensToShow5": ScreensToShowPreference.all.rawValue,
"screensToShow6": ScreensToShowPreference.all.rawValue,
"fadeOutAnimation": "false",
"hideSpaceNumberLabels": "false",
"hideStatusIcons": "false",
Expand All @@ -84,6 +91,7 @@ class Preferences {
"shortcutStyle3": ShortcutStylePreference.focusOnRelease.rawValue,
"shortcutStyle4": ShortcutStylePreference.focusOnRelease.rawValue,
"shortcutStyle5": ShortcutStylePreference.focusOnRelease.rawValue,
"shortcutStyle6": ShortcutStylePreference.focusOnRelease.rawValue,
"hideAppBadges": "false",
"hideWindowlessApps": "false",
"hideThumbnails": "false",
Expand Down Expand Up @@ -138,13 +146,13 @@ class Preferences {
static var alignThumbnails: AlignThumbnailsPreference { defaults.macroPref("alignThumbnails", AlignThumbnailsPreference.allCases) }
static var updatePolicy: UpdatePolicyPreference { defaults.macroPref("updatePolicy", UpdatePolicyPreference.allCases) }
static var crashPolicy: CrashPolicyPreference { defaults.macroPref("crashPolicy", CrashPolicyPreference.allCases) }
static var appsToShow: [AppsToShowPreference] { ["appsToShow", "appsToShow2", "appsToShow3", "appsToShow4", "appsToShow5"].map { defaults.macroPref($0, AppsToShowPreference.allCases) } }
static var spacesToShow: [SpacesToShowPreference] { ["spacesToShow", "spacesToShow2", "spacesToShow3", "spacesToShow4", "spacesToShow5"].map { defaults.macroPref($0, SpacesToShowPreference.allCases) } }
static var screensToShow: [ScreensToShowPreference] { ["screensToShow", "screensToShow2", "screensToShow3", "screensToShow4", "screensToShow5"].map { defaults.macroPref($0, ScreensToShowPreference.allCases) } }
static var showMinimizedWindows: [ShowHowPreference] { ["showMinimizedWindows", "showMinimizedWindows2", "showMinimizedWindows3", "showMinimizedWindows4", "showMinimizedWindows5"].map { defaults.macroPref($0, ShowHowPreference.allCases) } }
static var showHiddenWindows: [ShowHowPreference] { ["showHiddenWindows", "showHiddenWindows2", "showHiddenWindows3", "showHiddenWindows4", "showHiddenWindows5"].map { defaults.macroPref($0, ShowHowPreference.allCases) } }
static var showFullscreenWindows: [ShowHowPreference] { ["showFullscreenWindows", "showFullscreenWindows2", "showFullscreenWindows3", "showFullscreenWindows4", "showFullscreenWindows5"].map { defaults.macroPref($0, ShowHowPreference.allCases) } }
static var shortcutStyle: [ShortcutStylePreference] { ["shortcutStyle", "shortcutStyle2", "shortcutStyle3", "shortcutStyle4", "shortcutStyle5"].map { defaults.macroPref($0, ShortcutStylePreference.allCases) } }
static var appsToShow: [AppsToShowPreference] { ["appsToShow", "appsToShow2", "appsToShow3", "appsToShow4", "appsToShow5", "appsToShow6"].map { defaults.macroPref($0, AppsToShowPreference.allCases) } }
static var spacesToShow: [SpacesToShowPreference] { ["spacesToShow", "spacesToShow2", "spacesToShow3", "spacesToShow4", "spacesToShow5", "spacesToShow6"].map { defaults.macroPref($0, SpacesToShowPreference.allCases) } }
static var screensToShow: [ScreensToShowPreference] { ["screensToShow", "screensToShow2", "screensToShow3", "screensToShow4", "screensToShow5", "screensToShow6"].map { defaults.macroPref($0, ScreensToShowPreference.allCases) } }
static var showMinimizedWindows: [ShowHowPreference] { ["showMinimizedWindows", "showMinimizedWindows2", "showMinimizedWindows3", "showMinimizedWindows4", "showMinimizedWindows5", "showMinimizedWindows6"].map { defaults.macroPref($0, ShowHowPreference.allCases) } }
static var showHiddenWindows: [ShowHowPreference] { ["showHiddenWindows", "showHiddenWindows2", "showHiddenWindows3", "showHiddenWindows4", "showHiddenWindows5", "showHiddenWindows6"].map { defaults.macroPref($0, ShowHowPreference.allCases) } }
static var showFullscreenWindows: [ShowHowPreference] { ["showFullscreenWindows", "showFullscreenWindows2", "showFullscreenWindows3", "showFullscreenWindows4", "showFullscreenWindows5", "showFullscreenWindows6"].map { defaults.macroPref($0, ShowHowPreference.allCases) } }
static var shortcutStyle: [ShortcutStylePreference] { ["shortcutStyle", "shortcutStyle2", "shortcutStyle3", "shortcutStyle4", "shortcutStyle5", "shortcutStyle6"].map { defaults.macroPref($0, ShortcutStylePreference.allCases) } }
static var menubarIcon: MenubarIconPreference { defaults.macroPref("menubarIcon", MenubarIconPreference.allCases) }

// derived values
Expand Down Expand Up @@ -448,6 +456,18 @@ enum MenubarIconPreference: String, CaseIterable, MacroPreference {
}
}

enum GesturePreference: String, CaseIterable, MacroPreference {
case none = "0"
case threeFingerSwipe = "1"

var localizedString: LocalizedString {
switch self {
case .none: return ""
case .threeFingerSwipe: return NSLocalizedString("Swipe with Three Fingers", comment: "")
}
}
}

enum ShortcutStylePreference: String, CaseIterable, MacroPreference {
case focusOnRelease = "0"
case doNothingOnRelease = "1"
Expand Down
150 changes: 150 additions & 0 deletions src/logic/events/TrackpadEvents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import Cocoa

fileprivate var eventTap: CFMachPort!
fileprivate var shouldBeEnabled: Bool!

//TODO: Should we add a sensetivity setting instead of these magic numbers?
fileprivate let accVelXThreshold: Float = 0.05
fileprivate let accVelYThreshold: Float = 0.075
fileprivate var accVelX: Float = 0
fileprivate var accVelY: Float = 0
Comment on lines +7 to +10

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fileprivate let accVelXThreshold: Float = 0.05
fileprivate let accVelYThreshold: Float = 0.075
fileprivate var accVelX: Float = 0
fileprivate var accVelY: Float = 0
fileprivate let accVelXThreshold: Float = 0.03
fileprivate let accVelYThreshold: Float = 0.06
fileprivate var accVelX: Float = 0.35
fileprivate var accVelY: Float = 0.125

Thanks @lwouis for your work! I also tried your touch-pad and found the same problem with this PR. The sensitivity is not matched with Windows (in my experience). I tried different numbers and found that 0.03, 0.06, 0.35, and 0.125 are more sensitive and like how Windows works.

//TODO: Don't use string as key. Maybe we should use other data-sructure.
fileprivate var prevTouchPositions: [String: NSPoint] = [:]

//TODO: underlying content scrolls if both Mission Control and App Expose use 4-finger swipes or are off in Trackpad settings. It doesn't scroll if any of them use 3-finger swipe though.
class TrackpadEvents {
static func observe() {
observe_()
}

static func toggle(_ enabled: Bool) {
shouldBeEnabled = enabled
if let eventTap = eventTap {
CGEvent.tapEnable(tap: eventTap, enable: enabled)
}
}
}

private func observe_() {
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
eventTap = CGEvent.tapCreate(
tap: .cghidEventTap,
place: .headInsertEventTap,
options: .listenOnly,
eventsOfInterest: NSEvent.EventTypeMask.gesture.rawValue,
callback: eventHandler,
userInfo: nil)
if let eventTap = eventTap {
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
//TODO: Is CFRunLoopGetCurrent OK or do we need yet another thread with runLoop in BackgroundWork?
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes)
} else {
App.app.restart()
}
}

private func eventHandler(proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
if type.rawValue == NSEvent.EventType.gesture.rawValue, let nsEvent = NSEvent(cgEvent: cgEvent) {
touchEventHandler(nsEvent)
} else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) && shouldBeEnabled {
CGEvent.tapEnable(tap: eventTap!, enable: true)
}
return nil
}

private func touchEventHandler(_ nsEvent: NSEvent) {
let touches = nsEvent.allTouches()

// Sometimes there are empty touch events that we have to skip. There are no empty touch events if Mission Control or App Expose use 3-finger swipes though.
if touches.isEmpty {
return
}
let touchesCount = touches.allSatisfy({ $0.phase == .ended }) ? 0 : touches.count

// We don't care about non-3-fingers swipes.
if touchesCount != 3 {
// Except when we already started a gesture, so we need to end it.
if App.app.appIsBeingUsed && App.app.shortcutIndex == 5 && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease {
DispatchQueue.main.async {
App.app.focusTarget()
}
}
clearState()
return
}

let velocity = swipeVelocity(touches)
// We don't care about gestures other than horizontal or vertical swipes.
if velocity == nil {
return
}

accVelX += velocity!.x
accVelY += velocity!.y
// Not enough swiping.
if abs(accVelX) < accVelXThreshold && abs(accVelY) < accVelYThreshold {
return
}

let isHorizontal = abs(velocity!.x) > abs(velocity!.y)
if App.app.appIsBeingUsed {
let direction: Direction = isHorizontal
? accVelX < 0 ? .left : .right
: accVelY < 0 ? .down : .up
DispatchQueue.main.async { App.app.cycleSelection(direction) }
} else {
if isHorizontal {
DispatchQueue.main.async {
App.app.appIsBeingUsed = true
App.app.showUiOrCycleSelection(5)
}
}
}
clearState()
}

private func clearState() {
accVelX = 0
accVelY = 0
prevTouchPositions.removeAll()
}

private func swipeVelocity(_ touches: Set<NSTouch>) -> (x: Float, y: Float)? {
var allRight = true
var allLeft = true
var allUp = true
var allDown = true
var sumVelX = Float(0)
var sumVelY = Float(0)
for touch in touches {
let (velX, velY) = touchVelocity(touch)
allRight = allRight && velX >= 0
allLeft = allLeft && velX <= 0
allUp = allUp && velY >= 0
allDown = allDown && velY <= 0
sumVelX += velX
sumVelY += velY

if touch.phase == .ended {
prevTouchPositions.removeValue(forKey: "\(touch.identity)")
} else {
prevTouchPositions["\(touch.identity)"] = touch.normalizedPosition
}
}
// All fingers should move in the same direction.
if !allRight && !allLeft && !allUp && !allDown {
return nil
}

let velX = sumVelX / Float(touches.count)
let velY = sumVelY / Float(touches.count)
return (velX, velY)
}

private func touchVelocity(_ touch: NSTouch) -> (Float, Float) {
guard let prevPosition = prevTouchPositions["\(touch.identity)"] else {
return (0, 0)
}
let position = touch.normalizedPosition
return (Float(position.x - prevPosition.x), Float(position.y - prevPosition.y))
}
1 change: 1 addition & 0 deletions src/ui/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
self.thumbnailsPanel = ThumbnailsPanel()
Spaces.initialDiscovery()
Applications.initialDiscovery()
TrackpadEvents.observe()
self.preferencesWindow = PreferencesWindow()
self.feedbackWindow = FeedbackWindow()
KeyboardEvents.addEventHandlers()
Expand Down
Loading