• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2016 The Chromium Embedded Framework Authors. Portions copyright
2 // 2013 The Chromium Authors. All rights reserved. Use of this source code is
3 // governed by a BSD-style license that can be found in the LICENSE file.
4 
5 // Implementation based on ui/base/ime/win/imm32_manager.cc from Chromium.
6 
7 #include <msctf.h>
8 #include <windowsx.h>
9 
10 #include "include/base/cef_build.h"
11 #include "tests/cefclient/browser/osr_ime_handler_win.h"
12 #include "tests/cefclient/browser/resource.h"
13 #include "tests/shared/browser/geometry_util.h"
14 #include "tests/shared/browser/main_message_loop.h"
15 #include "tests/shared/browser/util_win.h"
16 
17 #define ColorUNDERLINE \
18   0xFF000000  // Black SkColor value for underline,
19               // same as Blink.
20 #define ColorBKCOLOR \
21   0x00000000  // White SkColor value for background,
22               // same as Blink.
23 
24 namespace client {
25 
26 namespace {
27 
28 // Determines whether or not the given attribute represents a selection
IsSelectionAttribute(char attribute)29 bool IsSelectionAttribute(char attribute) {
30   return (attribute == ATTR_TARGET_CONVERTED ||
31           attribute == ATTR_TARGET_NOTCONVERTED);
32 }
33 
34 // Helper function for OsrImeHandlerWin::GetCompositionInfo() method,
35 // to get the target range that's selected by the user in the current
36 // composition string.
GetCompositionSelectionRange(HIMC imc,int * target_start,int * target_end)37 void GetCompositionSelectionRange(HIMC imc,
38                                   int* target_start,
39                                   int* target_end) {
40   int attribute_size = ::ImmGetCompositionString(imc, GCS_COMPATTR, nullptr, 0);
41   if (attribute_size > 0) {
42     int start = 0;
43     int end = 0;
44     std::vector<char> attribute_data(attribute_size);
45 
46     ::ImmGetCompositionString(imc, GCS_COMPATTR, &attribute_data[0],
47                               attribute_size);
48     for (start = 0; start < attribute_size; ++start) {
49       if (IsSelectionAttribute(attribute_data[start]))
50         break;
51     }
52     for (end = start; end < attribute_size; ++end) {
53       if (!IsSelectionAttribute(attribute_data[end]))
54         break;
55     }
56 
57     *target_start = start;
58     *target_end = end;
59   }
60 }
61 
62 // Helper function for OsrImeHandlerWin::GetCompositionInfo() method, to get
63 // underlines information of the current composition string.
GetCompositionUnderlines(HIMC imc,int target_start,int target_end,std::vector<CefCompositionUnderline> & underlines)64 void GetCompositionUnderlines(
65     HIMC imc,
66     int target_start,
67     int target_end,
68     std::vector<CefCompositionUnderline>& underlines) {
69   int clause_size = ::ImmGetCompositionString(imc, GCS_COMPCLAUSE, nullptr, 0);
70   int clause_length = clause_size / sizeof(uint32);
71   if (clause_length) {
72     std::vector<uint32> clause_data(clause_length);
73 
74     ::ImmGetCompositionString(imc, GCS_COMPCLAUSE, &clause_data[0],
75                               clause_size);
76     for (int i = 0; i < clause_length - 1; ++i) {
77       cef_composition_underline_t underline;
78       underline.range.from = clause_data[i];
79       underline.range.to = clause_data[i + 1];
80       underline.color = ColorUNDERLINE;
81       underline.background_color = ColorBKCOLOR;
82       underline.thick = 0;
83 
84       // Use thick underline for the target clause.
85       if (underline.range.from >= target_start &&
86           underline.range.to <= target_end) {
87         underline.thick = 1;
88       }
89       underlines.push_back(underline);
90     }
91   }
92 }
93 
94 }  // namespace
95 
OsrImeHandlerWin(HWND hwnd)96 OsrImeHandlerWin::OsrImeHandlerWin(HWND hwnd)
97     : is_composing_(false),
98       input_language_id_(LANG_USER_DEFAULT),
99       system_caret_(false),
100       cursor_index_(-1),
101       hwnd_(hwnd) {
102   ime_rect_ = {-1, -1, 0, 0};
103 }
104 
~OsrImeHandlerWin()105 OsrImeHandlerWin::~OsrImeHandlerWin() {
106   DestroyImeWindow();
107 }
108 
SetInputLanguage()109 void OsrImeHandlerWin::SetInputLanguage() {
110   // Retrieve the current input language from the system's keyboard layout.
111   // Using GetKeyboardLayoutName instead of GetKeyboardLayout, because
112   // the language from GetKeyboardLayout is the language under where the
113   // keyboard layout is installed. And the language from GetKeyboardLayoutName
114   // indicates the language of the keyboard layout itself.
115   // See crbug.com/344834.
116   WCHAR keyboard_layout[KL_NAMELENGTH];
117   if (::GetKeyboardLayoutNameW(keyboard_layout)) {
118     input_language_id_ =
119         static_cast<LANGID>(_wtoi(&keyboard_layout[KL_NAMELENGTH >> 1]));
120   } else {
121     input_language_id_ = 0x0409;  // Fallback to en-US.
122   }
123 }
124 
CreateImeWindow()125 void OsrImeHandlerWin::CreateImeWindow() {
126   // Chinese/Japanese IMEs somehow ignore function calls to
127   // ::ImmSetCandidateWindow(), and use the position of the current system
128   // caret instead -::GetCaretPos().
129   // Therefore, we create a temporary system caret for Chinese IMEs and use
130   // it during this input context.
131   // Since some third-party Japanese IME also uses ::GetCaretPos() to determine
132   // their window position, we also create a caret for Japanese IMEs.
133   if (PRIMARYLANGID(input_language_id_) == LANG_CHINESE ||
134       PRIMARYLANGID(input_language_id_) == LANG_JAPANESE) {
135     if (!system_caret_) {
136       if (::CreateCaret(hwnd_, nullptr, 1, 1))
137         system_caret_ = true;
138     }
139   }
140 }
141 
DestroyImeWindow()142 void OsrImeHandlerWin::DestroyImeWindow() {
143   // Destroy the system caret if we have created for this IME input context.
144   if (system_caret_) {
145     ::DestroyCaret();
146     system_caret_ = false;
147   }
148 }
149 
MoveImeWindow()150 void OsrImeHandlerWin::MoveImeWindow() {
151   // Does nothing when the target window has no input focus.
152   if (GetFocus() != hwnd_)
153     return;
154 
155   CefRect rc = ime_rect_;
156   int location = cursor_index_;
157 
158   // If location is not specified fall back to the composition range start.
159   if (location == -1)
160     location = composition_range_.from;
161 
162   // Offset location by the composition range start if required.
163   if (location >= composition_range_.from)
164     location -= composition_range_.from;
165 
166   if (location < static_cast<int>(composition_bounds_.size()))
167     rc = composition_bounds_[location];
168   else
169     return;
170 
171   HIMC imc = ::ImmGetContext(hwnd_);
172   if (imc) {
173     const int kCaretMargin = 1;
174     if (PRIMARYLANGID(input_language_id_) == LANG_CHINESE) {
175       // Chinese IMEs ignore function calls to ::ImmSetCandidateWindow()
176       // when a user disables TSF (Text Service Framework) and CUAS (Cicero
177       // Unaware Application Support).
178       // On the other hand, when a user enables TSF and CUAS, Chinese IMEs
179       // ignore the position of the current system caret and use the
180       // parameters given to ::ImmSetCandidateWindow() with its 'dwStyle'
181       // parameter CFS_CANDIDATEPOS.
182       // Therefore, we do not only call ::ImmSetCandidateWindow() but also
183       // set the positions of the temporary system caret if it exists.
184       CANDIDATEFORM candidate_position = {
185           0, CFS_CANDIDATEPOS, {rc.x, rc.y}, {0, 0, 0, 0}};
186       ::ImmSetCandidateWindow(imc, &candidate_position);
187     }
188     if (system_caret_) {
189       switch (PRIMARYLANGID(input_language_id_)) {
190         case LANG_JAPANESE:
191           ::SetCaretPos(rc.x, rc.y + rc.height);
192           break;
193         default:
194           ::SetCaretPos(rc.x, rc.y);
195           break;
196       }
197     }
198 
199     if (PRIMARYLANGID(input_language_id_) == LANG_KOREAN) {
200       // Korean IMEs require the lower-left corner of the caret to move their
201       // candidate windows.
202       rc.y += kCaretMargin;
203     }
204 
205     // Japanese IMEs and Korean IMEs also use the rectangle given to
206     // ::ImmSetCandidateWindow() with its 'dwStyle' parameter CFS_EXCLUDE
207     // Therefore, we also set this parameter here.
208     CANDIDATEFORM exclude_rectangle = {
209         0,
210         CFS_EXCLUDE,
211         {rc.x, rc.y},
212         {rc.x, rc.y, rc.x + rc.width, rc.y + rc.height}};
213     ::ImmSetCandidateWindow(imc, &exclude_rectangle);
214 
215     ::ImmReleaseContext(hwnd_, imc);
216   }
217 }
218 
CleanupComposition()219 void OsrImeHandlerWin::CleanupComposition() {
220   // Notify the IMM attached to the given window to complete the ongoing
221   // composition (when given window is de-activated while composing and
222   // re-activated) and reset the composition status.
223   if (is_composing_) {
224     HIMC imc = ::ImmGetContext(hwnd_);
225     if (imc) {
226       ::ImmNotifyIME(imc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
227       ::ImmReleaseContext(hwnd_, imc);
228     }
229     ResetComposition();
230   }
231 }
232 
ResetComposition()233 void OsrImeHandlerWin::ResetComposition() {
234   // Reset the composition status.
235   is_composing_ = false;
236   cursor_index_ = -1;
237 }
238 
GetCompositionInfo(HIMC imc,LPARAM lparam,CefString & composition_text,std::vector<CefCompositionUnderline> & underlines,int & composition_start)239 void OsrImeHandlerWin::GetCompositionInfo(
240     HIMC imc,
241     LPARAM lparam,
242     CefString& composition_text,
243     std::vector<CefCompositionUnderline>& underlines,
244     int& composition_start) {
245   // We only care about GCS_COMPATTR, GCS_COMPCLAUSE and GCS_CURSORPOS, and
246   // convert them into underlines and selection range respectively.
247   underlines.clear();
248 
249   int length = static_cast<int>(composition_text.length());
250 
251   // Find out the range selected by the user.
252   int target_start = length;
253   int target_end = length;
254   if (lparam & GCS_COMPATTR)
255     GetCompositionSelectionRange(imc, &target_start, &target_end);
256 
257   // Retrieve the selection range information. If CS_NOMOVECARET is specified
258   // it means the cursor should not be moved and we therefore place the caret at
259   // the beginning of the composition string. Otherwise we should honour the
260   // GCS_CURSORPOS value if it's available.
261   // TODO(suzhe): Due to a bug in WebKit we currently can't use selection range
262   // with composition string.
263   // See: https://bugs.webkit.org/show_bug.cgi?id=40805
264   if (!(lparam & CS_NOMOVECARET) && (lparam & GCS_CURSORPOS)) {
265     // IMM32 does not support non-zero-width selection in a composition. So
266     // always use the caret position as selection range.
267     int cursor = ::ImmGetCompositionString(imc, GCS_CURSORPOS, nullptr, 0);
268     composition_start = cursor;
269   } else {
270     composition_start = 0;
271   }
272 
273   // Retrieve the clause segmentations and convert them to underlines.
274   if (lparam & GCS_COMPCLAUSE)
275     GetCompositionUnderlines(imc, target_start, target_end, underlines);
276 
277   // Set default underlines in case there is no clause information.
278   if (!underlines.size()) {
279     CefCompositionUnderline underline;
280     underline.color = ColorUNDERLINE;
281     underline.background_color = ColorBKCOLOR;
282     if (target_start > 0) {
283       underline.range.from = 0;
284       underline.range.to = target_start;
285       underline.thick = 0;
286       underlines.push_back(underline);
287     }
288     if (target_end > target_start) {
289       underline.range.from = target_start;
290       underline.range.to = target_end;
291       underline.thick = 1;
292       underlines.push_back(underline);
293     }
294     if (target_end < length) {
295       underline.range.from = target_end;
296       underline.range.to = length;
297       underline.thick = 0;
298       underlines.push_back(underline);
299     }
300   }
301 }
302 
GetString(HIMC imc,WPARAM lparam,int type,CefString & result)303 bool OsrImeHandlerWin::GetString(HIMC imc,
304                                  WPARAM lparam,
305                                  int type,
306                                  CefString& result) {
307   if (!(lparam & type))
308     return false;
309   LONG string_size = ::ImmGetCompositionString(imc, type, nullptr, 0);
310   if (string_size <= 0)
311     return false;
312 
313   // For trailing nullptr - ImmGetCompositionString excludes that.
314   string_size += sizeof(WCHAR);
315 
316   std::vector<wchar_t> buffer(string_size);
317   ::ImmGetCompositionString(imc, type, &buffer[0], string_size);
318   result.FromWString(&buffer[0]);
319   return true;
320 }
321 
GetResult(LPARAM lparam,CefString & result)322 bool OsrImeHandlerWin::GetResult(LPARAM lparam, CefString& result) {
323   bool ret = false;
324   HIMC imc = ::ImmGetContext(hwnd_);
325   if (imc) {
326     ret = GetString(imc, lparam, GCS_RESULTSTR, result);
327     ::ImmReleaseContext(hwnd_, imc);
328   }
329   return ret;
330 }
331 
GetComposition(LPARAM lparam,CefString & composition_text,std::vector<CefCompositionUnderline> & underlines,int & composition_start)332 bool OsrImeHandlerWin::GetComposition(
333     LPARAM lparam,
334     CefString& composition_text,
335     std::vector<CefCompositionUnderline>& underlines,
336     int& composition_start) {
337   bool ret = false;
338   HIMC imc = ::ImmGetContext(hwnd_);
339   if (imc) {
340     // Copy the composition string to the CompositionText object.
341     ret = GetString(imc, lparam, GCS_COMPSTR, composition_text);
342 
343     if (ret) {
344       // Retrieve the composition underlines and selection range information.
345       GetCompositionInfo(imc, lparam, composition_text, underlines,
346                          composition_start);
347 
348       // Mark that there is an ongoing composition.
349       is_composing_ = true;
350     }
351 
352     ::ImmReleaseContext(hwnd_, imc);
353   }
354   return ret;
355 }
356 
DisableIME()357 void OsrImeHandlerWin::DisableIME() {
358   CleanupComposition();
359   ::ImmAssociateContextEx(hwnd_, nullptr, 0);
360 }
361 
CancelIME()362 void OsrImeHandlerWin::CancelIME() {
363   if (is_composing_) {
364     HIMC imc = ::ImmGetContext(hwnd_);
365     if (imc) {
366       ::ImmNotifyIME(imc, NI_COMPOSITIONSTR, CPS_CANCEL, 0);
367       ::ImmReleaseContext(hwnd_, imc);
368     }
369     ResetComposition();
370   }
371 }
372 
EnableIME()373 void OsrImeHandlerWin::EnableIME() {
374   // Load the default IME context.
375   ::ImmAssociateContextEx(hwnd_, nullptr, IACE_DEFAULT);
376 }
377 
UpdateCaretPosition(int index)378 void OsrImeHandlerWin::UpdateCaretPosition(int index) {
379   // Save the caret position.
380   cursor_index_ = index;
381   // Move the IME window.
382   MoveImeWindow();
383 }
384 
ChangeCompositionRange(const CefRange & selection_range,const std::vector<CefRect> & bounds)385 void OsrImeHandlerWin::ChangeCompositionRange(
386     const CefRange& selection_range,
387     const std::vector<CefRect>& bounds) {
388   composition_range_ = selection_range;
389   composition_bounds_ = bounds;
390   MoveImeWindow();
391 }
392 
393 }  // namespace client
394