diff options
author | suzhe@google.com <suzhe@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-03-23 23:19:20 +0000 |
---|---|---|
committer | suzhe@google.com <suzhe@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-03-23 23:19:20 +0000 |
commit | a738cb179ea4f49083478316f5274c0a6accffd7 (patch) | |
tree | 5e8c879413326095a897ed31b377a490ef781269 /ui | |
parent | 511ca0d0a8c2941c64d4fd55c7bccaaef4d884ea (diff) | |
download | chromium_src-a738cb179ea4f49083478316f5274c0a6accffd7.zip chromium_src-a738cb179ea4f49083478316f5274c0a6accffd7.tar.gz chromium_src-a738cb179ea4f49083478316f5274c0a6accffd7.tar.bz2 |
Move chrome/browser/ime_input.* to ui/base/win/
Move ExtractCompositionInfo method in chrome/browser/renderer_host/gtk_im_context_wrapper.cc to ui/base/gtk/gtk_im_context_util.cc
BUG=75003
TEST=none
Review URL: http://codereview.chromium.org/6709023
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@79214 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui')
-rw-r--r-- | ui/base/gtk/gtk_im_context_util.cc | 114 | ||||
-rw-r--r-- | ui/base/gtk/gtk_im_context_util.h | 24 | ||||
-rw-r--r-- | ui/base/gtk/gtk_im_context_util_unittest.cc | 150 | ||||
-rw-r--r-- | ui/base/ime/composition_text.cc (renamed from ui/base/ime/composition.cc) | 8 | ||||
-rw-r--r-- | ui/base/ime/composition_text.h (renamed from ui/base/ime/composition.h) | 14 | ||||
-rw-r--r-- | ui/base/win/ime_input.cc | 561 | ||||
-rw-r--r-- | ui/base/win/ime_input.h | 313 | ||||
-rw-r--r-- | ui/ui_base.gypi | 12 | ||||
-rw-r--r-- | ui/ui_unittests.gypi | 1 |
9 files changed, 1184 insertions, 13 deletions
diff --git a/ui/base/gtk/gtk_im_context_util.cc b/ui/base/gtk/gtk_im_context_util.cc new file mode 100644 index 0000000..1e90729 --- /dev/null +++ b/ui/base/gtk/gtk_im_context_util.cc @@ -0,0 +1,114 @@ +// 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/base/gtk/gtk_im_context_util.h" + +#include "base/basictypes.h" +#include "base/i18n/char_iterator.h" +#include "base/string16.h" +#include "base/utf_string_conversions.h" +#include "ui/base/ime/composition_text.h" + +namespace ui { + +void ExtractCompositionTextFromGtkPreedit(const gchar* utf8_text, + PangoAttrList* attrs, + int cursor_position, + CompositionText* composition) { + composition->Clear(); + composition->text = UTF8ToUTF16(utf8_text); + + if (composition->text.empty()) + return; + + // Gtk/Pango uses character index for cursor position and byte index for + // attribute range, but we use char16 offset for them. So we need to do + // conversion here. + std::vector<size_t> char16_offsets; + size_t length = composition->text.length(); + base::i18n::UTF16CharIterator char_iterator(&composition->text); + do { + char16_offsets.push_back(char_iterator.array_pos()); + } while (char_iterator.Advance()); + + // The text length in Unicode characters. + int char_length = static_cast<int>(char16_offsets.size()); + // Make sure we can convert the value of |char_length| as well. + char16_offsets.push_back(length); + + size_t cursor_offset = + char16_offsets[std::max(0, std::min(char_length, cursor_position))]; + + composition->selection = ui::Range(cursor_offset); + + if (attrs) { + int utf8_length = strlen(utf8_text); + PangoAttrIterator* iter = pango_attr_list_get_iterator(attrs); + + // We only care about underline and background attributes and convert + // background attribute into selection if possible. + do { + gint start, end; + pango_attr_iterator_range(iter, &start, &end); + + start = std::min(start, utf8_length); + end = std::min(end, utf8_length); + if (start >= end) + continue; + + start = g_utf8_pointer_to_offset(utf8_text, utf8_text + start); + end = g_utf8_pointer_to_offset(utf8_text, utf8_text + end); + + // Double check, in case |utf8_text| is not a valid utf-8 string. + start = std::min(start, char_length); + end = std::min(end, char_length); + if (start >= end) + continue; + + PangoAttribute* background_attr = + pango_attr_iterator_get(iter, PANGO_ATTR_BACKGROUND); + PangoAttribute* underline_attr = + pango_attr_iterator_get(iter, PANGO_ATTR_UNDERLINE); + + if (background_attr || underline_attr) { + // Use a black thin underline by default. + CompositionUnderline underline( + char16_offsets[start], char16_offsets[end], SK_ColorBLACK, false); + + // Always use thick underline for a range with background color, which + // is usually the selection range. + if (background_attr) { + underline.thick = true; + // If the cursor is at start or end of this underline, then we treat + // it as the selection range as well, but make sure to set the cursor + // position to the selection end. + if (underline.start_offset == cursor_offset) { + composition->selection.set_start(underline.end_offset); + composition->selection.set_end(cursor_offset); + } else if (underline.end_offset == cursor_offset) { + composition->selection.set_start(underline.start_offset); + composition->selection.set_end(cursor_offset); + } + } + if (underline_attr) { + int type = reinterpret_cast<PangoAttrInt*>(underline_attr)->value; + if (type == PANGO_UNDERLINE_DOUBLE) + underline.thick = true; + else if (type == PANGO_UNDERLINE_ERROR) + underline.color = SK_ColorRED; + } + composition->underlines.push_back(underline); + } + } while (pango_attr_iterator_next(iter)); + pango_attr_iterator_destroy(iter); + } + + // Use a black thin underline by default. + if (composition->underlines.empty()) { + composition->underlines.push_back( + CompositionUnderline(0, length, SK_ColorBLACK, false)); + } +} + +} // namespace ui diff --git a/ui/base/gtk/gtk_im_context_util.h b/ui/base/gtk/gtk_im_context_util.h new file mode 100644 index 0000000..dd560a9 --- /dev/null +++ b/ui/base/gtk/gtk_im_context_util.h @@ -0,0 +1,24 @@ +// 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. + +#ifndef UI_BASE_GTK_GTK_IM_CONTEXT_UTIL_H_ +#define UI_BASE_GTK_GTK_IM_CONTEXT_UTIL_H_ +#pragma once + +#include <pango/pango-attributes.h> + +namespace ui { + +struct CompositionText; + +// Extracts composition text information (text, underlines, selection range) +// from given Gtk preedit data (utf-8 text, pango attributes, cursor position). +void ExtractCompositionTextFromGtkPreedit(const gchar* utf8_text, + PangoAttrList* attrs, + int cursor_position, + CompositionText* composition); + +} // namespace ui + +#endif // UI_BASE_GTK_GTK_IM_CONTEXT_UTIL_H_ diff --git a/ui/base/gtk/gtk_im_context_util_unittest.cc b/ui/base/gtk/gtk_im_context_util_unittest.cc new file mode 100644 index 0000000..89dff22 --- /dev/null +++ b/ui/base/gtk/gtk_im_context_util_unittest.cc @@ -0,0 +1,150 @@ +// 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 <string> +#include <utility> + +#include "base/basictypes.h" +#include "base/logging.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/gtk/gtk_im_context_util.h" +#include "ui/base/ime/composition_text.h" + +namespace { + +struct AttributeInfo { + int type; + int value; + int start_offset; + int end_offset; +}; + +struct Underline { + unsigned start_offset; + unsigned end_offset; + uint32 color; + bool thick; +}; + +struct TestData { + const char* text; + const AttributeInfo attrs[10]; + const Underline underlines[10]; +}; + +const TestData kTestData[] = { + // Normal case + { "One Two Three", + { { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 0, 3 }, + { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_DOUBLE, 4, 7 }, + { PANGO_ATTR_BACKGROUND, 0, 4, 7 }, + { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 8, 13 }, + { 0, 0, 0, 0 } }, + { { 0, 3, SK_ColorBLACK, false }, + { 4, 7, SK_ColorBLACK, true }, + { 8, 13, SK_ColorBLACK, false }, + { 0, 0, 0, false } } + }, + + // Offset overflow. + { "One Two Three", + { { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 0, 3 }, + { PANGO_ATTR_BACKGROUND, 0, 4, 7 }, + { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 8, 20 }, + { 0, 0, 0, 0 } }, + { { 0, 3, SK_ColorBLACK, false }, + { 4, 7, SK_ColorBLACK, true }, + { 8, 13, SK_ColorBLACK, false }, + { 0, 0, 0, false} } + }, + + // Error underline. + { "One Two Three", + { { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 0, 3 }, + { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_ERROR, 4, 7 }, + { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 8, 13 }, + { 0, 0, 0, 0 } }, + { { 0, 3, SK_ColorBLACK, false }, + { 4, 7, SK_ColorRED, false }, + { 8, 13, SK_ColorBLACK, false }, + { 0, 0, 0, false} } + }, + + // Default underline. + { "One Two Three", + { { 0, 0, 0, 0 } }, + { { 0, 13, SK_ColorBLACK, false }, + { 0, 0, 0, false } } + }, + + // Unicode, including non-BMP characters: "123你好𠀀𠀁一丁 456" + { "123\xE4\xBD\xA0\xE5\xA5\xBD\xF0\xA0\x80\x80\xF0\xA0\x80\x81\xE4\xB8\x80" + "\xE4\xB8\x81 456", + { { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 0, 3 }, + { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 3, 5 }, + { PANGO_ATTR_BACKGROUND, 0, 5, 7 }, + { PANGO_ATTR_UNDERLINE, PANGO_UNDERLINE_SINGLE, 7, 13 }, + { 0, 0, 0, 0 } }, + { { 0, 3, SK_ColorBLACK, false }, + { 3, 5, SK_ColorBLACK, false }, + { 5, 9, SK_ColorBLACK, true }, + { 9, 15, SK_ColorBLACK, false }, + { 0, 0, 0, false } } + }, +}; + +void CompareUnderline(const Underline& a, + const ui::CompositionUnderline& b) { + EXPECT_EQ(a.start_offset, b.start_offset); + EXPECT_EQ(a.end_offset, b.end_offset); + EXPECT_EQ(a.color, b.color); + EXPECT_EQ(a.thick, b.thick); +} + +class GtkIMContextWrapperTest : public testing::Test { +}; + +TEST(GtkIMContextUtilTest, ExtractCompositionText) { + for (size_t i = 0; i < arraysize(kTestData); ++i) { + const char* text = kTestData[i].text; + const AttributeInfo* attrs = kTestData[i].attrs; + SCOPED_TRACE(testing::Message() << "Testing:" << i + << " text:" << text); + + PangoAttrList* pango_attrs = pango_attr_list_new(); + for (size_t a = 0; attrs[a].type; ++a) { + PangoAttribute* pango_attr = NULL; + switch (attrs[a].type) { + case PANGO_ATTR_UNDERLINE: + pango_attr = pango_attr_underline_new( + static_cast<PangoUnderline>(attrs[a].value)); + break; + case PANGO_ATTR_BACKGROUND: + pango_attr = pango_attr_background_new(0, 0, 0); + break; + default: + NOTREACHED(); + } + pango_attr->start_index = + g_utf8_offset_to_pointer(text, attrs[a].start_offset) - text; + pango_attr->end_index = + g_utf8_offset_to_pointer(text, attrs[a].end_offset) - text; + pango_attr_list_insert(pango_attrs, pango_attr); + } + + ui::CompositionText result; + ui::ExtractCompositionTextFromGtkPreedit(text, pango_attrs, 0, &result); + + const Underline* underlines = kTestData[i].underlines; + for (size_t u = 0; underlines[u].color && + u < result.underlines.size(); ++u) { + SCOPED_TRACE(testing::Message() << "Underline:" << u); + CompareUnderline(underlines[u], result.underlines[u]); + } + + pango_attr_list_unref(pango_attrs); + } +} + +} // namespace diff --git a/ui/base/ime/composition.cc b/ui/base/ime/composition_text.cc index 855a713..660b3ef 100644 --- a/ui/base/ime/composition.cc +++ b/ui/base/ime/composition_text.cc @@ -2,17 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "ui/base/ime/composition.h" +#include "ui/base/ime/composition_text.h" namespace ui { -Composition::Composition() { +CompositionText::CompositionText() { } -Composition::~Composition() { +CompositionText::~CompositionText() { } -void Composition::Clear() { +void CompositionText::Clear() { text.clear(); underlines.clear(); selection = Range(); diff --git a/ui/base/ime/composition.h b/ui/base/ime/composition_text.h index e59d976..2014e50 100644 --- a/ui/base/ime/composition.h +++ b/ui/base/ime/composition_text.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef UI_BASE_IME_COMPOSITION_H_ -#define UI_BASE_IME_COMPOSITION_H_ +#ifndef UI_BASE_IME_COMPOSITION_TEXT_H_ +#define UI_BASE_IME_COMPOSITION_TEXT_H_ #pragma once #include "base/string16.h" @@ -12,10 +12,10 @@ namespace ui { -// A struct represents the status of an ongoing composition. -struct Composition { - Composition(); - ~Composition(); +// A struct represents the status of an ongoing composition text. +struct CompositionText { + CompositionText(); + ~CompositionText(); void Clear(); @@ -36,4 +36,4 @@ struct Composition { } // namespace ui -#endif // UI_BASE_IME_COMPOSITION_H_ +#endif // UI_BASE_IME_COMPOSITION_TEXT_H_ diff --git a/ui/base/win/ime_input.cc b/ui/base/win/ime_input.cc new file mode 100644 index 0000000..a520f03 --- /dev/null +++ b/ui/base/win/ime_input.cc @@ -0,0 +1,561 @@ +// 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/base/win/ime_input.h" + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "base/string16.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.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(char16), wchar_t__char16_diff); + +/////////////////////////////////////////////////////////////////////////////// +// ImeInput + +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 ImeInput::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_array<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; + } + if (start == attribute_size) { + // This composition clause does not contain any target clauses, + // i.e. this clauses is an input clause. + // We treat the whole composition as a target clause. + start = 0; + end = attribute_size; + } + } + *target_start = start; + *target_end = end; + } +} + +// Helper function for ImeInput::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_array<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; + + // Use thick underline for the target clause. + if (underline.start_offset >= static_cast<unsigned>(target_start) && + underline.end_offset <= static_cast<unsigned>(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 { + +ImeInput::ImeInput() + : ime_status_(false), + input_language_id_(LANG_USER_DEFAULT), + is_composing_(false), + system_caret_(false), + caret_rect_(-1, -1, 0, 0) { +} + +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<LANGID>(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. + // 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 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; + return ::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_rect_.x(); + int y = caret_rect_.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_) { + switch (PRIMARYLANGID(input_language_id_)) { + case LANG_JAPANESE: + ::SetCaretPos(x, y + caret_rect_.height()); + break; + default: + ::SetCaretPos(x, y); + break; + } + } + 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 ImeInput::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 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::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) { + composition->selection = ui::Range(0); + } else if (lparam & GCS_CURSORPOS) { + // If cursor position is same as target_start or target_end, then selects + // the target range instead. We always use cursor position as selection end, + // so that if the client doesn't support drawing selection with composition, + // it can always retrieve the correct cursor position. + int cursor = ::ImmGetCompositionString(imm_context, GCS_CURSORPOS, NULL, 0); + if (cursor == target_start) + composition->selection = ui::Range(target_end, cursor); + else if (cursor == target_end) + composition->selection = ui::Range(target_start, cursor); + else + composition->selection = ui::Range(cursor); + } else { + composition->selection = ui::Range(target_start, target_end); + } + + // 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; + if (target_start > 0) { + underline.start_offset = 0; + underline.end_offset = target_start; + underline.thick = false; + composition->underlines.push_back(underline); + } + if (target_end > target_start) { + underline.start_offset = target_start; + underline.end_offset = target_end; + underline.thick = true; + composition->underlines.push_back(underline); + } + if (target_end < length) { + underline.start_offset = target_end; + underline.end_offset = length; + underline.thick = false; + composition->underlines.push_back(underline); + } + } +} + +bool ImeInput::GetString(HIMC imm_context, WPARAM lparam, int type, + string16* result) { + bool ret = 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(result, string_length + 1); + if (string_data) { + // Fill the given result object. + ::ImmGetCompositionString(imm_context, type, string_data, string_size); + ret = true; + } + } + } + return ret; +} + +bool ImeInput::GetResult(HWND window_handle, LPARAM lparam, 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 ImeInput::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 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::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 ImeInput::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 ImeInput::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); + } + } +} + +std::string ImeInput::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; + 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; + WideToUTF8(buffer, length - 1, ®ion); + return language.append(1, '-').append(region); +} + +base::i18n::TextDirection ImeInput::GetTextDirection() const { + return IsRTLPrimaryLangID(PRIMARYLANGID(input_language_id_)) ? + base::i18n::RIGHT_TO_LEFT : base::i18n::LEFT_TO_RIGHT; +} + +// static +bool ImeInput::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_array<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 ImeInput::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 adandon 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; + for (int i = 0; i <= VK_PACKET; ++i) { + if (keystate[i] & kKeyDownMask) + return false; + } + return true; +} + +} // namespace ui diff --git a/ui/base/win/ime_input.h b/ui/base/win/ime_input.h new file mode 100644 index 0000000..9e8ab75 --- /dev/null +++ b/ui/base/win/ime_input.h @@ -0,0 +1,313 @@ +// 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. + +#ifndef UI_BASE_WIN_IME_INPUT_H_ +#define UI_BASE_WIN_IME_INPUT_H_ +#pragma once + +#include <windows.h> + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/i18n/rtl.h" +#include "base/string16.h" +#include "ui/gfx/rect.h" + +namespace ui { + +struct CompositionText; + +// This header file defines a struct and a class used for encapsulating IMM32 +// APIs, controls IMEs attached to a window, and enables the 'on-the-spot' +// input without deep knowledge about the APIs, i.e. knowledge about the +// language-specific and IME-specific behaviors. +// The following items enumerates the simplest steps for an (window) +// application to control its IMEs with the struct and the class defined +// this file. +// 1. Add an instance of the ImeInput class to its window class. +// (The ImeInput class needs a window handle.) +// 2. Add messages handlers listed in the following subsections, follow the +// instructions written in each subsection, and use the ImeInput class. +// 2.1. WM_IME_SETCONTEXT (0x0281) +// Call the functions listed below: +// - ImeInput::CreateImeWindow(); +// - ImeInput::CleanupComposition(), and; +// - ImeInput::SetImeWindowStyle(). +// An application MUST prevent from calling ::DefWindowProc(). +// 2.2. WM_IME_STARTCOMPOSITION (0x010D) +// Call the functions listed below: +// - ImeInput::CreateImeWindow(), and; +// - ImeInput::ResetComposition(). +// An application MUST prevent from calling ::DefWindowProc(). +// 2.3. WM_IME_COMPOSITION (0x010F) +// Call the functions listed below: +// - ImeInput::UpdateImeWindow(); +// - ImeInput::GetResult(); +// - ImeInput::GetComposition(), and; +// - ImeInput::ResetComposition() (optional). +// An application MUST prevent from calling ::DefWindowProc(). +// 2.4. WM_IME_ENDCOMPOSITION (0x010E) +// Call the functions listed below: +// - ImeInput::ResetComposition(), and; +// - ImeInput::DestroyImeWindow(). +// An application CAN call ::DefWindowProc(). +// 2.5. WM_INPUTLANGCHANGE (0x0051) +// Call the functions listed below: +// - ImeInput::SetInputLanguage(). +// An application CAN call ::DefWindowProc(). + +// This class controls the IMM (Input Method Manager) through IMM32 APIs and +// enables it to retrieve the string being controled by the IMM. (I wrote +// a note to describe the reason why I do not use 'IME' but 'IMM' below.) +// NOTE(hbono): +// Fortunately or unfortunately, TSF (Text Service Framework) and +// CUAS (Cicero Unaware Application Support) allows IMM32 APIs for +// retrieving not only the inputs from IMEs (Input Method Editors), used +// only for inputting East-Asian language texts, but also the ones from +// tablets (on Windows XP Tablet PC Edition and Windows Vista), voice +// recognizers (e.g. ViaVoice and Microsoft Office), etc. +// We can disable TSF and CUAS in Windows XP Tablet PC Edition. On the other +// hand, we can NEVER disable either TSF or CUAS in Windows Vista, i.e. +// THIS CLASS IS NOT ONLY USED ON THE INPUT CONTEXTS OF EAST-ASIAN +// LANGUAGES BUT ALSO USED ON THE INPUT CONTEXTS OF ALL LANGUAGES. +class ImeInput { + public: + ImeInput(); + ~ImeInput(); + + // Retrieves whether or not there is an ongoing composition. + bool is_composing() const { return is_composing_; } + + // Retrieves the input language from Windows and update it. + // Return values + // * true + // The given input language has IMEs. + // * false + // The given input language does not have IMEs. + bool SetInputLanguage(); + + // Creates the IME windows, and allocate required resources for them. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + void CreateImeWindow(HWND window_handle); + + // Updates the style of the IME windows. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + // * message [in] (UINT) + // * wparam [in] (WPARAM) + // * lparam [in] (LPARAM) + // Represent the windows message of the caller. + // These parameters are used for verifying if this function is called + // in a handler function for WM_IME_SETCONTEXT messages because this + // function uses ::DefWindowProc() to update the style. + // A caller just has to pass the input parameters for the handler + // function without modifications. + // * handled [out] (BOOL*) + // Returns ::DefWindowProc() is really called in this function. + // PLEASE DO NOT CALL ::DefWindowProc() IF THIS VALUE IS TRUE! + // All the window styles set in this function are over-written when + // calling ::DefWindowProc() after returning this function. + // Returns the value returned by DefWindowProc. + LRESULT SetImeWindowStyle(HWND window_handle, UINT message, + WPARAM wparam, LPARAM lparam, BOOL* handled); + + // Destroys the IME windows and all the resources attached to them. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + void DestroyImeWindow(HWND window_handle); + + // Updates the position of the IME windows. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + void UpdateImeWindow(HWND window_handle); + + // Cleans up the all resources attached to the given ImeInput object, and + // reset its composition status. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + void CleanupComposition(HWND window_handle); + + // Resets the composition status. + // Cancel the ongoing composition if it exists. + // NOTE(hbono): This method does not release the allocated resources. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + void ResetComposition(HWND window_handle); + + // Retrieves a composition result of the ongoing composition if it exists. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + // * lparam [in] (LPARAM) + // Specifies the updated members of the ongoing composition, and must be + // the same parameter of a WM_IME_COMPOSITION message handler. + // This parameter is used for checking if the ongoing composition has + // its result string, + // * result [out] (string16) + // Represents the object contains the composition result. + // Return values + // * true + // The ongoing composition has a composition result. + // * false + // The ongoing composition does not have composition results. + // Remarks + // This function is designed for being called from WM_IME_COMPOSITION + // message handlers. + bool GetResult(HWND window_handle, LPARAM lparam, string16* result); + + // Retrieves the current composition status of the ongoing composition. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + // * lparam [in] (LPARAM) + // Specifies the updated members of the ongoing composition, and must be + // the same parameter of a WM_IME_COMPOSITION message handler. + // This parameter is used for checking if the ongoing composition has + // its result string, + // * composition [out] (Composition) + // Represents the struct contains the composition status. + // Return values + // * true + // The status of the ongoing composition is updated. + // * false + // The status of the ongoing composition is not updated. + // Remarks + // This function is designed for being called from WM_IME_COMPOSITION + // message handlers. + bool GetComposition(HWND window_handle, LPARAM lparam, + CompositionText* composition); + + // Enables the IME attached to the given window, i.e. allows user-input + // events to be dispatched to the IME. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + // * complete [in] (bool) + // Represents whether or not to complete the ongoing composition. + // + true + // After finishing the ongoing composition and close its IME windows, + // start another composition and display its IME windows to the given + // position. + // + false + // Just move the IME windows of the ongoing composition to the given + // position without finishing it. + void EnableIME(HWND window_handle); + + // Disables the IME attached to the given window, i.e. prohibits any + // user-input events from being dispatched to the IME. + // In Chrome, this function is used when: + // * a renreder process sets its input focus to a password input. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + void DisableIME(HWND window_handle); + + // Cancels an ongoing composition of the IME attached to the given window. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + void CancelIME(HWND window_handle); + + // Updates the caret position of the given window. + // Parameters + // * window_handle [in] (HWND) + // Represents the window handle of the caller. + // * caret_rect [in] (const gfx::Rect&) + // Represent the rectangle of the input caret. + // This rectangle is used for controlling the positions of IME windows. + void UpdateCaretRect(HWND window_handle, const gfx::Rect& caret_rect); + + // Returns the current input language id. + LANGID input_language_id() const { return input_language_id_; } + + // Returns BCP-47 tag name of the current input language. + std::string GetInputLanguageName() const; + + // Returns the text direction of the current input language. + base::i18n::TextDirection GetTextDirection() const; + + + // Helper functions ---------------------------------------------------------- + + // Checks if there is any RTL keyboard layout installed in the system. + static bool IsRTLKeyboardLayoutInstalled(); + + // Checks if the user pressed both Ctrl and right or left Shift keys to + // requrest 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|. + static bool IsCtrlShiftPressed(base::i18n::TextDirection* direction); + + protected: + // Retrieves the composition information. + void GetCompositionInfo(HIMC imm_context, LPARAM lparam, + CompositionText* composition); + + // Updates the position of the IME windows. + void MoveImeWindow(HWND window_handle, HIMC imm_context); + + // Completes the ongoing composition if it exists. + void CompleteComposition(HWND window_handle, HIMC imm_context); + + // Retrieves a string from the IMM. + bool GetString(HIMC imm_context, WPARAM lparam, int type, string16* result); + + private: + // Represents whether or not there is an ongoing composition in a browser + // process, i.e. whether or not a browser process is composing a text. + bool is_composing_; + + // This value represents whether or not the current input context has IMEs. + // The following table shows the list of IME status: + // Value Description + // false The current input language does not have IMEs. + // true The current input language has IMEs. + bool ime_status_; + + // The current input Language ID retrieved from Windows, which consists of: + // * Primary Language ID (bit 0 to bit 9), which shows a natunal language + // (English, Korean, Chinese, Japanese, etc.) and; + // * Sub-Language ID (bit 10 to bit 15), which shows a geometrical region + // the language is spoken (For English, United States, United Kingdom, + // Australia, Canada, etc.) + // The following list enumerates some examples for the Language ID: + // * "en-US" (0x0409) + // MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + // * "ko-KR" (0x0412) + // MAKELANGID(LANG_KOREAN, SUBLANG_KOREAN); + // * "zh-TW" (0x0404) + // MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL); + // * "zh-CN" (0x0804) + // MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED); + // * "ja-JP" (0x0411) + // MAKELANGID(LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN), etc. + // (See <winnt.h> for other available values.) + // This Language ID is used for processing language-specific operations in + // IME functions. + LANGID input_language_id_; + + // Represents whether or not the current input context has created a system + // caret to set the position of its IME candidate window. + // * true: it creates a system caret. + // * false: it does not create a system caret. + bool system_caret_; + + // The rectangle of the input caret retrieved from a renderer process. + gfx::Rect caret_rect_; + + DISALLOW_COPY_AND_ASSIGN(ImeInput); +}; + +} // namespace ui + +#endif // UI_BASE_WIN_IME_INPUT_H_ diff --git a/ui/ui_base.gypi b/ui/ui_base.gypi index db12016..e9e891e 100644 --- a/ui/ui_base.gypi +++ b/ui/ui_base.gypi @@ -49,10 +49,12 @@ 'base/clipboard/clipboard_win.cc', 'base/clipboard/scoped_clipboard_writer.cc', 'base/clipboard/scoped_clipboard_writer.h', - 'base/ime/composition.cc', - 'base/ime/composition.h', + 'base/ime/composition_text.cc', + 'base/ime/composition_text.h', 'base/ime/composition_underline.h', 'base/ime/text_input_type.h', + 'base/gtk/gtk_im_context_util.cc', + 'base/gtk/gtk_im_context_util.h', 'base/range/range.cc', 'base/range/range.h', 'base/range/range.mm', @@ -65,6 +67,12 @@ '../build/linux/system.gyp:xext', ], }], + ['OS=="win"', { + 'sources': [ + 'base/win/ime_input.cc', + 'base/win/ime_input.h', + ], + }], ], }, ], diff --git a/ui/ui_unittests.gypi b/ui/ui_unittests.gypi index 617e434..2a767a4 100644 --- a/ui/ui_unittests.gypi +++ b/ui/ui_unittests.gypi @@ -24,6 +24,7 @@ 'base/animation/multi_animation_unittest.cc', 'base/animation/slide_animation_unittest.cc', 'base/clipboard/clipboard_unittest.cc', + 'base/gtk/gtk_im_context_util_unittest.cc', 'base/range/range_unittest.cc', 'base/range/range_unittest.mm', 'gfx/blit_unittest.cc', |