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

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: Address comment from ananta. Add text_service_delegate.h, which I forgot to upload. Style fixes. 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
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/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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698