1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
|
// 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_result.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 PrefService* pref_service) 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 AutocompleteResult& result,
const PrefService* pref_service,
TemplateURLService* template_url_service,
Direction direction,
Step step,
bool force_hide_row_header) 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(result, pref_service,
template_url_service, direction, step,
force_hide_row_header);
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 AutocompleteResult& result,
const PrefService* pref_service,
TemplateURLService* template_url_service,
Direction direction,
Step step,
bool force_hide_row_header) {
// 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;
if (step == kWholeLine || step == kAllLines) {
all_states.push_back(NORMAL);
// Whole line stepping can go straight into keyword mode.
all_states.push_back(KEYWORD_MODE);
} else {
all_states.push_back(NORMAL);
all_states.push_back(KEYWORD_MODE);
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
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);
}
DCHECK(std::is_sorted(all_states.begin(), all_states.end()))
<< "This algorithm depends on a sorted list of line states.";
// Now, for each accessible line, add all the available line states to a list.
std::vector<OmniboxPopupSelection> available_selections;
for (size_t line_number = 0; line_number < result.size(); ++line_number) {
for (LineState line_state : all_states) {
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, pref_service)) {
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, pref_service)) {
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, pref_service)) {
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;
}
|