// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ui/base/ime/win/input_method_win_base.h"
#include <stddef.h>
#include <stdint.h>
#include <memory>
#include <vector>
#include "base/auto_reset.h"
#include "base/command_line.h"
#include "base/containers/heap_array.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "base/win/windows_version.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_utils.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/events/types/event_type.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<VirtualKeyboardController> CreateKeyboardController(
HWND attached_window_handle) {
if (base::win::GetVersion() >= base::win::Version::WIN10_RS4) {
return std::make_unique<OnScreenKeyboardDisplayManagerInputPane>(
attached_window_handle);
} else {
return std::make_unique<OnScreenKeyboardDisplayManagerTabTip>(
attached_window_handle);
}
}
// 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.
auto layouts = base::HeapArray<HKL>::Uninit(size);
::GetKeyboardLayoutList(size, layouts.data());
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(
ImeKeyEventDispatcher* ime_key_event_dispatcher,
HWND attached_window_handle)
: InputMethodBase(ime_key_event_dispatcher,
CreateKeyboardController(attached_window_handle)),
attached_window_handle_(attached_window_handle),
pending_requested_direction_(base::i18n::UNKNOWN_DIRECTION) {}
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) {
CHROME_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<CHROME_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.
if (!HandlePeekMessage(native_key_event.hwnd, WM_CHAR, WM_DEADCHAR,
&char_msgs)) {
return DispatcherDestroyedDetails();
}
if (!HandlePeekMessage(native_key_event.hwnd, WM_SYSCHAR, WM_SYSDEADCHAR,
&char_msgs)) {
return DispatcherDestroyedDetails();
}
// Handles ctrl-shift key to change text direction and layout alignment.
if (IsRTLKeyboardLayoutInstalled() && !IsTextInputTypeNone()) {
ui::KeyboardCode code = event->key_code();
if (event->type() == ui::EventType::kKeyPressed) {
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::EventType::kKeyReleased &&
(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 && !base::IsAsciiControl(char_msgs[0].wParam)) {
event->set_character(static_cast<char16_t>(char_msgs[0].wParam));
}
return ProcessUnhandledKeyEvent(event, &char_msgs);
}
bool InputMethodWinBase::HandlePeekMessage(HWND hwnd,
UINT msg_filter_min,
UINT msg_filter_max,
std::vector<CHROME_MSG>* char_msgs) {
auto ref = weak_ptr_factory_.GetWeakPtr();
while (true) {
CHROME_MSG msg_found;
const bool result =
!!::PeekMessage(ChromeToWindowsType(&msg_found), hwnd, msg_filter_min,
msg_filter_max, PM_REMOVE);
// PeekMessage may result in WM_NCDESTROY which will cause deletion of
// |this|. We should use WeakPtr to check whether |this| is destroyed.
if (!ref)
return false;
if (result && msg_found.message == msg_filter_min)
char_msgs->push_back(msg_found);
if (!result)
return true;
}
}
bool InputMethodWinBase::IsWindowFocused(const TextInputClient* client) const {
if (!client)
return false;
// The reason why we check |GetActiveWindow()| here was to take care of
// situations when this method gets called as a response to WM_NCACTIVATE as
// discussed at crbug.com/287620. This approach works if and only if
// |attached_window_handle_| is a top-level window, which is assumed to be
// true for Chromium-based browser products at least.
// We need to relax this condition by checking |GetFocus()| so this works fine
// for embedded Chromium windows.
// TODO(crbug.com/40815890): Check if this can be replaced with |GetFocus()|.
return attached_window_handle_ &&
(GetActiveWindow() == attached_window_handle_ ||
GetFocus() == attached_window_handle_);
}
LRESULT InputMethodWinBase::OnChar(HWND window_handle,
UINT message,
WPARAM wparam,
LPARAM lparam,
const CHROME_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 char16_t kCarriageReturn = u'\r';
const char16_t ch = static_cast<char16_t>(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);
}
}
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;
std::u16string 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.
std::u16string 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(
attached_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<CHROME_MSG>* char_msgs,
bool is_handled) {
if (is_handled)
return;
ProcessUnhandledKeyEvent(event, char_msgs);
}
ui::EventDispatchDetails InputMethodWinBase::ProcessUnhandledKeyEvent(
ui::KeyEvent* event,
const std::vector<CHROME_MSG>* char_msgs) {
DCHECK(event);
ui::EventDispatchDetails details = DispatchKeyEventPostIME(event);
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