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