Initial commit
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
// 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<EventHotKeyID>.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))
|
||||
}
|
||||
Reference in New Issue
Block a user