Fix keyboard shortcuts not working with Korean (한글) input mode

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) <noreply@anthropic.com>
This commit is contained in:
최준영 2026-03-21 21:16:10 +09:00
parent 6ff81579d9
commit 8cd9cd96c1
4 changed files with 31 additions and 8 deletions

View file

@ -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)

View file

@ -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,

View file

@ -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.

View file

@ -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