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