-
Notifications
You must be signed in to change notification settings - Fork 455
Description
Description
While Mozc clients (TSF, IBus, IMKit ...) are not expected to send key-up events in general, one of notable exceptions is when a single modifier key (e.g. SHIFT) is pressed and released without any other keys.
Steps to reproduce
- Install Mozc
- Open Notepad
- Enable Mozc
- Down SHIFT -> Down M -> Up M -> SHIFT
- Down O -> Up O
- Down Z -> Up Z
- Down C -> Up C
- Down SHIFT -> Up SHIFT
- Down A -> Up A
Expected behavior
- After the step 3, the mode indicator is half-alphanumeric mode.
- After the step 4, the composing text is
M, and the mode indicator is half-alphanumeric mode. - After the step 7, the composing text is
Mozc, and the mode indicator is half-alphanumeric mode. - After the step 8, the composing text is
Mozc, and the mode indicator is Hiragana mode. - After the step 9, the composing text is
Mozcあ, and the mode indicator is Hiragana mode.
Actual behavior
- After the step 3, the mode indicator is half-alphanumeric mode.
- After the step 4, the composing text is
M, and the mode indicator is half-alphanumeric mode. - After the step 7, the composing text is
Mozc, and the mode indicator is half-alphanumeric mode. - After the step 8, the composing text is
Mozc, and the mode indicator is half-alphanumeric mode. - After the step 9, the composing text is
Mozca, and the mode indicator is half-alphanumeric mode.
Version or commit-id
Environment
- OS: Windows 11 25H2
- Related Applications: Notepad
Additional context
The actual condition is when ITfKeyEventSink::OnKeyDown and ITfKeyEventSink::OnKeyUp are called without ITfKeyEventSink::OnTestKeyDown and ITfKeyEventSink::OnTestKeyUp.
mozc/src/win32/tip/tip_text_service.cc
Lines 778 to 817 in f6786ca
| STDMETHODIMP OnTestKeyDown(ITfContext *context, WPARAM wparam, LPARAM lparam, | |
| BOOL *eaten) override { | |
| BOOL dummy_eaten = FALSE; | |
| if (eaten == nullptr) { | |
| eaten = &dummy_eaten; | |
| } | |
| *eaten = FALSE; | |
| return TipKeyeventHandler::OnTestKeyDown(this, context, wparam, lparam, | |
| eaten); | |
| } | |
| // ITfKeyEventSink | |
| STDMETHODIMP OnTestKeyUp(ITfContext *context, WPARAM wparam, LPARAM lparam, | |
| BOOL *eaten) override { | |
| BOOL dummy_eaten = FALSE; | |
| if (eaten == nullptr) { | |
| eaten = &dummy_eaten; | |
| } | |
| *eaten = FALSE; | |
| return TipKeyeventHandler::OnTestKeyUp(this, context, wparam, lparam, | |
| eaten); | |
| } | |
| STDMETHODIMP OnKeyDown(ITfContext *context, WPARAM wparam, LPARAM lparam, | |
| BOOL *eaten) override { | |
| BOOL dummy_eaten = FALSE; | |
| if (eaten == nullptr) { | |
| eaten = &dummy_eaten; | |
| } | |
| *eaten = FALSE; | |
| return TipKeyeventHandler::OnKeyDown(this, context, wparam, lparam, eaten); | |
| } | |
| STDMETHODIMP OnKeyUp(ITfContext *context, WPARAM wparam, LPARAM lparam, | |
| BOOL *eaten) override { | |
| BOOL dummy_eaten = FALSE; | |
| if (eaten == nullptr) { | |
| eaten = &dummy_eaten; | |
| } | |
| *eaten = FALSE; | |
| return TipKeyeventHandler::OnKeyUp(this, context, wparam, lparam, eaten); | |
| } |
This is because the following logic is called only through ITfKeyEventSink::OnTestKeyDown and ITfKeyEventSink::OnTestKeyUp.
mozc/src/win32/base/keyevent_handler.cc
Lines 793 to 823 in f6786ca
| // Although Mozc has not explicitly supported any key-up message, there exist | |
| // some situations where the client has to send key message when it receives | |
| // a key-up message. Currently we have following exceptions. | |
| // - Shift/Control/Alt keys | |
| // The Mozc protocol had originally allowed the client to ignore key-up | |
| // events of these modifier keys but later has changed to expect the | |
| // client to send a key message which contains only modifiers field and | |
| // mode field to support b/2269058 and b/1995170. | |
| if (is_key_down) { | |
| // This is an ugly workaround to determine which key-up message for a | |
| // modifier key should be sent to the server. Currently, the Mozc server | |
| // expects the client to send such a key-up message only when a modifier | |
| // key is released just after the same key is pressed, that is, any other | |
| // key is not pressed between the key-down and key-up of a modifier key. | |
| // Here are some examples, where [D] and [U] mean 'key down' and 'key up'. | |
| // (1) [D]Shift -> [D]A -> [U]Shift -> [U]A | |
| // In this case, only 'A' will be sent to the server | |
| // (2) [D]Shift -> [U]Shift -> [D]A -> [U]A | |
| // In this case, 'Shift' and 'A' will be sent to the server. | |
| // (3) [D]Shift -> [D]Control -> [U]Shift -> [U]Control | |
| // In this case, no key message will be sent to the server. | |
| // (4) [D]Shift -> [D]Control -> [U]Control -> [U]Shift | |
| // In this case, 'Control+Shift' will be sent to the server. Note | |
| // that |KeyEvent::modifier_keys| will contain all the modifier keys | |
| // when the client receives '[U]Control'. | |
| // Unfortunately, it is currently client's responsibility to remember the | |
| // key sequence to generate appropriate key messages as expected by the | |
| // server. Strictly speaking, the Mozc client is actually stateful in | |
| // this sense. | |
| next_state->last_down_key = virtual_key; | |
| } |
As a result, ime_state.last_down_key in the following logic remains to be 0 when ITfKeyEventSink::OnTestKeyDown and ITfKeyEventSink::OnTestKeyUp are completely bypassed, which results in ignoring an isolated modifier key press event at the step 8
mozc/src/win32/base/keyevent_handler.cc
Lines 728 to 752 in f6786ca
| switch (virtual_key.virtual_key()) { | |
| case VK_SHIFT: | |
| case VK_CONTROL: | |
| case VK_MENU: | |
| if (is_key_down) { | |
| // Will not eat this message. | |
| result.succeeded = true; | |
| result.should_be_eaten = false; | |
| result.should_be_sent_to_server = false; | |
| return result; | |
| } | |
| if (ime_state.last_down_key.virtual_key() != virtual_key.virtual_key()) { | |
| // Will not eat this message. | |
| result.succeeded = true; | |
| result.should_be_eaten = false; | |
| result.should_be_sent_to_server = false; | |
| return result; | |
| } | |
| // We will send this message to the server. | |
| result.succeeded = true; | |
| result.should_be_eaten = true; | |
| result.should_be_sent_to_server = true; | |
| return result; | |
| break; | |
| } |