Open Bug 880971 Opened 7 years ago Updated 9 months ago

Keys with modifiers works only on simplest Windows’ keyboard layouts


(Core :: DOM: UI Events & Focus Handling, defect)

20 Branch
Windows 7
Not set





(Reporter: nospam-abuse, Unassigned)


(Depends on 5 open bugs)


I extensively tested many keyboard layouts, and found my way through the event handling C++ code.  Now I can match most problems to particular pieces of buggy logic in the input handling code, and I think that I know simple workarounds.  The initial indications of the problems which led me to this whole investigation are listed on  Now, when I read the source code, I found many more problems.

The key reason for the problems is: one should not have used the advice from to implement keyboard input handling.  There are easier solutions which actually work (in contrast to the current approach, which is complicated, and works only time to time).

But now, when this approach is implemented, not everything is lost: one may need to ignore most information gathered with the current logic, but the rest of the code is in pretty good shape — only minor changes are needed to make it run.  The major problem with this bug report is that it is my first for Mozilla, I do not know the protocols, and I need to indicate a few dozens of pieces of buggy code, match them to detectable bugs, and describe workarounds.

The situation is complicated by the fact that apparently, the current logic is build upon a wrong understanding of Windows’ keyboard input architecture.  So let me start here with the description of the architecture.  (Most of this is documented, some is deduced from experiments; all I write below is experimentally checked.)

A) A keyboard driver is just a static table describing what to do with keypresses; the table is packed into a DLL.

A') There is no way to query this table less than in 2²⁶⁴ system calls.

B) The keyboard subsystem remembers 16 bits of state (the “dead key”), extra 3 bits for flip-state of VK_CAPITAL/VK_KANA/VK_NUMLOCK, and has 6 bits of context for each keypress.  Theoretically, with these 6+3 bits of context+flipState, one can define 66 characters per keypress (but apparently, some of these 66 choices are reserved — I experimented with about 20 of them, and 1 of 20 does not work; so it is at most 65).  (Why 66?  With CapsLock on, only one bit is handled; with CapsLock off, there are 6bit.  Roughly speaking…)

B') There is no way to query the 16-bit state of deadKey, and the 6 bits of context.

B") The 6 bits of context are calculated based on bitmap of currently pressed 256 keys by ORing 6-bit mask associated with every virtual key by the keyboard driver.  One cannot query these masks.  (On the other hand, one cannot calculate the context, but one memorize this state by storing the whole 256bit mask.)

C) The application can query which character is produced by pressing a given key in the context of the given 256-bit mask.  However, such querying CHANGES THE STATE.  (Well, everybody knows this — it is what is described in the MicroSoft link above.)

D) The keyboard subsystem is doing some processing in addition to what is described in the driver, but it is mostly harmless for the questions I discuss (I know about handling of AltGr-NUMBER input of Unicode, some bruhaha with different VK delivered on C-Break [and NumLock/Shift on keypad keys], and some default mappings from scancode to VK on exotic keys not defined in the drivers).  I will ignore this part in what follows (it happens on KeyUp).

E) There are some additional ramifications for FarEastern keyboards, but I cannot find any documentation, and do not have context to do experiments.
That’s it.  (Well, one can also mention that the state is PER-DRIVER; if several drivers are active on the system, each keeps a separate state.  But the state is shared by all the windows/threads using THE SAME driver.  As one can imagine, this may lead to nice scenarios if user switches windows in a middle of a deadkey sequence…)

=================== SUMMARY

One cannot (easily) deduce which character is going to be produced by the keydown without examining the following WM_CHAR message.  But nsWindow::OnKeyDown() is, in many cases, examines the WM_CHAR(s), then throws away the results and substitutes some guesses instead.

(It was in, but now I see I need to reexamine the sources.)  In short: instead of throwing away the information completely, one should only do it WHEN NEEDED: for control chars (and 0x7F), 0x20 (delivered by C-Space), and, possibly, for undefined deadKey sequences.  (But I think throwing away undefined deadKey sequences is a bad UI.  But this is a topic for separate discussion.)

Now when the code has changed, I need to reexamine it; sigh…  Anyway, I suspect that the state of mind which lead to throughing away WM_CHAR messages is not applicable nowadays: I think it was to deliver the charCode on keyDown; but doing experiments with, I see that charCode is no longer delivered!  I must go now; when I reexamine the new code, I will comment more.
OK, now I looked again in the sourcecode again.  Here I comment on

The sad truth is that most of assumptions there are more or less wrong.  This explains at least the second bug in


1046       // Dead-key can pair only with such key that produces exactly one base
1047       // character.

This is wrong.  Dead key will pair with everything.

1057         // Depending on the character the followed the dead-key, the keyboard
1058         // driver can produce one composite character, or a dead-key character
1059         // followed by a second character.

The first part is wrong.  A partially recognized combination may produce up to four
16-bit wcharacters.

The second part is wrong.  An unrecognized combination may produce a dead-key character
followed by up to four 16-bit wcharacters produced by the second keypress.

1074             ret = ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)baseChars,
1075                                 ArrayLength(baseChars), 0, mKeyboardLayout);
1076             NS_ASSERTION(ret == 1, "One base character expected");
1082             deadKeyActive = false;

This assertion is not substantiated.  What follows a dead key is not considered for its “deadness”.  So if it produces a defined combination AFTER a dead key, it still may be a dead key when used standalone (as above).

AFAIU, this causes the second bug in

##### The solution: depending on ret, set a correct state of deadKeyActive.
I continue discussion of the source code.

The second important component of the layout-scanner is KeyboardLayout::EnsureDeadKeyActive().

Just look at it.  It is an infinite loop (at least potentially).  On Windows, “a dead key” is just a state in a finite automaton.  The only limitation on the automaton is the number of states (at most 2^16-1).  Infinite loops ARE possible.

And thinking of it more, making a keyboard layout which would make this code loop forever is a very nice keyboard’s UI!  [See below.]

The solution: first of all, it is possible to design a (malicious???) keyboard layout (a finite automaton) such that after pressing a certain key, one will not be able to return back to the “state 0” of the automaton.  So there is no way to code this function bullet-proof; it must be heuristic.  There should be a way to return failure.

Second, duplicating a dead key is not a very good heuristic to get out of the loop.  Think of a dead key meaning “an alternative variant”.  (Good for languages where MOST of the base characters take at most one accent/mogrifier; then there is little sense to use separate deadkeys for ´, `, ¨, ¸ etc. For example, see the aksent Polish keyboard.)  Then it makes sense to make double-press of the dead key to access the OTHER alternative variant (for the — presumably rare — keys which take two different accents).  Or look at how *I* use repeated Green/Ripe keys in

#####  The solution I use in reset_ToUnicode() in

Heuristic 0: from the point of view of UI design, the best candidate on exiting the loop (so producing a printable character) is SPACE.  So it is tried first (without modifiers), up to 5 times.
Heuristic 1: if (0) fails, try what the function ensureDeadKeyActive() does NOW: up to 5 keypresses of deadkey.
Heuristic 2: if (1) fails, try up to 5 keypresses of all 'a'-'z','0'-'1' VKs (without modifiers).
Heuristic 3: if (2) fails, just report the failure upstream.

   [OFF TOPIC: why making a looping-forever deadkey is a good UI.  Consider the situation of “satellite” layouts (as in Green/Ripe mentioned above).  It is very convenient to punch a key 1, 2, or maybe 3 times.  More than 3 is a problematic UI (I punch it as 2+2, or 3+2 if I need to repeat 4 or 5 times).  On the other hand, if a deadkey has repeat semantic: if all presses after the 3rd one produce “the same result”, then one gets 4 states which are all easy-to-type: just long-press the deadkey and let the typematic-repeat bring you into the 4th state.]
I continue discussion of the source code in KeyboardLayout.cpp.

The assertion at
          NS_ASSERTION(ret == 2, "Expecting twice repeated dead-key character");
does not make any sense.  What twice-repeated dead key is doing is completely arbitrary (both in theory and in practice).

The assertion above is met only if the dead key does not define what happens when it is pressed twice.  However, in this case what is returned is just the numeric ID of the state of the finite automaton; in many cases this ID converted to a character bears a resemblance to the FUNCTION of the deadkey, but in other cases it is not going to.  Moreover, the information in question IS ALREADY AVAILABLE prior to this call.  The return value -1 from ToUnicode[Ex]() means that one wchar was put into uniChars[0], and this is exactly what deadChar[0] will contain if the assertion holds.

I expect what this code tries to achieve is to find a visual representation of the FUNCTION of the deadkey.  This may be important in some situations (but I do not think it is ever USED by Mozilla!).  To achieve this, I would try the following heuristics:
  1) Try combining with SPACE to get a defined combination;
  2) Try combining with itself to get a defined combination;
  3) if both fail, use the numeric ID returned in uniChars[0].

Moreover, the code in question does not reset the DeadKeyActive state.  One MUST call EnsureDeadKeyActive(false) after doing such ToUnicode[Ex]() calls.
I continue discussion of the source code in KeyboardLayout.cpp.

920   // Alt+Space key is handled by OS, we shouldn't touch it.
921   if (mModKeyState.IsAlt() && !mModKeyState.IsControl() &&
922       mVirtualKeyCode == VK_SPACE) {
923     return false;
924   }
926   // Bug 818235: Ignore Ctrl+Enter.
927   if (!mModKeyState.IsAlt() && mModKeyState.IsControl() &&
928       mVirtualKeyCode == VK_RETURN) {
929     return false;
930   }

Let me recall again that doing decisions based on IsAlt() and IsControl() alone is not possible.  There may be other modifiers not visible via mModKeyState.  One should drop messages only if the Unicode character they are delivering is the expected one (above, that would be SPACE or '\n' or '\r').
I continue discussion of the source code in KeyboardLayout.cpp.

416         if (WinUtils::PeekMessage(&msg, mMsg.hwnd, WM_KEYFIRST, WM_KEYLAST,
417                                   PM_NOREMOVE | PM_NOYIELD) &&
418             (msg.message == WM_CHAR || msg.message == WM_SYSCHAR ||
419              msg.message == WM_DEADCHAR)) {

This misses the case of WM_SYSDEADCHAR.
I continue discussion of the source code in KeyboardLayout.cpp.

349 VirtualKey::FillKbdState(PBYTE aKbdState,
350                          const ShiftState aShiftState)

If asked to find which character Control-Alt-key is producing: the most probably intent is to find what AltGr-key is producing.  So when both STATE_CONTROL and STATE_ALT are set, one should also do
     aKbdState[VK_RMENU] |= 0x80;

This omission causes the 7th bug in
Ilya, thanks for looking at the code, but could you please file separate bug for each issue you find.
And hopefully add a testcase too, especially if the issue is such that web page can observe it.
Also, testing what other browsers do is valuable.
This bug could become as a meta bug, and depend on those other bugs.

And patches are always very welcome ;)
Depends on: 900750
No longer depends on: 900750
Depends on: 900750
Depends on: 900773
Depends on: 900777
Depends on: 900787
Depends on: 900802
Depends on: 900814
(In reply to Olli Pettay [:smaug] from comment #7)
> Ilya, thanks for looking at the code, but could you please file separate bug
> for each issue you find.


> And hopefully add a testcase too, especially if the issue is such that web
> page can observe it.

Not clear: do you mean visible to JavaScript, or what?  Some of the bugs I noticed would require compiling a special keyboard driver to trigger them.  This I would prefer to leave to somebody else. ;-)  For the rest, I provide references to my (massive) bug report on Firefox keyboard handling in (about the keyboard driver of

> Also, testing what other browsers do is valuable.

IE mostly crashes.  Chrome has an orthogonal collection of bugs.  ;-)

> And patches are always very welcome ;)

I provided only untested patches against the file pulled via

(I will not be able to pull mercurial and/or test the patches.)

> This bug could become as a meta bug, and depend on those other bugs.

SIGH…  This is a big problem.  Firefox' keyboard handling (at least its state in April) is/was COMPLETELY broken.  The individual problems in Comments #1–#6 above are just the tip of the iceberg.  Most of them concern the code which just SHOULD NOT HAVE BEEN CALLED at all.

The main problem is, as I said above, that FF decided to follow the model outlined in  So there is a thick chunk of code which tries to scans the table of keyboard layout (which is impossible to do from the user code — unless one loads the driver's DLL and examines the tables directly; this is how JAVA operates — but their scanning code is also wrong ;-).  THEN it tries to use these (wrong!) tables instead of perfectly correct info provided in WM_(SYS)(DEAD)KEYs.  (Again: this is the state of April; tell me if it changed.)

     [I will summarize how it should have been done in the following message.  I expect that this is a very minor (counting in LOC) edit of the current state; — however, some architectural problem prevent passing the correct info downstream, to the platform independent code.]
(In reply to Ilya Zakharevich from comment #8)
>  THEN it tries to use these
> (wrong!) tables instead of perfectly correct info provided in
> WM_(SYS)(DEAD)KEYs.  (Again: this is the state of April; tell me if it
> changed.)

This should have been WM_(SYS)(DEAD)CHARs, not WM_(SYS)(DEAD)KEYs.
OK, here is the gist of how to proceed with correct keyboard handling for an application which wants both to process input characters, and have bindable special-key “trigger-a-command” events.

(A) On KEYDOWN event, the application should know whether this keypress will contribute to the input of characters, or is a candidate for a “trigger-a-command” event (or both: see below⁽¹⁾).  Some extra bits may be calculated too: is this keypress triggering a deadkey?  (ANOTHER extra bit to calculate: is the delivered character calculatable via Shift/CapsLock/Alt/Ctrl modifier model, or is there some other, platform-specific, modifier present?)

(B) The application should be able to pass the info found in (A) downstream, to the code which handles the mapping of keypresses to commands.  

   [I think the communication channel mentioned in (B) is missing in Firefox…  Correct?]

The absence of communication channel in (B) is a very significant shortcoming.  However, let’s ignore it for a moment, and concentrate on Windows-specific code, i.e., on (A).

(*) Firefox uses the standard message pump: TranslateMessage()/DispatchMessage().  So when WM_(SYS)KEYDOWN is processed for a character-producing keypress, WM_(SYS)(DEAD)CHAR is already sitting in the message queue (if the keypress generates more than one wchar, there may be several WM_(SYS)CHARs in the queue).  [At this moment I ignore the IME messages and UNICHAR; they may be treated in a very similar way.]

(**) Checking for the next keyboard message via PeekMessage() would detect whether one of this (and which one) is present.  So the only problem one has is to detect when this message is a “fake” one, generated for keypresses which are not character-producing (but see ⁽²⁾ below).  Such may be Ctrl-key and Alt-key when the only other modifier is Shift (or none at all).

(**α) The first subcase, of Ctrl without Alt, is very easy: the ONLY fake WM_CHAR for Ctrl are those with character in the range 0x00–0x20 and 0x7F; the only such char which may appear in character-producing keys is 0x20=SPACE.  So to ignore these messages is easy:

(**α*) if the character in WM_(SYS)CHAR is one of 0x00–0x1F or 0x7F, such message should be ignored.

(**α**) If the character is 0x20, this is the ONLY case when the current logic of Mozilla fully makes the sense: in this particular case, one should check that Ctrl is present, and the only other detectable modifier is Shift.  If so, the message should be dropped.

(**β) The second subcase, of Alt without Ctrl, is more subtle. In such case the “SYS” flavor of messages is delivered; in all other respects, the keyboard subsystem ignores the Alt modifier, but keeps ALL other modifiers (even if the keyboard driver defines the binding for Alt-SomeMod-a, the WM_SYS(DEAD)CHAR for the character produced by SomeMod-a is delivered; note that this may be a deadkey, or it may be modified by the preceding deadkey).

(**β*) An application MAY request to get a “real” character associated to AltGr-SomeMod-a, but this is rarely done (e.g., the console is doing this), so an absence of such a refinement is tolerable as the first approximation.

(**β**) So the current Mozilla behaviour of dropping WM_(*)CHAR when Alt-without-Ctrl is present is tolerable.

(**β***) On the other hand, one can do better: since currently a scan of keyboard table is performed anyway, one can check whether the delivered character is one tabulated for the key with the currently present Shift/CapsLock keys; if it is not, then some other [invisible to application] modifier is present, and it makes sense to request the info about the REAL binding of AltGr-SomeMod-a; this may be a little bit tricky if this keypress arrived after a deadkey, since this deadkey is already consumed.

SUMMARY: the application may (and should, in cases 2 and 3) drop the WM_*CHAR messages ONLY in the following cases:

  1) Alt is present, Ctrl is not;
  2) The delivered char is 0x20, Ctrl is present, Alt is not;
  3) The delivered char is in 0x00..0x1F or 0x7F.

In all other cases, what is passed downstream, to the platform-independent code, should be based on the peeked WM_*CHAR (and one should remember that up to 4 WM_[SYS]CHAR may be present).  The information about whether the character is a deadkey or not should be based ONLY on the peeked messages.  With this scheme, the only place where the collected info about the layout table may be used is in “how to name” keys reported downstream.
  ⁽¹⁾ When a keypress is a candidate for both character-event and command-event: Windows treats the keys which produce characters with Alt present (but no Control; but possibly with some other modifiers present!) very specially.  For example, the console reports the Alt-less form on KEYDOWN, and the form-with-Alt on KEYUP (using the flag wFlags=2 of ToUnicode[Ex]()).  So the application should choose whether it is processing Alt-ExtraMod-F as a character-producing key, or as the accessor for a menu entry with the hot-char generated by ExtraMod-F.

Example: on English Windows, load the flavor of Russian keyboard which produces English when ExtraMod is down.  (For example, ExtraMod may be the Mnu=ApplicationMenu key.  This is similar to Assume also that Alt-Mnu-letter produces the corresponding greek letter.  So:
         f gives ф;    (it is a Russian keyboard!)
     Mnu-f gives f;    (so it is the same letter as in the “File” menu entry)
 Alt-Mnu-f gives φ.    (there is no other way to enter φ)
Now, when an application processes Alt-Mnu-f, should it enter φ into an editable field, or should it trigger the “File” menu entry?

  (I think that in the case of Mozilla, when it is KNOWN that tapping Alt activates the menu, such boundary cases should be resolved by preferring entering φ.  To activate “File” of menu, just use Alt Mnu-f instead of Alt-Mnu-f (i.e., release Alt before pressing Mnu-f.)
⁽²⁾ Well, there is also a case when the keypress IS character-producing, but Windows is not (yet) generating WM_*CHAR.  This is the case when one uses Alt-NUMPAD_Digits, or Alt-NUMPAD_PLUS-HEXDIGITS or Alt-NUMPAD_DOT-HEXDIGITS.  Currently Firefox fails spectacularly in these cases.  (The way to detect this is to check the first keydown after keydown of Alt, keeping in mind keyrepeat for Alt; if it does not generate a character, and is one of the keys listed above, keep the flag set as long as appropriate.)
Depends on: 905958
To make the long story (a little bit) short(er), I wrote down a summary of how the kernel delivers keyboard messages, and an explanation of how an application can have a very simple, but almost-robust handling of keyboard messages:,_Part_I:_what_is_the_kernel_doing?
and 6 following section.

  [The trailing ? is a part of URL; I'm afraid autolinkificator would cut it off…]

[Let me recall that no application I checked has decent handling of keyboard messages. Firefox is BY FAR the worst of them (in my test suite, about 38% of situations are handled correctly — and I suspect that about half of these 38% is due to one bug cancelling another…).]
Writing the summary mentioned in preceding comment, I noted that 3 statements I made in this (and dependent) reports are wrong/unprecise:

 a) “The ORed mask” (sometimes I call it “a bitmap”) of modifier flags is not 6-bit, but 8-bit wide.  I actually checked that 7 of these 8 bits actually work; I'm currently working on testing whether the 8th bit works (this would make design of intuitive “orthogonalized” keyboard so much easier).

 b) ToUnicode() call can return a string of arbitrary length.  (Same for TranslateMessage(): it can generate arbitrarily long sequence of WM_(SYS)CHAR.)  I said that the limit is 5 UTF-16 code units (one deadkey ID, plus up to 4 units for expansion of a keypress).  It turns out that the limit of 4 codeunits is due to the front end MSKLC for generation of keyboard layout tables, not the limit of keyboard layout tables themselves.

 c) The finite automaton processing deadkeys is (of course!) not “arbitrary”.  The (only) limitations are that
 • any output resets the state to “no dead key” one;
 • the semantic for “unrecognized input” is not customizable; and
 • it can produce at most 1 output code unit per recognized input code unit (except for “no dead key” state).
Here are the details of the Firefox support of keyboard input (tested with izKeys keyboard v0.61 and FF v22).  The test suite is described in
Firefox fails all but 42 (out of 112) test cases.  (37.5% success rate.)  Successes are marked by ☑.

  AltGr-a                                --->  æ
☑ Shift-AltGr-Space a                    --->  æ        (Shift-AltGr-Space is a prefix equivalent to using AltGr-)
  AltGr-(                                --->  ‘
  AltGr-)                                --->  ’
  AltGr-9                                --->  “
  AltGr-0                                --->  ”

☑ lAlt-lCtrl-a                           --->  α	(Green)
☑ Shift-Space a                          --->  α	(Green)	
  Shift-Space AltGr-a                    --->  ά	(Green AltGr)	
  Shift-Space Shift-Space c              --->  ℂ	(Green × 2 == Blue)
  Shift-Space Shift-Space Shift-Space L  --->  £	(Green × 3 == Business)
  AltGr-$ L                              --->  £	(Business)

☑ Menu-q                                 --->  я	(Ripe)
☑ AltGr-Space q                          --->  я        (Ripe; Or use Shift-AltGr-Space Space q)
☑ AltGr-Space AltGr-Space a              --->  א        (Ripe × 2; Likewise, one can replace AltGr- as above)
  Menu-rCtrl-a			         --->  א        (Ripe × 2)
☑ AltGr-Space AltGr-Space AltGr-Space g  --->  ӷ	(Ripe × 3 == Extra cyrillic)

  AltGr-/ =                              --->  ≠
  AltGr-^ =                              --->  ≝	(Amplified)
☑ AltGr-` *                              --->  ⋉	(AddLeft)
  AltGr-" a                              --->  ä	(Or      Shift-AltGr-Space " a)

  Shift-Space Shift-Space 2              --->  ∫	(Blue)
  AltGr-; 2                              --->  ∫	(Blue)
  AltGr-^ 2                              --->  ∬	(Amplified Blue)
  Shift-Space Shift-Space AltGr-2        --->  ∭	(Blue AltGr; same with AltGr-;)
  AltGr-^ AltGr-2                        --->  ⨜	(Amplified Blue AltGr; 0.63 has ⨌)

☑ Menu-Right				 --->  →
☑ Menu-Shift-Right			 --->  ⇒
☑ Menu-Insert				 --->  ↔
☑ Menu-Delete				 --->  ⟷
  Menu-Enter				 --->  ↕

  AltGr-KeyPad6				 --->  ┤	(With NumLock)
  AltGr-Menu-KeyPad6			 --->  ╣	(With NumLock; Menu=Double)
  AltGr-rCtrl-KeyPad6			 --->  ╢	(With NumLock; rCtrl=[flip]DoubleToRight)
  AltGr-Menu-rCtrl-KeyPad6		 --->  ╡	(With NumLock; Double and flip DoubleToRight)
☑ Menu-KeyPad6				 --->  ┫	(With NumLock; no-AltGr means Bold)
  Menu-rCtrl-KeyPad6			 --->  ┨	(With NumLock; [this and the next line are exchanged at v0.63])
  lAlt-rCtrl-KeyPad6			 --->  ┥	(With NumLock; lAlt must be added to loner rCtrl)

☑ lAlt-rCtrl-Right			 --->  (▌)	(Draw a frame by whitespace on black background)

☑ Menu-F3				 --->  —	(3rd dash == em)
☑ Menu-Shift-F3				 --->  ( )	(⅓em whitespace)

☑ AltGr-Right n				 --->  ɳ	(Hook)
☑ AltGr-Left n				 --->  ɲ	(Hook)
☑ AltGr-Up i				 --->  ⁱ
☑ AltGr-Down i				 --->  ᵢ
☑ AltGr-Down I				 --->  ɪ	(SmallCaps)
  AltGr-Up AltGr-e			 --->  ə	(Upgrade to phonetic)
☑ AltGr-Right u				 --->  ᴝ	(rotate)
  AltGr-Right AltGr-u			 --->  ᴞ	(rotate)

☑ Shift-Space 1				 --->  (¹)	(Green)
  Shift-Space Ctrl-(Shift-)i		 --->  (ⁱ)   	(Green Ctrl)
  Shift-Space Shift-Space Ctrl-(Shift-)i --->  (ᴵ)	(Blue Ctrl)
☑ AltGr-Space 1				 --->  (₁)   	(Ripe)
  AltGr-Space Ctrl-(Shift-)i		 --->  (ᵢ)	(Ripe Ctrl)

  Shift-Space Shift-Space Shift-Space Ctrl-@ --->  ␀
  AltGr-Space AltGr-Space Enter		 --->  ⏎
  AltGr-Space AltGr-Space AltGr-Space Enter --->  ↲

☑ Shift-Space Space			 ---> ␣
  Shift-Space AltGr-Space		 ---> ⍽
  Shift-Space Shift-Space Space		 ---> ␠
  Shift-Space Shift-Space AltGr-Space	 ---> ␢
  AltGr-Space Space			 ---> ( )	(NBSP)
☑ AltGr-Space Shift-Space		 ---> ( )	(U+202f NNBSP ⅙em thin)
☑ AltGr-Space AltGr-Space Space		 ---> ( )	(U+2009 ⅙em thin)
☑ AltGr-Space AltGr-Space Shift-Space	 ---> ( )	(U+200A ⅛em hair)

  AltGr-' Space				 ---> ´		(Spacing diacritic)
  e AltGr-' AltGr-'			 ---> (é)	(Combining-´)
  AltGr-' Shift-Space			 ---> ʼ		(2nd Spacing diacritic)
  AltGr-' AltGr-Space			 ---> ʾ		(3rd Spacing diacritic)
  = AltGr-^ '				 ---> (=̑)	(Technical Combining-^)
  = AltGr-^ "				 ---> (=͒)	(2nd Technical Combining-^)
  = AltGr-^ AltGr-'			 ---> (=͡)	(3rd Technical Combining-^)
  = AltGr-^ AltGr-"			 ---> (=̽)	(4th Technical Combining-^)

  AltGr-_				 ---> —		(m-dash)
☑ Shift-Space _				 ---> –		(n-dash)
  Shift-Space Shift-Space _		 ---> ―		(horizontal dash; not before 0.62)

  AltGr-Menu-C				 ---> ℂ		(Double-struck)
  AltGr-rCtrl-C				 ---> 
Component: Event Handling → User events and focus handling
You need to log in before you can comment on or make changes to this bug.