// HotkeyManager.swift // GreenshotMac // // System wide hotkeys through the Carbon hotkey API, the macOS counterpart of // Greenshot's HotkeyControl (RegisterHotKey on Win32). import AppKit import Carbon.HIToolbox final class HotkeyManager { static let shared = HotkeyManager() private struct Registration { let ref: EventHotKeyRef let handler: () -> Void } private var registrations: [UInt32: Registration] = [:] private var nextID: UInt32 = 1 private var eventHandler: EventHandlerRef? private init() {} /// Installs the process wide Carbon event handler. Safe to call more than once. private func installHandlerIfNeeded() { guard eventHandler == nil else { return } var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)) let callback: EventHandlerUPP = { _, event, _ in var hotkeyID = EventHotKeyID() let status = GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout.size, nil, &hotkeyID) guard status == noErr else { return status } DispatchQueue.main.async { HotkeyManager.shared.fire(id: hotkeyID.id) } return noErr } InstallEventHandler(GetApplicationEventTarget(), callback, 1, &eventType, nil, &eventHandler) } fileprivate func fire(id: UInt32) { registrations[id]?.handler() } /// Registers a hotkey and returns its identifier, or nil when the combination is taken. @discardableResult func register(_ spec: HotkeySpec, handler: @escaping () -> Void) -> UInt32? { guard !spec.isEmpty else { return nil } installHandlerIfNeeded() let id = nextID nextID += 1 var hotkeyRef: EventHotKeyRef? let hotkeyID = EventHotKeyID(signature: OSType(0x47535354), id: id) // 'GSST' let status = RegisterEventHotKey(spec.keyCode, spec.modifiers, hotkeyID, GetApplicationEventTarget(), 0, &hotkeyRef) guard status == noErr, let hotkeyRef else { return nil } registrations[id] = Registration(ref: hotkeyRef, handler: handler) return id } func unregister(_ id: UInt32) { guard let registration = registrations.removeValue(forKey: id) else { return } UnregisterEventHotKey(registration.ref) } func unregisterAll() { for id in registrations.keys { unregister(id) } } // MARK: - Display helpers static func description(of spec: HotkeySpec) -> String { guard !spec.isEmpty else { return "None" } var text = "" if spec.modifiers & UInt32(controlKey) != 0 { text += "⌃" } if spec.modifiers & UInt32(optionKey) != 0 { text += "⌥" } if spec.modifiers & UInt32(shiftKey) != 0 { text += "⇧" } if spec.modifiers & UInt32(cmdKey) != 0 { text += "⌘" } text += keyName(for: spec.keyCode) return text } static func keyName(for keyCode: UInt32) -> String { let names: [UInt32: String] = [ UInt32(kVK_ANSI_A): "A", UInt32(kVK_ANSI_B): "B", UInt32(kVK_ANSI_C): "C", UInt32(kVK_ANSI_D): "D", UInt32(kVK_ANSI_E): "E", UInt32(kVK_ANSI_F): "F", UInt32(kVK_ANSI_G): "G", UInt32(kVK_ANSI_H): "H", UInt32(kVK_ANSI_I): "I", UInt32(kVK_ANSI_J): "J", UInt32(kVK_ANSI_K): "K", UInt32(kVK_ANSI_L): "L", UInt32(kVK_ANSI_M): "M", UInt32(kVK_ANSI_N): "N", UInt32(kVK_ANSI_O): "O", UInt32(kVK_ANSI_P): "P", UInt32(kVK_ANSI_Q): "Q", UInt32(kVK_ANSI_R): "R", UInt32(kVK_ANSI_S): "S", UInt32(kVK_ANSI_T): "T", UInt32(kVK_ANSI_U): "U", UInt32(kVK_ANSI_V): "V", UInt32(kVK_ANSI_W): "W", UInt32(kVK_ANSI_X): "X", UInt32(kVK_ANSI_Y): "Y", UInt32(kVK_ANSI_Z): "Z", UInt32(kVK_ANSI_1): "1", UInt32(kVK_ANSI_2): "2", UInt32(kVK_ANSI_3): "3", UInt32(kVK_ANSI_4): "4", UInt32(kVK_ANSI_5): "5", UInt32(kVK_ANSI_6): "6", UInt32(kVK_ANSI_7): "7", UInt32(kVK_ANSI_8): "8", UInt32(kVK_ANSI_9): "9", UInt32(kVK_ANSI_0): "0", UInt32(kVK_Space): "Space", UInt32(kVK_Return): "Return", UInt32(kVK_Escape): "Esc", UInt32(kVK_F1): "F1", UInt32(kVK_F2): "F2", UInt32(kVK_F3): "F3", UInt32(kVK_F4): "F4", UInt32(kVK_F5): "F5", UInt32(kVK_F6): "F6", UInt32(kVK_F7): "F7", UInt32(kVK_F8): "F8", UInt32(kVK_F9): "F9", UInt32(kVK_F10): "F10", UInt32(kVK_F11): "F11", UInt32(kVK_F12): "F12" ] return names[keyCode] ?? "Key \(keyCode)" } /// Converts AppKit modifier flags to the Carbon mask used by RegisterEventHotKey. static func carbonModifiers(from flags: NSEvent.ModifierFlags) -> UInt32 { var modifiers: UInt32 = 0 if flags.contains(.command) { modifiers |= UInt32(cmdKey) } if flags.contains(.option) { modifiers |= UInt32(optionKey) } if flags.contains(.control) { modifiers |= UInt32(controlKey) } if flags.contains(.shift) { modifiers |= UInt32(shiftKey) } return modifiers } } /// Greenshot's Windows defaults (PrintScreen based) have no direct macOS equivalent, /// so these mirror the Greenshot key layout using ⌘⇧ combinations that macOS leaves free. enum DefaultHotkeys { static let region = HotkeySpec(keyCode: UInt32(kVK_ANSI_4), modifiers: UInt32(cmdKey | shiftKey | optionKey)) static let fullscreen = HotkeySpec(keyCode: UInt32(kVK_ANSI_3), modifiers: UInt32(cmdKey | shiftKey | optionKey)) static let window = HotkeySpec(keyCode: UInt32(kVK_ANSI_5), modifiers: UInt32(cmdKey | shiftKey | optionKey)) static let lastRegion = HotkeySpec(keyCode: UInt32(kVK_ANSI_6), modifiers: UInt32(cmdKey | shiftKey | optionKey)) }