diff options
-rw-r--r-- | ui/aura/remote_root_window_host_win.cc | 88 | ||||
-rw-r--r-- | ui/aura/remote_root_window_host_win.h | 22 | ||||
-rw-r--r-- | win8/metro_driver/chrome_app_view_ash.cc | 54 | ||||
-rw-r--r-- | win8/metro_driver/chrome_app_view_ash.h | 30 | ||||
-rw-r--r-- | win8/metro_driver/ime/ime.gypi | 16 | ||||
-rw-r--r-- | win8/metro_driver/ime/input_scope.cc | 87 | ||||
-rw-r--r-- | win8/metro_driver/ime/input_scope.h | 22 | ||||
-rw-r--r-- | win8/metro_driver/ime/text_service.cc | 494 | ||||
-rw-r--r-- | win8/metro_driver/ime/text_service.h | 53 | ||||
-rw-r--r-- | win8/metro_driver/ime/text_service_delegate.h | 40 | ||||
-rw-r--r-- | win8/metro_driver/ime/text_store.cc | 913 | ||||
-rw-r--r-- | win8/metro_driver/ime/text_store.h | 308 | ||||
-rw-r--r-- | win8/metro_driver/ime/text_store_delegate.h | 55 | ||||
-rw-r--r-- | win8/metro_driver/metro_driver.gyp | 3 |
14 files changed, 2183 insertions, 2 deletions
diff --git a/ui/aura/remote_root_window_host_win.cc b/ui/aura/remote_root_window_host_win.cc index ff52d07..0b58d1a 100644 --- a/ui/aura/remote_root_window_host_win.cc +++ b/ui/aura/remote_root_window_host_win.cc @@ -11,9 +11,15 @@ #include "base/message_loop/message_loop.h" #include "ipc/ipc_message.h" #include "ipc/ipc_sender.h" +#include "ui/aura/client/aura_constants.h" #include "ui/aura/client/cursor_client.h" #include "ui/aura/root_window.h" +#include "ui/aura/window_property.h" #include "ui/base/cursor/cursor_loader_win.h" +#include "ui/base/ime/composition_text.h" +#include "ui/base/ime/input_method.h" +#include "ui/base/ime/remote_input_method_win.h" +#include "ui/base/ime/text_input_client.h" #include "ui/events/event_utils.h" #include "ui/events/keycodes/keyboard_code_conversion_win.h" #include "ui/base/view_prop.h" @@ -54,6 +60,25 @@ void SetVirtualKeyStates(uint32 flags) { ::SetKeyboardState(keyboard_state); } +void FillCompositionText( + const string16& text, + int32 selection_start, + int32 selection_end, + const std::vector<metro_viewer::UnderlineInfo>& underlines, + ui::CompositionText* composition_text) { + composition_text->Clear(); + composition_text->text = text; + composition_text->selection.set_start(selection_start); + composition_text->selection.set_end(selection_end); + composition_text->underlines.resize(underlines.size()); + for (size_t i = 0; i < underlines.size(); ++i) { + composition_text->underlines[i].start_offset = underlines[i].start_offset; + composition_text->underlines[i].end_offset = underlines[i].end_offset; + composition_text->underlines[i].color = SK_ColorBLACK; + composition_text->underlines[i].thick = underlines[i].thick; + } +} + } // namespace void HandleOpenFile(const base::string16& title, @@ -152,6 +177,10 @@ void RemoteRootWindowHostWin::Connected(IPC::Sender* host, HWND remote_window) { void RemoteRootWindowHostWin::Disconnected() { // Don't CHECK here, Disconnected is called on a channel error which can // happen before we're successfully Connected. + ui::RemoteInputMethodPrivateWin* remote_input_method_private = + GetRemoteInputMethodPrivate(); + if (remote_input_method_private) + remote_input_method_private->SetRemoteDelegate(NULL); host_ = NULL; remote_window_ = NULL; } @@ -184,6 +213,10 @@ bool RemoteRootWindowHostWin::OnMessageReceived(const IPC::Message& message) { OnSetCursorPosAck) IPC_MESSAGE_HANDLER(MetroViewerHostMsg_ActivateDesktopDone, OnDesktopActivated) + IPC_MESSAGE_HANDLER(MetroViewerHostMsg_ImeCompositionChanged, + OnImeCompositionChanged) + IPC_MESSAGE_HANDLER(MetroViewerHostMsg_ImeTextCommitted, + OnImeTextCommitted) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; @@ -318,6 +351,10 @@ gfx::AcceleratedWidget RemoteRootWindowHostWin::GetAcceleratedWidget() { } void RemoteRootWindowHostWin::Show() { + ui::RemoteInputMethodPrivateWin* remote_input_method_private = + GetRemoteInputMethodPrivate(); + if (remote_input_method_private) + remote_input_method_private->SetRemoteDelegate(this); } void RemoteRootWindowHostWin::Hide() { @@ -422,6 +459,27 @@ void RemoteRootWindowHostWin::OnDeviceScaleFactorChanged( void RemoteRootWindowHostWin::PrepareForShutdown() { } +void RemoteRootWindowHostWin::CancelComposition() { + host_->Send(new MetroViewerHostMsg_ImeCancelComposition); +} + +void RemoteRootWindowHostWin::OnTextInputClientUpdated( + const std::vector<int32>& input_scopes, + const std::vector<gfx::Rect>& composition_character_bounds) { + std::vector<metro_viewer::CharacterBounds> character_bounds; + for (size_t i = 0; i < composition_character_bounds.size(); ++i) { + const gfx::Rect& rect = composition_character_bounds[i]; + metro_viewer::CharacterBounds bounds; + bounds.left = rect.x(); + bounds.top = rect.y(); + bounds.right = rect.right(); + bounds.bottom = rect.bottom(); + character_bounds.push_back(bounds); + } + host_->Send(new MetroViewerHostMsg_ImeTextInputClientUpdated( + input_scopes, character_bounds)); +} + void RemoteRootWindowHostWin::OnMouseMoved(int32 x, int32 y, int32 flags) { if (ignore_mouse_moves_until_set_cursor_ack_) return; @@ -581,6 +639,36 @@ void RemoteRootWindowHostWin::OnDesktopActivated() { temp.Run(); } +ui::RemoteInputMethodPrivateWin* +RemoteRootWindowHostWin::GetRemoteInputMethodPrivate() { + ui::InputMethod* input_method = GetAshWindow()->GetProperty( + aura::client::kRootWindowInputMethodKey); + return ui::RemoteInputMethodPrivateWin::Get(input_method); +} + +void RemoteRootWindowHostWin::OnImeCompositionChanged( + const string16& text, + int32 selection_start, + int32 selection_end, + const std::vector<metro_viewer::UnderlineInfo>& underlines) { + ui::RemoteInputMethodPrivateWin* remote_input_method_private = + GetRemoteInputMethodPrivate(); + if (!remote_input_method_private) + return; + ui::CompositionText composition_text; + FillCompositionText( + text, selection_start, selection_end, underlines, &composition_text); + remote_input_method_private->OnCompositionChanged(composition_text); +} + +void RemoteRootWindowHostWin::OnImeTextCommitted(const string16& text) { + ui::RemoteInputMethodPrivateWin* remote_input_method_private = + GetRemoteInputMethodPrivate(); + if (!remote_input_method_private) + return; + remote_input_method_private->OnTextCommitted(text); +} + void RemoteRootWindowHostWin::DispatchKeyboardMessage(ui::EventType type, uint32 vkey, uint32 repeat_count, diff --git a/ui/aura/remote_root_window_host_win.h b/ui/aura/remote_root_window_host_win.h index b7d9148..92788da 100644 --- a/ui/aura/remote_root_window_host_win.h +++ b/ui/aura/remote_root_window_host_win.h @@ -11,15 +11,18 @@ #include "base/compiler_specific.h" #include "base/strings/string16.h" #include "ui/aura/window_tree_host.h" +#include "ui/base/ime/remote_input_method_delegate_win.h" #include "ui/events/event.h" #include "ui/events/event_constants.h" #include "ui/gfx/native_widget_types.h" +#include "ui/metro_viewer/ime_types.h" namespace base { class FilePath; } namespace ui { +class RemoteInputMethodPrivateWin; class ViewProp; } @@ -92,7 +95,9 @@ AURA_EXPORT void HandleActivateDesktop( // RootWindowHost implementaton that receives events from a different // process. In the case of Windows this is the Windows 8 (aka Metro) // frontend process, which forwards input events to this class. -class AURA_EXPORT RemoteRootWindowHostWin : public RootWindowHost { +class AURA_EXPORT RemoteRootWindowHostWin + : public RootWindowHost, + public ui::internal::RemoteInputMethodDelegateWin { public: // Returns the only RemoteRootWindowHostWin, if this is the first time // this function is called, it will call Create() wiht empty bounds. @@ -185,6 +190,15 @@ class AURA_EXPORT RemoteRootWindowHostWin : public RootWindowHost { void OnSetCursorPosAck(); void OnDesktopActivated(); + // For Input Method support: + ui::RemoteInputMethodPrivateWin* GetRemoteInputMethodPrivate(); + void OnImeCompositionChanged( + const string16& text, + int32 selection_start, + int32 selection_end, + const std::vector<metro_viewer::UnderlineInfo>& underlines); + void OnImeTextCommitted(const string16& text); + // RootWindowHost overrides: virtual RootWindow* GetRootWindow() OVERRIDE; virtual gfx::AcceleratedWidget GetAcceleratedWidget() OVERRIDE; @@ -208,6 +222,12 @@ class AURA_EXPORT RemoteRootWindowHostWin : public RootWindowHost { virtual void OnDeviceScaleFactorChanged(float device_scale_factor) OVERRIDE; virtual void PrepareForShutdown() OVERRIDE; + // ui::internal::RemoteInputMethodDelegateWin overrides: + virtual void CancelComposition() OVERRIDE; + virtual void OnTextInputClientUpdated( + const std::vector<int32>& input_scopes, + const std::vector<gfx::Rect>& composition_character_bounds) OVERRIDE; + // Helper function to dispatch a keyboard message to the desired target. // The default target is the RootWindowHostDelegate. For nested message loop // invocations we post a synthetic keyboard message directly into the message 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', |