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:

committed by
Chromium LUCI CQ

parent
700e102e2f
commit
86d7483d59
ash
chromeos/ui/frame
caption_buttons
multitask_menu
@ -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_;
|
||||
|
Reference in New Issue
Block a user