JetBrains / JetBrains/jcef

macOS: Option(⌥)+character keys deliver wrong KeyboardEvent.code (always "KeyA") — native_key_code derived from character, -1 coerced to kVK_ANSI_A

Open
#46 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
371
Forks
52
PR merge metrics
No merged PRs in 30d

Description

## Summary

On macOS, `Option(⌥) + ` keystrokes arrive in a JCEF web view with a **wrong but plausible-looking** `KeyboardEvent.code` — almost always `"KeyA"`. The web app therefore silently executes *the wrong shortcut* instead of doing nothing, which makes the failure very hard to diagnose.

The root cause is in `native/keyboard_utils.cpp`: on macOS the CEF `native_key_code` for character keys is reverse-derived from the **character** (`KeyEvent.getKeyChar()`) instead of the **physical key** (`KeyEvent.getKeyCode()`), and the "unknown character" sentinel `-1` is coerced to `0`, which is a valid virtual key code — `kVK_ANSI_A`.

AWT is not at fault: the `KeyEvent` handed to JCEF already carries the correct `getKeyCode()`. JCEF discards it.

## Environment

* JBR 25.0.4+1-508.27-jcef (bundled with IntelliJ IDEA 2026.1.4), macOS 15 arm64
* Reproduces in **both** in-process and out-of-process JCEF (`native/CefBrowser_N.cpp` and `remote/PlatformUtils.cpp` both call `javaKeyEventToCef`), and in both windowed and OSR mode — so there is no Registry workaround
* Reproduces only with a **Latin input source** (`com.apple.keylayout.ABC` / `.US`). It does **not** reproduce with `com.apple.keylayout.2SetHangul`, which is a strong red herring: it makes the bug look like an IME issue when it is not

## Steps to reproduce

1. Set the macOS input source to **ABC** or **U.S.**
2. Open any JCEF browser on a page that logs `keydown` (`event.key`, `event.code`)
3. Press `⌥N`, `⌥M`, `⌥K`, `⌥1`, `⌥2`, `⌥Space`

**Expected:** `code` = `KeyN`, `KeyM`, `KeyK`, `Digit1`, `Digit2`, `Space`
**Actual:** `code` = `KeyA` for every one of them

## Root cause

[`native/keyboard_utils.cpp`](https://github.com/JetBrains/jcef/blob/dev/native/keyboard_utils.cpp#L1219), macOS branch of `javaKeyEventToCef`:

```cpp
} else {
result->native_key_code = GetMacKeyCodeFromChar(key_char);
if (result->native_key_code == -1)
result->native_key_code = 0; // <-- kVK_ANSI_A == 0
...
}
```

The preceding `if/else if` chain handles ~40 special keys (Backspace, Delete, Enter, Escape, Tab, arrows, PageUp/Down, Home/End, F1–F19, modifiers) via `getKeyCode()`. **Every character key and Space falls into the `else` branch above.**

[`GetMacKeyCodeFromChar`](https://github.com/JetBrains/jcef/blob/dev/native/keyboard_utils.cpp#L172) is a 117-entry lookup whose domain is only **U+000A–U+007E**:

```
sub w8, w0, #0xa
cmp w8, #0x74 ; 0x0A + 0x74 = 0x7E
b.hi -> return -1
```

On a macOS Latin layout the Option modifier rewrites the character to a non-ASCII one, so the lookup always misses. Measured with `UCKeyTranslate` on `com.apple.keylayout.ABC`:

| Keystroke | Resulting char | In table? | `native_key_code` | Resulting `event.code` |
|---|---|---|---|---|
| `⌥A` | `å` U+00E5 | no | `-1` → `0` | `KeyA` (correct by luck) |
| `⌥N` | dead key | no | `-1` → `0` | **`KeyA`** |
| `⌥M` | `µ` U+00B5 | no | `-1` → `0` | **`KeyA`** |
| `⌥K` | `˚` U+02DA | no | `-1` → `0` | **`KeyA`** |
| `⌥Z` | `Ω` U+03A9 | no | `-1` → `0` | **`KeyA`** |
| `⌥1` | `¡` U+00A1 | no | `-1` → `0` | **`KeyA`** |
| `⌥2` | `™` U+2122 | no | `-1` → `0` | **`KeyA`** |
| `⌥Space` | NBSP U+00A0 | no | `-1` → `0` | **`KeyA`** |

Confirmed at instruction level in the shipped `libjcef.dylib` (JBR 25.0.4+1-b508.27, arm64):

```
3dbdc: mov x0, x22
3dbe0: bl GetMacKeyCodeFromChar
3dbe4: cmn w0, #0x1 ; == -1 ?
3dbe8: csel w8, wzr, w0, eq ; yes -> 0
3dbec: str w8, [x21, #0xc] ; native_key_code = 0
```

Downstream this is deterministic and not Chromium's fault: CEF's `TranslateWebKeyEvent` synthesizes an `NSEvent` with `keyCode: native_key_code`, and Chromium's `DomCodeFromNSEvent(0)` maps to `DomCode::US_A` → `code: "KeyA"`.

Note that `⌘` does not suppress the Option layer (`⌘⌥A` still yields `å`), and `⌃`+letter produces control characters U+0001–U+001A which are also outside the table. In practice the only modifier combinations that are safe on character keys are *none*, `Shift`, `⌘`, and `⇧⌘`.

## Why this has gone unnoticed

[`java_tests/tests/junittests/data/keyboard_scenario_mac.json`](https://github.com/JetBrains/jcef/blob/dev/java_tests/tests/junittests/data/keyboard_scenario_mac.json) **encodes the bug as the expected result.** Of the 70 `⌥+` scenarios, **53 expect `"code": "KeyA"`** — including `⌥+N`, `⌥+M`, `⌥+K`, `⌥+Q`, `⌥+1`, `⌥+2`, `⌥+␣`. The 17 that expect a correct `code` are exactly the ones routed through the `getKeyCode()` special-case chain (Escape, Tab, arrows, Home/End, PageUp/Down, modifiers).

The scenario files appear to be recorded from live output via `ScenarioMaker`, so the defect was captured as ground truth and the regression suite now actively protects it. This is presumably why the earlier macOS keyboard fixes ([JBR-5880](https://youtrack.jetbrains.com/issue/JBR-5880), [JBR-5115](https://youtrack.jetbrains.com/issue/JBR-5115)) did not surface it.

Also worth noting: the same JSON shows the AWT events carry the **correct** `keyCode` — `⌥+N` has `keyCode: 78`, `⌥+M` has `77`, `⌥+K` has `75`, `⌥+1` has `49`. The information needed to produce the right answer is already in the event.

## Impact

Any JCEF-hosted web UI that binds `Alt`/`Option` + letter or digit shortcuts is affected, and the failure is silent rather than inert. In our plugin (an ERD editor embedded via JCEF) the observable result is:

* `Alt+KeyN`, `Alt+KeyM`, `Alt+KeyK`, `Alt+Space` all fire the handler bound to `Alt+KeyA`
* `$mod+Alt+Digit1` … `Digit4` all fire the handler bound to `$mod+Alt+KeyA`

So pressing "add table" selects every column instead. Users reasonably report this as "the shortcut does the wrong thing", not "the shortcut doesn't work".

There is no workaround available to the embedder. The corruption happens inside the IDE JVM, so Chromium only ever sees the synthetic `NSEvent` with `keyCode: 0`; `event.key`, `event.code`, `event.keyCode` and `event.which` are all consistently wrong on the JS side, and `navigator.keyboard.getLayoutMap()` cannot help because it maps *from* `code` rather than *to* it. Embedders must either avoid `Alt`+character bindings entirely or route them through an IntelliJ `AnAction` and a custom bridge.

## Suggested fix

Prefer the physical key code, which AWT already provides correctly, and stop inferring it from the character:

1. In the macOS branch, extend the `getKeyCode()`-based mapping to cover `VK_A`–`VK_Z`, `VK_0`–`VK_9` and `VK_SPACE` before falling back to `GetMacKeyCodeFromChar`. `CPlatformResponder` derives `getKeyCode()` from the physical key, so it is correct regardless of the active layout or Option layer.
2. Independently, stop coercing `-1` to `0`. Whatever the fallback ends up being, mapping "unknown" onto a real key (`kVK_ANSI_A`) turns a missing keystroke into a wrong keystroke. Leaving `native_key_code` unset/invalid so Chromium reports `code: ""` would at least be honest.
3. `keyboard_scenario_mac.json` will need its `⌥` expectations regenerated — 53 entries currently assert the buggy value.

## Related issues

* [JBR-5880](https://youtrack.jetbrains.com/issue/JBR-5880) (fixed) — macOS non-English layouts. Same function, different trigger; the Option-layer path on Latin layouts was not covered.
* [JBR-5115](https://youtrack.jetbrains.com/issue/JBR-5115) (fixed) — OSR non-ASCII symbols not processed. Closest in mechanism.
* [JBR-4407](https://youtrack.jetbrains.com/issue/JBR-4407) (duplicate of [JBR-5348](https://youtrack.jetbrains.com/issue/JBR-5348), fixed) — the Linux analogue: wrong `code` in the web view.
* [JBR-4408](https://youtrack.jetbrains.com/issue/JBR-4408) (open) — macOS, modifier+Backspace not reaching the web view.
* #17 (closed) — non-US layouts on macOS in OSR mode.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in native/keyboard_utils.cpp at the macOS branch of javaKeyEventToCef and compare its getKeyCode handling with the AWT values described here. Review java_tests/tests/junittests/data/keyboard_scenario_mac.json and its ScenarioMaker-generated expectations. Done means Option character-key scenarios report the physical codes such as KeyN, Digit1, and Space without regressing the existing special-key cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, java, macos
Domain
desktop, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.