본문 바로가기
C.W.K.
Stream
Lesson 02 of 04 · published

RegisterEventHotKey 는 실제로 어떻게 도나

~13 min · carbon, register-event-hotkey, event-handler, modifiers

Level 0식은 부싯돌
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"조합을 등록하고, 처리기 하나 달고, 모디파이어를 옮겨. 그리고 Carbon 이 이 파일 밖으로 못 새게 해."

등록하는 부분

움직이는 부품이 셋이야. 먼저 조합을 등록해. RegisterEventHotKey 가 받는 건 넷이야. 가상 키 코드, 모디파이어 마스크, 내가 지어낸 EventHotKeyID, 그리고 이벤트를 보낼 대상. EventHotKeyID 는 시그니처에 작은 숫자 하나 붙인 건데, 조합끼리 구분하려고 쓰는 거고. 그리고 나중에 등록을 풀 때 쓸 참조를 돌려줘. 등록은 OSStatus 를 반환하는데, noErr 이 아니면 그게 누구랑 부딪혔다는 첫 신호야. 그 얘기는 다음 레슨에서 하고.

조합은 여럿, 처리기는 하나

둘째, kEventHotKeyPressed 에 Carbon 이벤트 처리기를 딱 하나만 달아. 등록해둔 조합이 전부 그 하나로 들어와. 그 안에서 이벤트에 실려온 EventHotKeyID 를 꺼내고, 그 번호가 어느 매크로인지 찾아서 돌리면 돼. 단축키마다 처리기를 다는 게 아니라 처리기 하나에 번호로 갈라 보내는 거지. 그래야 Carbon 이 닿는 면적이 작게 유지돼. 설치 한 번, 콜백 하나, 번호에서 동작으로 가는 사전 하나.

Carbon 모디파이어 마스크는 Cocoa 플래그가 아니야. 이게 그 유명한 함정이야. NSEvent.ModifierFlags 랑 Carbon 의 cmdKey / optionKey / controlKey / shiftKey 는 비트 배치가 달라. Cocoa 플래그 값을 RegisterEventHotKey 에 그대로 넣으면 조합이 엉뚱하게 등록돼. Cmd+K 가 딴 키가 되거나, 아무것도 안 되거나. 경계에서 하나씩 손으로 옮겨줘야 해.

등록하고, 풀고, 다시 걸고

셋째, 참조를 쥐고 있어. 매크로의 조합이 바뀌면 UnregisterEventHotKey 로 옛 참조를 풀고 새로 등록해. 풀지 않은 등록을 남기면 안 돼. OS 는 내가 잊어버린 조합도 계속 쏘거든. 이걸 전부 타입 하나로 감싸. 번호에서 참조로, 번호에서 동작으로 가는 사전을 쥔 핫키 센터 같은 걸로. Carbon 은 전부 그 안에서 살고, 앱 나머지는 키 코드랑 모디파이어랑 클로저만 건네주면 돼. Carbon 은 import 조차 안 하고.

Code

HotkeyCenter — Carbon 을 만지는 유일한 파일·swift
import Carbon.HIToolbox

final class HotkeyCenter {
    private var refs: [UInt32: EventHotKeyRef] = [:]    // 내 번호 -> OS 등록
    private var actions: [UInt32: () -> Void] = [:]     // 내 번호 -> 돌릴 동작

    init() {
        var spec = EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
                                 eventKind: UInt32(kEventHotKeyPressed))
        // 처리기는 하나만 단다. 안에서 번호로 갈라 보낸다.
        InstallEventHandler(GetEventDispatcherTarget(), { _, event, userData in
            var hkID = EventHotKeyID()
            GetEventParameter(event, EventParamName(kEventParamDirectObject),
                              EventParamType(typeEventHotKeyID), nil,
                              MemoryLayout<EventHotKeyID>.size, nil, &hkID)
            let center = Unmanaged<HotkeyCenter>
                .fromOpaque(userData!).takeUnretainedValue()
            center.actions[hkID.id]?()      // 이 조합에 걸린 매크로를 돌린다
            return noErr
        }, 1, &spec, Unmanaged.passUnretained(self).toOpaque(), nil)
    }

    func register(id: UInt32, keyCode: UInt32, carbonModifiers: UInt32,
                  action: @escaping () -> Void) {
        let hkID = EventHotKeyID(signature: OSType(0x464C4E54), id: id)  // 'FLNT'
        var ref: EventHotKeyRef?
        let status = RegisterEventHotKey(keyCode, carbonModifiers, hkID,
                                         GetEventDispatcherTarget(), 0, &ref)
        guard status == noErr, let ref else { return }   // noErr 아니면 부딪힌 것
        refs[id] = ref
        actions[id] = action
    }

    func unregister(id: UInt32) {
        if let ref = refs.removeValue(forKey: id) { UnregisterEventHotKey(ref) }
        actions[id] = nil    // 푼 뒤에도 계속 쏘는 조합을 남기지 않는다
    }
}
경계에서 Cocoa 모디파이어를 Carbon 마스크로 옮기기·swift
// 오후 하나를 통째로 잡아먹는 버그: NSEvent 플래그를 그대로 넣기.
// Carbon 쪽은 비트가 다르다. 하나씩 손으로 옮긴다.
func carbonModifiers(from flags: NSEvent.ModifierFlags) -> UInt32 {
    var m: UInt32 = 0
    if flags.contains(.command) { m |= UInt32(cmdKey) }
    if flags.contains(.option)  { m |= UInt32(optionKey) }
    if flags.contains(.control) { m |= UInt32(controlKey) }
    if flags.contains(.shift)   { m |= UInt32(shiftKey) }
    return m
}

Exercise

사용자가 Cmd+Shift+K 를 붙인 순간부터 Cmd+Shift+L 로 바꾸는 순간까지, 매크로 하나의 단축키가 어떻게 사는지 훑어봐. Carbon 이 끼어드는 지점을 전부 짚어. 모디파이어 옮기기, 등록, 처리기 발동, 옛 조합 풀기, 새 조합 등록. Cocoa 랑 Carbon 마스크가 안 맞으면 정확히 어디서 조용히 깨지고, 푸는 걸 잊으면 어디서 유령 조합이 계속 쏘게 될까?
Hint
마스크가 안 맞는 건 등록에서 물어. 엉뚱한 비트로 등록되니까 사용자가 고른 키는 아무것도 안 하고 엉뚱한 조합이 대신 발동하지. 유령 조합은 다시 걸 때 나와. 옛 참조를 안 풀고 새로 등록하면 OS 가 옛 것도 계속 쏘거든. 그래서 언제나 풀고 나서 등록, 그것도 매크로의 안 바뀌는 번호로.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.