blob: d9d6e933755bdaa72f128b6ea0afdcdd6b8f940b [file] [log] [blame]
// Copyright (c) 2012 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 "components/omnibox/browser/omnibox_popup_model.h"
#include <algorithm>
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/omnibox/browser/omnibox_client.h"
#include "components/omnibox/browser/omnibox_edit_controller.h"
#include "components/omnibox/browser/omnibox_popup_view.h"
#include "components/omnibox/common/omnibox_features.h"
#include "third_party/icu/source/common/unicode/ubidi.h"
#include "ui/gfx/geometry/rect.h"
#if !defined(OS_ANDROID) && !defined(OS_IOS)
#include "components/omnibox/browser/vector_icons.h" // nogncheck
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_types.h"
#endif
///////////////////////////////////////////////////////////////////////////////
// OmniboxPopupModel
const size_t OmniboxPopupModel::kNoMatch = static_cast<size_t>(-1);
OmniboxPopupModel::OmniboxPopupModel(OmniboxPopupView* popup_view,
OmniboxEditModel* edit_model)
: view_(popup_view),
edit_model_(edit_model),
selection_(kNoMatch, NORMAL),
has_selected_match_(false) {
edit_model->set_popup_model(this);
}
OmniboxPopupModel::~OmniboxPopupModel() = default;
// static
void OmniboxPopupModel::ComputeMatchMaxWidths(int contents_width,
int separator_width,
int description_width,
int available_width,
bool description_on_separate_line,
bool allow_shrinking_contents,
int* contents_max_width,
int* description_max_width) {
available_width = std::max(available_width, 0);
*contents_max_width = std::min(contents_width, available_width);
*description_max_width = std::min(description_width, available_width);
// If the description is empty, or the contents and description are on
// separate lines, each can get the full available width.
if (!description_width || description_on_separate_line)
return;
// If we want to display the description, we need to reserve enough space for
// the separator.
available_width -= separator_width;
if (available_width < 0) {
*description_max_width = 0;
return;
}
if (contents_width + description_width > available_width) {
if (allow_shrinking_contents) {
// Try to split the available space fairly between contents and
// description (if one wants less than half, give it all it wants and
// give the other the remaining space; otherwise, give each half).
// However, if this makes the contents too narrow to show a significant
// amount of information, give the contents more space.
*contents_max_width = std::max(
(available_width + 1) / 2, available_width - description_width);
const int kMinimumContentsWidth = 300;
*contents_max_width = std::min(
std::min(std::max(*contents_max_width, kMinimumContentsWidth),
contents_width),
available_width);
}
// Give the description the remaining space, unless this makes it too small
// to display anything meaningful, in which case just hide the description
// and let the contents take up the whole width.
*description_max_width =
std::min(description_width, available_width - *contents_max_width);
const int kMinimumDescriptionWidth = 75;
if (*description_max_width <
std::min(description_width, kMinimumDescriptionWidth)) {
*description_max_width = 0;
// Since we're not going to display the description, the contents can have
// the space we reserved for the separator.
available_width += separator_width;
*contents_max_width = std::min(contents_width, available_width);
}
}
}
bool OmniboxPopupModel::IsOpen() const {
return view_->IsOpen();
}
void OmniboxPopupModel::SetSelectedLine(size_t line,
bool reset_to_default,
bool force) {
const AutocompleteResult& result = this->result();
if (result.empty())
return;
// Cancel the query so the matches don't change on the user.
autocomplete_controller()->Stop(false);
if (line != kNoMatch)
line = std::min(line, result.size() - 1);
has_selected_match_ = !reset_to_default;
if (line == selected_line() && !force)
return; // Nothing else to do.
// We need to update selection before calling InvalidateLine(), since it will
// check them to determine how to draw. We also need to update
// |selection_.line| before calling OnPopupDataChanged(), so that when the
// edit notifies its controller that something has changed, the controller
// can get the correct updated data.
const size_t prev_selected_line = selected_line();
SetSelection(Selection(line, NORMAL));
view_->OnSelectedLineChanged(prev_selected_line, selected_line());
if (line == kNoMatch)
return;
// Update the edit with the new data for this match.
// TODO(pkasting): If |selection_.line| moves to the controller, this can be
// eliminated and just become a call to the observer on the edit.
base::string16 keyword;
bool is_keyword_hint;
TemplateURLService* service = edit_model_->client()->GetTemplateURLService();
const AutocompleteMatch& match = result.match_at(line);
match.GetKeywordUIState(service, &keyword, &is_keyword_hint);
if (reset_to_default) {
edit_model_->OnPopupDataChanged(match.inline_autocompletion,
/*is_temporary_text=*/false, keyword,
is_keyword_hint);
} else {
edit_model_->OnPopupDataChanged(match.fill_into_edit,
/*is_temporary_text=*/true, keyword,
is_keyword_hint);
}
}
void OmniboxPopupModel::ResetToInitialState() {
size_t new_line = result().default_match() ? 0 : kNoMatch;
SetSelectedLine(new_line, true, false);
view_->OnDragCanceled();
}
void OmniboxPopupModel::MoveTo(size_t new_line) {
if (result().empty())
return;
SetSelectedLine(new_line, false, false);
}
void OmniboxPopupModel::SetSelectedLineState(LineState state) {
DCHECK(!result().empty());
DCHECK_NE(kNoMatch, selected_line());
const AutocompleteMatch& match = result().match_at(selected_line());
GURL current_destination(match.destination_url);
DCHECK((state != KEYWORD) || match.associated_keyword.get());
if (state == BUTTON_FOCUSED)
old_focused_url_ = current_destination;
// selected_line_state_ = state;
SetSelection(Selection(selected_line(), state));
view_->InvalidateLine(selected_line());
if (state == BUTTON_FOCUSED) {
edit_model_->SetAccessibilityLabel(match);
view_->ProvideButtonFocusHint(selected_line());
}
}
void OmniboxPopupModel::TryDeletingLine(size_t line) {
// When called with line == selected_line(), we could use
// GetInfoForCurrentText() here, but it seems better to try and delete the
// actual selection, rather than any "in progress, not yet visible" one.
if (line == kNoMatch)
return;
// Cancel the query so the matches don't change on the user.
autocomplete_controller()->Stop(false);
const AutocompleteMatch& match = result().match_at(line);
if (match.SupportsDeletion()) {
// Try to preserve the selection even after match deletion.
const size_t old_selected_line = selected_line();
const bool was_temporary_text = has_selected_match_;
// This will synchronously notify both the edit and us that the results
// have changed, causing both to revert to the default match.
autocomplete_controller()->DeleteMatch(match);
const AutocompleteResult& result = this->result();
if (!result.empty() &&
(was_temporary_text || old_selected_line != selected_line())) {
// Move the selection to the next choice after the deleted one.
// SetSelectedLine() will clamp to take care of the case where we deleted
// the last item.
// TODO(pkasting): Eventually the controller should take care of this
// before notifying us, reducing flicker. At that point the check for
// deletability can move there too.
SetSelectedLine(old_selected_line, false, true);
}
}
}
bool OmniboxPopupModel::IsStarredMatch(const AutocompleteMatch& match) const {
auto* bookmark_model = edit_model_->client()->GetBookmarkModel();
return bookmark_model && bookmark_model->IsBookmarked(match.destination_url);
}
void OmniboxPopupModel::OnResultChanged() {
rich_suggestion_bitmaps_.clear();
const AutocompleteResult& result = this->result();
size_t old_selected_line = selected_line();
has_selected_match_ = false;
if (result.default_match()) {
Selection selection(0, selected_line_state());
// If selected line state was |BUTTON_FOCUSED| and nothing has changed,
// leave it.
const bool has_focused_match =
selection.state == BUTTON_FOCUSED &&
result.match_at(selection.line).has_tab_match;
const bool has_changed =
selection.line != old_selected_line ||
result.match_at(selection.line).destination_url != old_focused_url_;
if (!has_focused_match || has_changed) {
selection.state = NORMAL;
}
SetSelection(selection);
} else {
SetSelection(Selection(kNoMatch, NORMAL));
}
bool popup_was_open = view_->IsOpen();
view_->UpdatePopupAppearance();
if (view_->IsOpen() != popup_was_open)
edit_model_->controller()->OnPopupVisibilityChanged();
}
const SkBitmap* OmniboxPopupModel::RichSuggestionBitmapAt(
int result_index) const {
const auto iter = rich_suggestion_bitmaps_.find(result_index);
if (iter == rich_suggestion_bitmaps_.end()) {
return nullptr;
}
return &iter->second;
}
void OmniboxPopupModel::SetRichSuggestionBitmap(int result_index,
const SkBitmap& bitmap) {
rich_suggestion_bitmaps_[result_index] = bitmap;
view_->UpdatePopupAppearance();
}
// Android and iOS have their own platform-specific icon logic.
#if !defined(OS_ANDROID) && !defined(OS_IOS)
gfx::Image OmniboxPopupModel::GetMatchIcon(const AutocompleteMatch& match,
SkColor vector_icon_color) {
gfx::Image extension_icon =
edit_model_->client()->GetIconIfExtensionMatch(match);
// Extension icons are the correct size for non-touch UI but need to be
// adjusted to be the correct size for touch mode.
if (!extension_icon.IsEmpty())
return edit_model_->client()->GetSizedIcon(extension_icon);
// Get the favicon for navigational suggestions.
if (!AutocompleteMatch::IsSearchType(match.type) &&
match.type != AutocompleteMatchType::DOCUMENT_SUGGESTION) {
// Because the Views UI code calls GetMatchIcon in both the layout and
// painting code, we may generate multiple OnFaviconFetched callbacks,
// all run one after another. This seems to be harmless as the callback
// just flips a flag to schedule a repaint. However, if it turns out to be
// costly, we can optimize away the redundant extra callbacks.
gfx::Image favicon = edit_model_->client()->GetFaviconForPageUrl(
match.destination_url,
base::BindOnce(&OmniboxPopupModel::OnFaviconFetched,
weak_factory_.GetWeakPtr(), match.destination_url));
// Extension icons are the correct size for non-touch UI but need to be
// adjusted to be the correct size for touch mode.
if (!favicon.IsEmpty())
return edit_model_->client()->GetSizedIcon(favicon);
}
const auto& vector_icon_type = match.GetVectorIcon(IsStarredMatch(match));
return edit_model_->client()->GetSizedIcon(vector_icon_type,
vector_icon_color);
}
#endif // !defined(OS_ANDROID) && !defined(OS_IOS)
bool OmniboxPopupModel::SelectedLineIsTabSwitchSuggestion() {
return selected_line() != kNoMatch &&
result().match_at(selected_line()).IsTabSwitchSuggestion();
}
void OmniboxPopupModel::SetSelection(Selection selection) {
selection_ = selection;
}
void OmniboxPopupModel::OnFaviconFetched(const GURL& page_url,
const gfx::Image& icon) {
if (icon.IsEmpty() || !view_->IsOpen())
return;
// Notify all affected matches.
for (size_t i = 0; i < result().size(); ++i) {
auto& match = result().match_at(i);
if (!AutocompleteMatch::IsSearchType(match.type) &&
match.destination_url == page_url) {
view_->OnMatchIconUpdated(i);
}
}
}