Don't add an aura tooltip to bubble close buttons on Windows.
[chromium-blink-merge.git] / win8 / metro_driver / ime / text_service.cc
blob4601f27b381316fc8e4233755bcbee6bf7ae6a78
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"
7 #include <msctf.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:
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.
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
35 // is updated.
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.
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 // -> RemoteWindowTreeHostWin
50 // -> RemoteInputMethodWin
52 // browser process -> metro_driver process
53 // Message Type:
54 // - MetroViewerHostMsg_ImeCancelComposition
55 // - MetroViewerHostMsg_ImeTextInputClientUpdated
56 // Message Routing:
57 // RemoteInputMethodWin
58 // -> RemoteWindowTreeHostWin
59 // -- (process boundary) --
60 // -> ChromeAppViewAsh
61 // -> TextServiceImpl
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
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.
88 namespace metro_driver {
89 namespace {
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);
99 if (FAILED(hr)) {
100 LOG(ERROR) << "QueryFrom failed. hr = " << hr;
101 return false;
103 base::win::ScopedComPtr<ITfCompartment> sentence_compartment;
104 hr = thread_compartment_manager->GetCompartment(
105 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE,
106 sentence_compartment.Receive());
107 if (FAILED(hr)) {
108 LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
109 return false;
112 base::win::ScopedVariant sentence_variant;
113 sentence_variant.Set(TF_SENTENCEMODE_PHRASEPREDICT);
114 hr = sentence_compartment->SetValue(client_id, sentence_variant.ptr());
115 if (FAILED(hr)) {
116 LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
117 return false;
119 return true;
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);
126 if (FAILED(hr)) {
127 LOG(ERROR) << "QueryFrom failed. hr = " << hr;
128 return false;
131 base::win::ScopedComPtr<ITfCompartment> disabled_compartment;
132 hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_DISABLED,
133 disabled_compartment.Receive());
134 if (FAILED(hr)) {
135 LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
136 return false;
139 base::win::ScopedVariant variant;
140 variant.Set(1);
141 hr = disabled_compartment->SetValue(client_id, variant.ptr());
142 if (FAILED(hr)) {
143 LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
144 return false;
147 base::win::ScopedComPtr<ITfCompartment> empty_context;
148 hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_EMPTYCONTEXT,
149 empty_context.Receive());
150 if (FAILED(hr)) {
151 LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
152 return false;
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());
158 if (FAILED(hr)) {
159 LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
160 return false;
163 return true;
166 bool IsPasswordField(const std::vector<InputScope>& input_scopes) {
167 return std::find(input_scopes.begin(), input_scopes.end(), IS_PASSWORD) !=
168 input_scopes.end();
171 // A class that manages the lifetime of the event callback registration. When
172 // this object is destroyed, corresponding event callback will be unregistered.
173 class EventSink {
174 public:
175 EventSink(DWORD cookie, base::win::ScopedComPtr<ITfSource> source)
176 : cookie_(cookie),
177 source_(source) {}
178 ~EventSink() {
179 if (!source_.get() || cookie_ != TF_INVALID_COOKIE)
180 return;
181 source_->UnadviseSink(cookie_);
182 cookie_ = TF_INVALID_COOKIE;
183 source_.Release();
186 private:
187 DWORD cookie_;
188 base::win::ScopedComPtr<ITfSource> source_;
189 DISALLOW_COPY_AND_ASSIGN(EventSink);
192 scoped_ptr<EventSink> CreateTextEditSink(ITfContext* context,
193 ITfTextEditSink* text_store) {
194 DCHECK(text_store);
195 base::win::ScopedComPtr<ITfSource> source;
196 DWORD cookie = TF_INVALID_EDIT_COOKIE;
197 HRESULT hr = source.QueryFrom(context);
198 if (FAILED(hr)) {
199 LOG(ERROR) << "QueryFrom failed, hr = " << hr;
200 return scoped_ptr<EventSink>();
202 hr = source->AdviseSink(IID_ITfTextEditSink, text_store, &cookie);
203 if (FAILED(hr)) {
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
211 // are maintained.
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 {
219 public:
220 ~DocumentBinding() {
221 if (!document_manager_.get())
222 return;
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,
230 HWND window_handle,
231 TextStoreDelegate* delegate) {
232 base::win::ScopedComPtr<ITfDocumentMgr> document_manager;
233 HRESULT hr = thread_manager->CreateDocumentMgr(document_manager.Receive());
234 if (FAILED(hr)) {
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(
256 client_id,
258 static_cast<ITextStoreACP*>(text_store.get()),
259 context.Receive(),
260 &edit_cookie);
261 if (FAILED(hr)) {
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());
283 if (FAILED(hr)) {
284 LOG(ERROR) << "ITfDocumentMgr::Push failed. hr = " << hr;
285 return scoped_ptr<DocumentBinding>();
287 return scoped_ptr<DocumentBinding>(
288 new DocumentBinding(text_store,
289 document_manager,
290 text_edit_sink.Pass()));
293 ITfDocumentMgr* document_manager() const { return document_manager_.get(); }
295 scoped_refptr<TextStore> text_store() const {
296 return text_store_;
299 private:
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 {
316 public:
317 TextServiceImpl(ITfThreadMgr* thread_manager,
318 TfClientId client_id,
319 HWND window_handle,
320 TextServiceDelegate* delegate)
321 : client_id_(client_id),
322 window_handle_(window_handle),
323 delegate_(delegate),
324 thread_manager_(thread_manager) {
325 DCHECK_NE(TF_CLIENTID_NULL, client_id);
326 DCHECK(window_handle != NULL);
327 DCHECK(thread_manager_.get());
329 virtual ~TextServiceImpl() {
330 thread_manager_->Deactivate();
333 private:
334 // TextService overrides:
335 virtual void CancelComposition() override {
336 if (!current_document_) {
337 VLOG(0) << "|current_document_| is NULL due to the previous error.";
338 return;
340 scoped_refptr<TextStore> text_store = current_document_->text_store();
341 if (!text_store.get())
342 return;
343 text_store->CancelComposition();
346 virtual void OnDocumentChanged(
347 const std::vector<int32>& input_scopes,
348 const std::vector<metro_viewer::CharacterBounds>& character_bounds)
349 override {
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 virtual void OnWindowActivated() override {
358 if (!current_document_) {
359 VLOG(0) << "|current_document_| is NULL due to the previous error.";
360 return;
362 ITfDocumentMgr* document_manager = current_document_->document_manager();
363 if (!document_manager) {
364 VLOG(0) << "|document_manager| is NULL due to the previous error.";
365 return;
367 HRESULT hr = thread_manager_->SetFocus(document_manager);
368 if (FAILED(hr)) {
369 LOG(ERROR) << "ITfThreadMgr::SetFocus failed. hr = " << hr;
370 return;
374 virtual void OnCompositionChanged(
375 const base::string16& text,
376 int32 selection_start,
377 int32 selection_end,
378 const std::vector<metro_viewer::UnderlineInfo>& underlines) override {
379 if (!delegate_)
380 return;
381 delegate_->OnCompositionChanged(text,
382 selection_start,
383 selection_end,
384 underlines);
387 virtual void OnTextCommitted(const base::string16& text) override {
388 if (!delegate_)
389 return;
390 delegate_->OnTextCommitted(text);
393 virtual RECT GetCaretBounds() override {
394 if (composition_character_bounds_.empty()) {
395 const RECT rect = {};
396 return 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);
404 const RECT rect = {
405 left_top.x,
406 left_top.y,
407 right_bottom.x,
408 right_bottom.y,
410 return rect;
413 virtual bool GetCompositionCharacterBounds(uint32 index,
414 RECT* rect) override {
415 if (index >= composition_character_bounds_.size()) {
416 return false;
418 const metro_viewer::CharacterBounds& bounds =
419 composition_character_bounds_[index];
420 POINT left_top = { bounds.left, bounds.top };
421 POINT right_bottom = { bounds.right, bounds.bottom };
422 ClientToScreen(window_handle_, &left_top);
423 ClientToScreen(window_handle_, &right_bottom);
424 SetRect(rect, left_top.x, left_top.y, right_bottom.x, right_bottom.y);
425 return true;
428 void OnDocumentTypeChanged(const std::vector<int32>& input_scopes) {
429 std::vector<InputScope> native_input_scopes(input_scopes.size());
430 for (size_t i = 0; i < input_scopes.size(); ++i)
431 native_input_scopes[i] = static_cast<InputScope>(input_scopes[i]);
432 scoped_ptr<DocumentBinding> new_document =
433 DocumentBinding::Create(thread_manager_.get(),
434 client_id_,
435 native_input_scopes,
436 window_handle_,
437 this);
438 LOG_IF(ERROR, !new_document) << "Failed to create a new document.";
439 current_document_.swap(new_document);
440 OnWindowActivated();
443 TfClientId client_id_;
444 HWND window_handle_;
445 TextServiceDelegate* delegate_;
446 scoped_ptr<DocumentBinding> current_document_;
447 base::win::ScopedComPtr<ITfThreadMgr> thread_manager_;
449 // A vector of InputScope enumeration, which represents the document type of
450 // the focused text field. Note that in our IPC message protocol, an empty
451 // |input_scopes_| has special meaning that IMEs must be disabled on this
452 // document.
453 std::vector<int32> input_scopes_;
454 // Character bounds of the composition. When there is no composition but this
455 // vector is not empty, the first element contains the caret bounds.
456 std::vector<metro_viewer::CharacterBounds> composition_character_bounds_;
458 DISALLOW_COPY_AND_ASSIGN(TextServiceImpl);
461 } // namespace
463 scoped_ptr<TextService>
464 CreateTextService(TextServiceDelegate* delegate, HWND window_handle) {
465 if (!delegate)
466 return scoped_ptr<TextService>();
467 base::win::ScopedComPtr<ITfThreadMgr> thread_manager;
468 HRESULT hr = thread_manager.CreateInstance(CLSID_TF_ThreadMgr);
469 if (FAILED(hr)) {
470 LOG(ERROR) << "Failed to create instance of CLSID_TF_ThreadMgr. hr = "
471 << hr;
472 return scoped_ptr<TextService>();
474 TfClientId client_id = TF_CLIENTID_NULL;
475 hr = thread_manager->Activate(&client_id);
476 if (FAILED(hr)) {
477 LOG(ERROR) << "ITfThreadMgr::Activate failed. hr = " << hr;
478 return scoped_ptr<TextService>();
480 if (!InitializeSentenceMode(thread_manager.get(), client_id)) {
481 LOG(ERROR) << "InitializeSentenceMode failed.";
482 thread_manager->Deactivate();
483 return scoped_ptr<TextService>();
485 return scoped_ptr<TextService>(new TextServiceImpl(
486 thread_manager.get(), client_id, window_handle, delegate));
489 } // namespace metro_driver