blob: 6b7c3bef62aec0027f1c8e1ad9d86e975d0964c2 [file] [log] [blame]
// 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