blob: 9bae9fa1c73522d3457177f5c2eb55d15c1adf89 [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/find_bar_host.h"
#include <algorithm>
#include "base/check_is_test.h"
#include "base/i18n/rtl.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/find_bar/find_bar_controller.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/find_bar_view.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/theme_copying_widget.h"
#include "components/find_in_page/find_tab_helper.h"
#include "components/find_in_page/find_types.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_user_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/border.h"
#include "ui/views/focus/external_focus_tracker.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#endif
#if defined(IS_AURA)
#include "ui/aura/window.h"
#include "ui/views/view_constants_aura.h"
#endif
using input::NativeWebKeyboardEvent;
namespace {
class FindBarHostHelper
: public content::WebContentsUserData<FindBarHostHelper> {
public:
static FindBarHostHelper* CreateOrGetFromWebContents(
content::WebContents* web_contents) {
CreateForWebContents(web_contents);
return FromWebContents(web_contents);
}
void SetExternalFocusTracker(
std::unique_ptr<views::ExternalFocusTracker> external_focus_tracker) {
external_focus_tracker_ = std::move(external_focus_tracker);
}
std::unique_ptr<views::ExternalFocusTracker> TakeExternalFocusTracker() {
return std::move(external_focus_tracker_);
}
views::ExternalFocusTracker* focus_tracker() {
return external_focus_tracker_.get();
}
private:
friend class content::WebContentsUserData<FindBarHostHelper>;
explicit FindBarHostHelper(content::WebContents* web_contents)
: content::WebContentsUserData<FindBarHostHelper>(*web_contents) {}
std::unique_ptr<views::ExternalFocusTracker> external_focus_tracker_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
};
WEB_CONTENTS_USER_DATA_KEY_IMPL(FindBarHostHelper);
gfx::Rect GetLocationForFindBarView(gfx::Rect view_location,
const gfx::Rect& dialog_bounds,
const gfx::Rect& avoid_overlapping_rect) {
// Clamp to the `dialog_bounds`.
view_location.set_width(
std::min(view_location.width(), dialog_bounds.width()));
if (base::i18n::IsRTL()) {
int boundary = dialog_bounds.width() - view_location.width();
view_location.set_x(std::min(view_location.x(), boundary));
} else {
view_location.set_x(std::max(view_location.x(), dialog_bounds.x()));
}
gfx::Rect new_pos = view_location;
// The minimum space between the FindInPage window and the search result.
constexpr int kMinFindWndDistanceFromSelection = 5;
// If the selection rectangle intersects the current position on screen then
// we try to move our dialog to the left (right for RTL) of the selection
// rectangle.
if (!avoid_overlapping_rect.IsEmpty() &&
avoid_overlapping_rect.Intersects(new_pos)) {
if (base::i18n::IsRTL()) {
new_pos.set_x(avoid_overlapping_rect.x() +
avoid_overlapping_rect.width() +
(2 * kMinFindWndDistanceFromSelection));
// If we moved it off-screen to the right, we won't move it at all.
if (new_pos.x() + new_pos.width() > dialog_bounds.width()) {
new_pos = view_location; // Reset.
}
} else {
new_pos.set_x(avoid_overlapping_rect.x() - new_pos.width() -
kMinFindWndDistanceFromSelection);
// If we moved it off-screen to the left, we won't move it at all.
if (new_pos.x() < 0) {
new_pos = view_location; // Reset.
}
}
}
return new_pos;
}
// During testing we can disable animations by setting this flag to true,
// so that opening and closing the dropdown bar is shown instantly, instead of
// having to poll it while it animates to open/closed status.
// TODO(https://crbug.com/40183900): Make this private and push disabling for
// testing into here instead of `find_bar_host_unittest_util`.
static bool kDisableAnimationsForTesting = false;
} // namespace
////////////////////////////////////////////////////////////////////////////////
// FindBarHost, public:
FindBarHost::FindBarHost(BrowserView* browser_view)
: AnimationDelegateViews(browser_view), browser_view_(browser_view) {
auto find_bar_view = std::make_unique<FindBarView>(this);
// The |clip_view| exists to paint to a layer so that it can clip descendent
// Views which also paint to a Layer. See http://crbug.com/589497
auto clip_view = std::make_unique<views::View>();
clip_view->SetPaintToLayer();
clip_view->layer()->SetFillsBoundsOpaquely(false);
clip_view->layer()->SetMasksToBounds(true);
view_ = clip_view->AddChildView(std::move(find_bar_view));
// Initialize the host.
host_ = std::make_unique<ThemeCopyingWidget>(browser_view_->GetWidget());
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_CONTROL);
params.delegate = this;
params.name = "FindBarHost";
params.parent = browser_view_->GetWidgetForAnchoring()->GetNativeView();
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
#if BUILDFLAG(IS_MAC)
params.activatable = views::Widget::InitParams::Activatable::kYes;
#endif
host_->Init(std::move(params));
host_->SetContentsView(std::move(clip_view));
#if defined(IS_AURA)
host_->GetNativeView()->SetProperty(views::kHostViewKey,
browser_view->find_bar_host_view());
#endif
// Start listening to focus changes, so we can register and unregister our
// own handler for Escape.
focus_manager_ = host_->GetFocusManager();
focus_manager_observation_.Observe(focus_manager_.get());
animation_ = std::make_unique<gfx::SlideAnimation>(this);
if (!gfx::Animation::ShouldRenderRichAnimation()) {
animation_->SetSlideDuration(base::TimeDelta());
}
// Update the widget and |view_| bounds to the hidden state.
AnimationProgressed(animation_.get());
SetAccessibleWindowRole(ax::mojom::Role::kDialog);
}
FindBarHost::~FindBarHost() {
focus_tracker_.reset();
}
bool FindBarHost::MaybeForwardKeyEventToWebpage(const ui::KeyEvent& key_event) {
switch (key_event.key_code()) {
case ui::VKEY_DOWN:
case ui::VKEY_UP:
case ui::VKEY_PRIOR:
case ui::VKEY_NEXT:
break;
case ui::VKEY_HOME:
case ui::VKEY_END:
if (key_event.IsControlDown()) {
break;
}
[[fallthrough]];
default:
return false;
}
if (!web_contents()) {
return false;
}
// Make sure we don't have a text field element interfering with keyboard
// input. Otherwise Up and Down arrow key strokes get eaten. "Nom Nom Nom".
web_contents()->ClearFocusedElement();
NativeWebKeyboardEvent event(key_event);
web_contents()
->GetPrimaryMainFrame()
->GetRenderViewHost()
->GetWidget()
->ForwardKeyboardEventWithLatencyInfo(event, *key_event.latency());
return true;
}
bool FindBarHost::IsVisible() const {
return is_visible_;
}
#if BUILDFLAG(IS_MAC)
views::Widget* FindBarHost::GetHostWidget() {
return host_.get();
}
#endif
FindBarController* FindBarHost::GetFindBarController() const {
return find_bar_controller_;
}
bool FindBarHost::HasFocus() const {
return view_->ContainsFocus();
}
void FindBarHost::SetFindBarController(FindBarController* find_bar_controller) {
find_bar_controller_ = find_bar_controller;
if (GetWidget()) {
GetWidget()->UpdateAccessibleNameForRootView();
}
}
void FindBarHost::Show(bool animate, bool focus) {
RestoreOrCreateFocusTracker();
DCHECK(host_);
SetDialogPosition(GetDialogPosition(gfx::Rect()));
// If we're in the middle of a close animation, stop it and skip to the end.
// This ensures that the state is consistent and prepared to show the drop-
// down bar.
if (animation_->IsClosing()) {
animation_->End();
}
if (focus) {
host_->Show();
} else {
host_->ShowInactive();
}
bool was_visible = is_visible_;
is_visible_ = true;
if (!animate || kDisableAnimationsForTesting) {
animation_->Reset(1);
AnimationProgressed(animation_.get());
} else if (!was_visible) {
// Don't re-start the animation.
animation_->Reset();
animation_->Show();
}
if (!was_visible) {
OnVisibilityChanged();
}
}
void FindBarHost::Hide(bool animate) {
// Restore/Save is non-symmetric as hiding the FindBarHost could change
// the focus state of the external view. Saving the focus tracker before the
// hide preserves the appropriate view in the event the FindBarHost visibility
// is restored as part of a tab change.
SaveFocusTracker();
if (!is_visible_) {
return;
}
if (animate && !kDisableAnimationsForTesting && !animation_->IsClosing()) {
animation_->Hide();
} else {
if (animation_->IsClosing()) {
// If we're in the middle of a close animation, skip immediately to the
// end of the animation.
animation_->End();
} else {
// Otherwise we need to set both the animation state to ended and the
// DropdownBarHost state to ended/hidden, otherwise the next time we try
// to show the bar, it might refuse to do so. Note that we call
// AnimationEnded ourselves as Reset does not call it if we are not
// animating here.
animation_->Reset();
AnimationEnded(animation_.get());
}
}
}
void FindBarHost::SetFocusAndSelection() {
view_->FocusAndSelectAll();
SetFindBarIsFocusedOnCurrentTab(true);
}
void FindBarHost::ClearResults(
const find_in_page::FindNotificationDetails& results) {
view_->UpdateForResult(results, std::u16string());
}
void FindBarHost::StopAnimation() {
animation_->End();
}
void FindBarHost::MoveWindowIfNecessary() {
MoveWindowIfNecessaryWithRect(gfx::Rect());
}
void FindBarHost::SetFindTextAndSelectedRange(
const std::u16string& find_text,
const gfx::Range& selected_range) {
view_->SetFindTextAndSelectedRange(find_text, selected_range);
}
std::u16string_view FindBarHost::GetFindText() const {
return view_->GetFindText();
}
gfx::Range FindBarHost::GetSelectedRange() const {
return view_->GetSelectedRange();
}
void FindBarHost::UpdateUIForFindResult(
const find_in_page::FindNotificationDetails& result,
const std::u16string& find_text) {
if (!find_text.empty()) {
view_->UpdateForResult(result, find_text);
} else {
view_->ClearMatchCount();
}
// We now need to check if the window is obscuring the search results.
MoveWindowIfNecessaryWithRect(result.selection_rect());
// Once we find a match we no longer want to keep track of what had
// focus. EndFindSession will then set the focus to the page content.
if (result.number_of_matches() > 0) {
focus_tracker_.reset();
}
}
void FindBarHost::AudibleAlert() {
++audible_alerts_;
#if BUILDFLAG(IS_WIN)
MessageBeep(MB_OK);
#endif
}
bool FindBarHost::IsFindBarVisible() const {
return is_visible_;
}
void FindBarHost::RestoreSavedFocus() {
SetFindBarIsFocusedOnCurrentTab(false);
std::unique_ptr<views::ExternalFocusTracker> focus_tracker_from_web_contents;
views::ExternalFocusTracker* tracker = focus_tracker_.get();
if (!tracker && web_contents()) {
auto* helper = FindBarHostHelper::FromWebContents(web_contents());
if (helper) {
focus_tracker_from_web_contents = helper->TakeExternalFocusTracker();
tracker = focus_tracker_from_web_contents.get();
}
}
if (tracker) {
tracker->FocusLastFocusedExternalView();
focus_tracker_.reset();
} else {
// TODO(brettw): Focus() should be on WebContentsView.
web_contents()->Focus();
}
}
bool FindBarHost::HasGlobalFindPasteboard() const {
#if BUILDFLAG(IS_MAC)
return true;
#else
return false;
#endif
}
void FindBarHost::UpdateFindBarForChangedWebContents() {
if (GetWidget()) {
GetWidget()->UpdateAccessibleNameForRootView();
}
}
const FindBarTesting* FindBarHost::GetFindBarTesting() const {
return this;
}
////////////////////////////////////////////////////////////////////////////////
// FindBarWin, ui::AcceleratorTarget implementation:
bool FindBarHost::AcceleratorPressed(const ui::Accelerator& accelerator) {
ui::KeyboardCode key = accelerator.key_code();
if (key == ui::VKEY_RETURN && accelerator.IsCtrlDown()) {
// Ctrl+Enter closes the Find session and navigates any link that is active.
find_bar_controller_->EndFindSession(
find_in_page::SelectionAction::kActivate,
find_in_page::ResultAction::kClear);
return true;
}
CHECK_EQ(key, ui::VKEY_ESCAPE);
// This will end the Find session and hide the window, causing it to loose
// focus and in the process unregister us as the handler for the Escape
// accelerator through the OnWillChangeFocus event.
find_bar_controller_->EndFindSession(find_in_page::SelectionAction::kKeep,
find_in_page::ResultAction::kKeep);
return true;
}
bool FindBarHost::CanHandleAccelerators() const {
return true;
}
////////////////////////////////////////////////////////////////////////////////
// FindBarTesting implementation:
bool FindBarHost::GetFindBarWindowInfo(gfx::Point* position,
bool* fully_visible) const {
if (!find_bar_controller_) {
if (position) {
*position = gfx::Point();
}
if (fully_visible) {
*fully_visible = false;
}
return false;
}
gfx::Rect window_rect = host_->GetWindowBoundsInScreen();
if (position) {
*position = window_rect.origin();
}
if (fully_visible) {
*fully_visible = is_visible_ && !animation_->is_animating();
}
return true;
}
std::u16string_view FindBarHost::GetFindSelectedText() const {
return view_->GetFindSelectedText();
}
std::u16string_view FindBarHost::GetMatchCountText() const {
return view_->GetMatchCountText();
}
int FindBarHost::GetContentsWidth() const {
return view_->GetContentsBounds().width();
}
size_t FindBarHost::GetAudibleAlertCount() const {
return audible_alerts_;
}
std::u16string FindBarHost::GetAccessibleWindowTitle() const {
// This can be called in tests by AccessibilityChecker before the controller
// is registered with this object. So to handle that case, we need to bail out
// if there is no controller.
const FindBarController* const controller = GetFindBarController();
if (!controller) {
return std::u16string();
}
return l10n_util::GetStringFUTF16(
IDS_FIND_IN_PAGE_ACCESSIBLE_TITLE,
browser_view_->browser()->GetWindowTitleForCurrentTab(false));
}
FindBarView* FindBarHost::GetFindBarViewForTesting() {
CHECK_IS_TEST();
return view_;
}
void FindBarHost::SetEnableAnimationsForTesting(bool enable_animations) {
CHECK_IS_TEST();
kDisableAnimationsForTesting = !enable_animations;
}
////////////////////////////////////////////////////////////////////////////////
// private:
void FindBarHost::GetWidgetPositionNative(gfx::Rect* avoid_overlapping_rect) {
gfx::Rect frame_rect = host_->GetTopLevelWidget()->GetWindowBoundsInScreen();
gfx::Rect webcontents_rect = web_contents()->GetViewBounds();
avoid_overlapping_rect->Offset(0, webcontents_rect.y() - frame_rect.y());
}
void FindBarHost::MoveWindowIfNecessaryWithRect(
const gfx::Rect& selection_rect) {
// We only move the window if one is active for the current WebContents. If we
// don't check this, then SetDialogPosition below will end up making the Find
// Bar visible.
if (!web_contents()) {
return;
}
find_in_page::FindTabHelper* find_tab_helper =
find_in_page::FindTabHelper::FromWebContents(web_contents());
if (!find_tab_helper || !find_tab_helper->find_ui_active()) {
return;
}
gfx::Rect new_pos = GetDialogPosition(selection_rect);
SetDialogPosition(new_pos);
// May need to redraw our frame to accommodate bookmark bar styles.
view_->DeprecatedLayoutImmediately(); // Bounds may have changed.
view_->SchedulePaint();
}
void FindBarHost::SaveFocusTracker() {
if (!web_contents()) {
return;
}
if (focus_tracker_) {
focus_tracker_->SetFocusManager(nullptr);
FindBarHostHelper::CreateOrGetFromWebContents(web_contents())
->SetExternalFocusTracker(std::move(focus_tracker_));
}
}
void FindBarHost::RestoreOrCreateFocusTracker() {
if (!web_contents()) {
return;
}
std::unique_ptr<views::ExternalFocusTracker> focus_tracker =
FindBarHostHelper::CreateOrGetFromWebContents(web_contents())
->TakeExternalFocusTracker();
if (focus_tracker) {
focus_tracker_ = std::move(focus_tracker);
focus_tracker_->SetFocusManager(host_->GetFocusManager());
} else {
focus_tracker_ =
std::make_unique<views::ExternalFocusTracker>(view_, focus_manager_);
}
}
void FindBarHost::SetFindBarIsFocusedOnCurrentTab(bool focus) {
if (web_contents()) {
find_in_page::FindTabHelper::FromWebContents(web_contents())
->set_find_ui_focused(focus);
}
}
void FindBarHost::OnVisibilityChanged() {
// Tell the immersive mode controller about the find bar's bounds. The
// immersive mode controller uses the bounds to keep the top-of-window views
// revealed when the mouse is hovered over the find bar.
gfx::Rect visible_bounds;
if (is_visible_) {
visible_bounds = host_->GetWindowBoundsInScreen();
}
browser_view_->immersive_mode_controller()->OnFindBarVisibleBoundsChanged(
visible_bounds);
browser_view_->browser()->OnFindBarVisibilityChanged();
}
void FindBarHost::RegisterAccelerators() {
DCHECK(!esc_accel_target_registered_);
ui::Accelerator escape(ui::VKEY_ESCAPE, ui::EF_NONE);
focus_manager_->RegisterAccelerator(
escape, ui::AcceleratorManager::kNormalPriority, this);
esc_accel_target_registered_ = true;
// Register for Ctrl+Return.
ui::Accelerator ctrl_ret(ui::VKEY_RETURN, ui::EF_CONTROL_DOWN);
focus_manager_->RegisterAccelerator(
ctrl_ret, ui::AcceleratorManager::kNormalPriority, this);
}
void FindBarHost::UnregisterAccelerators() {
// Unregister Ctrl+Return.
ui::Accelerator ctrl_ret(ui::VKEY_RETURN, ui::EF_CONTROL_DOWN);
focus_manager_->UnregisterAccelerator(ctrl_ret, this);
DCHECK(esc_accel_target_registered_);
ui::Accelerator escape(ui::VKEY_ESCAPE, ui::EF_NONE);
focus_manager_->UnregisterAccelerator(escape, this);
esc_accel_target_registered_ = false;
}
gfx::Rect FindBarHost::GetDialogPosition(gfx::Rect avoid_overlapping_rect) {
// Find the area we have to work with (after accounting for scrollbars, etc).
// The BrowserView does Layout for the components that we care about
// positioning relative to, so we ask it to tell us where we should go.
gfx::Rect find_bar_bounds = browser_view_->GetFindBarBoundingBox();
if (find_bar_bounds.IsEmpty()) {
return gfx::Rect();
}
// Ask the view how large an area it needs to draw on.
gfx::Size prefsize = view_->GetPreferredSize();
// Don't show the find bar if |widget_bounds| is not tall enough to fit.
gfx::Insets insets = view_->GetInsets();
if (find_bar_bounds.height() < prefsize.height() - insets.height()) {
return gfx::Rect();
}
// Place the view in the top right corner of the widget boundaries (top left
// for RTL languages). Adjust for the view insets to ensure the border lines
// up with the location bar.
int x = find_bar_bounds.x() - insets.left();
if (!base::i18n::IsRTL()) {
x += find_bar_bounds.width() - prefsize.width() + insets.width();
}
int y = find_bar_bounds.y() - insets.top();
const gfx::Rect view_location(x, y, prefsize.width(), prefsize.height());
// When we get Find results back, we specify a selection rect, which we
// should strive to avoid overlapping. But first, we need to offset the
// selection rect (if one was provided).
if (!avoid_overlapping_rect.IsEmpty()) {
// For comparison (with the Intersects function below) we need to account
// for the fact that we draw the Find widget relative to the Chrome frame,
// whereas the selection rect is relative to the page.
GetWidgetPositionNative(&avoid_overlapping_rect);
}
gfx::Rect widget_bounds = browser_view_->bounds();
return GetLocationForFindBarView(view_location, widget_bounds,
avoid_overlapping_rect);
}
void FindBarHost::SetDialogPosition(const gfx::Rect& new_pos) {
view_->SetSize(new_pos.size());
if (new_pos.IsEmpty()) {
return;
}
host_->SetBounds(new_pos);
// Tell the immersive mode controller about the find bar's new bounds. The
// immersive mode controller uses the bounds to keep the top-of-window views
// revealed when the mouse is hovered over the find bar.
browser_view_->immersive_mode_controller()->OnFindBarVisibleBoundsChanged(
host_->GetWindowBoundsInScreen());
browser_view_->browser()->OnFindBarVisibilityChanged();
}
void FindBarHost::OnWillChangeFocus(views::View* focused_before,
views::View* focused_now) {
// First we need to determine if one or both of the views passed in are child
// views of our view.
bool our_view_before = focused_before && view_->Contains(focused_before);
bool our_view_now = focused_now && view_->Contains(focused_now);
// When both our_view_before and our_view_now are false, it means focus is
// changing hands elsewhere in the application (and we shouldn't do anything).
// Similarly, when both are true, focus is changing hands within the dropdown
// widget (and again, we should not do anything). We therefore only need to
// look at when we gain initial focus and when we loose it.
if (!our_view_before && our_view_now) {
// We are gaining focus from outside the dropdown widget so we must register
// a handler for Escape.
RegisterAccelerators();
SetFindBarIsFocusedOnCurrentTab(true);
} else if (our_view_before && !our_view_now) {
// We are losing focus to something outside our widget so we restore the
// original handler for Escape.
UnregisterAccelerators();
}
if (!our_view_now) {
SetFindBarIsFocusedOnCurrentTab(false);
}
}
void FindBarHost::AnimationProgressed(const gfx::Animation* animation) {
// First, we calculate how many pixels to slide the widget.
gfx::Size pref_size = view_->GetPreferredSize();
int view_offset = static_cast<int>((animation_->GetCurrentValue() - 1.0) *
pref_size.height());
// This call makes sure |view_| appears in the right location, the size and
// shape is correct and that it slides in the right direction.
view_->SetPosition(gfx::Point(0, view_offset));
}
void FindBarHost::AnimationEnded(const gfx::Animation* animation) {
// Ensure the position gets a final update. This is important when ending the
// animation early (e.g. closing a tab with an open find bar), since otherwise
// the position will be out of date at the start of the next animation.
AnimationProgressed(animation);
if (!animation_->IsShowing()) {
// Animation has finished closing.
DCHECK(host_);
host_->Hide();
is_visible_ = false;
OnVisibilityChanged();
}
}