Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(18)

Side by Side Diff: win8/metro_driver/ime/text_service.cc

Issue 83233002: Enable basic IME functionality under Ash on Windows (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix warnings (that are treated as an error) on 64-bit build Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « win8/metro_driver/ime/text_service.h ('k') | win8/metro_driver/ime/text_service_delegate.h » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 HRESULT hr = thread_compartment_manager.QueryFrom(thread_manager);
100 if (FAILED(hr)) {
101 LOG(ERROR) << "QueryFrom failed. hr = " << hr;
102 return false;
103 }
104 base::win::ScopedComPtr<ITfCompartment> sentence_compartment;
105 hr = thread_compartment_manager->GetCompartment(
106 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE,
107 sentence_compartment.Receive());
108 if (FAILED(hr)) {
109 LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
110 return false;
111 }
112
113 base::win::ScopedVariant sentence_variant;
114 sentence_variant.Set(TF_SENTENCEMODE_PHRASEPREDICT);
115 hr = sentence_compartment->SetValue(client_id, &sentence_variant);
116 if (FAILED(hr)) {
117 LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
118 return false;
119 }
120 return true;
121 }
122
123 // Initializes |context| as disabled context where IMEs will be disabled.
124 bool InitializeDisabledContext(ITfContext* context, TfClientId client_id) {
125 base::win::ScopedComPtr<ITfCompartmentMgr> compartment_mgr;
126 HRESULT hr = compartment_mgr.QueryFrom(context);
127 if (FAILED(hr)) {
128 LOG(ERROR) << "QueryFrom failed. hr = " << hr;
129 return false;
130 }
131
132 base::win::ScopedComPtr<ITfCompartment> disabled_compartment;
133 hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_DISABLED,
134 disabled_compartment.Receive());
135 if (FAILED(hr)) {
136 LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
137 return false;
138 }
139
140 base::win::ScopedVariant variant;
141 variant.Set(1);
142 hr = disabled_compartment->SetValue(client_id, &variant);
143 if (FAILED(hr)) {
144 LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
145 return false;
146 }
147
148 base::win::ScopedComPtr<ITfCompartment> empty_context;
149 hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_EMPTYCONTEXT,
150 empty_context.Receive());
151 if (FAILED(hr)) {
152 LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
153 return false;
154 }
155
156 base::win::ScopedVariant empty_context_variant;
157 empty_context_variant.Set(static_cast<int32>(1));
158 hr = empty_context->SetValue(client_id, &empty_context_variant);
159 if (FAILED(hr)) {
160 LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
161 return false;
162 }
163
164 return true;
165 }
166
167 bool IsPasswordField(const std::vector<InputScope>& input_scopes) {
168 return std::find(input_scopes.begin(), input_scopes.end(), IS_PASSWORD) !=
169 input_scopes.end();
170 }
171
172 // A class that manages the lifetime of the event callback registration. When
173 // this object is destroyed, corresponding event callback will be unregistered.
174 class EventSink {
175 public:
176 EventSink(DWORD cookie, base::win::ScopedComPtr<ITfSource> source)
177 : cookie_(cookie),
178 source_(source) {}
179 ~EventSink() {
180 if (!source_ || cookie_ != TF_INVALID_COOKIE)
181 return;
182 source_->UnadviseSink(cookie_);
183 cookie_ = TF_INVALID_COOKIE;
184 source_.Release();
185 }
186
187 private:
188 DWORD cookie_;
189 base::win::ScopedComPtr<ITfSource> source_;
190 DISALLOW_COPY_AND_ASSIGN(EventSink);
191 };
192
193 scoped_ptr<EventSink> CreateTextEditSink(ITfContext* context,
194 ITfTextEditSink* text_store) {
195 DCHECK(text_store);
196 base::win::ScopedComPtr<ITfSource> source;
197 DWORD cookie = TF_INVALID_EDIT_COOKIE;
198 HRESULT hr = source.QueryFrom(context);
199 if (FAILED(hr)) {
200 LOG(ERROR) << "QueryFrom failed, hr = " << hr;
201 return scoped_ptr<EventSink>();
202 }
203 hr = source->AdviseSink(IID_ITfTextEditSink, text_store, &cookie);
204 if (FAILED(hr)) {
205 LOG(ERROR) << "AdviseSink failed, hr = " << hr;
206 return scoped_ptr<EventSink>();
207 }
208 return scoped_ptr<EventSink>(new EventSink(cookie, source));
209 }
210
211 // A set of objects that should have the same lifetime. Following things
212 // are maintained.
213 // - TextStore: a COM object that abstracts text buffer. This object is
214 // actually implemented by us in text_store.cc
215 // - ITfDocumentMgr: a focusable unit in TSF. This object is implemented by
216 // TSF runtime and works as a container of TextStore.
217 // - EventSink: an object that ensures that the event callback between
218 // TSF runtime and TextStore is unregistered when this object is destroyed.
219 class DocumentBinding {
220 public:
221 ~DocumentBinding() {
222 if (!document_manager_)
223 return;
224 document_manager_->Pop(TF_POPF_ALL);
225 }
226
227 static scoped_ptr<DocumentBinding> Create(
228 ITfThreadMgr2* thread_manager,
229 TfClientId client_id,
230 const std::vector<InputScope>& input_scopes,
231 HWND window_handle,
232 TextStoreDelegate* delegate) {
233 base::win::ScopedComPtr<ITfDocumentMgr> document_manager;
234 HRESULT hr = thread_manager->CreateDocumentMgr(document_manager.Receive());
235 if (FAILED(hr)) {
236 LOG(ERROR) << "ITfThreadMgr2::CreateDocumentMgr failed. hr = " << hr;
237 return scoped_ptr<DocumentBinding>();
238 }
239
240 // Note: In our IPC protocol, an empty |input_scopes| is used to indicate
241 // that an IME must be disabled in this context. In such case, we need not
242 // instantiate TextStore.
243 const bool use_null_text_store = input_scopes.empty();
244
245 scoped_refptr<TextStore> text_store;
246 if (!use_null_text_store) {
247 text_store = TextStore::Create(window_handle, input_scopes, delegate);
248 if (!text_store) {
249 LOG(ERROR) << "Failed to create TextStore.";
250 return scoped_ptr<DocumentBinding>();
251 }
252 }
253
254 base::win::ScopedComPtr<ITfContext> context;
255 DWORD edit_cookie = TF_INVALID_EDIT_COOKIE;
256 hr = document_manager->CreateContext(
257 client_id,
258 0,
259 static_cast<ITextStoreACP*>(text_store.get()),
260 context.Receive(),
261 &edit_cookie);
262 if (FAILED(hr)) {
263 LOG(ERROR) << "ITfDocumentMgr::CreateContext failed. hr = " << hr;
264 return scoped_ptr<DocumentBinding>();
265 }
266
267 // If null-TextStore is used or |input_scopes| looks like a password field,
268 // set special properties to tell IMEs to be disabled.
269 if ((use_null_text_store || IsPasswordField(input_scopes)) &&
270 !InitializeDisabledContext(context, client_id)) {
271 LOG(ERROR) << "InitializeDisabledContext failed.";
272 return scoped_ptr<DocumentBinding>();
273 }
274
275 scoped_ptr<EventSink> text_edit_sink;
276 if (!use_null_text_store) {
277 text_edit_sink = CreateTextEditSink(context, text_store);
278 if (!text_edit_sink) {
279 LOG(ERROR) << "CreateTextEditSink failed.";
280 return scoped_ptr<DocumentBinding>();
281 }
282 }
283 hr = document_manager->Push(context);
284 if (FAILED(hr)) {
285 LOG(ERROR) << "ITfDocumentMgr::Push failed. hr = " << hr;
286 return scoped_ptr<DocumentBinding>();
287 }
288 return scoped_ptr<DocumentBinding>(
289 new DocumentBinding(text_store,
290 document_manager,
291 text_edit_sink.Pass()));
292 }
293
294 ITfDocumentMgr* document_manager() const {
295 return document_manager_;
296 }
297
298 scoped_refptr<TextStore> text_store() const {
299 return text_store_;
300 }
301
302 private:
303 DocumentBinding(scoped_refptr<TextStore> text_store,
304 base::win::ScopedComPtr<ITfDocumentMgr> document_manager,
305 scoped_ptr<EventSink> text_edit_sink)
306 : text_store_(text_store),
307 document_manager_(document_manager),
308 text_edit_sink_(text_edit_sink.Pass()) {}
309
310 scoped_refptr<TextStore> text_store_;
311 base::win::ScopedComPtr<ITfDocumentMgr> document_manager_;
312 scoped_ptr<EventSink> text_edit_sink_;
313
314 DISALLOW_COPY_AND_ASSIGN(DocumentBinding);
315 };
316
317 class TextServiceImpl : public TextService,
318 public TextStoreDelegate {
319 public:
320 TextServiceImpl(ITfThreadMgr2* thread_manager,
321 TfClientId client_id,
322 HWND window_handle,
323 TextServiceDelegate* delegate)
324 : client_id_(client_id),
325 window_handle_(window_handle),
326 delegate_(delegate),
327 thread_manager_(thread_manager) {
328 DCHECK_NE(TF_CLIENTID_NULL, client_id);
329 DCHECK(window_handle != NULL);
330 DCHECK(thread_manager_);
331 }
332 virtual ~TextServiceImpl() {
333 thread_manager_->Deactivate();
334 }
335
336 private:
337 // TextService overrides:
338 virtual void TextService::CancelComposition() OVERRIDE {
339 if (!current_document_) {
340 VLOG(0) << "|current_document_| is NULL due to the previous error.";
341 return;
342 }
343 TextStore* text_store = current_document_->text_store();
344 if (!text_store)
345 return;
346 text_store->CancelComposition();
347 }
348
349 virtual void OnDocumentChanged(
350 const std::vector<int32>& input_scopes,
351 const std::vector<metro_viewer::CharacterBounds>& character_bounds)
352 OVERRIDE {
353 bool document_type_changed = input_scopes_ != input_scopes;
354 input_scopes_ = input_scopes;
355 composition_character_bounds_ = character_bounds;
356 if (document_type_changed)
357 OnDocumentTypeChanged(input_scopes);
358 }
359
360 virtual void OnWindowActivated() OVERRIDE {
361 if (!current_document_) {
362 VLOG(0) << "|current_document_| is NULL due to the previous error.";
363 return;
364 }
365 ITfDocumentMgr* document_manager = current_document_->document_manager();
366 if (!document_manager) {
367 VLOG(0) << "|document_manager| is NULL due to the previous error.";
368 return;
369 }
370 HRESULT hr = thread_manager_->SetFocus(document_manager);
371 if (FAILED(hr)) {
372 LOG(ERROR) << "ITfThreadMgr2::SetFocus failed. hr = " << hr;
373 return;
374 }
375 }
376
377 virtual void OnCompositionChanged(
378 const string16& text,
379 int32 selection_start,
380 int32 selection_end,
381 const std::vector<metro_viewer::UnderlineInfo>& underlines) OVERRIDE {
382 if (!delegate_)
383 return;
384 delegate_->OnCompositionChanged(text,
385 selection_start,
386 selection_end,
387 underlines);
388 }
389
390 virtual void OnTextCommitted(const string16& text) OVERRIDE {
391 if (!delegate_)
392 return;
393 delegate_->OnTextCommitted(text);
394 }
395
396 virtual RECT GetCaretBounds() {
397 if (composition_character_bounds_.empty()) {
398 const RECT rect = {};
399 return rect;
400 }
401 const metro_viewer::CharacterBounds& bounds =
402 composition_character_bounds_[0];
403 POINT left_top = { bounds.left, bounds.top };
404 POINT right_bottom = { bounds.right, bounds.bottom };
405 ClientToScreen(window_handle_, &left_top);
406 ClientToScreen(window_handle_, &right_bottom);
407 const RECT rect = {
408 left_top.x,
409 left_top.y,
410 right_bottom.x,
411 right_bottom.y,
412 };
413 return rect;
414 }
415
416 virtual bool GetCompositionCharacterBounds(uint32 index,
417 RECT* rect) OVERRIDE {
418 if (index >= composition_character_bounds_.size()) {
419 return false;
420 }
421 const metro_viewer::CharacterBounds& bounds =
422 composition_character_bounds_[index];
423 POINT left_top = { bounds.left, bounds.top };
424 POINT right_bottom = { bounds.right, bounds.bottom };
425 ClientToScreen(window_handle_, &left_top);
426 ClientToScreen(window_handle_, &right_bottom);
427 SetRect(rect, left_top.x, left_top.y, right_bottom.x, right_bottom.y);
428 return true;
429 }
430
431 void OnDocumentTypeChanged(const std::vector<int32>& input_scopes) {
432 std::vector<InputScope> native_input_scopes(input_scopes.size());
433 for (size_t i = 0; i < input_scopes.size(); ++i)
434 native_input_scopes[i] = static_cast<InputScope>(input_scopes[i]);
435 scoped_ptr<DocumentBinding> new_document =
436 DocumentBinding::Create(thread_manager_.get(),
437 client_id_,
438 native_input_scopes,
439 window_handle_,
440 this);
441 LOG_IF(ERROR, !new_document) << "Failed to create a new document.";
442 current_document_.swap(new_document);
443 OnWindowActivated();
444 }
445
446 TfClientId client_id_;
447 HWND window_handle_;
448 TextServiceDelegate* delegate_;
449 scoped_ptr<DocumentBinding> current_document_;
450 base::win::ScopedComPtr<ITfThreadMgr2> thread_manager_;
451
452 // A vector of InputScope enumeration, which represents the document type of
453 // the focused text field. Note that in our IPC message protocol, an empty
454 // |input_scopes_| has special meaning that IMEs must be disabled on this
455 // document.
456 std::vector<int32> input_scopes_;
457 // Character bounds of the composition. When there is no composition but this
458 // vector is not empty, the first element contains the caret bounds.
459 std::vector<metro_viewer::CharacterBounds> composition_character_bounds_;
460
461 DISALLOW_COPY_AND_ASSIGN(TextServiceImpl);
462 };
463
464 } // namespace
465
466 scoped_ptr<TextService>
467 CreateTextService(TextServiceDelegate* delegate, HWND window_handle) {
468 if (!delegate)
469 return scoped_ptr<TextService>();
470 base::win::ScopedComPtr<ITfThreadMgr2> thread_manager;
471 HRESULT hr = thread_manager.CreateInstance(CLSID_TF_ThreadMgr);
472 if (FAILED(hr)) {
473 LOG(ERROR) << "Failed to create instance of CLSID_TF_ThreadMgr. hr = "
474 << hr;
475 return scoped_ptr<TextService>();
476 }
477 TfClientId client_id = TF_CLIENTID_NULL;
478 hr = thread_manager->ActivateEx(&client_id, 0);
479 if (FAILED(hr)) {
480 LOG(ERROR) << "ITfThreadMgr2::ActivateEx failed. hr = " << hr;
481 return scoped_ptr<TextService>();
482 }
483 if (!InitializeSentenceMode(thread_manager, client_id)) {
484 LOG(ERROR) << "InitializeSentenceMode failed.";
485 thread_manager->Deactivate();
486 return scoped_ptr<TextService>();
487 }
488 return scoped_ptr<TextService>(new TextServiceImpl(thread_manager,
489 client_id,
490 window_handle,
491 delegate));
492 }
493
494 } // namespace metro_driver
OLDNEW
« no previous file with comments | « win8/metro_driver/ime/text_service.h ('k') | win8/metro_driver/ime/text_service_delegate.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698