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(ITfThreadMgr
* 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
.ptr());
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
.ptr());
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
.ptr());
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_
.get() || 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_
.get())
223 document_manager_
->Pop(TF_POPF_ALL
);
226 static scoped_ptr
<DocumentBinding
> Create(
227 ITfThreadMgr
* 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
) << "ITfThreadMgr::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
);
247 if (!text_store
.get()) {
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
.get(), 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
.get(), text_store
.get());
277 if (!text_edit_sink
) {
278 LOG(ERROR
) << "CreateTextEditSink failed.";
279 return scoped_ptr
<DocumentBinding
>();
282 hr
= document_manager
->Push(context
.get());
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 { return document_manager_
.get(); }
295 scoped_refptr
<TextStore
> text_store() const {
300 DocumentBinding(scoped_refptr
<TextStore
> text_store
,
301 base::win::ScopedComPtr
<ITfDocumentMgr
> document_manager
,
302 scoped_ptr
<EventSink
> text_edit_sink
)
303 : text_store_(text_store
),
304 document_manager_(document_manager
),
305 text_edit_sink_(text_edit_sink
.Pass()) {}
307 scoped_refptr
<TextStore
> text_store_
;
308 base::win::ScopedComPtr
<ITfDocumentMgr
> document_manager_
;
309 scoped_ptr
<EventSink
> text_edit_sink_
;
311 DISALLOW_COPY_AND_ASSIGN(DocumentBinding
);
314 class TextServiceImpl
: public TextService
,
315 public TextStoreDelegate
{
317 TextServiceImpl(ITfThreadMgr
* thread_manager
,
318 TfClientId client_id
,
320 TextServiceDelegate
* delegate
)
321 : client_id_(client_id
),
322 window_handle_(window_handle
),
324 thread_manager_(thread_manager
) {
325 DCHECK_NE(TF_CLIENTID_NULL
, client_id
);
326 DCHECK(window_handle
!= NULL
);
327 DCHECK(thread_manager_
.get());
329 ~TextServiceImpl() override
{
330 thread_manager_
->Deactivate();
334 // TextService overrides:
335 void CancelComposition() override
{
336 if (!current_document_
) {
337 VLOG(0) << "|current_document_| is NULL due to the previous error.";
340 scoped_refptr
<TextStore
> text_store
= current_document_
->text_store();
341 if (!text_store
.get())
343 text_store
->CancelComposition();
346 void OnDocumentChanged(
347 const std::vector
<int32
>& input_scopes
,
348 const std::vector
<metro_viewer::CharacterBounds
>& character_bounds
)
350 bool document_type_changed
= input_scopes_
!= input_scopes
;
351 input_scopes_
= input_scopes
;
352 composition_character_bounds_
= character_bounds
;
353 if (document_type_changed
)
354 OnDocumentTypeChanged(input_scopes
);
357 void OnWindowActivated() override
{
358 if (!current_document_
) {
359 VLOG(0) << "|current_document_| is NULL due to the previous error.";
362 ITfDocumentMgr
* document_manager
= current_document_
->document_manager();
363 if (!document_manager
) {
364 VLOG(0) << "|document_manager| is NULL due to the previous error.";
367 HRESULT hr
= thread_manager_
->SetFocus(document_manager
);
369 LOG(ERROR
) << "ITfThreadMgr::SetFocus failed. hr = " << hr
;
374 void OnCompositionChanged(
375 const base::string16
& text
,
376 int32 selection_start
,
378 const std::vector
<metro_viewer::UnderlineInfo
>& underlines
) override
{
381 delegate_
->OnCompositionChanged(text
,
387 void OnTextCommitted(const base::string16
& text
) override
{
390 delegate_
->OnTextCommitted(text
);
393 RECT
GetCaretBounds() override
{
394 if (composition_character_bounds_
.empty()) {
395 const RECT rect
= {};
398 const metro_viewer::CharacterBounds
& bounds
=
399 composition_character_bounds_
[0];
400 POINT left_top
= { bounds
.left
, bounds
.top
};
401 POINT right_bottom
= { bounds
.right
, bounds
.bottom
};
402 ClientToScreen(window_handle_
, &left_top
);
403 ClientToScreen(window_handle_
, &right_bottom
);
413 bool GetCompositionCharacterBounds(uint32 index
, RECT
* rect
) override
{
414 if (index
>= composition_character_bounds_
.size()) {
417 const metro_viewer::CharacterBounds
& bounds
=
418 composition_character_bounds_
[index
];
419 POINT left_top
= { bounds
.left
, bounds
.top
};
420 POINT right_bottom
= { bounds
.right
, bounds
.bottom
};
421 ClientToScreen(window_handle_
, &left_top
);
422 ClientToScreen(window_handle_
, &right_bottom
);
423 SetRect(rect
, left_top
.x
, left_top
.y
, right_bottom
.x
, right_bottom
.y
);
427 void OnDocumentTypeChanged(const std::vector
<int32
>& input_scopes
) {
428 std::vector
<InputScope
> native_input_scopes(input_scopes
.size());
429 for (size_t i
= 0; i
< input_scopes
.size(); ++i
)
430 native_input_scopes
[i
] = static_cast<InputScope
>(input_scopes
[i
]);
431 scoped_ptr
<DocumentBinding
> new_document
=
432 DocumentBinding::Create(thread_manager_
.get(),
437 LOG_IF(ERROR
, !new_document
) << "Failed to create a new document.";
438 current_document_
.swap(new_document
);
442 TfClientId client_id_
;
444 TextServiceDelegate
* delegate_
;
445 scoped_ptr
<DocumentBinding
> current_document_
;
446 base::win::ScopedComPtr
<ITfThreadMgr
> thread_manager_
;
448 // A vector of InputScope enumeration, which represents the document type of
449 // the focused text field. Note that in our IPC message protocol, an empty
450 // |input_scopes_| has special meaning that IMEs must be disabled on this
452 std::vector
<int32
> input_scopes_
;
453 // Character bounds of the composition. When there is no composition but this
454 // vector is not empty, the first element contains the caret bounds.
455 std::vector
<metro_viewer::CharacterBounds
> composition_character_bounds_
;
457 DISALLOW_COPY_AND_ASSIGN(TextServiceImpl
);
462 scoped_ptr
<TextService
>
463 CreateTextService(TextServiceDelegate
* delegate
, HWND window_handle
) {
465 return scoped_ptr
<TextService
>();
466 base::win::ScopedComPtr
<ITfThreadMgr
> thread_manager
;
467 HRESULT hr
= thread_manager
.CreateInstance(CLSID_TF_ThreadMgr
);
469 LOG(ERROR
) << "Failed to create instance of CLSID_TF_ThreadMgr. hr = "
471 return scoped_ptr
<TextService
>();
473 TfClientId client_id
= TF_CLIENTID_NULL
;
474 hr
= thread_manager
->Activate(&client_id
);
476 LOG(ERROR
) << "ITfThreadMgr::Activate failed. hr = " << hr
;
477 return scoped_ptr
<TextService
>();
479 if (!InitializeSentenceMode(thread_manager
.get(), client_id
)) {
480 LOG(ERROR
) << "InitializeSentenceMode failed.";
481 thread_manager
->Deactivate();
482 return scoped_ptr
<TextService
>();
484 return scoped_ptr
<TextService
>(new TextServiceImpl(
485 thread_manager
.get(), client_id
, window_handle
, delegate
));
488 } // namespace metro_driver