Skip to content

Commit db84081

Browse files
committed
feat: add shortcut modifier side
1 parent b84b6c9 commit db84081

File tree

7 files changed

+176
-59
lines changed

7 files changed

+176
-59
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
@@ -86,6 +86,11 @@ class Preferences {
8686
"rowsCount": rowCountDependingOnScreenRatio(),
8787
"windowMinWidthInRow": "15",
8888
"windowMaxWidthInRow": "30",
89+
"shortcutModifierSide": ShortcutModifierSidePreference.any.rawValue,
90+
"shortcutModifierSide2": ShortcutModifierSidePreference.any.rawValue,
91+
"shortcutModifierSide3": ShortcutModifierSidePreference.any.rawValue,
92+
"shortcutModifierSide4": ShortcutModifierSidePreference.any.rawValue,
93+
"shortcutModifierSide5": ShortcutModifierSidePreference.any.rawValue,
8994
"shortcutStyle": ShortcutStylePreference.focusOnRelease.rawValue,
9095
"shortcutStyle2": ShortcutStylePreference.focusOnRelease.rawValue,
9196
"shortcutStyle3": ShortcutStylePreference.focusOnRelease.rawValue,
@@ -156,6 +161,7 @@ class Preferences {
156161
static var showHiddenWindows: [ShowHowPreference] { ["showHiddenWindows", "showHiddenWindows2", "showHiddenWindows3", "showHiddenWindows4", "showHiddenWindows5"].map { defaults.macroPref($0, ShowHowPreference.allCases) } }
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) } }
164+
static var shortcutModifierSide: [ShortcutModifierSidePreference] { ["shortcutModifierSide", "shortcutModifierSide2", "shortcutModifierSide3", "shortcutModifierSide4", "shortcutModifierSide5"].map { defaults.macroPref($0, ShortcutModifierSidePreference.allCases) } }
159165
static var shortcutStyle: [ShortcutStylePreference] { ["shortcutStyle", "shortcutStyle2", "shortcutStyle3", "shortcutStyle4", "shortcutStyle5"].map { defaults.macroPref($0, ShortcutStylePreference.allCases) } }
160166
static var menubarIcon: MenubarIconPreference { defaults.macroPref("menubarIcon", MenubarIconPreference.allCases) }
161167

@@ -465,6 +471,20 @@ enum MenubarIconPreference: String, CaseIterable, MacroPreference {
465471
}
466472
}
467473

474+
enum ShortcutModifierSidePreference: String, CaseIterable, MacroPreference {
475+
case any = "0"
476+
case left = "1"
477+
case right = "2"
478+
479+
var localizedString: LocalizedString {
480+
switch self {
481+
case .any: return NSLocalizedString("", comment: "")
482+
case .left: return NSLocalizedString("L", comment: "")
483+
case .right: return NSLocalizedString("R", comment: "")
484+
}
485+
}
486+
}
487+
468488
enum ShortcutStylePreference: String, CaseIterable, MacroPreference {
469489
case focusOnRelease = "0"
470490
case doNothingOnRelease = "1"

src/logic/events/KeyboardEvents.swift

Lines changed: 83 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,12 @@ class KeyboardEvents {
2424
static var hotKeyReleasedEventHandler: EventHandlerRef?
2525
static var localMonitor: Any!
2626

27-
static func addGlobalShortcut(_ controlId: String, _ shortcut: Shortcut) {
28-
addGlobalHandlerIfNeeded(shortcut)
29-
registerHotKeyIfNeeded(controlId, shortcut)
30-
}
31-
32-
static func removeGlobalShortcut(_ controlId: String, _ shortcut: Shortcut) {
33-
unregisterHotKeyIfNeeded(controlId, shortcut)
34-
removeHandlerIfNeeded()
35-
}
36-
37-
private static func unregisterHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
38-
if shortcut.keyCode != .none {
39-
UnregisterEventHotKey(eventHotKeyRefs[controlId]!)
40-
eventHotKeyRefs[controlId] = nil
41-
}
42-
}
43-
44-
static func registerHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
45-
if shortcut.keyCode != .none {
27+
static func addGlobalShortcutIfNeeded(_ controlId: String, _ shortcut: Shortcut, checkEnabled: Bool = true, checkAnyModifierSide: Bool = true) {
28+
if
29+
shortcut.keyCode != .none, eventHotKeyRefs[controlId] == nil,
30+
!checkEnabled || !App.app.globalShortcutsAreDisabled,
31+
!checkAnyModifierSide || Preferences.shortcutModifierSide[Preferences.nameToIndex(controlId)] == .any
32+
{
4633
let id = globalShortcutsIds[controlId]!
4734
let hotkeyId = EventHotKeyID(signature: signature, id: UInt32(id))
4835
let key = shortcut.carbonKeyCode
@@ -54,12 +41,22 @@ class KeyboardEvents {
5441
}
5542
}
5643

44+
static func removeGlobalShortcutIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
45+
if shortcut.keyCode != .none, eventHotKeyRefs[controlId] != nil {
46+
UnregisterEventHotKey(eventHotKeyRefs[controlId]!)
47+
eventHotKeyRefs[controlId] = nil
48+
}
49+
}
50+
5751
static func toggleGlobalShortcuts(_ shouldDisable: Bool) {
5852
if shouldDisable != App.app.globalShortcutsAreDisabled {
59-
let fn = shouldDisable ? unregisterHotKeyIfNeeded : registerHotKeyIfNeeded
6053
for shortcutId in globalShortcutsIds.keys {
6154
if let shortcut = ControlsTab.shortcuts[shortcutId]?.shortcut {
62-
fn(shortcutId, shortcut)
55+
if shouldDisable {
56+
removeGlobalShortcutIfNeeded(shortcutId, shortcut)
57+
} else {
58+
addGlobalShortcutIfNeeded(shortcutId, shortcut, checkEnabled: false)
59+
}
6360
}
6461
}
6562
debugPrint("toggleGlobalShortcuts", shouldDisable)
@@ -69,15 +66,33 @@ class KeyboardEvents {
6966

7067
static func addEventHandlers() {
7168
addLocalMonitorForKeyDownAndKeyUp()
69+
addGlobalHandler()
7270
addCgEventTapForModifierFlags()
7371
}
7472

7573
private static func addLocalMonitorForKeyDownAndKeyUp() {
7674
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)
75+
let someShortcutTriggered = handleEvent(nil, nil, event.type == .keyDown ? UInt32(event.keyCode) : nil, cocoaToCarbonFlags(event.modifierFlags), event.type == .keyDown ? event.isARepeat : false, .local)
7876
return someShortcutTriggered ? nil : event
7977
}
8078
}
79+
80+
private static func addGlobalHandler() {
81+
var hotKeyPressedEventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed))]
82+
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
83+
var id = EventHotKeyID()
84+
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
85+
handleEvent(id, .down, nil, nil, false, .global)
86+
return noErr
87+
}, hotKeyPressedEventTypes.count, &hotKeyPressedEventTypes, nil, &hotKeyPressedEventHandler)
88+
var hotKeyReleasedEventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyReleased))]
89+
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
90+
var id = EventHotKeyID()
91+
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
92+
handleEvent(id, .up, nil, nil, false, .global)
93+
return noErr
94+
}, hotKeyReleasedEventTypes.count, &hotKeyReleasedEventTypes, nil, &hotKeyReleasedEventHandler)
95+
}
8196

8297
private static func addCgEventTapForModifierFlags() {
8398
let eventMask = [CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
@@ -97,45 +112,59 @@ class KeyboardEvents {
97112
App.app.restart()
98113
}
99114
}
115+
}
100116

101-
private static func addGlobalHandlerIfNeeded(_ shortcut: Shortcut) {
102-
if shortcut.keyCode != .none && hotKeyPressedEventHandler == nil {
103-
var eventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed))]
104-
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
105-
var id = EventHotKeyID()
106-
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
107-
handleEvent(id, .down, nil, nil, false)
108-
return noErr
109-
}, eventTypes.count, &eventTypes, nil, &hotKeyPressedEventHandler)
117+
fileprivate func handleShortcutModifierSide(_ modifiers: NSEvent.ModifierFlags) {
118+
let sideModifiers: [(any: NSEvent.ModifierFlags, left: NSEvent.ModifierFlags, right: NSEvent.ModifierFlags)] = [
119+
(.shift, .leftShift, .rightShift),
120+
(.control, .leftControl, .rightControl),
121+
(.option, .leftOption, .rightOption),
122+
(.command, .leftCommand, .rightCommand)
123+
]
124+
var removeShortcuts = [(id: String, shortcut: Shortcut)]()
125+
var addShortcuts = [(id: String, shortcut: Shortcut)]()
126+
for shortcutIndex in 0...4 {
127+
let shortcutModifierSide = Preferences.shortcutModifierSide[shortcutIndex]
128+
guard shortcutModifierSide != .any else {
129+
continue
110130
}
111-
if shortcut.keyCode != .none && hotKeyReleasedEventHandler == nil {
112-
var eventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyReleased))]
113-
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
114-
var id = EventHotKeyID()
115-
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
116-
handleEvent(id, .up, nil, nil, false)
117-
return noErr
118-
}, eventTypes.count, &eventTypes, nil, &hotKeyReleasedEventHandler)
131+
let holdShortcutId = Preferences.indexToName("holdShortcut", shortcutIndex)
132+
let nextWindowShortcutId = Preferences.indexToName("nextWindowShortcut", shortcutIndex)
133+
guard
134+
let holdShortcut = ControlsTab.shortcuts[holdShortcutId],
135+
let nextWindowShortcut = ControlsTab.shortcuts[nextWindowShortcutId]
136+
else {
137+
continue
119138
}
120-
}
121-
122-
private static func removeHandlerIfNeeded() {
123-
let globalShortcuts = ControlsTab.shortcuts.values.filter { $0.scope == .global }
124-
if let hotKeyPressedEventHandler_ = hotKeyPressedEventHandler, let hotKeyReleasedEventHandler_ = hotKeyReleasedEventHandler,
125-
(globalShortcuts.allSatisfy { $0.shortcut.keyCode == .none }) {
126-
RemoveEventHandler(hotKeyPressedEventHandler_)
127-
hotKeyPressedEventHandler = nil
128-
RemoveEventHandler(hotKeyReleasedEventHandler_)
129-
hotKeyReleasedEventHandler = nil
139+
if
140+
(sideModifiers.filter {
141+
holdShortcut.shortcut.modifierFlags.contains($0.any)
142+
}.allSatisfy {
143+
modifiers.contains(shortcutModifierSide == .left ? $0.left : $0.right) &&
144+
!modifiers.contains(shortcutModifierSide == .left ? $0.right : $0.left)
145+
})
146+
{
147+
addShortcuts.append((nextWindowShortcutId, nextWindowShortcut.shortcut))
148+
} else {
149+
if holdShortcut.shouldTrigger() {
150+
holdShortcut.executeAction(false)
151+
}
152+
removeShortcuts.append((nextWindowShortcutId, nextWindowShortcut.shortcut))
130153
}
131154
}
155+
removeShortcuts.forEach {
156+
KeyboardEvents.removeGlobalShortcutIfNeeded($0.id, $0.shortcut)
157+
}
158+
addShortcuts.forEach {
159+
KeyboardEvents.addGlobalShortcutIfNeeded($0.id, $0.shortcut, checkAnyModifierSide: false)
160+
}
132161
}
133162

134163
@discardableResult
135-
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool) -> Bool {
164+
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool, _ shortcutScope: ShortcutScope) -> Bool {
136165
var someShortcutTriggered = false
137166
for shortcut in ControlsTab.shortcuts.values {
138-
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat) && shortcut.shouldTrigger() {
167+
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat, shortcutScope) && shortcut.shouldTrigger() {
139168
shortcut.executeAction(isARepeat)
140169
someShortcutTriggered = true
141170
}
@@ -145,8 +174,9 @@ fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutStat
145174

146175
fileprivate func cgEventFlagsChangedHandler(proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
147176
if type == .flagsChanged {
148-
let modifiers = cocoaToCarbonFlags(NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue)))
149-
handleEvent(nil, nil, nil, modifiers, false)
177+
let modifiers = NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue))
178+
handleShortcutModifierSide(modifiers)
179+
handleEvent(nil, nil, nil, cocoaToCarbonFlags(modifiers), false, .global)
150180
} else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) {
151181
CGEvent.tapEnable(tap: eventTap!, enable: true)
152182
}

src/ui/generic-components/CustomRecorderControl.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class CustomRecorderControl: RecorderControl, RecorderControlDelegate {
8787
}
8888
}
8989

90-
private func isShortcutAlreadyAssigned(_ shortcut: Shortcut) -> ATShortcut? {
90+
func isShortcutAlreadyAssigned(_ shortcut: Shortcut, shortcutModifierSide: ShortcutModifierSidePreference? = nil) -> ATShortcut? {
9191
let comboShortcutName = id.starts(with: "holdShortcut") ?
9292
Preferences.indexToName("nextWindowShortcut", Preferences.nameToIndex(id)) :
9393
(id.starts(with: "nextWindowShortcut") ?
@@ -117,6 +117,22 @@ class CustomRecorderControl: RecorderControl, RecorderControlDelegate {
117117
} else if !ControlsTab.combinedModifiersMatch(shortcut2.carbonModifierFlags, shortcut.carbonModifierFlags) {
118118
return false
119119
}
120+
if (id.starts(with: "holdShortcut") || id.starts(with: "nextWindowShortcut")) && id2.starts(with: "nextWindowShortcut") {
121+
let shortcutIndex = Preferences.nameToIndex(id)
122+
let shortcutIndex2 = Preferences.nameToIndex(id2)
123+
let shortcutModifierSide = shortcutModifierSide ?? Preferences.shortcutModifierSide[shortcutIndex]
124+
let shortcutModifierSide2 = Preferences.shortcutModifierSide[shortcutIndex2]
125+
if
126+
shortcutModifierSide != .any,
127+
shortcutModifierSide2 != .any,
128+
shortcutModifierSide != shortcutModifierSide2,
129+
let holdShortcut = id.starts(with: "holdShortcut") ? shortcut : comboShortcut,
130+
let holdShortcut2 = ControlsTab.shortcuts[Preferences.indexToName("holdShortcut", shortcutIndex2)]?.shortcut,
131+
holdShortcut.carbonModifierFlags & holdShortcut2.carbonModifierFlags > 0
132+
{
133+
return false
134+
}
135+
}
120136
return true
121137
})?
122138
.value

src/ui/preferences-window/LabelAndControl.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ class LabelAndControl: NSObject {
1515
ControlsTab.shortcutControls[rawName] = (input, labelText)
1616
return views
1717
}
18+
19+
static func makeRecorder(_ labelText: String, _ rawName: String, _ shortcutString: String, _ clearable: Bool = true) -> CustomRecorderControl {
20+
let input = CustomRecorderControl(shortcutString, clearable, rawName)
21+
_ = setupControl(input, rawName, extraAction: { _ in ControlsTab.shortcutChangedCallback(input) })
22+
ControlsTab.shortcutChangedCallback(input)
23+
ControlsTab.shortcutControls[rawName] = (input, labelText)
24+
return input
25+
}
1826

1927
static func makeLabelWithCheckbox(_ labelText: String, _ rawName: String, extraAction: ActionClosure? = nil, labelPosition: LabelPosition = .leftWithSeparator) -> [NSView] {
2028
let checkbox = NSButton(checkboxWithTitle: labelPosition == .right ? labelText : "", target: nil, action: nil)

0 commit comments

Comments
 (0)