123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- // Copyright 2018 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- #include "ui/views/cocoa/text_input_host.h"
- #include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
- #include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
- #include "ui/base/ime/text_input_client.h"
- #include "ui/events/keycodes/dom/dom_code.h"
- #include "ui/views/cocoa/native_widget_mac_ns_window_host.h"
- namespace {
- // Returns the boundary rectangle for composition characters in the
- // |requested_range|. Sets |actual_range| corresponding to the returned
- // rectangle. For cases, where there is no composition text or the
- // |requested_range| lies outside the composition range, a zero width rectangle
- // corresponding to the caret bounds is returned. Logic used is similar to
- // RenderWidgetHostViewMac::GetCachedFirstRectForCharacterRange(...).
- gfx::Rect GetFirstRectForRangeHelper(const ui::TextInputClient* client,
- const gfx::Range& requested_range,
- gfx::Range* actual_range) {
- // NSRange doesn't support reversed ranges.
- DCHECK(!requested_range.is_reversed());
- DCHECK(actual_range);
- // Set up default return values, to be returned in case of unusual cases.
- gfx::Rect default_rect;
- *actual_range = gfx::Range::InvalidRange();
- if (!client)
- return default_rect;
- default_rect = client->GetCaretBounds();
- default_rect.set_width(0);
- // If possible, modify actual_range to correspond to caret position.
- gfx::Range selection_range;
- if (client->GetEditableSelectionRange(&selection_range)) {
- // Caret bounds correspond to end index of selection_range.
- *actual_range = gfx::Range(selection_range.end());
- }
- gfx::Range composition_range;
- if (!client->HasCompositionText() ||
- !client->GetCompositionTextRange(&composition_range) ||
- !requested_range.IsBoundedBy(composition_range)) {
- return default_rect;
- }
- DCHECK(!composition_range.is_reversed());
- const size_t from = requested_range.start() - composition_range.start();
- const size_t to = requested_range.end() - composition_range.start();
- // Pick the first character's bounds as the initial rectangle, then grow it to
- // the full |requested_range| if possible.
- const bool request_is_composition_end = from == composition_range.length();
- const size_t first_index = request_is_composition_end ? from - 1 : from;
- gfx::Rect union_rect;
- if (!client->GetCompositionCharacterBounds(first_index, &union_rect))
- return default_rect;
- // If requested_range is empty, return a zero width rectangle corresponding to
- // it.
- if (from == to) {
- if (request_is_composition_end &&
- client->GetTextDirection() != base::i18n::RIGHT_TO_LEFT) {
- // In case of an empty requested range at end of composition, return the
- // rectangle to the right of the last compositioned character.
- union_rect.set_origin(union_rect.top_right());
- }
- union_rect.set_width(0);
- *actual_range = requested_range;
- return union_rect;
- }
- // Toolkit-views textfields are always single-line, so no need to check for
- // line breaks.
- for (size_t i = from + 1; i < to; i++) {
- gfx::Rect current_rect;
- if (client->GetCompositionCharacterBounds(i, ¤t_rect)) {
- union_rect.Union(current_rect);
- } else {
- *actual_range =
- gfx::Range(requested_range.start(), i + composition_range.start());
- return union_rect;
- }
- }
- *actual_range = requested_range;
- return union_rect;
- }
- // Returns the string corresponding to |requested_range| for the given |client|.
- // If a gfx::Range::InvalidRange() is passed, the full string stored by |client|
- // is returned. Sets |actual_range| corresponding to the returned string.
- std::u16string AttributedSubstringForRangeHelper(
- const ui::TextInputClient* client,
- const gfx::Range& requested_range,
- gfx::Range* actual_range) {
- // NSRange doesn't support reversed ranges.
- DCHECK(!requested_range.is_reversed());
- DCHECK(actual_range);
- std::u16string substring;
- gfx::Range text_range;
- *actual_range = gfx::Range::InvalidRange();
- if (!client || !client->GetTextRange(&text_range))
- return substring;
- // gfx::Range::Intersect() behaves a bit weirdly. If B is an empty range
- // contained inside a non-empty range A, B intersection A returns
- // gfx::Range::InvalidRange(), instead of returning B.
- *actual_range = text_range.Contains(requested_range)
- ? requested_range
- : text_range.Intersect(requested_range);
- // This is a special case for which the complete string should should be
- // returned. NSTextView also follows this, though the same is not mentioned in
- // NSTextInputClient documentation.
- if (!requested_range.IsValid())
- *actual_range = text_range;
- client->GetTextFromRange(*actual_range, &substring);
- return substring;
- }
- } // namespace
- namespace views {
- ////////////////////////////////////////////////////////////////////////////////
- // TextInputHost, public:
- TextInputHost::TextInputHost(NativeWidgetMacNSWindowHost* host_impl)
- : host_impl_(host_impl) {}
- TextInputHost::~TextInputHost() = default;
- void TextInputHost::BindReceiver(
- mojo::PendingAssociatedReceiver<remote_cocoa::mojom::TextInputHost>
- receiver) {
- mojo_receiver_.Bind(std::move(receiver),
- ui::WindowResizeHelperMac::Get()->task_runner());
- }
- ui::TextInputClient* TextInputHost::GetTextInputClient() const {
- return text_input_client_;
- }
- void TextInputHost::SetTextInputClient(
- ui::TextInputClient* new_text_input_client) {
- if (pending_text_input_client_ == new_text_input_client)
- return;
- // This method may cause the IME window to dismiss, which may cause it to
- // insert text (e.g. to replace marked text with "real" text). That should
- // happen in the old -inputContext (which AppKit stores a reference to).
- // Unfortunately, the only way to invalidate the the old -inputContext is to
- // invoke -[NSApp updateWindows], which also wants a reference to the _new_
- // -inputContext. So put the new inputContext in |pendingTextInputClient_| and
- // only use it for -inputContext.
- ui::TextInputClient* old_text_input_client = text_input_client_;
- // Since dismissing an IME may insert text, a misbehaving IME or a
- // ui::TextInputClient that acts on InsertChar() to change focus a second time
- // may invoke -setTextInputClient: recursively; with [NSApp updateWindows]
- // still on the stack. Calling [NSApp updateWindows] recursively may upset
- // an IME. Since the rest of this method is only to decide whether to call
- // updateWindows, and we're already calling it, just bail out.
- if (text_input_client_ != pending_text_input_client_) {
- pending_text_input_client_ = new_text_input_client;
- return;
- }
- // Start by assuming no need to invoke -updateWindows.
- text_input_client_ = new_text_input_client;
- pending_text_input_client_ = new_text_input_client;
- if (host_impl_->in_process_ns_window_bridge_ &&
- host_impl_->in_process_ns_window_bridge_->NeedsUpdateWindows()) {
- text_input_client_ = old_text_input_client;
- [NSApp updateWindows];
- // Note: |pending_text_input_client_| (and therefore +[NSTextInputContext
- // currentInputContext] may have changed if called recursively.
- text_input_client_ = pending_text_input_client_;
- }
- }
- ////////////////////////////////////////////////////////////////////////////////
- // TextInputHost, remote_cocoa::mojom::TextInputHost:
- bool TextInputHost::HasClient(bool* out_has_client) {
- *out_has_client = text_input_client_ != nullptr;
- return true;
- }
- bool TextInputHost::HasInputContext(bool* out_has_input_context) {
- *out_has_input_context = false;
- // If the textInputClient_ does not exist, return nil since this view does not
- // conform to NSTextInputClient protocol.
- if (!pending_text_input_client_)
- return true;
- // If a menu is active, and -[NSView interpretKeyEvents:] asks for the
- // input context, return nil. This ensures the action message is sent to
- // the view, rather than any NSTextInputClient a subview has installed.
- bool has_menu_controller = false;
- host_impl_->GetHasMenuController(&has_menu_controller);
- if (has_menu_controller)
- return true;
- // When not in an editable mode, or while entering passwords
- // (http://crbug.com/23219), we don't want to show IME candidate windows.
- // Returning nil prevents this view from getting messages defined as part of
- // the NSTextInputClient protocol.
- switch (pending_text_input_client_->GetTextInputType()) {
- case ui::TEXT_INPUT_TYPE_NONE:
- case ui::TEXT_INPUT_TYPE_PASSWORD:
- return true;
- default:
- *out_has_input_context = true;
- }
- return true;
- }
- bool TextInputHost::IsRTL(bool* out_is_rtl) {
- *out_is_rtl = text_input_client_ && text_input_client_->GetTextDirection() ==
- base::i18n::RIGHT_TO_LEFT;
- return true;
- }
- bool TextInputHost::GetSelectionRange(gfx::Range* out_range) {
- if (!text_input_client_ ||
- !text_input_client_->GetEditableSelectionRange(out_range)) {
- *out_range = gfx::Range::InvalidRange();
- }
- return true;
- }
- bool TextInputHost::GetSelectionText(bool* out_result,
- std::u16string* out_text) {
- *out_result = false;
- if (!text_input_client_)
- return true;
- gfx::Range selection_range;
- if (!text_input_client_->GetEditableSelectionRange(&selection_range))
- return true;
- *out_result = text_input_client_->GetTextFromRange(selection_range, out_text);
- return true;
- }
- void TextInputHost::InsertText(const std::u16string& text, bool as_character) {
- if (!text_input_client_)
- return;
- if (as_character) {
- // If a single character is inserted by keyDown's call to
- // interpretKeyEvents: then use InsertChar() to allow editing events to be
- // merged. We use ui::VKEY_UNKNOWN as the key code since it's not feasible
- // to determine the correct key code for each unicode character. Also a
- // correct keycode is not needed in the current context. Send ui::EF_NONE as
- // the key modifier since |text| already accounts for the pressed key
- // modifiers.
- text_input_client_->InsertChar(ui::KeyEvent(
- text[0], ui::VKEY_UNKNOWN, ui::DomCode::NONE, ui::EF_NONE));
- } else {
- text_input_client_->InsertText(
- text,
- ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
- }
- }
- void TextInputHost::DeleteRange(const gfx::Range& range) {
- if (!text_input_client_)
- return;
- text_input_client_->DeleteRange(range);
- }
- void TextInputHost::SetCompositionText(const std::u16string& text,
- const gfx::Range& selected_range,
- const gfx::Range& replacement_range) {
- if (!text_input_client_)
- return;
- text_input_client_->DeleteRange(replacement_range);
- ui::CompositionText composition;
- composition.text = text;
- composition.selection = selected_range;
- // Add an underline with text color and a transparent background to the
- // composition text. TODO(karandeepb): On Cocoa textfields, the target clause
- // of the composition has a thick underlines. The composition text also has
- // discontinuous underlines for different clauses. This is also supported in
- // the Chrome renderer. Add code to extract underlines from |text| once our
- // render text implementation supports thick underlines and discontinuous
- // underlines for consecutive characters. See http://crbug.com/612675.
- composition.ime_text_spans.push_back(ui::ImeTextSpan(
- ui::ImeTextSpan::Type::kComposition, 0, text.length(),
- ui::ImeTextSpan::Thickness::kThin,
- ui::ImeTextSpan::UnderlineStyle::kSolid, SK_ColorTRANSPARENT));
- text_input_client_->SetCompositionText(composition);
- }
- void TextInputHost::ConfirmCompositionText() {
- if (!text_input_client_)
- return;
- text_input_client_->ConfirmCompositionText(/* keep_selection */ false);
- }
- bool TextInputHost::HasCompositionText(bool* out_has_composition_text) {
- *out_has_composition_text = false;
- if (!text_input_client_)
- return true;
- *out_has_composition_text = text_input_client_->HasCompositionText();
- return true;
- }
- bool TextInputHost::GetCompositionTextRange(gfx::Range* out_composition_range) {
- *out_composition_range = gfx::Range::InvalidRange();
- if (!text_input_client_)
- return true;
- if (!text_input_client_->HasCompositionText())
- return true;
- text_input_client_->GetCompositionTextRange(out_composition_range);
- return true;
- }
- bool TextInputHost::GetAttributedSubstringForRange(
- const gfx::Range& requested_range,
- std::u16string* out_text,
- gfx::Range* out_actual_range) {
- *out_text = AttributedSubstringForRangeHelper(
- text_input_client_, requested_range, out_actual_range);
- return true;
- }
- bool TextInputHost::GetFirstRectForRange(const gfx::Range& requested_range,
- gfx::Rect* out_rect,
- gfx::Range* out_actual_range) {
- *out_rect = GetFirstRectForRangeHelper(text_input_client_, requested_range,
- out_actual_range);
- return true;
- }
- ////////////////////////////////////////////////////////////////////////////////
- // TextInputHost, remote_cocoa::mojom::TextInputHost synchronous methods:
- void TextInputHost::HasClient(HasClientCallback callback) {
- bool has_client = false;
- HasClient(&has_client);
- std::move(callback).Run(has_client);
- }
- void TextInputHost::HasInputContext(HasInputContextCallback callback) {
- bool has_input_context = false;
- HasClient(&has_input_context);
- std::move(callback).Run(has_input_context);
- }
- void TextInputHost::IsRTL(IsRTLCallback callback) {
- bool is_rtl = false;
- IsRTL(&is_rtl);
- std::move(callback).Run(is_rtl);
- }
- void TextInputHost::GetSelectionRange(GetSelectionRangeCallback callback) {
- gfx::Range range = gfx::Range::InvalidRange();
- GetSelectionRange(&range);
- std::move(callback).Run(range);
- }
- void TextInputHost::GetSelectionText(GetSelectionTextCallback callback) {
- bool result = false;
- std::u16string text;
- GetSelectionText(&result, &text);
- std::move(callback).Run(result, text);
- }
- void TextInputHost::HasCompositionText(HasCompositionTextCallback callback) {
- bool has_composition_text = false;
- HasCompositionText(&has_composition_text);
- std::move(callback).Run(has_composition_text);
- }
- void TextInputHost::GetCompositionTextRange(
- GetCompositionTextRangeCallback callback) {
- gfx::Range range = gfx::Range::InvalidRange();
- GetCompositionTextRange(&range);
- std::move(callback).Run(range);
- }
- void TextInputHost::GetAttributedSubstringForRange(
- const gfx::Range& requested_range,
- GetAttributedSubstringForRangeCallback callback) {
- std::u16string text;
- gfx::Range actual_range = gfx::Range::InvalidRange();
- GetAttributedSubstringForRange(requested_range, &text, &actual_range);
- std::move(callback).Run(text, actual_range);
- }
- void TextInputHost::GetFirstRectForRange(
- const gfx::Range& requested_range,
- GetFirstRectForRangeCallback callback) {
- gfx::Rect rect;
- gfx::Range actual_range;
- GetFirstRectForRange(requested_range, &rect, &actual_range);
- std::move(callback).Run(rect, actual_range);
- }
- } // namespace views
|