Index: win8/metro_driver/ime/text_service.cc |
diff --git a/win8/metro_driver/ime/text_service.cc b/win8/metro_driver/ime/text_service.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..73a2928c45967791ec68bf1c834386d8bd29f6d1 |
--- /dev/null |
+++ b/win8/metro_driver/ime/text_service.cc |
@@ -0,0 +1,435 @@ |
+// Copyright (c) 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 <msctf.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: |
ananta
2013/11/27 20:02:47
Thanks for the detailed overview here. Very helpfu
yukawa
2013/11/28 06:06:20
Yey!
|
+// 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 tying 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 functionality for IMEs. |
+// |
+// metro_driver process -> browser process |
+// Message Type: |
+// - MetroViewerHostMsg_ImeCompositionChanged |
+// - MetroViewerHostMsg_ImeTextCommitted |
+// Message Routing: |
+// TextServiceImpl |
+// -> ChromeAppViewAsh |
+// -- (process boundary) -- |
+// -> RemoteRootWindowHostWin |
+// -> RemoteInputMethodWin |
+// |
+// browser process -> metro_driver process |
+// Message Type: |
+// - MetroViewerHostMsg_ImeCancelComposition |
+// - MetroViewerHostMsg_ImeTextInputClientUpdated |
+// Message Routing: |
+// RemoteInputMethodWin |
+// -> RemoteRootWindowHostWin |
+// -- (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 list of the major tasks that are handled in TextServiceImpl. |
+// - Manages a session object obtained from the TSF runtime. We need them |
+// to call most of TSF APIs. |
+// - Handles OnDocumentChanged event. Whenever the document context 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 dramatically change their behavior depending on |
+// properties exposed from the virtual document, we need to set up a lot |
+// attributes 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 mroe underlying details. |
+ |
+namespace metro_driver { |
+namespace { |
+typedef TSFTextStore TextStore; |
+ |
+// Japanese IME expects the default value of this compartment is |
+// TF_SENTENCEMODE_PHRASEPREDICT like IMM32 implementation. This value is |
+// managed per thread, thus setting this value at once is enough. This |
+// value never affects other IMEs except for Japanese. |
+bool InitializeSentenceMode(ITfThreadMgr2* thread_manager, |
+ TfClientId client_id) { |
+ base::win::ScopedComPtr<ITfCompartmentMgr> thread_compartment_manager; |
+ if (FAILED(thread_compartment_manager.QueryFrom(thread_manager))) |
+ return false; |
+ base::win::ScopedComPtr<ITfCompartment> sentence_compartment; |
+ if (FAILED(thread_compartment_manager->GetCompartment( |
+ GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE, |
+ sentence_compartment.Receive()))) { |
+ return false; |
+ } |
+ |
+ base::win::ScopedVariant sentence_variant; |
+ sentence_variant.Set(TF_SENTENCEMODE_PHRASEPREDICT); |
+ if (FAILED(sentence_compartment->SetValue(client_id, &sentence_variant))) |
+ return false; |
+ return true; |
+} |
+ |
+// Initializes |context| as disabled context where IMEs will be disabled. |
+bool InitializeDisabledContext(ITfContext* context, TfClientId client_id) { |
+ base::win::ScopedComPtr<ITfCompartmentMgr> compartment_mgr; |
+ if (FAILED(compartment_mgr.QueryFrom(context))) |
+ return false; |
+ |
+ base::win::ScopedComPtr<ITfCompartment> disabled_compartment; |
+ if (FAILED(compartment_mgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_DISABLED, |
+ disabled_compartment.Receive()))) { |
+ return false; |
+ } |
+ |
+ base::win::ScopedVariant variant; |
+ variant.Set(1); |
+ if (FAILED(disabled_compartment->SetValue(client_id, &variant))) |
+ return false; |
+ |
+ base::win::ScopedComPtr<ITfCompartment> empty_context; |
+ if (FAILED(compartment_mgr->GetCompartment(GUID_COMPARTMENT_EMPTYCONTEXT, |
+ empty_context.Receive()))) { |
+ return false; |
+ } |
+ |
+ base::win::ScopedVariant empty_context_variant; |
+ empty_context_variant.Set(static_cast<int32>(1)); |
+ if (FAILED(empty_context->SetValue(client_id, &empty_context_variant))) |
+ return false; |
+ |
+ return true; |
+} |
+ |
+scoped_refptr<TextStore> CreateTextStore(const std::vector<int32>& input_scopes, |
+ HWND window_handle, |
+ TextStoreDelegate* delegate) { |
+ // Note: An empty |input_scopes| is not an error but means that an IME must be |
+ // disabled in this context. We can safely omit its instantiation. |
+ if (input_scopes.empty()) |
+ return NULL; |
+ std::vector<InputScope> buffer(input_scopes.size()); |
+ for (size_t i = 0; i < input_scopes.size(); ++i) |
+ buffer[i] = static_cast<InputScope>(input_scopes[i]); |
+ return new TextStore(window_handle, buffer, delegate); |
+} |
+ |
+// 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<ITfSource> source) |
+ : cookie_(cookie), |
+ source_(source) {} |
+ ~EventSink() { |
+ if (!source_ || cookie_ != TF_INVALID_COOKIE) |
+ return; |
+ source_->UnadviseSink(cookie_); |
+ cookie_ = TF_INVALID_COOKIE; |
+ source_.Release(); |
+ } |
+ |
+ private: |
+ DWORD cookie_; |
+ base::win::ScopedComPtr<ITfSource> source_; |
+ DISALLOW_COPY_AND_ASSIGN(EventSink); |
+}; |
+ |
+scoped_ptr<EventSink> CreateTextEditSink(ITfContext* context, |
+ ITfTextEditSink* text_store) { |
+ if (!text_store) |
+ return scoped_ptr<EventSink>(); |
+ base::win::ScopedComPtr<ITfSource> source; |
+ DWORD cookie = TF_INVALID_EDIT_COOKIE; |
+ if (FAILED(source.QueryFrom(context))) |
+ return scoped_ptr<EventSink>(); |
+ if (FAILED(source->AdviseSink(IID_ITfTextEditSink, text_store, &cookie))) |
+ return scoped_ptr<EventSink>(); |
+ return scoped_ptr<EventSink>(new EventSink(cookie, source)); |
+} |
+ |
+// A set of objects that should have the same lifetime. Following things |
+// are maingained. |
ananta
2013/11/27 20:02:47
maintained.
yukawa
2013/11/28 06:06:20
Done.
|
+// - 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_) |
+ return; |
+ document_manager_->Pop(TF_POPF_ALL); |
+ } |
+ |
+ static scoped_ptr<DocumentBinding> Create( |
+ ITfThreadMgr2* thread_manager, |
+ TfClientId client_id, |
+ const std::vector<int32>& input_scopes, |
+ HWND window_handle, |
+ TextStoreDelegate* delegate) { |
+ base::win::ScopedComPtr<ITfDocumentMgr> document_manager; |
+ if (!thread_manager) |
+ 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
|
+ if (FAILED(thread_manager->CreateDocumentMgr(document_manager.Receive()))) |
+ return scoped_ptr<DocumentBinding>(); |
+ |
+ // Note: |text_store| can be NULL and it's not an error. It means that the |
+ // document expects an IME to be disabled while the document is focused. |
+ scoped_refptr<TextStore> text_store = CreateTextStore(input_scopes, |
+ 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
|
+ delegate); |
+ |
+ base::win::ScopedComPtr<ITfContext> context; |
+ DWORD edit_cookie = TF_INVALID_EDIT_COOKIE; |
+ if (FAILED(document_manager->CreateContext( |
+ client_id, |
+ 0, |
+ static_cast<ITextStoreACP*>(text_store.get()), |
+ context.Receive(), |
+ &edit_cookie))) { |
+ 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.
|
+ } |
+ const bool is_password_field = |
+ std::find(input_scopes.begin(), input_scopes.end(), IS_PASSWORD) != |
+ input_scopes.end(); |
+ const bool use_disabled_context = input_scopes.empty() || is_password_field; |
+ if (use_disabled_context && !InitializeDisabledContext(context, client_id)) |
+ return scoped_ptr<DocumentBinding>(); |
+ scoped_ptr<EventSink> text_edit_sink = CreateTextEditSink(context, |
+ text_store); |
+ if (!text_edit_sink) |
+ 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.
|
+ if (FAILED(document_manager->Push(context))) |
+ return scoped_ptr<DocumentBinding>(); |
ananta
2013/11/27 20:02:47
Ditto
yukawa
2013/11/28 06:06:20
Ditto.
|
+ return scoped_ptr<DocumentBinding>( |
+ new DocumentBinding(text_store, |
+ document_manager, |
+ text_edit_sink.Pass())); |
+ } |
+ |
+ ITfDocumentMgr* document_manager() const { |
+ return document_manager_; |
+ } |
+ |
+ scoped_refptr<TextStore> text_store() { |
+ return text_store_; |
+ } |
+ |
+ private: |
+ DocumentBinding(scoped_refptr<TextStore> text_store, |
+ base::win::ScopedComPtr<ITfDocumentMgr> document_manager, |
+ scoped_ptr<EventSink> text_edit_sink) |
+ : text_store_(text_store), |
+ document_manager_(document_manager), |
+ text_edit_sink_(text_edit_sink.Pass()) {} |
+ |
+ scoped_refptr<TextStore> text_store_; |
+ base::win::ScopedComPtr<ITfDocumentMgr> document_manager_; |
+ scoped_ptr<EventSink> text_edit_sink_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(DocumentBinding); |
+}; |
+ |
+class TextServiceImpl : public TextService, |
+ public TextStoreDelegate { |
+ public: |
+ 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.
|
+ 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_); |
+ } |
+ virtual ~TextServiceImpl() { |
+ thread_manager_->Deactivate(); |
+ } |
+ |
+ private: |
+ // TextService overrides: |
+ virtual void TextService::CancelComposition() OVERRIDE { |
+ DocumentBinding* document = current_document_.get(); |
+ if (!document) |
+ return; |
+ TextStore* text_store = document->text_store(); |
+ if (!text_store) |
+ return; |
+ text_store->CancelComposition(); |
+ } |
+ |
+ virtual void OnDocumentChanged( |
+ const std::vector<int32>& input_scopes, |
+ const std::vector<metro_viewer::CharacterBounds>& 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); |
+ } |
+ |
+ virtual void OnWindowActivated() OVERRIDE { |
+ 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
|
+ return; |
+ ITfDocumentMgr* document_manager = current_document_->document_manager(); |
+ 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.
|
+ return; |
+ 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
|
+ return; |
+ } |
+ |
+ virtual void OnCompositionChanged( |
+ const string16& text, |
+ int32 selection_start, |
+ int32 selection_end, |
+ const std::vector<metro_viewer::UnderlineInfo>& underlines) OVERRIDE { |
+ if (!delegate_) |
+ return; |
+ delegate_->OnCompositionChanged(text, |
+ selection_start, |
+ selection_end, |
+ underlines); |
+ } |
+ |
+ virtual void OnTextCommitted(const string16& text) OVERRIDE { |
+ if (!delegate_) |
+ return; |
+ delegate_->OnTextCommitted(text); |
+ } |
+ |
+ virtual RECT GetCaretBounds() { |
+ 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; |
+ } |
+ |
+ virtual bool GetCompositionCharacterBounds(uint32 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<int32>& input_scopes) { |
+ scoped_ptr<DocumentBinding> new_document = |
+ DocumentBinding::Create(thread_manager_.get(), |
+ client_id_, |
+ input_scopes, |
+ window_handle_, |
+ this); |
+ 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 "
|
+ return; |
+ current_document_.swap(new_document); |
+ OnWindowActivated(); |
+ } |
+ |
+ TfClientId client_id_; |
+ HWND window_handle_; |
+ TextServiceDelegate* delegate_; |
+ scoped_ptr<DocumentBinding> current_document_; |
+ base::win::ScopedComPtr<ITfThreadMgr2> thread_manager_; |
+ |
+ // An vector of InputScope enumeration, which represent the context type of |
+ // the focused text field. If this vector is empty, it means that IMEs must |
+ // be disabled there. |
+ std::vector<int32> 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<metro_viewer::CharacterBounds> composition_character_bounds_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(TextServiceImpl); |
+}; |
+ |
+} // namespace |
+ |
+scoped_ptr<TextService> |
+CreateTextService(TextServiceDelegate* delegate, HWND window_handle) { |
+ if (!delegate) |
+ return scoped_ptr<TextService>(); |
+ base::win::ScopedComPtr<ITfThreadMgr2> thread_manager; |
+ if (FAILED(thread_manager.CreateInstance(CLSID_TF_ThreadMgr))) |
+ return scoped_ptr<TextService>(); |
+ TfClientId client_id = TF_CLIENTID_NULL; |
+ if (FAILED(thread_manager->ActivateEx(&client_id, 0))) |
+ return scoped_ptr<TextService>(); |
+ if (!InitializeSentenceMode(thread_manager, client_id)) { |
+ thread_manager->Deactivate(); |
+ return scoped_ptr<TextService>(); |
+ } |
+ return scoped_ptr<TextService>(new TextServiceImpl(thread_manager, |
+ client_id, |
+ window_handle, |
+ delegate)); |
+} |
+ |
+} // namespace metro_driver |