Commit fb3061b1 authored by Elly Fong-Jones's avatar Elly Fong-Jones Committed by Commit Bot

views: support Mac menu closure animations

This change:
1) Adds MenuClosureAnimationMac, which implements the Mac menu closure
   animation;
2) Adds logic to MenuItemView to force drawing the item in a selected
   or unselected state as needed, for use in this animation;
3) Adds support to MenuController for disabling handling input events,
   so that the closure animation can appear to be "synchronous";
4) Makes MenuController::Accept possibly asynchronous;
5) Updates unit tests to account for (4)

Bug: 829347
Change-Id: I8676718e6a5e9704b422ed4cb08e8d10002bf45a
Reviewed-on: https://chromium-review.googlesource.com/999803
Commit-Queue: Elly Fong-Jones <ellyjones@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Cr-Commit-Position: refs/heads/master@{#550160}
parent 5208b8e8
......@@ -11,6 +11,7 @@
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/test/menu_test_utils.h"
#include "ui/views/widget/widget.h"
MenuTestBase::MenuTestBase()
......@@ -29,6 +30,7 @@ void MenuTestBase::Click(views::View* view, const base::Closure& next) {
ui_controls::LEFT,
ui_controls::DOWN | ui_controls::UP,
next);
views::test::WaitForMenuClosureAnimation();
}
void MenuTestBase::KeyPress(ui::KeyboardCode keycode, base::OnceClosure next) {
......@@ -42,6 +44,8 @@ int MenuTestBase::GetMenuRunnerFlags() {
}
void MenuTestBase::SetUp() {
views::test::DisableMenuClosureAnimations();
button_ = new views::MenuButton(base::ASCIIToUTF16("Menu Test"), this, true);
menu_ = new views::MenuItemView(this);
BuildMenu(menu_);
......
......@@ -31,6 +31,7 @@
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/test/menu_test_utils.h"
namespace {
......@@ -127,6 +128,7 @@ void TestWhileContextMenuOpen(Browser* browser,
ui_controls::SendMouseMove(action_view_loc.x(), action_view_loc.y());
EXPECT_TRUE(ui_test_utils::SendMouseEventsSync(
ui_controls::LEFT, ui_controls::DOWN | ui_controls::UP));
views::test::WaitForMenuClosureAnimation();
// Test resumes in the main test body.
}
......@@ -216,6 +218,7 @@ IN_PROC_BROWSER_TEST_F(ToolbarActionViewInteractiveUITest,
IN_PROC_BROWSER_TEST_F(ToolbarActionViewInteractiveUITest,
MAYBE_TestContextMenuOnOverflowedAction) {
views::MenuController::TurnOffMenuSelectionHoldForTest();
views::test::DisableMenuClosureAnimations();
// Load an extension that has a home page (important for the context menu's
// first item being enabled).
......
......@@ -120,6 +120,7 @@ jumbo_component("views") {
"controls/label.h",
"controls/link.h",
"controls/link_listener.h",
"controls/menu/menu_closure_animation_mac.h",
"controls/menu/menu_config.h",
"controls/menu/menu_controller.h",
"controls/menu/menu_controller_delegate.h",
......@@ -317,6 +318,7 @@ jumbo_component("views") {
"controls/label.cc",
"controls/link.cc",
"controls/menu/display_change_listener_mac.cc",
"controls/menu/menu_closure_animation_mac.mm",
"controls/menu/menu_config.cc",
"controls/menu/menu_config_chromeos.cc",
"controls/menu/menu_config_linux.cc",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_VIEWS_CONTROLS_MENU_MENU_CLOSURE_ANIMATION_MAC_H_
#define UI_VIEWS_CONTROLS_MENU_MENU_CLOSURE_ANIMATION_MAC_H_
#include "base/macros.h"
#include "base/timer/timer.h"
#include "ui/gfx/animation/animation.h"
#include "ui/gfx/animation/animation_delegate.h"
#include "ui/views/views_export.h"
namespace views {
class MenuItemView;
// This class implements the Mac menu closure animation:
// 1) For 100ms, the selected item is drawn as unselected
// 2) Then, for another 100ms, the selected item is drawn as selected
// 3) Then, and the window fades over 250ms to transparency
// Note that this class is owned by the involved MenuController, so if the menu
// is destructed early for any reason, this class will be destructed also, which
// will stop the timer or animation (if they are running), so the callback will
// *not* be run - which is good, since the MenuController that would have
// received it is being deleted.
class VIEWS_EXPORT MenuClosureAnimationMac : public gfx::AnimationDelegate {
public:
// After this closure animation is done, |callback| is run to finally accept
// |item|.
MenuClosureAnimationMac(MenuItemView* item, base::OnceClosure callback);
~MenuClosureAnimationMac() override;
// Start the animation.
void Start();
// Returns the MenuItemView this animation targets.
MenuItemView* item() { return item_; }
// Causes animations to take no time for testing purposes. Note that this
// still causes the completion callback to be run asynchronously, so test
// situations have the same control flow as non-test situations.
static void DisableAnimationsForTesting();
private:
enum class AnimationStep {
kStart,
kUnselected,
kSelected,
kFading,
kFinish,
};
static constexpr AnimationStep NextStepFor(AnimationStep step);
void AdvanceAnimation();
// gfx::AnimationDelegate:
void AnimationProgressed(const gfx::Animation* animation) override;
void AnimationEnded(const gfx::Animation* animation) override;
void AnimationCanceled(const gfx::Animation* animation) override;
base::OnceClosure callback_;
base::OneShotTimer timer_;
std::unique_ptr<gfx::Animation> fade_animation_;
MenuItemView* item_;
AnimationStep step_;
DISALLOW_COPY_AND_ASSIGN(MenuClosureAnimationMac);
};
} // namespace views
#endif // UI_VIEWS_CONTROLS_MENU_MENU_CLOSURE_ANIMATION_MAC_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/controls/menu/menu_closure_animation_mac.h"
#import <Cocoa/Cocoa.h>
#include "base/logging.h"
#include "base/threading/thread_task_runner_handle.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/widget/widget.h"
namespace {
static bool g_disable_animations_for_testing = false;
}
namespace views {
MenuClosureAnimationMac::MenuClosureAnimationMac(MenuItemView* item,
base::OnceClosure callback)
: callback_(std::move(callback)),
item_(item),
step_(AnimationStep::kStart) {}
MenuClosureAnimationMac::~MenuClosureAnimationMac() {}
void MenuClosureAnimationMac::Start() {
DCHECK_EQ(step_, AnimationStep::kStart);
if (g_disable_animations_for_testing) {
// Even when disabling animations, simulate the fact that the eventual
// accept callback will happen after a runloop cycle by skipping to the end
// of the animation.
step_ = AnimationStep::kFading;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&MenuClosureAnimationMac::AdvanceAnimation,
base::Unretained(this)));
return;
}
AdvanceAnimation();
}
// static
constexpr MenuClosureAnimationMac::AnimationStep
MenuClosureAnimationMac::NextStepFor(
MenuClosureAnimationMac::AnimationStep step) {
switch (step) {
case AnimationStep::kStart:
return AnimationStep::kUnselected;
case AnimationStep::kUnselected:
return AnimationStep::kSelected;
case AnimationStep::kSelected:
return AnimationStep::kFading;
case AnimationStep::kFading:
return AnimationStep::kFinish;
case AnimationStep::kFinish:
return AnimationStep::kFinish;
}
}
void MenuClosureAnimationMac::AdvanceAnimation() {
step_ = NextStepFor(step_);
if (step_ == AnimationStep::kUnselected ||
step_ == AnimationStep::kSelected) {
item_->SetForcedVisualSelection(step_ == AnimationStep::kSelected);
timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(80),
base::BindRepeating(&MenuClosureAnimationMac::AdvanceAnimation,
base::Unretained(this)));
} else if (step_ == AnimationStep::kFading) {
auto fade = std::make_unique<gfx::LinearAnimation>(this);
fade->SetDuration(base::TimeDelta::FromMilliseconds(200));
fade_animation_ = std::move(fade);
fade_animation_->Start();
} else if (step_ == AnimationStep::kFinish) {
std::move(callback_).Run();
}
}
// static
void MenuClosureAnimationMac::DisableAnimationsForTesting() {
g_disable_animations_for_testing = true;
}
void MenuClosureAnimationMac::AnimationProgressed(
const gfx::Animation* animation) {
NSWindow* window = item_->GetWidget()->GetNativeWindow();
[window setAlphaValue:animation->CurrentValueBetween(1.0, 0.0)];
}
void MenuClosureAnimationMac::AnimationEnded(const gfx::Animation* animation) {
AdvanceAnimation();
}
void MenuClosureAnimationMac::AnimationCanceled(
const gfx::Animation* animation) {
NOTREACHED();
}
} // namespace views
......@@ -1148,6 +1148,13 @@ void MenuController::TurnOffMenuSelectionHoldForTest() {
menu_selection_hold_time_ms = -1;
}
void MenuController::OnMenuItemDestroying(MenuItemView* menu_item) {
#if defined(OS_MACOSX)
if (menu_closure_animation_ && menu_closure_animation_->item() == menu_item)
menu_closure_animation_.reset();
#endif
}
void MenuController::SetSelection(MenuItemView* menu_item,
int selection_types) {
size_t paths_differ_at = 0;
......@@ -1473,6 +1480,18 @@ void MenuController::UpdateInitialLocation(const gfx::Rect& bounds,
}
void MenuController::Accept(MenuItemView* item, int event_flags) {
#if defined(OS_MACOSX)
menu_closure_animation_ = std::make_unique<MenuClosureAnimationMac>(
item,
base::BindOnce(&MenuController::ReallyAccept, base::Unretained(this),
base::Unretained(item), event_flags));
menu_closure_animation_->Start();
#else
ReallyAccept(item, event_flags);
#endif
}
void MenuController::ReallyAccept(MenuItemView* item, int event_flags) {
DCHECK(IsBlockingRun());
result_ = item;
if (item && !menu_stack_.empty() &&
......@@ -2747,4 +2766,12 @@ void MenuController::SetHotTrackedButton(Button* hot_button) {
}
}
bool MenuController::CanProcessInputEvents() const {
#if defined(OS_MACOSX)
return !menu_closure_animation_;
#else
return true;
#endif
}
} // namespace views
......@@ -25,6 +25,10 @@
#include "ui/views/controls/menu/menu_delegate.h"
#include "ui/views/widget/widget_observer.h"
#if defined(OS_MACOSX)
#include "ui/views/controls/menu/menu_closure_animation_mac.h"
#endif
namespace ui {
class OSExchangeData;
}
......@@ -204,6 +208,13 @@ class VIEWS_EXPORT MenuController
}
bool use_touchable_layout() const { return use_touchable_layout_; }
// Notifies |this| that |menu_item| is being destroyed.
void OnMenuItemDestroying(MenuItemView* menu_item);
// Returns whether this menu can handle input events right now. This method
// can return false while running animations.
bool CanProcessInputEvents() const;
private:
friend class internal::MenuRunnerImpl;
friend class test::MenuControllerTest;
......@@ -344,6 +355,7 @@ class VIEWS_EXPORT MenuController
// Invoked when the user accepts the selected item. This is only used
// when blocking. This schedules the loop to quit.
void Accept(MenuItemView* item, int event_flags);
void ReallyAccept(MenuItemView* item, int event_flags);
bool ShowSiblingMenu(SubmenuView* source, const gfx::Point& mouse_location);
......@@ -699,6 +711,10 @@ class VIEWS_EXPORT MenuController
// A mask of the EventFlags for the mouse buttons currently pressed.
int current_mouse_pressed_state_ = 0;
#if defined(OS_MACOSX)
std::unique_ptr<MenuClosureAnimationMac> menu_closure_animation_;
#endif
#if defined(USE_AURA)
std::unique_ptr<MenuPreTargetHandler> menu_pre_target_handler_;
#endif
......
......@@ -464,6 +464,7 @@ class MenuControllerTest : public ViewsTestBase {
void Accept(MenuItemView* item, int event_flags) {
menu_controller_->Accept(item, event_flags);
views::test::WaitForMenuClosureAnimation();
}
// Causes the |menu_controller_| to begin dragging. Use TestDragDropClient to
......@@ -943,6 +944,8 @@ TEST_F(MenuControllerTest, ChildButtonHotTrackedWhenNested) {
// Tests that a menu opened asynchronously, will notify its
// MenuControllerDelegate when Accept is called.
TEST_F(MenuControllerTest, AsynchronousAccept) {
views::test::DisableMenuClosureAnimations();
MenuController* controller = menu_controller();
controller->Run(owner(), nullptr, menu_item(), gfx::Rect(),
MENU_ANCHOR_TOPLEFT, false, false);
......@@ -1254,6 +1257,7 @@ TEST_F(MenuControllerTest, AsynchronousRepostEventDeletesController) {
// Tests that having the MenuController deleted during OnGestureEvent does not
// cause a crash. ASAN bots should not detect use-after-free in MenuController.
TEST_F(MenuControllerTest, AsynchronousGestureDeletesController) {
views::test::DisableMenuClosureAnimations();
MenuController* controller = menu_controller();
std::unique_ptr<TestMenuControllerDelegate> nested_delegate(
new TestMenuControllerDelegate());
......@@ -1277,6 +1281,7 @@ TEST_F(MenuControllerTest, AsynchronousGestureDeletesController) {
// gesture event. The remainder of this test, and TearDown should not crash.
DestroyMenuControllerOnMenuClosed(nested_delegate.get());
controller->OnGestureEvent(sub_menu, &event);
views::test::WaitForMenuClosureAnimation();
// Close to remove observers before test TearDown
sub_menu->Close();
......
......@@ -14,40 +14,41 @@ MenuHostRootView::MenuHostRootView(Widget* widget, SubmenuView* submenu)
: internal::RootView(widget), submenu_(submenu) {}
bool MenuHostRootView::OnMousePressed(const ui::MouseEvent& event) {
return GetMenuController() &&
GetMenuController()->OnMousePressed(submenu_, event);
return GetMenuControllerForInputEvents() &&
GetMenuControllerForInputEvents()->OnMousePressed(submenu_, event);
}
bool MenuHostRootView::OnMouseDragged(const ui::MouseEvent& event) {
return GetMenuController() &&
GetMenuController()->OnMouseDragged(submenu_, event);
return GetMenuControllerForInputEvents() &&
GetMenuControllerForInputEvents()->OnMouseDragged(submenu_, event);
}
void MenuHostRootView::OnMouseReleased(const ui::MouseEvent& event) {
if (GetMenuController())
GetMenuController()->OnMouseReleased(submenu_, event);
if (GetMenuControllerForInputEvents())
GetMenuControllerForInputEvents()->OnMouseReleased(submenu_, event);
}
void MenuHostRootView::OnMouseMoved(const ui::MouseEvent& event) {
if (GetMenuController())
GetMenuController()->OnMouseMoved(submenu_, event);
if (GetMenuControllerForInputEvents())
GetMenuControllerForInputEvents()->OnMouseMoved(submenu_, event);
}
bool MenuHostRootView::OnMouseWheel(const ui::MouseWheelEvent& event) {
return GetMenuController() &&
GetMenuController()->OnMouseWheel(submenu_, event);
return GetMenuControllerForInputEvents() &&
GetMenuControllerForInputEvents()->OnMouseWheel(submenu_, event);
}
View* MenuHostRootView::GetTooltipHandlerForPoint(const gfx::Point& point) {
return GetMenuController()
? GetMenuController()->GetTooltipHandlerForPoint(submenu_, point)
return GetMenuControllerForInputEvents()
? GetMenuControllerForInputEvents()->GetTooltipHandlerForPoint(
submenu_, point)
: nullptr;
}
void MenuHostRootView::ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) {
if (GetMenuController())
GetMenuController()->ViewHierarchyChanged(submenu_, details);
if (GetMenuControllerForInputEvents())
GetMenuControllerForInputEvents()->ViewHierarchyChanged(submenu_, details);
RootView::ViewHierarchyChanged(details);
}
......@@ -87,4 +88,10 @@ MenuController* MenuHostRootView::GetMenuController() {
return submenu_ ? submenu_->GetMenuItem()->GetMenuController() : NULL;
}
MenuController* MenuHostRootView::GetMenuControllerForInputEvents() {
return GetMenuController() && GetMenuController()->CanProcessInputEvents()
? GetMenuController()
: nullptr;
}
} // namespace views
......@@ -47,6 +47,7 @@ class MenuHostRootView : public internal::RootView {
// Returns the MenuController for this MenuHostRootView.
MenuController* GetMenuController();
MenuController* GetMenuControllerForInputEvents();
// The SubmenuView we contain.
SubmenuView* submenu_;
......
......@@ -628,6 +628,11 @@ void MenuItemView::SetMargins(int top_margin, int bottom_margin) {
invalidate_dimensions();
}
void MenuItemView::SetForcedVisualSelection(bool selected) {
forced_visual_selection_ = selected;
SchedulePaint();
}
MenuItemView::MenuItemView(MenuItemView* parent,
int command,
MenuItemView::Type type)
......@@ -654,6 +659,8 @@ MenuItemView::MenuItemView(MenuItemView* parent,
}
MenuItemView::~MenuItemView() {
if (GetMenuController())
GetMenuController()->OnMenuItemDestroying(this);
delete submenu_;
for (auto* item : removed_items_)
delete item;
......@@ -825,6 +832,8 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) {
(mode == PB_NORMAL && IsSelected() &&
parent_menu_item_->GetSubmenu()->GetShowSelection(this) &&
(NonIconChildViewsCount() == 0));
if (forced_visual_selection_.has_value())
render_selection = *forced_visual_selection_;
MenuDelegate *delegate = GetDelegate();
bool emphasized =
......
......@@ -12,6 +12,7 @@
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/strings/string16.h"
#include "build/build_config.h"
#include "ui/base/models/menu_separator_types.h"
......@@ -337,6 +338,11 @@ class VIEWS_EXPORT MenuItemView : public View {
use_right_margin_ = use_right_margin;
}
// Controls whether this menu has a forced visual selection state. This is
// used when animating item acceptance on Mac. Note that once this is set
// there's no way to unset it for this MenuItemView!
void SetForcedVisualSelection(bool selected);
protected:
// Creates a MenuItemView. This is used by the various AddXXX methods.
MenuItemView(MenuItemView* parent, int command, Type type);
......@@ -556,6 +562,9 @@ class VIEWS_EXPORT MenuItemView : public View {
// The submenu indicator arrow icon in case the menu item has a Submenu.
ImageView* submenu_arrow_image_view_;
// The forced visual selection state of this item, if any.
base::Optional<bool> forced_visual_selection_;
DISALLOW_COPY_AND_ASSIGN(MenuItemView);
};
......
......@@ -163,6 +163,7 @@ TEST_F(MenuRunnerTest, LatinMnemonic) {
if (IsMus())
return;
views::test::DisableMenuClosureAnimations();
InitMenuRunner(0);
MenuRunner* runner = menu_runner();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT,
......@@ -171,6 +172,7 @@ TEST_F(MenuRunnerTest, LatinMnemonic) {
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
generator.PressKey(ui::VKEY_O, 0);
views::test::WaitForMenuClosureAnimation();
EXPECT_FALSE(runner->IsRunning());
TestMenuDelegate* delegate = menu_delegate();
EXPECT_EQ(1, delegate->execute_command_id());
......@@ -186,6 +188,7 @@ TEST_F(MenuRunnerTest, NonLatinMnemonic) {
if (IsMus())
return;
views::test::DisableMenuClosureAnimations();
InitMenuRunner(0);
MenuRunner* runner = menu_runner();
runner->RunMenuAt(owner(), nullptr, gfx::Rect(), MENU_ANCHOR_TOPLEFT,
......@@ -195,6 +198,7 @@ TEST_F(MenuRunnerTest, NonLatinMnemonic) {
ui::test::EventGenerator generator(GetContext(), owner()->GetNativeWindow());
ui::KeyEvent key_press(0x062f, ui::VKEY_N, 0);
generator.Dispatch(&key_press);
views::test::WaitForMenuClosureAnimation();
EXPECT_FALSE(runner->IsRunning());
TestMenuDelegate* delegate = menu_delegate();
EXPECT_EQ(2, delegate->execute_command_id());
......
......@@ -4,8 +4,14 @@
#include "ui/views/test/menu_test_utils.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "ui/views/controls/menu/menu_controller.h"
#if defined(OS_MACOSX)
#include "ui/views/controls/menu/menu_closure_animation_mac.h"
#endif
namespace views {
namespace test {
......@@ -75,5 +81,17 @@ void MenuControllerTestApi::SetShowing(bool showing) {
controller_->showing_ = showing;
}
void DisableMenuClosureAnimations() {
#if defined(OS_MACOSX)
MenuClosureAnimationMac::DisableAnimationsForTesting();
#endif
}
void WaitForMenuClosureAnimation() {
#if defined(OS_MACOSX)
base::RunLoop().RunUntilIdle();
#endif
}
} // namespace test
} // namespace views
......@@ -98,6 +98,15 @@ class MenuControllerTestApi {
DISALLOW_COPY_AND_ASSIGN(MenuControllerTestApi);
};
// On platforms which have menu closure animations, these functions are
// necessary to:
// 1) Disable those animations (make them take zero time) to avoid slowing
// down tests;
// 2) Wait for maybe-asynchronous menu closure to finish.
// On platforms without menu closure animations, these do nothing.
void DisableMenuClosureAnimations();
void WaitForMenuClosureAnimation();
} // namespace test
} // namespace views
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment