Commit 1a51c144 authored by Toni Barzic's avatar Toni Barzic Committed by Commit Bot

Add contextual nudge for home to overview gesture

BUG=1008963

Change-Id: I1f40729b3338c12360be783c300bca3ac5c7d7d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2069438
Commit-Queue: Toni Baržić <tbarzic@chromium.org>
Reviewed-by: default avatarManu Cornet <manucornet@chromium.org>
Reviewed-by: default avatarYulun Wu <yulunwu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#744814}
parent 8f008470
...@@ -608,6 +608,8 @@ component("ash") { ...@@ -608,6 +608,8 @@ component("ash") {
"shelf/home_button.h", "shelf/home_button.h",
"shelf/home_button_controller.cc", "shelf/home_button_controller.cc",
"shelf/home_button_controller.h", "shelf/home_button_controller.h",
"shelf/home_to_overview_nudge_controller.cc",
"shelf/home_to_overview_nudge_controller.h",
"shelf/hotseat_transition_animator.cc", "shelf/hotseat_transition_animator.cc",
"shelf/hotseat_transition_animator.h", "shelf/hotseat_transition_animator.h",
"shelf/hotseat_widget.cc", "shelf/hotseat_widget.cc",
...@@ -1872,6 +1874,7 @@ test("ash_unittests") { ...@@ -1872,6 +1874,7 @@ test("ash_unittests") {
"shelf/back_button_unittest.cc", "shelf/back_button_unittest.cc",
"shelf/contextual_tooltip_unittest.cc", "shelf/contextual_tooltip_unittest.cc",
"shelf/home_button_unittest.cc", "shelf/home_button_unittest.cc",
"shelf/home_to_overview_nudge_controller_unittest.cc",
"shelf/hotseat_widget_unittest.cc", "shelf/hotseat_widget_unittest.cc",
"shelf/login_shelf_view_unittest.cc", "shelf/login_shelf_view_unittest.cc",
"shelf/scrollable_shelf_view_unittest.cc", "shelf/scrollable_shelf_view_unittest.cc",
......
...@@ -503,6 +503,9 @@ This file contains the strings for ash. ...@@ -503,6 +503,9 @@ This file contains the strings for ash.
<message name="IDS_ASH_DRAG_HANDLE_NUDGE" desc="The text shown to users above the drag handle when the device is in tablet mode and showing the in-app shelf. Instructs them that they can transition to home screen by swiping up from the shelf."> <message name="IDS_ASH_DRAG_HANDLE_NUDGE" desc="The text shown to users above the drag handle when the device is in tablet mode and showing the in-app shelf. Instructs them that they can transition to home screen by swiping up from the shelf.">
Swipe up to go home Swipe up to go home
</message> </message>
<message name="IDS_ASH_HOME_TO_OVERVIEW_CONTEXTUAL_NUDGE" desc="Text shown to the user below shelf on the home screen in tablet mode, instructing them that they can transition to overview UI by swiping up from the shelf and holding.">
Swipe up and hold to see open apps
</message>
<message name="IDS_ASH_FULLSCREEN_MAGNIFIER_SHORTCUT_DISABLED" <message name="IDS_ASH_FULLSCREEN_MAGNIFIER_SHORTCUT_DISABLED"
desc="The label used in the notification used to indicate that, the shortcut for full-screen magnifier feature has been disabled."> desc="The label used in the notification used to indicate that, the shortcut for full-screen magnifier feature has been disabled.">
the full-screen magnifier the full-screen magnifier
......
bdce7dec724db1b0c24110cfadbdfe831f9d0ed8
\ No newline at end of file
...@@ -24,12 +24,24 @@ constexpr int kTooltipHeight = 18; ...@@ -24,12 +24,24 @@ constexpr int kTooltipHeight = 18;
// ash/tooltip/tooltip_controller.cc // ash/tooltip/tooltip_controller.cc
constexpr int kTooltipMaxWidth = 250; constexpr int kTooltipMaxWidth = 250;
views::BubbleBorder::Arrow GetArrowForPosition(
ContextualNudge::Position position) {
switch (position) {
case ContextualNudge::Position::kTop:
return views::BubbleBorder::BOTTOM_CENTER;
case ContextualNudge::Position::kBottom:
return views::BubbleBorder::TOP_CENTER;
}
}
} // namespace } // namespace
ContextualNudge::ContextualNudge(views::View* anchor, ContextualNudge::ContextualNudge(views::View* anchor,
const base::string16& text) aura::Window* parent_window,
const base::string16& text,
Position position)
: views::BubbleDialogDelegateView(anchor, : views::BubbleDialogDelegateView(anchor,
views::BubbleBorder::BOTTOM_CENTER, GetArrowForPosition(position),
views::BubbleBorder::NO_ASSETS) { views::BubbleBorder::NO_ASSETS) {
set_color(SK_ColorTRANSPARENT); set_color(SK_ColorTRANSPARENT);
set_margins(gfx::Insets(0, 0)); set_margins(gfx::Insets(0, 0));
...@@ -40,9 +52,13 @@ ContextualNudge::ContextualNudge(views::View* anchor, ...@@ -40,9 +52,13 @@ ContextualNudge::ContextualNudge(views::View* anchor,
set_shadow(views::BubbleBorder::NO_ASSETS); set_shadow(views::BubbleBorder::NO_ASSETS);
DialogDelegate::set_buttons(ui::DIALOG_BUTTON_NONE); DialogDelegate::set_buttons(ui::DIALOG_BUTTON_NONE);
if (parent_window) {
set_parent_window(parent_window);
} else if (anchor_widget()) {
set_parent_window( set_parent_window(
anchor_widget()->GetNativeWindow()->GetRootWindow()->GetChildById( anchor_widget()->GetNativeWindow()->GetRootWindow()->GetChildById(
kShellWindowId_ShelfContainer)); kShellWindowId_ShelfContainer));
}
SetLayoutManager(std::make_unique<views::FillLayout>()); SetLayoutManager(std::make_unique<views::FillLayout>());
...@@ -62,6 +78,10 @@ ContextualNudge::ContextualNudge(views::View* anchor, ...@@ -62,6 +78,10 @@ ContextualNudge::ContextualNudge(views::View* anchor,
ContextualNudge::~ContextualNudge() = default; ContextualNudge::~ContextualNudge() = default;
void ContextualNudge::UpdateAnchorRect(const gfx::Rect& rect) {
SetAnchorRect(rect);
}
gfx::Size ContextualNudge::CalculatePreferredSize() const { gfx::Size ContextualNudge::CalculatePreferredSize() const {
const gfx::Size size = BubbleDialogDelegateView::CalculatePreferredSize(); const gfx::Size size = BubbleDialogDelegateView::CalculatePreferredSize();
return gfx::Size(std::min(size.width(), kTooltipMaxWidth), return gfx::Size(std::min(size.width(), kTooltipMaxWidth),
......
...@@ -18,7 +18,21 @@ namespace ash { ...@@ -18,7 +18,21 @@ namespace ash {
// The implementation of contextual nudge tooltip bubbles. // The implementation of contextual nudge tooltip bubbles.
class ASH_EXPORT ContextualNudge : public views::BubbleDialogDelegateView { class ASH_EXPORT ContextualNudge : public views::BubbleDialogDelegateView {
public: public:
ContextualNudge(views::View* anchor, const base::string16& text); // Indicates whether the nudge should be shown below or above the anchor.
enum class Position { kBottom, kTop };
// |anchor| - The view to which the nudge bubble should be anchored. May be
// nullptr, in which case anchor bounds should be provided using
// UpdateAnchorRect().
// |parent_window| - if set, the window that should parent the nudge native
// window. If not set, the shelf container in the anchor view's root
// window will be used.
// |text| - The nudge text.
// |position| - The nudge position relative to the anchor rectangle.
ContextualNudge(views::View* anchor,
aura::Window* parent_window,
const base::string16& text,
Position position);
~ContextualNudge() override; ~ContextualNudge() override;
ContextualNudge(const ContextualNudge&) = delete; ContextualNudge(const ContextualNudge&) = delete;
...@@ -26,6 +40,10 @@ class ASH_EXPORT ContextualNudge : public views::BubbleDialogDelegateView { ...@@ -26,6 +40,10 @@ class ASH_EXPORT ContextualNudge : public views::BubbleDialogDelegateView {
views::Label* label() { return label_; } views::Label* label() { return label_; }
// Sets the nudge bubble anchor rect - should be used to set the anchor rect
// if no valid anchor was passed to the nudge bubble.
void UpdateAnchorRect(const gfx::Rect& rect);
// BubbleDialogDelegateView: // BubbleDialogDelegateView:
gfx::Size CalculatePreferredSize() const override; gfx::Size CalculatePreferredSize() const override;
ui::LayerType GetLayerType() const override; ui::LayerType GetLayerType() const override;
......
...@@ -39,6 +39,8 @@ std::string TooltipTypeToString(TooltipType type) { ...@@ -39,6 +39,8 @@ std::string TooltipTypeToString(TooltipType type) {
return "drag_handle"; return "drag_handle";
case TooltipType::kBackGesture: case TooltipType::kBackGesture:
return "back_gesture"; return "back_gesture";
case TooltipType::kHomeToOverview:
return "home_to_overview";
} }
return "invalid"; return "invalid";
} }
......
...@@ -18,6 +18,7 @@ namespace contextual_tooltip { ...@@ -18,6 +18,7 @@ namespace contextual_tooltip {
enum class TooltipType { enum class TooltipType {
kDragHandle, kDragHandle,
kBackGesture, kBackGesture,
kHomeToOverview,
}; };
// Maximum number of times a user can be shown a contextual nudge if the user // Maximum number of times a user can be shown a contextual nudge if the user
......
...@@ -128,8 +128,10 @@ void DragHandle::OnGestureEvent(ui::GestureEvent* event) { ...@@ -128,8 +128,10 @@ void DragHandle::OnGestureEvent(ui::GestureEvent* event) {
void DragHandle::ShowDragHandleTooltip() { void DragHandle::ShowDragHandleTooltip() {
DCHECK(!drag_handle_nudge_); DCHECK(!drag_handle_nudge_);
drag_handle_nudge_ = new ContextualNudge( drag_handle_nudge_ =
this, l10n_util::GetStringUTF16(IDS_ASH_DRAG_HANDLE_NUDGE)); new ContextualNudge(this, nullptr /*parent_window*/,
l10n_util::GetStringUTF16(IDS_ASH_DRAG_HANDLE_NUDGE),
ContextualNudge::Position::kTop);
drag_handle_nudge_->GetWidget()->Show(); drag_handle_nudge_->GetWidget()->Show();
drag_handle_nudge_->label()->layer()->SetOpacity(0.0f); drag_handle_nudge_->label()->layer()->SetOpacity(0.0f);
......
// Copyright 2020 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 "ash/shelf/home_to_overview_nudge_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/contextual_nudge.h"
#include "ash/shelf/contextual_tooltip.h"
#include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/scrollable_shelf_view.h"
#include "ash/shelf/shelf.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/bind.h"
#include "base/location.h"
#include "base/time/time.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/transform.h"
namespace ash {
namespace {
// The amount of time after home shelf is shown before showing the nudge.
constexpr base::TimeDelta kShowDelay = base::TimeDelta::FromSeconds(2);
// The duration of nudge opacity animations.
constexpr base::TimeDelta kNudgeFadeDuration =
base::TimeDelta::FromMilliseconds(300);
// The duration of a single component of the nudge position animation - the
// nudge is transformed vertically up and down for a preset number of
// iterations.
constexpr base::TimeDelta kNudgeTranformComponentDuration =
base::TimeDelta::FromMilliseconds(600);
// The baseline vertical offset from default kShown state bounds added to
// hotseat position when the nudge is shown - this is the offset that the
// hotseat will have once show throb animation completes.
constexpr int kHotseatBaselineNudgeOffset = -20;
// The number of times the nudge should be moved up and down when the nudge is
// shown.
constexpr int kNudgeShowThrobIterations = 5;
// The vertical max vertical ofsset from the baseline position during nudge show
// animation.
constexpr int kNudgeShowThrobAmplitude = 6;
// The vertical distance between the nudge widget and the hotseat.
constexpr int kNudgeMargins = 4;
class ObserverToCloseWidget : public ui::ImplicitAnimationObserver {
public:
explicit ObserverToCloseWidget(views::Widget* widget) : widget_(widget) {}
ObserverToCloseWidget(const ObserverToCloseWidget& other) = delete;
ObserverToCloseWidget& operator=(const ObserverToCloseWidget& other) = delete;
~ObserverToCloseWidget() override { StopObservingImplicitAnimations(); }
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
widget_->Close();
delete this;
}
private:
views::Widget* const widget_;
};
} // namespace
HomeToOverviewNudgeController::HomeToOverviewNudgeController(
HotseatWidget* hotseat_widget)
: hotseat_widget_(hotseat_widget) {}
HomeToOverviewNudgeController::~HomeToOverviewNudgeController() = default;
void HomeToOverviewNudgeController::SetNudgeAllowedForCurrentShelf(
bool allowed) {
if (nudge_allowed_for_shelf_state_ == allowed)
return;
nudge_allowed_for_shelf_state_ = allowed;
if (!nudge_allowed_for_shelf_state_) {
nudge_show_timer_.Stop();
nudge_hide_timer_.Stop();
HideNudge();
return;
}
DCHECK(!nudge_);
PrefService* pref_service =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (!contextual_tooltip::ShouldShowNudge(
pref_service, contextual_tooltip::TooltipType::kHomeToOverview)) {
return;
}
nudge_hide_timer_.Stop();
nudge_show_timer_.Start(
FROM_HERE, kShowDelay,
base::BindOnce(&HomeToOverviewNudgeController::ShowNudge,
base::Unretained(this)));
}
void HomeToOverviewNudgeController::OnWidgetDestroying(views::Widget* widget) {
nudge_ = nullptr;
widget_observer_.RemoveAll();
}
void HomeToOverviewNudgeController::OnWidgetBoundsChanged(
views::Widget* widget,
const gfx::Rect& new_bounds) {
if (widget == hotseat_widget_)
UpdateNudgeAnchorBounds();
}
bool HomeToOverviewNudgeController::HasShowTimerForTesting() const {
return nudge_show_timer_.IsRunning();
}
void HomeToOverviewNudgeController::FireShowTimerForTesting() {
nudge_show_timer_.FireNow();
}
bool HomeToOverviewNudgeController::HasHideTimerForTesting() const {
return nudge_hide_timer_.IsRunning();
}
void HomeToOverviewNudgeController::FireHideTimerForTesting() {
nudge_hide_timer_.FireNow();
}
void HomeToOverviewNudgeController::ShowNudge() {
DCHECK(!nudge_);
// The nudge is effectively anchored below the hotseat widget, but the nudge
// center is not generally aligned with the hotseat widget center. The nudge
// should be horizontally centered in the screen, which might not be the
// case for the hotseat widget bounds on home screen.
// To work around this, HomeToOverviewNudgeController will update the anchor
// bounds directly - see UpdateNudgeAnchorBounds().
nudge_ = new ContextualNudge(
nullptr, hotseat_widget_->GetNativeWindow()->parent(),
l10n_util::GetStringUTF16(IDS_ASH_HOME_TO_OVERVIEW_CONTEXTUAL_NUDGE),
ContextualNudge::Position::kBottom);
nudge_->set_margins(gfx::Insets(kNudgeMargins));
UpdateNudgeAnchorBounds();
widget_observer_.Add(nudge_->GetWidget());
widget_observer_.Add(hotseat_widget_);
nudge_->GetWidget()->Show();
nudge_->GetWidget()->GetLayer()->SetTransform(gfx::Transform());
nudge_->label()->layer()->SetOpacity(0.0f);
hotseat_widget_->GetLayer()->SetTransform(gfx::Transform());
base::TimeDelta total_animation_duration;
// Initial animation - nudge slides in form the bottom, and hotseat moves up.
auto animate_initial_transform = [](ui::Layer* layer) -> base::TimeDelta {
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetTweenType(gfx::Tween::LINEAR_OUT_SLOW_IN);
settings.SetTransitionDuration(kNudgeTranformComponentDuration);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
gfx::Transform transform;
transform.Translate(0, kHotseatBaselineNudgeOffset);
layer->SetTransform(transform);
return layer->GetAnimator()->GetTransitionDuration();
};
total_animation_duration +=
animate_initial_transform(hotseat_widget_->GetLayer());
animate_initial_transform(nudge_->GetWidget()->GetLayer());
// Additionally the nudge label should fade in.
{
ui::ScopedLayerAnimationSettings settings(
nudge_->label()->layer()->GetAnimator());
settings.SetTweenType(gfx::Tween::LINEAR);
settings.SetTransitionDuration(kNudgeFadeDuration);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
nudge_->label()->layer()->SetOpacity(1.0f);
}
auto enqueue_loop_transform = [](ui::Layer* layer,
bool up) -> base::TimeDelta {
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetTweenType(gfx::Tween::EASE_IN_OUT_2);
settings.SetTransitionDuration(kNudgeTranformComponentDuration);
// Use enqueue preemption strategy, as the animation is expected to run
// after other previously scheduled animations.
settings.SetPreemptionStrategy(ui::LayerAnimator::ENQUEUE_NEW_ANIMATION);
gfx::Transform transform;
transform.Translate(0, kHotseatBaselineNudgeOffset +
(up ? 0 : 1) * kNudgeShowThrobAmplitude);
layer->SetTransform(transform);
return layer->GetAnimator()->GetTransitionDuration();
};
// Enqueue series of animated up-down transforms on the nudge and the hotseat.
// The final position should match the position after the initial animated
// transform.
for (int i = 0; i < kNudgeShowThrobIterations; ++i) {
total_animation_duration +=
enqueue_loop_transform(hotseat_widget_->GetLayer(), false /*up*/);
enqueue_loop_transform(nudge_->GetWidget()->GetLayer(), false /*up*/);
total_animation_duration +=
enqueue_loop_transform(hotseat_widget_->GetLayer(), true /*up*/);
enqueue_loop_transform(nudge_->GetWidget()->GetLayer(), true /*up*/);
}
PrefService* pref_service =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
base::TimeDelta nudge_duration = contextual_tooltip::GetNudgeTimeout(
pref_service, contextual_tooltip::TooltipType::kHomeToOverview);
contextual_tooltip::HandleNudgeShown(
pref_service, contextual_tooltip::TooltipType::kHomeToOverview);
// If the nudge has a timeout, schedule a task to hide it. The timeout should
// start when the animation sequence finishes.
if (!nudge_duration.is_zero()) {
nudge_hide_timer_.Start(
FROM_HERE, nudge_duration + total_animation_duration,
base::BindOnce(&HomeToOverviewNudgeController::HideNudge,
base::Unretained(this)));
}
}
void HomeToOverviewNudgeController::HideNudge() {
if (!nudge_)
return;
auto animate_hide_transform = [](ui::Layer* layer) {
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetTweenType(gfx::Tween::EASE_OUT_2);
settings.SetTransitionDuration(kNudgeTranformComponentDuration);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
layer->SetTransform(gfx::Transform());
};
animate_hide_transform(hotseat_widget_->GetLayer());
animate_hide_transform(nudge_->GetWidget()->GetLayer());
{
ui::ScopedLayerAnimationSettings settings(
nudge_->label()->layer()->GetAnimator());
settings.SetTweenType(gfx::Tween::LINEAR);
settings.SetTransitionDuration(kNudgeFadeDuration);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
settings.AddObserver(new ObserverToCloseWidget(nudge_->GetWidget()));
nudge_->label()->layer()->SetOpacity(0.0f);
}
widget_observer_.RemoveAll();
nudge_ = nullptr;
}
void HomeToOverviewNudgeController::UpdateNudgeAnchorBounds() {
// Update the nudge anchor bounds - use the hotseat bounds vertical
// coordinates, so the nudge follows the vertical hotseat position, but the
// shelf window horizontal coordinates to center nudge horizontally in the
// shelf widget bounds (the hotseat widget parent).
const gfx::Rect hotseat_bounds =
hotseat_widget_->GetNativeWindow()->GetTargetBounds();
const gfx::Rect shelf_bounds =
hotseat_widget_->GetNativeWindow()->parent()->GetTargetBounds();
nudge_->UpdateAnchorRect(
gfx::Rect(gfx::Point(shelf_bounds.x(), hotseat_bounds.y()),
gfx::Size(shelf_bounds.width(), hotseat_bounds.height())));
}
} // namespace ash
// Copyright 2020 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 ASH_SHELF_HOME_TO_OVERVIEW_NUDGE_CONTROLLER_H_
#define ASH_SHELF_HOME_TO_OVERVIEW_NUDGE_CONTROLLER_H_
#include <memory>
#include "ash/ash_export.h"
#include "base/scoped_observer.h"
#include "base/timer/timer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
namespace ash {
class ContextualNudge;
class HotseatWidget;
class ASH_EXPORT HomeToOverviewNudgeController : views::WidgetObserver {
public:
explicit HomeToOverviewNudgeController(HotseatWidget* hotseat_widget);
HomeToOverviewNudgeController(const HomeToOverviewNudgeController& other) =
delete;
~HomeToOverviewNudgeController() override;
HomeToOverviewNudgeController& operator=(
const HomeToOverviewNudgeController& other) = delete;
// Sets whether the home to overview nudge can be shown for the current shelf
// state. If nudge is allowed, controller may show the nudge (if required). If
// the nudge is not allowed, the nudge will be hidden if currently shown.
void SetNudgeAllowedForCurrentShelf(bool allowed);
// views::WidgetObserver:
void OnWidgetDestroying(views::Widget* widget) override;
void OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) override;
ContextualNudge* nudge_for_testing() { return nudge_; }
bool HasShowTimerForTesting() const;
void FireShowTimerForTesting();
bool HasHideTimerForTesting() const;
void FireHideTimerForTesting();
private:
// Creates and shows the nudge bubble, schedules showing animation for the
// nudge and hotseat widgets, and schedules nudge hide timer as needed.
void ShowNudge();
// Sets up hotseat and nudge wdget animation for hiding the nudge, and closes
// the nudge widget when the animation finishes.
void HideNudge();
// Updates the nudge anchor bounds for the current hotseat and shelf bounds.
void UpdateNudgeAnchorBounds();
bool nudge_allowed_for_shelf_state_ = false;
HotseatWidget* const hotseat_widget_;
ContextualNudge* nudge_ = nullptr;
base::OneShotTimer nudge_show_timer_;
base::OneShotTimer nudge_hide_timer_;
// Observes hotseat widget to detect the hotseat bounds changes, and the
// nudge widget to detect that the widget is being destroyed.
ScopedObserver<views::Widget, views::WidgetObserver> widget_observer_{this};
};
} // namespace ash
#endif // ASH_SHELF_HOME_TO_OVERVIEW_NUDGE_CONTROLLER_H_
// Copyright 2020 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 "ash/shelf/home_to_overview_nudge_controller.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/shelf/contextual_nudge.h"
#include "ash/shelf/contextual_tooltip.h"
#include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/window_state.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/transform.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#include "ui/wm/core/window_util.h"
namespace ash {
class WidgetCloseObserver : public views::WidgetObserver {
public:
WidgetCloseObserver(views::Widget* widget) : widget_(widget) {
if (widget_)
widget_->AddObserver(this);
}
~WidgetCloseObserver() override { CleanupWidget(); }
bool WidgetClosed() const { return !widget_; }
// views::WidgetObserver:
void OnWidgetClosing(views::Widget* widget) override { CleanupWidget(); }
void CleanupWidget() {
if (widget_) {
widget_->RemoveObserver(this);
widget_ = nullptr;
}
}
private:
views::Widget* widget_;
};
class HomeToOverviewNudgeControllerWithNudgesDisabledTest : public AshTestBase {
public:
HomeToOverviewNudgeControllerWithNudgesDisabledTest() {
scoped_feature_list_.InitAndDisableFeature(
ash::features::kContextualNudges);
}
~HomeToOverviewNudgeControllerWithNudgesDisabledTest() override = default;
HomeToOverviewNudgeControllerWithNudgesDisabledTest(
const HomeToOverviewNudgeControllerWithNudgesDisabledTest& other) =
delete;
HomeToOverviewNudgeControllerWithNudgesDisabledTest& operator=(
const HomeToOverviewNudgeControllerWithNudgesDisabledTest& other) =
delete;
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
class HomeToOverviewNudgeControllerTest : public AshTestBase {
public:
HomeToOverviewNudgeControllerTest() {
scoped_feature_list_.InitAndEnableFeature(ash::features::kContextualNudges);
}
~HomeToOverviewNudgeControllerTest() override = default;
HomeToOverviewNudgeControllerTest(
const HomeToOverviewNudgeControllerTest& other) = delete;
HomeToOverviewNudgeControllerTest& operator=(
const HomeToOverviewNudgeControllerTest& other) = delete;
// AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
test_clock_.Advance(base::TimeDelta::FromHours(2));
contextual_tooltip::OverrideClockForTesting(&test_clock_);
}
void TearDown() override {
contextual_tooltip::ClearClockOverrideForTesting();
AshTestBase::TearDown();
}
HomeToOverviewNudgeController* GetNudgeController() {
return GetPrimaryShelf()
->shelf_layout_manager()
->home_to_overview_nudge_controller_for_testing();
}
views::Widget* GetNudgeWidget() {
if (!GetNudgeController()->nudge_for_testing())
return nullptr;
return GetNudgeController()->nudge_for_testing()->GetWidget();
}
HotseatWidget* GetHotseatWidget() {
return GetPrimaryShelf()->shelf_widget()->hotseat_widget();
}
void SanityCheckNudgeBounds() {
views::Widget* const nudge_widget = GetNudgeWidget();
ASSERT_TRUE(nudge_widget);
EXPECT_TRUE(nudge_widget->IsVisible());
gfx::RectF nudge_bounds_f(
nudge_widget->GetNativeWindow()->GetTargetBounds());
nudge_widget->GetLayer()->transform().TransformRect(&nudge_bounds_f);
const gfx::Rect nudge_bounds = gfx::ToEnclosingRect(nudge_bounds_f);
HotseatWidget* const hotseat = GetHotseatWidget();
gfx::RectF hotseat_bounds_f(hotseat->GetNativeWindow()->GetTargetBounds());
hotseat->GetLayer()->transform().TransformRect(&hotseat_bounds_f);
const gfx::Rect hotseat_bounds = gfx::ToEnclosingRect(hotseat_bounds_f);
// Nudge and hotseat should have the same transform.
EXPECT_EQ(hotseat->GetLayer()->transform(),
nudge_widget->GetLayer()->transform());
// Nudge should be under the hotseat.
EXPECT_LE(hotseat_bounds.bottom(), nudge_bounds.y());
const gfx::Rect display_bounds = GetPrimaryDisplay().bounds();
EXPECT_TRUE(display_bounds.Contains(nudge_bounds))
<< display_bounds.ToString() << " contains " << nudge_bounds.ToString();
// Verify that the nudge is centered within the display bounds.
EXPECT_LE((nudge_bounds.x() - display_bounds.x()) -
(display_bounds.right() - nudge_bounds.right()),
1)
<< nudge_bounds.ToString() << " within " << display_bounds.ToString();
// Verify that the nudge label is visible
EXPECT_EQ(
1.0f,
GetNudgeController()->nudge_for_testing()->label()->layer()->opacity());
}
base::SimpleTestClock test_clock_;
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests that home to overview gesture nudge is not shown if contextual nudges
// are disabled.
TEST_F(HomeToOverviewNudgeControllerWithNudgesDisabledTest,
NoNudgeOnHomeScreen) {
EXPECT_FALSE(GetPrimaryShelf()
->shelf_layout_manager()
->home_to_overview_nudge_controller_for_testing());
TabletModeControllerTestApi().EnterTabletMode();
EXPECT_FALSE(GetPrimaryShelf()
->shelf_layout_manager()
->home_to_overview_nudge_controller_for_testing());
}
// Test the flow for showing the home to overview gesture nudge - when shown the
// first time, nudge should remain visible until the hotseat state changes. On
// subsequent shows, the nudge should be hidden after a timeout.
TEST_F(HomeToOverviewNudgeControllerTest, ShownOnHomeScreen) {
// The nudge should not be shown in clamshell.
EXPECT_FALSE(GetNudgeController());
// Entering tablt mode should schedule the nudge to get shown.
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
ASSERT_TRUE(GetNudgeController()->nudge_for_testing());
{
SCOPED_TRACE("First nudge");
SanityCheckNudgeBounds();
}
EXPECT_FALSE(GetNudgeController()->HasHideTimerForTesting());
// Transitioning to overview should hide the nudge.
Shell::Get()->overview_controller()->StartOverview();
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
// Ending overview, and transitioning to the home screen again should not show
// the nudge.
Shell::Get()->overview_controller()->EndOverview();
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
EXPECT_EQ(gfx::Transform(), GetHotseatWidget()->GetLayer()->transform());
// Advance time for more than a day (which should enable the nudge again).
test_clock_.Advance(base::TimeDelta::FromHours(25));
// The nudge should not show up unless the user actually transitions to home.
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
// Create and delete a test window to force a transition to home.
std::unique_ptr<aura::Window> window =
CreateTestWindow(gfx::Rect(0, 0, 400, 400));
wm::ActivateWindow(window.get());
WindowState::Get(window.get())->Minimize();
// Nudge should be shown again.
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
ASSERT_TRUE(GetNudgeController()->nudge_for_testing());
{
SCOPED_TRACE("Second nudge");
SanityCheckNudgeBounds();
}
// The second time, the nudge should be hidden after a timeout.
ASSERT_TRUE(GetNudgeController()->HasHideTimerForTesting());
GetNudgeController()->FireHideTimerForTesting();
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
EXPECT_EQ(gfx::Transform(), GetHotseatWidget()->GetLayer()->transform());
}
// Tests that the nudge eventually stops showing.
TEST_F(HomeToOverviewNudgeControllerTest, ShownLimitedNumberOfTimes) {
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
// Show the nudge kNotificationLimit amount of time.
for (int i = 0; i < contextual_tooltip::kNotificationLimit; ++i) {
SCOPED_TRACE(testing::Message() << "Attempt " << i);
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
ASSERT_TRUE(GetNudgeController()->nudge_for_testing());
std::unique_ptr<aura::Window> window =
CreateTestWindow(gfx::Rect(0, 0, 400, 400));
wm::ActivateWindow(window.get());
test_clock_.Advance(base::TimeDelta::FromHours(25));
WindowState::Get(window.get())->Minimize();
}
// At this point, given the nudge was shown the intended number of times
// already, the nudge should not show up, even though the device is on home
// screen.
EXPECT_FALSE(GetNudgeController()->HasShowTimerForTesting());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
}
// Tests that the nudge is hidden when tablet mode exits.
TEST_F(HomeToOverviewNudgeControllerTest, HiddenOnTabletModeExit) {
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
TabletModeControllerTestApi().LeaveTabletMode();
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
EXPECT_EQ(gfx::Transform(),
GetHotseatWidget()->GetLayer()->GetTargetTransform());
}
// Tests that the nudge show is canceled when tablet mode exits.
TEST_F(HomeToOverviewNudgeControllerTest, ShowCanceledOnTabletModeExit) {
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
TabletModeControllerTestApi().LeaveTabletMode();
EXPECT_FALSE(GetNudgeController()->HasShowTimerForTesting());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
EXPECT_EQ(gfx::Transform(),
GetHotseatWidget()->GetLayer()->GetTargetTransform());
}
// Tests that the nudge show animation is canceled when tablet mode exits.
TEST_F(HomeToOverviewNudgeControllerTest,
ShowAnimationCanceledOnTabletModeExit) {
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
GetNudgeController()->FireShowTimerForTesting();
ASSERT_TRUE(GetNudgeWidget()->GetLayer()->GetAnimator()->is_animating());
TabletModeControllerTestApi().LeaveTabletMode();
EXPECT_FALSE(GetNudgeController()->HasShowTimerForTesting());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
EXPECT_EQ(gfx::Transform(),
GetHotseatWidget()->GetLayer()->GetTargetTransform());
}
// Tests that the nudge is hidden when the screen is locked.
TEST_F(HomeToOverviewNudgeControllerTest, HiddenOnScreenLock) {
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::LOCKED);
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
EXPECT_EQ(gfx::Transform(),
GetHotseatWidget()->GetLayer()->GetTargetTransform());
}
// Tests that the nudge show is canceled if the in-app shelf is shown before the
// show timer runs.
TEST_F(HomeToOverviewNudgeControllerTest, InAppShelfShownBeforeShowTimer) {
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
EXPECT_TRUE(GetNudgeController()->HasShowTimerForTesting());
std::unique_ptr<aura::Window> window =
CreateTestWindow(gfx::Rect(0, 0, 400, 400));
wm::ActivateWindow(window.get());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
EXPECT_FALSE(GetNudgeController()->HasShowTimerForTesting());
// When the home screen is shown the next time, the nudge should be shown
// again, without timeout to hide it.
WindowState::Get(window.get())->Minimize();
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
EXPECT_TRUE(GetNudgeController()->nudge_for_testing());
EXPECT_FALSE(GetNudgeController()->HasHideTimerForTesting());
}
// Tests that in-app shelf will hide the nudge if it happens during the
// animation to show the nudge.
TEST_F(HomeToOverviewNudgeControllerTest, NudgeHiddenDuringShowAnimation) {
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
GetNudgeController()->FireShowTimerForTesting();
ASSERT_TRUE(GetNudgeWidget()->GetLayer()->GetAnimator()->is_animating());
// Cache the widget, as GetNudgeWidget() will start returning nullptr when the
// nudge starts hiding.
ContextualNudge* nudge = GetNudgeController()->nudge_for_testing();
views::Widget* nudge_widget = nudge->GetWidget();
WidgetCloseObserver widget_close_observer(nudge_widget);
std::unique_ptr<aura::Window> window =
CreateTestWindow(gfx::Rect(0, 0, 400, 400));
wm::ActivateWindow(window.get());
EXPECT_FALSE(GetNudgeWidget());
ASSERT_TRUE(nudge_widget->GetLayer()->GetAnimator()->is_animating());
EXPECT_TRUE(nudge_widget->IsVisible());
EXPECT_EQ(gfx::Transform(), nudge_widget->GetLayer()->GetTargetTransform());
EXPECT_EQ(gfx::Transform(),
GetHotseatWidget()->GetLayer()->GetTargetTransform());
EXPECT_FALSE(widget_close_observer.WidgetClosed());
ASSERT_TRUE(nudge->label()->layer()->GetAnimator()->is_animating());
EXPECT_EQ(0.0f, nudge->label()->layer()->GetTargetOpacity());
nudge->label()->layer()->GetAnimator()->StopAnimating();
EXPECT_TRUE(widget_close_observer.WidgetClosed());
EXPECT_TRUE(GetHotseatWidget()->GetLayer()->GetAnimator()->is_animating());
EXPECT_EQ(gfx::Transform(),
GetHotseatWidget()->GetLayer()->GetTargetTransform());
// When the nudge is shown again, it should be hidden after a timeout.
test_clock_.Advance(base::TimeDelta::FromHours(25));
WindowState::Get(window.get())->Minimize();
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
EXPECT_TRUE(GetNudgeController()->nudge_for_testing());
EXPECT_TRUE(GetNudgeController()->HasHideTimerForTesting());
}
// Tests that there is no crash if the nudge widget gets closed unexpectedly.
TEST_F(HomeToOverviewNudgeControllerTest, NoCrashIfNudgeWidgetGetsClosed) {
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
GetNudgeWidget()->CloseNow();
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
std::unique_ptr<aura::Window> window =
CreateTestWindow(gfx::Rect(0, 0, 400, 400));
wm::ActivateWindow(window.get());
EXPECT_FALSE(GetNudgeController()->nudge_for_testing());
}
// Tests that nudge and hotseat get repositioned appropriatelly if the display
// bounds change.
TEST_F(HomeToOverviewNudgeControllerTest,
NudgeBoundsUpdatedOnDisplayBoundsChange) {
UpdateDisplay("768x1200");
TabletModeControllerTestApi().EnterTabletMode();
ASSERT_TRUE(GetNudgeController());
ASSERT_TRUE(GetNudgeController()->HasShowTimerForTesting());
GetNudgeController()->FireShowTimerForTesting();
{
SCOPED_TRACE("Initial bounds");
SanityCheckNudgeBounds();
}
UpdateDisplay("1200x768");
{
SCOPED_TRACE("Updated bounds");
SanityCheckNudgeBounds();
}
}
} // namespace ash
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
#include "ash/screen_util.h" #include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h" #include "ash/session/session_controller_impl.h"
#include "ash/shelf/contextual_tooltip.h" #include "ash/shelf/contextual_tooltip.h"
#include "ash/shelf/home_to_overview_nudge_controller.h"
#include "ash/shelf/hotseat_widget.h" #include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/shelf.h" #include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager_observer.h" #include "ash/shelf/shelf_layout_manager_observer.h"
...@@ -591,6 +592,9 @@ void ShelfLayoutManager::UpdateAutoHideForMouseEvent(ui::MouseEvent* event, ...@@ -591,6 +592,9 @@ void ShelfLayoutManager::UpdateAutoHideForMouseEvent(ui::MouseEvent* event,
} }
void ShelfLayoutManager::UpdateContextualNudges() { void ShelfLayoutManager::UpdateContextualNudges() {
if (!ash::features::AreContextualNudgesEnabled())
return;
const bool in_app_shelf = ShelfConfig::Get()->is_in_app(); const bool in_app_shelf = ShelfConfig::Get()->is_in_app();
const bool in_tablet_mode = Shell::Get()->IsInTabletMode(); const bool in_tablet_mode = Shell::Get()->IsInTabletMode();
...@@ -603,10 +607,29 @@ void ShelfLayoutManager::UpdateContextualNudges() { ...@@ -603,10 +607,29 @@ void ShelfLayoutManager::UpdateContextualNudges() {
} else { } else {
shelf_widget_->HideDragHandleNudge(); shelf_widget_->HideDragHandleNudge();
} }
// Create home to overview nudge controller if home to overview nudge is
// allowed by the current shelf state.
const bool allow_home_to_overview_nudge = in_tablet_mode && !in_app_shelf;
if (allow_home_to_overview_nudge && !home_to_overview_nudge_controller_) {
home_to_overview_nudge_controller_ =
std::make_unique<HomeToOverviewNudgeController>(
shelf_->hotseat_widget());
}
if (home_to_overview_nudge_controller_) {
home_to_overview_nudge_controller_->SetNudgeAllowedForCurrentShelf(
allow_home_to_overview_nudge);
}
} }
void ShelfLayoutManager::HideContextualNudges() { void ShelfLayoutManager::HideContextualNudges() {
if (!ash::features::AreContextualNudgesEnabled())
return;
shelf_widget_->HideDragHandleNudge(); shelf_widget_->HideDragHandleNudge();
if (home_to_overview_nudge_controller_)
home_to_overview_nudge_controller_->SetNudgeAllowedForCurrentShelf(false);
} }
void ShelfLayoutManager::ProcessGestureEventOfAutoHideShelf( void ShelfLayoutManager::ProcessGestureEventOfAutoHideShelf(
......
...@@ -53,6 +53,7 @@ namespace ash { ...@@ -53,6 +53,7 @@ namespace ash {
enum class AnimationChangeType; enum class AnimationChangeType;
class DragWindowFromShelfController; class DragWindowFromShelfController;
class HomeToOverviewNudgeController;
class PanelLayoutManagerTest; class PanelLayoutManagerTest;
class PresentationTimeRecorder; class PresentationTimeRecorder;
class Shelf; class Shelf;
...@@ -286,6 +287,11 @@ class ASH_EXPORT ShelfLayoutManager ...@@ -286,6 +287,11 @@ class ASH_EXPORT ShelfLayoutManager
return window_drag_controller_.get(); return window_drag_controller_.get();
} }
HomeToOverviewNudgeController*
home_to_overview_nudge_controller_for_testing() {
return home_to_overview_nudge_controller_.get();
}
bool IsDraggingApplist() const { bool IsDraggingApplist() const {
return drag_status_ == kDragAppListInProgress; return drag_status_ == kDragAppListInProgress;
} }
...@@ -627,6 +633,9 @@ class ASH_EXPORT ShelfLayoutManager ...@@ -627,6 +633,9 @@ class ASH_EXPORT ShelfLayoutManager
// up from shelf to homescreen, overview or splitview. // up from shelf to homescreen, overview or splitview.
std::unique_ptr<DragWindowFromShelfController> window_drag_controller_; std::unique_ptr<DragWindowFromShelfController> window_drag_controller_;
std::unique_ptr<HomeToOverviewNudgeController>
home_to_overview_nudge_controller_;
// Whether upward fling from shelf should be handled as potential gesture from // Whether upward fling from shelf should be handled as potential gesture from
// overview to home. This is set when the swipe would otherwise be handled by // overview to home. This is set when the swipe would otherwise be handled by
// |window_drag_controller_|, but the swipe cannot be associated with a window // |window_drag_controller_|, but the swipe cannot be associated with a window
......
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