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

feat: add shortcut modifier side #2993

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
21 changes: 19 additions & 2 deletions src/ui/generic-components/CustomRecorderControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ class CustomRecorderControl: RecorderControl, RecorderControlDelegate {
func alertIfSameShortcutAlreadyAssigned(_ shortcut: Shortcut, _ shortcutAlreadyAssigned: ATShortcut) {
let isArrowKeys = ["←", "→", "↑", "↓"].contains(shortcutAlreadyAssigned.id)
let isVimKeys = shortcutAlreadyAssigned.id.starts(with: "vimCycle")
let isShortcutKeys = shortcutAlreadyAssigned.id.starts(with: "nextWindowShortcut")
let existingShortcutLabel = ControlsTab.shortcutControls[shortcutAlreadyAssigned.id]
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = NSLocalizedString("Conflicting shortcut", comment: "")
alert.informativeText = String(format: NSLocalizedString("Shortcut already assigned to another action: %@", comment: ""),
(isArrowKeys ? "Arrow keys" : (isVimKeys ? "Vim keys" : existingShortcutLabel!.1)).replacingOccurrences(of: " ", with: "\u{00A0}"))
(isArrowKeys ? "Arrow keys" : (isVimKeys ? "Vim keys" : (isShortcutKeys ? "Shortcut \(Preferences.nameToIndex(shortcutAlreadyAssigned.id) + 1) keys" : existingShortcutLabel!.1))).replacingOccurrences(of: " ", with: "\u{00A0}"))
if !id.starts(with: "holdShortcut") {
alert.addButton(withTitle: NSLocalizedString("Unassign existing shortcut and continue", comment: "")).setAccessibilityFocused(true)
}
Expand Down Expand Up @@ -86,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 @@ -116,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