blob: 293331597518426f2b9a5d8e3e2fe36021594132 [file] [log] [blame]
// Copyright 2017 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/wm/splitview/split_view_utils.h"
#include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/toast_data.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/toast/toast_manager_impl.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_state.h"
#include "base/command_line.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/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/transient_window_manager.h"
namespace ash {
namespace {
using ::chromeos::WindowStateType;
// The animation speed at which the highlights fade in or out.
constexpr base::TimeDelta kHighlightsFadeInOut = base::Milliseconds(250);
// The animation speed which the other highlight fades in or out.
constexpr base::TimeDelta kOtherFadeInOut = base::Milliseconds(133);
// The delay before the other highlight starts fading in.
constexpr base::TimeDelta kOtherFadeInDelay = base::Milliseconds(117);
// The animation speed at which the preview area fades out (when you snap a
// window).
constexpr base::TimeDelta kPreviewAreaFadeOut = base::Milliseconds(67);
// The time duration for the indicator label opacity animations.
constexpr base::TimeDelta kLabelAnimation = base::Milliseconds(83);
// The delay before the indicator labels start fading in.
constexpr base::TimeDelta kLabelAnimationDelay = base::Milliseconds(167);
// Toast data.
constexpr char kAppCannotSnapToastId[] = "split_view_app_cannot_snap";
constexpr int kAppCannotSnapToastDurationMs = 2500;
// Gets the duration, tween type and delay before animation based on |type|.
void GetAnimationValuesForType(
SplitviewAnimationType type,
base::TimeDelta* out_duration,
gfx::Tween::Type* out_tween_type,
ui::LayerAnimator::PreemptionStrategy* out_preemption_strategy,
base::TimeDelta* out_delay) {
*out_preemption_strategy = ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET;
switch (type) {
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN:
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN_CANNOT_SNAP:
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_IN:
case SPLITVIEW_ANIMATION_OVERVIEW_ITEM_FADE_IN:
case SPLITVIEW_ANIMATION_OVERVIEW_ITEM_FADE_OUT:
case SPLITVIEW_ANIMATION_TEXT_FADE_IN_WITH_HIGHLIGHT:
case SPLITVIEW_ANIMATION_TEXT_FADE_OUT_WITH_HIGHLIGHT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_SLIDE_IN:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_SLIDE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_TEXT_SLIDE_IN:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_TEXT_SLIDE_OUT:
*out_duration = kHighlightsFadeInOut;
*out_tween_type = gfx::Tween::FAST_OUT_SLOW_IN;
return;
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN_CANNOT_SNAP:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_SLIDE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_TEXT_SLIDE_IN:
*out_delay = kOtherFadeInDelay;
*out_duration = kOtherFadeInOut;
*out_tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN;
*out_preemption_strategy = ui::LayerAnimator::ENQUEUE_NEW_ANIMATION;
return;
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_OUT:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_SLIDE_OUT:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_TEXT_SLIDE_OUT:
*out_duration = kOtherFadeInOut;
*out_tween_type = gfx::Tween::FAST_OUT_LINEAR_IN;
return;
case SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_NIX_INSET:
*out_duration = kPreviewAreaFadeOut;
*out_tween_type = gfx::Tween::FAST_OUT_LINEAR_IN;
return;
case SPLITVIEW_ANIMATION_TEXT_FADE_IN:
*out_delay = kLabelAnimationDelay;
*out_duration = kLabelAnimation;
*out_tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN;
*out_preemption_strategy = ui::LayerAnimator::ENQUEUE_NEW_ANIMATION;
return;
case SPLITVIEW_ANIMATION_TEXT_FADE_OUT:
*out_duration = kLabelAnimation;
*out_tween_type = gfx::Tween::FAST_OUT_LINEAR_IN;
return;
case SPLITVIEW_ANIMATION_SET_WINDOW_TRANSFORM:
*out_duration = kSplitviewWindowTransformDuration;
*out_tween_type = gfx::Tween::FAST_OUT_SLOW_IN;
*out_preemption_strategy =
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET;
return;
}
NOTREACHED();
}
// Helper function to apply animation values to |settings|.
void ApplyAnimationSettings(
ui::ScopedLayerAnimationSettings* settings,
ui::LayerAnimator* animator,
ui::LayerAnimationElement::AnimatableProperties animated_property,
base::TimeDelta duration,
gfx::Tween::Type tween,
ui::LayerAnimator::PreemptionStrategy preemption_strategy,
base::TimeDelta delay) {
DCHECK_EQ(settings->GetAnimator(), animator);
settings->SetTransitionDuration(duration);
settings->SetTweenType(tween);
settings->SetPreemptionStrategy(preemption_strategy);
if (!delay.is_zero())
animator->SchedulePauseForProperties(delay, animated_property);
}
// Returns BubbleDialogDelegateView if |transient_window| is a bubble dialog.
views::BubbleDialogDelegate* AsBubbleDialogDelegate(
aura::Window* transient_window) {
views::Widget* widget =
views::Widget::GetWidgetForNativeWindow(transient_window);
if (!widget || !widget->widget_delegate())
return nullptr;
return widget->widget_delegate()->AsBubbleDialogDelegate();
}
} // namespace
WindowTransformAnimationObserver::WindowTransformAnimationObserver(
aura::Window* window)
: window_(window) {
window_->AddObserver(this);
}
WindowTransformAnimationObserver::~WindowTransformAnimationObserver() {
if (window_)
window_->RemoveObserver(this);
}
void WindowTransformAnimationObserver::OnImplicitAnimationsCompleted() {
// After window transform animation is done and if the window's transform is
// set to identity transform, force to relayout all its transient bubble
// dialogs.
if (!window_->layer()->GetTargetTransform().IsIdentity()) {
delete this;
return;
}
for (auto* transient_window :
::wm::TransientWindowManager::GetOrCreate(window_)
->transient_children()) {
// For now we only care about bubble dialog type transient children.
views::BubbleDialogDelegate* bubble_delegate_view =
AsBubbleDialogDelegate(transient_window);
if (bubble_delegate_view)
bubble_delegate_view->OnAnchorBoundsChanged();
}
delete this;
}
void WindowTransformAnimationObserver::OnWindowDestroying(
aura::Window* window) {
delete this;
}
void DoSplitviewOpacityAnimation(ui::Layer* layer,
SplitviewAnimationType type) {
float target_opacity = 0.f;
switch (type) {
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_OUT:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_OUT:
case SPLITVIEW_ANIMATION_OVERVIEW_ITEM_FADE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_OUT:
case SPLITVIEW_ANIMATION_TEXT_FADE_OUT:
case SPLITVIEW_ANIMATION_TEXT_FADE_OUT_WITH_HIGHLIGHT:
target_opacity = 0.f;
break;
case SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_IN:
target_opacity = features::IsDarkLightModeEnabled()
? kDarkLightPreviewAreaHighlightOpacity
: kPreviewAreaHighlightOpacity;
break;
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN:
target_opacity = features::IsDarkLightModeEnabled()
? kDarkLightHighlightOpacity
: kHighlightOpacity;
break;
case SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN_CANNOT_SNAP:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN_CANNOT_SNAP:
target_opacity = features::IsDarkLightModeEnabled()
? kDarkLightHighlightCannotSnapOpacity
: kHighlightOpacity;
break;
case SPLITVIEW_ANIMATION_OVERVIEW_ITEM_FADE_IN:
case SPLITVIEW_ANIMATION_TEXT_FADE_IN:
case SPLITVIEW_ANIMATION_TEXT_FADE_IN_WITH_HIGHLIGHT:
target_opacity = 1.f;
break;
default:
NOTREACHED() << "Not a valid split view opacity animation type.";
return;
}
if (layer->GetTargetOpacity() == target_opacity)
return;
base::TimeDelta duration;
gfx::Tween::Type tween;
ui::LayerAnimator::PreemptionStrategy preemption_strategy;
base::TimeDelta delay;
GetAnimationValuesForType(type, &duration, &tween, &preemption_strategy,
&delay);
ui::LayerAnimator* animator = layer->GetAnimator();
ui::ScopedLayerAnimationSettings settings(animator);
ApplyAnimationSettings(&settings, animator,
ui::LayerAnimationElement::OPACITY, duration, tween,
preemption_strategy, delay);
layer->SetOpacity(target_opacity);
}
void DoSplitviewTransformAnimation(
ui::Layer* layer,
SplitviewAnimationType type,
const gfx::Transform& target_transform,
std::unique_ptr<ui::ImplicitAnimationObserver> animation_observer) {
if (layer->GetTargetTransform() == target_transform)
return;
switch (type) {
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_TEXT_SLIDE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_TEXT_SLIDE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_NIX_INSET:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_TEXT_SLIDE_IN:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_TEXT_SLIDE_OUT:
case SPLITVIEW_ANIMATION_SET_WINDOW_TRANSFORM:
break;
default:
NOTREACHED() << "Not a valid split view transform type.";
return;
}
base::TimeDelta duration;
gfx::Tween::Type tween;
ui::LayerAnimator::PreemptionStrategy preemption_strategy;
base::TimeDelta delay;
GetAnimationValuesForType(type, &duration, &tween, &preemption_strategy,
&delay);
ui::LayerAnimator* animator = layer->GetAnimator();
ui::ScopedLayerAnimationSettings settings(animator);
if (animation_observer.get())
settings.AddObserver(animation_observer.release());
ApplyAnimationSettings(&settings, animator,
ui::LayerAnimationElement::TRANSFORM, duration, tween,
preemption_strategy, delay);
layer->SetTransform(target_transform);
}
void DoSplitviewClipRectAnimation(
ui::Layer* layer,
SplitviewAnimationType type,
const gfx::Rect& target_clip_rect,
std::unique_ptr<ui::ImplicitAnimationObserver> animation_observer) {
ui::LayerAnimator* animator = layer->GetAnimator();
if (animator->GetTargetClipRect() == target_clip_rect)
return;
switch (type) {
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_SLIDE_IN:
case SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_SLIDE_OUT:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_NIX_INSET:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_SLIDE_IN:
case SPLITVIEW_ANIMATION_PREVIEW_AREA_SLIDE_OUT:
break;
default:
NOTREACHED() << "Not a valid split view clip rect type.";
return;
}
base::TimeDelta duration;
gfx::Tween::Type tween;
ui::LayerAnimator::PreemptionStrategy preemption_strategy;
base::TimeDelta delay;
GetAnimationValuesForType(type, &duration, &tween, &preemption_strategy,
&delay);
ui::ScopedLayerAnimationSettings settings(animator);
if (animation_observer.get())
settings.AddObserver(animation_observer.release());
ApplyAnimationSettings(&settings, animator, ui::LayerAnimationElement::CLIP,
duration, tween, preemption_strategy, delay);
layer->SetClipRect(target_clip_rect);
}
void MaybeRestoreSplitView(bool refresh_snapped_windows) {
if (!ShouldAllowSplitView() ||
!Shell::Get()->tablet_mode_controller()->InTabletMode()) {
return;
}
// Search for snapped windows to detect if the now active user session, or
// desk were in split view. In case multiple windows were snapped to one side,
// one window after another, there may be multiple windows in a LEFT_SNAPPED
// state or multiple windows in a RIGHT_SNAPPED state. For each of those two
// state types that belongs to multiple windows, the relevant window will be
// listed first among those windows, and a null check in the loop body below
// will filter out the rest of them.
// TODO(amusbach): The windows that were in split view may have later been
// destroyed or changed to non-snapped states. Then the following for loop
// could snap windows that were not in split view. Also, a window may have
// become full screen, and if so, then it would be better not to reactivate
// split view. See https://crbug.com/944134.
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
if (refresh_snapped_windows) {
const MruWindowTracker::WindowList windows =
Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
kActiveDesk);
for (aura::Window* window : windows) {
if (!split_view_controller->CanSnapWindow(window)) {
// Since we are in tablet mode, and this window is not snappable, we
// should maximize it.
WindowState::Get(window)->Maximize();
continue;
}
switch (WindowState::Get(window)->GetStateType()) {
case WindowStateType::kPrimarySnapped:
if (!split_view_controller->left_window()) {
split_view_controller->SnapWindow(window,
SplitViewController::LEFT);
}
break;
case WindowStateType::kSecondarySnapped:
if (!split_view_controller->right_window()) {
split_view_controller->SnapWindow(window,
SplitViewController::RIGHT);
}
break;
default:
break;
}
if (split_view_controller->state() ==
SplitViewController::State::kBothSnapped)
break;
}
}
// Ensure that overview mode is active if and only if there is a window
// snapped to one side but no window snapped to the other side.
OverviewController* overview_controller = Shell::Get()->overview_controller();
SplitViewController::State state = split_view_controller->state();
if (state == SplitViewController::State::kLeftSnapped ||
state == SplitViewController::State::kRightSnapped) {
overview_controller->StartOverview(OverviewStartAction::kSplitView);
} else {
overview_controller->EndOverview(OverviewEndAction::kSplitView);
}
}
bool ShouldAllowSplitView() {
// Don't allow split view if we're in pinned mode.
if (Shell::Get()->screen_pinning_controller()->IsPinned())
return false;
// TODO(crubg.com/853588): Disallow window dragging and split screen while
// ChromeVox is on until they are in a usable state.
if (Shell::Get()->accessibility_controller()->spoken_feedback().enabled())
return false;
return true;
}
void ShowAppCannotSnapToast() {
Shell::Get()->toast_manager()->Show(ToastData(
kAppCannotSnapToastId,
l10n_util::GetStringUTF16(IDS_ASH_SPLIT_VIEW_CANNOT_SNAP),
kAppCannotSnapToastDurationMs, absl::optional<std::u16string>()));
}
SplitViewController::SnapPosition GetSnapPositionForLocation(
aura::Window* root_window,
const gfx::Point& location_in_screen,
const absl::optional<gfx::Point>& initial_location_in_screen,
int snap_distance_from_edge,
int minimum_drag_distance,
int horizontal_edge_inset,
int vertical_edge_inset) {
if (!ShouldAllowSplitView())
return SplitViewController::NONE;
const bool horizontal = SplitViewController::IsLayoutHorizontal(root_window);
const bool right_side_up = SplitViewController::IsLayoutPrimary(root_window);
// Check to see if the current event location |location_in_screen| is within
// the drag indicators bounds.
const gfx::Rect work_area(
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window));
SplitViewController::SnapPosition snap_position = SplitViewController::NONE;
if (horizontal) {
gfx::Rect area(work_area);
area.Inset(horizontal_edge_inset, 0);
if (location_in_screen.x() <= area.x()) {
snap_position = right_side_up ? SplitViewController::LEFT
: SplitViewController::RIGHT;
} else if (location_in_screen.x() >= area.right() - 1) {
snap_position = right_side_up ? SplitViewController::RIGHT
: SplitViewController::LEFT;
}
} else {
gfx::Rect area(work_area);
area.Inset(0, vertical_edge_inset);
if (location_in_screen.y() <= area.y()) {
snap_position = right_side_up ? SplitViewController::LEFT
: SplitViewController::RIGHT;
} else if (location_in_screen.y() >= area.bottom() - 1) {
snap_position = right_side_up ? SplitViewController::RIGHT
: SplitViewController::LEFT;
}
}
if (snap_position == SplitViewController::NONE)
return snap_position;
// To avoid accidental snap, the window needs to be dragged inside
// |snap_distance_from_edge| from edge or dragged toward the edge for at least
// |minimum_drag_distance| until it's dragged into |horizontal_edge_inset| or
// |vertical_edge_inset| region.
// The window should always be snapped if inside |snap_distance_from_edge|
// from edge.
bool drag_end_near_edge = false;
gfx::Rect area(work_area);
area.Inset(snap_distance_from_edge, snap_distance_from_edge);
if (horizontal ? location_in_screen.x() < area.x() ||
location_in_screen.x() > area.right()
: location_in_screen.y() < area.y() ||
location_in_screen.y() > area.bottom()) {
drag_end_near_edge = true;
}
if (!drag_end_near_edge && initial_location_in_screen) {
// Check how far the window has been dragged.
const auto distance = location_in_screen - *initial_location_in_screen;
const int primary_axis_distance = horizontal ? distance.x() : distance.y();
const bool is_left_or_top =
SplitViewController::IsPhysicalLeftOrTop(snap_position, root_window);
if ((is_left_or_top && primary_axis_distance > -minimum_drag_distance) ||
(!is_left_or_top && primary_axis_distance < minimum_drag_distance)) {
snap_position = SplitViewController::NONE;
}
}
return snap_position;
}
SplitViewController::SnapPosition GetSnapPosition(
aura::Window* root_window,
aura::Window* window,
const gfx::Point& location_in_screen,
const gfx::Point& initial_location_in_screen,
int snap_distance_from_edge,
int minimum_drag_distance,
int horizontal_edge_inset,
int vertical_edge_inset) {
if (!SplitViewController::Get(root_window)->CanSnapWindow(window)) {
return SplitViewController::NONE;
}
absl::optional<gfx::Point> initial_location_in_current_screen = absl::nullopt;
if (window->GetRootWindow() == root_window)
initial_location_in_current_screen = initial_location_in_screen;
return GetSnapPositionForLocation(
root_window, location_in_screen, initial_location_in_current_screen,
snap_distance_from_edge, minimum_drag_distance, horizontal_edge_inset,
vertical_edge_inset);
}
} // namespace ash