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