Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 | |
| OLD | NEW |