From 8cd9cd96c1cb99e0a3ca6d0ec1f6ec95f75e81c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A4=80=EC=98=81?= Date: Sat, 21 Mar 2026 21:16:10 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20keyboard=20shortcuts=20not=20working=20wi?= =?UTF-8?q?th=20Korean=20(=ED=95=9C=EA=B8=80)=20input=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a non-Latin input source like Korean 두벌식 is active, event.charactersIgnoringModifiers returns Hangul characters (e.g. ㅅ for T key) instead of Latin letters. This caused all character-based shortcut matching to fail — Cmd+T, Cmd+D, Cmd+1-9, Ctrl+N/P, etc. Root cause: KeyboardLayout.character(forKeyCode:modifierFlags:) assumed CJK input sources lack kTISPropertyUnicodeKeyLayoutData, but Korean 두벌식 has it. UCKeyTranslate returned Korean characters and the ASCII fallback was never reached. Fix: - KeyboardLayout.character(): check result is ASCII before accepting; fall through to TISCopyCurrentASCIICapableKeyboardInputSource() when the current source returns non-ASCII characters - Add KeyboardLayout.normalizedCharacters(for:) helper that normalizes event.charactersIgnoringModifiers for shortcut comparison - Apply normalization in handleCustomShortcut (AppDelegate), BrowserPanelView omnibar key handler, and BrowserPopupWindowController Cmd+W handler Co-Authored-By: Claude Opus 4.6 (1M context) --- Sources/AppDelegate.swift | 6 ++++- Sources/KeyboardLayout.swift | 26 +++++++++++++++---- Sources/Panels/BrowserPanelView.swift | 5 +++- .../Panels/BrowserPopupWindowController.swift | 2 +- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index d7da8180..a628d303 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -8889,7 +8889,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private func handleCustomShortcut(event: NSEvent) -> Bool { // `charactersIgnoringModifiers` can be nil for some synthetic NSEvents and certain special keys. // Treat nil as "" and rely on keyCode/layout-aware fallback logic where needed. - let chars = (event.charactersIgnoringModifiers ?? "").lowercased() + // When a non-Latin input source is active (Korean, Chinese, Japanese, etc.), + // charactersIgnoringModifiers returns non-ASCII characters that never match + // Latin shortcut keys. Normalize via KeyboardLayout so downstream comparisons + // (Cmd+1-9, Ctrl+1-9, omnibar N/P, command palette, etc.) work correctly. + let chars = KeyboardLayout.normalizedCharacters(for: event) let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) let hasControl = flags.contains(.control) let hasCommand = flags.contains(.command) diff --git a/Sources/KeyboardLayout.swift b/Sources/KeyboardLayout.swift index 2a04e981..42e05b93 100644 --- a/Sources/KeyboardLayout.swift +++ b/Sources/KeyboardLayout.swift @@ -15,18 +15,22 @@ class KeyboardLayout { /// Translate a physical keyCode to the character AppKit would use for shortcut matching, /// preserving command-aware layouts such as "Dvorak - QWERTY Command". - /// CJK input sources (Korean, Chinese, Japanese) lack kTISPropertyUnicodeKeyLayoutData, - /// so we fall back to TISCopyCurrentASCIICapableKeyboardInputSource() in that case. + /// Some CJK input sources lack kTISPropertyUnicodeKeyLayoutData, and others (Korean + /// 두벌식) have it but UCKeyTranslate still returns non-ASCII characters. In either + /// case we fall back to TISCopyCurrentASCIICapableKeyboardInputSource(). static func character( forKeyCode keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags = [] ) -> String? { if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(), - let result = characterFromInputSource(source, forKeyCode: keyCode, modifierFlags: modifierFlags) { + let result = characterFromInputSource(source, forKeyCode: keyCode, modifierFlags: modifierFlags), + result.allSatisfy(\.isASCII) { return result } - // Current input source has no Unicode layout data (e.g. Korean, Chinese, Japanese IME). - // Fall back to the ASCII-capable source so shortcut matching still works. + // Current input source has no Unicode layout data or returned a non-ASCII + // character (e.g. Korean 두벌식 has layout data but UCKeyTranslate still + // produces Hangul). Fall back to the ASCII-capable source so shortcut + // matching still works. if let asciiSource = TISCopyCurrentASCIICapableKeyboardInputSource()?.takeRetainedValue(), let result = characterFromInputSource(asciiSource, forKeyCode: keyCode, modifierFlags: modifierFlags) { return result @@ -34,6 +38,18 @@ class KeyboardLayout { return nil } + /// Return the ASCII-normalized equivalent of `event.charactersIgnoringModifiers`, + /// falling back through the ASCII-capable input source for non-Latin input methods. + /// Use this wherever code compares raw event characters against Latin shortcut keys. + static func normalizedCharacters(for event: NSEvent) -> String { + let raw = (event.charactersIgnoringModifiers ?? "").lowercased() + if raw.allSatisfy(\.isASCII) { return raw } + if let layoutChar = character(forKeyCode: event.keyCode, modifierFlags: []) { + return layoutChar + } + return raw + } + private static func characterFromInputSource( _ source: TISInputSource, forKeyCode keyCode: UInt16, diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 70694a0e..42c8cf4d 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -3735,7 +3735,10 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable { #endif let keyCode = event.keyCode let modifiers = event.modifierFlags.intersection([.command, .control, .shift, .option, .function]) - let lowered = event.charactersIgnoringModifiers?.lowercased() ?? "" + // When a non-Latin input source is active (Korean, Chinese, Japanese), + // charactersIgnoringModifiers returns non-ASCII characters. Normalize + // via KeyboardLayout so Cmd/Ctrl+N/P navigation works across input sources. + let lowered = KeyboardLayout.normalizedCharacters(for: event) let hasCommandOrControl = modifiers.contains(.command) || modifiers.contains(.control) // Cmd/Ctrl+N and Cmd/Ctrl+P should repeat while held. diff --git a/Sources/Panels/BrowserPopupWindowController.swift b/Sources/Panels/BrowserPopupWindowController.swift index 0c97d5a8..b457ba85 100644 --- a/Sources/Panels/BrowserPopupWindowController.swift +++ b/Sources/Panels/BrowserPopupWindowController.swift @@ -50,7 +50,7 @@ private class BrowserPopupPanel: NSPanel { // Cmd+W: close this popup panel only let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) if flags == .command, - event.charactersIgnoringModifiers == "w" { + KeyboardLayout.normalizedCharacters(for: event) == "w" { #if DEBUG dlog("popup.panel.cmdW close") #endif