blob: d03780f6291eac7b5ea597a8440122bd26b7e568 [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 "chrome/browser/ui/views/omnibox/omnibox_popup_view_views.h"
#include <memory>
#include <numeric>
#include <optional>
#include <string_view>
#include <utility>
#include "base/auto_reset.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/omnibox/omnibox_controller.h"
#include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
#include "chrome/browser/ui/omnibox/omnibox_next_features.h"
#include "chrome/browser/ui/omnibox/omnibox_popup_state_manager.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_header_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_row_grouped_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_row_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_view_views.h"
#include "chrome/browser/ui/views/omnibox/rounded_omnibox_results_frame.h"
#include "chrome/browser/ui/views/theme_copying_widget.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
#include "components/omnibox/common/omnibox_features.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/closure_animation_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/image/image.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/cascading_property.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/widget/widget.h"
namespace {
#if BUILDFLAG(IS_WIN)
// Removes forced software compositing for `AutocompletePopupWidget` below.
// TODO(thestig): Remove this kill switch after a safe rollout, in M145.
BASE_FEATURE(kOmniboxRemovePopupWidgetSoftwareCompositing,
base::FEATURE_ENABLED_BY_DEFAULT);
#endif
// Returns the bounds for the popup widget's content frame (before shadow
// insets) when in debug mode. The bounds are calculated such that:
// 1. The widget takes up half the width of the content area.
// 2. The widget is positioned on the far right of the content area.
// 3. The top of the results aligns with the top of the content area.
// Returns std::nullopt if the bounds could not be determined.
std::optional<gfx::Rect> GetDebugWidgetBounds(
LocationBarView* location_bar_view,
int popup_results_height) {
Browser* browser = location_bar_view->browser();
if (!browser) {
return std::nullopt;
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
if (!browser_view || !browser_view->contents_web_view()) {
return std::nullopt;
}
gfx::Rect contents_bounds =
browser_view->contents_web_view()->GetBoundsInScreen();
int frame_width = contents_bounds.width() / 2;
int frame_height = popup_results_height;
gfx::Rect frame_bounds;
frame_bounds.set_width(frame_width);
frame_bounds.set_height(frame_height);
// Calculate the frame's X position so the widget's right edge aligns with the
// content area's right edge.
const gfx::Insets shadow_insets =
RoundedOmniboxResultsFrame::GetShadowInsets();
frame_bounds.set_x(contents_bounds.right() - shadow_insets.right() -
frame_width);
// Calculate the frame's Y position so the top of the results area
// (frame_y + non_result_height) aligns with the content area's top.
frame_bounds.set_y(contents_bounds.y() -
RoundedOmniboxResultsFrame::GetNonResultSectionHeight());
return frame_bounds;
}
} // namespace
class OmniboxPopupViewViews::PopupWidget final : public ThemeCopyingWidget {
public:
// TODO(tapted): Remove |role_model| when the omnibox is completely decoupled
// from NativeTheme.
explicit PopupWidget(views::Widget* role_model)
: ThemeCopyingWidget(role_model) {}
PopupWidget(const PopupWidget&) = delete;
PopupWidget& operator=(const PopupWidget&) = delete;
~PopupWidget() override = default;
void InitOmniboxPopup(const views::Widget* parent_widget) {
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_POPUP);
#if BUILDFLAG(IS_WIN)
// On Windows use the software compositor to ensure that we don't block
// the UI thread during command buffer creation. We can revert this change
// once http://crbug.com/125248 is fixed.
params.force_software_compositing = !base::FeatureList::IsEnabled(
kOmniboxRemovePopupWidgetSoftwareCompositing);
#endif
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.parent = parent_widget->GetNativeView();
params.context = parent_widget->GetNativeWindow();
RoundedOmniboxResultsFrame::OnBeforeWidgetInit(&params, this);
Init(std::move(params));
}
void SetPopupContentsView(OmniboxPopupViewViews* contents) {
SetContentsView(std::make_unique<RoundedOmniboxResultsFrame>(
contents, contents->location_bar_view_, /*forward_mouse_events=*/true));
}
void SetTargetBounds(const gfx::Rect& bounds) {
base::AutoReset<bool> reset(&is_setting_popup_bounds_, true);
SetBounds(bounds);
}
void ShowAnimated() {
// Set the initial opacity to 0 and ease into fully opaque.
GetLayer()->SetOpacity(0.0);
ShowInactive();
const auto scoped_settings = GetScopedAnimationSettings();
GetLayer()->SetOpacity(1.0);
}
void CloseAnimated() {
// If the opening or shrinking animations still running, abort them, as the
// popup is closing. This is an edge case for superhumanly fast users.
GetLayer()->GetAnimator()->AbortAllAnimations();
auto scoped_settings = GetScopedAnimationSettings();
GetLayer()->SetOpacity(0.0);
is_animating_closed_ = true;
// Destroy the popup when done. The observer deletes itself on completion.
scoped_settings->AddObserver(new ui::ClosureAnimationObserver(
base::BindOnce(&PopupWidget::Close, AsWeakPtr())));
}
void OnWidgetDestroying(views::Widget* widget) override {
// ThemeCopyingWidget observation is set on the role_model widget, which in
// the case of `PopupWidget` is the hosting parent widget.
CHECK_NE(widget, this);
ThemeCopyingWidget::OnWidgetDestroying(widget);
// In the case the host widget is destroyed, close the popup widget
// synchronously. This is necessary as the popup widget's contents view has
// dependencies on the hosting widget's BrowserView (see
// `SetPopupContentsView()` above). Since the popup widget is owned by its
// NativeWidget there is a risk of dangling pointers if it is not destroyed
// synchronously with its parent.
// TODO(crbug.com/40232479): Once this is migrated to CLIENT_OWNS_WIDGET
// this will no longer be necessary.
CloseNow();
}
void OnMouseEvent(ui::MouseEvent* event) override {
// Ignore mouse events if the popup is closed or animating closed.
if (IsClosed() || is_animating_closed_) {
if (event->cancelable()) {
event->SetHandled();
}
return;
}
ThemeCopyingWidget::OnMouseEvent(event);
}
void OnGestureEvent(ui::GestureEvent* event) override {
// Ignore gesture events if the popup is closed or animating closed.
// However, just like the base class, we do not capture the event, so
// multiple widgets may get tap events at the same time.
if (IsClosed() || is_animating_closed_) {
return;
}
ThemeCopyingWidget::OnGestureEvent(event);
}
bool is_setting_popup_bounds() const { return is_setting_popup_bounds_; }
base::WeakPtr<PopupWidget> AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
private:
std::unique_ptr<ui::ScopedLayerAnimationSettings>
GetScopedAnimationSettings() {
auto settings = std::make_unique<ui::ScopedLayerAnimationSettings>(
GetLayer()->GetAnimator());
settings->SetTweenType(gfx::Tween::Type::FAST_OUT_SLOW_IN);
constexpr base::TimeDelta kPopupOpacityAnimationDuration =
base::Milliseconds(82);
settings->SetTransitionDuration(kPopupOpacityAnimationDuration);
return settings;
}
// True if the popup is in the process of closing via animation.
bool is_animating_closed_ = false;
// True if the popup's bounds are currently being set.
bool is_setting_popup_bounds_ = false;
base::WeakPtrFactory<PopupWidget> weak_ptr_factory_{this};
};
OmniboxPopupViewViews::WidgetObserverHelper::WidgetObserverHelper(
OmniboxPopupViewViews* popup_view)
: popup_view_(popup_view) {}
OmniboxPopupViewViews::WidgetObserverHelper::~WidgetObserverHelper() = default;
void OmniboxPopupViewViews::WidgetObserverHelper::OnWidgetBoundsChanged(
views::Widget* widget,
const gfx::Rect& new_bounds) {
popup_view_->OnWidgetBoundsChanged(widget, new_bounds);
}
void OmniboxPopupViewViews::WidgetObserverHelper::OnWidgetVisibilityChanged(
views::Widget* widget,
bool visible) {
popup_view_->OnWidgetVisibilityChanged(widget, visible);
}
void OmniboxPopupViewViews::WidgetObserverHelper::OnWidgetDestroying(
views::Widget* widget) {
popup_view_->OnWidgetDestroying(widget);
}
OmniboxPopupViewViews::OmniboxPopupViewViews(OmniboxViewViews* omnibox_view,
OmniboxController* controller,
LocationBarView* location_bar_view)
: OmniboxPopupView(controller),
omnibox_view_(omnibox_view),
location_bar_view_(location_bar_view) {
controller->edit_model()->set_popup_view(this);
edit_model_observation_.Observe(controller->edit_model());
if (omnibox_view_) {
GetViewAccessibility().SetPopupForId(
omnibox_view_->GetViewAccessibility().GetUniqueId());
}
// The contents is owned by the LocationBarView.
set_owned_by_client(OwnedByClientPassKey());
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
GetViewAccessibility().SetRole(ax::mojom::Role::kListBox);
UpdateAccessibleStates();
UpdateAccessibleControlIds();
UpdateAccessibleActiveDescendantForInvokingView();
}
OmniboxPopupViewViews::~OmniboxPopupViewViews() {
// We don't need to close or delete `widget_` here. The OS either has already
// closed the window, in which case it's been deleted, or it will soon.
if (widget_) {
widget_->RemoveObserver(&widget_observer_helper_);
}
CHECK(!widget_observer_helper_.IsInObserverList());
controller()->edit_model()->set_popup_view(nullptr);
UpdateAccessibleControlIds();
}
gfx::Image OmniboxPopupViewViews::GetMatchIcon(
const AutocompleteMatch& match,
SkColor vector_icon_color) const {
auto* color_provider = GetColorProvider();
bool dark_mode =
color_provider && color_utils::IsDark(color_provider->GetColor(
kColorOmniboxResultsBackground));
return controller()->edit_model()->GetMatchIcon(match, vector_icon_color,
dark_mode);
}
void OmniboxPopupViewViews::SetSelectedIndex(size_t index) {
DCHECK(HasMatchAt(index));
if (index != controller()->edit_model()->GetPopupSelection().line) {
OmniboxPopupSelection::LineState line_state = OmniboxPopupSelection::NORMAL;
controller()->edit_model()->SetPopupSelection(
OmniboxPopupSelection(index, line_state));
OnPropertyChanged(controller()->edit_model(), views::kPropertyEffectsNone);
}
}
size_t OmniboxPopupViewViews::GetSelectedIndex() const {
return GetSelection().line;
}
OmniboxPopupSelection OmniboxPopupViewViews::GetSelection() const {
return controller()->edit_model()->GetPopupSelection();
}
void OmniboxPopupViewViews::UpdatePopupBounds() {
if (widget_) {
widget_->SetTargetBounds(GetTargetBounds());
}
}
bool OmniboxPopupViewViews::IsOpen() const {
return widget_ != nullptr;
}
void OmniboxPopupViewViews::InvalidateLine(size_t line) {
// TODO(tommycli): This is weird, but https://crbug.com/1063071 shows that
// crashes like this have happened, so we add this to avoid it for now.
if (line >= row_views_.size()) {
return;
}
row_views_[line]->OnSelectionStateChanged();
}
void OmniboxPopupViewViews::UpdatePopupAppearance() {
const auto* autocomplete_controller = controller()->autocomplete_controller();
const bool should_be_open =
controller()->popup_state_manager()->popup_state() !=
OmniboxPopupState::kAim &&
!autocomplete_controller->result().empty() &&
!omnibox_view_->IsImeShowingPopup();
const bool was_open = !!widget_;
if (!should_be_open) {
if (was_open) {
// Check whether omnibox should not close per UI DevTools settings.
if (!widget_->ShouldHandleNativeWidgetActivationChanged(false)) {
return;
}
widget_->CloseAnimated(); // This will eventually delete the popup.
widget_->RemoveObserver(&widget_observer_helper_);
widget_.reset();
// Update the popup state manager that the classic popup is closing.
// Do this AFTER widget operations. `LocationBarView` is subscribed to
// state changes and attempts to call `UpdatePopupAppearance()` again if
// the widget is open.
// Only update the state if it's currently `kClassic`. If it's already
// transitioning to another state (e.g., `kAim`), don't override it.
if (controller()->popup_state_manager()->popup_state() ==
OmniboxPopupState::kClassic) {
controller()->popup_state_manager()->SetPopupState(
OmniboxPopupState::kNone);
}
if (contextual_group_view_) {
contextual_group_view_->OnPopupHide();
}
UpdateAccessibleStates();
UpdateAccessibleControlIds();
// The active descendant should be cleared when the popup closes.
UpdateAccessibleActiveDescendantForInvokingView();
FireAXEventsForNewActiveDescendant(nullptr);
}
return;
}
// Ensure that we have an existing popup widget prior to creating the result
// views to ensure the proper initialization of the views hierarchy.
if (!was_open) {
views::Widget* popup_parent = location_bar_view_->GetWidget();
// If the popup is currently closed, we need to create it.
popup_create_start_time_ = base::TimeTicks::Now();
// Self-deleting. See comment for `widget_` in the header.
widget_ = (new PopupWidget(popup_parent))->AsWeakPtr();
widget_->InitOmniboxPopup(popup_parent);
// Third-party software such as DigitalPersona identity verification can
// hook the underlying window creation methods and use SendMessage to
// synchronously change focus/activation, resulting in the popup being
// destroyed by the time control returns here. Bail out in this case to
// avoid a nullptr dereference.
if (!widget_) {
return;
}
widget_->SetVisibilityAnimationTransition(views::Widget::ANIMATE_NONE);
widget_->SetPopupContentsView(this);
widget_->AddObserver(&widget_observer_helper_);
}
// Update the match cached by each row, in the process of doing so make sure
// we have enough row views.
const size_t result_size = autocomplete_controller->result().size();
std::u16string previous_row_header;
// Contextual search suggestions will be grouped into a single subview for a
// joint animation if the feature is enabled.
size_t grouped_matches_start_index = result_size;
// Clear the row views to ensure that it does not have stale row views from a
// previous popup appearance. This does not remove the row views from the view
// hierarchy.
row_views_.clear();
const int contextual_group_view_count = contextual_group_view_ ? 1 : 0;
for (size_t i = 0; i < result_size; ++i) {
// Create child views lazily. Since especially the first result view may
// be expensive to create due to loading font data, this saves time and
// memory during browser startup. https://crbug.com/1021323
// If the row group view is created, it should not be counted in the number
// of children.
if (children().size() - contextual_group_view_count == i) {
AddChildViewAt(std::make_unique<OmniboxRowView>(i, /*popup_view=*/this),
i);
}
OmniboxRowView* row_view = static_cast<OmniboxRowView*>(children()[i]);
row_views_.push_back(row_view);
const AutocompleteMatch& match = GetMatchAtIndex(i);
// Contextual search suggestions will be grouped into a single subview for a
// joint animation if the feature is enabled.
if (omnibox_feature_configs::ContextualSearch::Get()
.enable_loading_suggestions_animation &&
match.IsContextualSearchSuggestion()) {
row_view->SetVisible(false);
if (grouped_matches_start_index == result_size) {
grouped_matches_start_index = i;
}
continue;
}
row_view->SetVisible(true);
previous_row_header = UpdateRowView(row_view, match, previous_row_header);
}
// If we have more views than matches, hide the surplus ones.
for (auto i = children().begin() + result_size; i != children().end(); ++i) {
(*i)->SetVisible(false);
}
UpdateContextualSuggestionsGroup(grouped_matches_start_index);
widget_->SetTargetBounds(GetTargetBounds());
if (!was_open) {
widget_->ShowAnimated();
// Popup is now expanded and first item will be selected.
UpdateAccessibleStates();
UpdateAccessibleControlIds();
UpdateAccessibleActiveDescendantForInvokingView();
OmniboxResultView* result_view = result_view_at(0);
if (result_view) {
result_view->GetViewAccessibility().SetIsSelected(true);
FireAXEventsForNewActiveDescendant(result_view);
}
// Update the popup state manager that the classic popup is opening.
controller()->popup_state_manager()->SetPopupState(
OmniboxPopupState::kClassic);
}
InvalidateLayout();
}
void OmniboxPopupViewViews::ProvideButtonFocusHint(size_t line) {
DCHECK(GetSelection().IsButtonFocused());
views::View* active_button =
row_views_[line]->GetActiveAuxiliaryButtonForAccessibility();
// TODO(tommycli): |active_button| can sometimes be nullptr, because the
// suggestion button row is not completely implemented.
if (active_button) {
// The accessible selection cannot be in both the button and the result
// view. When the button gets selected and is active, remove the selected
// state from the result view. This is so that if a subsequent action
// creates a OmniboxPopupSelection::NORMAL we fire the event.
result_view_at(line)->GetViewAccessibility().SetIsSelected(false);
FireAXEventsForNewActiveDescendant(active_button);
}
}
void OmniboxPopupViewViews::OnDragCanceled() {
SetMouseAndGestureHandler(nullptr);
}
void OmniboxPopupViewViews::GetPopupAccessibleNodeData(
ui::AXNodeData* node_data) const {
return GetViewAccessibility().GetAccessibleNodeData(node_data);
}
std::u16string_view OmniboxPopupViewViews::GetAccessibleButtonTextForResult(
size_t line) const {
const OmniboxResultView* result_view = result_view_at(line);
if (!result_view) {
return std::u16string_view();
}
const auto* button = result_view->GetActiveAuxiliaryButtonForAccessibility();
return static_cast<const views::LabelButton*>(button)->GetText();
}
raw_ptr<OmniboxPopupViewWebUI>
OmniboxPopupViewViews::GetOmniboxPopupViewWebUI() {
return nullptr;
}
bool OmniboxPopupViewViews::OnMouseDragged(const ui::MouseEvent& event) {
const size_t index = GetIndexForPoint(event.location());
// If the drag event is over the bounds of one of the result views, pass
// control to that view.
if (HasMatchAt(index)) {
SetMouseAndGestureHandler(result_view_at(index));
return false;
}
// If the drag event is not over any of the result views, that means that it
// has passed outside the bounds of the popup view. Return true to keep
// receiving the drag events, as the drag may return in which case we will
// want to respond to it again.
return true;
}
void OmniboxPopupViewViews::OnGestureEvent(ui::GestureEvent* event) {
const size_t index = GetIndexForPoint(event->location());
if (!HasMatchAt(index)) {
return;
}
switch (event->type()) {
case ui::EventType::kGestureTapDown:
case ui::EventType::kGestureScrollBegin:
case ui::EventType::kGestureScrollUpdate:
SetSelectedIndex(index);
break;
case ui::EventType::kGestureTap:
case ui::EventType::kGestureScrollEnd: {
DCHECK(HasMatchAt(index));
controller()->edit_model()->OpenSelection(OmniboxPopupSelection(index),
event->time_stamp());
break;
}
default:
return;
}
event->SetHandled();
}
void OmniboxPopupViewViews::OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) {
// Because we don't directly control the lifetime of the widget, gracefully
// handle "stale" notifications by ignoring them. https://crbug.com/1108762
if (!widget_ || widget_.get() != widget) {
return;
}
// This is called on rotation or device scale change. We have to re-align to
// the new location bar location.
// Ignore cases when we are internally updating the popup bounds.
if (widget_->is_setting_popup_bounds()) {
return;
}
UpdatePopupAppearance();
}
void OmniboxPopupViewViews::OnWidgetVisibilityChanged(views::Widget* widget,
bool visible) {
if (!widget_ || widget != widget_.get()) {
return;
}
if (visible && popup_create_start_time_.has_value()) {
// Use the popup's compositor. The next presentation time will correspond to
// the first visual presentation of the bubble's content after the Widget
// has been created.
widget_->GetCompositor()->RequestSuccessfulPresentationTimeForNextFrame(
base::BindOnce(
[](base::TimeTicks popup_create_start_time,
const viz::FrameTimingDetails& frame_timing_details) {
base::TimeTicks presentation_timestamp =
frame_timing_details.presentation_feedback.timestamp;
base::UmaHistogramTimes(
"Omnibox.Views.PopupFirstPaint",
presentation_timestamp - popup_create_start_time);
},
popup_create_start_time_.value()));
popup_create_start_time_.reset();
}
}
void OmniboxPopupViewViews::OnWidgetDestroying(views::Widget* widget) {
CHECK_EQ(widget, widget_.get());
if (widget_) {
widget_->RemoveObserver(&widget_observer_helper_);
widget_ = nullptr;
}
UpdateAccessibleStates();
// Update the popup state manager if widget was destroyed externally, e.g., by
// the OS. This ensures the popup state manager stays in sync. Do this AFTER
// widget operations. `LocationBarView` is subscribed to state changes and
// attempts to call `UpdatePopupAppearance()` again if the widget is open.
if (controller()->popup_state_manager()->popup_state() ==
OmniboxPopupState::kClassic) {
controller()->popup_state_manager()->SetPopupState(
OmniboxPopupState::kNone);
}
}
void OmniboxPopupViewViews::OnSelectionChanged(
OmniboxPopupSelection old_selection,
OmniboxPopupSelection new_selection) {
// Do not invalidate the same line twice, in order to avoid redundant
// accessibility events.
if (old_selection.line != OmniboxPopupSelection::kNoMatch &&
old_selection.line != new_selection.line) {
InvalidateLine(old_selection.line);
}
if (new_selection.line != OmniboxPopupSelection::kNoMatch) {
InvalidateLine(new_selection.line);
}
UpdateAccessibleActiveDescendantForInvokingView();
}
void OmniboxPopupViewViews::OnMatchIconUpdated(size_t match_index) {
if (OmniboxResultView* result_view = result_view_at(match_index)) {
result_view->OnMatchIconUpdated();
}
}
void OmniboxPopupViewViews::OnContentsChanged() {
UpdatePopupAppearance();
}
void OmniboxPopupViewViews::FireAXEventsForNewActiveDescendant(
View* descendant_view) {
// Selected children changed is fired on the popup.
NotifyAccessibilityEventDeprecated(ax::mojom::Event::kSelectedChildrenChanged,
true);
}
gfx::Rect OmniboxPopupViewViews::GetTargetBounds() const {
const size_t result_size =
controller()->autocomplete_controller()->result().size();
auto children_span = base::span(children()).first(result_size);
int popup_height = std::accumulate(
children_span.begin(), children_span.end(), 0,
[](int height, const views::View* v) {
return v->GetVisible() ? height + v->GetPreferredSize().height()
: height;
});
if (contextual_group_view_) {
popup_height += contextual_group_view_->GetCurrentHeight();
}
// Add space at the bottom for aesthetic reasons. It's expected that this
// space is dead unclickable/unhighlightable space. This extra padding is not
// added if the results section has no height (result set is empty or all
// results are hidden). See https://crbug.com/1076646 for additional context.
if (popup_height != 0) {
// The amount of extra space is dependent on whether the last match is the
// toolbelt or not. The toolbelt doesn't have an icon or image on the left
// like a regular suggestion nor a big background highlight like an IPH
// suggestion so it doesn't require as much space.
const size_t last_result_index = result_size - 1;
int extra_bottom_padding =
GetMatchAtIndex(last_result_index).IsToolbelt() ? 2 : 8;
popup_height += extra_bottom_padding;
}
// Add enough space on the top and bottom so it looks like there is the same
// amount of space between the text and the popup border as there is in the
// interior between each row of text.
popup_height += RoundedOmniboxResultsFrame::GetNonResultSectionHeight();
// The rounded popup is always offset the same amount from the omnibox.
gfx::Rect content_rect = location_bar_view_->GetBoundsInScreen();
content_rect.Inset(
-RoundedOmniboxResultsFrame::GetLocationBarAlignmentInsets());
content_rect.set_height(popup_height);
if (base::FeatureList::IsEnabled(omnibox::kWebUIOmniboxPopup) &&
!base::FeatureList::IsEnabled(omnibox::kWebUIOmniboxFullPopup) &&
!omnibox::IsAimPopupFeatureEnabled() &&
base::FeatureList::IsEnabled(omnibox::kWebUIOmniboxPopupDebug) &&
omnibox::kWebUIOmniboxPopupDebugSxSParam.Get()) {
if (auto bounds = GetDebugWidgetBounds(location_bar_view_, popup_height)) {
content_rect = *bounds;
}
}
// Finally, expand the widget to accommodate the custom-drawn shadows.
content_rect.Inset(-RoundedOmniboxResultsFrame::GetShadowInsets());
return content_rect;
}
OmniboxHeaderView* OmniboxPopupViewViews::header_view_at(size_t i) {
return i < row_views_.size() ? row_views_[i]->header_view() : nullptr;
}
OmniboxResultView* OmniboxPopupViewViews::result_view_at(size_t i) {
return const_cast<OmniboxResultView*>(std::as_const(*this).result_view_at(i));
}
const OmniboxResultView* OmniboxPopupViewViews::result_view_at(size_t i) const {
return i < row_views_.size() ? row_views_[i]->result_view() : nullptr;
}
bool OmniboxPopupViewViews::HasMatchAt(size_t index) const {
return index < controller()->autocomplete_controller()->result().size();
}
const AutocompleteMatch& OmniboxPopupViewViews::GetMatchAtIndex(
size_t index) const {
return controller()->autocomplete_controller()->result().match_at(index);
}
size_t OmniboxPopupViewViews::GetIndexForPoint(const gfx::Point& point) const {
if (!HitTestPoint(point)) {
return OmniboxPopupSelection::kNoMatch;
}
size_t nb_match = controller()->autocomplete_controller()->result().size();
// Iterate through all the row views that correspond to current matches.
// `row_views_` contains all the `OmniboxRowView` instances, regardless of
// whether they are direct children or hosted in `contextual_group_view_`.
for (OmniboxRowView* row_view : base::span(row_views_).first(nb_match)) {
// Only consider visible rows.
if (row_view->GetVisible()) {
gfx::Point point_in_child_coords(point);
View::ConvertPointToTarget(this, row_view, &point_in_child_coords);
if (row_view->HitTestPoint(point_in_child_coords)) {
return row_view->line();
}
}
}
return OmniboxPopupSelection::kNoMatch;
}
void OmniboxPopupViewViews::UpdateAccessibleStates() const {
if (IsOpen()) {
GetViewAccessibility().SetIsExpanded();
GetViewAccessibility().SetIsInvisible(false);
} else {
GetViewAccessibility().SetIsCollapsed();
GetViewAccessibility().SetIsInvisible(true);
}
}
void OmniboxPopupViewViews::UpdateAccessibleControlIds() {
if (!omnibox_view_) {
return;
}
// Establish a "CONTROLS" relationship between the omnibox and the
// the popup. This allows a screen reader to understand the relationship
// between the omnibox and the list of suggestions, and determine which
// suggestion is currently selected, even though focus remains here on
// the omnibox.
if (IsOpen()) {
int32_t popup_view_id = GetViewAccessibility().GetUniqueId();
omnibox_view_->GetViewAccessibility().SetControlIds({popup_view_id});
} else {
omnibox_view_->GetViewAccessibility().RemoveControlIds();
}
}
void OmniboxPopupViewViews::UpdateAccessibleActiveDescendantForInvokingView() {
if (!omnibox_view_) {
return;
}
// This logic aims to update the "active descendant" accessibility
// property of the omnibox text field (`omnibox_view_`).
//
// This property tells assistive technologies (like screen readers) which
// element within the popup should be considered the currently "active" or
// "focused" item, even though the actual keyboard focus remains on the
// text field.
OmniboxPopupSelection selection = GetSelection();
if (IsOpen() && selection.line != OmniboxPopupSelection::kNoMatch) {
if (OmniboxResultView* result_view = result_view_at(selection.line)) {
omnibox_view_->GetViewAccessibility().SetActiveDescendant(*result_view);
} else {
omnibox_view_->GetViewAccessibility().ClearActiveDescendant();
}
} else {
omnibox_view_->GetViewAccessibility().ClearActiveDescendant();
}
}
std::u16string OmniboxPopupViewViews::UpdateRowView(
OmniboxRowView* row_view,
const AutocompleteMatch& match,
const std::u16string& previous_row_header) {
std::u16string current_row_header =
controller()->edit_model()->GetSuggestionGroupHeaderText(
match.suggestion_group_id);
// Show the header if it's distinct from the previous match's header.
if (!current_row_header.empty() &&
current_row_header != previous_row_header) {
row_view->ShowHeader(current_row_header);
} else {
row_view->HideHeader();
}
OmniboxResultView* const result_view = row_view->result_view();
result_view->SetMatch(match);
result_view->UpdateAccessibilityProperties();
result_view->SetVisible(!controller()->IsSuggestionHidden(match));
const SkBitmap* bitmap =
controller()->edit_model()->GetPopupRichSuggestionBitmap(
row_view->line());
if (bitmap) {
result_view->SetRichSuggestionImage(
gfx::ImageSkia::CreateFrom1xBitmap(*bitmap));
}
return current_row_header;
}
void OmniboxPopupViewViews::UpdateContextualSuggestionsGroup(
size_t match_start_index) {
const size_t result_size =
controller()->autocomplete_controller()->result().size();
if (match_start_index >= result_size) {
return;
}
if (!contextual_group_view_) {
contextual_group_view_ =
AddChildView(std::make_unique<OmniboxRowGroupedView>(this));
}
size_t current_row_index = 0;
std::u16string previous_row_header;
for (size_t match_index = match_start_index; match_index < result_size;
match_index++) {
// A row view should have been created for each match.
CHECK(row_views_[match_index]);
row_views_[match_index]->SetVisible(false);
// If the group view already created the row view, reuse it. Otherwise,
// create a new row view and add it to the group view.
if (contextual_group_view_->children().size() == current_row_index) {
row_views_[match_index] = contextual_group_view_->AddChildView(
std::make_unique<OmniboxRowView>(match_index, /*popup_view=*/this));
} else {
row_views_[match_index] = static_cast<OmniboxRowView*>(
contextual_group_view_->children()[current_row_index]);
}
OmniboxRowView* row_view = row_views_[match_index];
row_view->SetVisible(true);
previous_row_header = UpdateRowView(row_view, GetMatchAtIndex(match_index),
previous_row_header);
current_row_index++;
}
auto surplus_row_views =
base::span(contextual_group_view_->children()).subspan(current_row_index);
for (auto& row_view : surplus_row_views) {
row_view->SetVisible(false);
}
contextual_group_view_->SetVisible(true);
contextual_group_view_->MaybeStartAnimation();
}
BEGIN_METADATA(OmniboxPopupViewViews)
ADD_PROPERTY_METADATA(size_t, SelectedIndex)
ADD_READONLY_PROPERTY_METADATA(gfx::Rect, TargetBounds)
END_METADATA