// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "win8/metro_driver/ime/text_service.h" #include #include #include #include #include "base/logging.h" #include "base/macros.h" #include "base/win/scoped_variant.h" #include "ui/metro_viewer/ime_types.h" #include "win8/metro_driver/ime/text_service_delegate.h" #include "win8/metro_driver/ime/text_store.h" #include "win8/metro_driver/ime/text_store_delegate.h" // Architecture overview of input method support on Ash mode: // // Overview: // On Ash mode, the system keyboard focus is owned by the metro_driver process // while most of event handling are still implemented in the browser process. // Thus the metro_driver basically works as a proxy that simply forwards // keyevents to the metro_driver process. IME support must be involved somewhere // in this flow. // // In short, we need to interact with an IME in the metro_driver process since // TSF (Text Services Framework) runtime wants to processes keyevents while // (and only while) the attached UI thread owns keyboard focus. // // Due to this limitation, we need to split IME handling into two parts, one // is in the metro_driver process and the other is in the browser process. // The metro_driver process is responsible for implementing the primary data // store for the composition text and wiring it up with an IME via TSF APIs. // On the other hand, the browser process is responsible for calculating // character position in the composition text whenever the composition text // is updated. // // IPC overview: // Fortunately, we don't need so many IPC messages to support IMEs. In fact, // only 4 messages are required to enable basic IME functionality. // // metro_driver process -> browser process // Message Type: // - MetroViewerHostMsg_ImeCompositionChanged // - MetroViewerHostMsg_ImeTextCommitted // Message Routing: // TextServiceImpl // -> ChromeAppViewAsh // -- (process boundary) -- // -> RemoteWindowTreeHostWin // -> RemoteInputMethodWin // // browser process -> metro_driver process // Message Type: // - MetroViewerHostMsg_ImeCancelComposition // - MetroViewerHostMsg_ImeTextInputClientUpdated // Message Routing: // RemoteInputMethodWin // -> RemoteWindowTreeHostWin // -- (process boundary) -- // -> ChromeAppViewAsh // -> TextServiceImpl // // Note that a keyevent may be forwarded through a different path. When a // keyevent is not handled by an IME, such keyevent and subsequent character // events will be sent from the metro_driver process to the browser process as // following IPC messages. // - MetroViewerHostMsg_KeyDown // - MetroViewerHostMsg_KeyUp // - MetroViewerHostMsg_Character // // How TextServiceImpl works: // Here is the list of the major tasks that are handled in TextServiceImpl. // - Manages a session object obtained from TSF runtime. We need them to call // most of TSF APIs. // - Handles OnDocumentChanged event. Whenever the document type is changed, // TextServiceImpl destroyes the current document and initializes new one // according to the given |input_scopes|. // - Stores the |composition_character_bounds_| passed from OnDocumentChanged // event so that an IME or on-screen keyboard can query the character // position synchronously. // The most complicated part is the OnDocumentChanged handler. Since some IMEs // such as Japanese IMEs drastically change their behavior depending on // properties exposed from the virtual document, we need to set up a lot // properties carefully and correctly. See DocumentBinding class in this file // about what will be involved in this multi-phase construction. See also // text_store.cc and input_scope.cc for more underlying details. namespace metro_driver { namespace { // Japanese IME expects the default value of this compartment is // TF_SENTENCEMODE_PHRASEPREDICT to emulate IMM32 behavior. This value is // managed per thread, thus setting this value at once is sufficient. This // value never affects non-Japanese IMEs. bool InitializeSentenceMode(ITfThreadMgr* thread_manager, TfClientId client_id) { base::win::ScopedComPtr thread_compartment_manager; HRESULT hr = thread_compartment_manager.QueryFrom(thread_manager); if (FAILED(hr)) { LOG(ERROR) << "QueryFrom failed. hr = " << hr; return false; } base::win::ScopedComPtr sentence_compartment; hr = thread_compartment_manager->GetCompartment( GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE, sentence_compartment.Receive()); if (FAILED(hr)) { LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr; return false; } base::win::ScopedVariant sentence_variant; sentence_variant.Set(TF_SENTENCEMODE_PHRASEPREDICT); hr = sentence_compartment->SetValue(client_id, sentence_variant.ptr()); if (FAILED(hr)) { LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr; return false; } return true; } // Initializes |context| as disabled context where IMEs will be disabled. bool InitializeDisabledContext(ITfContext* context, TfClientId client_id) { base::win::ScopedComPtr compartment_mgr; HRESULT hr = compartment_mgr.QueryFrom(context); if (FAILED(hr)) { LOG(ERROR) << "QueryFrom failed. hr = " << hr; return false; } base::win::ScopedComPtr disabled_compartment; hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_DISABLED, disabled_compartment.Receive()); if (FAILED(hr)) { LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr; return false; } base::win::ScopedVariant variant; variant.Set(1); hr = disabled_compartment->SetValue(client_id, variant.ptr()); if (FAILED(hr)) { LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr; return false; } base::win::ScopedComPtr empty_context; hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_EMPTYCONTEXT, empty_context.Receive()); if (FAILED(hr)) { LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr; return false; } base::win::ScopedVariant empty_context_variant; empty_context_variant.Set(static_cast(1)); hr = empty_context->SetValue(client_id, empty_context_variant.ptr()); if (FAILED(hr)) { LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr; return false; } return true; } bool IsPasswordField(const std::vector& input_scopes) { return std::find(input_scopes.begin(), input_scopes.end(), IS_PASSWORD) != input_scopes.end(); } // A class that manages the lifetime of the event callback registration. When // this object is destroyed, corresponding event callback will be unregistered. class EventSink { public: EventSink(DWORD cookie, base::win::ScopedComPtr source) : cookie_(cookie), source_(source) {} ~EventSink() { if (!source_.get() || cookie_ != TF_INVALID_COOKIE) return; source_->UnadviseSink(cookie_); cookie_ = TF_INVALID_COOKIE; source_.Release(); } private: DWORD cookie_; base::win::ScopedComPtr source_; DISALLOW_COPY_AND_ASSIGN(EventSink); }; scoped_ptr CreateTextEditSink(ITfContext* context, ITfTextEditSink* text_store) { DCHECK(text_store); base::win::ScopedComPtr source; DWORD cookie = TF_INVALID_EDIT_COOKIE; HRESULT hr = source.QueryFrom(context); if (FAILED(hr)) { LOG(ERROR) << "QueryFrom failed, hr = " << hr; return scoped_ptr(); } hr = source->AdviseSink(IID_ITfTextEditSink, text_store, &cookie); if (FAILED(hr)) { LOG(ERROR) << "AdviseSink failed, hr = " << hr; return scoped_ptr(); } return scoped_ptr(new EventSink(cookie, source)); } // A set of objects that should have the same lifetime. Following things // are maintained. // - TextStore: a COM object that abstracts text buffer. This object is // actually implemented by us in text_store.cc // - ITfDocumentMgr: a focusable unit in TSF. This object is implemented by // TSF runtime and works as a container of TextStore. // - EventSink: an object that ensures that the event callback between // TSF runtime and TextStore is unregistered when this object is destroyed. class DocumentBinding { public: ~DocumentBinding() { if (!document_manager_.get()) return; document_manager_->Pop(TF_POPF_ALL); } static scoped_ptr Create( ITfThreadMgr* thread_manager, TfClientId client_id, const std::vector& input_scopes, HWND window_handle, TextStoreDelegate* delegate) { base::win::ScopedComPtr document_manager; HRESULT hr = thread_manager->CreateDocumentMgr(document_manager.Receive()); if (FAILED(hr)) { LOG(ERROR) << "ITfThreadMgr::CreateDocumentMgr failed. hr = " << hr; return scoped_ptr(); } // Note: In our IPC protocol, an empty |input_scopes| is used to indicate // that an IME must be disabled in this context. In such case, we need not // instantiate TextStore. const bool use_null_text_store = input_scopes.empty(); scoped_refptr text_store; if (!use_null_text_store) { text_store = TextStore::Create(window_handle, input_scopes, delegate); if (!text_store.get()) { LOG(ERROR) << "Failed to create TextStore."; return scoped_ptr(); } } base::win::ScopedComPtr context; DWORD edit_cookie = TF_INVALID_EDIT_COOKIE; hr = document_manager->CreateContext( client_id, 0, static_cast(text_store.get()), context.Receive(), &edit_cookie); if (FAILED(hr)) { LOG(ERROR) << "ITfDocumentMgr::CreateContext failed. hr = " << hr; return scoped_ptr(); } // If null-TextStore is used or |input_scopes| looks like a password field, // set special properties to tell IMEs to be disabled. if ((use_null_text_store || IsPasswordField(input_scopes)) && !InitializeDisabledContext(context.get(), client_id)) { LOG(ERROR) << "InitializeDisabledContext failed."; return scoped_ptr(); } scoped_ptr text_edit_sink; if (!use_null_text_store) { text_edit_sink = CreateTextEditSink(context.get(), text_store.get()); if (!text_edit_sink) { LOG(ERROR) << "CreateTextEditSink failed."; return scoped_ptr(); } } hr = document_manager->Push(context.get()); if (FAILED(hr)) { LOG(ERROR) << "ITfDocumentMgr::Push failed. hr = " << hr; return scoped_ptr(); } return scoped_ptr(new DocumentBinding( text_store, document_manager, std::move(text_edit_sink))); } ITfDocumentMgr* document_manager() const { return document_manager_.get(); } scoped_refptr text_store() const { return text_store_; } private: DocumentBinding(scoped_refptr text_store, base::win::ScopedComPtr document_manager, scoped_ptr text_edit_sink) : text_store_(text_store), document_manager_(document_manager), text_edit_sink_(std::move(text_edit_sink)) {} scoped_refptr text_store_; base::win::ScopedComPtr document_manager_; scoped_ptr text_edit_sink_; DISALLOW_COPY_AND_ASSIGN(DocumentBinding); }; class TextServiceImpl : public TextService, public TextStoreDelegate { public: TextServiceImpl(ITfThreadMgr* thread_manager, TfClientId client_id, HWND window_handle, TextServiceDelegate* delegate) : client_id_(client_id), window_handle_(window_handle), delegate_(delegate), thread_manager_(thread_manager) { DCHECK_NE(TF_CLIENTID_NULL, client_id); DCHECK(window_handle != NULL); DCHECK(thread_manager_.get()); } ~TextServiceImpl() override { thread_manager_->Deactivate(); } private: // TextService overrides: void CancelComposition() override { if (!current_document_) { VLOG(0) << "|current_document_| is NULL due to the previous error."; return; } scoped_refptr text_store = current_document_->text_store(); if (!text_store.get()) return; text_store->CancelComposition(); } void OnDocumentChanged(const std::vector& input_scopes, const std::vector& character_bounds) override { bool document_type_changed = input_scopes_ != input_scopes; input_scopes_ = input_scopes; composition_character_bounds_ = character_bounds; if (document_type_changed) OnDocumentTypeChanged(input_scopes); } void OnWindowActivated() override { if (!current_document_) { VLOG(0) << "|current_document_| is NULL due to the previous error."; return; } ITfDocumentMgr* document_manager = current_document_->document_manager(); if (!document_manager) { VLOG(0) << "|document_manager| is NULL due to the previous error."; return; } HRESULT hr = thread_manager_->SetFocus(document_manager); if (FAILED(hr)) { LOG(ERROR) << "ITfThreadMgr::SetFocus failed. hr = " << hr; return; } } void OnCompositionChanged( const base::string16& text, int32_t selection_start, int32_t selection_end, const std::vector& underlines) override { if (!delegate_) return; delegate_->OnCompositionChanged(text, selection_start, selection_end, underlines); } void OnTextCommitted(const base::string16& text) override { if (!delegate_) return; delegate_->OnTextCommitted(text); } RECT GetCaretBounds() override { if (composition_character_bounds_.empty()) { const RECT rect = {}; return rect; } const metro_viewer::CharacterBounds& bounds = composition_character_bounds_[0]; POINT left_top = { bounds.left, bounds.top }; POINT right_bottom = { bounds.right, bounds.bottom }; ClientToScreen(window_handle_, &left_top); ClientToScreen(window_handle_, &right_bottom); const RECT rect = { left_top.x, left_top.y, right_bottom.x, right_bottom.y, }; return rect; } bool GetCompositionCharacterBounds(uint32_t index, RECT* rect) override { if (index >= composition_character_bounds_.size()) { return false; } const metro_viewer::CharacterBounds& bounds = composition_character_bounds_[index]; POINT left_top = { bounds.left, bounds.top }; POINT right_bottom = { bounds.right, bounds.bottom }; ClientToScreen(window_handle_, &left_top); ClientToScreen(window_handle_, &right_bottom); SetRect(rect, left_top.x, left_top.y, right_bottom.x, right_bottom.y); return true; } void OnDocumentTypeChanged(const std::vector& input_scopes) { std::vector native_input_scopes(input_scopes.size()); for (size_t i = 0; i < input_scopes.size(); ++i) native_input_scopes[i] = static_cast(input_scopes[i]); scoped_ptr new_document = DocumentBinding::Create(thread_manager_.get(), client_id_, native_input_scopes, window_handle_, this); LOG_IF(ERROR, !new_document) << "Failed to create a new document."; current_document_.swap(new_document); OnWindowActivated(); } TfClientId client_id_; HWND window_handle_; TextServiceDelegate* delegate_; scoped_ptr current_document_; base::win::ScopedComPtr thread_manager_; // A vector of InputScope enumeration, which represents the document type of // the focused text field. Note that in our IPC message protocol, an empty // |input_scopes_| has special meaning that IMEs must be disabled on this // document. std::vector input_scopes_; // Character bounds of the composition. When there is no composition but this // vector is not empty, the first element contains the caret bounds. std::vector composition_character_bounds_; DISALLOW_COPY_AND_ASSIGN(TextServiceImpl); }; } // namespace scoped_ptr CreateTextService(TextServiceDelegate* delegate, HWND window_handle) { if (!delegate) return scoped_ptr(); base::win::ScopedComPtr thread_manager; HRESULT hr = thread_manager.CreateInstance(CLSID_TF_ThreadMgr); if (FAILED(hr)) { LOG(ERROR) << "Failed to create instance of CLSID_TF_ThreadMgr. hr = " << hr; return scoped_ptr(); } TfClientId client_id = TF_CLIENTID_NULL; hr = thread_manager->Activate(&client_id); if (FAILED(hr)) { LOG(ERROR) << "ITfThreadMgr::Activate failed. hr = " << hr; return scoped_ptr(); } if (!InitializeSentenceMode(thread_manager.get(), client_id)) { LOG(ERROR) << "InitializeSentenceMode failed."; thread_manager->Deactivate(); return scoped_ptr(); } return scoped_ptr(new TextServiceImpl( thread_manager.get(), client_id, window_handle, delegate)); } } // namespace metro_driver