OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "win8/metro_driver/ime/text_service.h" | |
6 | |
7 #include <msctf.h> | |
8 | |
9 #include "base/logging.h" | |
10 #include "base/win/scoped_variant.h" | |
11 #include "ui/metro_viewer/ime_types.h" | |
12 #include "win8/metro_driver/ime/text_service_delegate.h" | |
13 #include "win8/metro_driver/ime/text_store.h" | |
14 #include "win8/metro_driver/ime/text_store_delegate.h" | |
15 | |
16 // Architecture overview of input method support on Ash mode: | |
17 // | |
18 // Overview: | |
19 // On Ash mode, the system keyboard focus is owned by the metro_driver process | |
20 // while most of event handling are still implemented in the browser process. | |
21 // Thus the metro_driver basically works as a proxy that simply forwards | |
22 // keyevents to the metro_driver process. IME support must be involved somewhere | |
23 // in this flow. | |
24 // | |
25 // In short, we need to interact with an IME in the metro_driver process since | |
26 // TSF (Text Services Framework) runtime wants to processes keyevents while | |
27 // (and only while) the attached UI thread owns keyboard focus. | |
28 // | |
29 // Due to this limitation, we need to split IME handling into two parts, one | |
30 // is in the metro_driver process and the other is in the browser process. | |
31 // The metro_driver process is responsible for implementing the primary data | |
32 // store for the composition text and wiring it up with an IME via TSF APIs. | |
33 // On the other hand, the browser process is responsible for calculating | |
34 // character position in the composition text whenever the composition text | |
35 // is updated. | |
36 // | |
37 // IPC overview: | |
38 // Fortunately, we don't need so many IPC messages to support IMEs. In fact, | |
39 // only 4 messages are required to enable basic IME functionality. | |
40 // | |
41 // metro_driver process -> browser process | |
42 // Message Type: | |
43 // - MetroViewerHostMsg_ImeCompositionChanged | |
44 // - MetroViewerHostMsg_ImeTextCommitted | |
45 // Message Routing: | |
46 // TextServiceImpl | |
47 // -> ChromeAppViewAsh | |
48 // -- (process boundary) -- | |
49 // -> RemoteRootWindowHostWin | |
50 // -> RemoteInputMethodWin | |
51 // | |
52 // browser process -> metro_driver process | |
53 // Message Type: | |
54 // - MetroViewerHostMsg_ImeCancelComposition | |
55 // - MetroViewerHostMsg_ImeTextInputClientUpdated | |
56 // Message Routing: | |
57 // RemoteInputMethodWin | |
58 // -> RemoteRootWindowHostWin | |
59 // -- (process boundary) -- | |
60 // -> ChromeAppViewAsh | |
61 // -> TextServiceImpl | |
62 // | |
63 // Note that a keyevent may be forwarded through a different path. When a | |
64 // keyevent is not handled by an IME, such keyevent and subsequent character | |
65 // events will be sent from the metro_driver process to the browser process as | |
66 // following IPC messages. | |
67 // - MetroViewerHostMsg_KeyDown | |
68 // - MetroViewerHostMsg_KeyUp | |
69 // - MetroViewerHostMsg_Character | |
70 // | |
71 // How TextServiceImpl works: | |
72 // Here is the list of the major tasks that are handled in TextServiceImpl. | |
73 // - Manages a session object obtained from TSF runtime. We need them to call | |
74 // most of TSF APIs. | |
75 // - Handles OnDocumentChanged event. Whenever the document type is changed, | |
76 // TextServiceImpl destroyes the current document and initializes new one | |
77 // according to the given |input_scopes|. | |
78 // - Stores the |composition_character_bounds_| passed from OnDocumentChanged | |
79 // event so that an IME or on-screen keyboard can query the character | |
80 // position synchronously. | |
81 // The most complicated part is the OnDocumentChanged handler. Since some IMEs | |
82 // such as Japanese IMEs drastically change their behavior depending on | |
83 // properties exposed from the virtual document, we need to set up a lot | |
84 // properties carefully and correctly. See DocumentBinding class in this file | |
85 // about what will be involved in this multi-phase construction. See also | |
86 // text_store.cc and input_scope.cc for more underlying details. | |
87 | |
88 namespace metro_driver { | |
89 namespace { | |
90 typedef TSFTextStore TextStore; | |
91 | |
92 // Japanese IME expects the default value of this compartment is | |
93 // TF_SENTENCEMODE_PHRASEPREDICT to emulate IMM32 behavior. This value is | |
94 // managed per thread, thus setting this value at once is sufficient. This | |
95 // value never affects non-Japanese IMEs. | |
96 bool InitializeSentenceMode(ITfThreadMgr2* thread_manager, | |
97 TfClientId client_id) { | |
98 base::win::ScopedComPtr<ITfCompartmentMgr> thread_compartment_manager; | |
99 if (FAILED(thread_compartment_manager.QueryFrom(thread_manager))) | |
100 return false; | |
101 base::win::ScopedComPtr<ITfCompartment> sentence_compartment; | |
102 if (FAILED(thread_compartment_manager->GetCompartment( | |
103 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE, | |
104 sentence_compartment.Receive()))) { | |
105 return false; | |
106 } | |
107 | |
108 base::win::ScopedVariant sentence_variant; | |
109 sentence_variant.Set(TF_SENTENCEMODE_PHRASEPREDICT); | |
110 if (FAILED(sentence_compartment->SetValue(client_id, &sentence_variant))) | |
111 return false; | |
112 return true; | |
113 } | |
114 | |
115 // Initializes |context| as disabled context where IMEs will be disabled. | |
116 bool InitializeDisabledContext(ITfContext* context, TfClientId client_id) { | |
117 base::win::ScopedComPtr<ITfCompartmentMgr> compartment_mgr; | |
118 if (FAILED(compartment_mgr.QueryFrom(context))) | |
119 return false; | |
120 | |
121 base::win::ScopedComPtr<ITfCompartment> disabled_compartment; | |
122 if (FAILED(compartment_mgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_DISABLED, | |
123 disabled_compartment.Receive()))) { | |
124 return false; | |
125 } | |
126 | |
127 base::win::ScopedVariant variant; | |
128 variant.Set(1); | |
129 if (FAILED(disabled_compartment->SetValue(client_id, &variant))) | |
130 return false; | |
131 | |
132 base::win::ScopedComPtr<ITfCompartment> empty_context; | |
133 if (FAILED(compartment_mgr->GetCompartment(GUID_COMPARTMENT_EMPTYCONTEXT, | |
134 empty_context.Receive()))) { | |
135 return false; | |
136 } | |
137 | |
138 base::win::ScopedVariant empty_context_variant; | |
139 empty_context_variant.Set(static_cast<int32>(1)); | |
140 if (FAILED(empty_context->SetValue(client_id, &empty_context_variant))) | |
141 return false; | |
142 | |
143 return true; | |
144 } | |
145 | |
146 bool IsPasswordField(const std::vector<InputScope>& input_scopes) { | |
147 return std::find(input_scopes.begin(), input_scopes.end(), IS_PASSWORD) != | |
148 input_scopes.end(); | |
149 } | |
150 | |
151 // A class that manages the lifetime of the event callback registration. When | |
152 // this object is destroyed, corresponding event callback will be unregistered. | |
153 class EventSink { | |
154 public: | |
155 EventSink(DWORD cookie, base::win::ScopedComPtr<ITfSource> source) | |
156 : cookie_(cookie), | |
157 source_(source) {} | |
158 ~EventSink() { | |
159 if (!source_ || cookie_ != TF_INVALID_COOKIE) | |
160 return; | |
161 source_->UnadviseSink(cookie_); | |
162 cookie_ = TF_INVALID_COOKIE; | |
163 source_.Release(); | |
164 } | |
165 | |
166 private: | |
167 DWORD cookie_; | |
168 base::win::ScopedComPtr<ITfSource> source_; | |
169 DISALLOW_COPY_AND_ASSIGN(EventSink); | |
170 }; | |
171 | |
172 scoped_ptr<EventSink> CreateTextEditSink(ITfContext* context, | |
173 ITfTextEditSink* text_store) { | |
174 if (!text_store) | |
ananta
2013/12/02 23:15:23
This cannot be NULL if I am reading the code right
yukawa
2013/12/03 02:34:30
Done.
| |
175 return scoped_ptr<EventSink>(); | |
176 base::win::ScopedComPtr<ITfSource> source; | |
177 DWORD cookie = TF_INVALID_EDIT_COOKIE; | |
178 if (FAILED(source.QueryFrom(context))) | |
ananta
2013/12/02 23:15:23
The failures here seem unexpected. Please add trac
yukawa
2013/12/03 02:34:30
Done.
| |
179 return scoped_ptr<EventSink>(); | |
180 if (FAILED(source->AdviseSink(IID_ITfTextEditSink, text_store, &cookie))) | |
181 return scoped_ptr<EventSink>(); | |
ananta
2013/12/02 23:15:23
Ditto
yukawa
2013/12/03 02:34:30
Done.
| |
182 return scoped_ptr<EventSink>(new EventSink(cookie, source)); | |
183 } | |
184 | |
185 // A set of objects that should have the same lifetime. Following things | |
186 // are maintained. | |
187 // - TextStore: a COM object that abstracts text buffer. This object is | |
188 // actually implemented by us in text_store.cc | |
189 // - ITfDocumentMgr: a focusable unit in TSF. This object is implemented by | |
190 // TSF runtime and works as a container of TextStore. | |
191 // - EventSink: an object that ensures that the event callback between | |
192 // TSF runtime and TextStore is unregistered when this object is destroyed. | |
193 class DocumentBinding { | |
194 public: | |
195 ~DocumentBinding() { | |
196 if (!document_manager_) | |
197 return; | |
198 document_manager_->Pop(TF_POPF_ALL); | |
199 } | |
200 | |
201 static scoped_ptr<DocumentBinding> Create( | |
202 ITfThreadMgr2* thread_manager, | |
203 TfClientId client_id, | |
204 const std::vector<InputScope>& input_scopes, | |
205 HWND window_handle, | |
206 TextStoreDelegate* delegate) { | |
207 base::win::ScopedComPtr<ITfDocumentMgr> document_manager; | |
208 if (FAILED(thread_manager->CreateDocumentMgr(document_manager.Receive()))) { | |
ananta
2013/12/02 23:15:23
It would be great if we can trace the COM error co
yukawa
2013/12/03 02:34:30
Agree. It's worth doing. Done.
| |
209 LOG(ERROR) << "ITfThreadMgr2::CreateDocumentMgr failed."; | |
210 return scoped_ptr<DocumentBinding>(); | |
211 } | |
212 | |
213 // Note: In our IPC protocol, an empty |input_scopes| is used to indicate | |
214 // that an IME must be disabled in this context. In such case, we need not | |
215 // instantiate TextStore. | |
216 const bool use_null_text_store = input_scopes.empty(); | |
217 | |
218 scoped_refptr<TextStore> text_store; | |
219 if (!use_null_text_store) { | |
220 text_store = TextStore::Create(window_handle, input_scopes, delegate); | |
221 if (!text_store) { | |
222 LOG(ERROR) << "Failed to create TextStore."; | |
223 return scoped_ptr<DocumentBinding>(); | |
224 } | |
225 } | |
226 | |
227 base::win::ScopedComPtr<ITfContext> context; | |
228 DWORD edit_cookie = TF_INVALID_EDIT_COOKIE; | |
229 if (FAILED(document_manager->CreateContext( | |
230 client_id, | |
231 0, | |
232 static_cast<ITextStoreACP*>(text_store.get()), | |
233 context.Receive(), | |
234 &edit_cookie))) { | |
235 LOG(ERROR) << "ITfDocumentMgr::CreateContext failed."; | |
236 return scoped_ptr<DocumentBinding>(); | |
237 } | |
238 | |
239 // If null-TextStore is used or |input_scopes| looks like a password field, | |
240 // set special properties to tell IMEs to be disabled. | |
241 if ((use_null_text_store || IsPasswordField(input_scopes)) && | |
242 !InitializeDisabledContext(context, client_id)) { | |
243 LOG(ERROR) << "InitializeDisabledContext failed."; | |
244 return scoped_ptr<DocumentBinding>(); | |
245 } | |
246 | |
247 scoped_ptr<EventSink> text_edit_sink; | |
248 if (!use_null_text_store) { | |
249 text_edit_sink = CreateTextEditSink(context, text_store); | |
250 if (!text_edit_sink) { | |
251 LOG(ERROR) << "CreateTextEditSink failed."; | |
252 return scoped_ptr<DocumentBinding>(); | |
253 } | |
254 } | |
255 if (FAILED(document_manager->Push(context))) { | |
256 LOG(ERROR) << "ITfDocumentMgr::Push failed."; | |
257 return scoped_ptr<DocumentBinding>(); | |
258 } | |
259 return scoped_ptr<DocumentBinding>( | |
260 new DocumentBinding(text_store, | |
261 document_manager, | |
262 text_edit_sink.Pass())); | |
263 } | |
264 | |
265 ITfDocumentMgr* document_manager() const { | |
266 return document_manager_; | |
267 } | |
268 | |
269 scoped_refptr<TextStore> text_store() const { | |
270 return text_store_; | |
271 } | |
272 | |
273 private: | |
274 DocumentBinding(scoped_refptr<TextStore> text_store, | |
275 base::win::ScopedComPtr<ITfDocumentMgr> document_manager, | |
276 scoped_ptr<EventSink> text_edit_sink) | |
277 : text_store_(text_store), | |
278 document_manager_(document_manager), | |
279 text_edit_sink_(text_edit_sink.Pass()) {} | |
280 | |
281 scoped_refptr<TextStore> text_store_; | |
282 base::win::ScopedComPtr<ITfDocumentMgr> document_manager_; | |
283 scoped_ptr<EventSink> text_edit_sink_; | |
284 | |
285 DISALLOW_COPY_AND_ASSIGN(DocumentBinding); | |
286 }; | |
287 | |
288 class TextServiceImpl : public TextService, | |
289 public TextStoreDelegate { | |
290 public: | |
291 TextServiceImpl(ITfThreadMgr2* thread_manager, | |
292 TfClientId client_id, | |
293 HWND window_handle, | |
294 TextServiceDelegate* delegate) | |
295 : client_id_(client_id), | |
296 window_handle_(window_handle), | |
297 delegate_(delegate), | |
298 thread_manager_(thread_manager) { | |
299 DCHECK_NE(TF_CLIENTID_NULL, client_id); | |
300 DCHECK(window_handle != NULL); | |
301 DCHECK(thread_manager_); | |
302 } | |
303 virtual ~TextServiceImpl() { | |
304 thread_manager_->Deactivate(); | |
305 } | |
306 | |
307 private: | |
308 // TextService overrides: | |
309 virtual void TextService::CancelComposition() OVERRIDE { | |
310 if (!current_document_) { | |
311 VLOG(0) << "|current_document_| is NULL due to the previous error."; | |
312 return; | |
313 } | |
314 TextStore* text_store = current_document_->text_store(); | |
315 if (!text_store) | |
316 return; | |
317 text_store->CancelComposition(); | |
318 } | |
319 | |
320 virtual void OnDocumentChanged( | |
321 const std::vector<int32>& input_scopes, | |
322 const std::vector<metro_viewer::CharacterBounds>& character_bounds) | |
323 OVERRIDE { | |
324 bool document_type_changed = input_scopes_ != input_scopes; | |
325 input_scopes_ = input_scopes; | |
326 composition_character_bounds_ = character_bounds; | |
327 if (document_type_changed) | |
328 OnDocumentTypeChanged(input_scopes); | |
329 } | |
330 | |
331 virtual void OnWindowActivated() OVERRIDE { | |
332 if (!current_document_) { | |
333 VLOG(0) << "|current_document_| is NULL due to the previous error."; | |
334 return; | |
335 } | |
336 ITfDocumentMgr* document_manager = current_document_->document_manager(); | |
337 if (!document_manager) { | |
338 VLOG(0) << "|document_manager| is NULL due to the previous error."; | |
339 return; | |
340 } | |
341 if (FAILED(thread_manager_->SetFocus(document_manager))) { | |
342 LOG(ERROR) << "ITfThreadMgr2::SetFocus failed."; | |
343 return; | |
344 } | |
345 } | |
346 | |
347 virtual void OnCompositionChanged( | |
348 const string16& text, | |
349 int32 selection_start, | |
350 int32 selection_end, | |
351 const std::vector<metro_viewer::UnderlineInfo>& underlines) OVERRIDE { | |
352 if (!delegate_) | |
353 return; | |
354 delegate_->OnCompositionChanged(text, | |
355 selection_start, | |
356 selection_end, | |
357 underlines); | |
358 } | |
359 | |
360 virtual void OnTextCommitted(const string16& text) OVERRIDE { | |
361 if (!delegate_) | |
362 return; | |
363 delegate_->OnTextCommitted(text); | |
364 } | |
365 | |
366 virtual RECT GetCaretBounds() { | |
367 if (composition_character_bounds_.empty()) { | |
368 const RECT rect = {}; | |
369 return rect; | |
370 } | |
371 const metro_viewer::CharacterBounds& bounds = | |
372 composition_character_bounds_[0]; | |
373 POINT left_top = { bounds.left, bounds.top }; | |
374 POINT right_bottom = { bounds.right, bounds.bottom }; | |
375 ClientToScreen(window_handle_, &left_top); | |
376 ClientToScreen(window_handle_, &right_bottom); | |
377 const RECT rect = { | |
378 left_top.x, | |
379 left_top.y, | |
380 right_bottom.x, | |
381 right_bottom.y, | |
382 }; | |
383 return rect; | |
384 } | |
385 | |
386 virtual bool GetCompositionCharacterBounds(uint32 index, | |
387 RECT* rect) OVERRIDE { | |
388 if (index >= composition_character_bounds_.size()) { | |
389 return false; | |
390 } | |
391 const metro_viewer::CharacterBounds& bounds = | |
392 composition_character_bounds_[index]; | |
393 POINT left_top = { bounds.left, bounds.top }; | |
394 POINT right_bottom = { bounds.right, bounds.bottom }; | |
395 ClientToScreen(window_handle_, &left_top); | |
396 ClientToScreen(window_handle_, &right_bottom); | |
397 SetRect(rect, left_top.x, left_top.y, right_bottom.x, right_bottom.y); | |
398 return true; | |
399 } | |
400 | |
401 void OnDocumentTypeChanged(const std::vector<int32>& input_scopes) { | |
402 std::vector<InputScope> native_input_scopes(input_scopes.size()); | |
403 for (size_t i = 0; i < input_scopes.size(); ++i) | |
404 native_input_scopes[i] = static_cast<InputScope>(input_scopes[i]); | |
405 scoped_ptr<DocumentBinding> new_document = | |
406 DocumentBinding::Create(thread_manager_.get(), | |
407 client_id_, | |
408 native_input_scopes, | |
409 window_handle_, | |
410 this); | |
411 LOG_IF(ERROR, !new_document) << "Failed to create a new document."; | |
412 current_document_.swap(new_document); | |
413 OnWindowActivated(); | |
414 } | |
415 | |
416 TfClientId client_id_; | |
417 HWND window_handle_; | |
418 TextServiceDelegate* delegate_; | |
419 scoped_ptr<DocumentBinding> current_document_; | |
420 base::win::ScopedComPtr<ITfThreadMgr2> thread_manager_; | |
421 | |
422 // A vector of InputScope enumeration, which represents the document type of | |
423 // the focused text field. Note that in our IPC message protocol, an empty | |
424 // |input_scopes_| has special meaning that IMEs must be disabled on this | |
425 // document. | |
426 std::vector<int32> input_scopes_; | |
427 // Character bounds of the composition. When there is no composition but this | |
428 // vector is not empty, the first element contains the caret bounds. | |
429 std::vector<metro_viewer::CharacterBounds> composition_character_bounds_; | |
430 | |
431 DISALLOW_COPY_AND_ASSIGN(TextServiceImpl); | |
432 }; | |
433 | |
434 } // namespace | |
435 | |
436 scoped_ptr<TextService> | |
437 CreateTextService(TextServiceDelegate* delegate, HWND window_handle) { | |
438 if (!delegate) | |
439 return scoped_ptr<TextService>(); | |
440 base::win::ScopedComPtr<ITfThreadMgr2> thread_manager; | |
441 if (FAILED(thread_manager.CreateInstance(CLSID_TF_ThreadMgr))) { | |
442 LOG(ERROR) << "Failed to create instance of CLSID_TF_ThreadMgr."; | |
443 return scoped_ptr<TextService>(); | |
444 } | |
445 TfClientId client_id = TF_CLIENTID_NULL; | |
446 if (FAILED(thread_manager->ActivateEx(&client_id, 0))) { | |
447 LOG(ERROR) << "ITfThreadMgr2::ActivateEx failed."; | |
448 return scoped_ptr<TextService>(); | |
449 } | |
450 if (!InitializeSentenceMode(thread_manager, client_id)) { | |
451 LOG(ERROR) << "InitializeSentenceMode failed."; | |
452 thread_manager->Deactivate(); | |
453 return scoped_ptr<TextService>(); | |
454 } | |
455 return scoped_ptr<TextService>(new TextServiceImpl(thread_manager, | |
456 client_id, | |
457 window_handle, | |
458 delegate)); | |
459 } | |
460 | |
461 } // namespace metro_driver | |
OLD | NEW |