summaryrefslogtreecommitdiffstats
path: root/win8/metro_driver
diff options
context:
space:
mode:
authoryukawa@chromium.org <yukawa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-12-05 08:31:40 +0000
committeryukawa@chromium.org <yukawa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-12-05 08:31:40 +0000
commit69b5d489f120a19375ca4a06321bbf1123e9db76 (patch)
tree0527bb7001765cdf0eaca25d6242de6ca2c78dfa /win8/metro_driver
parenta31ecc0d319f9490e67319e6bcabb78341e361a4 (diff)
downloadchromium_src-69b5d489f120a19375ca4a06321bbf1123e9db76.zip
chromium_src-69b5d489f120a19375ca4a06321bbf1123e9db76.tar.gz
chromium_src-69b5d489f120a19375ca4a06321bbf1123e9db76.tar.bz2
Enable basic IME functionality under Ash on Windows
With this CL, IMEs become functional under Ash environment on Windows. Some optional features for Chromium internal use (e.g., IME popup window detection, RTL language detection) will be covered by subsequent CLs. DesignDoc: https://docs.google.com/a/chromium.org/document/d/14TBE0LoamQz_MImcNWzeTIW_lo5EUrLJwNwmCi20V1o/edit# Note: In this CL, ui/base/ime/win/tsf_text_store.* is not moved but copied to win8/metro_driver/ime/text_store.* in order not to break non-Aura build. That said, hereafter we will use win8/metro_driver/ime/text_store.* as primary implementation of TSF TextStore. Anyway, ui/base/ime/win/tsf_text_store.* will be removed when Aura transition is successfully completed. I also found some style issues in win8/metro_driver/ime/text_store.cc but I'd like to fix them in subsequent CLs to keep this CL as minimum as possible. BUG=164964 TEST=manually done on Windows 8.1 Review URL: https://codereview.chromium.org/83233002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@238921 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'win8/metro_driver')
-rw-r--r--win8/metro_driver/chrome_app_view_ash.cc54
-rw-r--r--win8/metro_driver/chrome_app_view_ash.h30
-rw-r--r--win8/metro_driver/ime/ime.gypi16
-rw-r--r--win8/metro_driver/ime/input_scope.cc87
-rw-r--r--win8/metro_driver/ime/input_scope.h22
-rw-r--r--win8/metro_driver/ime/text_service.cc494
-rw-r--r--win8/metro_driver/ime/text_service.h53
-rw-r--r--win8/metro_driver/ime/text_service_delegate.h40
-rw-r--r--win8/metro_driver/ime/text_store.cc913
-rw-r--r--win8/metro_driver/ime/text_store.h308
-rw-r--r--win8/metro_driver/ime/text_store_delegate.h55
-rw-r--r--win8/metro_driver/metro_driver.gyp3
12 files changed, 2074 insertions, 1 deletions
diff --git a/win8/metro_driver/chrome_app_view_ash.cc b/win8/metro_driver/chrome_app_view_ash.cc
index 331ec16..c557bb5 100644
--- a/win8/metro_driver/chrome_app_view_ash.cc
+++ b/win8/metro_driver/chrome_app_view_ash.cc
@@ -24,6 +24,7 @@
#include "ui/events/gestures/gesture_sequence.h"
#include "ui/metro_viewer/metro_viewer_messages.h"
#include "win8/metro_driver/file_picker_ash.h"
+#include "win8/metro_driver/ime/text_service.h"
#include "win8/metro_driver/metro_driver.h"
#include "win8/metro_driver/winrt_utils.h"
#include "win8/viewer/metro_viewer_constants.h"
@@ -164,6 +165,10 @@ class ChromeChannelListener : public IPC::Listener {
IPC_MESSAGE_HANDLER(MetroViewerHostMsg_DisplaySelectFolder,
OnDisplayFolderPicker)
IPC_MESSAGE_HANDLER(MetroViewerHostMsg_SetCursorPos, OnSetCursorPos)
+ IPC_MESSAGE_HANDLER(MetroViewerHostMsg_ImeCancelComposition,
+ OnImeCancelComposition)
+ IPC_MESSAGE_HANDLER(MetroViewerHostMsg_ImeTextInputClientUpdated,
+ OnImeTextInputClientChanged)
IPC_MESSAGE_UNHANDLED(__debugbreak())
IPC_END_MESSAGE_MAP()
return true;
@@ -241,6 +246,23 @@ class ChromeChannelListener : public IPC::Listener {
x, y));
}
+ void OnImeCancelComposition() {
+ ui_proxy_->PostTask(
+ FROM_HERE,
+ base::Bind(&ChromeAppViewAsh::OnImeCancelComposition,
+ base::Unretained(app_view_)));
+ }
+
+ void OnImeTextInputClientChanged(
+ const std::vector<int32>& input_scopes,
+ const std::vector<metro_viewer::CharacterBounds>& character_bounds) {
+ ui_proxy_->PostTask(
+ FROM_HERE,
+ base::Bind(&ChromeAppViewAsh::OnImeUpdateTextInputClient,
+ base::Unretained(app_view_),
+ input_scopes,
+ character_bounds));
+ }
scoped_refptr<base::MessageLoopProxy> ui_proxy_;
ChromeAppViewAsh* app_view_;
@@ -472,6 +494,8 @@ ChromeAppViewAsh::SetWindow(winui::Core::ICoreWindow* window) {
hr = interop->get_WindowHandle(&core_window_hwnd_);
CheckHR(hr);
+ text_service_ = metro_driver::CreateTextService(this, core_window_hwnd_);
+
hr = window_->add_SizeChanged(mswr::Callback<SizeChangedHandler>(
this, &ChromeAppViewAsh::OnSizeChanged).Get(),
&sizechange_token_);
@@ -619,6 +643,7 @@ ChromeAppViewAsh::Run() {
IFACEMETHODIMP
ChromeAppViewAsh::Uninitialize() {
DVLOG(1) << __FUNCTION__;
+ text_service_.reset();
window_ = nullptr;
view_ = nullptr;
core_window_hwnd_ = NULL;
@@ -787,6 +812,33 @@ void ChromeAppViewAsh::OnFolderPickerCompleted(
delete folder_picker;
}
+void ChromeAppViewAsh::OnImeCancelComposition() {
+ if (!text_service_)
+ return;
+ text_service_->CancelComposition();
+}
+
+void ChromeAppViewAsh::OnImeUpdateTextInputClient(
+ const std::vector<int32>& input_scopes,
+ const std::vector<metro_viewer::CharacterBounds>& character_bounds) {
+ if (!text_service_)
+ return;
+ text_service_->OnDocumentChanged(input_scopes, character_bounds);
+}
+
+void ChromeAppViewAsh::OnCompositionChanged(
+ const string16& text,
+ int32 selection_start,
+ int32 selection_end,
+ const std::vector<metro_viewer::UnderlineInfo>& underlines) {
+ ui_channel_->Send(new MetroViewerHostMsg_ImeCompositionChanged(
+ text, selection_start, selection_end, underlines));
+}
+
+void ChromeAppViewAsh::OnTextCommitted(const string16& text) {
+ ui_channel_->Send(new MetroViewerHostMsg_ImeTextCommitted(text));
+}
+
HRESULT ChromeAppViewAsh::OnActivate(
winapp::Core::ICoreApplicationView*,
winapp::Activation::IActivatedEventArgs* args) {
@@ -1033,6 +1085,8 @@ HRESULT ChromeAppViewAsh::OnWindowActivated(
// clicked back in Ash after using another app on another monitor) the same.
if (state == winui::Core::CoreWindowActivationState_CodeActivated ||
state == winui::Core::CoreWindowActivationState_PointerActivated) {
+ if (text_service_)
+ text_service_->OnWindowActivated();
ui_channel_->Send(new MetroViewerHostMsg_WindowActivated());
}
return S_OK;
diff --git a/win8/metro_driver/chrome_app_view_ash.h b/win8/metro_driver/chrome_app_view_ash.h
index 9efd3f7..d50255b 100644
--- a/win8/metro_driver/chrome_app_view_ash.h
+++ b/win8/metro_driver/chrome_app_view_ash.h
@@ -15,6 +15,7 @@
#include "base/strings/string16.h"
#include "ui/events/event_constants.h"
#include "win8/metro_driver/direct3d_helper.h"
+#include "win8/metro_driver/ime/text_service_delegate.h"
namespace base {
class FilePath;
@@ -25,6 +26,15 @@ class Listener;
class ChannelProxy;
}
+namespace metro_driver {
+class TextService;
+}
+
+namespace metro_viewer {
+struct CharacterBounds;
+struct UnderlineInfo;
+}
+
class OpenFilePickerSession;
class SaveFilePickerSession;
class FolderPickerSession;
@@ -33,7 +43,8 @@ class FilePickerSessionBase;
struct MetroViewerHostMsg_SaveAsDialogParams;
class ChromeAppViewAsh
- : public mswr::RuntimeClass<winapp::Core::IFrameworkView> {
+ : public mswr::RuntimeClass<winapp::Core::IFrameworkView>,
+ public metro_driver::TextServiceDelegate {
public:
ChromeAppViewAsh();
~ChromeAppViewAsh();
@@ -82,9 +93,23 @@ class ChromeAppViewAsh
void OnFolderPickerCompleted(FolderPickerSession* folder_picker,
bool success);
+ void OnImeCancelComposition();
+ void OnImeUpdateTextInputClient(
+ const std::vector<int32>& input_scopes,
+ const std::vector<metro_viewer::CharacterBounds>& character_bounds);
+
HWND core_window_hwnd() const { return core_window_hwnd_; }
+
private:
+ // TextServiceDelegate overrides.
+ virtual void OnCompositionChanged(
+ const string16& text,
+ int32 selection_start,
+ int32 selection_end,
+ const std::vector<metro_viewer::UnderlineInfo>& underlines) OVERRIDE;
+ virtual void OnTextCommitted(const string16& text) OVERRIDE;
+
HRESULT OnActivate(winapp::Core::ICoreApplicationView* view,
winapp::Activation::IActivatedEventArgs* args);
@@ -163,6 +188,9 @@ class ChromeAppViewAsh
// UI message loop to allow message passing into this thread.
base::MessageLoop ui_loop_;
+
+ // For IME support.
+ scoped_ptr<metro_driver::TextService> text_service_;
};
#endif // WIN8_METRO_DRIVER_CHROME_APP_VIEW_ASH_H_
diff --git a/win8/metro_driver/ime/ime.gypi b/win8/metro_driver/ime/ime.gypi
new file mode 100644
index 0000000..c5d7f1e
--- /dev/null
+++ b/win8/metro_driver/ime/ime.gypi
@@ -0,0 +1,16 @@
+# 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.
+
+{
+ 'sources': [
+ 'input_scope.cc',
+ 'input_scope.h',
+ 'text_service.cc',
+ 'text_service.h',
+ 'text_service_delegate.h',
+ 'text_store.cc',
+ 'text_store.h',
+ 'text_store_delegate.h',
+ ],
+}
diff --git a/win8/metro_driver/ime/input_scope.cc b/win8/metro_driver/ime/input_scope.cc
new file mode 100644
index 0000000..82679c3
--- /dev/null
+++ b/win8/metro_driver/ime/input_scope.cc
@@ -0,0 +1,87 @@
+// 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/input_scope.h"
+
+#include <atlbase.h>
+#include <atlcom.h>
+
+#include "base/logging.h"
+#include "ui/base/win/atl_module.h"
+
+namespace metro_driver {
+namespace {
+
+// An implementation of ITfInputScope interface.
+// This implementation only covers ITfInputScope::GetInputScopes since built-in
+// on-screen keyboard on Windows 8+ changes its layout depending on the returned
+// value of this method.
+// Although other advanced features of ITfInputScope such as phase list or
+// regex support might be useful for IMEs or on-screen keyboards in future,
+// no IME seems to be utilizing such features as of Windows 8.1.
+class ATL_NO_VTABLE InputScopeImpl
+ : public CComObjectRootEx<CComMultiThreadModel>,
+ public ITfInputScope {
+ public:
+ InputScopeImpl() {}
+
+ BEGIN_COM_MAP(InputScopeImpl)
+ COM_INTERFACE_ENTRY(ITfInputScope)
+ END_COM_MAP()
+
+ void Initialize(const std::vector<InputScope>& input_scopes) {
+ input_scopes_ = input_scopes;
+ }
+
+ private:
+ // ITfInputScope overrides:
+ STDMETHOD(GetInputScopes)(InputScope** input_scopes, UINT* count) OVERRIDE {
+ if (!count || !input_scopes)
+ return E_INVALIDARG;
+ *input_scopes = static_cast<InputScope*>(
+ CoTaskMemAlloc(sizeof(InputScope) * input_scopes_.size()));
+ if (!input_scopes) {
+ *count = 0;
+ return E_OUTOFMEMORY;
+ }
+ std::copy(input_scopes_.begin(), input_scopes_.end(), *input_scopes);
+ *count = static_cast<UINT>(input_scopes_.size());
+ return S_OK;
+ }
+ STDMETHOD(GetPhrase)(BSTR** phrases, UINT* count) OVERRIDE {
+ return E_NOTIMPL;
+ }
+ STDMETHOD(GetRegularExpression)(BSTR* regexp) OVERRIDE {
+ return E_NOTIMPL;
+ }
+ STDMETHOD(GetSRGS)(BSTR* srgs) OVERRIDE {
+ return E_NOTIMPL;
+ }
+ STDMETHOD(GetXML)(BSTR* xml) OVERRIDE {
+ return E_NOTIMPL;
+ }
+
+ // Data which ITfInputScope::GetInputScopes should return.
+ std::vector<InputScope> input_scopes_;
+
+ DISALLOW_COPY_AND_ASSIGN(InputScopeImpl);
+};
+
+} // namespace
+
+base::win::ScopedComPtr<ITfInputScope>
+CreteInputScope(const std::vector<InputScope>& input_scopes) {
+ ui::win::CreateATLModuleIfNeeded();
+ CComObject<InputScopeImpl>* object = NULL;
+ HRESULT hr = CComObject<InputScopeImpl>::CreateInstance(&object);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "CComObject<InputScopeImpl>::CreateInstance failed. hr = "
+ << hr;
+ return base::win::ScopedComPtr<ITfInputScope>();
+ }
+ object->Initialize(input_scopes);
+ return base::win::ScopedComPtr<ITfInputScope>(object);
+}
+
+} // namespace metro_driver
diff --git a/win8/metro_driver/ime/input_scope.h b/win8/metro_driver/ime/input_scope.h
new file mode 100644
index 0000000..99ed283
--- /dev/null
+++ b/win8/metro_driver/ime/input_scope.h
@@ -0,0 +1,22 @@
+// 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.
+
+#ifndef WIN8_METRO_DRIVER_IME_INPUT_SCOPE_H_
+#define WIN8_METRO_DRIVER_IME_INPUT_SCOPE_H_
+
+#include <InputScope.h>
+#include <vector>
+
+#include "base/win/scoped_comptr.h"
+
+namespace metro_driver {
+
+// Returns an instance of ITfInputScope object, which represents an array of
+// InputScope enumeration passed as |input_scopes|.
+base::win::ScopedComPtr<ITfInputScope>
+CreteInputScope(const std::vector<InputScope>& input_scopes);
+
+} // namespace metro_driver
+
+#endif // WIN8_METRO_DRIVER_IME_TEXT_STORE_H_
diff --git a/win8/metro_driver/ime/text_service.cc b/win8/metro_driver/ime/text_service.cc
new file mode 100644
index 0000000..ecb7ddf
--- /dev/null
+++ b/win8/metro_driver/ime/text_service.cc
@@ -0,0 +1,494 @@
+// 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 <msctf.h>
+
+#include "base/logging.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) --
+// -> 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 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 {
+typedef TSFTextStore TextStore;
+
+// 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(ITfThreadMgr2* thread_manager,
+ TfClientId client_id) {
+ base::win::ScopedComPtr<ITfCompartmentMgr> 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<ITfCompartment> 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);
+ 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<ITfCompartmentMgr> compartment_mgr;
+ HRESULT hr = compartment_mgr.QueryFrom(context);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "QueryFrom failed. hr = " << hr;
+ return false;
+ }
+
+ base::win::ScopedComPtr<ITfCompartment> 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);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
+ return false;
+ }
+
+ base::win::ScopedComPtr<ITfCompartment> 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<int32>(1));
+ hr = empty_context->SetValue(client_id, &empty_context_variant);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
+ return false;
+ }
+
+ return true;
+}
+
+bool IsPasswordField(const std::vector<InputScope>& 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<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) {
+ DCHECK(text_store);
+ base::win::ScopedComPtr<ITfSource> source;
+ DWORD cookie = TF_INVALID_EDIT_COOKIE;
+ HRESULT hr = source.QueryFrom(context);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "QueryFrom failed, hr = " << hr;
+ return scoped_ptr<EventSink>();
+ }
+ hr = source->AdviseSink(IID_ITfTextEditSink, text_store, &cookie);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "AdviseSink failed, hr = " << hr;
+ 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 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_)
+ return;
+ document_manager_->Pop(TF_POPF_ALL);
+ }
+
+ static scoped_ptr<DocumentBinding> Create(
+ ITfThreadMgr2* thread_manager,
+ TfClientId client_id,
+ const std::vector<InputScope>& input_scopes,
+ HWND window_handle,
+ TextStoreDelegate* delegate) {
+ base::win::ScopedComPtr<ITfDocumentMgr> document_manager;
+ HRESULT hr = thread_manager->CreateDocumentMgr(document_manager.Receive());
+ if (FAILED(hr)) {
+ LOG(ERROR) << "ITfThreadMgr2::CreateDocumentMgr failed. hr = " << hr;
+ return scoped_ptr<DocumentBinding>();
+ }
+
+ // 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<TextStore> text_store;
+ if (!use_null_text_store) {
+ text_store = TextStore::Create(window_handle, input_scopes, delegate);
+ if (!text_store) {
+ LOG(ERROR) << "Failed to create TextStore.";
+ return scoped_ptr<DocumentBinding>();
+ }
+ }
+
+ base::win::ScopedComPtr<ITfContext> context;
+ DWORD edit_cookie = TF_INVALID_EDIT_COOKIE;
+ hr = document_manager->CreateContext(
+ client_id,
+ 0,
+ static_cast<ITextStoreACP*>(text_store.get()),
+ context.Receive(),
+ &edit_cookie);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "ITfDocumentMgr::CreateContext failed. hr = " << hr;
+ return scoped_ptr<DocumentBinding>();
+ }
+
+ // 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, client_id)) {
+ LOG(ERROR) << "InitializeDisabledContext failed.";
+ return scoped_ptr<DocumentBinding>();
+ }
+
+ scoped_ptr<EventSink> text_edit_sink;
+ if (!use_null_text_store) {
+ text_edit_sink = CreateTextEditSink(context, text_store);
+ if (!text_edit_sink) {
+ LOG(ERROR) << "CreateTextEditSink failed.";
+ return scoped_ptr<DocumentBinding>();
+ }
+ }
+ hr = document_manager->Push(context);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "ITfDocumentMgr::Push failed. hr = " << hr;
+ return scoped_ptr<DocumentBinding>();
+ }
+ 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() const {
+ 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(ITfThreadMgr2* 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_);
+ }
+ virtual ~TextServiceImpl() {
+ thread_manager_->Deactivate();
+ }
+
+ private:
+ // TextService overrides:
+ virtual void TextService::CancelComposition() OVERRIDE {
+ if (!current_document_) {
+ VLOG(0) << "|current_document_| is NULL due to the previous error.";
+ return;
+ }
+ TextStore* text_store = current_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_) {
+ 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) << "ITfThreadMgr2::SetFocus failed. hr = " << hr;
+ 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) {
+ std::vector<InputScope> native_input_scopes(input_scopes.size());
+ for (size_t i = 0; i < input_scopes.size(); ++i)
+ native_input_scopes[i] = static_cast<InputScope>(input_scopes[i]);
+ scoped_ptr<DocumentBinding> 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<DocumentBinding> current_document_;
+ base::win::ScopedComPtr<ITfThreadMgr2> 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<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;
+ 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<TextService>();
+ }
+ TfClientId client_id = TF_CLIENTID_NULL;
+ hr = thread_manager->ActivateEx(&client_id, 0);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "ITfThreadMgr2::ActivateEx failed. hr = " << hr;
+ return scoped_ptr<TextService>();
+ }
+ if (!InitializeSentenceMode(thread_manager, client_id)) {
+ LOG(ERROR) << "InitializeSentenceMode failed.";
+ thread_manager->Deactivate();
+ return scoped_ptr<TextService>();
+ }
+ return scoped_ptr<TextService>(new TextServiceImpl(thread_manager,
+ client_id,
+ window_handle,
+ delegate));
+}
+
+} // namespace metro_driver
diff --git a/win8/metro_driver/ime/text_service.h b/win8/metro_driver/ime/text_service.h
new file mode 100644
index 0000000..6ddd21f
--- /dev/null
+++ b/win8/metro_driver/ime/text_service.h
@@ -0,0 +1,53 @@
+// 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.
+
+#ifndef WIN8_METRO_DRIVER_IME_TEXT_SERVICE_H_
+#define WIN8_METRO_DRIVER_IME_TEXT_SERVICE_H_
+
+#include <Windows.h>
+
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/memory/scoped_ptr.h"
+
+namespace metro_viewer {
+struct CharacterBounds;
+}
+
+namespace metro_driver {
+
+class TextServiceDelegate;
+
+// An interface to manage a virtual text store with which an IME communicates.
+class TextService {
+ public:
+ virtual ~TextService() {}
+
+ // Cancels on-going composition. Does nothing if there is no composition.
+ virtual void CancelComposition() = 0;
+
+ // Updates document type with |input_scopes| and caret/composition position
+ // with |character_bounds|. An empty |input_scopes| indicates that IMEs
+ // should be disabled until non-empty |input_scopes| is specified.
+ // Note: |input_scopes| is defined as std::vector<int32> here rather than
+ // std::vector<InputScope> because the wire format of IPC message
+ // MetroViewerHostMsg_ImeTextInputClientUpdated uses std::vector<int32> to
+ // avoid dependency on <InputScope.h> header.
+ virtual void OnDocumentChanged(
+ const std::vector<int32>& input_scopes,
+ const std::vector<metro_viewer::CharacterBounds>& character_bounds) = 0;
+
+ // Must be called whenever the attached window gains keyboard focus.
+ virtual void OnWindowActivated() = 0;
+};
+
+// Returns an instance of TextService that works together with
+// |text_store_delegate| as if it was an text area owned by |window_handle|.
+scoped_ptr<TextService>
+CreateTextService(TextServiceDelegate* delegate, HWND window_handle);
+
+} // namespace metro_driver
+
+#endif // WIN8_METRO_DRIVER_IME_TEXT_SERVICE_H_
diff --git a/win8/metro_driver/ime/text_service_delegate.h b/win8/metro_driver/ime/text_service_delegate.h
new file mode 100644
index 0000000..ea9cde7
--- /dev/null
+++ b/win8/metro_driver/ime/text_service_delegate.h
@@ -0,0 +1,40 @@
+// 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.
+
+#ifndef WIN8_METRO_DRIVER_IME_TEXT_SERVICE_DELEGATE_H_
+#define WIN8_METRO_DRIVER_IME_TEXT_SERVICE_DELEGATE_H_
+
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/strings/string16.h"
+
+namespace metro_viewer {
+struct UnderlineInfo;
+}
+
+namespace metro_driver {
+
+// A delegate which works together with virtual text service.
+// Objects that implement this delegate will receive notifications from a
+// virtual text service whenever an IME updates the composition or commits text.
+class TextServiceDelegate {
+ public:
+ virtual ~TextServiceDelegate() {}
+
+ // Called when on-going composition is updated. An empty |text| represents
+ // that the composition is canceled.
+ virtual void OnCompositionChanged(
+ const string16& text,
+ int32 selection_start,
+ int32 selection_end,
+ const std::vector<metro_viewer::UnderlineInfo>& underlines) = 0;
+
+ // Called when |text| is committed.
+ virtual void OnTextCommitted(const string16& text) = 0;
+};
+
+} // namespace metro_driver
+
+#endif // WIN8_METRO_DRIVER_IME_TEXT_SERVICE_DELEGATE_H_
diff --git a/win8/metro_driver/ime/text_store.cc b/win8/metro_driver/ime/text_store.cc
new file mode 100644
index 0000000..960af56
--- /dev/null
+++ b/win8/metro_driver/ime/text_store.cc
@@ -0,0 +1,913 @@
+// 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.
+
+#define INITGUID // required for GUID_PROP_INPUTSCOPE
+#include "win8/metro_driver/ime/text_store.h"
+
+#include <InputScope.h>
+#include <OleCtl.h>
+
+#include <algorithm>
+
+#include "base/win/scoped_variant.h"
+#include "win8/metro_driver/ime/input_scope.h"
+#include "win8/metro_driver/ime/text_store_delegate.h"
+
+namespace metro_driver {
+namespace {
+
+// We support only one view.
+const TsViewCookie kViewCookie = 1;
+
+} // namespace
+
+TSFTextStore::TSFTextStore(HWND window_handle,
+ ITfCategoryMgr* category_manager,
+ ITfDisplayAttributeMgr* display_attribute_manager,
+ ITfInputScope* input_scope,
+ TextStoreDelegate* delegate)
+ : ref_count_(0),
+ text_store_acp_sink_mask_(0),
+ window_handle_(window_handle),
+ delegate_(delegate),
+ committed_size_(0),
+ selection_start_(0),
+ selection_end_(0),
+ edit_flag_(false),
+ current_lock_type_(0),
+ category_manager_(category_manager),
+ display_attribute_manager_(display_attribute_manager),
+ input_scope_(input_scope) {
+}
+
+// static
+scoped_refptr<TSFTextStore> TSFTextStore::Create(
+ HWND window_handle,
+ const std::vector<InputScope>& input_scopes,
+ TextStoreDelegate* delegate) {
+ if (!delegate) {
+ LOG(ERROR) << "|delegate| must be non-NULL.";
+ return scoped_refptr<TSFTextStore>();
+ }
+ base::win::ScopedComPtr<ITfCategoryMgr> category_manager;
+ HRESULT hr = category_manager.CreateInstance(CLSID_TF_CategoryMgr);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "Failed to initialize CategoryMgr. hr = " << hr;
+ return scoped_refptr<TSFTextStore>();
+ }
+ base::win::ScopedComPtr<ITfDisplayAttributeMgr> display_attribute_manager;
+ hr = display_attribute_manager.CreateInstance(CLSID_TF_DisplayAttributeMgr);
+ if (FAILED(hr)) {
+ LOG(ERROR) << "Failed to initialize DisplayAttributeMgr. hr = " << hr;
+ return scoped_refptr<TSFTextStore>();
+ }
+ base::win::ScopedComPtr<ITfInputScope> input_scope =
+ CreteInputScope(input_scopes);
+ if (!input_scope) {
+ LOG(ERROR) << "Failed to initialize InputScope.";
+ return scoped_refptr<TSFTextStore>();
+ }
+ return scoped_refptr<TSFTextStore>(
+ new TSFTextStore(window_handle,
+ category_manager,
+ display_attribute_manager,
+ input_scope,
+ delegate));
+}
+
+TSFTextStore::~TSFTextStore() {
+}
+
+ULONG STDMETHODCALLTYPE TSFTextStore::AddRef() {
+ return InterlockedIncrement(&ref_count_);
+}
+
+ULONG STDMETHODCALLTYPE TSFTextStore::Release() {
+ const LONG count = InterlockedDecrement(&ref_count_);
+ if (!count) {
+ delete this;
+ return 0;
+ }
+ return static_cast<ULONG>(count);
+}
+
+STDMETHODIMP TSFTextStore::QueryInterface(REFIID iid, void** result) {
+ if (iid == IID_IUnknown || iid == IID_ITextStoreACP) {
+ *result = static_cast<ITextStoreACP*>(this);
+ } else if (iid == IID_ITfContextOwnerCompositionSink) {
+ *result = static_cast<ITfContextOwnerCompositionSink*>(this);
+ } else if (iid == IID_ITfTextEditSink) {
+ *result = static_cast<ITfTextEditSink*>(this);
+ } else {
+ *result = NULL;
+ return E_NOINTERFACE;
+ }
+ AddRef();
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::AdviseSink(REFIID iid,
+ IUnknown* unknown,
+ DWORD mask) {
+ if (!IsEqualGUID(iid, IID_ITextStoreACPSink))
+ return E_INVALIDARG;
+ if (text_store_acp_sink_) {
+ if (text_store_acp_sink_.IsSameObject(unknown)) {
+ text_store_acp_sink_mask_ = mask;
+ return S_OK;
+ } else {
+ return CONNECT_E_ADVISELIMIT;
+ }
+ }
+ if (FAILED(text_store_acp_sink_.QueryFrom(unknown)))
+ return E_UNEXPECTED;
+ text_store_acp_sink_mask_ = mask;
+
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::FindNextAttrTransition(
+ LONG acp_start,
+ LONG acp_halt,
+ ULONG num_filter_attributes,
+ const TS_ATTRID* filter_attributes,
+ DWORD flags,
+ LONG* acp_next,
+ BOOL* found,
+ LONG* found_offset) {
+ if (!acp_next || !found || !found_offset)
+ return E_INVALIDARG;
+ // We don't support any attributes.
+ // So we always return "not found".
+ *acp_next = 0;
+ *found = FALSE;
+ *found_offset = 0;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::GetACPFromPoint(TsViewCookie view_cookie,
+ const POINT* point,
+ DWORD flags,
+ LONG* acp) {
+ NOTIMPLEMENTED();
+ if (view_cookie != kViewCookie)
+ return E_INVALIDARG;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP TSFTextStore::GetActiveView(TsViewCookie* view_cookie) {
+ if (!view_cookie)
+ return E_INVALIDARG;
+ // We support only one view.
+ *view_cookie = kViewCookie;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::GetEmbedded(LONG acp_pos,
+ REFGUID service,
+ REFIID iid,
+ IUnknown** unknown) {
+ // We don't support any embedded objects.
+ NOTIMPLEMENTED();
+ if (!unknown)
+ return E_INVALIDARG;
+ *unknown = NULL;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP TSFTextStore::GetEndACP(LONG* acp) {
+ if (!acp)
+ return E_INVALIDARG;
+ if (!HasReadLock())
+ return TS_E_NOLOCK;
+ *acp = static_cast<LONG>(string_buffer_.size());
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::GetFormattedText(LONG acp_start, LONG acp_end,
+ IDataObject** data_object) {
+ NOTIMPLEMENTED();
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP TSFTextStore::GetScreenExt(TsViewCookie view_cookie, RECT* rect) {
+ if (view_cookie != kViewCookie)
+ return E_INVALIDARG;
+ if (!rect)
+ return E_INVALIDARG;
+
+ // {0, 0, 0, 0} means that the document rect is not currently displayed.
+ SetRect(rect, 0, 0, 0, 0);
+
+ RECT client_rect = {};
+ if (!GetClientRect(window_handle_, &client_rect))
+ return E_FAIL;
+ POINT left_top = {client_rect.left, client_rect.top};
+ POINT right_bottom = {client_rect.right, client_rect.bottom};
+ if (!ClientToScreen(window_handle_, &left_top))
+ return E_FAIL;
+ if (!ClientToScreen(window_handle_, &right_bottom))
+ return E_FAIL;
+
+ rect->left = left_top.x;
+ rect->top = left_top.y;
+ rect->right = right_bottom.x;
+ rect->bottom = right_bottom.y;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::GetSelection(ULONG selection_index,
+ ULONG selection_buffer_size,
+ TS_SELECTION_ACP* selection_buffer,
+ ULONG* fetched_count) {
+ if (!selection_buffer)
+ return E_INVALIDARG;
+ if (!fetched_count)
+ return E_INVALIDARG;
+ if (!HasReadLock())
+ return TS_E_NOLOCK;
+ *fetched_count = 0;
+ if ((selection_buffer_size > 0) &&
+ ((selection_index == 0) || (selection_index == TS_DEFAULT_SELECTION))) {
+ selection_buffer[0].acpStart = selection_start_;
+ selection_buffer[0].acpEnd = selection_end_;
+ selection_buffer[0].style.ase = TS_AE_END;
+ selection_buffer[0].style.fInterimChar = FALSE;
+ *fetched_count = 1;
+ }
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::GetStatus(TS_STATUS* status) {
+ if (!status)
+ return E_INVALIDARG;
+
+ status->dwDynamicFlags = 0;
+ // We use transitory contexts and we don't support hidden text.
+ status->dwStaticFlags = TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT;
+
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::GetText(LONG acp_start,
+ LONG acp_end,
+ wchar_t* text_buffer,
+ ULONG text_buffer_size,
+ ULONG* text_buffer_copied,
+ TS_RUNINFO* run_info_buffer,
+ ULONG run_info_buffer_size,
+ ULONG* run_info_buffer_copied,
+ LONG* next_acp) {
+ if (!text_buffer_copied || !run_info_buffer_copied)
+ return E_INVALIDARG;
+ if (!text_buffer && text_buffer_size != 0)
+ return E_INVALIDARG;
+ if (!run_info_buffer && run_info_buffer_size != 0)
+ return E_INVALIDARG;
+ if (!next_acp)
+ return E_INVALIDARG;
+ if (!HasReadLock())
+ return TF_E_NOLOCK;
+ const LONG string_buffer_size = static_cast<LONG>(string_buffer_.size());
+ if (acp_end == -1)
+ acp_end = string_buffer_size;
+ if (!((0 <= acp_start) &&
+ (acp_start <= acp_end) &&
+ (acp_end <= string_buffer_size))) {
+ return TF_E_INVALIDPOS;
+ }
+ acp_end = std::min(acp_end, acp_start + static_cast<LONG>(text_buffer_size));
+ *text_buffer_copied = acp_end - acp_start;
+
+ const string16& result =
+ string_buffer_.substr(acp_start, *text_buffer_copied);
+ for (size_t i = 0; i < result.size(); ++i) {
+ text_buffer[i] = result[i];
+ }
+
+ if (run_info_buffer_size) {
+ run_info_buffer[0].uCount = *text_buffer_copied;
+ run_info_buffer[0].type = TS_RT_PLAIN;
+ *run_info_buffer_copied = 1;
+ }
+
+ *next_acp = acp_end;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::GetTextExt(TsViewCookie view_cookie,
+ LONG acp_start,
+ LONG acp_end,
+ RECT* rect,
+ BOOL* clipped) {
+ if (!rect || !clipped)
+ return E_INVALIDARG;
+ if (view_cookie != kViewCookie)
+ return E_INVALIDARG;
+ if (!HasReadLock())
+ return TS_E_NOLOCK;
+ if (!((static_cast<LONG>(committed_size_) <= acp_start) &&
+ (acp_start <= acp_end) &&
+ (acp_end <= static_cast<LONG>(string_buffer_.size())))) {
+ return TS_E_INVALIDPOS;
+ }
+
+ // According to a behavior of notepad.exe and wordpad.exe, top left corner of
+ // rect indicates a first character's one, and bottom right corner of rect
+ // indicates a last character's one.
+ // We use RECT instead of gfx::Rect since left position may be bigger than
+ // right position when composition has multiple lines.
+ RECT result;
+ RECT tmp_rect;
+ const uint32 start_pos = acp_start - committed_size_;
+ const uint32 end_pos = acp_end - committed_size_;
+
+ if (start_pos == end_pos) {
+ // According to MSDN document, if |acp_start| and |acp_end| are equal it is
+ // OK to just return E_INVALIDARG.
+ // http://msdn.microsoft.com/en-us/library/ms538435
+ // But when using Pinin IME of Windows 8, this method is called with the
+ // equal values of |acp_start| and |acp_end|. So we handle this condition.
+ if (start_pos == 0) {
+ if (delegate_->GetCompositionCharacterBounds(0, &tmp_rect)) {
+ tmp_rect.right = tmp_rect.right;
+ result = tmp_rect;
+ } else if (string_buffer_.size() == committed_size_) {
+ result = delegate_->GetCaretBounds();
+ } else {
+ return TS_E_NOLAYOUT;
+ }
+ } else if (delegate_->GetCompositionCharacterBounds(start_pos - 1,
+ &tmp_rect)) {
+ tmp_rect.left = tmp_rect.right;
+ result = tmp_rect;
+ } else {
+ return TS_E_NOLAYOUT;
+ }
+ } else {
+ if (delegate_->GetCompositionCharacterBounds(start_pos, &tmp_rect)) {
+ result = tmp_rect;
+ if (delegate_->GetCompositionCharacterBounds(end_pos - 1, &tmp_rect)) {
+ result.right = tmp_rect.right;
+ result.bottom = tmp_rect.bottom;
+ } else {
+ // We may not be able to get the last character bounds, so we use the
+ // first character bounds instead of returning TS_E_NOLAYOUT.
+ }
+ } else {
+ // Hack for PPAPI flash. PPAPI flash does not support GetCaretBounds, so
+ // it's better to return previous caret rectangle instead.
+ // TODO(nona, kinaba): Remove this hack.
+ if (start_pos == 0) {
+ result = delegate_->GetCaretBounds();
+ } else {
+ return TS_E_NOLAYOUT;
+ }
+ }
+ }
+
+ *rect = result;
+ *clipped = FALSE;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::GetWnd(TsViewCookie view_cookie,
+ HWND* window_handle) {
+ if (!window_handle)
+ return E_INVALIDARG;
+ if (view_cookie != kViewCookie)
+ return E_INVALIDARG;
+ *window_handle = window_handle_;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::InsertEmbedded(DWORD flags,
+ LONG acp_start,
+ LONG acp_end,
+ IDataObject* data_object,
+ TS_TEXTCHANGE* change) {
+ // We don't support any embedded objects.
+ NOTIMPLEMENTED();
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP TSFTextStore::InsertEmbeddedAtSelection(DWORD flags,
+ IDataObject* data_object,
+ LONG* acp_start,
+ LONG* acp_end,
+ TS_TEXTCHANGE* change) {
+ // We don't support any embedded objects.
+ NOTIMPLEMENTED();
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP TSFTextStore::InsertTextAtSelection(DWORD flags,
+ const wchar_t* text_buffer,
+ ULONG text_buffer_size,
+ LONG* acp_start,
+ LONG* acp_end,
+ TS_TEXTCHANGE* text_change) {
+ const LONG start_pos = selection_start_;
+ const LONG end_pos = selection_end_;
+ const LONG new_end_pos = start_pos + text_buffer_size;
+
+ if (flags & TS_IAS_QUERYONLY) {
+ if (!HasReadLock())
+ return TS_E_NOLOCK;
+ if (acp_start)
+ *acp_start = start_pos;
+ if (acp_end) {
+ *acp_end = end_pos;
+ }
+ return S_OK;
+ }
+
+ if (!HasReadWriteLock())
+ return TS_E_NOLOCK;
+ if (!text_buffer)
+ return E_INVALIDARG;
+
+ DCHECK_LE(start_pos, end_pos);
+ string_buffer_ = string_buffer_.substr(0, start_pos) +
+ string16(text_buffer, text_buffer + text_buffer_size) +
+ string_buffer_.substr(end_pos);
+ if (acp_start)
+ *acp_start = start_pos;
+ if (acp_end)
+ *acp_end = new_end_pos;
+ if (text_change) {
+ text_change->acpStart = start_pos;
+ text_change->acpOldEnd = end_pos;
+ text_change->acpNewEnd = new_end_pos;
+ }
+ selection_start_ = start_pos;
+ selection_end_ = new_end_pos;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::QueryInsert(
+ LONG acp_test_start,
+ LONG acp_test_end,
+ ULONG text_size,
+ LONG* acp_result_start,
+ LONG* acp_result_end) {
+ if (!acp_result_start || !acp_result_end || acp_test_start > acp_test_end)
+ return E_INVALIDARG;
+ const LONG committed_size = static_cast<LONG>(committed_size_);
+ const LONG buffer_size = static_cast<LONG>(string_buffer_.size());
+ *acp_result_start = std::min(std::max(committed_size, acp_test_start),
+ buffer_size);
+ *acp_result_end = std::min(std::max(committed_size, acp_test_end),
+ buffer_size);
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::QueryInsertEmbedded(const GUID* service,
+ const FORMATETC* format,
+ BOOL* insertable) {
+ if (!format)
+ return E_INVALIDARG;
+ // We don't support any embedded objects.
+ if (insertable)
+ *insertable = FALSE;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::RequestAttrsAtPosition(
+ LONG acp_pos,
+ ULONG attribute_buffer_size,
+ const TS_ATTRID* attribute_buffer,
+ DWORD flags) {
+ // We don't support any document attributes.
+ // This method just returns S_OK, and the subsequently called
+ // RetrieveRequestedAttrs() returns 0 as the number of supported attributes.
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::RequestAttrsTransitioningAtPosition(
+ LONG acp_pos,
+ ULONG attribute_buffer_size,
+ const TS_ATTRID* attribute_buffer,
+ DWORD flags) {
+ // We don't support any document attributes.
+ // This method just returns S_OK, and the subsequently called
+ // RetrieveRequestedAttrs() returns 0 as the number of supported attributes.
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::RequestLock(DWORD lock_flags, HRESULT* result) {
+ if (!text_store_acp_sink_.get())
+ return E_FAIL;
+ if (!result)
+ return E_INVALIDARG;
+
+ if (current_lock_type_ != 0) {
+ if (lock_flags & TS_LF_SYNC) {
+ // Can't lock synchronously.
+ *result = TS_E_SYNCHRONOUS;
+ return S_OK;
+ }
+ // Queue the lock request.
+ lock_queue_.push_back(lock_flags & TS_LF_READWRITE);
+ *result = TS_S_ASYNC;
+ return S_OK;
+ }
+
+ // Lock
+ current_lock_type_ = (lock_flags & TS_LF_READWRITE);
+
+ edit_flag_ = false;
+ const uint32 last_committed_size = committed_size_;
+
+ // Grant the lock.
+ *result = text_store_acp_sink_->OnLockGranted(current_lock_type_);
+
+ // Unlock
+ current_lock_type_ = 0;
+
+ // Handles the pending lock requests.
+ while (!lock_queue_.empty()) {
+ current_lock_type_ = lock_queue_.front();
+ lock_queue_.pop_front();
+ text_store_acp_sink_->OnLockGranted(current_lock_type_);
+ current_lock_type_ = 0;
+ }
+
+ if (!edit_flag_)
+ return S_OK;
+
+ // If the text store is edited in OnLockGranted(), we may need to call
+ // TextStoreDelegate::ConfirmComposition() or
+ // TextStoreDelegate::SetComposition().
+ const uint32 new_committed_size = committed_size_;
+ const string16& new_committed_string =
+ string_buffer_.substr(last_committed_size,
+ new_committed_size - last_committed_size);
+ const string16& composition_string =
+ string_buffer_.substr(new_committed_size);
+
+ // If there is new committed string, calls
+ // TextStoreDelegate::ConfirmComposition().
+ if ((!new_committed_string.empty()))
+ delegate_->OnTextCommitted(new_committed_string);
+
+ // Calls TextInputClient::SetCompositionText().
+ std::vector<metro_viewer::UnderlineInfo> underlines = underlines_;
+ // Adjusts the offset.
+ for (size_t i = 0; i < underlines_.size(); ++i) {
+ underlines[i].start_offset -= new_committed_size;
+ underlines[i].end_offset -= new_committed_size;
+ }
+ int32 selection_start = 0;
+ int32 selection_end = 0;
+ if (selection_start_ >= new_committed_size)
+ selection_start = selection_start_ - new_committed_size;
+ if (selection_end_ >= new_committed_size)
+ selection_end = selection_end_ - new_committed_size;
+ delegate_->OnCompositionChanged(
+ composition_string, selection_start, selection_end, underlines);
+
+ // If there is no composition string, clear the text store status.
+ // And call OnSelectionChange(), OnLayoutChange(), and OnTextChange().
+ if ((composition_string.empty()) && (new_committed_size != 0)) {
+ string_buffer_.clear();
+ committed_size_ = 0;
+ selection_start_ = 0;
+ selection_end_ = 0;
+ if (text_store_acp_sink_mask_ & TS_AS_SEL_CHANGE)
+ text_store_acp_sink_->OnSelectionChange();
+ if (text_store_acp_sink_mask_ & TS_AS_LAYOUT_CHANGE)
+ text_store_acp_sink_->OnLayoutChange(TS_LC_CHANGE, 0);
+ if (text_store_acp_sink_mask_ & TS_AS_TEXT_CHANGE) {
+ TS_TEXTCHANGE textChange;
+ textChange.acpStart = 0;
+ textChange.acpOldEnd = new_committed_size;
+ textChange.acpNewEnd = 0;
+ text_store_acp_sink_->OnTextChange(0, &textChange);
+ }
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::RequestSupportedAttrs(
+ DWORD /* flags */, // Seems that we should ignore this.
+ ULONG attribute_buffer_size,
+ const TS_ATTRID* attribute_buffer) {
+ if (!attribute_buffer)
+ return E_INVALIDARG;
+ if (!input_scope_)
+ return E_FAIL;
+ // We support only input scope attribute.
+ for (size_t i = 0; i < attribute_buffer_size; ++i) {
+ if (IsEqualGUID(GUID_PROP_INPUTSCOPE, attribute_buffer[i]))
+ return S_OK;
+ }
+ return E_FAIL;
+}
+
+STDMETHODIMP TSFTextStore::RetrieveRequestedAttrs(
+ ULONG attribute_buffer_size,
+ TS_ATTRVAL* attribute_buffer,
+ ULONG* attribute_buffer_copied) {
+ if (!attribute_buffer_copied)
+ return E_INVALIDARG;
+ *attribute_buffer_copied = 0;
+ if (!attribute_buffer)
+ return E_INVALIDARG;
+ if (!input_scope_)
+ return E_UNEXPECTED;
+ // We support only input scope attribute.
+ *attribute_buffer_copied = 0;
+ if (attribute_buffer_size == 0)
+ return S_OK;
+
+ attribute_buffer[0].dwOverlapId = 0;
+ attribute_buffer[0].idAttr = GUID_PROP_INPUTSCOPE;
+ attribute_buffer[0].varValue.vt = VT_UNKNOWN;
+ attribute_buffer[0].varValue.punkVal = input_scope_.get();
+ attribute_buffer[0].varValue.punkVal->AddRef();
+ *attribute_buffer_copied = 1;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::SetSelection(
+ ULONG selection_buffer_size,
+ const TS_SELECTION_ACP* selection_buffer) {
+ if (!HasReadWriteLock())
+ return TF_E_NOLOCK;
+ if (selection_buffer_size > 0) {
+ const LONG start_pos = selection_buffer[0].acpStart;
+ const LONG end_pos = selection_buffer[0].acpEnd;
+ if (!((static_cast<LONG>(committed_size_) <= start_pos) &&
+ (start_pos <= end_pos) &&
+ (end_pos <= static_cast<LONG>(string_buffer_.size())))) {
+ return TF_E_INVALIDPOS;
+ }
+ selection_start_ = start_pos;
+ selection_end_ = end_pos;
+ }
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::SetText(DWORD flags,
+ LONG acp_start,
+ LONG acp_end,
+ const wchar_t* text_buffer,
+ ULONG text_buffer_size,
+ TS_TEXTCHANGE* text_change) {
+ if (!HasReadWriteLock())
+ return TS_E_NOLOCK;
+ if (!((static_cast<LONG>(committed_size_) <= acp_start) &&
+ (acp_start <= acp_end) &&
+ (acp_end <= static_cast<LONG>(string_buffer_.size())))) {
+ return TS_E_INVALIDPOS;
+ }
+
+ TS_SELECTION_ACP selection;
+ selection.acpStart = acp_start;
+ selection.acpEnd = acp_end;
+ selection.style.ase = TS_AE_NONE;
+ selection.style.fInterimChar = 0;
+
+ HRESULT ret;
+ ret = SetSelection(1, &selection);
+ if (ret != S_OK)
+ return ret;
+
+ TS_TEXTCHANGE change;
+ ret = InsertTextAtSelection(0, text_buffer, text_buffer_size,
+ &acp_start, &acp_end, &change);
+ if (ret != S_OK)
+ return ret;
+
+ if (text_change)
+ *text_change = change;
+
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::UnadviseSink(IUnknown* unknown) {
+ if (!text_store_acp_sink_.IsSameObject(unknown))
+ return CONNECT_E_NOCONNECTION;
+ text_store_acp_sink_.Release();
+ text_store_acp_sink_mask_ = 0;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::OnStartComposition(
+ ITfCompositionView* composition_view,
+ BOOL* ok) {
+ if (ok)
+ *ok = TRUE;
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::OnUpdateComposition(
+ ITfCompositionView* composition_view,
+ ITfRange* range) {
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::OnEndComposition(
+ ITfCompositionView* composition_view) {
+ return S_OK;
+}
+
+STDMETHODIMP TSFTextStore::OnEndEdit(ITfContext* context,
+ TfEditCookie read_only_edit_cookie,
+ ITfEditRecord* edit_record) {
+ if (!context || !edit_record)
+ return E_INVALIDARG;
+ if (!GetCompositionStatus(context, read_only_edit_cookie, &committed_size_,
+ &underlines_)) {
+ return S_OK;
+ }
+ edit_flag_ = true;
+ return S_OK;
+}
+
+bool TSFTextStore::GetDisplayAttribute(TfGuidAtom guid_atom,
+ TF_DISPLAYATTRIBUTE* attribute) {
+ GUID guid;
+ if (FAILED(category_manager_->GetGUID(guid_atom, &guid)))
+ return false;
+
+ base::win::ScopedComPtr<ITfDisplayAttributeInfo> display_attribute_info;
+ if (FAILED(display_attribute_manager_->GetDisplayAttributeInfo(
+ guid, display_attribute_info.Receive(), NULL))) {
+ return false;
+ }
+ return SUCCEEDED(display_attribute_info->GetAttributeInfo(attribute));
+}
+
+bool TSFTextStore::GetCompositionStatus(
+ ITfContext* context,
+ const TfEditCookie read_only_edit_cookie,
+ uint32* committed_size,
+ std::vector<metro_viewer::UnderlineInfo>* undelines) {
+ DCHECK(context);
+ DCHECK(committed_size);
+ DCHECK(undelines);
+ const GUID* rgGuids[2] = {&GUID_PROP_COMPOSING, &GUID_PROP_ATTRIBUTE};
+ base::win::ScopedComPtr<ITfReadOnlyProperty> track_property;
+ if (FAILED(context->TrackProperties(rgGuids, 2, NULL, 0,
+ track_property.Receive()))) {
+ return false;
+ }
+
+ *committed_size = 0;
+ undelines->clear();
+ base::win::ScopedComPtr<ITfRange> start_to_end_range;
+ base::win::ScopedComPtr<ITfRange> end_range;
+ if (FAILED(context->GetStart(read_only_edit_cookie,
+ start_to_end_range.Receive()))) {
+ return false;
+ }
+ if (FAILED(context->GetEnd(read_only_edit_cookie, end_range.Receive())))
+ return false;
+ if (FAILED(start_to_end_range->ShiftEndToRange(read_only_edit_cookie,
+ end_range, TF_ANCHOR_END))) {
+ return false;
+ }
+
+ base::win::ScopedComPtr<IEnumTfRanges> ranges;
+ if (FAILED(track_property->EnumRanges(read_only_edit_cookie, ranges.Receive(),
+ start_to_end_range))) {
+ return false;
+ }
+
+ while (true) {
+ base::win::ScopedComPtr<ITfRange> range;
+ if (ranges->Next(1, range.Receive(), NULL) != S_OK)
+ break;
+ base::win::ScopedVariant value;
+ base::win::ScopedComPtr<IEnumTfPropertyValue> enum_prop_value;
+ if (FAILED(track_property->GetValue(read_only_edit_cookie, range,
+ value.Receive()))) {
+ return false;
+ }
+ if (FAILED(enum_prop_value.QueryFrom(value.AsInput()->punkVal)))
+ return false;
+
+ TF_PROPERTYVAL property_value;
+ bool is_composition = false;
+ bool has_display_attribute = false;
+ TF_DISPLAYATTRIBUTE display_attribute;
+ while (enum_prop_value->Next(1, &property_value, NULL) == S_OK) {
+ if (IsEqualGUID(property_value.guidId, GUID_PROP_COMPOSING)) {
+ is_composition = (property_value.varValue.lVal == TRUE);
+ } else if (IsEqualGUID(property_value.guidId, GUID_PROP_ATTRIBUTE)) {
+ TfGuidAtom guid_atom =
+ static_cast<TfGuidAtom>(property_value.varValue.lVal);
+ if (GetDisplayAttribute(guid_atom, &display_attribute))
+ has_display_attribute = true;
+ }
+ VariantClear(&property_value.varValue);
+ }
+
+ base::win::ScopedComPtr<ITfRangeACP> range_acp;
+ range_acp.QueryFrom(range);
+ LONG start_pos, length;
+ range_acp->GetExtent(&start_pos, &length);
+ if (!is_composition) {
+ if (*committed_size < static_cast<size_t>(start_pos + length))
+ *committed_size = start_pos + length;
+ } else {
+ metro_viewer::UnderlineInfo underline;
+ underline.start_offset = start_pos;
+ underline.end_offset = start_pos + length;
+ underline.thick = !!display_attribute.fBoldLine;
+ undelines->push_back(underline);
+ }
+ }
+ return true;
+}
+
+bool TSFTextStore::CancelComposition() {
+ // If there is an on-going document lock, we must not edit the text.
+ if (edit_flag_)
+ return false;
+
+ if (string_buffer_.empty())
+ return true;
+
+ // Unlike ImmNotifyIME(NI_COMPOSITIONSTR, CPS_CANCEL, 0) in IMM32, TSF does
+ // not have a dedicated method to cancel composition. However, CUAS actually
+ // has a protocol conversion from CPS_CANCEL into TSF operations. According
+ // to the observations on Windows 7, TIPs are expected to cancel composition
+ // when an on-going composition text is replaced with an empty string. So
+ // we use the same operation to cancel composition here to minimize the risk
+ // of potential compatibility issues.
+
+ const uint32 previous_buffer_size =
+ static_cast<uint32>(string_buffer_.size());
+ string_buffer_.clear();
+ committed_size_ = 0;
+ selection_start_ = 0;
+ selection_end_ = 0;
+ if (text_store_acp_sink_mask_ & TS_AS_SEL_CHANGE)
+ text_store_acp_sink_->OnSelectionChange();
+ if (text_store_acp_sink_mask_ & TS_AS_LAYOUT_CHANGE)
+ text_store_acp_sink_->OnLayoutChange(TS_LC_CHANGE, 0);
+ if (text_store_acp_sink_mask_ & TS_AS_TEXT_CHANGE) {
+ TS_TEXTCHANGE textChange = {};
+ textChange.acpStart = 0;
+ textChange.acpOldEnd = previous_buffer_size;
+ textChange.acpNewEnd = 0;
+ text_store_acp_sink_->OnTextChange(0, &textChange);
+ }
+ return true;
+}
+
+bool TSFTextStore::ConfirmComposition() {
+ // If there is an on-going document lock, we must not edit the text.
+ if (edit_flag_)
+ return false;
+
+ if (string_buffer_.empty())
+ return true;
+
+ // See the comment in TSFTextStore::CancelComposition.
+ // This logic is based on the observation about how to emulate
+ // ImmNotifyIME(NI_COMPOSITIONSTR, CPS_COMPLETE, 0) by CUAS.
+
+ const string16& composition_text = string_buffer_.substr(committed_size_);
+ if (!composition_text.empty())
+ delegate_->OnTextCommitted(composition_text);
+
+ const uint32 previous_buffer_size =
+ static_cast<uint32>(string_buffer_.size());
+ string_buffer_.clear();
+ committed_size_ = 0;
+ selection_start_ = 0;
+ selection_end_ = 0;
+ if (text_store_acp_sink_mask_ & TS_AS_SEL_CHANGE)
+ text_store_acp_sink_->OnSelectionChange();
+ if (text_store_acp_sink_mask_ & TS_AS_LAYOUT_CHANGE)
+ text_store_acp_sink_->OnLayoutChange(TS_LC_CHANGE, 0);
+ if (text_store_acp_sink_mask_ & TS_AS_TEXT_CHANGE) {
+ TS_TEXTCHANGE textChange = {};
+ textChange.acpStart = 0;
+ textChange.acpOldEnd = previous_buffer_size;
+ textChange.acpNewEnd = 0;
+ text_store_acp_sink_->OnTextChange(0, &textChange);
+ }
+ return true;
+}
+
+void TSFTextStore::SendOnLayoutChange() {
+ if (text_store_acp_sink_ && (text_store_acp_sink_mask_ & TS_AS_LAYOUT_CHANGE))
+ text_store_acp_sink_->OnLayoutChange(TS_LC_CHANGE, 0);
+}
+
+bool TSFTextStore::HasReadLock() const {
+ return (current_lock_type_ & TS_LF_READ) == TS_LF_READ;
+}
+
+bool TSFTextStore::HasReadWriteLock() const {
+ return (current_lock_type_ & TS_LF_READWRITE) == TS_LF_READWRITE;
+}
+
+} // namespace metro_driver
diff --git a/win8/metro_driver/ime/text_store.h b/win8/metro_driver/ime/text_store.h
new file mode 100644
index 0000000..3a308cf
--- /dev/null
+++ b/win8/metro_driver/ime/text_store.h
@@ -0,0 +1,308 @@
+// 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.
+
+#ifndef WIN8_METRO_DRIVER_IME_TEXT_STORE_H_
+#define WIN8_METRO_DRIVER_IME_TEXT_STORE_H_
+
+#include <inputscope.h>
+#include <msctf.h>
+
+#include <deque>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/compiler_specific.h"
+#include "base/memory/ref_counted.h"
+#include "base/strings/string16.h"
+#include "base/win/scoped_comptr.h"
+#include "ui/metro_viewer/ime_types.h"
+
+namespace metro_driver {
+
+class TextStoreDelegate;
+
+// TSFTextStore is used to interact with the input method via TSF manager.
+// TSFTextStore have a string buffer which is manipulated by TSF manager through
+// ITextStoreACP interface methods such as SetText().
+// When the input method updates the composition, TSFTextStore calls
+// TextInputClient::SetCompositionText(). And when the input method finishes the
+// composition, TSFTextStore calls TextInputClient::InsertText() and clears the
+// buffer.
+//
+// How TSFTextStore works:
+// - The user enters "a".
+// - The input method set composition as "a".
+// - TSF manager calls TSFTextStore::RequestLock().
+// - TSFTextStore callbacks ITextStoreACPSink::OnLockGranted().
+// - In OnLockGranted(), TSF manager calls
+// - TSFTextStore::OnStartComposition()
+// - TSFTextStore::SetText()
+// The string buffer is set as "a".
+// - TSFTextStore::OnUpdateComposition()
+// - TSFTextStore::OnEndEdit()
+// TSFTextStore can get the composition information such as underlines.
+// - TSFTextStore calls TextInputClient::SetCompositionText().
+// "a" is shown with an underline as composition string.
+// - The user enters <space>.
+// - The input method set composition as "A".
+// - TSF manager calls TSFTextStore::RequestLock().
+// - TSFTextStore callbacks ITextStoreACPSink::OnLockGranted().
+// - In OnLockGranted(), TSF manager calls
+// - TSFTextStore::SetText()
+// The string buffer is set as "A".
+// - TSFTextStore::OnUpdateComposition()
+// - TSFTextStore::OnEndEdit()
+// - TSFTextStore calls TextInputClient::SetCompositionText().
+// "A" is shown with an underline as composition string.
+// - The user enters <enter>.
+// - The input method commits "A".
+// - TSF manager calls TSFTextStore::RequestLock().
+// - TSFTextStore callbacks ITextStoreACPSink::OnLockGranted().
+// - In OnLockGranted(), TSF manager calls
+// - TSFTextStore::OnEndComposition()
+// - TSFTextStore::OnEndEdit()
+// TSFTextStore knows "A" is committed.
+// - TSFTextStore calls TextInputClient::InsertText().
+// "A" is shown as committed string.
+// - TSFTextStore clears the string buffer.
+// - TSFTextStore calls OnSelectionChange(), OnLayoutChange() and
+// OnTextChange() of ITextStoreACPSink to let TSF manager know that the
+// string buffer has been changed.
+//
+// About the locking scheme:
+// When TSF manager manipulates the string buffer it calls RequestLock() to get
+// the lock of the document. If TSFTextStore can grant the lock request, it
+// callbacks ITextStoreACPSink::OnLockGranted().
+// RequestLock() is called from only one thread, but called recursively in
+// OnLockGranted() or OnSelectionChange() or OnLayoutChange() or OnTextChange().
+// If the document is locked and the lock request is asynchronous, TSFTextStore
+// queues the request. The queued requests will be handled after the current
+// lock is removed.
+// More information about document locks can be found here:
+// http://msdn.microsoft.com/en-us/library/ms538064
+//
+// More information about TSF can be found here:
+// http://msdn.microsoft.com/en-us/library/ms629032
+// TODO(yukawa): Rename TSFTextStore to TextStore.
+class TSFTextStore : public ITextStoreACP,
+ public ITfContextOwnerCompositionSink,
+ public ITfTextEditSink {
+ public:
+ virtual ~TSFTextStore();
+
+ // ITextStoreACP:
+ STDMETHOD_(ULONG, AddRef)() OVERRIDE;
+ STDMETHOD_(ULONG, Release)() OVERRIDE;
+ STDMETHOD(QueryInterface)(REFIID iid, void** ppv) OVERRIDE;
+ STDMETHOD(AdviseSink)(REFIID iid, IUnknown* unknown, DWORD mask) OVERRIDE;
+ STDMETHOD(FindNextAttrTransition)(LONG acp_start,
+ LONG acp_halt,
+ ULONG num_filter_attributes,
+ const TS_ATTRID* filter_attributes,
+ DWORD flags,
+ LONG* acp_next,
+ BOOL* found,
+ LONG* found_offset) OVERRIDE;
+ STDMETHOD(GetACPFromPoint)(TsViewCookie view_cookie,
+ const POINT* point,
+ DWORD flags,
+ LONG* acp) OVERRIDE;
+ STDMETHOD(GetActiveView)(TsViewCookie* view_cookie) OVERRIDE;
+ STDMETHOD(GetEmbedded)(LONG acp_pos,
+ REFGUID service,
+ REFIID iid,
+ IUnknown** unknown) OVERRIDE;
+ STDMETHOD(GetEndACP)(LONG* acp) OVERRIDE;
+ STDMETHOD(GetFormattedText)(LONG acp_start,
+ LONG acp_end,
+ IDataObject** data_object) OVERRIDE;
+ STDMETHOD(GetScreenExt)(TsViewCookie view_cookie, RECT* rect) OVERRIDE;
+ STDMETHOD(GetSelection)(ULONG selection_index,
+ ULONG selection_buffer_size,
+ TS_SELECTION_ACP* selection_buffer,
+ ULONG* fetched_count) OVERRIDE;
+ STDMETHOD(GetStatus)(TS_STATUS* pdcs) OVERRIDE;
+ STDMETHOD(GetText)(LONG acp_start,
+ LONG acp_end,
+ wchar_t* text_buffer,
+ ULONG text_buffer_size,
+ ULONG* text_buffer_copied,
+ TS_RUNINFO* run_info_buffer,
+ ULONG run_info_buffer_size,
+ ULONG* run_info_buffer_copied,
+ LONG* next_acp) OVERRIDE;
+ STDMETHOD(GetTextExt)(TsViewCookie view_cookie,
+ LONG acp_start,
+ LONG acp_end,
+ RECT* rect,
+ BOOL* clipped) OVERRIDE;
+ STDMETHOD(GetWnd)(TsViewCookie view_cookie, HWND* window_handle) OVERRIDE;
+ STDMETHOD(InsertEmbedded)(DWORD flags,
+ LONG acp_start,
+ LONG acp_end,
+ IDataObject* data_object,
+ TS_TEXTCHANGE* change) OVERRIDE;
+ STDMETHOD(InsertEmbeddedAtSelection)(DWORD flags,
+ IDataObject* data_object,
+ LONG* acp_start,
+ LONG* acp_end,
+ TS_TEXTCHANGE* change) OVERRIDE;
+ STDMETHOD(InsertTextAtSelection)(DWORD flags,
+ const wchar_t* text_buffer,
+ ULONG text_buffer_size,
+ LONG* acp_start,
+ LONG* acp_end,
+ TS_TEXTCHANGE* text_change) OVERRIDE;
+ STDMETHOD(QueryInsert)(LONG acp_test_start,
+ LONG acp_test_end,
+ ULONG text_size,
+ LONG* acp_result_start,
+ LONG* acp_result_end) OVERRIDE;
+ STDMETHOD(QueryInsertEmbedded)(const GUID* service,
+ const FORMATETC* format,
+ BOOL* insertable) OVERRIDE;
+ STDMETHOD(RequestAttrsAtPosition)(LONG acp_pos,
+ ULONG attribute_buffer_size,
+ const TS_ATTRID* attribute_buffer,
+ DWORD flags) OVERRIDE;
+ STDMETHOD(RequestAttrsTransitioningAtPosition)(
+ LONG acp_pos,
+ ULONG attribute_buffer_size,
+ const TS_ATTRID* attribute_buffer,
+ DWORD flags) OVERRIDE;
+ STDMETHOD(RequestLock)(DWORD lock_flags, HRESULT* result) OVERRIDE;
+ STDMETHOD(RequestSupportedAttrs)(DWORD flags,
+ ULONG attribute_buffer_size,
+ const TS_ATTRID* attribute_buffer) OVERRIDE;
+ STDMETHOD(RetrieveRequestedAttrs)(ULONG attribute_buffer_size,
+ TS_ATTRVAL* attribute_buffer,
+ ULONG* attribute_buffer_copied) OVERRIDE;
+ STDMETHOD(SetSelection)(ULONG selection_buffer_size,
+ const TS_SELECTION_ACP* selection_buffer) OVERRIDE;
+ STDMETHOD(SetText)(DWORD flags,
+ LONG acp_start,
+ LONG acp_end,
+ const wchar_t* text_buffer,
+ ULONG text_buffer_size,
+ TS_TEXTCHANGE* text_change) OVERRIDE;
+ STDMETHOD(UnadviseSink)(IUnknown* unknown) OVERRIDE;
+
+ // ITfContextOwnerCompositionSink:
+ STDMETHOD(OnStartComposition)(ITfCompositionView* composition_view,
+ BOOL* ok) OVERRIDE;
+ STDMETHOD(OnUpdateComposition)(ITfCompositionView* composition_view,
+ ITfRange* range) OVERRIDE;
+ STDMETHOD(OnEndComposition)(ITfCompositionView* composition_view) OVERRIDE;
+
+ // ITfTextEditSink:
+ STDMETHOD(OnEndEdit)(ITfContext* context, TfEditCookie read_only_edit_cookie,
+ ITfEditRecord* edit_record) OVERRIDE;
+
+ // Cancels the ongoing composition if exists.
+ bool CancelComposition();
+
+ // Confirms the ongoing composition if exists.
+ bool ConfirmComposition();
+
+ // Sends OnLayoutChange() via |text_store_acp_sink_|.
+ void SendOnLayoutChange();
+
+ // Creates an instance of TSFTextStore. Returns NULL if fails.
+ static scoped_refptr<TSFTextStore> Create(
+ HWND window_handle,
+ const std::vector<InputScope>& input_scopes,
+ TextStoreDelegate* delegate);
+
+ private:
+ TSFTextStore(HWND window_handle,
+ ITfCategoryMgr* category_manager,
+ ITfDisplayAttributeMgr* display_attribute_manager,
+ ITfInputScope* input_scope,
+ TextStoreDelegate* delegate);
+
+ // Checks if the document has a read-only lock.
+ bool HasReadLock() const;
+
+ // Checks if the document has a read and write lock.
+ bool HasReadWriteLock() const;
+
+ // Gets the display attribute structure.
+ bool GetDisplayAttribute(TfGuidAtom guid_atom,
+ TF_DISPLAYATTRIBUTE* attribute);
+
+ // Gets the committed string size and underline information of the context.
+ bool GetCompositionStatus(
+ ITfContext* context,
+ const TfEditCookie read_only_edit_cookie,
+ uint32* committed_size,
+ std::vector<metro_viewer::UnderlineInfo>* undelines);
+
+ // The refrence count of this instance.
+ volatile LONG ref_count_;
+
+ // A pointer of ITextStoreACPSink, this instance is given in AdviseSink.
+ base::win::ScopedComPtr<ITextStoreACPSink> text_store_acp_sink_;
+
+ // The current mask of |text_store_acp_sink_|.
+ DWORD text_store_acp_sink_mask_;
+
+ // HWND of the attached window.
+ HWND window_handle_;
+
+ // The delegate attached to this text store.
+ TextStoreDelegate* delegate_;
+
+ // |string_buffer_| contains committed string and composition string.
+ // Example: "aoi" is committed, and "umi" is under composition.
+ // |string_buffer_|: "aoiumi"
+ // |committed_size_|: 3
+ string16 string_buffer_;
+ uint32 committed_size_;
+
+ // |selection_start_| and |selection_end_| indicates the selection range.
+ // Example: "iue" is selected
+ // |string_buffer_|: "aiueo"
+ // |selection_start_|: 1
+ // |selection_end_|: 4
+ uint32 selection_start_;
+ uint32 selection_end_;
+
+ // |start_offset| and |end_offset| of |composition_undelines_| indicates
+ // the offsets in |string_buffer_|.
+ // Example: "aoi" is committed. There are two underlines in "umi" and "no".
+ // |string_buffer_|: "aoiumino"
+ // |committed_size_|: 3
+ // underlines_[0].start_offset: 3
+ // underlines_[0].end_offset: 6
+ // underlines_[1].start_offset: 6
+ // underlines_[1].end_offset: 8
+ std::vector<metro_viewer::UnderlineInfo> underlines_;
+
+ // |edit_flag_| indicates that the status is edited during
+ // ITextStoreACPSink::OnLockGranted().
+ bool edit_flag_;
+
+ // The type of current lock.
+ // 0: No lock.
+ // TS_LF_READ: read-only lock.
+ // TS_LF_READWRITE: read/write lock.
+ DWORD current_lock_type_;
+
+ // Queue of the lock request used in RequestLock().
+ std::deque<DWORD> lock_queue_;
+
+ // Category manager and Display attribute manager are used to obtain the
+ // attributes of the composition string.
+ base::win::ScopedComPtr<ITfCategoryMgr> category_manager_;
+ base::win::ScopedComPtr<ITfDisplayAttributeMgr> display_attribute_manager_;
+
+ // Represents the context information of this text.
+ base::win::ScopedComPtr<ITfInputScope> input_scope_;
+
+ DISALLOW_COPY_AND_ASSIGN(TSFTextStore);
+};
+
+} // namespace metro_driver
+
+#endif // WIN8_METRO_DRIVER_IME_TEXT_STORE_H_
diff --git a/win8/metro_driver/ime/text_store_delegate.h b/win8/metro_driver/ime/text_store_delegate.h
new file mode 100644
index 0000000..af06516
--- /dev/null
+++ b/win8/metro_driver/ime/text_store_delegate.h
@@ -0,0 +1,55 @@
+// 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.
+
+#ifndef WIN8_METRO_DRIVER_IME_TEXT_STORE_DELEGATE_H_
+#define WIN8_METRO_DRIVER_IME_TEXT_STORE_DELEGATE_H_
+
+#include <vector>
+
+#include <windows.h>
+
+#include "base/basictypes.h"
+#include "base/strings/string16.h"
+
+namespace metro_viewer {
+struct UnderlineInfo;
+}
+
+namespace metro_driver {
+
+// A delegate which works together with virtual text stores.
+// Objects that implement this delegate will receive notifications from a
+// virtual text store whenever an IME updates the composition or commits text.
+// Objects that implement this delegate are also responsible for calculating
+// the character position of composition and caret position upon request.
+class TextStoreDelegate {
+ public:
+ virtual ~TextStoreDelegate() {}
+
+ // Called when on-going composition is updated. An empty |text| represents
+ // that the composition is canceled.
+ virtual void OnCompositionChanged(
+ const string16& text,
+ int32 selection_start,
+ int32 selection_end,
+ const std::vector<metro_viewer::UnderlineInfo>& underlines) = 0;
+
+ // Called when |text| is committed.
+ virtual void OnTextCommitted(const string16& text) = 0;
+
+ // Called when an IME requests the caret position. Objects that implement
+ // this method must return the caret position in screen coordinates.
+ virtual RECT GetCaretBounds() = 0;
+
+ // Called when an IME requests the bounding box of an character whose
+ // index is |index| in the on-going composition. position. Objects that
+ // implement this method must return true and fill the character bounds into
+ // |rect| in screen coordinates.
+ // Should return false if |index| is invalid.
+ virtual bool GetCompositionCharacterBounds(uint32 index, RECT* rect) = 0;
+};
+
+} // namespace metro_driver
+
+#endif // WIN8_METRO_DRIVER_IME_TEXT_STORE_DELEGATE_H_
diff --git a/win8/metro_driver/metro_driver.gyp b/win8/metro_driver/metro_driver.gyp
index 33e7e48..1b81a79 100644
--- a/win8/metro_driver/metro_driver.gyp
+++ b/win8/metro_driver/metro_driver.gyp
@@ -85,6 +85,9 @@
'file_picker_ash.cc',
'file_picker_ash.h',
],
+ 'includes': [
+ 'ime/ime.gypi',
+ ],
}, { # use_aura!=1
'sources': [
'chrome_app_view.cc',