// Copyright 2008, Google Inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "chrome/browser/ime_input.h" #include "base/scoped_ptr.h" #include "base/string_util.h" // "imm32.lib" is required by IMM32 APIs used in this file. // NOTE(hbono): To comply with a comment from Darin, I have added // this #pragma directive instead of adding "imm32.lib" to a project file. #pragma comment(lib, "imm32.lib") /////////////////////////////////////////////////////////////////////////////// // ImeInput ImeInput::ImeInput() : ime_status_(false), input_language_id_(LANG_USER_DEFAULT), is_composing_(false), system_caret_(false), caret_x_(-1), caret_y_(-1) { } ImeInput::~ImeInput() { } bool ImeInput::SetInputLanguage() { // Retrieve the current keyboard layout from Windows and determine whether // or not the current input context has IMEs. // Also save its input language for language-specific operations required // while composing a text. HKL keyboard_layout = ::GetKeyboardLayout(0); input_language_id_ = reinterpret_cast(keyboard_layout); ime_status_ = (::ImmIsIME(keyboard_layout) == TRUE) ? true : false; return ime_status_; } void ImeInput::CreateImeWindow(HWND window_handle) { // When a user disables TSF (Text Service Framework) and CUAS (Cicero // Unaware Application Support), Chinese IMEs somehow ignore function calls // to ::ImmSetCandidateWindow(), i.e. they do not move their candidate // window to the position given as its parameters, and use the position // of the current system caret instead, i.e. it uses ::GetCaretPos() to // retrieve the position of their IME candidate window. // Therefore, we create a temporary system caret for Chinese IMEs and use // it during this input context. if (PRIMARYLANGID(input_language_id_) == LANG_CHINESE) { if (!system_caret_) { if (::CreateCaret(window_handle, NULL, 1, 1)) { system_caret_ = true; } } } // Restore the positions of the IME windows. UpdateImeWindow(window_handle); } void ImeInput::SetImeWindowStyle(HWND window_handle, UINT message, WPARAM wparam, LPARAM lparam, BOOL* handled) { // To prevent the IMM (Input Method Manager) from displaying the IME // composition window, Update the styles of the IME windows and EXPLICITLY // call ::DefWindowProc() here. // NOTE(hbono): We can NEVER let WTL call ::DefWindowProc() when we update // the styles of IME windows because the 'lparam' variable is a local one // and all its updates disappear in returning from this function, i.e. WTL // does not call ::DefWindowProc() with our updated 'lparam' value but call // the function with its original value and over-writes our window styles. *handled = TRUE; lparam &= ~ISC_SHOWUICOMPOSITIONWINDOW; ::DefWindowProc(window_handle, message, wparam, lparam); } void ImeInput::DestroyImeWindow(HWND window_handle) { // Destroy the system caret if we have created for this IME input context. if (system_caret_) { ::DestroyCaret(); system_caret_ = false; } } void ImeInput::MoveImeWindow(HWND window_handle, HIMC imm_context) { int x = caret_x_; int y = caret_y_; const int kCaretMargin = 1; // As written in a comment in ImeInput::CreateImeWindow(), // Chinese IMEs ignore function calls to ::ImmSetCandidateWindow() // when a user disables TSF (Text Service Framework) and CUAS (Cicero // Unaware Application Support). // On the other hand, when a user enables TSF and CUAS, Chinese IMEs // ignore the position of the current system caret and uses the // parameters given to ::ImmSetCandidateWindow() with its 'dwStyle' // parameter CFS_CANDIDATEPOS. // Therefore, we do not only call ::ImmSetCandidateWindow() but also // set the positions of the temporary system caret if it exists. CANDIDATEFORM candidate_position = {0, CFS_CANDIDATEPOS, {x, y}, {0, 0, 0, 0}}; ::ImmSetCandidateWindow(imm_context, &candidate_position); if (system_caret_) { ::SetCaretPos(x, y); } if (PRIMARYLANGID(input_language_id_) == LANG_KOREAN) { // Chinese IMEs and Japanese IMEs require the upper-left corner of // the caret to move the position of their candidate windows. // On the other hand, Korean IMEs require the lower-left corner of the // caret to move their candidate windows. y += kCaretMargin; } // Japanese IMEs and Korean IMEs also use the rectangle given to // ::ImmSetCandidateWindow() with its 'dwStyle' parameter CFS_EXCLUDE // to move their candidate windows when a user disables TSF and CUAS. // Therefore, we also set this parameter here. CANDIDATEFORM exclude_rectangle = {0, CFS_EXCLUDE, {x, y}, {x, y, x, y + kCaretMargin}}; ::ImmSetCandidateWindow(imm_context, &exclude_rectangle); } void ImeInput::UpdateImeWindow(HWND window_handle) { // Just move the IME window attached to the given window. if (caret_x_ >= 0 && caret_y_ >= 0) { HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { MoveImeWindow(window_handle, imm_context); ::ImmReleaseContext(window_handle, imm_context); } } } void ImeInput::CleanupComposition(HWND window_handle) { // Notify the IMM attached to the given window to complete the ongoing // composition, (this case happens when the given window is de-activated // while composing a text and re-activated), and reset the omposition status. if (is_composing_) { HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { ::ImmNotifyIME(imm_context, NI_COMPOSITIONSTR, CPS_COMPLETE, 0); ::ImmReleaseContext(window_handle, imm_context); } ResetComposition(window_handle); } } void ImeInput::ResetComposition(HWND window_handle) { // Currently, just reset the composition status. is_composing_ = false; } void ImeInput::CompleteComposition(HWND window_handle, HIMC imm_context) { // We have to confirm there is an ongoing composition before completing it. // This is for preventing some IMEs from getting confused while completing an // ongoing composition even if they do not have any ongoing compositions.) if (is_composing_) { ::ImmNotifyIME(imm_context, NI_COMPOSITIONSTR, CPS_COMPLETE, 0); ResetComposition(window_handle); } } void ImeInput::GetCaret(HIMC imm_context, LPARAM lparam, ImeComposition* composition) { // This operation is optional and language-dependent because the caret // style is depended on the language, e.g.: // * Korean IMEs: the caret is a blinking block, // (It contains only one hangul character); // * Chinese IMEs: the caret is a blinking line, // (i.e. they do not need to retrieve the target selection); // * Japanese IMEs: the caret is a selection (or undelined) block, // (which can contain one or more Japanese characters). int target_start = -1; int target_end = -1; switch (PRIMARYLANGID(input_language_id_)) { case LANG_KOREAN: if (lparam & CS_NOMOVECARET) { target_start = 0; target_end = 1; } break; case LANG_CHINESE: break; case LANG_JAPANESE: // For Japanese IMEs, the robustest way to retrieve the caret // is scanning the attribute of the latest composition string and // retrieving the begining and the end of the target clause, i.e. // a clause being converted. size_t composition_length = composition->ime_string.length(); composition->cursor_position = static_cast(composition_length); if (lparam & GCS_COMPATTR) { int attribute_size = ::ImmGetCompositionString(imm_context, GCS_COMPATTR, NULL, 0); if (attribute_size > 0) { scoped_array attribute_data(new char[attribute_size]); if (attribute_data.get()) { ::ImmGetCompositionString(imm_context, GCS_COMPATTR, attribute_data.get(), attribute_size); int i = 0; for (; i < attribute_size; i++) { if (IsTargetAttribute(attribute_data[i])) break; } target_start = i; for (; i < attribute_size; i++) { if (!IsTargetAttribute(attribute_data[i])) break; } target_end = i; } } } break; } composition->target_start = target_start; composition->target_end = target_end; } bool ImeInput::GetString(HIMC imm_context, WPARAM lparam, int type, ImeComposition* composition) { bool result = false; if (lparam & type) { int string_size = ::ImmGetCompositionString(imm_context, type, NULL, 0); if (string_size > 0) { int string_length = string_size / sizeof(wchar_t); wchar_t *string_data = WriteInto(&composition->ime_string, string_length + 1); if (string_data) { // Fill the given ImeComposition object. ::ImmGetCompositionString(imm_context, type, string_data, string_size); composition->string_type = type; result = true; } } } return result; } bool ImeInput::GetResult(HWND window_handle, LPARAM lparam, ImeComposition* composition) { bool result = false; HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { // Copy the result string to the ImeComposition object. result = GetString(imm_context, lparam, GCS_RESULTSTR, composition); // Reset all the other parameters because a result string does not // have composition attributes. composition->cursor_position = -1; composition->target_start = -1; composition->target_end = -1; ::ImmReleaseContext(window_handle, imm_context); } return result; } bool ImeInput::GetComposition(HWND window_handle, LPARAM lparam, ImeComposition* composition) { bool result = false; HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { // Copy the composition string to the ImeComposition object. result = GetString(imm_context, lparam, GCS_COMPSTR, composition); // Retrieve the cursor position in the IME composition. int cursor_position = ::ImmGetCompositionString(imm_context, GCS_CURSORPOS, NULL, 0); composition->cursor_position = cursor_position; composition->target_start = -1; composition->target_end = -1; // Retrieve the target selection and Update the ImeComposition // object. GetCaret(imm_context, lparam, composition); // Mark that there is an ongoing composition. is_composing_ = true; ::ImmReleaseContext(window_handle, imm_context); } return result; } void ImeInput::DisableIME(HWND window_handle) { // A renderer process have moved its input focus to a password input // when there is an ongoing composition, e.g. a user has clicked a // mouse button and selected a password input while composing a text. // For this case, we have to complete the ongoing composition and // clean up the resources attached to this object BEFORE DISABLING THE IME. CleanupComposition(window_handle); ::ImmAssociateContextEx(window_handle, NULL, 0); } void ImeInput::EnableIME(HWND window_handle, int x, int y, bool complete) { // Load the default IME context. // NOTE(hbono) // IMM ignores this call if the IME context is loaded. Therefore, we do // not have to check whether or not the IME context is loaded. ::ImmAssociateContextEx(window_handle, NULL, IACE_DEFAULT); // Complete the ongoing composition and move the IME windows. HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { if (complete) { // A renderer process have moved its input focus to another edit // control when there is an ongoing composition, e.g. a user has // clicked a mouse button and selected another edit control while // composing a text. // For this case, we have to complete the ongoing composition and // hide the IME windows BEFORE MOVING THEM. CompleteComposition(window_handle, imm_context); } // Save the caret position, and Update the position of the IME window. // This update is used for moving an IME window when a renderer process // resize/moves the input caret. if (x >= 0 && y >= 0) { caret_x_ = x; caret_y_ = y; MoveImeWindow(window_handle, imm_context); } ::ImmReleaseContext(window_handle, imm_context); } }