blob: 7109fc87f9cd33df3501af97704cfa0e109e00ca [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/omnibox/model/omnibox_text_model.h"
#import "base/metrics/user_metrics.h"
#import "base/trace_event/trace_event.h"
#import "ios/chrome/browser/omnibox/model/omnibox_text_controller.h"
OmniboxTextModel::OmniboxTextModel(OmniboxClient* client)
: omnibox_client(client),
focus_state(OMNIBOX_FOCUS_NONE),
user_input_in_progress(false),
user_text(u""),
focus_resulted_in_navigation(false),
just_deleted_text(false),
inline_autocompletion(u""),
paste_state(OmniboxPasteState::kNone) {}
OmniboxTextModel::~OmniboxTextModel() = default;
bool OmniboxTextModel::SetInputInProgressNoNotify(bool in_progress) {
if (user_input_in_progress == in_progress) {
return false;
}
user_input_in_progress = in_progress;
if (user_input_in_progress) {
time_user_first_modified_omnibox = base::TimeTicks::Now();
base::RecordAction(base::UserMetricsAction("OmniboxInputInProgress"));
}
return true;
}
bool OmniboxTextModel::HasFocus() {
return focus_state != OMNIBOX_FOCUS_NONE;
}
void OmniboxTextModel::KillFocus() {
focus_state = OMNIBOX_FOCUS_NONE;
last_omnibox_focus = base::TimeTicks();
paste_state = OmniboxPasteState::kNone;
if (omnibox_client) {
omnibox_client->OnFocusChanged(focus_state, OMNIBOX_FOCUS_CHANGE_EXPLICIT);
}
}
void OmniboxTextModel::OnSetFocus() {
TRACE_EVENT0("omnibox", "OmniboxTextModel::OnSetFocus");
last_omnibox_focus = base::TimeTicks::Now();
focus_resulted_in_navigation = false;
// If the omnibox lost focus while the caret was hidden and then regained
// focus, OnSetFocus() is called and should restore visibility. Note that
// focus can be regained without an accompanying call to
// OmniboxViewIOS::SetFocus(), e.g. by tabbing in.
SetFocusState(OMNIBOX_FOCUS_VISIBLE, OMNIBOX_FOCUS_CHANGE_EXPLICIT);
if (user_input_in_progress || !in_revert) {
omnibox_client->OnInputStateChanged();
}
}
void OmniboxTextModel::SetFocusState(OmniboxFocusState state,
OmniboxFocusChangeReason reason) {
if (state == focus_state) {
return;
}
focus_state = state;
omnibox_client->OnFocusChanged(focus_state, reason);
}
void OmniboxTextModel::UpdateUserText(const std::u16string& text) {
user_text = text;
just_deleted_text = false;
inline_autocompletion.clear();
}
bool OmniboxTextModel::UpdateStateAfterPossibleChange(
const OmniboxStateChanges& state_changes) {
// Update the paste state as appropriate: if we're just finishing a paste
// that replaced all the text, preserve that information; otherwise, if we've
// made some other edit, clear paste tracking.
if (paste_state == OmniboxPasteState::kPasting) {
paste_state = OmniboxPasteState::kPasted;
GURL url = GURL(*(state_changes.new_text));
if (url.is_valid() && omnibox_client) {
omnibox_client->OnUserPastedInOmniboxResultingInValidURL();
}
} else if (state_changes.text_differs) {
paste_state = OmniboxPasteState::kNone;
}
if (state_changes.text_differs || state_changes.selection_differs) {
// Restore caret visibility whenever the user changes text or selection in
// the omnibox.
SetFocusState(OMNIBOX_FOCUS_VISIBLE, OMNIBOX_FOCUS_CHANGE_TYPING);
}
// If the user text does not need to be changed, return now, so we don't
// change any other state, lest arrowing around the omnibox do something like
// reset `just_deleted_text_`. Note that modifying the selection accepts any
// inline autocompletion, which results in a user text change.
if (!state_changes.text_differs &&
(!state_changes.selection_differs || inline_autocompletion.empty())) {
return false;
}
UpdateUserText(*state_changes.new_text);
just_deleted_text = state_changes.just_deleted_text;
return true;
}
OmniboxStateChanges OmniboxTextModel::GetStateChanges(
const OmniboxTextState& before,
const OmniboxTextState& after) const {
OmniboxStateChanges state_changes;
state_changes.old_text = &before.text;
state_changes.new_text = &after.text;
state_changes.new_sel_start = after.sel_start;
state_changes.new_sel_end = after.sel_end;
const bool old_sel_empty = before.sel_start == before.sel_end;
const bool new_sel_empty = after.sel_start == after.sel_end;
const bool sel_same_ignoring_direction =
std::min(before.sel_start, before.sel_end) ==
std::min(after.sel_start, after.sel_end) &&
std::max(before.sel_start, before.sel_end) ==
std::max(after.sel_start, after.sel_end);
state_changes.selection_differs =
(!old_sel_empty || !new_sel_empty) && !sel_same_ignoring_direction;
state_changes.text_differs = before.text != after.text;
state_changes.just_deleted_text =
before.text.length() > after.text.length() &&
// Check that the cursor is at or before the start of the old selection.
// This ensures that if the user selected text and typed, it's not
// considered a deletion even if the new text is shorter.
after.sel_start <= std::min(before.sel_start, before.sel_end);
return state_changes;
}