| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/base/ime/input_method_win_base.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <cwctype> |
| |
| #include "base/auto_reset.h" |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/command_line.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/win/windows_version.h" |
| #include "ui/base/ime/ime_bridge.h" |
| #include "ui/base/ime/ime_engine_handler_interface.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/base/ime/win/on_screen_keyboard_display_manager_input_pane.h" |
| #include "ui/base/ime/win/on_screen_keyboard_display_manager_tab_tip.h" |
| #include "ui/base/ime/win/tsf_input_scope.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/display/win/screen_win.h" |
| #include "ui/events/event.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/keycodes/keyboard_codes.h" |
| #include "ui/gfx/win/hwnd_util.h" |
| |
| namespace ui { |
| namespace { |
| |
| // Extra number of chars before and after selection (or composition) range which |
| // is returned to IME for improving conversion accuracy. |
| constexpr size_t kExtraNumberOfChars = 20; |
| |
| std::unique_ptr<InputMethodKeyboardController> CreateKeyboardController( |
| HWND toplevel_window_handle) { |
| if (base::FeatureList::IsEnabled(features::kInputPaneOnScreenKeyboard) && |
| base::win::GetVersion() >= base::win::VERSION_WIN10_RS4) { |
| return std::make_unique<OnScreenKeyboardDisplayManagerInputPane>( |
| toplevel_window_handle); |
| } else if (base::win::GetVersion() >= base::win::VERSION_WIN8) { |
| return std::make_unique<OnScreenKeyboardDisplayManagerTabTip>( |
| toplevel_window_handle); |
| } |
| return nullptr; |
| } |
| |
| // Checks if a given primary language ID is a RTL language. |
| bool IsRTLPrimaryLangID(LANGID lang) { |
| switch (lang) { |
| case LANG_ARABIC: |
| case LANG_HEBREW: |
| case LANG_PERSIAN: |
| case LANG_SYRIAC: |
| case LANG_UIGHUR: |
| case LANG_URDU: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| // Checks if there is any RTL keyboard layout installed in the system. |
| bool IsRTLKeyboardLayoutInstalled() { |
| static enum { |
| RTL_KEYBOARD_LAYOUT_NOT_INITIALIZED, |
| RTL_KEYBOARD_LAYOUT_INSTALLED, |
| RTL_KEYBOARD_LAYOUT_NOT_INSTALLED, |
| RTL_KEYBOARD_LAYOUT_ERROR, |
| } layout = RTL_KEYBOARD_LAYOUT_NOT_INITIALIZED; |
| |
| // Cache the result value. |
| if (layout != RTL_KEYBOARD_LAYOUT_NOT_INITIALIZED) |
| return layout == RTL_KEYBOARD_LAYOUT_INSTALLED; |
| |
| // Retrieve the number of layouts installed in this system. |
| int size = GetKeyboardLayoutList(0, NULL); |
| if (size <= 0) { |
| layout = RTL_KEYBOARD_LAYOUT_ERROR; |
| return false; |
| } |
| |
| // Retrieve the keyboard layouts in an array and check if there is an RTL |
| // layout in it. |
| std::unique_ptr<HKL[]> layouts(new HKL[size]); |
| ::GetKeyboardLayoutList(size, layouts.get()); |
| for (int i = 0; i < size; ++i) { |
| if (IsRTLPrimaryLangID( |
| PRIMARYLANGID(reinterpret_cast<uintptr_t>(layouts[i])))) { |
| layout = RTL_KEYBOARD_LAYOUT_INSTALLED; |
| return true; |
| } |
| } |
| |
| layout = RTL_KEYBOARD_LAYOUT_NOT_INSTALLED; |
| return false; |
| } |
| |
| // Checks if the user pressed both Ctrl and right or left Shift keys to |
| // request to change the text direction and layout alignment explicitly. |
| // Returns true if only a Ctrl key and a Shift key are down. The desired text |
| // direction will be stored in |*direction|. |
| bool IsCtrlShiftPressed(base::i18n::TextDirection* direction) { |
| uint8_t keystate[256]; |
| if (!::GetKeyboardState(&keystate[0])) |
| return false; |
| |
| // To check if a user is pressing only a control key and a right-shift key |
| // (or a left-shift key), we use the steps below: |
| // 1. Check if a user is pressing a control key and a right-shift key (or |
| // a left-shift key). |
| // 2. If the condition 1 is true, we should check if there are any other |
| // keys pressed at the same time. |
| // To ignore the keys checked in 1, we set their status to 0 before |
| // checking the key status. |
| const int kKeyDownMask = 0x80; |
| if ((keystate[VK_CONTROL] & kKeyDownMask) == 0) |
| return false; |
| |
| if (keystate[VK_RSHIFT] & kKeyDownMask) { |
| keystate[VK_RSHIFT] = 0; |
| *direction = base::i18n::RIGHT_TO_LEFT; |
| } else if (keystate[VK_LSHIFT] & kKeyDownMask) { |
| keystate[VK_LSHIFT] = 0; |
| *direction = base::i18n::LEFT_TO_RIGHT; |
| } else { |
| return false; |
| } |
| |
| // Scan the key status to find pressed keys. We should abandon changing the |
| // text direction when there are other pressed keys. |
| // This code is executed only when a user is pressing a control key and a |
| // right-shift key (or a left-shift key), i.e. we should ignore the status of |
| // the keys: VK_SHIFT, VK_CONTROL, VK_RCONTROL, and VK_LCONTROL. |
| // So, we reset their status to 0 and ignore them. |
| keystate[VK_SHIFT] = 0; |
| keystate[VK_CONTROL] = 0; |
| keystate[VK_RCONTROL] = 0; |
| keystate[VK_LCONTROL] = 0; |
| // Oddly, pressing F10 in another application seemingly breaks all subsequent |
| // calls to GetKeyboardState regarding the state of the F22 key. Perhaps this |
| // defect is limited to my keyboard driver, but ignoring F22 should be okay. |
| keystate[VK_F22] = 0; |
| for (int i = 0; i <= VK_PACKET; ++i) { |
| if (keystate[i] & kKeyDownMask) |
| return false; |
| } |
| return true; |
| } |
| |
| ui::EventDispatchDetails DispatcherDestroyedDetails() { |
| ui::EventDispatchDetails dispatcher_details; |
| dispatcher_details.dispatcher_destroyed = true; |
| return dispatcher_details; |
| } |
| |
| } // namespace |
| |
| InputMethodWinBase::InputMethodWinBase(internal::InputMethodDelegate* delegate, |
| HWND toplevel_window_handle) |
| : InputMethodBase(delegate, |
| CreateKeyboardController(toplevel_window_handle)), |
| toplevel_window_handle_(toplevel_window_handle), |
| pending_requested_direction_(base::i18n::UNKNOWN_DIRECTION), |
| weak_ptr_factory_(this) {} |
| |
| InputMethodWinBase::~InputMethodWinBase() {} |
| |
| void InputMethodWinBase::OnDidChangeFocusedClient( |
| TextInputClient* focused_before, |
| TextInputClient* focused) { |
| if (focused_before != focused) |
| accept_carriage_return_ = false; |
| } |
| |
| ui::EventDispatchDetails InputMethodWinBase::DispatchKeyEvent( |
| ui::KeyEvent* event) { |
| MSG native_key_event = MSGFromKeyEvent(event); |
| if (native_key_event.message == WM_CHAR) { |
| auto ref = weak_ptr_factory_.GetWeakPtr(); |
| BOOL handled = FALSE; |
| OnChar(native_key_event.hwnd, native_key_event.message, |
| native_key_event.wParam, native_key_event.lParam, native_key_event, |
| &handled); |
| if (!ref) |
| return DispatcherDestroyedDetails(); |
| if (handled) |
| event->StopPropagation(); |
| return ui::EventDispatchDetails(); |
| } |
| |
| std::vector<MSG> char_msgs; |
| // Combines the WM_KEY* and WM_CHAR messages in the event processing flow |
| // which is necessary to let Chrome IME extension to process the key event |
| // and perform corresponding IME actions. |
| // Chrome IME extension may wants to consume certain key events based on |
| // the character information of WM_CHAR messages. Holding WM_KEY* messages |
| // until WM_CHAR is processed by the IME extension is not feasible because |
| // there is no way to know whether there will or not be a WM_CHAR following |
| // the WM_KEY*. |
| // Chrome never handles dead chars so it is safe to remove/ignore |
| // WM_*DEADCHAR messages. |
| MSG msg; |
| while (::PeekMessage(&msg, native_key_event.hwnd, WM_CHAR, WM_DEADCHAR, |
| PM_REMOVE)) { |
| if (msg.message == WM_CHAR) |
| char_msgs.push_back(msg); |
| } |
| while (::PeekMessage(&msg, native_key_event.hwnd, WM_SYSCHAR, WM_SYSDEADCHAR, |
| PM_REMOVE)) { |
| if (msg.message == WM_SYSCHAR) |
| char_msgs.push_back(msg); |
| } |
| |
| // Handles ctrl-shift key to change text direction and layout alignment. |
| if (IsRTLKeyboardLayoutInstalled() && !IsTextInputTypeNone()) { |
| ui::KeyboardCode code = event->key_code(); |
| if (event->type() == ui::ET_KEY_PRESSED) { |
| if (code == ui::VKEY_SHIFT) { |
| base::i18n::TextDirection dir; |
| if (IsCtrlShiftPressed(&dir)) |
| pending_requested_direction_ = dir; |
| } else if (code != ui::VKEY_CONTROL) { |
| pending_requested_direction_ = base::i18n::UNKNOWN_DIRECTION; |
| } |
| } else if (event->type() == ui::ET_KEY_RELEASED && |
| (code == ui::VKEY_SHIFT || code == ui::VKEY_CONTROL) && |
| pending_requested_direction_ != base::i18n::UNKNOWN_DIRECTION) { |
| GetTextInputClient()->ChangeTextDirectionAndLayoutAlignment( |
| pending_requested_direction_); |
| pending_requested_direction_ = base::i18n::UNKNOWN_DIRECTION; |
| } |
| } |
| |
| // If only 1 WM_CHAR per the key event, set it as the character of it. |
| if (char_msgs.size() == 1 && |
| !std::iswcntrl(static_cast<wint_t>(char_msgs[0].wParam))) |
| event->set_character(static_cast<base::char16>(char_msgs[0].wParam)); |
| |
| // Dispatches the key events to the Chrome IME extension which is listening to |
| // key events on the following two situations: |
| // 1) |char_msgs| is empty when the event is non-character key. |
| // 2) |char_msgs|.size() == 1 when the event is character key and the WM_CHAR |
| // messages have been combined in the event processing flow. |
| if (char_msgs.size() <= 1 && GetEngine() && |
| GetEngine()->IsInterestedInKeyEvent()) { |
| ui::IMEEngineHandlerInterface::KeyEventDoneCallback callback = |
| base::BindOnce(&InputMethodWinBase::ProcessKeyEventDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Owned(new ui::KeyEvent(*event)), |
| base::Owned(new std::vector<MSG>(char_msgs))); |
| GetEngine()->ProcessKeyEvent(*event, std::move(callback)); |
| return ui::EventDispatchDetails(); |
| } |
| |
| return ProcessUnhandledKeyEvent(event, &char_msgs); |
| } |
| |
| bool InputMethodWinBase::IsWindowFocused(const TextInputClient* client) const { |
| if (!client) |
| return false; |
| // When Aura is enabled, |attached_window_handle| should always be a top-level |
| // window. So we can safely assume that |attached_window_handle| is ready for |
| // receiving keyboard input as long as it is an active window. This works well |
| // even when the |attached_window_handle| becomes active but has not received |
| // WM_FOCUS yet. |
| return toplevel_window_handle_ && |
| GetActiveWindow() == toplevel_window_handle_; |
| } |
| |
| LRESULT InputMethodWinBase::OnChar(HWND window_handle, |
| UINT message, |
| WPARAM wparam, |
| LPARAM lparam, |
| const MSG& event, |
| BOOL* handled) { |
| *handled = TRUE; |
| |
| // We need to send character events to the focused text input client event if |
| // its text input type is ui::TEXT_INPUT_TYPE_NONE. |
| if (GetTextInputClient()) { |
| const base::char16 kCarriageReturn = L'\r'; |
| const base::char16 ch = static_cast<base::char16>(wparam); |
| // A mask to determine the previous key state from |lparam|. The value is 1 |
| // if the key is down before the message is sent, or it is 0 if the key is |
| // up. |
| const uint32_t kPrevKeyDownBit = 0x40000000; |
| if (ch == kCarriageReturn && !(lparam & kPrevKeyDownBit)) |
| accept_carriage_return_ = true; |
| // Conditionally ignore '\r' events to work around https://crbug.com/319100. |
| // TODO(yukawa, IME): Figure out long-term solution. |
| if (ch != kCarriageReturn || accept_carriage_return_) { |
| ui::KeyEvent char_event = ui::KeyEventFromMSG(event); |
| GetTextInputClient()->InsertChar(char_event); |
| } |
| } |
| |
| // Explicitly show the system menu at a good location on [Alt]+[Space]. |
| // Note: Setting |handled| to FALSE for DefWindowProc triggering of the system |
| // menu causes undesirable titlebar artifacts in the classic theme. |
| if (message == WM_SYSCHAR && wparam == VK_SPACE) |
| gfx::ShowSystemMenu(window_handle); |
| |
| return 0; |
| } |
| |
| LRESULT InputMethodWinBase::OnImeRequest(UINT message, |
| WPARAM wparam, |
| LPARAM lparam, |
| BOOL* handled) { |
| *handled = FALSE; |
| |
| // Should not receive WM_IME_REQUEST message, if IME is disabled. |
| const ui::TextInputType type = GetTextInputType(); |
| if (type == ui::TEXT_INPUT_TYPE_NONE || |
| type == ui::TEXT_INPUT_TYPE_PASSWORD) { |
| return 0; |
| } |
| |
| switch (wparam) { |
| case IMR_RECONVERTSTRING: |
| *handled = TRUE; |
| return OnReconvertString(reinterpret_cast<RECONVERTSTRING*>(lparam)); |
| case IMR_DOCUMENTFEED: |
| *handled = TRUE; |
| return OnDocumentFeed(reinterpret_cast<RECONVERTSTRING*>(lparam)); |
| case IMR_QUERYCHARPOSITION: |
| *handled = TRUE; |
| return OnQueryCharPosition(reinterpret_cast<IMECHARPOSITION*>(lparam)); |
| default: |
| return 0; |
| } |
| } |
| |
| LRESULT InputMethodWinBase::OnDocumentFeed(RECONVERTSTRING* reconv) { |
| ui::TextInputClient* client = GetTextInputClient(); |
| if (!client) |
| return 0; |
| |
| gfx::Range text_range; |
| if (!client->GetTextRange(&text_range) || text_range.is_empty()) |
| return 0; |
| |
| bool result = false; |
| gfx::Range target_range; |
| if (client->HasCompositionText()) |
| result = client->GetCompositionTextRange(&target_range); |
| |
| if (!result || target_range.is_empty()) { |
| if (!client->GetEditableSelectionRange(&target_range) || |
| !target_range.IsValid()) { |
| return 0; |
| } |
| } |
| |
| if (!text_range.Contains(target_range)) |
| return 0; |
| |
| if (target_range.GetMin() - text_range.start() > kExtraNumberOfChars) |
| text_range.set_start(target_range.GetMin() - kExtraNumberOfChars); |
| |
| if (text_range.end() - target_range.GetMax() > kExtraNumberOfChars) |
| text_range.set_end(target_range.GetMax() + kExtraNumberOfChars); |
| |
| size_t len = text_range.length(); |
| size_t need_size = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR); |
| |
| if (!reconv) |
| return need_size; |
| |
| if (reconv->dwSize < need_size) |
| return 0; |
| |
| base::string16 text; |
| if (!GetTextInputClient()->GetTextFromRange(text_range, &text)) |
| return 0; |
| DCHECK_EQ(text_range.length(), text.length()); |
| |
| reconv->dwVersion = 0; |
| reconv->dwStrLen = len; |
| reconv->dwStrOffset = sizeof(RECONVERTSTRING); |
| reconv->dwCompStrLen = |
| client->HasCompositionText() ? target_range.length() : 0; |
| reconv->dwCompStrOffset = |
| (target_range.GetMin() - text_range.start()) * sizeof(WCHAR); |
| reconv->dwTargetStrLen = target_range.length(); |
| reconv->dwTargetStrOffset = reconv->dwCompStrOffset; |
| |
| memcpy((char*)reconv + sizeof(RECONVERTSTRING), text.c_str(), |
| len * sizeof(WCHAR)); |
| |
| // According to Microsoft API document, IMR_RECONVERTSTRING and |
| // IMR_DOCUMENTFEED should return reconv, but some applications return |
| // need_size. |
| return reinterpret_cast<LRESULT>(reconv); |
| } |
| |
| LRESULT InputMethodWinBase::OnReconvertString(RECONVERTSTRING* reconv) { |
| ui::TextInputClient* client = GetTextInputClient(); |
| if (!client) |
| return 0; |
| |
| // If there is a composition string already, we don't allow reconversion. |
| if (client->HasCompositionText()) |
| return 0; |
| |
| gfx::Range text_range; |
| if (!client->GetTextRange(&text_range) || text_range.is_empty()) |
| return 0; |
| |
| gfx::Range selection_range; |
| if (!client->GetEditableSelectionRange(&selection_range) || |
| selection_range.is_empty()) { |
| return 0; |
| } |
| |
| DCHECK(text_range.Contains(selection_range)); |
| |
| size_t len = selection_range.length(); |
| size_t need_size = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR); |
| |
| if (!reconv) |
| return need_size; |
| |
| if (reconv->dwSize < need_size) |
| return 0; |
| |
| // TODO(penghuang): Return some extra context to help improve IME's |
| // reconversion accuracy. |
| base::string16 text; |
| if (!GetTextInputClient()->GetTextFromRange(selection_range, &text)) |
| return 0; |
| DCHECK_EQ(selection_range.length(), text.length()); |
| |
| reconv->dwVersion = 0; |
| reconv->dwStrLen = len; |
| reconv->dwStrOffset = sizeof(RECONVERTSTRING); |
| reconv->dwCompStrLen = len; |
| reconv->dwCompStrOffset = 0; |
| reconv->dwTargetStrLen = len; |
| reconv->dwTargetStrOffset = 0; |
| |
| memcpy(reinterpret_cast<char*>(reconv) + sizeof(RECONVERTSTRING), |
| text.c_str(), len * sizeof(WCHAR)); |
| |
| // According to Microsoft API document, IMR_RECONVERTSTRING and |
| // IMR_DOCUMENTFEED should return reconv, but some applications return |
| // need_size. |
| return reinterpret_cast<LRESULT>(reconv); |
| } |
| |
| LRESULT InputMethodWinBase::OnQueryCharPosition(IMECHARPOSITION* char_positon) { |
| if (!char_positon) |
| return 0; |
| |
| if (char_positon->dwSize < sizeof(IMECHARPOSITION)) |
| return 0; |
| |
| ui::TextInputClient* client = GetTextInputClient(); |
| if (!client) |
| return 0; |
| |
| // Tentatively assume that the returned value from |client| is DIP (Density |
| // Independent Pixel). See the comment in text_input_client.h and |
| // http://crbug.com/360334. |
| gfx::Rect dip_rect; |
| if (client->HasCompositionText()) { |
| if (!client->GetCompositionCharacterBounds(char_positon->dwCharPos, |
| &dip_rect)) { |
| return 0; |
| } |
| } else { |
| // If there is no composition and the first character is queried, returns |
| // the caret bounds. This behavior is the same to that of RichEdit control. |
| if (char_positon->dwCharPos != 0) |
| return 0; |
| dip_rect = client->GetCaretBounds(); |
| } |
| const gfx::Rect rect = display::win::ScreenWin::DIPToScreenRect( |
| toplevel_window_handle_, dip_rect); |
| |
| char_positon->pt.x = rect.x(); |
| char_positon->pt.y = rect.y(); |
| char_positon->cLineHeight = rect.height(); |
| return 1; // returns non-zero value when succeeded. |
| } |
| |
| void InputMethodWinBase::ProcessKeyEventDone(ui::KeyEvent* event, |
| const std::vector<MSG>* char_msgs, |
| bool is_handled) { |
| if (is_handled) |
| return; |
| ProcessUnhandledKeyEvent(event, char_msgs); |
| } |
| |
| ui::EventDispatchDetails InputMethodWinBase::ProcessUnhandledKeyEvent( |
| ui::KeyEvent* event, |
| const std::vector<MSG>* char_msgs) { |
| DCHECK(event); |
| ui::EventDispatchDetails details = |
| DispatchKeyEventPostIME(event, base::NullCallback()); |
| if (details.dispatcher_destroyed || details.target_destroyed || |
| event->stopped_propagation()) { |
| return details; |
| } |
| |
| BOOL handled; |
| for (const auto& msg : (*char_msgs)) { |
| auto ref = weak_ptr_factory_.GetWeakPtr(); |
| OnChar(msg.hwnd, msg.message, msg.wParam, msg.lParam, msg, &handled); |
| if (!ref) |
| return DispatcherDestroyedDetails(); |
| } |
| return details; |
| } |
| |
| } // namespace ui |