import AppKit
|
import Carbon
|
import Foundation
|
|
enum LauncherHotkeyValidationError: Equatable {
|
case modifierRequired
|
case unsupportedKey
|
case reservedBySystem
|
case duplicate
|
|
var messageKey: String {
|
switch self {
|
case .modifierRequired:
|
return "hotkeys.error.modifierRequired"
|
case .unsupportedKey:
|
return "hotkeys.error.unsupportedKey"
|
case .reservedBySystem:
|
return "hotkeys.error.reserved"
|
case .duplicate:
|
return "hotkeys.error.duplicate"
|
}
|
}
|
}
|
|
enum LauncherHotkeyCustomizationResult: Equatable {
|
case saved
|
case restored
|
case locked
|
case invalid(LauncherHotkeyValidationError)
|
case registrationFailed(OSStatus)
|
|
var messageKey: String {
|
switch self {
|
case .saved:
|
return "hotkeys.saved"
|
case .restored:
|
return "hotkeys.restored"
|
case .locked:
|
return "hotkeys.proRequired"
|
case .invalid(let error):
|
return error.messageKey
|
case .registrationFailed:
|
return "hotkeys.error.registrationFailed"
|
}
|
}
|
}
|
|
enum LauncherHotkeySettings {
|
private static let namespace = "customHotkeys.v1"
|
private static let allowedModifierMask = UInt32(
|
controlKey | optionKey | shiftKey | cmdKey | kEventKeyModifierFnMask
|
)
|
private static let functionKeyCodes: Set<Int> = [
|
kVK_F1, kVK_F2, kVK_F3, kVK_F4, kVK_F5, kVK_F6,
|
kVK_F7, kVK_F8, kVK_F9, kVK_F10, kVK_F11, kVK_F12
|
]
|
|
static func defaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey {
|
kind.hotkey
|
}
|
|
static func effectiveHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey {
|
guard ProEntitlementPolicy.isUnlocked(.customHotkeys),
|
let saved = savedHotkey(for: kind),
|
structuralValidationError(for: saved) == nil,
|
!duplicatesOtherEffectiveHotkey(saved, kind: kind)
|
else {
|
return defaultHotkey(for: kind)
|
}
|
return normalized(saved)
|
}
|
|
static func hasCustomHotkey(for kind: LauncherHotkeyKind) -> Bool {
|
savedHotkey(for: kind) != nil
|
}
|
|
static func savedHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey? {
|
let defaults = UserDefaults.standard
|
guard defaults.object(forKey: keyCodeKey(for: kind)) != nil,
|
defaults.object(forKey: modifiersKey(for: kind)) != nil
|
else {
|
return nil
|
}
|
let keyCode = defaults.integer(forKey: keyCodeKey(for: kind))
|
let modifiers = defaults.integer(forKey: modifiersKey(for: kind))
|
guard keyCode >= 0, modifiers >= 0 else { return nil }
|
return LauncherHotkey(
|
keyCode: UInt32(keyCode),
|
modifiers: UInt32(modifiers) & allowedModifierMask
|
)
|
}
|
|
static func save(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind) {
|
let defaults = UserDefaults.standard
|
defaults.set(Int(hotkey.keyCode), forKey: keyCodeKey(for: kind))
|
defaults.set(Int(hotkey.modifiers & allowedModifierMask), forKey: modifiersKey(for: kind))
|
NotificationCenter.default.post(name: .tagLauncherHotkeyRegistrationChanged, object: nil)
|
}
|
|
static func clearCustomHotkey(for kind: LauncherHotkeyKind) {
|
let defaults = UserDefaults.standard
|
defaults.removeObject(forKey: keyCodeKey(for: kind))
|
defaults.removeObject(forKey: modifiersKey(for: kind))
|
NotificationCenter.default.post(name: .tagLauncherHotkeyRegistrationChanged, object: nil)
|
}
|
|
static func candidate(from event: NSEvent) -> LauncherHotkey {
|
LauncherHotkey(
|
keyCode: UInt32(event.keyCode),
|
modifiers: carbonModifiers(from: event.modifierFlags)
|
)
|
}
|
|
static func validationError(for hotkey: LauncherHotkey, kind: LauncherHotkeyKind) -> LauncherHotkeyValidationError? {
|
let normalizedHotkey = normalized(hotkey)
|
if let structuralError = structuralValidationError(for: normalizedHotkey) {
|
return structuralError
|
}
|
if duplicatesOtherEffectiveHotkey(normalizedHotkey, kind: kind) {
|
return .duplicate
|
}
|
return nil
|
}
|
|
private static func normalized(_ hotkey: LauncherHotkey) -> LauncherHotkey {
|
LauncherHotkey(
|
keyCode: hotkey.keyCode,
|
modifiers: hotkey.modifiers & allowedModifierMask
|
)
|
}
|
|
private static func structuralValidationError(for hotkey: LauncherHotkey) -> LauncherHotkeyValidationError? {
|
guard isSupportedKey(hotkey.keyCode) else {
|
return .unsupportedKey
|
}
|
guard hasSupportedModifierCombination(hotkey) else {
|
return .modifierRequired
|
}
|
guard !isReservedBySystem(hotkey) else {
|
return .reservedBySystem
|
}
|
return nil
|
}
|
|
private static func duplicatesOtherEffectiveHotkey(_ hotkey: LauncherHotkey, kind: LauncherHotkeyKind) -> Bool {
|
let otherKind: LauncherHotkeyKind = kind == .main ? .quickSearch : .main
|
return hotkey == effectiveHotkeyIgnoringDuplicateCheck(for: otherKind)
|
}
|
|
private static func effectiveHotkeyIgnoringDuplicateCheck(for kind: LauncherHotkeyKind) -> LauncherHotkey {
|
guard ProEntitlementPolicy.isUnlocked(.customHotkeys),
|
let saved = savedHotkey(for: kind),
|
structuralValidationError(for: saved) == nil
|
else {
|
return defaultHotkey(for: kind)
|
}
|
return normalized(saved)
|
}
|
|
private static func keyCodeKey(for kind: LauncherHotkeyKind) -> String {
|
"\(namespace).\(kind.rawValue).keyCode"
|
}
|
|
private static func modifiersKey(for kind: LauncherHotkeyKind) -> String {
|
"\(namespace).\(kind.rawValue).modifiers"
|
}
|
|
private static func carbonModifiers(from flags: NSEvent.ModifierFlags) -> UInt32 {
|
let normalized = flags.intersection(.deviceIndependentFlagsMask)
|
var modifiers: UInt32 = 0
|
if normalized.contains(.control) {
|
modifiers |= UInt32(controlKey)
|
}
|
if normalized.contains(.option) {
|
modifiers |= UInt32(optionKey)
|
}
|
if normalized.contains(.shift) {
|
modifiers |= UInt32(shiftKey)
|
}
|
if normalized.contains(.command) {
|
modifiers |= UInt32(cmdKey)
|
}
|
if normalized.contains(.function) {
|
modifiers |= UInt32(kEventKeyModifierFnMask)
|
}
|
return modifiers & allowedModifierMask
|
}
|
|
private static func hasSupportedModifierCombination(_ hotkey: LauncherHotkey) -> Bool {
|
let modifiers = hotkey.modifiers & allowedModifierMask
|
guard modifiers != 0 else { return false }
|
if modifiers == UInt32(shiftKey) {
|
return false
|
}
|
if modifiers == UInt32(kEventKeyModifierFnMask) {
|
return hotkey.keyCode == UInt32(kVK_Space)
|
}
|
return true
|
}
|
|
private static func isSupportedKey(_ keyCode: UInt32) -> Bool {
|
switch Int(keyCode) {
|
case kVK_Space, kVK_Return, kVK_Tab, kVK_Delete:
|
return true
|
case kVK_LeftArrow, kVK_RightArrow, kVK_UpArrow, kVK_DownArrow:
|
return true
|
default:
|
let key = Int(keyCode)
|
return functionKeyCodes.contains(key) || LauncherHotkey.printableKeyCodes.contains(key)
|
}
|
}
|
|
private static func isReservedBySystem(_ hotkey: LauncherHotkey) -> Bool {
|
let key = Int(hotkey.keyCode)
|
let modifiers = hotkey.modifiers & allowedModifierMask
|
let hasCommand = modifiers & UInt32(cmdKey) != 0
|
let hasOption = modifiers & UInt32(optionKey) != 0
|
let hasControl = modifiers & UInt32(controlKey) != 0
|
|
if key == kVK_Space, hasCommand {
|
return true
|
}
|
if key == kVK_Tab, hasCommand {
|
return true
|
}
|
if key == kVK_Escape, hasCommand && hasOption {
|
return true
|
}
|
if [kVK_ANSI_Q, kVK_ANSI_W, kVK_ANSI_H, kVK_ANSI_M].contains(key), hasCommand {
|
return true
|
}
|
if [kVK_LeftArrow, kVK_RightArrow, kVK_UpArrow, kVK_DownArrow].contains(key), hasControl {
|
return true
|
}
|
return false
|
}
|
}
|