// Copyright 2013 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/win/imm32_manager.h" #include <msctf.h> #include "base/basictypes.h" #include "base/memory/scoped_ptr.h" #include "base/strings/string16.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/win/scoped_comptr.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/base/ime/composition_text.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") // Following code requires wchar_t to be same as char16. It should always be // true on Windows. COMPILE_ASSERT(sizeof(wchar_t) == sizeof(base::char16), wchar_t__char16_diff); /////////////////////////////////////////////////////////////////////////////// // IMM32Manager namespace { // Determines whether or not the given attribute represents a target // (a.k.a. a selection). bool IsTargetAttribute(char attribute) { return (attribute == ATTR_TARGET_CONVERTED || attribute == ATTR_TARGET_NOTCONVERTED); } // Helper function for IMM32Manager::GetCompositionInfo() method, to get the // target range that's selected by the user in the current composition string. void GetCompositionTargetRange(HIMC imm_context, int* target_start, int* target_end) { int attribute_size = ::ImmGetCompositionString(imm_context, GCS_COMPATTR, NULL, 0); if (attribute_size > 0) { int start = 0; int end = 0; scoped_ptr<char[]> attribute_data(new char[attribute_size]); if (attribute_data.get()) { ::ImmGetCompositionString(imm_context, GCS_COMPATTR, attribute_data.get(), attribute_size); for (start = 0; start < attribute_size; ++start) { if (IsTargetAttribute(attribute_data[start])) break; } for (end = start; end < attribute_size; ++end) { if (!IsTargetAttribute(attribute_data[end])) break; } } *target_start = start; *target_end = end; } } // Helper function for IMM32Manager::GetCompositionInfo() method, to get // underlines information of the current composition string. void GetCompositionUnderlines(HIMC imm_context, int target_start, int target_end, ui::CompositionUnderlines* underlines) { int clause_size = ::ImmGetCompositionString(imm_context, GCS_COMPCLAUSE, NULL, 0); int clause_length = clause_size / sizeof(uint32); if (clause_length) { scoped_ptr<uint32[]> clause_data(new uint32[clause_length]); if (clause_data.get()) { ::ImmGetCompositionString(imm_context, GCS_COMPCLAUSE, clause_data.get(), clause_size); for (int i = 0; i < clause_length - 1; ++i) { ui::CompositionUnderline underline; underline.start_offset = clause_data[i]; underline.end_offset = clause_data[i+1]; underline.color = SK_ColorBLACK; underline.thick = false; underline.background_color = SK_ColorTRANSPARENT; // Use thick underline for the target clause. if (underline.start_offset >= static_cast<uint32>(target_start) && underline.end_offset <= static_cast<uint32>(target_end)) { underline.thick = true; } underlines->push_back(underline); } } } } // 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; } } } // namespace namespace ui { IMM32Manager::IMM32Manager() : ime_status_(false), input_language_id_(LANG_USER_DEFAULT), is_composing_(false), system_caret_(false), caret_rect_(-1, -1, 0, 0), use_composition_window_(false) { } IMM32Manager::~IMM32Manager() { } bool IMM32Manager::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<LANGID>(keyboard_layout); // Check TSF Input Processor first. // If the active profile is TSF INPUTPROCESSOR, this is IME. base::win::ScopedComPtr<ITfInputProcessorProfileMgr> prof_mgr; TF_INPUTPROCESSORPROFILE prof; if (SUCCEEDED(prof_mgr.CreateInstance(CLSID_TF_InputProcessorProfiles)) && SUCCEEDED(prof_mgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &prof)) && prof.hkl == NULL && prof.dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR) { ime_status_ = true; } else { // If the curent language is not using TSF, check IMM32 based IMEs. // As ImmIsIME always returns non-0 value on Vista+, use ImmGetIMEFileName // instead to check if this HKL has any associated IME file. ime_status_ = (ImmGetIMEFileName(keyboard_layout, NULL, 0) != 0); } return ime_status_; } void IMM32Manager::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. // Since some third-party Japanese IME also uses ::GetCaretPos() to determine // their window position, we also create a caret for Japanese IMEs. if (PRIMARYLANGID(input_language_id_) == LANG_CHINESE || PRIMARYLANGID(input_language_id_) == LANG_JAPANESE) { if (!system_caret_) { if (::CreateCaret(window_handle, NULL, 1, 1)) { system_caret_ = true; } } } // Restore the positions of the IME windows. UpdateImeWindow(window_handle); } LRESULT IMM32Manager::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; return ::DefWindowProc(window_handle, message, wparam, lparam); } void IMM32Manager::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 IMM32Manager::MoveImeWindow(HWND window_handle, HIMC imm_context) { // Does nothing when the target window has no input focus. This is important // because the renderer may issue SelectionBoundsChanged event even when it // has no input focus. (e.g. the page update caused by incremental search.) // So this event should be ignored when the |window_handle| no longer has the // input focus. if (GetFocus() != window_handle) return; int x = caret_rect_.x(); int y = caret_rect_.y(); const int kCaretMargin = 1; if (!use_composition_window_ && PRIMARYLANGID(input_language_id_) == LANG_CHINESE) { // As written in a comment in IMM32Manager::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_) { switch (PRIMARYLANGID(input_language_id_)) { case LANG_JAPANESE: ::SetCaretPos(x, y + caret_rect_.height()); break; default: ::SetCaretPos(x, y); break; } } if (use_composition_window_) { // Moves the composition text window. COMPOSITIONFORM cf = {CFS_POINT, {x, y}}; ::ImmSetCompositionWindow(imm_context, &cf); // Don't need to set the position of candidate window. return; } 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 + caret_rect_.width(), y + caret_rect_.height()}}; ::ImmSetCandidateWindow(imm_context, &exclude_rectangle); } void IMM32Manager::UpdateImeWindow(HWND window_handle) { // Just move the IME window attached to the given window. if (caret_rect_.x() >= 0 && caret_rect_.y() >= 0) { HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { MoveImeWindow(window_handle, imm_context); ::ImmReleaseContext(window_handle, imm_context); } } } void IMM32Manager::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 IMM32Manager::ResetComposition(HWND window_handle) { // Currently, just reset the composition status. is_composing_ = false; } void IMM32Manager::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 IMM32Manager::GetCompositionInfo(HIMC imm_context, LPARAM lparam, CompositionText* composition) { // We only care about GCS_COMPATTR, GCS_COMPCLAUSE and GCS_CURSORPOS, and // convert them into underlines and selection range respectively. composition->underlines.clear(); int length = static_cast<int>(composition->text.length()); // Find out the range selected by the user. int target_start = length; int target_end = length; if (lparam & GCS_COMPATTR) GetCompositionTargetRange(imm_context, &target_start, &target_end); // Retrieve the selection range information. If CS_NOMOVECARET is specified, // that means the cursor should not be moved, then we just place the caret at // the beginning of the composition string. Otherwise we should honour the // GCS_CURSORPOS value if it's available. // TODO(suzhe): due to a bug of webkit, we currently can't use selection range // with composition string. See: https://bugs.webkit.org/show_bug.cgi?id=40805 if (!(lparam & CS_NOMOVECARET) && (lparam & GCS_CURSORPOS)) { // IMM32 does not support non-zero-width selection in a composition. So // always use the caret position as selection range. int cursor = ::ImmGetCompositionString(imm_context, GCS_CURSORPOS, NULL, 0); composition->selection = gfx::Range(cursor); } else { composition->selection = gfx::Range(0); } // Retrieve the clause segmentations and convert them to underlines. if (lparam & GCS_COMPCLAUSE) { GetCompositionUnderlines(imm_context, target_start, target_end, &composition->underlines); } // Set default underlines in case there is no clause information. if (!composition->underlines.size()) { CompositionUnderline underline; underline.color = SK_ColorBLACK; underline.background_color = SK_ColorTRANSPARENT; if (target_start > 0) { underline.start_offset = 0U; underline.end_offset = static_cast<uint32>(target_start); underline.thick = false; composition->underlines.push_back(underline); } if (target_end > target_start) { underline.start_offset = static_cast<uint32>(target_start); underline.end_offset = static_cast<uint32>(target_end); underline.thick = true; composition->underlines.push_back(underline); } if (target_end < length) { underline.start_offset = static_cast<uint32>(target_end); underline.end_offset = static_cast<uint32>(length); underline.thick = false; composition->underlines.push_back(underline); } } } bool IMM32Manager::GetString(HIMC imm_context, WPARAM lparam, int type, base::string16* result) { if (!(lparam & type)) return false; LONG string_size = ::ImmGetCompositionString(imm_context, type, NULL, 0); if (string_size <= 0) return false; DCHECK_EQ(0u, string_size % sizeof(wchar_t)); ::ImmGetCompositionString(imm_context, type, WriteInto(result, (string_size / sizeof(wchar_t)) + 1), string_size); return true; } bool IMM32Manager::GetResult( HWND window_handle, LPARAM lparam, base::string16* result) { bool ret = false; HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { ret = GetString(imm_context, lparam, GCS_RESULTSTR, result); ::ImmReleaseContext(window_handle, imm_context); } return ret; } bool IMM32Manager::GetComposition(HWND window_handle, LPARAM lparam, CompositionText* composition) { bool ret = false; HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { // Copy the composition string to the CompositionText object. ret = GetString(imm_context, lparam, GCS_COMPSTR, &composition->text); if (ret) { // This is a dirty workaround for facebook. Facebook deletes the // placeholder character (U+3000) used by Traditional-Chinese IMEs at the // beginning of composition text. This prevents WebKit from replacing this // placeholder character with a Traditional-Chinese character, i.e. we // cannot input any characters in a comment box of facebook with // Traditional-Chinese IMEs. As a workaround, we replace U+3000 at the // beginning of composition text with U+FF3F, a placeholder character used // by Japanese IMEs. if (input_language_id_ == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL) && composition->text[0] == 0x3000) { composition->text[0] = 0xFF3F; } // Retrieve the composition underlines and selection range information. GetCompositionInfo(imm_context, lparam, composition); // Mark that there is an ongoing composition. is_composing_ = true; } ::ImmReleaseContext(window_handle, imm_context); } return ret; } void IMM32Manager::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 IMM32Manager::CancelIME(HWND window_handle) { if (is_composing_) { HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { ::ImmNotifyIME(imm_context, NI_COMPOSITIONSTR, CPS_CANCEL, 0); ::ImmReleaseContext(window_handle, imm_context); } ResetComposition(window_handle); } } void IMM32Manager::EnableIME(HWND window_handle) { // 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); } void IMM32Manager::UpdateCaretRect(HWND window_handle, const gfx::Rect& caret_rect) { // 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 (caret_rect_ != caret_rect) { caret_rect_ = caret_rect; // Move the IME windows. HIMC imm_context = ::ImmGetContext(window_handle); if (imm_context) { MoveImeWindow(window_handle, imm_context); ::ImmReleaseContext(window_handle, imm_context); } } } void IMM32Manager::SetUseCompositionWindow(bool use_composition_window) { use_composition_window_ = use_composition_window; } std::string IMM32Manager::GetInputLanguageName() const { const LCID locale_id = MAKELCID(input_language_id_, SORT_DEFAULT); // max size for LOCALE_SISO639LANGNAME and LOCALE_SISO3166CTRYNAME is 9. wchar_t buffer[9]; // Get language id. int length = ::GetLocaleInfo(locale_id, LOCALE_SISO639LANGNAME, &buffer[0], arraysize(buffer)); if (length <= 1) return std::string(); std::string language; base::WideToUTF8(buffer, length - 1, &language); if (SUBLANGID(input_language_id_) == SUBLANG_NEUTRAL) return language; // Get region id. length = ::GetLocaleInfo(locale_id, LOCALE_SISO3166CTRYNAME, &buffer[0], arraysize(buffer)); if (length <= 1) return language; std::string region; base::WideToUTF8(buffer, length - 1, ®ion); return language.append(1, '-').append(region); } void IMM32Manager::SetTextInputMode(HWND window_handle, TextInputMode input_mode) { if (input_mode == ui::TEXT_INPUT_MODE_DEFAULT) return; const HIMC imm_context = ::ImmGetContext(window_handle); if (!imm_context) return; DWORD conversion_mode = 0; DWORD sentence_mode = 0; if (::ImmGetConversionStatus(imm_context, &conversion_mode, &sentence_mode) == FALSE) { return; } BOOL open = FALSE; ConvertInputModeToImmFlags(input_mode, conversion_mode, &open, &conversion_mode), ::ImmSetOpenStatus(imm_context, open); if (open) ::ImmSetConversionStatus(imm_context, conversion_mode, sentence_mode); ::ImmReleaseContext(window_handle, imm_context); } // static bool IMM32Manager::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. scoped_ptr<HKL[]> layouts(new HKL[size]); ::GetKeyboardLayoutList(size, layouts.get()); for (int i = 0; i < size; ++i) { if (IsRTLPrimaryLangID(PRIMARYLANGID(layouts[i]))) { layout = RTL_KEYBOARD_LAYOUT_INSTALLED; return true; } } layout = RTL_KEYBOARD_LAYOUT_NOT_INSTALLED; return false; } bool IMM32Manager::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; } void IMM32Manager::ConvertInputModeToImmFlags(TextInputMode input_mode, DWORD initial_conversion_mode, BOOL* open, DWORD* new_conversion_mode) { *open = TRUE; *new_conversion_mode = initial_conversion_mode; switch (input_mode) { case ui::TEXT_INPUT_MODE_FULL_WIDTH_LATIN: *new_conversion_mode |= IME_CMODE_FULLSHAPE; *new_conversion_mode &= ~(IME_CMODE_NATIVE | IME_CMODE_KATAKANA); break; case ui::TEXT_INPUT_MODE_KANA: *new_conversion_mode |= (IME_CMODE_NATIVE | IME_CMODE_FULLSHAPE); *new_conversion_mode &= ~IME_CMODE_KATAKANA; break; case ui::TEXT_INPUT_MODE_KATAKANA: *new_conversion_mode |= (IME_CMODE_NATIVE | IME_CMODE_KATAKANA | IME_CMODE_FULLSHAPE); break; default: *open = FALSE; break; } } } // namespace ui