From 87b455f0a6d49f8dea8086f7bbe0e305c2690aa2 Mon Sep 17 00:00:00 2001 From: Siye Liu Date: Wed, 13 Feb 2019 20:22:55 -0800 Subject: [PATCH] Add full support for Text Services Framework on Windows This change adds full support for TSF1 in Chromium. The change enables Windows Input Services to be aware of context of focused edit control. The change also enables editing features such as soft keyboard suggestions replacement, re-conversion feature for Korean IME and javascript keydown/keyup event during composition. --- .../render_widget_host_view_aura.cc | 20 + .../render_widget_host_view_aura.h | 9 + ui/base/ime/text_input_client.h | 16 + ui/base/ime/win/tsf_bridge.cc | 49 +- ui/base/ime/win/tsf_text_store.cc | 514 ++++++++-- ui/base/ime/win/tsf_text_store.h | 128 ++- ui/base/ime/win/tsf_text_store_unittest.cc | 898 ++++++++++++++++-- 7 files changed, 1414 insertions(+), 220 deletions(-) diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc index c037ef31ced17e..5ee56d1bf357c3 100644 --- a/content/browser/renderer_host/render_widget_host_view_aura.cc +++ b/content/browser/renderer_host/render_widget_host_view_aura.cc @@ -1541,6 +1541,26 @@ bool RenderWidgetHostViewAura::ShouldDoLearning() { return GetTextInputManager() && GetTextInputManager()->should_do_learning(); } +#if defined(OS_WIN) +void RenderWidgetHostViewAura::DispatchKeyEventForIME(ui::KeyEvent* key) { + if (window_ && window_->GetHost()) { + window_->GetHost()->DispatchKeyEventPostIME(key, base::NullCallback()); + } +} + +void RenderWidgetHostViewAura::SetCompositionFromExistingText( + size_t start, + size_t end, + const std::vector& ui_ime_text_spans) { + RenderFrameHostImpl* frame = GetFocusedFrame(); + if (frame) { + frame->GetFrameInputHandler()->SetCompositionFromExistingText( + start, end, ui_ime_text_spans); + has_composition_text_ = true; + } +} +#endif + //////////////////////////////////////////////////////////////////////////////// // RenderWidgetHostViewAura, display::DisplayObserver implementation: diff --git a/content/browser/renderer_host/render_widget_host_view_aura.h b/content/browser/renderer_host/render_widget_host_view_aura.h index fb40b669c5cb97..f2705278d29fef 100644 --- a/content/browser/renderer_host/render_widget_host_view_aura.h +++ b/content/browser/renderer_host/render_widget_host_view_aura.h @@ -242,6 +242,15 @@ class CONTENT_EXPORT RenderWidgetHostViewAura ukm::SourceId GetClientSourceForMetrics() const override; bool ShouldDoLearning() override; +#if defined(OS_WIN) + // Ovrridden for ui::TextInputClient(Windows only): + void DispatchKeyEventForIME(ui::KeyEvent* key) override; + void SetCompositionFromExistingText( + size_t start, + size_t end, + const std::vector& ui_ime_text_spans) override; +#endif + // Overridden from display::DisplayObserver: void OnDisplayMetricsChanged(const display::Display& display, uint32_t metrics) override; diff --git a/ui/base/ime/text_input_client.h b/ui/base/ime/text_input_client.h index 7209084139a463..bcaea7af7045c5 100644 --- a/ui/base/ime/text_input_client.h +++ b/ui/base/ime/text_input_client.h @@ -198,6 +198,22 @@ class UI_BASE_IME_EXPORT TextInputClient { // improve typing suggestions for the user. This should return false for text // fields that are considered 'private' (e.g. in incognito tabs). virtual bool ShouldDoLearning() = 0; + +#if defined(OS_WIN) + // Dispatch a key event from input service to text input client. This should + // only be used for composition scenario since the IME will consume windows + // native key message and we won't receive any key events. We need to + // synthesize key event and notify text input client to fire corresponding + // javascript key events. This is windows only. + virtual void DispatchKeyEventForIME(ui::KeyEvent* key) {} + + // Start a composition range for existing text. This should only be used for + // composition scenario when IME want to start composition on existing text. + virtual void SetCompositionFromExistingText( + size_t start, + size_t end, + const std::vector& ui_ime_text_spans) {} +#endif }; } // namespace ui diff --git a/ui/base/ime/win/tsf_bridge.cc b/ui/base/ime/win/tsf_bridge.cc index f3f094c54322eb..aeee84a66d2c02 100644 --- a/ui/base/ime/win/tsf_bridge.cc +++ b/ui/base/ime/win/tsf_bridge.cc @@ -126,6 +126,9 @@ class TSFBridgeImpl : public TSFBridge { // Represents the window that is currently owns text input focus. HWND attached_window_handle_ = nullptr; + // Handle to ITfKeyTraceEventSink. + DWORD key_trace_sink_cookie_ = 0; + DISALLOW_COPY_AND_ASSIGN(TSFBridgeImpl); }; @@ -135,6 +138,14 @@ TSFBridgeImpl::~TSFBridgeImpl() { DCHECK(base::MessageLoopCurrentForUI::IsSet()); if (!IsInitialized()) return; + + if (thread_manager_ != nullptr) { + Microsoft::WRL::ComPtr source; + if (SUCCEEDED(thread_manager_->QueryInterface(IID_PPV_ARGS(&source)))) { + source->UnadviseSink(key_trace_sink_cookie_); + } + } + for (TSFDocumentMap::iterator it = tsf_document_map_.begin(); it != tsf_document_map_.end(); ++it) { Microsoft::WRL::ComPtr context; @@ -311,6 +322,9 @@ bool TSFBridgeImpl::CreateDocumentManager(TSFTextStore* text_store, return false; } + if (!text_store || !source_cookie) + return true; + DWORD edit_cookie = TF_INVALID_EDIT_COOKIE; if (FAILED((*document_manager) ->CreateContext(client_id_, 0, @@ -325,9 +339,6 @@ bool TSFBridgeImpl::CreateDocumentManager(TSFTextStore* text_store, return false; } - if (!text_store || !source_cookie) - return true; - Microsoft::WRL::ComPtr source; if (FAILED((*context)->QueryInterface(IID_PPV_ARGS(&source)))) { DVLOG(1) << "Failed to get source."; @@ -341,6 +352,21 @@ bool TSFBridgeImpl::CreateDocumentManager(TSFTextStore* text_store, return false; } + Microsoft::WRL::ComPtr source_ITfThreadMgr; + if (FAILED(thread_manager_->QueryInterface( + IID_PPV_ARGS(&source_ITfThreadMgr)))) { + DVLOG(1) << "Failed to get source_ITfThreadMgr."; + return false; + } + + if (FAILED(source_ITfThreadMgr->AdviseSink( + IID_ITfKeyTraceEventSink, + static_cast(text_store), + &key_trace_sink_cookie_))) { + DVLOG(1) << "AdviseSink for ITfKeyTraceEventSink failed."; + return false; + } + if (*source_cookie == TF_INVALID_COOKIE) { DVLOG(1) << "The result of cookie is invalid."; return false; @@ -368,9 +394,8 @@ bool TSFBridgeImpl::InitializeDocumentMapInternal() { document_manager.GetAddressOf(), context.GetAddressOf(), cookie_ptr)) return false; - const bool use_disabled_context = (input_type == TEXT_INPUT_TYPE_PASSWORD || - input_type == TEXT_INPUT_TYPE_NONE); - if (use_disabled_context && !InitializeDisabledContext(context.Get())) + if ((input_type == TEXT_INPUT_TYPE_PASSWORD) && + !InitializeDisabledContext(context.Get())) return false; tsf_document_map_[input_type].text_store = text_store; tsf_document_map_[input_type].document_manager = document_manager; @@ -419,6 +444,10 @@ bool TSFBridgeImpl::InitializeDisabledContext(ITfContext* context) { } bool TSFBridgeImpl::IsFocused(ITfDocumentMgr* document_manager) { + if (!IsInitialized()) { + // Hasn't been initialized yet. Return false. + return false; + } Microsoft::WRL::ComPtr focused_document_manager; if (FAILED( thread_manager_->GetFocus(focused_document_manager.GetAddressOf()))) @@ -431,6 +460,10 @@ bool TSFBridgeImpl::IsInitialized() { } void TSFBridgeImpl::UpdateAssociateFocus() { + if (!IsInitialized()) { + // Hasn't been initialized yet. Do nothing. + return; + } if (attached_window_handle_ == nullptr) return; TSFDocument* document = GetAssociatedDocument(); @@ -450,6 +483,10 @@ void TSFBridgeImpl::UpdateAssociateFocus() { } void TSFBridgeImpl::ClearAssociateFocus() { + if (!IsInitialized()) { + // Hasn't been initialized yet. Do nothing. + return; + } if (attached_window_handle_ == nullptr) return; Microsoft::WRL::ComPtr previous_focus; diff --git a/ui/base/ime/win/tsf_text_store.cc b/ui/base/ime/win/tsf_text_store.cc index 6fb766ded9e90f..b57e29278d3e6b 100644 --- a/ui/base/ime/win/tsf_text_store.cc +++ b/ui/base/ime/win/tsf_text_store.cc @@ -14,6 +14,7 @@ #include "base/win/scoped_variant.h" #include "ui/base/ime/text_input_client.h" #include "ui/base/ime/win/tsf_input_scope.h" +#include "ui/display/win/screen_win.h" #include "ui/gfx/geometry/rect.h" namespace ui { @@ -60,6 +61,8 @@ STDMETHODIMP TSFTextStore::QueryInterface(REFIID iid, void** result) { *result = static_cast(this); } else if (iid == IID_ITfTextEditSink) { *result = static_cast(this); + } else if (iid == IID_ITfKeyTraceEventSink) { + *result = static_cast(this); } else { *result = nullptr; return E_NOINTERFACE; @@ -142,7 +145,7 @@ STDMETHODIMP TSFTextStore::GetEndACP(LONG* acp) { return E_INVALIDARG; if (!HasReadLock()) return TS_E_NOLOCK; - *acp = string_buffer_.size(); + *acp = string_buffer_document_.size(); return S_OK; } @@ -213,10 +216,8 @@ STDMETHODIMP TSFTextStore::GetStatus(TS_STATUS* status) { return E_INVALIDARG; status->dwDynamicFlags = 0; - // We use transitory contexts and we don't support hidden text. - // TODO(dtapuska): Remove TS_SS_TRANSITORY it was added to fix - // https://crbug.com/148355 - status->dwStaticFlags = TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT; + // We don't support hidden text. + status->dwStaticFlags = TS_SS_NOHIDDENTEXT; return S_OK; } @@ -240,7 +241,7 @@ STDMETHODIMP TSFTextStore::GetText(LONG acp_start, return E_INVALIDARG; if (!HasReadLock()) return TF_E_NOLOCK; - const LONG string_buffer_size = string_buffer_.size(); + const LONG string_buffer_size = string_buffer_document_.size(); if (acp_end == -1) acp_end = string_buffer_size; if (!((0 <= acp_start) && (acp_start <= acp_end) && @@ -251,7 +252,7 @@ STDMETHODIMP TSFTextStore::GetText(LONG acp_start, *text_buffer_copied = acp_end - acp_start; const base::string16& result = - string_buffer_.substr(acp_start, *text_buffer_copied); + string_buffer_document_.substr(acp_start, *text_buffer_copied); for (size_t i = 0; i < result.size(); ++i) { text_buffer[i] = result[i]; } @@ -281,7 +282,7 @@ STDMETHODIMP TSFTextStore::GetTextExt(TsViewCookie view_cookie, return TS_E_NOLOCK; if (!((static_cast(committed_size_) <= acp_start) && (acp_start <= acp_end) && - (acp_end <= static_cast(string_buffer_.size())))) { + (acp_end <= static_cast(string_buffer_document_.size())))) { return TS_E_INVALIDPOS; } @@ -290,7 +291,7 @@ STDMETHODIMP TSFTextStore::GetTextExt(TsViewCookie view_cookie, // 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; + gfx::Rect result_rect; gfx::Rect tmp_rect; const uint32_t start_pos = acp_start - committed_size_; const uint32_t end_pos = acp_end - committed_size_; @@ -304,32 +305,31 @@ STDMETHODIMP TSFTextStore::GetTextExt(TsViewCookie view_cookie, if (start_pos == 0) { if (text_input_client_->GetCompositionCharacterBounds(0, &tmp_rect)) { tmp_rect.set_width(0); - result = tmp_rect.ToRECT(); - } else if (string_buffer_.size() == committed_size_) { - result = text_input_client_->GetCaretBounds().ToRECT(); + result_rect = gfx::Rect(tmp_rect); + } else if (string_buffer_document_.size() == committed_size_) { + result_rect = gfx::Rect(text_input_client_->GetCaretBounds()); } else { return TS_E_NOLAYOUT; } } else if (text_input_client_->GetCompositionCharacterBounds(start_pos - 1, &tmp_rect)) { - result.left = tmp_rect.right(); - result.right = tmp_rect.right(); - result.top = tmp_rect.y(); - result.bottom = tmp_rect.bottom(); + tmp_rect.set_x(tmp_rect.right()); + tmp_rect.set_width(0); + result_rect = gfx::Rect(tmp_rect); + } else { return TS_E_NOLAYOUT; } } else { if (text_input_client_->GetCompositionCharacterBounds(start_pos, &tmp_rect)) { - result.left = tmp_rect.x(); - result.top = tmp_rect.y(); - result.right = tmp_rect.right(); - result.bottom = tmp_rect.bottom(); + result_rect = gfx::Rect(tmp_rect); if (text_input_client_->GetCompositionCharacterBounds(end_pos - 1, &tmp_rect)) { - result.right = tmp_rect.right(); - result.bottom = tmp_rect.bottom(); + result_rect.set_width(tmp_rect.x() - result_rect.x() + + tmp_rect.width()); + result_rect.set_height(tmp_rect.y() - result_rect.y() + + tmp_rect.height()); } 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. @@ -339,14 +339,14 @@ STDMETHODIMP TSFTextStore::GetTextExt(TsViewCookie view_cookie, // it's better to return previous caret rectangle instead. // TODO(nona, kinaba): Remove this hack. if (start_pos == 0) { - result = text_input_client_->GetCaretBounds().ToRECT(); + result_rect = gfx::Rect(text_input_client_->GetCaretBounds()); } else { return TS_E_NOLAYOUT; } } } - - *rect = result; + *rect = display::win::ScreenWin::DIPToScreenRect(window_handle_, result_rect) + .ToRECT(); *clipped = FALSE; return S_OK; } @@ -408,9 +408,15 @@ STDMETHODIMP TSFTextStore::InsertTextAtSelection(DWORD flags, return E_INVALIDARG; DCHECK_LE(start_pos, end_pos); - string_buffer_ = string_buffer_.substr(0, start_pos) + - base::string16(text_buffer, text_buffer + text_buffer_size) + - string_buffer_.substr(end_pos); + string_buffer_document_ = + string_buffer_document_.substr(0, start_pos) + + base::string16(text_buffer, text_buffer + text_buffer_size) + + string_buffer_document_.substr(end_pos); + + // reconstruct string that needs to be inserted. + string_pending_insertion_ = + string_buffer_document_.substr(start_pos, text_buffer_size); + if (acp_start) *acp_start = start_pos; if (acp_end) @@ -433,7 +439,7 @@ STDMETHODIMP TSFTextStore::QueryInsert(LONG acp_test_start, if (!acp_result_start || !acp_result_end || acp_test_start > acp_test_end) return E_INVALIDARG; const LONG committed_size = static_cast(committed_size_); - const LONG buffer_size = static_cast(string_buffer_.size()); + const LONG buffer_size = static_cast(string_buffer_document_.size()); *acp_result_start = std::min(std::max(committed_size, acp_test_start), buffer_size); *acp_result_end = @@ -475,6 +481,9 @@ STDMETHODIMP TSFTextStore::RequestAttrsTransitioningAtPosition( } STDMETHODIMP TSFTextStore::RequestLock(DWORD lock_flags, HRESULT* result) { + if (!text_input_client_) + return E_UNEXPECTED; + if (!text_store_acp_sink_.Get()) return E_FAIL; if (!result) @@ -496,7 +505,13 @@ STDMETHODIMP TSFTextStore::RequestLock(DWORD lock_flags, HRESULT* result) { current_lock_type_ = (lock_flags & TS_LF_READWRITE); edit_flag_ = false; - const size_t last_committed_size = committed_size_; + // if there is not already some composition text, they we are about to start + // composition. we need to set last_committed_size to the selection start. + // Otherwise we are updating an existing composition, we should use the cached + // committed_size_ for reference. + const size_t last_committed_size = text_input_client_->HasCompositionText() + ? committed_size_ + : selection_.start(); // Grant the lock. *result = text_store_acp_sink_->OnLockGranted(current_lock_type_); @@ -512,66 +527,85 @@ STDMETHODIMP TSFTextStore::RequestLock(DWORD lock_flags, HRESULT* result) { current_lock_type_ = 0; } + // if nothing has changed from input service, then only need to + // compare our cache with latest textinputstate. if (!edit_flag_) { + CalculateTextandSelectionDiffAndNotifyIfNeeded(true); return S_OK; } + if (!text_input_client_) + return E_UNEXPECTED; + + // If string_pending_insertion_ is empty, then there are three cases: + // 1. there is no composition We only need to do comparison between our + // cache and latest textinputstate and send notifications accordingly. + // 2. A new composition is about to start on existing text. We need to start + // composition on range from composition_range_. + // 3. There is composition. User cancels the composition by deleting all of + // the composing text, we need to reset the committed_size_ and call into + // blink to complete the existing composition(later in this method). + if (string_pending_insertion_.size() == 0) { + if (!text_input_client_->HasCompositionText()) { + if (has_composition_range_) { + StartCompositionOnExistingText(); + } else { + committed_size_ = selection_.start(); + CalculateTextandSelectionDiffAndNotifyIfNeeded(true); + } + return S_OK; + } else { + committed_size_ = last_committed_size; + } + } + + // If we saved a keydown event before this, now is the right time to fire it + // We should only fire JS key event during composition. + if (has_composition_range_ && wparam_keydown_cached_ != 0 && + lparam_keydown_cached_ != 0) { + DispatchKeyEvent(ui::ET_KEY_PRESSED, wparam_keydown_cached_, + lparam_keydown_cached_); + } + // If the text store is edited in OnLockGranted(), we may need to call // TextInputClient::InsertText() or TextInputClient::SetCompositionText(). const size_t new_committed_size = committed_size_; - const base::string16& new_committed_string = string_buffer_.substr( - last_committed_size, new_committed_size - last_committed_size); - const base::string16& composition_string = - string_buffer_.substr(new_committed_size); - // If there is new committed string, calls TextInputClient::InsertText(). - if ((!new_committed_string.empty()) && text_input_client_) { - text_input_client_->InsertText(new_committed_string); + // If new_committed_size is not equal to last_committed_size, + // then we know that there are some committed text. we need to call + // TextInputClient::InsertText to complete the current composition. When there + // are some committed text, it is not necessarily true that composition_string + // is empty. We need to complete current composition with committed text and + // start new composition with composition_string. + if ((new_committed_size != last_committed_size) && text_input_client_) { + CommitTextAndEndCompositionIfAny(last_committed_size, new_committed_size); } - // Calls TextInputClient::SetCompositionText(). - CompositionText composition_text; - composition_text.text = composition_string; - composition_text.ime_text_spans = text_spans_; - // Adjusts the offset. - for (size_t i = 0; i < composition_text.ime_text_spans.size(); ++i) { - composition_text.ime_text_spans[i].start_offset -= new_committed_size; - composition_text.ime_text_spans[i].end_offset -= new_committed_size; - } - if (selection_.start() < new_committed_size) { - composition_text.selection.set_start(0); - } else { - composition_text.selection.set_start(selection_.start() - - new_committed_size); + const base::string16& composition_string = string_buffer_document_.substr( + composition_range_.start(), + composition_range_.end() - composition_range_.start()); + + // Only need to set composition if the current composition string + // (composition_string) is not the same as previous composition string + // (prev_composition_string_) during same composition. + // If composition_string is empty and there is an existing composition going + // on, we still need to call into blink to complete the composition. + if (!previous_composition_string_._Equal(composition_string) || + (text_input_client_->HasCompositionText() && + composition_string.empty())) { + previous_composition_string_.clear(); + previous_composition_string_ = composition_string; + + StartCompositionOnNewText(new_committed_size, composition_string); } - if (selection_.end() < new_committed_size) { - composition_text.selection.set_end(0); - } else { - composition_text.selection.set_end(selection_.end() - new_committed_size); - } - if (text_input_client_) - text_input_client_->SetCompositionText(composition_text); - // 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_.set_start(0); - selection_.set_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); - } + // reset string_buffer_ if composition is no longer active. + if (!text_input_client_->HasCompositionText()) { + string_pending_insertion_.clear(); } + CalculateTextandSelectionDiffAndNotifyIfNeeded(true); + return S_OK; } @@ -625,9 +659,8 @@ STDMETHODIMP TSFTextStore::SetSelection( if (selection_buffer_size > 0) { const LONG start_pos = selection_buffer[0].acpStart; const LONG end_pos = selection_buffer[0].acpEnd; - if (!((static_cast(committed_size_) <= start_pos) && - (start_pos <= end_pos) && - (end_pos <= static_cast(string_buffer_.size())))) { + if (!((start_pos <= end_pos) && + (end_pos <= static_cast(string_buffer_document_.size())))) { return TF_E_INVALIDPOS; } selection_.set_start(start_pos); @@ -644,11 +677,6 @@ STDMETHODIMP TSFTextStore::SetText(DWORD flags, TS_TEXTCHANGE* text_change) { if (!HasReadWriteLock()) return TS_E_NOLOCK; - if (!((static_cast(committed_size_) <= acp_start) && - (acp_start <= acp_end) && - (acp_end <= static_cast(string_buffer_.size())))) { - return TS_E_INVALIDPOS; - } TS_SELECTION_ACP selection; selection.acpStart = acp_start; @@ -662,6 +690,13 @@ STDMETHODIMP TSFTextStore::SetText(DWORD flags, return ret; TS_TEXTCHANGE change; + if (text_buffer_size > 0) { + new_text_inserted_ = true; + replace_text_range_.set_start(acp_start); + replace_text_range_.set_end(acp_end); + replace_text_size_ = text_buffer_size; + } + ret = InsertTextAtSelection(0, text_buffer, text_buffer_size, &acp_start, &acp_end, &change); if (ret != S_OK) @@ -700,6 +735,53 @@ STDMETHODIMP TSFTextStore::OnEndComposition( return S_OK; } +STDMETHODIMP TSFTextStore::OnKeyTraceDown(WPARAM wParam, LPARAM lParam) { + // fire the event right away if we're in composition + if (has_composition_range_) { + DispatchKeyEvent(ui::ET_KEY_PRESSED, wParam, lParam); + } else { + // we're not in composition but we might be starting it - remember these key + // events to fire when composition starts + wparam_keydown_cached_ = wParam; + lparam_keydown_cached_ = lParam; + } + return S_OK; +} + +STDMETHODIMP TSFTextStore::OnKeyTraceUp(WPARAM wParam, LPARAM lParam) { + if (has_composition_range_ || wparam_keydown_fired_ == wParam) { + DispatchKeyEvent(ui::ET_KEY_RELEASED, wParam, lParam); + } + return S_OK; +} + +void TSFTextStore::DispatchKeyEvent(ui::EventType type, + WPARAM wparam, + LPARAM lparam) { + if (!text_input_client_) + return; + + if (type == ui::ET_KEY_PRESSED) { + // clear the saved values since we just fired a keydown + wparam_keydown_cached_ = 0; + lparam_keydown_cached_ = 0; + wparam_keydown_fired_ = wparam; + } else if (type == ui::ET_KEY_RELEASED) { + // clear the saved values since we just fired a keyup + wparam_keydown_fired_ = 0; + } else { + // shouldn't expect event other than et_key_pressed and et_key_released; + return; + } + + // prepare ui::KeyEvent. + UINT message = type == ui::ET_KEY_PRESSED ? WM_KEYDOWN : WM_KEYUP; + const MSG key_event_MSG = {window_handle_, message, VK_PROCESSKEY, lparam}; + ui::KeyEvent key_event = KeyEventFromMSG(key_event_MSG); + + text_input_client_->DispatchKeyEventForIME(&key_event); +} + STDMETHODIMP TSFTextStore::OnEndEdit(ITfContext* context, TfEditCookie read_only_edit_cookie, ITfEditRecord* edit_record) { @@ -713,8 +795,53 @@ STDMETHODIMP TSFTextStore::OnEndEdit(ITfContext* context, return S_OK; } text_spans_ = spans; - committed_size_ = committed_size; edit_flag_ = true; + + // This function is guaranteed to be called after each keystroke during + // composition Therefore we can use this function to update composition status + // after each keystroke. If there is existing composition range, we can cache + // the composition range and set composition start position as the start of + // composition range. If there is no existing composition range, then we know + // that there is no active composition, we then need to reset the cached + // composition range and set the new composition start as the current + // selection start. + DCHECK(context); + Microsoft::WRL::ComPtr context_composition; + if (SUCCEEDED(context->QueryInterface(IID_PPV_ARGS(&context_composition)))) { + Microsoft::WRL::ComPtr enum_composition_view; + if (SUCCEEDED(context_composition->EnumCompositions( + enum_composition_view.GetAddressOf()))) { + Microsoft::WRL::ComPtr composition_view; + if (enum_composition_view->Next(1, composition_view.GetAddressOf(), + nullptr) == S_OK) { + Microsoft::WRL::ComPtr range; + if (SUCCEEDED(composition_view->GetRange(range.GetAddressOf()))) { + Microsoft::WRL::ComPtr range_acp; + if (SUCCEEDED(range.CopyTo(range_acp.GetAddressOf()))) { + LONG start = 0; + LONG length = 0; + if (SUCCEEDED(range_acp->GetExtent(&start, &length))) { + gfx::Range composition_range(start, start + length); + composition_range.set_start(composition_range.start()); + committed_size_ = start; + has_composition_range_ = true; + composition_range_.set_start(start); + composition_range_.set_end(start + length); + } + } + } + } else { + committed_size_ = selection_.start(); + if (has_composition_range_) { + has_composition_range_ = false; + composition_range_.set_start(0); + composition_range_.set_end(0); + previous_composition_string_.clear(); + } + } + } + } + return S_OK; } @@ -823,6 +950,114 @@ bool TSFTextStore::GetCompositionStatus( return true; } +bool TSFTextStore::CalculateTextandSelectionDiffAndNotifyIfNeeded(bool notify) { + if (!text_input_client_) + return false; + + bool result = false; + gfx::Range latest_buffer_range_from_client; + base::string16 latest_buffer_from_client; + gfx::Range latest_selection_from_client; + + if (text_input_client_->GetTextRange(&latest_buffer_range_from_client) && + text_input_client_->GetTextFromRange(latest_buffer_range_from_client, + &latest_buffer_from_client) && + text_input_client_->GetEditableSelectionRange( + &latest_selection_from_client) && + latest_selection_from_client.start() <= + latest_buffer_range_from_client.end() && + latest_selection_from_client.end() <= + latest_buffer_range_from_client.end()) { + // if the text and selection from text input client is the same as the text + // and buffer we got last time, either the state hasn't changed since last + // time we synced or the change hasn't completed yet. Either case we don't + // want to update our buffer and selection cache. We also don't notify + // input service about the change. + if (!buffer_from_client_.compare(latest_buffer_from_client) && + selection_from_client_.EqualsIgnoringDirection( + latest_selection_from_client)) { + return result; + } + + // update cache value for next comparision. + buffer_from_client_.clear(); + buffer_from_client_.assign(latest_buffer_from_client.c_str()); + selection_from_client_.set_start(latest_selection_from_client.start()); + selection_from_client_.set_end(latest_selection_from_client.end()); + + if (has_composition_range_) { + return result; + } + + if (latest_buffer_from_client.compare(string_buffer_document_)) { + const wchar_t* latest_string_buffer_client = + latest_buffer_from_client.c_str(); + if (!latest_string_buffer_client) { + return result; + } + + TS_TEXTCHANGE text_change = {}; + bool notify_text_change = + ((text_store_acp_sink_mask_ & TS_AS_TEXT_CHANGE) != 0) && notify; + + // Execute diffing algorithm only if we need to send notification. + if (notify_text_change) { + size_t acp_start = 0; + size_t acp_old_end = string_buffer_document_.size(); + size_t acp_new_end = latest_buffer_from_client.size(); + + // Compare two strings to find first difference. + for (; acp_start < std::min(latest_buffer_from_client.size(), + string_buffer_document_.size()); + acp_start++) { + if (latest_buffer_from_client.at(acp_start) != + string_buffer_document_.at(acp_start)) { + break; + } + } + + // if two strings have same length, find last difference. + if (latest_buffer_from_client.size() == + string_buffer_document_.size()) { + for (acp_new_end = latest_buffer_from_client.size() - 1; + acp_new_end > acp_start; acp_new_end--) { + if (latest_buffer_from_client.at(acp_new_end) != + string_buffer_document_.at(acp_new_end)) { + break; + } + } + acp_new_end = acp_new_end + 1; + acp_old_end = acp_new_end; + } + + text_change.acpStart = acp_start; + text_change.acpOldEnd = acp_old_end; + text_change.acpNewEnd = acp_new_end; + } + + string_buffer_document_.clear(); + string_buffer_document_.assign(latest_string_buffer_client); + + if (notify_text_change) { + text_store_acp_sink_->OnTextChange(0, &text_change); + } + } + + if (!selection_.EqualsIgnoringDirection(latest_selection_from_client)) { + selection_.set_start(latest_selection_from_client.GetMin()); + selection_.set_end(latest_selection_from_client.GetMax()); + + if (text_store_acp_sink_mask_ & TS_AS_SEL_CHANGE) { + if (notify) { + text_store_acp_sink_->OnSelectionChange(); + } + } + } + } + + return result; +} + void TSFTextStore::SetFocusedTextInputClient( HWND focused_window, TextInputClient* text_input_client) { @@ -843,7 +1078,7 @@ bool TSFTextStore::CancelComposition() { if (edit_flag_) return false; - if (string_buffer_.empty()) + if (string_pending_insertion_.empty()) return true; // Unlike ImmNotifyIME(NI_COMPOSITIONSTR, CPS_CANCEL, 0) in IMM32, TSF does @@ -854,11 +1089,10 @@ bool TSFTextStore::CancelComposition() { // we use the same operation to cancel composition here to minimize the risk // of potential compatibility issues. - const size_t previous_buffer_size = string_buffer_.size(); - string_buffer_.clear(); - committed_size_ = 0; - selection_.set_start(0); - selection_.set_end(0); + previous_composition_string_.clear(); + const size_t previous_buffer_size = string_pending_insertion_.size(); + string_pending_insertion_.clear(); + committed_size_ = selection_.start(); 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) @@ -878,23 +1112,25 @@ bool TSFTextStore::ConfirmComposition() { if (edit_flag_) return false; - if (string_buffer_.empty()) + if (string_pending_insertion_.empty()) return true; + if (!text_input_client_) + return false; + // 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 base::string16& composition_text = - string_buffer_.substr(committed_size_); + string_buffer_document_.substr(committed_size_); if (!composition_text.empty()) text_input_client_->InsertText(composition_text); - const size_t previous_buffer_size = string_buffer_.size(); - string_buffer_.clear(); - committed_size_ = 0; - selection_.set_start(0); - selection_.set_end(0); + previous_composition_string_.clear(); + const size_t previous_buffer_size = string_pending_insertion_.size(); + string_pending_insertion_.clear(); + committed_size_ = selection_.start(); 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) @@ -910,6 +1146,7 @@ bool TSFTextStore::ConfirmComposition() { } void TSFTextStore::SendOnLayoutChange() { + CalculateTextandSelectionDiffAndNotifyIfNeeded(true); if (text_store_acp_sink_ && (text_store_acp_sink_mask_ & TS_AS_LAYOUT_CHANGE)) text_store_acp_sink_->OnLayoutChange(TS_LC_CHANGE, 0); } @@ -922,4 +1159,85 @@ bool TSFTextStore::HasReadWriteLock() const { return (current_lock_type_ & TS_LF_READWRITE) == TS_LF_READWRITE; } +void TSFTextStore::StartCompositionOnExistingText() const { + ui::ImeTextSpans text_spans = text_spans_; + // Adjusts the offset. + for (size_t i = 0; i < text_spans.size(); ++i) { + text_spans[i].start_offset -= committed_size_; + text_spans[i].end_offset -= committed_size_; + } + + text_input_client_->SetCompositionFromExistingText( + composition_range_.start(), composition_range_.end(), text_spans); +} + +void TSFTextStore::CommitTextAndEndCompositionIfAny(size_t old_size, + size_t new_size) const { + if (new_text_inserted_ && + (replace_text_range_.start() != replace_text_range_.end()) && + !text_input_client_->HasCompositionText()) { + // This is a special case to handle text replacement scenarios during + // English typing when we are trying to replace an existing text with some + // new text. + size_t new_text_size; + if (new_size == replace_text_range_.start()) { + // This usually happens when TSF is trying to replace a part of a string + // from the selection end + new_text_size = new_size; + } else { + new_text_size = new_size - replace_text_range_.start(); + } + const base::string16& new_committed_string = string_buffer_document_.substr( + replace_text_range_.start(), new_text_size); + text_input_client_->ExtendSelectionAndDelete( + replace_text_range_.end() - replace_text_range_.start(), 0); + text_input_client_->InsertText(new_committed_string); + } else { + // Construct string to be committed. + size_t new_committed_string_offset = old_size; + size_t new_committed_string_size = new_size - old_size; + // This is a special case. if we are replacing existing text, then + // commit the new text. + if (new_text_inserted_ && + (replace_text_range_.start() != replace_text_range_.end())) { + new_committed_string_offset = replace_text_range_.start(); + new_committed_string_size = replace_text_size_; + } + const base::string16& new_committed_string = string_buffer_document_.substr( + new_committed_string_offset, new_committed_string_size); + text_input_client_->InsertText(new_committed_string); + text_input_client_->SetEditableSelectionRange(selection_); + } +} + +void TSFTextStore::StartCompositionOnNewText( + size_t start_offset, + const base::string16& composition_string) { + CompositionText composition_text; + composition_text.text = composition_string; + composition_text.ime_text_spans = text_spans_; + + for (size_t i = 0; i < composition_text.ime_text_spans.size(); ++i) { + composition_text.ime_text_spans[i].start_offset -= start_offset; + composition_text.ime_text_spans[i].end_offset -= start_offset; + } + + if (selection_.start() < start_offset) { + composition_text.selection.set_start(0); + } else { + composition_text.selection.set_start(selection_.start() - start_offset); + } + + if (selection_.end() < start_offset) { + composition_text.selection.set_end(0); + } else { + composition_text.selection.set_end(selection_.end() - start_offset); + } + + if (text_input_client_) { + new_text_inserted_ = false; + text_input_client_->SetCompositionText(composition_text); + } +} + } // namespace ui diff --git a/ui/base/ime/win/tsf_text_store.h b/ui/base/ime/win/tsf_text_store.h index 687ca8f6d62651..57f5b5fda3d683 100644 --- a/ui/base/ime/win/tsf_text_store.h +++ b/ui/base/ime/win/tsf_text_store.h @@ -14,6 +14,7 @@ #include "base/strings/string16.h" #include "ui/base/ime/ime_text_span.h" #include "ui/base/ime/ui_base_ime_export.h" +#include "ui/events/event_utils.h" #include "ui/gfx/range/range.h" namespace ui { @@ -24,10 +25,10 @@ class TextInputClient; // 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. +// composition, TSFTextStore calls TextInputClient::InsertText(). // // How TSFTextStore works: +// - Assume the document is empty and in focus. // - The user enters "a". // - The input method set composition as "a". // - TSF manager calls TSFTextStore::RequestLock(). @@ -35,37 +36,53 @@ class TextInputClient; // - In OnLockGranted(), TSF manager calls // - TSFTextStore::OnStartComposition() // - TSFTextStore::SetText() -// The string buffer is set as "a". +// The pending string buffer is set as "a". +// The document whole 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 'b'. +// - The input method set composition as "ab". +// - TSF manager calls TSFTextStore::RequestLock(). +// - TSFTextStore callbacks ITextStoreACPSink::OnLockGranted(). +// - In OnLockGranted(), TSF manager calls +// - TSFTextStore::SetText() +// The pending string buffer is set as "b". +// The document whole buffer is changed to "ab". +// - TSFTextStore::OnUpdateComposition() +// - TSFTextStore::OnEndEdit() +// - TSFTextStore calls TextInputClient::SetCompositionText(). +// "ab" is shown with an underline as composition string. // - The user enters . -// - The input method set composition as "A". +// - The input method set composition as "aB". // - TSF manager calls TSFTextStore::RequestLock(). // - TSFTextStore callbacks ITextStoreACPSink::OnLockGranted(). // - In OnLockGranted(), TSF manager calls // - TSFTextStore::SetText() -// The string buffer is set as "A". +// The pending string buffer is set as "B". +// The document whole buffer is changed to "aB". // - TSFTextStore::OnUpdateComposition() // - TSFTextStore::OnEndEdit() // - TSFTextStore calls TextInputClient::SetCompositionText(). -// "A" is shown with an underline as composition string. +// "aB" is shown with an underline as composition string. // - The user enters . -// - The input method commits "A". +// - The input method commits "aB". // - TSF manager calls TSFTextStore::RequestLock(). // - TSFTextStore callbacks ITextStoreACPSink::OnLockGranted(). // - In OnLockGranted(), TSF manager calls // - TSFTextStore::OnEndComposition() // - TSFTextStore::OnEndEdit() -// TSFTextStore knows "A" is committed. +// TSFTextStore knows "aB" is committed. // - TSFTextStore calls TextInputClient::InsertText(). -// "A" is shown as committed string. -// - TSFTextStore clears the string buffer. -// - TSFTextStore calls OnSelectionChange(), OnLayoutChange() and +// "aB" is shown as committed string. +// - TSFTextStore clears the pending string buffer. +// - TSFTextStore verified if the document whole buffer is the same as the +// buffer returned from TextInputClient. If the buffer is different, then +// call OnSelectionChange(), OnLayoutChange() and // OnTextChange() of ITextStoreACPSink to let TSF manager know that the -// string buffer has been changed. +// string buffer has been changed other than IME. // // About the locking scheme: // When TSF manager manipulates the string buffer it calls RequestLock() to get @@ -83,6 +100,7 @@ class TextInputClient; // http://msdn.microsoft.com/en-us/library/ms629032 class UI_BASE_IME_EXPORT TSFTextStore : public ITextStoreACP, public ITfContextOwnerCompositionSink, + public ITfKeyTraceEventSink, public ITfTextEditSink { public: TSFTextStore(); @@ -208,6 +226,12 @@ class UI_BASE_IME_EXPORT TSFTextStore : public ITextStoreACP, TfEditCookie read_only_edit_cookie, ITfEditRecord* edit_record) override; + // ITfKeyTraceEventSink + STDMETHOD(OnKeyTraceDown) + (WPARAM wParam, LPARAM lParam) override; + STDMETHOD(OnKeyTraceUp) + (WPARAM wParam, LPARAM lParam) override; + // Sets currently focused TextInputClient. void SetFocusedTextInputClient(HWND focused_window, TextInputClient* text_input_client); @@ -227,6 +251,26 @@ class UI_BASE_IME_EXPORT TSFTextStore : public ITextStoreACP, friend class TSFTextStoreTest; friend class TSFTextStoreTestCallback; + // Compare our cached text buffer and selection with the up-to-date + // text buffer and selection from TextInputClient. We also update + // cached text buffer and selection with the new version. Then notify + // input service about the change if notify flag is set to true. + bool CalculateTextandSelectionDiffAndNotifyIfNeeded(bool notify); + + // Synthesize keyevent and send to text input client to fire corresponding + // javascript keyevent during composition. + void DispatchKeyEvent(ui::EventType type, WPARAM wparam, LPARAM lparam); + + // Start new composition on existing text. + void StartCompositionOnExistingText() const; + + // Start new composition with new text. + void StartCompositionOnNewText(size_t start_offset, + const base::string16& composition_string); + + // Commit and insert text into TextInputClient. End any ongoing composition. + void CommitTextAndEndCompositionIfAny(size_t old_size, size_t new_size) const; + // Checks if the document has a read-only lock. bool HasReadLock() const; @@ -258,26 +302,70 @@ class UI_BASE_IME_EXPORT TSFTextStore : public ITextStoreACP, // Current TextInputClient which is set in SetFocusedTextInputClient. TextInputClient* text_input_client_ = nullptr; - // TODO(dtapuska): determine if we can expose more the entire document - // more than the committed string and composition string to the TIP. - // |string_buffer_| contains committed string and composition string. + // |string_buffer_document_| contains all string in current active view. + // |string_pending_insertion_| contains only string in current edit session. + // |has_composition_range_| indicates the state of composition. + // |composition_range_| indicates the range of composition if any. // Example: "aoi" is committed, and "umi" is under composition. - // |string_buffer_|: "aoiumi" + // In current edit session, user press "i" on keyboard. + // |string_buffer_document_|: "aoiumi" + // |string_pending_insertion_| : "i" // |committed_size_|: 3 - base::string16 string_buffer_; + // |has_composition_range_| = true; + // |composition_range_start_| = 3; + // |composition_range_end_| = 6; + base::string16 string_buffer_document_; + base::string16 string_pending_insertion_; size_t committed_size_ = 0; + bool has_composition_range_ = false; + gfx::Range composition_range_; + + // |previous_composition_string_| indicicates composition string in last + // edit session during same composition. If RequestLock() is called during two + // edit sessions, we don't want to set same composition string twice. + base::string16 previous_composition_string_; + + // |new_text_inserted_| indicates there is text to be inserted + // into blink during ITextStoreACP::SetText(). + // |replace_text_range_| indicates the start and end offsets of the text to be + // replaced by the new text to be inserted. + // |replace_text_size_| indicates the size of the text to be inserted. + // Example: "k" is going to replace "i" + // |string_buffer_document_|: "aeiou" + // |new_text_inserted_|: true + // |replace_text_range_start_|: 2 + // |replace_text_range_end_|: 3 + // |replace_text_size_|: 1 + bool new_text_inserted_ = false; + gfx::Range replace_text_range_; + size_t replace_text_size_; + + // |buffer_from_client_| contains all string returned from + // TextInputClient::GetTextFromRange(); + base::string16 buffer_from_client_; + + // |selection_from_client_| indicates the selection range returned from + // TextInputClient::GetEditableSelectionRange(); + gfx::Range selection_from_client_; + + // |wparam_keydown_cached_| and |lparam_keydown_cached_| contains key event + // info that is used to synthesize key event during composition. + // |wparam_keydown_fired_| indicates if a keydown event has been fired. + WPARAM wparam_keydown_cached_ = 0; + LPARAM lparam_keydown_cached_ = 0; + WPARAM wparam_keydown_fired_ = 0; // |selection_start_| and |selection_end_| indicates the selection range. // Example: "iue" is selected - // |string_buffer_|: "aiueo" + // |string_buffer_document_|: "aiueo" // |selection_.start()|: 1 // |selection_.end()|: 4 gfx::Range selection_; // |start_offset| and |end_offset| of |text_spans_| indicates - // the offsets in |string_buffer_|. + // the offsets in |string_buffer_document_|. // Example: "aoi" is committed. There are two underlines in "umi" and "no". - // |string_buffer_|: "aoiumino" + // |string_buffer_document_|: "aoiumino" // |committed_size_|: 3 // text_spans_[0].start_offset: 3 // text_spans_[0].end_offset: 6 diff --git a/ui/base/ime/win/tsf_text_store_unittest.cc b/ui/base/ime/win/tsf_text_store_unittest.cc index 42380434e08158..cbb8f4508dc26c 100644 --- a/ui/base/ime/win/tsf_text_store_unittest.cc +++ b/ui/base/ime/win/tsf_text_store_unittest.cc @@ -60,6 +60,7 @@ class MockTextInputClient : public TextInputClient { MOCK_CONST_METHOD1(IsTextEditCommandEnabled, bool(TextEditCommand)); MOCK_METHOD1(SetTextEditCommandForNextKeyEvent, void(TextEditCommand)); MOCK_CONST_METHOD0(GetClientSourceForMetrics, ukm::SourceId()); + MOCK_METHOD1(DispatchKeyEventForIME, void(ui::KeyEvent*)); }; class MockStoreACPSink : public ITextStoreACPSink { @@ -136,7 +137,9 @@ class TSFTextStoreTest : public testing::Test { } // Accessors to the internal state of TSFTextStore. - base::string16* string_buffer() { return &text_store_->string_buffer_; } + base::string16* string_buffer() { + return &text_store_->string_buffer_document_; + } size_t* committed_size() { return &text_store_->committed_size_; } base::win::ScopedCOMInitializer com_initializer_; @@ -153,13 +156,36 @@ class TSFTextStoreTestCallback { } virtual ~TSFTextStoreTestCallback() {} + bool HasCompositionText() { return has_composition_text_; } + bool GetTextRange(gfx::Range* range) { + range->set_start(text_range_.start()); + range->set_end(text_range_.end()); + return true; + } + bool GetTextFromRange(const gfx::Range& range, base::string16* text) { + *text = text_buffer_.substr(range.GetMin(), range.length()); + return true; + } + bool GetEditableSelectionRange(gfx::Range* range) { + range->set_start(selection_range_.start()); + range->set_end(selection_range_.end()); + return true; + } + protected: // Accessors to the internal state of TSFTextStore. bool* edit_flag() { return &text_store_->edit_flag_; } - base::string16* string_buffer() { return &text_store_->string_buffer_; } + base::string16* string_buffer() { + return &text_store_->string_buffer_document_; + } + base::string16* string_pending_insertion() { + return &text_store_->string_pending_insertion_; + } size_t* committed_size() { return &text_store_->committed_size_; } gfx::Range* selection() { return &text_store_->selection_; } ImeTextSpans* text_spans() { return &text_store_->text_spans_; } + gfx::Range* composition_range() { return &text_store_->composition_range_; } + bool* has_composition_range() { return &text_store_->has_composition_range_; } void SetInternalState(const base::string16& new_string_buffer, LONG new_committed_size, @@ -170,6 +196,7 @@ class TSFTextStoreTestCallback { ASSERT_LE(new_selection_start, new_selection_end); ASSERT_LE(new_selection_end, static_cast(new_string_buffer.size())); *string_buffer() = new_string_buffer; + *string_pending_insertion() = new_string_buffer; *committed_size() = new_committed_size; selection()->set_start(new_selection_start); selection()->set_end(new_selection_end); @@ -306,6 +333,29 @@ class TSFTextStoreTestCallback { acp_end, &rect, &clipped)); } + void SetHasCompositionText(bool compText) { + has_composition_text_ = compText; + } + + void SetTextRange(uint32_t start, uint32_t end) { + text_range_.set_start(start); + text_range_.set_end(end); + } + + void SetSelectionRange(uint32_t start, uint32_t end) { + selection_range_.set_start(start); + selection_range_.set_end(end); + } + + void SetTextBuffer(const wchar_t* buffer) { + text_buffer_.clear(); + text_buffer_.assign(buffer); + } + + bool has_composition_text_ = false; + gfx::Range text_range_; + gfx::Range selection_range_; + base::string16 text_buffer_ = L""; scoped_refptr text_store_; private: @@ -320,8 +370,7 @@ TEST_F(TSFTextStoreTest, GetStatusTest) { TS_STATUS status = {}; EXPECT_EQ(S_OK, text_store_->GetStatus(&status)); EXPECT_EQ(0u, status.dwDynamicFlags); - EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), - status.dwStaticFlags); + EXPECT_EQ((ULONG)(TS_SS_NOHIDDENTEXT), status.dwStaticFlags); } TEST_F(TSFTextStoreTest, QueryInsertTest) { @@ -565,31 +614,40 @@ class RequestLockTextChangeTestCallback : public TSFTextStoreTestCallback { state_ = 3; } - void SetCompositionText(const ui::CompositionText& composition) { - EXPECT_EQ(3, state_); - EXPECT_EQ(L"", composition.text); - EXPECT_EQ(0u, composition.selection.start()); - EXPECT_EQ(0u, composition.selection.end()); - EXPECT_EQ(0u, composition.ime_text_spans.size()); - state_ = 4; + bool GetTextRange(gfx::Range* range) const { + range->set_start(0); + range->set_end(6); + return true; } - HRESULT OnTextChange(DWORD flags, const TS_TEXTCHANGE* change) { - EXPECT_EQ(4, state_); + bool GetTextFromRange(const gfx::Range& range, base::string16* text) const { + base::string16 string_buffer = L"012345"; + *text = string_buffer.substr(range.GetMin(), range.length()); + return true; + } + + bool GetEditableSelectionRange(gfx::Range* range) const { + range->set_start(0); + range->set_end(0); + return true; + } + + HRESULT OnSelectionChange() { + EXPECT_EQ(3, state_); HRESULT result = kInvalidResult; - state_ = 5; + state_ = 4; EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); EXPECT_EQ(S_OK, result); - EXPECT_EQ(6, state_); - state_ = 7; + EXPECT_EQ(5, state_); + state_ = 6; return S_OK; } HRESULT LockGranted2(DWORD flags) { - EXPECT_EQ(5, state_); + EXPECT_EQ(4, state_); EXPECT_TRUE(HasReadLock()); EXPECT_TRUE(HasReadWriteLock()); - state_ = 6; + state_ = 5; return S_OK; } @@ -607,17 +665,29 @@ TEST_F(TSFTextStoreTest, RequestLockOnTextChangeTest) { .WillOnce( Invoke(&callback, &RequestLockTextChangeTestCallback::LockGranted2)); - EXPECT_CALL(*sink_, OnSelectionChange()).WillOnce(Return(S_OK)); - EXPECT_CALL(*sink_, OnLayoutChange(_, _)).WillOnce(Return(S_OK)); - EXPECT_CALL(*sink_, OnTextChange(_, _)) - .WillOnce( - Invoke(&callback, &RequestLockTextChangeTestCallback::OnTextChange)); + EXPECT_CALL(*sink_, OnSelectionChange()) + .WillOnce(Invoke(&callback, + &RequestLockTextChangeTestCallback::OnSelectionChange)); EXPECT_CALL(text_input_client_, InsertText(_)) .WillOnce( Invoke(&callback, &RequestLockTextChangeTestCallback::InsertText)); - EXPECT_CALL(text_input_client_, SetCompositionText(_)) + EXPECT_CALL(text_input_client_, GetEditableSelectionRange(_)) + .WillOnce( + Invoke(&callback, + &RequestLockTextChangeTestCallback::GetEditableSelectionRange)) + .WillOnce(Invoke( + &callback, + &RequestLockTextChangeTestCallback::GetEditableSelectionRange)); + EXPECT_CALL(text_input_client_, GetTextFromRange(_, _)) .WillOnce(Invoke(&callback, - &RequestLockTextChangeTestCallback::SetCompositionText)); + &RequestLockTextChangeTestCallback::GetTextFromRange)) + .WillOnce(Invoke(&callback, + &RequestLockTextChangeTestCallback::GetTextFromRange)); + EXPECT_CALL(text_input_client_, GetTextRange(_)) + .WillOnce( + Invoke(&callback, &RequestLockTextChangeTestCallback::GetTextRange)) + .WillOnce( + Invoke(&callback, &RequestLockTextChangeTestCallback::GetTextRange)); HRESULT result = kInvalidResult; EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); @@ -654,18 +724,18 @@ class SelectionTestCallback : public TSFTextStoreTestCallback { SetInternalState(L"0123456", 3, 3, 3); - SetSelectionTest(0, 0, TF_E_INVALIDPOS); - SetSelectionTest(0, 1, TF_E_INVALIDPOS); - SetSelectionTest(0, 3, TF_E_INVALIDPOS); - SetSelectionTest(0, 6, TF_E_INVALIDPOS); - SetSelectionTest(0, 7, TF_E_INVALIDPOS); + SetSelectionTest(0, 0, S_OK); + SetSelectionTest(0, 1, S_OK); + SetSelectionTest(0, 3, S_OK); + SetSelectionTest(0, 6, S_OK); + SetSelectionTest(0, 7, S_OK); SetSelectionTest(0, 8, TF_E_INVALIDPOS); SetSelectionTest(1, 0, TF_E_INVALIDPOS); - SetSelectionTest(1, 1, TF_E_INVALIDPOS); - SetSelectionTest(1, 3, TF_E_INVALIDPOS); - SetSelectionTest(1, 6, TF_E_INVALIDPOS); - SetSelectionTest(1, 7, TF_E_INVALIDPOS); + SetSelectionTest(1, 1, S_OK); + SetSelectionTest(1, 3, S_OK); + SetSelectionTest(1, 6, S_OK); + SetSelectionTest(1, 7, S_OK); SetSelectionTest(1, 8, TF_E_INVALIDPOS); SetSelectionTest(3, 0, TF_E_INVALIDPOS); @@ -799,16 +869,16 @@ class SetGetTextTestCallback : public TSFTextStoreTestCallback { SetInternalState(L"0123456", 3, 3, 3); - SetTextTest(0, 0, L"", TS_E_INVALIDPOS); - SetTextTest(0, 1, L"", TS_E_INVALIDPOS); - SetTextTest(0, 3, L"", TS_E_INVALIDPOS); + SetTextTest(0, 0, L"", S_OK); + SetTextTest(0, 1, L"", S_OK); + SetTextTest(0, 3, L"", S_OK); SetTextTest(0, 6, L"", TS_E_INVALIDPOS); SetTextTest(0, 7, L"", TS_E_INVALIDPOS); SetTextTest(0, 8, L"", TS_E_INVALIDPOS); SetTextTest(1, 0, L"", TS_E_INVALIDPOS); - SetTextTest(1, 1, L"", TS_E_INVALIDPOS); - SetTextTest(1, 3, L"", TS_E_INVALIDPOS); + SetTextTest(1, 1, L"", S_OK); + SetTextTest(1, 3, L"", S_OK); SetTextTest(1, 6, L"", TS_E_INVALIDPOS); SetTextTest(1, 7, L"", TS_E_INVALIDPOS); SetTextTest(1, 8, L"", TS_E_INVALIDPOS); @@ -816,9 +886,9 @@ class SetGetTextTestCallback : public TSFTextStoreTestCallback { SetTextTest(3, 0, L"", TS_E_INVALIDPOS); SetTextTest(3, 1, L"", TS_E_INVALIDPOS); - SetTextTest(3, 3, L"", S_OK); - GetTextTest(0, -1, L"0123456", 7); - GetSelectionTest(3, 3); + SetTextTest(3, 3, L"", TS_E_INVALIDPOS); + GetTextTest(0, -1, L"4", 1); + GetSelectionTest(1, 1); SetInternalState(L"0123456", 3, 3, 3); SetTextTest(3, 6, L"", S_OK); @@ -1009,13 +1079,13 @@ class ScenarioTestCallback : public TSFTextStoreTestCallback { : TSFTextStoreTestCallback(text_store) {} HRESULT LockGranted1(DWORD flags) { - SetSelectionTest(0, 0, S_OK); - SetTextTest(0, 0, L"abc", S_OK); SetTextTest(1, 2, L"xyz", S_OK); GetTextTest(0, -1, L"axyzc", 5); + SetSelectionTest(0, 5, S_OK); + text_spans()->clear(); ImeTextSpan text_span; text_span.start_offset = 0; @@ -1026,13 +1096,16 @@ class ScenarioTestCallback : public TSFTextStoreTestCallback { text_spans()->push_back(text_span); *edit_flag() = true; *committed_size() = 0; + composition_range()->set_start(0); + composition_range()->set_end(5); + return S_OK; } void SetCompositionText1(const ui::CompositionText& composition) { EXPECT_EQ(L"axyzc", composition.text); - EXPECT_EQ(1u, composition.selection.start()); - EXPECT_EQ(4u, composition.selection.end()); + EXPECT_EQ(0u, composition.selection.start()); + EXPECT_EQ(5u, composition.selection.end()); ASSERT_EQ(1u, composition.ime_text_spans.size()); EXPECT_EQ(SK_ColorBLACK, composition.ime_text_spans[0].underline_color); EXPECT_EQ(SK_ColorTRANSPARENT, @@ -1044,45 +1117,33 @@ class ScenarioTestCallback : public TSFTextStoreTestCallback { } HRESULT LockGranted2(DWORD flags) { - SetTextTest(3, 4, L"ZCP", S_OK); + SetTextTest(0, 5, L"axyZCPc", S_OK); GetTextTest(0, -1, L"axyZCPc", 7); text_spans()->clear(); ImeTextSpan text_span; - text_span.start_offset = 3; + text_span.start_offset = 0; text_span.end_offset = 5; text_span.underline_color = SK_ColorBLACK; text_span.thickness = ImeTextSpan::Thickness::kThick; text_spans()->push_back(text_span); - text_span.start_offset = 5; - text_span.end_offset = 7; - text_span.underline_color = SK_ColorBLACK; - text_span.thickness = ImeTextSpan::Thickness::kThin; - text_spans()->push_back(text_span); *edit_flag() = true; - *committed_size() = 3; + *committed_size() = 7; + composition_range()->set_start(0); + composition_range()->set_end(7); return S_OK; } - void InsertText2(const base::string16& text) { EXPECT_EQ(L"axy", text); } + void InsertText2(const base::string16& text) { EXPECT_EQ(L"axyZCPc", text); } void SetCompositionText2(const ui::CompositionText& composition) { - EXPECT_EQ(L"ZCPc", composition.text); + EXPECT_EQ(L"axyZCPc", composition.text); EXPECT_EQ(0u, composition.selection.start()); - EXPECT_EQ(3u, composition.selection.end()); - ASSERT_EQ(2u, composition.ime_text_spans.size()); - EXPECT_EQ(SK_ColorBLACK, composition.ime_text_spans[0].underline_color); - EXPECT_EQ(0u, composition.ime_text_spans[0].start_offset); - EXPECT_EQ(2u, composition.ime_text_spans[0].end_offset); - EXPECT_EQ(ImeTextSpan::Thickness::kThick, - composition.ime_text_spans[0].thickness); - EXPECT_EQ(SK_ColorBLACK, composition.ime_text_spans[1].underline_color); - EXPECT_EQ(2u, composition.ime_text_spans[1].start_offset); - EXPECT_EQ(4u, composition.ime_text_spans[1].end_offset); - EXPECT_EQ(ImeTextSpan::Thickness::kThin, - composition.ime_text_spans[1].thickness); + EXPECT_EQ(0u, composition.selection.end()); + ASSERT_EQ(1u, composition.ime_text_spans.size()); + // There is no styling applied from TSF in English typing } HRESULT LockGranted3(DWORD flags) { @@ -1091,19 +1152,12 @@ class ScenarioTestCallback : public TSFTextStoreTestCallback { text_spans()->clear(); *edit_flag() = true; *committed_size() = 7; + composition_range()->set_start(0); + composition_range()->set_end(0); return S_OK; } - void InsertText3(const base::string16& text) { EXPECT_EQ(L"ZCPc", text); } - - void SetCompositionText3(const ui::CompositionText& composition) { - EXPECT_EQ(L"", composition.text); - EXPECT_EQ(0u, composition.selection.start()); - EXPECT_EQ(0u, composition.selection.end()); - EXPECT_EQ(0u, composition.ime_text_spans.size()); - } - private: DISALLOW_COPY_AND_ASSIGN(ScenarioTestCallback); }; @@ -1112,26 +1166,14 @@ TEST_F(TSFTextStoreTest, ScenarioTest) { ScenarioTestCallback callback(text_store_.get()); EXPECT_CALL(text_input_client_, SetCompositionText(_)) .WillOnce(Invoke(&callback, &ScenarioTestCallback::SetCompositionText1)) - .WillOnce(Invoke(&callback, &ScenarioTestCallback::SetCompositionText2)) - .WillOnce(Invoke(&callback, &ScenarioTestCallback::SetCompositionText3)); + .WillOnce(Invoke(&callback, &ScenarioTestCallback::SetCompositionText2)); EXPECT_CALL(text_input_client_, InsertText(_)) - .WillOnce(Invoke(&callback, &ScenarioTestCallback::InsertText2)) - .WillOnce(Invoke(&callback, &ScenarioTestCallback::InsertText3)); + .WillOnce(Invoke(&callback, &ScenarioTestCallback::InsertText2)); EXPECT_CALL(*sink_, OnLockGranted(_)) .WillOnce(Invoke(&callback, &ScenarioTestCallback::LockGranted1)) - .WillOnce(Invoke(&callback, &ScenarioTestCallback::LockGranted2)) - .WillOnce(Invoke(&callback, &ScenarioTestCallback::LockGranted3)); - - // OnSelectionChange will be called once after LockGranted3(). - EXPECT_CALL(*sink_, OnSelectionChange()).WillOnce(Return(S_OK)); - - // OnLayoutChange will be called once after LockGranted3(). - EXPECT_CALL(*sink_, OnLayoutChange(_, _)).WillOnce(Return(S_OK)); - - // OnTextChange will be called once after LockGranted3(). - EXPECT_CALL(*sink_, OnTextChange(_, _)).WillOnce(Return(S_OK)); + .WillOnce(Invoke(&callback, &ScenarioTestCallback::LockGranted2)); HRESULT result = kInvalidResult; EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); @@ -1140,8 +1182,6 @@ TEST_F(TSFTextStoreTest, ScenarioTest) { EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); EXPECT_EQ(S_OK, result); result = kInvalidResult; - EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); - EXPECT_EQ(S_OK, result); } class GetTextExtTestCallback : public TSFTextStoreTestCallback { @@ -1164,8 +1204,8 @@ class GetTextExtTestCallback : public TSFTextStoreTestCallback { GetTextExtTest(view_cookie, 10, 10, 110, 12, 110, 20); GetTextExtTest(view_cookie, 11, 11, 20, 112, 20, 120); GetTextExtTest(view_cookie, 11, 12, 21, 112, 30, 120); - GetTextExtTest(view_cookie, 9, 12, 101, 12, 30, 120); - GetTextExtTest(view_cookie, 9, 13, 101, 12, 40, 120); + GetTextExtTest(view_cookie, 9, 12, 101, 12, 101, 120); + GetTextExtTest(view_cookie, 9, 13, 101, 12, 101, 120); GetTextExtTest(view_cookie, 0, 13, 11, 12, 40, 120); GetTextExtTest(view_cookie, 13, 13, 40, 112, 40, 120); @@ -1300,5 +1340,671 @@ TEST_F(TSFTextStoreTest, RetrieveRequestedAttrs) { } } +class KeyEventTestCallback : public TSFTextStoreTestCallback { + public: + explicit KeyEventTestCallback(TSFTextStore* text_store) + : TSFTextStoreTestCallback(text_store) {} + + HRESULT LockGranted1(DWORD flags) { + SetTextTest(0, 0, L"a", S_OK); + + GetTextTest(0, -1, L"a", 1); + + SetSelectionTest(0, 1, S_OK); + + text_spans()->clear(); + ImeTextSpan text_span; + text_span.start_offset = 0; + text_span.end_offset = 1; + text_span.underline_color = SK_ColorBLACK; + text_span.thickness = ImeTextSpan::Thickness::kThin; + text_span.background_color = SK_ColorTRANSPARENT; + text_spans()->push_back(text_span); + *edit_flag() = true; + *committed_size() = 0; + composition_range()->set_start(0); + composition_range()->set_end(1); + text_store_->OnKeyTraceDown(65u, 1966081u); + *has_composition_range() = true; + + return S_OK; + } + + void SetCompositionText1(const ui::CompositionText& composition) { + EXPECT_EQ(L"a", composition.text); + EXPECT_EQ(0u, composition.selection.start()); + EXPECT_EQ(1u, composition.selection.end()); + ASSERT_EQ(1u, composition.ime_text_spans.size()); + EXPECT_EQ(SK_ColorBLACK, composition.ime_text_spans[0].underline_color); + EXPECT_EQ(SK_ColorTRANSPARENT, + composition.ime_text_spans[0].background_color); + EXPECT_EQ(0u, composition.ime_text_spans[0].start_offset); + EXPECT_EQ(1u, composition.ime_text_spans[0].end_offset); + EXPECT_EQ(ImeTextSpan::Thickness::kThin, + composition.ime_text_spans[0].thickness); + SetHasCompositionText(true); + } + + void DispatchKeyEventForIME1(ui::KeyEvent* key) { + EXPECT_EQ(ui::ET_KEY_PRESSED, key->type()); + EXPECT_EQ(VKEY_PROCESSKEY, key->key_code()); + } + + HRESULT LockGranted2(DWORD flags) { + SetSelectionTest(1, 1, S_OK); + InsertTextAtSelectionTest(L"B", 1, 1, 2, 1, 1, 2); + GetTextTest(0, -1, L"aB", 2); + + text_spans()->clear(); + ImeTextSpan text_span; + text_span.start_offset = 1; + text_span.end_offset = 2; + text_span.underline_color = SK_ColorBLACK; + text_span.thickness = ImeTextSpan::Thickness::kThick; + text_spans()->push_back(text_span); + + *edit_flag() = true; + *committed_size() = 1; + composition_range()->set_start(1); + composition_range()->set_end(2); + + text_store_->OnKeyTraceUp(65u, 1966081u); + text_store_->OnKeyTraceDown(66u, 3145729u); + return S_OK; + } + + void InsertText2(const base::string16& text) { + EXPECT_EQ(L"a", text); + SetHasCompositionText(false); + } + + void SetCompositionText2(const ui::CompositionText& composition) { + EXPECT_EQ(L"B", composition.text); + EXPECT_EQ(0u, composition.selection.start()); + EXPECT_EQ(1u, composition.selection.end()); + ASSERT_EQ(1u, composition.ime_text_spans.size()); + EXPECT_EQ(SK_ColorBLACK, composition.ime_text_spans[0].underline_color); + EXPECT_EQ(0u, composition.ime_text_spans[0].start_offset); + EXPECT_EQ(1u, composition.ime_text_spans[0].end_offset); + EXPECT_EQ(ImeTextSpan::Thickness::kThick, + composition.ime_text_spans[0].thickness); + SetHasCompositionText(true); + } + + void DispatchKeyEventForIME2(ui::KeyEvent* key) { + EXPECT_EQ(ui::ET_KEY_RELEASED, key->type()); + EXPECT_EQ(VKEY_PROCESSKEY, key->key_code()); + } + + void DispatchKeyEventForIME3a(ui::KeyEvent* key) { + EXPECT_EQ(ui::ET_KEY_PRESSED, key->type()); + EXPECT_EQ(VKEY_PROCESSKEY, key->key_code()); + } + + HRESULT LockGranted3(DWORD flags) { + GetTextTest(0, -1, L"aB", 2); + + text_spans()->clear(); + *edit_flag() = true; + *committed_size() = 2; + composition_range()->set_start(0); + composition_range()->set_end(0); + + *has_composition_range() = false; + text_store_->OnKeyTraceUp(66u, 3145729u); + return S_OK; + } + + void InsertText3(const base::string16& text) { + EXPECT_EQ(L"B", text); + SetHasCompositionText(false); + } + + void SetCompositionText3(const ui::CompositionText& composition) { + EXPECT_EQ(L"", composition.text); + EXPECT_EQ(0u, composition.selection.start()); + EXPECT_EQ(0u, composition.selection.end()); + EXPECT_EQ(0u, composition.ime_text_spans.size()); + } + + void DispatchKeyEventForIME3b(ui::KeyEvent* key) { + EXPECT_EQ(ui::ET_KEY_RELEASED, key->type()); + EXPECT_EQ(VKEY_PROCESSKEY, key->key_code()); + } + + HRESULT LockGranted4(DWORD flags) { + text_store_->OnKeyTraceDown(8u, 917505u); + text_store_->OnKeyTraceUp(8u, 917505u); + return S_OK; + } + + private: + DISALLOW_COPY_AND_ASSIGN(KeyEventTestCallback); +}; + +TEST_F(TSFTextStoreTest, KeyEventTest) { + KeyEventTestCallback callback(text_store_.get()); + EXPECT_CALL(text_input_client_, SetCompositionText(_)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::SetCompositionText1)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::SetCompositionText2)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::SetCompositionText3)); + + EXPECT_CALL(text_input_client_, InsertText(_)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::InsertText2)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::InsertText3)); + + EXPECT_CALL(text_input_client_, DispatchKeyEventForIME(_)) + .WillOnce( + Invoke(&callback, &KeyEventTestCallback::DispatchKeyEventForIME1)) + .WillOnce( + Invoke(&callback, &KeyEventTestCallback::DispatchKeyEventForIME2)) + .WillOnce( + Invoke(&callback, &KeyEventTestCallback::DispatchKeyEventForIME3a)) + .WillOnce( + Invoke(&callback, &KeyEventTestCallback::DispatchKeyEventForIME3b)); + + EXPECT_CALL(*sink_, OnLockGranted(_)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::LockGranted1)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::LockGranted2)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::LockGranted3)) + .WillOnce(Invoke(&callback, &KeyEventTestCallback::LockGranted4)); + + ON_CALL(text_input_client_, HasCompositionText()) + .WillByDefault( + Invoke(&callback, &TSFTextStoreTestCallback::HasCompositionText)); + + HRESULT result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); +} + +// Summary of test scenarios: +// 1. renderer proc changes buffer from "" to "a". +// 2. input service changes buffer from "a" to "abcde". +// 3. renderer proc changes buffer from "abcde" to "about". +// 4. renderer proc changes buffer from "about" to "abFGt". +// 5. renderer proc changes buffer from "abFGt" to "aHIGt". +// 6. renderer proc changes buffer from "aHIGt" to "JKLMN". +// 7. renderer proc changes buffer from "JKLMN" to "". +// 8. renderer proc changes buffer from "" to "OPQ". +// 9. renderer proc changes buffer from "OPQ" to "OPR". +// 10. renderer proc changes buffer from "OPR" to "SPR". +class DiffingAlgorithmTestCallback : public TSFTextStoreTestCallback { + public: + explicit DiffingAlgorithmTestCallback(TSFTextStore* text_store) + : TSFTextStoreTestCallback(text_store) {} + + HRESULT LockGranted1(DWORD flags) { + SetTextTest(0, 0, L"", S_OK); + GetTextTest(0, -1, L"", 0); + + SetTextRange(0, 1); + SetTextBuffer(L"a"); + SetSelectionRange(1, 1); + *committed_size() = 1; + return S_OK; + } + + HRESULT OnTextChange1(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(0, pChange->acpStart); + EXPECT_EQ(0, pChange->acpOldEnd); + EXPECT_EQ(1, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted1a(DWORD flags) { + GetTextTest(0, -1, L"a", 1); + + return S_OK; + } + + HRESULT OnSelectionChange1() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted1b(DWORD flags) { + GetSelectionTest(1, 1); + return S_OK; + } + + HRESULT LockGranted2(DWORD flags) { + SetTextTest(1, 1, L"bcde", S_OK); + GetTextTest(0, -1, L"abcde", 5); + SetSelectionTest(5, 5, S_OK); + + *edit_flag() = true; + *committed_size() = 5; + return S_OK; + } + + void InsertText2(const base::string16& text) { + EXPECT_EQ(L"bcde", text); + SetTextRange(0, 5); + SetSelectionRange(5, 5); + SetTextBuffer(L"abcde"); + } + + HRESULT LockGranted3(DWORD flags) { + SetTextRange(0, 5); + SetTextBuffer(L"about"); + SetSelectionRange(0, 5); + return S_OK; + } + + HRESULT OnTextChange3(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(2, pChange->acpStart); + EXPECT_EQ(5, pChange->acpOldEnd); + EXPECT_EQ(5, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted3a(DWORD flags) { + GetTextTest(1, 5, L"bout", 5); + + return S_OK; + } + + HRESULT OnSelectionChange3() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted3b(DWORD flags) { + GetSelectionTest(0, 5); + return S_OK; + } + + HRESULT LockGranted4(DWORD flags) { + SetTextRange(0, 5); + SetTextBuffer(L"abFGt"); + SetSelectionRange(3, 4); + return S_OK; + } + + HRESULT OnTextChange4(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(2, pChange->acpStart); + EXPECT_EQ(4, pChange->acpOldEnd); + EXPECT_EQ(4, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted4a(DWORD flags) { + GetTextTest(2, 4, L"FG", 4); + + return S_OK; + } + + HRESULT OnSelectionChange4() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted4b(DWORD flags) { + GetSelectionTest(3, 4); + return S_OK; + } + + HRESULT LockGranted5(DWORD flags) { + SetTextRange(0, 3); + SetTextBuffer(L"aHI"); + SetSelectionRange(3, 3); + return S_OK; + } + + HRESULT OnTextChange5(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(1, pChange->acpStart); + EXPECT_EQ(5, pChange->acpOldEnd); + EXPECT_EQ(3, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted5a(DWORD flags) { + GetTextTest(1, 3, L"HI", 3); + + return S_OK; + } + + HRESULT OnSelectionChange5() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted5b(DWORD flags) { + GetSelectionTest(3, 3); + return S_OK; + } + + HRESULT LockGranted6(DWORD flags) { + SetTextRange(0, 5); + SetTextBuffer(L"JKLMN"); + SetSelectionRange(2, 5); + return S_OK; + } + + HRESULT OnTextChange6(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(0, pChange->acpStart); + EXPECT_EQ(3, pChange->acpOldEnd); + EXPECT_EQ(5, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted6a(DWORD flags) { + GetTextTest(3, 5, L"MN", 5); + + return S_OK; + } + + HRESULT OnSelectionChange6() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted6b(DWORD flags) { + GetSelectionTest(2, 5); + return S_OK; + } + + HRESULT LockGranted7(DWORD flags) { + SetTextRange(0, 0); + SetTextBuffer(L""); + SetSelectionRange(0, 0); + return S_OK; + } + + HRESULT OnTextChange7(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(0, pChange->acpStart); + EXPECT_EQ(5, pChange->acpOldEnd); + EXPECT_EQ(0, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted7a(DWORD flags) { + GetTextTest(0, -1, L"", 0); + + return S_OK; + } + + HRESULT OnSelectionChange7() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted7b(DWORD flags) { + GetSelectionTest(0, 0); + return S_OK; + } + + HRESULT LockGranted8(DWORD flags) { + SetTextRange(0, 3); + SetTextBuffer(L"OPQ"); + SetSelectionRange(0, 2); + return S_OK; + } + + HRESULT OnTextChange8(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(0, pChange->acpStart); + EXPECT_EQ(0, pChange->acpOldEnd); + EXPECT_EQ(3, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted8a(DWORD flags) { + GetTextTest(0, -1, L"OPQ", 3); + + return S_OK; + } + + HRESULT OnSelectionChange8() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted8b(DWORD flags) { + GetSelectionTest(0, 2); + return S_OK; + } + + HRESULT LockGranted9(DWORD flags) { + SetTextRange(0, 3); + SetTextBuffer(L"OPR"); + SetSelectionRange(2, 3); + return S_OK; + } + + HRESULT OnTextChange9(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(2, pChange->acpStart); + EXPECT_EQ(3, pChange->acpOldEnd); + EXPECT_EQ(3, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted9a(DWORD flags) { + GetTextTest(2, 3, L"R", 3); + + return S_OK; + } + + HRESULT OnSelectionChange9() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted9b(DWORD flags) { + GetSelectionTest(2, 3); + return S_OK; + } + + HRESULT LockGranted10(DWORD flags) { + SetTextRange(0, 3); + SetTextBuffer(L"SPR"); + SetSelectionRange(0, 1); + return S_OK; + } + + HRESULT OnTextChange10(DWORD flag, const TS_TEXTCHANGE* pChange) { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(0, pChange->acpStart); + EXPECT_EQ(1, pChange->acpOldEnd); + EXPECT_EQ(1, pChange->acpNewEnd); + + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted10a(DWORD flags) { + GetTextTest(0, 1, L"S", 1); + + return S_OK; + } + + HRESULT OnSelectionChange10() { + HRESULT result = S_OK; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + return S_OK; + } + + HRESULT LockGranted10b(DWORD flags) { + GetSelectionTest(0, 1); + return S_OK; + } + + private: + DISALLOW_COPY_AND_ASSIGN(DiffingAlgorithmTestCallback); +}; + +TEST_F(TSFTextStoreTest, DiffingAlgorithmTest) { + DiffingAlgorithmTestCallback callback(text_store_.get()); + + EXPECT_CALL(*sink_, OnTextChange(_, _)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange1)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange3)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange4)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange5)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange6)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange7)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange8)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange9)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnTextChange10)); + + EXPECT_CALL(*sink_, OnSelectionChange()) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnSelectionChange1)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnSelectionChange3)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnSelectionChange4)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnSelectionChange5)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnSelectionChange6)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnSelectionChange7)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnSelectionChange8)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::OnSelectionChange9)) + .WillOnce(Invoke(&callback, + &DiffingAlgorithmTestCallback::OnSelectionChange10)); + + EXPECT_CALL(text_input_client_, InsertText(_)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::InsertText2)); + + EXPECT_CALL(*sink_, OnLockGranted(_)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted1)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted1a)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted1b)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted2)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted3)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted3a)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted3b)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted4)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted4a)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted4b)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted5)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted5a)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted5b)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted6)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted6a)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted6b)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted7)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted7a)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted7b)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted8)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted8a)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted8b)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted9)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted9a)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted9b)) + .WillOnce(Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted10)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted10a)) + .WillOnce( + Invoke(&callback, &DiffingAlgorithmTestCallback::LockGranted10b)); + + ON_CALL(text_input_client_, GetTextRange(_)) + .WillByDefault( + Invoke(&callback, &TSFTextStoreTestCallback::GetTextRange)); + + ON_CALL(text_input_client_, GetTextFromRange(_, _)) + .WillByDefault( + Invoke(&callback, &TSFTextStoreTestCallback::GetTextFromRange)); + + ON_CALL(text_input_client_, GetEditableSelectionRange(_)) + .WillByDefault(Invoke( + &callback, &TSFTextStoreTestCallback::GetEditableSelectionRange)); + + HRESULT result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; + EXPECT_EQ(S_OK, text_store_->RequestLock(TS_LF_READWRITE, &result)); + EXPECT_EQ(S_OK, result); + result = kInvalidResult; +} + } // namespace } // namespace ui