1 // Copyright 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.
5 #include "win8/metro_driver/ime/text_service.h"
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"
16 // Architecture overview of input method support on Ash mode:
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
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.
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
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.
41 // metro_driver process -> browser process
43 // - MetroViewerHostMsg_ImeCompositionChanged
44 // - MetroViewerHostMsg_ImeTextCommitted
47 // -> ChromeAppViewAsh
48 // -- (process boundary) --
49 // -> RemoteWindowTreeHostWin
50 // -> RemoteInputMethodWin
52 // browser process -> metro_driver process
54 // - MetroViewerHostMsg_ImeCancelComposition
55 // - MetroViewerHostMsg_ImeTextInputClientUpdated
57 // RemoteInputMethodWin
58 // -> RemoteWindowTreeHostWin
59 // -- (process boundary) --
60 // -> ChromeAppViewAsh
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
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
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.
88 namespace metro_driver
{
91 // Japanese IME expects the default value of this compartment is
92 // TF_SENTENCEMODE_PHRASEPREDICT to emulate IMM32 behavior. This value is
93 // managed per thread, thus setting this value at once is sufficient. This
94 // value never affects non-Japanese IMEs.
95 bool InitializeSentenceMode(ITfThreadMgr2
* thread_manager
,
96 TfClientId client_id
) {
97 base::win::ScopedComPtr
<ITfCompartmentMgr
> thread_compartment_manager
;
98 HRESULT hr
= thread_compartment_manager
.QueryFrom(thread_manager
);
100 LOG(ERROR
) << "QueryFrom failed. hr = " << hr
;
103 base::win::ScopedComPtr
<ITfCompartment
> sentence_compartment
;
104 hr
= thread_compartment_manager
->GetCompartment(
105 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE
,
106 sentence_compartment
.Receive());
108 LOG(ERROR
) << "ITfCompartment::GetCompartment failed. hr = " << hr
;
112 base::win::ScopedVariant sentence_variant
;
113 sentence_variant
.Set(TF_SENTENCEMODE_PHRASEPREDICT
);
114 hr
= sentence_compartment
->SetValue(client_id
, &sentence_variant
);
116 LOG(ERROR
) << "ITfCompartment::SetValue failed. hr = " << hr
;
122 // Initializes |context| as disabled context where IMEs will be disabled.
123 bool InitializeDisabledContext(ITfContext
* context
, TfClientId client_id
) {
124 base::win::ScopedComPtr
<ITfCompartmentMgr
> compartment_mgr
;
125 HRESULT hr
= compartment_mgr
.QueryFrom(context
);
127 LOG(ERROR
) << "QueryFrom failed. hr = " << hr
;
131 base::win::ScopedComPtr
<ITfCompartment
> disabled_compartment
;
132 hr
= compartment_mgr
->GetCompartment(GUID_COMPARTMENT_KEYBOARD_DISABLED
,
133 disabled_compartment
.Receive());
135 LOG(ERROR
) << "ITfCompartment::GetCompartment failed. hr = " << hr
;
139 base::win::ScopedVariant variant
;
141 hr
= disabled_compartment
->SetValue(client_id
, &variant
);
143 LOG(ERROR
) << "ITfCompartment::SetValue failed. hr = " << hr
;
147 base::win::ScopedComPtr
<ITfCompartment
> empty_context
;
148 hr
= compartment_mgr
->GetCompartment(GUID_COMPARTMENT_EMPTYCONTEXT
,
149 empty_context
.Receive());
151 LOG(ERROR
) << "ITfCompartment::GetCompartment failed. hr = " << hr
;
155 base::win::ScopedVariant empty_context_variant
;
156 empty_context_variant
.Set(static_cast<int32
>(1));
157 hr
= empty_context
->SetValue(client_id
, &empty_context_variant
);
159 LOG(ERROR
) << "ITfCompartment::SetValue failed. hr = " << hr
;
166 bool IsPasswordField(const std::vector
<InputScope
>& input_scopes
) {
167 return std::find(input_scopes
.begin(), input_scopes
.end(), IS_PASSWORD
) !=
171 // A class that manages the lifetime of the event callback registration. When
172 // this object is destroyed, corresponding event callback will be unregistered.
175 EventSink(DWORD cookie
, base::win::ScopedComPtr
<ITfSource
> source
)
179 if (!source_
|| cookie_
!= TF_INVALID_COOKIE
)
181 source_
->UnadviseSink(cookie_
);
182 cookie_
= TF_INVALID_COOKIE
;
188 base::win::ScopedComPtr
<ITfSource
> source_
;
189 DISALLOW_COPY_AND_ASSIGN(EventSink
);
192 scoped_ptr
<EventSink
> CreateTextEditSink(ITfContext
* context
,
193 ITfTextEditSink
* text_store
) {
195 base::win::ScopedComPtr
<ITfSource
> source
;
196 DWORD cookie
= TF_INVALID_EDIT_COOKIE
;
197 HRESULT hr
= source
.QueryFrom(context
);
199 LOG(ERROR
) << "QueryFrom failed, hr = " << hr
;
200 return scoped_ptr
<EventSink
>();
202 hr
= source
->AdviseSink(IID_ITfTextEditSink
, text_store
, &cookie
);
204 LOG(ERROR
) << "AdviseSink failed, hr = " << hr
;
205 return scoped_ptr
<EventSink
>();
207 return scoped_ptr
<EventSink
>(new EventSink(cookie
, source
));
210 // A set of objects that should have the same lifetime. Following things
212 // - TextStore: a COM object that abstracts text buffer. This object is
213 // actually implemented by us in text_store.cc
214 // - ITfDocumentMgr: a focusable unit in TSF. This object is implemented by
215 // TSF runtime and works as a container of TextStore.
216 // - EventSink: an object that ensures that the event callback between
217 // TSF runtime and TextStore is unregistered when this object is destroyed.
218 class DocumentBinding
{
221 if (!document_manager_
)
223 document_manager_
->Pop(TF_POPF_ALL
);
226 static scoped_ptr
<DocumentBinding
> Create(
227 ITfThreadMgr2
* thread_manager
,
228 TfClientId client_id
,
229 const std::vector
<InputScope
>& input_scopes
,
231 TextStoreDelegate
* delegate
) {
232 base::win::ScopedComPtr
<ITfDocumentMgr
> document_manager
;
233 HRESULT hr
= thread_manager
->CreateDocumentMgr(document_manager
.Receive());
235 LOG(ERROR
) << "ITfThreadMgr2::CreateDocumentMgr failed. hr = " << hr
;
236 return scoped_ptr
<DocumentBinding
>();
239 // Note: In our IPC protocol, an empty |input_scopes| is used to indicate
240 // that an IME must be disabled in this context. In such case, we need not
241 // instantiate TextStore.
242 const bool use_null_text_store
= input_scopes
.empty();
244 scoped_refptr
<TextStore
> text_store
;
245 if (!use_null_text_store
) {
246 text_store
= TextStore::Create(window_handle
, input_scopes
, delegate
);
248 LOG(ERROR
) << "Failed to create TextStore.";
249 return scoped_ptr
<DocumentBinding
>();
253 base::win::ScopedComPtr
<ITfContext
> context
;
254 DWORD edit_cookie
= TF_INVALID_EDIT_COOKIE
;
255 hr
= document_manager
->CreateContext(
258 static_cast<ITextStoreACP
*>(text_store
.get()),
262 LOG(ERROR
) << "ITfDocumentMgr::CreateContext failed. hr = " << hr
;
263 return scoped_ptr
<DocumentBinding
>();
266 // If null-TextStore is used or |input_scopes| looks like a password field,
267 // set special properties to tell IMEs to be disabled.
268 if ((use_null_text_store
|| IsPasswordField(input_scopes
)) &&
269 !InitializeDisabledContext(context
, client_id
)) {
270 LOG(ERROR
) << "InitializeDisabledContext failed.";
271 return scoped_ptr
<DocumentBinding
>();
274 scoped_ptr
<EventSink
> text_edit_sink
;
275 if (!use_null_text_store
) {
276 text_edit_sink
= CreateTextEditSink(context
, text_store
);
277 if (!text_edit_sink
) {
278 LOG(ERROR
) << "CreateTextEditSink failed.";
279 return scoped_ptr
<DocumentBinding
>();
282 hr
= document_manager
->Push(context
);
284 LOG(ERROR
) << "ITfDocumentMgr::Push failed. hr = " << hr
;
285 return scoped_ptr
<DocumentBinding
>();
287 return scoped_ptr
<DocumentBinding
>(
288 new DocumentBinding(text_store
,
290 text_edit_sink
.Pass()));
293 ITfDocumentMgr
* document_manager() const {
294 return document_manager_
;
297 scoped_refptr
<TextStore
> text_store() const {
302 DocumentBinding(scoped_refptr
<TextStore
> text_store
,
303 base::win::ScopedComPtr
<ITfDocumentMgr
> document_manager
,
304 scoped_ptr
<EventSink
> text_edit_sink
)
305 : text_store_(text_store
),
306 document_manager_(document_manager
),
307 text_edit_sink_(text_edit_sink
.Pass()) {}
309 scoped_refptr
<TextStore
> text_store_
;
310 base::win::ScopedComPtr
<ITfDocumentMgr
> document_manager_
;
311 scoped_ptr
<EventSink
> text_edit_sink_
;
313 DISALLOW_COPY_AND_ASSIGN(DocumentBinding
);
316 class TextServiceImpl
: public TextService
,
317 public TextStoreDelegate
{
319 TextServiceImpl(ITfThreadMgr2
* thread_manager
,
320 TfClientId client_id
,
322 TextServiceDelegate
* delegate
)
323 : client_id_(client_id
),
324 window_handle_(window_handle
),
326 thread_manager_(thread_manager
) {
327 DCHECK_NE(TF_CLIENTID_NULL
, client_id
);
328 DCHECK(window_handle
!= NULL
);
329 DCHECK(thread_manager_
);
331 virtual ~TextServiceImpl() {
332 thread_manager_
->Deactivate();
336 // TextService overrides:
337 virtual void CancelComposition() OVERRIDE
{
338 if (!current_document_
) {
339 VLOG(0) << "|current_document_| is NULL due to the previous error.";
342 TextStore
* text_store
= current_document_
->text_store();
345 text_store
->CancelComposition();
348 virtual void OnDocumentChanged(
349 const std::vector
<int32
>& input_scopes
,
350 const std::vector
<metro_viewer::CharacterBounds
>& character_bounds
)
352 bool document_type_changed
= input_scopes_
!= input_scopes
;
353 input_scopes_
= input_scopes
;
354 composition_character_bounds_
= character_bounds
;
355 if (document_type_changed
)
356 OnDocumentTypeChanged(input_scopes
);
359 virtual void OnWindowActivated() OVERRIDE
{
360 if (!current_document_
) {
361 VLOG(0) << "|current_document_| is NULL due to the previous error.";
364 ITfDocumentMgr
* document_manager
= current_document_
->document_manager();
365 if (!document_manager
) {
366 VLOG(0) << "|document_manager| is NULL due to the previous error.";
369 HRESULT hr
= thread_manager_
->SetFocus(document_manager
);
371 LOG(ERROR
) << "ITfThreadMgr2::SetFocus failed. hr = " << hr
;
376 virtual void OnCompositionChanged(
377 const base::string16
& text
,
378 int32 selection_start
,
380 const std::vector
<metro_viewer::UnderlineInfo
>& underlines
) OVERRIDE
{
383 delegate_
->OnCompositionChanged(text
,
389 virtual void OnTextCommitted(const base::string16
& text
) OVERRIDE
{
392 delegate_
->OnTextCommitted(text
);
395 virtual RECT
GetCaretBounds() {
396 if (composition_character_bounds_
.empty()) {
397 const RECT rect
= {};
400 const metro_viewer::CharacterBounds
& bounds
=
401 composition_character_bounds_
[0];
402 POINT left_top
= { bounds
.left
, bounds
.top
};
403 POINT right_bottom
= { bounds
.right
, bounds
.bottom
};
404 ClientToScreen(window_handle_
, &left_top
);
405 ClientToScreen(window_handle_
, &right_bottom
);
415 virtual bool GetCompositionCharacterBounds(uint32 index
,
416 RECT
* rect
) OVERRIDE
{
417 if (index
>= composition_character_bounds_
.size()) {
420 const metro_viewer::CharacterBounds
& bounds
=
421 composition_character_bounds_
[index
];
422 POINT left_top
= { bounds
.left
, bounds
.top
};
423 POINT right_bottom
= { bounds
.right
, bounds
.bottom
};
424 ClientToScreen(window_handle_
, &left_top
);
425 ClientToScreen(window_handle_
, &right_bottom
);
426 SetRect(rect
, left_top
.x
, left_top
.y
, right_bottom
.x
, right_bottom
.y
);
430 void OnDocumentTypeChanged(const std::vector
<int32
>& input_scopes
) {
431 std::vector
<InputScope
> native_input_scopes(input_scopes
.size());
432 for (size_t i
= 0; i
< input_scopes
.size(); ++i
)
433 native_input_scopes
[i
] = static_cast<InputScope
>(input_scopes
[i
]);
434 scoped_ptr
<DocumentBinding
> new_document
=
435 DocumentBinding::Create(thread_manager_
.get(),
440 LOG_IF(ERROR
, !new_document
) << "Failed to create a new document.";
441 current_document_
.swap(new_document
);
445 TfClientId client_id_
;
447 TextServiceDelegate
* delegate_
;
448 scoped_ptr
<DocumentBinding
> current_document_
;
449 base::win::ScopedComPtr
<ITfThreadMgr2
> thread_manager_
;
451 // A vector of InputScope enumeration, which represents the document type of
452 // the focused text field. Note that in our IPC message protocol, an empty
453 // |input_scopes_| has special meaning that IMEs must be disabled on this
455 std::vector
<int32
> input_scopes_
;
456 // Character bounds of the composition. When there is no composition but this
457 // vector is not empty, the first element contains the caret bounds.
458 std::vector
<metro_viewer::CharacterBounds
> composition_character_bounds_
;
460 DISALLOW_COPY_AND_ASSIGN(TextServiceImpl
);
465 scoped_ptr
<TextService
>
466 CreateTextService(TextServiceDelegate
* delegate
, HWND window_handle
) {
468 return scoped_ptr
<TextService
>();
469 base::win::ScopedComPtr
<ITfThreadMgr2
> thread_manager
;
470 HRESULT hr
= thread_manager
.CreateInstance(CLSID_TF_ThreadMgr
);
472 LOG(ERROR
) << "Failed to create instance of CLSID_TF_ThreadMgr. hr = "
474 return scoped_ptr
<TextService
>();
476 TfClientId client_id
= TF_CLIENTID_NULL
;
477 hr
= thread_manager
->ActivateEx(&client_id
, 0);
479 LOG(ERROR
) << "ITfThreadMgr2::ActivateEx failed. hr = " << hr
;
480 return scoped_ptr
<TextService
>();
482 if (!InitializeSentenceMode(thread_manager
, client_id
)) {
483 LOG(ERROR
) << "InitializeSentenceMode failed.";
484 thread_manager
->Deactivate();
485 return scoped_ptr
<TextService
>();
487 return scoped_ptr
<TextService
>(new TextServiceImpl(thread_manager
,
493 } // namespace metro_driver