Skip to content

Commit

Permalink
feat: add shortcut modifier side
Browse files Browse the repository at this point in the history
  • Loading branch information
decodism committed Nov 23, 2023
1 parent b84b6c9 commit db84081
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 59 deletions.
14 changes: 14 additions & 0 deletions src/api-wrappers/HelperExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,17 @@ extension Optional where Wrapped == String {
return (self ?? "").localizedStandardCompare(string ?? "")
}
}

extension NSEvent.ModifierFlags {
static let leftShift = Self(rawValue: UInt(NX_DEVICELSHIFTKEYMASK))
static let rightShift = Self(rawValue: UInt(NX_DEVICERSHIFTKEYMASK))

static let leftControl = Self(rawValue: UInt(NX_DEVICELCTLKEYMASK))
static let rightControl = Self(rawValue: UInt(NX_DEVICERCTLKEYMASK))

static let leftOption = Self(rawValue: UInt(NX_DEVICELALTKEYMASK))
static let rightOption = Self(rawValue: UInt(NX_DEVICERALTKEYMASK))

static let leftCommand = Self(rawValue: UInt(NX_DEVICELCMDKEYMASK))
static let rightCommand = Self(rawValue: UInt(NX_DEVICERCMDKEYMASK))
}
5 changes: 4 additions & 1 deletion src/logic/ATShortcut.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ class ATShortcut {
self.index = index
}

func matches(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool) -> Bool {
func matches(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool, _ shortcutScope: ShortcutScope) -> Bool {
guard shortcutScope == scope else {
return false
}
if let id = id, let shortcutState = shortcutState {
let shortcutIndex = Int(id.id)
let shortcutId = Array(KeyboardEvents.globalShortcutsIds).first { $0.value == shortcutIndex }!.key
Expand Down
20 changes: 20 additions & 0 deletions src/logic/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ class Preferences {
"rowsCount": rowCountDependingOnScreenRatio(),
"windowMinWidthInRow": "15",
"windowMaxWidthInRow": "30",
"shortcutModifierSide": ShortcutModifierSidePreference.any.rawValue,
"shortcutModifierSide2": ShortcutModifierSidePreference.any.rawValue,
"shortcutModifierSide3": ShortcutModifierSidePreference.any.rawValue,
"shortcutModifierSide4": ShortcutModifierSidePreference.any.rawValue,
"shortcutModifierSide5": ShortcutModifierSidePreference.any.rawValue,
"shortcutStyle": ShortcutStylePreference.focusOnRelease.rawValue,
"shortcutStyle2": ShortcutStylePreference.focusOnRelease.rawValue,
"shortcutStyle3": ShortcutStylePreference.focusOnRelease.rawValue,
Expand Down Expand Up @@ -156,6 +161,7 @@ class Preferences {
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 windowOrder: [WindowOrderPreference] { ["windowOrder", "windowOrder2", "windowOrder3", "windowOrder4", "windowOrder5"].map { defaults.macroPref($0, WindowOrderPreference.allCases) } }
static var shortcutModifierSide: [ShortcutModifierSidePreference] { ["shortcutModifierSide", "shortcutModifierSide2", "shortcutModifierSide3", "shortcutModifierSide4", "shortcutModifierSide5"].map { defaults.macroPref($0, ShortcutModifierSidePreference.allCases) } }
static var shortcutStyle: [ShortcutStylePreference] { ["shortcutStyle", "shortcutStyle2", "shortcutStyle3", "shortcutStyle4", "shortcutStyle5"].map { defaults.macroPref($0, ShortcutStylePreference.allCases) } }
static var menubarIcon: MenubarIconPreference { defaults.macroPref("menubarIcon", MenubarIconPreference.allCases) }

Expand Down Expand Up @@ -465,6 +471,20 @@ enum MenubarIconPreference: String, CaseIterable, MacroPreference {
}
}

enum ShortcutModifierSidePreference: String, CaseIterable, MacroPreference {
case any = "0"
case left = "1"
case right = "2"

var localizedString: LocalizedString {
switch self {
case .any: return NSLocalizedString("", comment: "")
case .left: return NSLocalizedString("L", comment: "")
case .right: return NSLocalizedString("R", comment: "")
}
}
}

enum ShortcutStylePreference: String, CaseIterable, MacroPreference {
case focusOnRelease = "0"
case doNothingOnRelease = "1"
Expand Down
136 changes: 83 additions & 53 deletions src/logic/events/KeyboardEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,12 @@ class KeyboardEvents {
static var hotKeyReleasedEventHandler: EventHandlerRef?
static var localMonitor: Any!

static func addGlobalShortcut(_ controlId: String, _ shortcut: Shortcut) {
addGlobalHandlerIfNeeded(shortcut)
registerHotKeyIfNeeded(controlId, shortcut)
}

static func removeGlobalShortcut(_ controlId: String, _ shortcut: Shortcut) {
unregisterHotKeyIfNeeded(controlId, shortcut)
removeHandlerIfNeeded()
}

private static func unregisterHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
if shortcut.keyCode != .none {
UnregisterEventHotKey(eventHotKeyRefs[controlId]!)
eventHotKeyRefs[controlId] = nil
}
}

static func registerHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
if shortcut.keyCode != .none {
static func addGlobalShortcutIfNeeded(_ controlId: String, _ shortcut: Shortcut, checkEnabled: Bool = true, checkAnyModifierSide: Bool = true) {
if
shortcut.keyCode != .none, eventHotKeyRefs[controlId] == nil,
!checkEnabled || !App.app.globalShortcutsAreDisabled,
!checkAnyModifierSide || Preferences.shortcutModifierSide[Preferences.nameToIndex(controlId)] == .any
{
let id = globalShortcutsIds[controlId]!
let hotkeyId = EventHotKeyID(signature: signature, id: UInt32(id))
let key = shortcut.carbonKeyCode
Expand All @@ -54,12 +41,22 @@ class KeyboardEvents {
}
}

static func removeGlobalShortcutIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
if shortcut.keyCode != .none, eventHotKeyRefs[controlId] != nil {
UnregisterEventHotKey(eventHotKeyRefs[controlId]!)
eventHotKeyRefs[controlId] = nil
}
}

static func toggleGlobalShortcuts(_ shouldDisable: Bool) {
if shouldDisable != App.app.globalShortcutsAreDisabled {
let fn = shouldDisable ? unregisterHotKeyIfNeeded : registerHotKeyIfNeeded
for shortcutId in globalShortcutsIds.keys {
if let shortcut = ControlsTab.shortcuts[shortcutId]?.shortcut {
fn(shortcutId, shortcut)
if shouldDisable {
removeGlobalShortcutIfNeeded(shortcutId, shortcut)
} else {
addGlobalShortcutIfNeeded(shortcutId, shortcut, checkEnabled: false)
}
}
}
debugPrint("toggleGlobalShortcuts", shouldDisable)
Expand All @@ -69,15 +66,33 @@ class KeyboardEvents {

static func addEventHandlers() {
addLocalMonitorForKeyDownAndKeyUp()
addGlobalHandler()
addCgEventTapForModifierFlags()
}

private static func addLocalMonitorForKeyDownAndKeyUp() {
localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .keyUp]) { (event: NSEvent) in
let someShortcutTriggered = handleEvent(nil, nil, event.type == .keyDown ? UInt32(event.keyCode) : nil, cocoaToCarbonFlags(event.modifierFlags), event.type == .keyDown ? event.isARepeat : false)
let someShortcutTriggered = handleEvent(nil, nil, event.type == .keyDown ? UInt32(event.keyCode) : nil, cocoaToCarbonFlags(event.modifierFlags), event.type == .keyDown ? event.isARepeat : false, .local)
return someShortcutTriggered ? nil : event
}
}

private static func addGlobalHandler() {
var hotKeyPressedEventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed))]
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
var id = EventHotKeyID()
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
handleEvent(id, .down, nil, nil, false, .global)
return noErr
}, hotKeyPressedEventTypes.count, &hotKeyPressedEventTypes, nil, &hotKeyPressedEventHandler)
var hotKeyReleasedEventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyReleased))]
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
var id = EventHotKeyID()
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
handleEvent(id, .up, nil, nil, false, .global)
return noErr
}, hotKeyReleasedEventTypes.count, &hotKeyReleasedEventTypes, nil, &hotKeyReleasedEventHandler)
}

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

private static func addGlobalHandlerIfNeeded(_ shortcut: Shortcut) {
if shortcut.keyCode != .none && hotKeyPressedEventHandler == nil {
var eventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed))]
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
var id = EventHotKeyID()
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
handleEvent(id, .down, nil, nil, false)
return noErr
}, eventTypes.count, &eventTypes, nil, &hotKeyPressedEventHandler)
fileprivate func handleShortcutModifierSide(_ modifiers: NSEvent.ModifierFlags) {
let sideModifiers: [(any: NSEvent.ModifierFlags, left: NSEvent.ModifierFlags, right: NSEvent.ModifierFlags)] = [
(.shift, .leftShift, .rightShift),
(.control, .leftControl, .rightControl),
(.option, .leftOption, .rightOption),
(.command, .leftCommand, .rightCommand)
]
var removeShortcuts = [(id: String, shortcut: Shortcut)]()
var addShortcuts = [(id: String, shortcut: Shortcut)]()
for shortcutIndex in 0...4 {
let shortcutModifierSide = Preferences.shortcutModifierSide[shortcutIndex]
guard shortcutModifierSide != .any else {
continue
}
if shortcut.keyCode != .none && hotKeyReleasedEventHandler == nil {
var eventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyReleased))]
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
var id = EventHotKeyID()
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
handleEvent(id, .up, nil, nil, false)
return noErr
}, eventTypes.count, &eventTypes, nil, &hotKeyReleasedEventHandler)
let holdShortcutId = Preferences.indexToName("holdShortcut", shortcutIndex)
let nextWindowShortcutId = Preferences.indexToName("nextWindowShortcut", shortcutIndex)
guard
let holdShortcut = ControlsTab.shortcuts[holdShortcutId],
let nextWindowShortcut = ControlsTab.shortcuts[nextWindowShortcutId]
else {
continue
}
}

private static func removeHandlerIfNeeded() {
let globalShortcuts = ControlsTab.shortcuts.values.filter { $0.scope == .global }
if let hotKeyPressedEventHandler_ = hotKeyPressedEventHandler, let hotKeyReleasedEventHandler_ = hotKeyReleasedEventHandler,
(globalShortcuts.allSatisfy { $0.shortcut.keyCode == .none }) {
RemoveEventHandler(hotKeyPressedEventHandler_)
hotKeyPressedEventHandler = nil
RemoveEventHandler(hotKeyReleasedEventHandler_)
hotKeyReleasedEventHandler = nil
if
(sideModifiers.filter {
holdShortcut.shortcut.modifierFlags.contains($0.any)
}.allSatisfy {
modifiers.contains(shortcutModifierSide == .left ? $0.left : $0.right) &&
!modifiers.contains(shortcutModifierSide == .left ? $0.right : $0.left)
})
{
addShortcuts.append((nextWindowShortcutId, nextWindowShortcut.shortcut))
} else {
if holdShortcut.shouldTrigger() {
holdShortcut.executeAction(false)
}
removeShortcuts.append((nextWindowShortcutId, nextWindowShortcut.shortcut))
}
}
removeShortcuts.forEach {
KeyboardEvents.removeGlobalShortcutIfNeeded($0.id, $0.shortcut)
}
addShortcuts.forEach {
KeyboardEvents.addGlobalShortcutIfNeeded($0.id, $0.shortcut, checkAnyModifierSide: false)
}
}

@discardableResult
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool) -> Bool {
fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: UInt32?, _ isARepeat: Bool, _ shortcutScope: ShortcutScope) -> Bool {
var someShortcutTriggered = false
for shortcut in ControlsTab.shortcuts.values {
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat) && shortcut.shouldTrigger() {
if shortcut.matches(id, shortcutState, keyCode, modifiers, isARepeat, shortcutScope) && shortcut.shouldTrigger() {
shortcut.executeAction(isARepeat)
someShortcutTriggered = true
}
Expand All @@ -145,8 +174,9 @@ fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutStat

fileprivate func cgEventFlagsChangedHandler(proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
if type == .flagsChanged {
let modifiers = cocoaToCarbonFlags(NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue)))
handleEvent(nil, nil, nil, modifiers, false)
let modifiers = NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue))
handleShortcutModifierSide(modifiers)
handleEvent(nil, nil, nil, cocoaToCarbonFlags(modifiers), false, .global)
} else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) {
CGEvent.tapEnable(tap: eventTap!, enable: true)
}
Expand Down
18 changes: 17 additions & 1 deletion src/ui/generic-components/CustomRecorderControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class CustomRecorderControl: RecorderControl, RecorderControlDelegate {
}
}

private func isShortcutAlreadyAssigned(_ shortcut: Shortcut) -> ATShortcut? {
func isShortcutAlreadyAssigned(_ shortcut: Shortcut, shortcutModifierSide: ShortcutModifierSidePreference? = nil) -> ATShortcut? {
let comboShortcutName = id.starts(with: "holdShortcut") ?
Preferences.indexToName("nextWindowShortcut", Preferences.nameToIndex(id)) :
(id.starts(with: "nextWindowShortcut") ?
Expand Down Expand Up @@ -117,6 +117,22 @@ class CustomRecorderControl: RecorderControl, RecorderControlDelegate {
} else if !ControlsTab.combinedModifiersMatch(shortcut2.carbonModifierFlags, shortcut.carbonModifierFlags) {
return false
}
if (id.starts(with: "holdShortcut") || id.starts(with: "nextWindowShortcut")) && id2.starts(with: "nextWindowShortcut") {
let shortcutIndex = Preferences.nameToIndex(id)
let shortcutIndex2 = Preferences.nameToIndex(id2)
let shortcutModifierSide = shortcutModifierSide ?? Preferences.shortcutModifierSide[shortcutIndex]
let shortcutModifierSide2 = Preferences.shortcutModifierSide[shortcutIndex2]
if
shortcutModifierSide != .any,
shortcutModifierSide2 != .any,
shortcutModifierSide != shortcutModifierSide2,
let holdShortcut = id.starts(with: "holdShortcut") ? shortcut : comboShortcut,
let holdShortcut2 = ControlsTab.shortcuts[Preferences.indexToName("holdShortcut", shortcutIndex2)]?.shortcut,
holdShortcut.carbonModifierFlags & holdShortcut2.carbonModifierFlags > 0
{
return false
}
}
return true
})?
.value
Expand Down
8 changes: 8 additions & 0 deletions src/ui/preferences-window/LabelAndControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ class LabelAndControl: NSObject {
ControlsTab.shortcutControls[rawName] = (input, labelText)
return views
}

static func makeRecorder(_ labelText: String, _ rawName: String, _ shortcutString: String, _ clearable: Bool = true) -> CustomRecorderControl {
let input = CustomRecorderControl(shortcutString, clearable, rawName)
_ = setupControl(input, rawName, extraAction: { _ in ControlsTab.shortcutChangedCallback(input) })
ControlsTab.shortcutChangedCallback(input)
ControlsTab.shortcutControls[rawName] = (input, labelText)
return input
}

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

0 comments on commit db84081

Please sign in to comment.