// Copyright (c) 2011 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/views/ime/input_method_win.h" #include "base/basictypes.h" #include "base/logging.h" #include "base/string_util.h" #include "ui/base/ime/composition_text.h" #include "ui/base/ime/text_input_client.h" #include "ui/base/keycodes/keyboard_codes.h" #include "ui/views/events/event.h" // Extra number of chars before and after selection (or composition) range which // is returned to IME for improving conversion accuracy. static const size_t kExtraNumberOfChars = 20; namespace views { InputMethodWin::InputMethodWin(internal::InputMethodDelegate* delegate) : active_(false), direction_(base::i18n::UNKNOWN_DIRECTION), pending_requested_direction_(base::i18n::UNKNOWN_DIRECTION) { set_delegate(delegate); } InputMethodWin::~InputMethodWin() { if (widget()) ime_input_.DisableIME(hwnd()); } void InputMethodWin::Init(Widget* widget) { // Gets the initial input locale and text direction information. OnInputLangChange(0, 0); InputMethodBase::Init(widget); } void InputMethodWin::OnFocus() { DCHECK(!widget_focused()); InputMethodBase::OnFocus(); UpdateIMEState(); } void InputMethodWin::OnBlur() { DCHECK(widget_focused()); ConfirmCompositionText(); InputMethodBase::OnBlur(); } void InputMethodWin::DispatchKeyEvent(const KeyEvent& key) { // Handles ctrl-shift key to change text direction and layout alignment. if (ui::ImeInput::IsRTLKeyboardLayoutInstalled() && !IsTextInputTypeNone()) { ui::KeyboardCode code = key.key_code(); if (key.type() == ui::ET_KEY_PRESSED) { if (code == ui::VKEY_SHIFT) { base::i18n::TextDirection dir; if (ui::ImeInput::IsCtrlShiftPressed(&dir)) pending_requested_direction_ = dir; } else if (code != ui::VKEY_CONTROL) { pending_requested_direction_ = base::i18n::UNKNOWN_DIRECTION; } } else if (key.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; } } DispatchKeyEventPostIME(key); } void InputMethodWin::OnTextInputTypeChanged(View* view) { if (IsViewFocused(view)) { ime_input_.CancelIME(hwnd()); UpdateIMEState(); } InputMethodBase::OnTextInputTypeChanged(view); } void InputMethodWin::OnCaretBoundsChanged(View* view) { gfx::Rect rect; if (!IsViewFocused(view) || !GetCaretBoundsInWidget(&rect)) return; ime_input_.UpdateCaretRect(hwnd(), rect); } void InputMethodWin::CancelComposition(View* view) { if (IsViewFocused(view)) ime_input_.CancelIME(hwnd()); } std::string InputMethodWin::GetInputLocale() { return locale_; } base::i18n::TextDirection InputMethodWin::GetInputTextDirection() { return direction_; } bool InputMethodWin::IsActive() { return active_; } LRESULT InputMethodWin::OnImeMessages( UINT message, WPARAM w_param, LPARAM l_param, BOOL* handled) { LRESULT result = 0; switch (message) { case WM_IME_SETCONTEXT: result = OnImeSetContext(message, w_param, l_param, handled); break; case WM_IME_STARTCOMPOSITION: result = OnImeStartComposition(message, w_param, l_param, handled); break; case WM_IME_COMPOSITION: result = OnImeComposition(message, w_param, l_param, handled); break; case WM_IME_ENDCOMPOSITION: result = OnImeEndComposition(message, w_param, l_param, handled); break; case WM_IME_REQUEST: result = OnImeRequest(message, w_param, l_param, handled); break; case WM_CHAR: case WM_SYSCHAR: result = OnChar(message, w_param, l_param, handled); break; case WM_DEADCHAR: case WM_SYSDEADCHAR: result = OnDeadChar(message, w_param, l_param, handled); break; default: NOTREACHED() << "Unknown IME message:" << message; break; } return result; } void InputMethodWin::OnWillChangeFocus(View* focused_before, View* focused) { ConfirmCompositionText(); } void InputMethodWin::OnDidChangeFocus(View* focused_before, View* focused) { UpdateIMEState(); } void InputMethodWin::OnInputLangChange(DWORD character_set, HKL input_language_id) { active_ = ime_input_.SetInputLanguage(); locale_ = ime_input_.GetInputLanguageName(); direction_ = ime_input_.GetTextDirection(); OnInputMethodChanged(); } LRESULT InputMethodWin::OnImeSetContext( UINT message, WPARAM wparam, LPARAM lparam, BOOL* handled) { active_ = (wparam == TRUE); if (active_) ime_input_.CreateImeWindow(hwnd()); OnInputMethodChanged(); return ime_input_.SetImeWindowStyle(hwnd(), message, wparam, lparam, handled); } LRESULT InputMethodWin::OnImeStartComposition( UINT message, WPARAM wparam, LPARAM lparam, BOOL* handled) { // We have to prevent WTL from calling ::DefWindowProc() because the function // calls ::ImmSetCompositionWindow() and ::ImmSetCandidateWindow() to // over-write the position of IME windows. *handled = TRUE; if (IsTextInputTypeNone()) return 0; // Reset the composition status and create IME windows. ime_input_.CreateImeWindow(hwnd()); ime_input_.ResetComposition(hwnd()); return 0; } LRESULT InputMethodWin::OnImeComposition( UINT message, WPARAM wparam, LPARAM lparam, BOOL* handled) { // We have to prevent WTL from calling ::DefWindowProc() because we do not // want for the IMM (Input Method Manager) to send WM_IME_CHAR messages. *handled = TRUE; if (IsTextInputTypeNone()) return 0; // At first, update the position of the IME window. ime_input_.UpdateImeWindow(hwnd()); // Retrieve the result string and its attributes of the ongoing composition // and send it to a renderer process. ui::CompositionText composition; if (ime_input_.GetResult(hwnd(), lparam, &composition.text)) { GetTextInputClient()->InsertText(composition.text); ime_input_.ResetComposition(hwnd()); // Fall though and try reading the composition string. // Japanese IMEs send a message containing both GCS_RESULTSTR and // GCS_COMPSTR, which means an ongoing composition has been finished // by the start of another composition. } // Retrieve the composition string and its attributes of the ongoing // composition and send it to a renderer process. if (ime_input_.GetComposition(hwnd(), lparam, &composition)) GetTextInputClient()->SetCompositionText(composition); return 0; } LRESULT InputMethodWin::OnImeEndComposition( UINT message, WPARAM wparam, LPARAM lparam, BOOL* handled) { // Let WTL call ::DefWindowProc() and release its resources. *handled = FALSE; if (IsTextInputTypeNone()) return 0; if (GetTextInputClient()->HasCompositionText()) GetTextInputClient()->ClearCompositionText(); ime_input_.ResetComposition(hwnd()); ime_input_.DestroyImeWindow(hwnd()); return 0; } LRESULT InputMethodWin::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(lparam)); case IMR_DOCUMENTFEED: *handled = TRUE; return OnDocumentFeed(reinterpret_cast(lparam)); default: return 0; } } LRESULT InputMethodWin::OnChar( UINT message, WPARAM wparam, LPARAM lparam, 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()) return 0; int flags = 0; flags |= (::GetKeyState(VK_MENU) & 0x80)? ui::EF_ALT_DOWN : 0; flags |= (::GetKeyState(VK_SHIFT) & 0x80)? ui::EF_SHIFT_DOWN : 0; flags |= (::GetKeyState(VK_CONTROL) & 0x80)? ui::EF_CONTROL_DOWN : 0; GetTextInputClient()->InsertChar(static_cast(wparam), flags); return 0; } LRESULT InputMethodWin::OnDeadChar( UINT message, WPARAM wparam, LPARAM lparam, BOOL* handled) { *handled = TRUE; if (IsTextInputTypeNone()) return 0; // Shows the dead character as a composition text, so that the user can know // what dead key was pressed. ui::CompositionText composition; composition.text.assign(1, static_cast(wparam)); composition.selection = ui::Range(0, 1); composition.underlines.push_back( ui::CompositionUnderline(0, 1, SK_ColorBLACK, false)); GetTextInputClient()->SetCompositionText(composition); return 0; } LRESULT InputMethodWin::OnDocumentFeed(RECONVERTSTRING* reconv) { ui::TextInputClient* client = GetTextInputClient(); if (!client) return 0; ui::Range text_range; if (!client->GetTextRange(&text_range) || text_range.is_empty()) return 0; bool result = false; ui::Range target_range; if (client->HasCompositionText()) result = client->GetCompositionTextRange(&target_range); if (!result || target_range.is_empty()) { if (!client->GetSelectionRange(&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; 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 Microsft API document, IMR_RECONVERTSTRING and // IMR_DOCUMENTFEED should return reconv, but some applications return // need_size. return reinterpret_cast(reconv); } LRESULT InputMethodWin::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; ui::Range text_range; if (!client->GetTextRange(&text_range) || text_range.is_empty()) return 0; ui::Range selection_range; if (!client->GetSelectionRange(&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. 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(reconv) + sizeof(RECONVERTSTRING), text.c_str(), len * sizeof(WCHAR)); // According to Microsft API document, IMR_RECONVERTSTRING and // IMR_DOCUMENTFEED should return reconv, but some applications return // need_size. return reinterpret_cast(reconv); } void InputMethodWin::ConfirmCompositionText() { if (!IsTextInputTypeNone()) { ime_input_.CleanupComposition(hwnd()); // Though above line should confirm the client's composition text by sending // a result text to us, in case the input method and the client are in // inconsistent states, we check the client's composition state again. if (GetTextInputClient()->HasCompositionText()) GetTextInputClient()->ConfirmCompositionText(); } } void InputMethodWin::UpdateIMEState() { // Use switch here in case we are going to add more text input types. // We disable input method in password field. switch (GetTextInputType()) { case ui::TEXT_INPUT_TYPE_NONE: case ui::TEXT_INPUT_TYPE_PASSWORD: ime_input_.DisableIME(hwnd()); break; default: ime_input_.EnableIME(hwnd()); break; } } } // namespace views