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