Skip to content

Commit 8e36156

Browse files
committed
feat: add shortcut modifier side
1 parent 9e03d5f commit 8e36156

File tree

5 files changed

+98
-11
lines changed

5 files changed

+98
-11
lines changed

src/api-wrappers/HelperExtensions.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,17 @@ extension Optional where Wrapped == String {
189189
return (self ?? "").localizedStandardCompare(string ?? "")
190190
}
191191
}
192+
193+
extension NSEvent.ModifierFlags {
194+
static let leftShift = Self(rawValue: UInt(NX_DEVICELSHIFTKEYMASK))
195+
static let rightShift = Self(rawValue: UInt(NX_DEVICERSHIFTKEYMASK))
196+
197+
static let leftControl = Self(rawValue: UInt(NX_DEVICELCTLKEYMASK))
198+
static let rightControl = Self(rawValue: UInt(NX_DEVICERCTLKEYMASK))
199+
200+
static let leftOption = Self(rawValue: UInt(NX_DEVICELALTKEYMASK))
201+
static let rightOption = Self(rawValue: UInt(NX_DEVICERALTKEYMASK))
202+
203+
static let leftCommand = Self(rawValue: UInt(NX_DEVICELCMDKEYMASK))
204+
static let rightCommand = Self(rawValue: UInt(NX_DEVICERCMDKEYMASK))
205+
}

src/logic/ATShortcut.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ class ATShortcut {
1717
self.index = index
1818
}
1919

20-
func matches(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool) -> Bool {
20+
func matches(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool, _ shortcutScope: ShortcutScope) -> Bool {
21+
guard shortcutScope == scope else {
22+
return false
23+
}
2124
if let id = id, let shortcutState = shortcutState {
2225
let shortcutIndex = Int(id.id)
2326
let shortcutId = Array(KeyboardEvents.globalShortcutsIds).first { $0.value == shortcutIndex }!.key

src/logic/Preferences.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ class Preferences {
9191
"shortcutStyle3": ShortcutStylePreference.focusOnRelease.rawValue,
9292
"shortcutStyle4": ShortcutStylePreference.focusOnRelease.rawValue,
9393
"shortcutStyle5": ShortcutStylePreference.focusOnRelease.rawValue,
94+
"shortcutModifierSide": ShortcutModifierSidePreference.any.rawValue,
95+
"shortcutModifierSide2": ShortcutModifierSidePreference.any.rawValue,
96+
"shortcutModifierSide3": ShortcutModifierSidePreference.any.rawValue,
97+
"shortcutModifierSide4": ShortcutModifierSidePreference.any.rawValue,
98+
"shortcutModifierSide5": ShortcutModifierSidePreference.any.rawValue,
9499
"hideAppBadges": "false",
95100
"hideWindowlessApps": "false",
96101
"hideThumbnails": "false",
@@ -157,6 +162,7 @@ class Preferences {
157162
static var showFullscreenWindows: [ShowHowPreference] { ["showFullscreenWindows", "showFullscreenWindows2", "showFullscreenWindows3", "showFullscreenWindows4", "showFullscreenWindows5"].map { defaults.macroPref($0, ShowHowPreference.allCases) } }
158163
static var windowOrder: [WindowOrderPreference] { ["windowOrder", "windowOrder2", "windowOrder3", "windowOrder4", "windowOrder5"].map { defaults.macroPref($0, WindowOrderPreference.allCases) } }
159164
static var shortcutStyle: [ShortcutStylePreference] { ["shortcutStyle", "shortcutStyle2", "shortcutStyle3", "shortcutStyle4", "shortcutStyle5"].map { defaults.macroPref($0, ShortcutStylePreference.allCases) } }
165+
static var shortcutModifierSide: [ShortcutModifierSidePreference] { ["shortcutModifierSide", "shortcutModifierSide2", "shortcutModifierSide3", "shortcutModifierSide4", "shortcutModifierSide5"].map { defaults.macroPref($0, ShortcutModifierSidePreference.allCases) } }
160166
static var menubarIcon: MenubarIconPreference { defaults.macroPref("menubarIcon", MenubarIconPreference.allCases) }
161167

162168
// derived values
@@ -477,6 +483,20 @@ enum ShortcutStylePreference: String, CaseIterable, MacroPreference {
477483
}
478484
}
479485

486+
enum ShortcutModifierSidePreference: String, CaseIterable, MacroPreference {
487+
case any = "0"
488+
case left = "1"
489+
case right = "2"
490+
491+
var localizedString: LocalizedString {
492+
switch self {
493+
case .any: return NSLocalizedString("Any", comment: "")
494+
case .left: return NSLocalizedString("Left", comment: "")
495+
case .right: return NSLocalizedString("Right", comment: "")
496+
}
497+
}
498+
}
499+
480500
enum ShowHowPreference: String, CaseIterable, MacroPreference {
481501
case show = "0"
482502
case hide = "1"

src/logic/events/KeyboardEvents.swift

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ class KeyboardEvents {
3434
removeHandlerIfNeeded()
3535
}
3636

37-
private static func unregisterHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
38-
if shortcut.keyCode != .none {
37+
static func unregisterHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
38+
if shortcut.keyCode != .none, eventHotKeyRefs[controlId] != nil {
3939
UnregisterEventHotKey(eventHotKeyRefs[controlId]!)
4040
eventHotKeyRefs[controlId] = nil
4141
}
4242
}
4343

4444
static func registerHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
45-
if shortcut.keyCode != .none {
45+
if shortcut.keyCode != .none, eventHotKeyRefs[controlId] == nil {
4646
let id = globalShortcutsIds[controlId]!
4747
let hotkeyId = EventHotKeyID(signature: signature, id: UInt32(id))
4848
let key = shortcut.carbonKeyCode
@@ -74,7 +74,7 @@ class KeyboardEvents {
7474

7575
private static func addLocalMonitorForKeyDownAndKeyUp() {
7676
localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .keyUp]) { (event: NSEvent) in
77-
let someShortcutTriggered = handleEvent(nil, nil, event.type == .keyDown ? UInt32(event.keyCode) : nil, cocoaToCarbonFlags(event.modifierFlags), event.type == .keyDown ? event.isARepeat : false)
77+
let someShortcutTriggered = handleEvent(nil, nil, event.type == .keyDown ? UInt32(event.keyCode) : nil, cocoaToCarbonFlags(event.modifierFlags), event.type == .keyDown ? event.isARepeat : false, .local)
7878
return someShortcutTriggered ? nil : event
7979
}
8080
}
@@ -104,7 +104,7 @@ class KeyboardEvents {
104104
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
105105
var id = EventHotKeyID()
106106
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
107-
handleEvent(id, .down, nil, nil, false)
107+
handleEvent(id, .down, nil, nil, false, .global)
108108
return noErr
109109
}, eventTypes.count, &eventTypes, nil, &hotKeyPressedEventHandler)
110110
}
@@ -113,7 +113,7 @@ class KeyboardEvents {
113113
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
114114
var id = EventHotKeyID()
115115
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
116-
handleEvent(id, .up, nil, nil, false)
116+
handleEvent(id, .up, nil, nil, false, .global)
117117
return noErr
118118
}, eventTypes.count, &eventTypes, nil, &hotKeyReleasedEventHandler)
119119
}
@@ -131,11 +131,58 @@ class KeyboardEvents {
131131
}
132132
}
133133

134+
fileprivate func handleShortcutModifierSide(_ modifiers: NSEvent.ModifierFlags) {
135+
let sideModifiers: [(any: NSEvent.ModifierFlags, left: NSEvent.ModifierFlags, right: NSEvent.ModifierFlags)] = [
136+
(.shift, .leftShift, .rightShift),
137+
(.control, .leftControl, .rightControl),
138+
(.option, .leftOption, .rightOption),
139+
(.command, .leftCommand, .rightCommand)
140+
]
141+
for shortcutIndex in 0...4 {
142+
let shortcutModifierSide = Preferences.shortcutModifierSide[shortcutIndex]
143+
let shortcutIds = [
144+
Preferences.indexToName("holdShortcut", shortcutIndex),
145+
Preferences.indexToName("nextWindowShortcut", shortcutIndex)
146+
]
147+
var register = true
148+
if shortcutModifierSide != .any {
149+
let shortcutModifiers = shortcutIds.reduce(into: NSEvent.ModifierFlags()) {
150+
guard let shortcut = ControlsTab.shortcuts[$1] else {
151+
return
152+
}
153+
$0.formUnion(shortcut.shortcut.modifierFlags)
154+
}
155+
if
156+
(sideModifiers.contains {
157+
shortcutModifiers.contains($0.any) &&
158+
!modifiers.contains(shortcutModifierSide == .left ? $0.left : $0.right) &&
159+
modifiers.contains(shortcutModifierSide == .left ? $0.right : $0.left)
160+
})
161+
{
162+
register = false
163+
}
164+
}
165+
shortcutIds.forEach {
166+
guard let shortcut = ControlsTab.shortcuts[$0] else {
167+
return
168+
}
169+
if register {
170+
KeyboardEvents.registerHotKeyIfNeeded($0, shortcut.shortcut)
171+
} else {
172+
KeyboardEvents.unregisterHotKeyIfNeeded($0, shortcut.shortcut)
173+
}
174+
}
175+
if !register && App.app.shortcutIndex == shortcutIndex && App.app.appIsBeingUsed {
176+
App.app.hideUi()
177+
}
178+
}
179+
}
180+
134181
@discardableResult
135-
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool) -> Bool {
182+
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool, _ shortcutScope: ShortcutScope) -> Bool {
136183
var someShortcutTriggered = false
137184
for shortcut in ControlsTab.shortcuts.values {
138-
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat) && shortcut.shouldTrigger() {
185+
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat, shortcutScope) && shortcut.shouldTrigger() {
139186
shortcut.executeAction(isARepeat)
140187
someShortcutTriggered = true
141188
}
@@ -145,8 +192,9 @@ fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutStat
145192

146193
fileprivate func cgEventFlagsChangedHandler(proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
147194
if type == .flagsChanged {
148-
let modifiers = cocoaToCarbonFlags(NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue)))
149-
handleEvent(nil, nil, nil, modifiers, false)
195+
let modifiers = NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue))
196+
handleShortcutModifierSide(modifiers)
197+
handleEvent(nil, nil, nil, cocoaToCarbonFlags(modifiers), false, .global)
150198
} else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) {
151199
CGEvent.tapEnable(tap: eventTap!, enable: true)
152200
}

src/ui/preferences-window/tabs/ControlsTab.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class ControlsTab {
120120
separator.boxType = .separator
121121
let nextWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select next window", comment: ""), Preferences.indexToName("nextWindowShortcut", index), Preferences.nextWindowShortcut[index], labelPosition: .right)
122122
let shortcutStyle = LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Then release:", comment: ""), Preferences.indexToName("shortcutStyle", index), ShortcutStylePreference.allCases)
123+
let shortcutModifierSide = LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Modifier side:", comment: ""), Preferences.indexToName("shortcutModifierSide", index), ShortcutModifierSidePreference.allCases)
123124
let toShowDropdowns = StackView([appsToShow, spacesToShow, screensToShow], .vertical, false)
124125
toShowDropdowns.spacing = TabView.padding
125126
toShowDropdowns.fit()
@@ -132,6 +133,7 @@ class ControlsTab {
132133
[separator],
133134
[holdAndPress, StackView(nextWindowShortcut)],
134135
shortcutStyle,
136+
shortcutModifierSide,
135137
], TabView.padding)
136138
tab.column(at: 0).xPlacement = .trailing
137139
tab.mergeCells(inHorizontalRange: NSRange(location: 0, length: 2), verticalRange: NSRange(location: 5, length: 1))

0 commit comments

Comments
 (0)