blob: 540a8ef5d73f32cc0fbd082b4a84c03bd7aaa886 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/omnibox/browser/omnibox_popup_selection.h"
#include <algorithm>
#include "build/build_config.h"
#include "components/omnibox/browser/actions/omnibox_action.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/autocomplete_result.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/search_engines/template_url_service.h"
constexpr bool kIsDesktop = !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS);
constexpr size_t OmniboxPopupSelection::kNoMatch = static_cast<size_t>(-1);
bool OmniboxPopupSelection::IsChangeToKeyword(
OmniboxPopupSelection from) const {
return state == KEYWORD_MODE && from.state != KEYWORD_MODE;
}
bool OmniboxPopupSelection::IsButtonFocused() const {
return state != NORMAL && state != KEYWORD_MODE;
}
bool OmniboxPopupSelection::IsAction() const {
return state == FOCUSED_BUTTON_ACTION;
}
bool OmniboxPopupSelection::IsControlPresentOnMatch(
const AutocompleteResult& result) const {
if (line >= result.size()) {
return false;
}
const auto& match = result.match_at(line);
switch (state) {
case NORMAL:
// `NULL_RESULT_MESSAGE` cannot be focused.
return match.type != AutocompleteMatchType::NULL_RESULT_MESSAGE;
case KEYWORD_MODE:
return match.associated_keyword != nullptr;
case FOCUSED_BUTTON_ACTION: {
// Actions buttons should not be shown in keyword mode.
return !match.from_keyword && action_index < match.actions.size();
}
case FOCUSED_BUTTON_THUMBS_UP:
case FOCUSED_BUTTON_THUMBS_DOWN:
return match.type == AutocompleteMatchType::HISTORY_EMBEDDINGS;
case FOCUSED_BUTTON_REMOVE_SUGGESTION:
return match.SupportsDeletion();
case FOCUSED_IPH_LINK:
return match.IsIphSuggestion() && !match.iph_link_url.is_empty();
default:
break;
}
NOTREACHED();
}
OmniboxPopupSelection OmniboxPopupSelection::GetNextSelection(
const AutocompleteInput& input,
const AutocompleteResult& result,
TemplateURLService* template_url_service,
const AutocompleteProviderClient* client,
Direction direction,
Step step) const {
if (result.empty()) {
return *this;
}
// Implementing this was like a Google Interview Problem. It was always a
// tough problem to handle all the cases, but has gotten much harder since
// we can now hide whole rows from view by collapsing sections.
//
// The only sane thing to do is to first enumerate all available selections.
// Other approaches I've tried all end up being a jungle of branching code.
// It's not necessarily optimal to generate this list for each keypress, but
// in practice it's only something like ~10 elements long, and makes the code
// easy to reason about.
std::vector<OmniboxPopupSelection> all_available_selections =
GetAllAvailableSelectionsSorted(input, result, template_url_service,
client, step);
if (all_available_selections.empty()) {
return *this;
}
// Handle the simple case of just getting the first or last element.
if (step == kAllLines) {
return direction == kForward ? all_available_selections.back()
: all_available_selections.front();
}
if (direction == kForward) {
// To go forward, we want to change to the first selection that's larger
// than the current selection, and std::upper_bound() does just
// that.
const auto next = std::upper_bound(all_available_selections.begin(),
all_available_selections.end(), *this);
// If we can't find any selections larger than the current
// selection, wrap.
if (next == all_available_selections.end())
return all_available_selections.front();
// Normal case where we found the next selection.
return *next;
} else if (direction == kBackward) {
// To go backwards, decrement one from std::lower_bound(), which finds the
// current selection. I didn't use std::find() here, because
// std::lower_bound() can gracefully handle the case where
// selection is no longer within the list of available selections.
const auto current =
std::lower_bound(all_available_selections.begin(),
all_available_selections.end(), *this);
// If the current selection is the first one, wrap.
if (current == all_available_selections.begin()) {
return all_available_selections.back();
}
// Decrement one from the current selection.
return *(current - 1);
}
NOTREACHED();
}
// static
std::vector<OmniboxPopupSelection>
OmniboxPopupSelection::GetAllAvailableSelectionsSorted(
const AutocompleteInput& input,
const AutocompleteResult& result,
TemplateURLService* template_url_service,
const AutocompleteProviderClient* client,
Step step) {
// First enumerate all the accessible states based on `direction` and `step`,
// as well as enabled feature flags. This doesn't mean each match will have
// all of these states - just that it's possible to get there, if available.
std::vector<LineState> all_states;
switch (step) {
case kWholeLine:
case kAllLines:
all_states.push_back(NORMAL);
// Whole line stepping can go straight into keyword mode.
all_states.push_back(KEYWORD_MODE);
break;
case kStateOrLine:
all_states.push_back(NORMAL);
all_states.push_back(KEYWORD_MODE);
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
all_states.push_back(FOCUSED_BUTTON_AIM);
all_states.push_back(FOCUSED_BUTTON_ACTION);
#endif
all_states.push_back(FOCUSED_BUTTON_THUMBS_UP);
all_states.push_back(FOCUSED_BUTTON_THUMBS_DOWN);
all_states.push_back(FOCUSED_BUTTON_REMOVE_SUGGESTION);
all_states.push_back(FOCUSED_IPH_LINK);
break;
}
DCHECK(std::is_sorted(all_states.begin(), all_states.end()))
<< "Line states must be added in sorted order for the algorithm to work.";
std::vector<OmniboxPopupSelection> available_selections;
const bool aim_button_enabled =
OmniboxFieldTrial::IsAimOmniboxEntrypointEnabled(client);
// The AIM button is included as a special case selection on the `kNoMatch`
// line if:
// - The AIM button is enabled,
// - The user is moving focus with tab or shift-tab (`kStateOrLine`).
// - The user is in zero suggest state. When they're not in zero suggest, the
// AIM button selection will instead be added to the default match, below.
// Note that the ordering logic in `operator<=>` ensures that `kNoMatch` comes
// before other selections.
if (aim_button_enabled && step == kStateOrLine && input.IsZeroSuggest()) {
available_selections.emplace_back(kNoMatch, FOCUSED_BUTTON_AIM);
}
// Now, for each accessible line, add all the available line states to a list.
for (size_t line_number = 0; line_number < result.size(); ++line_number) {
for (LineState line_state : all_states) {
if (line_state == FOCUSED_BUTTON_AIM) {
// The AIM button is included in the focus order if:
// - The AIM button is enabled,
// - This is the first match (`line_number == 0`),
// - The match is not from a keyword (not in keyword mode),
// - The input is not ZeroSuggest (i.e., the user has typed something).
if (aim_button_enabled && line_number == 0 &&
!result.match_at(0).from_keyword && !input.IsZeroSuggest()) {
available_selections.emplace_back(line_number, line_state);
}
} else if (line_state == FOCUSED_BUTTON_ACTION) {
constexpr size_t kMaxActionCount = 8;
for (size_t i = 0; i < kMaxActionCount; i++) {
OmniboxPopupSelection selection(line_number, line_state, i);
if (selection.IsControlPresentOnMatch(result)) {
available_selections.push_back(selection);
} else {
// Break early when there are no more actions. Note, this
// implies that a match takeover action should be last
// to allow other actions on the match to be included.
break;
}
}
} else if (line_state == KEYWORD_MODE && kIsDesktop) {
OmniboxPopupSelection selection(line_number, line_state);
if (selection.IsControlPresentOnMatch(result)) {
if (result.match_at(line_number)
.HasInstantKeyword(template_url_service)) {
if (available_selections.size() > 0 &&
available_selections.back().line == line_number &&
available_selections.back().state == LineState::NORMAL) {
// Remove the preceding normal state selection so that keyword
// mode will be entered immediately when the user arrows down
// to this keyword line.
available_selections.pop_back();
}
available_selections.push_back(selection);
} else if (step == kStateOrLine) {
available_selections.push_back(selection);
}
}
} else {
OmniboxPopupSelection selection(line_number, line_state);
if (selection.IsControlPresentOnMatch(result)) {
available_selections.push_back(selection);
}
}
}
}
DCHECK(
std::is_sorted(available_selections.begin(), available_selections.end()))
<< "This algorithm depends on a sorted list of available selections.";
return available_selections;
}