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 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 |
OLD | NEW |