0

float: Add a timer for multitask menu to close on mouse out

If the user opens the multitask menu via hovering over the size button,
the menu will auto close after 3 seconds of not being hovered over the
size button or the multitask menu.

Test: manual
Change-Id: I0cc0a1b2590cc2d67da13dd629e4655794164696
Fixed: b/266441890
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4356609
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Commit-Queue: Sammie Quon <sammiequon@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1122006}
This commit is contained in:
Sammie Quon
2023-03-24 23:39:09 +00:00
committed by Chromium LUCI CQ
parent 700e102e2f
commit 86d7483d59
7 changed files with 123 additions and 26 deletions

@ -673,14 +673,14 @@ class MultitaskMenuTest : public FrameSizeButtonTest {
: nullptr;
}
void ShowMultitaskMenu() {
void ShowMultitaskMenu(MultitaskMenuEntryType entry_type =
MultitaskMenuEntryType::kFrameSizeButtonHover) {
DCHECK(size_button());
views::NamedWidgetShownWaiter waiter(
views::test::AnyWidgetTestPasskey{},
std::string(kMultitaskMenuBubbleWidgetName));
static_cast<FrameSizeButton*>(size_button())
->ShowMultitaskMenu(MultitaskMenuEntryType::kFrameSizeButtonHover);
static_cast<FrameSizeButton*>(size_button())->ShowMultitaskMenu(entry_type);
waiter.WaitIfNeededAndGet();
}
@ -752,7 +752,7 @@ TEST_F(MultitaskMenuTest, HalfButtonSecondaryLayout) {
.SetDisplayRotation(display::Display::ROTATE_180,
display::Display::RotationSource::ACTIVE);
ShowMultitaskMenu();
ShowMultitaskMenu(MultitaskMenuEntryType::kAccel);
// Click on the left side of the half button. It should be in secondary
// snapped state, because in this orientation secondary snapped is actually
@ -941,4 +941,40 @@ TEST_F(MultitaskMenuTest, CloseOnClickOutside) {
ASSERT_FALSE(GetMultitaskMenu());
}
// Tests that moving the mouse outside the menu will close the menu, if opened
// via hovering on the frame size button.
TEST_F(MultitaskMenuTest, MoveMouseOutsideMenu) {
chromeos::MultitaskMenuView::SetSkipMouseOutDelayFoTesting(true);
// Simulate opening the menu by moving the mouse to the frame size button and
// opening the menu.
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(
size_button()->GetBoundsInScreen().CenterPoint());
ShowMultitaskMenu();
MultitaskMenu* multitask_menu = GetMultitaskMenu();
ASSERT_TRUE(multitask_menu);
event_generator->MoveMouseTo(
multitask_menu->GetBoundsInScreen().CenterPoint());
// Widget is closed with a post task.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetMultitaskMenu());
event_generator->MoveMouseTo(gfx::Point(1, 1));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(GetMultitaskMenu());
// Open the menu using the accelerator.
event_generator->MoveMouseTo(
size_button()->GetBoundsInScreen().CenterPoint());
ShowMultitaskMenu(MultitaskMenuEntryType::kAccel);
// Test that the menu remains open if we move outside when using the
// accelerator.
event_generator->MoveMouseTo(gfx::Point(1, 1));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetMultitaskMenu());
}
} // namespace ash

@ -108,7 +108,7 @@ class TabletModeMultitaskMenuView : public views::View {
menu_view_base_ =
AddChildView(std::make_unique<chromeos::MultitaskMenuView>(
window, std::move(callback), buttons));
window, std::move(callback), buttons, /*anchor_view=*/nullptr));
// base::Unretained() is safe since `this` also destroys `menu_view_base_`
// and its child `feedback_button_`.

@ -244,7 +244,9 @@ void FrameSizeButton::ShowMultitaskMenu(MultitaskMenuEntryType entry_type) {
// Owned by the bubble which contains this view. If there is an existing
// bubble, it will be deactivated and then close and destroy itself.
auto menu_delegate = std::make_unique<MultitaskMenu>(
/*anchor=*/this, GetWidget());
/*anchor=*/this, GetWidget(),
/*close_on_move_out=*/entry_type ==
MultitaskMenuEntryType::kFrameSizeButtonHover);
auto* menu_delegate_ptr = menu_delegate.get();
multitask_menu_widget_ =
base::WrapUnique(views::BubbleDialogDelegateView::CreateBubble(

@ -33,7 +33,8 @@ constexpr int kButtonHeight = 28;
} // namespace
MultitaskMenu::MultitaskMenu(views::View* anchor,
views::Widget* parent_widget) {
views::Widget* parent_widget,
bool close_on_move_out) {
DCHECK(parent_widget);
set_corner_radius(kMultitaskMenuBubbleCornerRadius);
@ -61,7 +62,7 @@ MultitaskMenu::MultitaskMenu(views::View* anchor,
multitask_menu_view_ = AddChildView(std::make_unique<MultitaskMenuView>(
parent_window(),
base::BindRepeating(&MultitaskMenu::HideBubble, base::Unretained(this)),
buttons));
buttons, close_on_move_out ? anchor : nullptr));
auto* layout = multitask_menu_view_->SetLayoutManager(
std::make_unique<views::TableLayout>());

@ -30,7 +30,9 @@ class COMPONENT_EXPORT(CHROMEOS_UI_FRAME) MultitaskMenu
public:
METADATA_HEADER(MultitaskMenu);
MultitaskMenu(views::View* anchor, views::Widget* parent_widget);
MultitaskMenu(views::View* anchor,
views::Widget* parent_widget,
bool close_on_move_out);
MultitaskMenu(const MultitaskMenu&) = delete;
MultitaskMenu& operator=(const MultitaskMenu&) = delete;
~MultitaskMenu() override;

@ -9,6 +9,7 @@
#include "base/check.h"
#include "base/functional/callback_forward.h"
#include "base/metrics/user_metrics.h"
#include "base/timer/timer.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "chromeos/ui/base/display_util.h"
#include "chromeos/ui/base/window_properties.h"
@ -43,6 +44,8 @@ namespace chromeos {
namespace {
bool g_skip_mouse_out_delay_for_testing = false;
constexpr int kCenterPadding = 4;
constexpr int kLabelFontSize = 13;
@ -54,6 +57,11 @@ constexpr int kButtonImageSpacing = 4;
constexpr float kButtonRadDivisor = 2.f;
constexpr gfx::Insets kButtonInsets = gfx::Insets::TLBR(0, 6, 0, 8);
// If the menu was opened as a result of hovering over the frame size button,
// moving the mouse outside the menu or size button will result in closing it
// after 3 seconds have elapsed.
constexpr base::TimeDelta kMouseExitMenuTimeout = base::Seconds(3);
// Creates multitask button with label.
std::unique_ptr<views::View> CreateButtonContainer(
std::unique_ptr<views::View> button_view,
@ -78,22 +86,41 @@ std::unique_ptr<views::View> CreateButtonContainer(
class MultitaskMenuView::MenuPreTargetHandler : public ui::EventHandler {
public:
MenuPreTargetHandler(aura::Window* menu_window,
base::RepeatingClosure close_callback)
: menu_window_(menu_window), close_callback_(std::move(close_callback)) {
MenuPreTargetHandler(views::Widget* menu_widget,
base::RepeatingClosure close_callback,
views::View* anchor_view)
: menu_widget_(menu_widget),
anchor_view_(anchor_view),
close_callback_(std::move(close_callback)) {
aura::Env::GetInstance()->AddPreTargetHandler(
this, ui::EventTarget::Priority::kSystem);
}
MenuPreTargetHandler(const MenuPreTargetHandler&) = delete;
MenuPreTargetHandler& operator=(const MenuPreTargetHandler&) = delete;
~MenuPreTargetHandler() override {
aura::Env::GetInstance()->RemovePreTargetHandler(this);
}
void OnMouseEvent(ui::MouseEvent* event) override {
// TODO(b/266441890): Consider closing the menu on ET_MOUSE_MOVED.
if (event->type() == ui::ET_MOUSE_PRESSED) {
ProcessPressedEvent(*event);
}
if (event->type() == ui::ET_MOUSE_MOVED && anchor_view_) {
const gfx::Point screen_location =
event->target()->GetScreenLocation(*event);
// Stop the existing timer if either the anchor or the menu contain the
// event.
if (menu_widget_->GetWindowBoundsInScreen().Contains(screen_location) ||
anchor_view_->GetBoundsInScreen().Contains(screen_location)) {
exit_timer_.Stop();
} else if (g_skip_mouse_out_delay_for_testing) {
OnExitTimerFinished();
} else {
exit_timer_.Start(FROM_HERE, kMouseExitMenuTimeout, this,
&MenuPreTargetHandler::OnExitTimerFinished);
}
}
}
void OnTouchEvent(ui::TouchEvent* event) override {
@ -105,15 +132,24 @@ class MultitaskMenuView::MenuPreTargetHandler : public ui::EventHandler {
void ProcessPressedEvent(const ui::LocatedEvent& event) {
const gfx::Point screen_location = event.target()->GetScreenLocation(event);
// If the event is out of menu bounds, close the menu.
if (!menu_window_->GetBoundsInScreen().Contains(screen_location)) {
if (!menu_widget_->GetWindowBoundsInScreen().Contains(screen_location)) {
close_callback_.Run();
}
}
private:
// The multitask menu that is currently shown. Guaranteed to outlive `this`,
// which will get destroyed when the menu is destructed in `close_callback_`.
aura::Window* const menu_window_;
void OnExitTimerFinished() { close_callback_.Run(); }
// The widget of the multitask menu that is currently shown. Guaranteed to
// outlive `this`, which will get destroyed when the menu is destructed in
// `close_callback_`.
views::Widget* const menu_widget_;
// The anchor of the menu's widget if it exists. Set if there is an anchor and
// we want the menu to close if the mouse has exited the menu bounds.
views::View* anchor_view_ = nullptr;
base::OneShotTimer exit_timer_;
base::RepeatingClosure close_callback_;
};
@ -123,8 +159,11 @@ class MultitaskMenuView::MenuPreTargetHandler : public ui::EventHandler {
MultitaskMenuView::MultitaskMenuView(aura::Window* window,
base::RepeatingClosure close_callback,
uint8_t buttons)
: window_(window), close_callback_(std::move(close_callback)) {
uint8_t buttons,
views::View* anchor_view)
: window_(window),
anchor_view_(anchor_view),
close_callback_(std::move(close_callback)) {
DCHECK(window);
DCHECK(close_callback_);
SetUseDefaultFillLayout(true);
@ -220,8 +259,8 @@ MultitaskMenuView::~MultitaskMenuView() {
void MultitaskMenuView::AddedToWidget() {
// When the menu widget is shown, we install `MenuPreTargetHandler` to close
// the menu on any events outside.
event_handler_ = std::make_unique<MultitaskMenuView::MenuPreTargetHandler>(
GetWidget()->GetNativeWindow(), close_callback_);
event_handler_ = std::make_unique<MenuPreTargetHandler>(
GetWidget(), close_callback_, anchor_view_);
}
void MultitaskMenuView::OnThemeChanged() {
@ -241,6 +280,11 @@ void MultitaskMenuView::OnThemeChanged() {
ui::kColorMultitaskFeedbackButtonLabelForeground)));
}
// static
void MultitaskMenuView::SetSkipMouseOutDelayFoTesting(bool val) {
g_skip_mouse_out_delay_for_testing = val;
}
void MultitaskMenuView::SplitButtonPressed(SnapDirection direction) {
SnapController::Get()->CommitSnap(window_, direction, kDefaultSnapRatio);
close_callback_.Run();

@ -37,9 +37,13 @@ class COMPONENT_EXPORT(CHROMEOS_UI_FRAME) MultitaskMenuView
kFloat = 1 << 3,
};
// `window` is the window that the buttons on this view act on. `anchor_view`
// should be passed when we want the functionality of auto-closing the menu
// when the mouse moves out of the menu or the anchor.
MultitaskMenuView(aura::Window* window,
base::RepeatingClosure on_any_button_pressed,
uint8_t buttons);
uint8_t buttons,
views::View* anchor_view);
MultitaskMenuView(const MultitaskMenuView&) = delete;
MultitaskMenuView& operator=(const MultitaskMenuView&) = delete;
@ -51,6 +55,12 @@ class COMPONENT_EXPORT(CHROMEOS_UI_FRAME) MultitaskMenuView
// views::View:
void AddedToWidget() override;
void OnThemeChanged() override;
// If the menu is opened because of mouse hover, moving the mouse outside the
// menu for 3 seconds will result in it auto closing. This function reduces
// that 3 second dealy to
static void SetSkipMouseOutDelayFoTesting(bool val);
// For testing.
SplitButtonView* half_button_for_testing() {
@ -63,9 +73,6 @@ class COMPONENT_EXPORT(CHROMEOS_UI_FRAME) MultitaskMenuView
return float_button_for_testing_.get();
}
// views::View:
void OnThemeChanged() override;
private:
class MenuPreTargetHandler;
@ -86,6 +93,11 @@ class COMPONENT_EXPORT(CHROMEOS_UI_FRAME) MultitaskMenuView
// The window which the buttons act on. It is guaranteed to outlive `this`.
aura::Window* const window_;
// The view the menu is anchored to if any. This is only passed if we want to
// close the menu when the mouse moves out of the multitask menu or its anchor
// view.
views::View* const anchor_view_;
// Runs after any of the buttons are pressed, or a press out of the menu
// bounds.
base::RepeatingClosure close_callback_;