blob: 6a6940b17dfaf9936587cee371f4625622432a1f [file] [log] [blame]
// 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/drag_handle.h"
#include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/contextual_tooltip.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shelf/shelf_observer.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/overview/overview_controller.h"
#include "base/bind.h"
#include "base/timer/timer.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
// Vertical padding to make the drag handle easier to tap.
constexpr int kVerticalClickboxPadding = 15;
// Drag handle translation distance for the first part of nudge animation.
constexpr int kInAppToHomeNudgeVerticalMarginRise = -4;
// Drag handle translation distance for the second part of nudge animation.
constexpr int kInAppToHomeVerticalMarginDrop = 10;
// Drag handle contextual nudge text box translation distance for the nudge
// animation at the end.
constexpr int kInAppToHomeNudgeVerticalMarginDrop = 8;
// Animation time for each translation of drag handle to show contextual nudge.
constexpr base::TimeDelta kInAppToHomeAnimationTime =
base::TimeDelta::FromMilliseconds(300);
// Animation time to return drag handle to original position after hiding
// contextual nudge.
constexpr base::TimeDelta kInAppToHomeHideAnimationDuration =
base::TimeDelta::FromMilliseconds(600);
// Animation time to return drag handle to original position after the user taps
// to hide the contextual nudge.
constexpr base::TimeDelta kInAppToHomeHideOnTapAnimationDuration =
base::TimeDelta::FromMilliseconds(100);
// Delay between animating drag handle and tooltip opacity.
constexpr base::TimeDelta kInAppToHomeNudgeOpacityDelay =
base::TimeDelta::FromMilliseconds(500);
// Fade in time for drag handle nudge tooltip.
constexpr base::TimeDelta kInAppToHomeNudgeOpacityAnimationDuration =
base::TimeDelta::FromMilliseconds(200);
// Delay before animating the drag handle and showing the drag handle nudge.
constexpr base::TimeDelta kShowNudgeDelay = base::TimeDelta::FromSeconds(2);
// This class is deleted after OnImplicitAnimationsCompleted() is called.
class HideNudgeObserver : public ui::ImplicitAnimationObserver {
public:
HideNudgeObserver(ContextualNudge* drag_handle_nudge)
: drag_handle_nudge_(drag_handle_nudge) {}
~HideNudgeObserver() override = default;
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
drag_handle_nudge_->GetWidget()->CloseWithReason(
views::Widget::ClosedReason::kUnspecified);
delete this;
}
private:
ContextualNudge* drag_handle_nudge_;
};
} // namespace
DragHandle::DragHandle(int drag_handle_corner_radius, Shelf* shelf)
: views::Button(base::BindRepeating(&DragHandle::ButtonPressed,
base::Unretained(this))),
shelf_(shelf) {
SetPaintToLayer(ui::LAYER_SOLID_COLOR);
layer()->SetRoundedCornerRadius(
{drag_handle_corner_radius, drag_handle_corner_radius,
drag_handle_corner_radius, drag_handle_corner_radius});
SetSize(ShelfConfig::Get()->DragHandleSize());
SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
shell_observer_.Add(Shell::Get());
Shell::Get()->accessibility_controller()->AddObserver(this);
shelf_->AddObserver(this);
OnAccessibilityStatusChanged();
}
DragHandle::~DragHandle() {
StopObservingImplicitAnimations();
Shell::Get()->accessibility_controller()->RemoveObserver(this);
shelf_->RemoveObserver(this);
}
bool DragHandle::DoesIntersectRect(const views::View* target,
const gfx::Rect& rect) const {
DCHECK_EQ(target, this);
gfx::Rect drag_handle_bounds = target->GetLocalBounds();
drag_handle_bounds.set_y(drag_handle_bounds.y() - kVerticalClickboxPadding);
drag_handle_bounds.set_height(drag_handle_bounds.height() +
2 * kVerticalClickboxPadding);
return drag_handle_bounds.Intersects(rect);
}
bool DragHandle::MaybeShowDragHandleNudge() {
// Stop observing overview state if nudge show timer has fired.
if (!show_drag_handle_nudge_timer_.IsRunning())
overview_observer_.RemoveAll();
if (!features::AreContextualNudgesEnabled())
return false;
// Do not show drag handle nudge if it is already shown or drag handle is not
// visible.
if (gesture_nudge_target_visibility() ||
window_drag_from_shelf_in_progress_ || !GetVisible() ||
SplitViewController::Get(shelf_->shelf_widget()->GetNativeWindow())
->InSplitViewMode()) {
return false;
}
show_nudge_animation_in_progress_ = true;
auto_hide_lock_ = std::make_unique<Shelf::ScopedAutoHideLock>(shelf_);
StopDragHandleNudgeShowTimer();
ShowDragHandleNudge();
return true;
}
void DragHandle::ShowDragHandleNudge() {
DCHECK(!gesture_nudge_target_visibility_);
PrefService* pref =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
base::TimeDelta nudge_duration = contextual_tooltip::GetNudgeTimeout(
pref, contextual_tooltip::TooltipType::kInAppToHome);
AnimateDragHandleShow();
ShowDragHandleTooltip();
gesture_nudge_target_visibility_ = true;
split_view_observer_.Add(
SplitViewController::Get(shelf_->shelf_widget()->GetNativeWindow()));
if (!nudge_duration.is_zero()) {
hide_drag_handle_nudge_timer_.Start(
FROM_HERE, nudge_duration,
base::BindOnce(&DragHandle::HideDragHandleNudge, base::Unretained(this),
contextual_tooltip::DismissNudgeReason::kTimeout));
}
contextual_tooltip::HandleNudgeShown(
pref, contextual_tooltip::TooltipType::kInAppToHome);
}
void DragHandle::ScheduleShowDragHandleNudge() {
if (gesture_nudge_target_visibility_ ||
show_drag_handle_nudge_timer_.IsRunning() ||
window_drag_from_shelf_in_progress_ ||
Shell::Get()->overview_controller()->InOverviewSession()) {
return;
}
// Observe overview controller to detect overview session start - this should
// cancel the scheduled nudge show.
overview_observer_.Add(Shell::Get()->overview_controller());
show_drag_handle_nudge_timer_.Start(
FROM_HERE, kShowNudgeDelay,
base::BindOnce(base::IgnoreResult(&DragHandle::MaybeShowDragHandleNudge),
base::Unretained(this)));
}
void DragHandle::HideDragHandleNudge(
contextual_tooltip::DismissNudgeReason reason) {
StopDragHandleNudgeShowTimer();
if (!gesture_nudge_target_visibility())
return;
split_view_observer_.RemoveAll();
hide_drag_handle_nudge_timer_.Stop();
if (reason == contextual_tooltip::DismissNudgeReason::kPerformedGesture) {
contextual_tooltip::HandleGesturePerformed(
Shell::Get()->session_controller()->GetLastActiveUserPrefService(),
contextual_tooltip::TooltipType::kInAppToHome);
} else {
// HandleGesturePerformed will also call MaybeLogNudgeDismissedMetrics so we
// do not need to call it separately for kPerformedGesture.
contextual_tooltip::MaybeLogNudgeDismissedMetrics(
contextual_tooltip::TooltipType::kInAppToHome, reason);
}
HideDragHandleNudgeHelper(/*hidden_by_tap=*/reason ==
contextual_tooltip::DismissNudgeReason::kTap);
gesture_nudge_target_visibility_ = false;
}
void DragHandle::SetWindowDragFromShelfInProgress(bool gesture_in_progress) {
if (window_drag_from_shelf_in_progress_ == gesture_in_progress)
return;
window_drag_from_shelf_in_progress_ = gesture_in_progress;
// If the contextual nudge is not yet shown, make sure that any scheduled
// nudge show request is canceled.
if (!gesture_nudge_target_visibility()) {
StopDragHandleNudgeShowTimer();
return;
}
// If the drag handle nudge is shown when the gesture to home or overview
// starts, keep it around until the gesture completes.
if (window_drag_from_shelf_in_progress_) {
hide_drag_handle_nudge_timer_.Stop();
} else {
HideDragHandleNudge(
contextual_tooltip::DismissNudgeReason::kPerformedGesture);
}
}
void DragHandle::UpdateColor() {
layer()->SetColor(AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kShelfHandleColor));
}
void DragHandle::OnGestureEvent(ui::GestureEvent* event) {
if (!features::AreContextualNudgesEnabled() ||
!gesture_nudge_target_visibility_) {
return;
}
if (event->type() == ui::ET_GESTURE_TAP) {
HandleTapOnNudge();
event->StopPropagation();
}
}
gfx::Rect DragHandle::GetAnchorBoundsInScreen() const {
gfx::Rect anchor_bounds = ConvertRectToWidget(GetLocalBounds());
// Ignore any transform set on the drag handle - drag handle is used as an
// anchor for contextual nudges, and their bounds are set relative to the
// handle bounds without transform (for example, for in-app to home nudge both
// drag handle and the nudge will have non-indentity, identical transforms).
gfx::Point origin_in_screen = anchor_bounds.origin();
layer()->transform().TransformPointReverse(&origin_in_screen);
// If the parent widget has a transform set, it should be ignored as well (the
// transform is set during shelf widget animations, and will animate to
// identity transform), so the nudge bounds are set relative to the target
// shelf bounds.
aura::Window* const widget_window = GetWidget()->GetNativeWindow();
origin_in_screen += widget_window->bounds().origin().OffsetFromOrigin();
wm::ConvertPointToScreen(widget_window->parent(), &origin_in_screen);
anchor_bounds.set_origin(origin_in_screen);
return anchor_bounds;
}
void DragHandle::GetAccessibleNodeData(ui::AXNodeData* node_data) {
Button::GetAccessibleNodeData(node_data);
base::string16 accessible_name = base::string16();
switch (shelf_->shelf_layout_manager()->hotseat_state()) {
case HotseatState::kNone:
case HotseatState::kShownClamshell:
case HotseatState::kShownHomeLauncher:
break;
case HotseatState::kHidden:
accessible_name = l10n_util::GetStringUTF16(
IDS_ASH_DRAG_HANDLE_HOTSEAT_SHOW_ACCESSIBLE_NAME);
break;
case HotseatState::kExtended:
// The name should be empty when the hotseat is extended but we cannot
// hide it.
if (force_show_hotseat_resetter_)
accessible_name = l10n_util::GetStringUTF16(
IDS_ASH_DRAG_HANDLE_HOTSEAT_HIDE_ACCESSIBLE_NAME);
break;
}
node_data->SetName(accessible_name);
}
void DragHandle::OnOverviewModeStarting() {
StopDragHandleNudgeShowTimer();
}
void DragHandle::OnShellDestroying() {
shell_observer_.RemoveAll();
// Removes the overview controller observer.
StopDragHandleNudgeShowTimer();
hide_drag_handle_nudge_timer_.Stop();
}
void DragHandle::OnSplitViewStateChanged(
SplitViewController::State previous_state,
SplitViewController::State state) {
if (SplitViewController::Get(shelf_->shelf_widget()->GetNativeWindow())
->InSplitViewMode()) {
HideDragHandleNudge(contextual_tooltip::DismissNudgeReason::kOther);
}
}
void DragHandle::OnHotseatStateChanged(HotseatState old_state,
HotseatState new_state) {
// Reset |force_show_hotseat_resetter_| when it is no longer extended.
if (force_show_hotseat_resetter_ && new_state != HotseatState::kExtended) {
shelf_->hotseat_widget()->set_manually_extended(false);
force_show_hotseat_resetter_.RunAndReset();
}
}
void DragHandle::OnAccessibilityStatusChanged() {
// Only enable the button if shelf controls are shown for accessibility.
views::View::SetEnabled(
ShelfConfig::Get()->ShelfControlsForcedShownForAccessibility());
}
void DragHandle::ButtonPressed() {
if (shelf_->shelf_layout_manager()->hotseat_state() ==
HotseatState::kHidden) {
force_show_hotseat_resetter_ =
shelf_->shelf_widget()->ForceShowHotseatInTabletMode();
} else if (force_show_hotseat_resetter_) {
// Hide hotseat only if it's been brought up by tapping the drag handle.
shelf_->hotseat_widget()->set_manually_extended(false);
force_show_hotseat_resetter_.RunAndReset();
}
}
void DragHandle::OnImplicitAnimationsCompleted() {
show_nudge_animation_in_progress_ = false;
auto_hide_lock_.reset();
}
void DragHandle::ShowDragHandleTooltip() {
DCHECK(!drag_handle_nudge_);
drag_handle_nudge_ = new ContextualNudge(
this, nullptr /*parent_window*/, ContextualNudge::Position::kTop,
gfx::Insets(), l10n_util::GetStringUTF16(IDS_ASH_DRAG_HANDLE_NUDGE),
AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kTextColorPrimary),
base::BindRepeating(&DragHandle::HandleTapOnNudge,
weak_factory_.GetWeakPtr()));
drag_handle_nudge_->GetWidget()->Show();
drag_handle_nudge_->label()->layer()->SetOpacity(0.0f);
{
// Layer transform should be animated after a delay so the animator must
// first schedules a pause for transform animation.
ui::LayerAnimator* transform_animator =
drag_handle_nudge_->GetWidget()->GetLayer()->GetAnimator();
transform_animator->SchedulePauseForProperties(
kInAppToHomeAnimationTime, ui::LayerAnimationElement::TRANSFORM);
// Enqueue transform animation to start after pause.
ui::ScopedLayerAnimationSettings transform_animation_settings(
transform_animator);
transform_animation_settings.SetTweenType(gfx::Tween::FAST_OUT_LINEAR_IN);
transform_animation_settings.SetTransitionDuration(
kInAppToHomeAnimationTime);
transform_animation_settings.SetPreemptionStrategy(
ui::LayerAnimator::ENQUEUE_NEW_ANIMATION);
// gfx::Transform translate;
gfx::Transform translate;
translate.Translate(0, kInAppToHomeNudgeVerticalMarginDrop);
drag_handle_nudge_->GetWidget()->GetLayer()->SetTransform(translate);
}
{
// Layer opacity should be animated after a delay so the animator must first
// schedules a pause for opacity animation.
ui::LayerAnimator* opacity_animator =
drag_handle_nudge_->label()->layer()->GetAnimator();
opacity_animator->SchedulePauseForProperties(
kInAppToHomeNudgeOpacityDelay, ui::LayerAnimationElement::OPACITY);
// Enqueue opacity animation to start after pause.
ui::ScopedLayerAnimationSettings opacity_animation_settings(
opacity_animator);
opacity_animation_settings.SetPreemptionStrategy(
ui::LayerAnimator::ENQUEUE_NEW_ANIMATION);
opacity_animation_settings.SetTweenType(gfx::Tween::LINEAR);
opacity_animation_settings.SetTransitionDuration(
kInAppToHomeNudgeOpacityAnimationDuration);
opacity_animation_settings.AddObserver(this);
drag_handle_nudge_->label()->layer()->SetOpacity(1.0f);
}
}
void DragHandle::HideDragHandleNudgeHelper(bool hidden_by_tap) {
ScheduleDragHandleTranslationAnimation(
0,
hidden_by_tap ? kInAppToHomeHideOnTapAnimationDuration
: kInAppToHomeHideAnimationDuration,
hidden_by_tap ? gfx::Tween::FAST_OUT_LINEAR_IN
: gfx::Tween::FAST_OUT_SLOW_IN,
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
if (drag_handle_nudge_) {
ui::LayerAnimator* opacity_animator =
drag_handle_nudge_->label()->layer()->GetAnimator();
ui::ScopedLayerAnimationSettings opacity_animation_settings(
opacity_animator);
opacity_animation_settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
opacity_animation_settings.SetTweenType(gfx::Tween::LINEAR);
opacity_animation_settings.SetTransitionDuration(
hidden_by_tap ? kInAppToHomeHideOnTapAnimationDuration
: kInAppToHomeNudgeOpacityAnimationDuration);
// Register an animation observer to close the tooltip widget once the label
// opacity is animated to 0 as the widget will no longer be needed after
// this point.
opacity_animation_settings.AddObserver(
new HideNudgeObserver(drag_handle_nudge_));
drag_handle_nudge_->label()->layer()->SetOpacity(0.0f);
drag_handle_nudge_ = nullptr;
}
}
void DragHandle::AnimateDragHandleShow() {
// Drag handle is animated in two steps that run in sequence. The first step
// uses |IMMEDIATELY_ANIMATE_TO_NEW_TARGET| to preempt any in-progress
// animations while the second step uses |ENQUEUE_NEW_ANIMATION| so it runs
// after the first animation.
ScheduleDragHandleTranslationAnimation(
kInAppToHomeNudgeVerticalMarginRise, kInAppToHomeAnimationTime,
gfx::Tween::FAST_OUT_SLOW_IN,
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
ScheduleDragHandleTranslationAnimation(
kInAppToHomeVerticalMarginDrop, kInAppToHomeAnimationTime,
gfx::Tween::FAST_OUT_LINEAR_IN, ui::LayerAnimator::ENQUEUE_NEW_ANIMATION);
}
void DragHandle::ScheduleDragHandleTranslationAnimation(
int vertical_offset,
base::TimeDelta animation_time,
gfx::Tween::Type tween_type,
ui::LayerAnimator::PreemptionStrategy strategy) {
ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
animation.SetTweenType(tween_type);
animation.SetTransitionDuration(animation_time);
animation.SetPreemptionStrategy(strategy);
gfx::Transform translate;
translate.Translate(0, vertical_offset);
SetTransform(translate);
}
void DragHandle::HandleTapOnNudge() {
if (!drag_handle_nudge_)
return;
HideDragHandleNudge(contextual_tooltip::DismissNudgeReason::kTap);
}
void DragHandle::StopDragHandleNudgeShowTimer() {
show_drag_handle_nudge_timer_.Stop();
overview_observer_.RemoveAll();
}
} // namespace ash