blob: 624cf171037b8d3efd49fadf69cb8d1523451f6a [file] [log] [blame]
// Copyright 2024 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/user_education/views/help_bubble_event_relay.h"
#include "base/functional/bind.h"
#include "components/user_education/views/help_bubble_view.h"
#include "ui/events/event.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace user_education {
namespace {
bool IsInButton(const gfx::Point& screen_coords, const views::Button* button) {
return button && button->HitTestPoint(views::View::ConvertPointFromScreen(
button, screen_coords));
}
} // namespace
HelpBubbleEventRelay::HelpBubbleEventRelay() = default;
HelpBubbleEventRelay::~HelpBubbleEventRelay() = default;
void HelpBubbleEventRelay::Init(HelpBubbleView* help_bubble) {
CHECK(!help_bubble_);
help_bubble_ = help_bubble;
}
views::Button* HelpBubbleEventRelay::GetButtonAt(
const gfx::Point& screen_coords) const {
if (!help_bubble_) {
return nullptr;
}
if (IsInButton(screen_coords, help_bubble_->close_button_)) {
return help_bubble_->close_button_;
}
if (IsInButton(screen_coords, help_bubble_->default_button_)) {
return help_bubble_->default_button_;
}
for (views::MdTextButton* const button : help_bubble_->non_default_buttons_) {
if (IsInButton(screen_coords, button)) {
return button;
}
}
return nullptr;
}
bool HelpBubbleEventRelay::OnEvent(const ui::LocatedEvent& event,
const gfx::Point& screen_coords) {
if (!help_bubble_) {
return false;
}
const views::Widget* const widget = help_bubble_->GetWidget();
if (!widget || !widget->GetWindowBoundsInScreen().Contains(screen_coords)) {
return false;
}
views::Button* const target_button = GetButtonAt(screen_coords);
const gfx::Point target_point =
target_button
? views::View::ConvertPointFromScreen(target_button, screen_coords)
: gfx::Point();
switch (event.type()) {
// Pass mouse events on to the button as normal.
case ui::EventType::kMousePressed:
if (target_button) {
auto* const mouse_event = event.AsMouseEvent();
target_button->OnMousePressed(ui::MouseEvent(
ui::EventType::kMousePressed, gfx::PointF(target_point),
gfx::PointF(screen_coords), mouse_event->time_stamp(),
mouse_event->flags(), mouse_event->changed_button_flags()));
sent_click_ = true;
}
break;
case ui::EventType::kMouseReleased:
if (target_button) {
auto* const mouse_event = event.AsMouseEvent();
target_button->OnMouseReleased(ui::MouseEvent(
ui::EventType::kMouseReleased, gfx::PointF(target_point),
gfx::PointF(screen_coords), mouse_event->time_stamp(),
mouse_event->flags(), mouse_event->changed_button_flags()));
}
break;
// Touch events are not processed directly by Views; they are typically
// converted to something else. So, convert them to mouse clicks for the
// purpose of pressing buttons.
case ui::EventType::kTouchPressed:
if (target_button) {
auto* const touch_event = event.AsTouchEvent();
target_button->OnMousePressed(ui::MouseEvent(
ui::EventType::kMousePressed, gfx::PointF(target_point),
gfx::PointF(screen_coords), touch_event->time_stamp(),
touch_event->flags() | ui::EF_LEFT_MOUSE_BUTTON | ui::EF_FROM_TOUCH,
ui::EF_LEFT_MOUSE_BUTTON));
sent_click_ = true;
}
break;
case ui::EventType::kTouchReleased:
if (target_button) {
auto* const touch_event = event.AsTouchEvent();
target_button->OnMouseReleased(ui::MouseEvent(
ui::EventType::kMouseReleased, gfx::PointF(target_point),
gfx::PointF(screen_coords), touch_event->time_stamp(),
touch_event->flags() | ui::EF_LEFT_MOUSE_BUTTON | ui::EF_FROM_TOUCH,
ui::EF_LEFT_MOUSE_BUTTON));
}
break;
// If a gesture is received, forward it as-is.
case ui::EventType::kGestureTap:
if (target_button) {
auto* const gesture = event.AsGestureEvent();
ui::GestureEvent tap(gesture->x(), gesture->y(), gesture->flags(),
gesture->time_stamp(), gesture->details(),
gesture->unique_touch_event_id());
target_button->button_controller()->OnGestureEvent(&tap);
sent_click_ = true;
}
break;
// Mouse moves could be routed through the inkdrop controller but it's
// easier to just set hovered state directly.
case ui::EventType::kMouseMoved:
if (target_button != hovered_button_) {
if (hovered_button_) {
views::InkDrop* const ink_drop =
views::InkDrop::Get(hovered_button_)->GetInkDrop();
if (ink_drop) {
ink_drop->SetHovered(false);
}
}
if (target_button) {
views::InkDrop* const ink_drop =
views::InkDrop::Get(target_button)->GetInkDrop();
if (ink_drop) {
ink_drop->SetHovered(true);
}
}
hovered_button_ = target_button;
}
break;
// For cases where the help bubble overlaps the target area, the mouse
// exiting should result in un-hovering the current button.
case ui::EventType::kMouseExited:
if (ShouldUnHoverOnMouseExit() && hovered_button_) {
if (views::InkDrop* const ink_drop =
views::InkDrop::Get(hovered_button_)->GetInkDrop()) {
ink_drop->SetHovered(false);
}
hovered_button_ = nullptr;
}
return false;
default:
return false;
}
return true;
}
void HelpBubbleEventRelay::OnConnectionLost() {
if (!sent_click_ && help_bubble_ && help_bubble_->GetWidget() &&
!help_bubble_->GetWidget()->IsClosed()) {
help_bubble_->GetWidget()->CloseWithReason(
views::Widget::ClosedReason::kLostFocus);
}
}
namespace internal {
MenuHelpBubbleEventProcessor::MenuHelpBubbleEventProcessor(
views::MenuItemView* menu_item)
: callback_handle_(menu_item->GetMenuController()->AddAnnotationCallback(
base::BindRepeating(&MenuHelpBubbleEventProcessor::OnEvent,
base::Unretained(this)))) {}
MenuHelpBubbleEventProcessor::~MenuHelpBubbleEventProcessor() = default;
bool MenuHelpBubbleEventProcessor::OnEvent(const ui::LocatedEvent& event) {
return HelpBubbleEventRelay::OnEvent(event, event.root_location());
}
bool MenuHelpBubbleEventProcessor::ShouldHelpBubbleProcessEvents() const {
return true;
}
bool MenuHelpBubbleEventProcessor::ShouldUnHoverOnMouseExit() const {
return false;
}
} // namespace internal
} // namespace user_education