blob: 60b1f7aa42d5184fbbc84300915d6275d81cdfd5 [file] [log] [blame]
// Copyright 2014 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/window_cycle_list.h"
#include <algorithm>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/window_mini_view.h"
#include "ash/wm/window_preview_view.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/ranges.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/scoped_window_targeter.h"
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#include "ui/compositor/animation_metrics_reporter.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/painter.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/visibility_controller.h"
#include "ui/wm/core/window_animations.h"
namespace ash {
namespace {
bool g_disable_initial_delay = false;
// Shield rounded corner radius
constexpr gfx::RoundedCornersF kBackgroundCornerRadius{4.f};
// Shield background blur sigma.
constexpr float kBackgroundBlurSigma =
static_cast<float>(AshColorProvider::LayerBlurSigma::kBlurDefault);
// Quality of the shield background blur.
constexpr float kBackgroundBlurQuality = 0.33f;
// All previews are the same height (this is achieved via a combination of
// scaling and padding).
constexpr int kFixedPreviewHeightDp = 256;
// The min and max width for preview size are in relation to the fixed height.
constexpr int kMinPreviewWidthDp = kFixedPreviewHeightDp / 2;
constexpr int kMaxPreviewWidthDp = kFixedPreviewHeightDp * 2;
// Padding between the alt-tab bandshield and the window previews.
constexpr int kInsideBorderHorizontalPaddingDp = 64;
constexpr int kInsideBorderVerticalPaddingDp = 60;
// Padding between the window previews within the alt-tab bandshield.
constexpr int kBetweenChildPaddingDp = 10;
// The alt-tab cycler widget is not activatable (except when ChromeVox is on),
// so we use WindowTargeter to send input events to the widget.
class CustomWindowTargeter : public aura::WindowTargeter {
public:
explicit CustomWindowTargeter(aura::Window* tab_cycler)
: tab_cycler_(tab_cycler) {}
~CustomWindowTargeter() override = default;
// aura::WindowTargeter
ui::EventTarget* FindTargetForEvent(ui::EventTarget* root,
ui::Event* event) override {
if (event->IsLocatedEvent())
return aura::WindowTargeter::FindTargetForEvent(root, event);
return tab_cycler_;
}
private:
aura::Window* tab_cycler_;
DISALLOW_COPY_AND_ASSIGN(CustomWindowTargeter);
};
// The UMA histogram that logs smoothness of the fade-in animation.
constexpr char kWindowCycleShowAnimationSmoothness[] =
"Ash.WindowCycleView.AnimationSmoothness.Show";
// The UMA histogram that logs smoothness of the window container animation.
constexpr char kContainerAnimationSmoothness[] =
"Ash.WindowCycleView.AnimationSmoothness.Container";
class WindowCycleAnimationMetricsReporter
: public ui::AnimationMetricsReporter {
public:
explicit WindowCycleAnimationMetricsReporter(const char* name)
: name_(name) {}
~WindowCycleAnimationMetricsReporter() override = default;
WindowCycleAnimationMetricsReporter(
const WindowCycleAnimationMetricsReporter&) = delete;
WindowCycleAnimationMetricsReporter& operator=(
const WindowCycleAnimationMetricsReporter&) = delete;
// ui::AnimationMetricsReporter:
void Report(int value) override {
base::UmaHistogramPercentage(name_, value);
}
private:
const std::string name_;
};
class WindowCycleAnimationObserver : public ui::LayerAnimationObserver {
public:
WindowCycleAnimationObserver()
: animation_metrics_reporter_(
std::make_unique<WindowCycleAnimationMetricsReporter>(
kContainerAnimationSmoothness)) {}
// ui::LayerAnimationObserver:
void OnLayerAnimationStarted(ui::LayerAnimationSequence* sequence) override {}
void OnLayerAnimationEnded(ui::LayerAnimationSequence* sequence) override {}
void OnLayerAnimationAborted(ui::LayerAnimationSequence* sequence) override {}
void OnLayerAnimationScheduled(
ui::LayerAnimationSequence* sequence) override {
sequence->SetAnimationMetricsReporter(animation_metrics_reporter_.get());
}
std::unique_ptr<WindowCycleAnimationMetricsReporter>
animation_metrics_reporter_;
};
} // namespace
// This view represents a single aura::Window by displaying a title and a
// thumbnail of the window's contents.
class WindowCycleItemView : public WindowMiniView {
public:
explicit WindowCycleItemView(aura::Window* window) : WindowMiniView(window) {
SetShowPreview(/*show=*/true);
UpdatePreviewRoundedCorners(/*show=*/true);
SetFocusBehavior(FocusBehavior::ALWAYS);
UpdateIconView();
}
~WindowCycleItemView() override = default;
private:
// WindowMiniView:
// Returns the size for the preview view, scaled to fit within the max bounds.
// Scaling is always 1:1 and we only scale down, never up.
gfx::Size GetPreviewViewSize() const override {
gfx::Size preview_pref_size = preview_view()->GetPreferredSize();
if (preview_pref_size.width() > kMaxPreviewWidthDp ||
preview_pref_size.height() > kFixedPreviewHeightDp) {
const float scale =
std::min(kMaxPreviewWidthDp / float{preview_pref_size.width()},
kFixedPreviewHeightDp / float{preview_pref_size.height()});
preview_pref_size =
gfx::ScaleToFlooredSize(preview_pref_size, scale, scale);
}
return preview_pref_size;
}
// views::View:
void Layout() override {
WindowMiniView::Layout();
// Show the backdrop if the preview view does not take up all the bounds
// allocated for it.
gfx::Rect preview_max_bounds = GetContentsBounds();
preview_max_bounds.Subtract(GetHeaderBounds());
const gfx::Rect preview_area_bounds = preview_view()->bounds();
SetBackdropVisibility(preview_max_bounds.size() !=
preview_area_bounds.size());
}
gfx::Size CalculatePreferredSize() const override {
// Previews can range in width from half to double of
// |kFixedPreviewHeightDp|. Padding will be added to the sides to achieve
// this if the preview is too narrow.
// TODO(sammiequon): Investigate whether we can remove some of these
// calculations and use the views framework to layout the children.
gfx::Size preview_size = GetPreviewViewSize();
// All previews are the same height (this may add padding on top and
// bottom).
preview_size.set_height(kFixedPreviewHeightDp);
// Previews should never be narrower than half or wider than double their
// fixed height.
preview_size.set_width(base::ClampToRange(
preview_size.width(), kMinPreviewWidthDp, kMaxPreviewWidthDp));
const int margin = GetInsets().width();
preview_size.Enlarge(margin, margin + WindowMiniView::kHeaderHeightDp);
return preview_size;
}
DISALLOW_COPY_AND_ASSIGN(WindowCycleItemView);
};
// A view that shows a collection of windows the user can tab through.
class WindowCycleView : public views::WidgetDelegateView {
public:
explicit WindowCycleView(const WindowCycleList::WindowList& windows)
: target_window_(nullptr),
animation_metrics_reporter_(
std::make_unique<WindowCycleAnimationMetricsReporter>(
kWindowCycleShowAnimationSmoothness)) {
DCHECK(!windows.empty());
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
layer()->SetMasksToBounds(true);
layer()->SetOpacity(0.0);
{
ui::ScopedLayerAnimationSettings animate_fade(layer()->GetAnimator());
animate_fade.SetAnimationMetricsReporter(
animation_metrics_reporter_.get());
animate_fade.SetTransitionDuration(
base::TimeDelta::FromMilliseconds(100));
layer()->SetOpacity(1.0);
}
mirror_container_ = AddChildView(std::make_unique<views::View>());
views::BoxLayout* layout =
mirror_container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets(kInsideBorderVerticalPaddingDp,
kInsideBorderHorizontalPaddingDp),
kBetweenChildPaddingDp));
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStart);
mirror_container_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
mirror_container_->layer()->SetFillsBoundsOpaquely(false);
SkColor background_color = AshColorProvider::Get()->GetBaseLayerColor(
AshColorProvider::BaseLayerType::kTransparent80,
AshColorProvider::AshColorMode::kDark);
mirror_container_->layer()->SetColor(background_color);
mirror_container_->layer()->SetBackgroundBlur(kBackgroundBlurSigma);
mirror_container_->layer()->SetBackdropFilterQuality(
kBackgroundBlurQuality);
mirror_container_->layer()->AddCacheRenderSurfaceRequest();
mirror_container_->layer()->SetName("windowCycleList/MirrorContainer");
for (auto* window : windows) {
// |mirror_container_| owns |view|. The |preview_view_| in |view| will
// use trilinear filtering in InitLayerOwner().
auto* view = mirror_container_->AddChildView(
std::make_unique<WindowCycleItemView>(window));
window_view_map_[window] = view;
}
// The insets in the WindowCycleItemView are coming from its border, which
// paints the focus ring around the view when it is highlighted. Exclude the
// insets such that the spacing between the contents of the views rather
// than the views themselves is |kBetweenChildPaddingDp|.
const gfx::Insets cycle_item_insets =
window_view_map_.begin()->second->GetInsets();
layout->set_between_child_spacing(kBetweenChildPaddingDp -
cycle_item_insets.width());
}
~WindowCycleView() override = default;
void SetTargetWindow(aura::Window* target) {
// Hide the focus border of the previous target window and show the focus
// border of the new one.
if (target_window_) {
auto target_it = window_view_map_.find(target_window_);
if (target_it != window_view_map_.end())
target_it->second->UpdateBorderState(/*show=*/false);
}
target_window_ = target;
auto target_it = window_view_map_.find(target_window_);
if (target_it != window_view_map_.end())
target_it->second->UpdateBorderState(/*show=*/true);
if (GetWidget()) {
Layout();
if (target_window_)
window_view_map_[target_window_]->RequestFocus();
}
}
void HandleWindowDestruction(aura::Window* destroying_window,
aura::Window* new_target) {
auto view_iter = window_view_map_.find(destroying_window);
views::View* preview = view_iter->second;
views::View* parent = preview->parent();
DCHECK_EQ(mirror_container_, parent);
window_view_map_.erase(view_iter);
delete preview;
// With one of its children now gone, we must re-layout
// |mirror_container_|. This must happen before SetTargetWindow() to make
// sure our own Layout() works correctly when it's calculating highlight
// bounds.
parent->Layout();
SetTargetWindow(new_target);
}
void DestroyContents() {
window_view_map_.clear();
target_window_ = nullptr;
RemoveAllChildViews(true);
}
// views::WidgetDelegateView overrides:
gfx::Size CalculatePreferredSize() const override {
return mirror_container_->GetPreferredSize();
}
void Layout() override {
if (!target_window_ || bounds().IsEmpty())
return;
bool first_layout = mirror_container_->bounds().IsEmpty();
// If |mirror_container_| has not yet been laid out, we must lay it and
// its descendants out so that the calculations based on |target_view|
// work properly.
if (first_layout) {
mirror_container_->SizeToPreferredSize();
if (mirror_container_->GetPreferredSize().width() < width()) {
mirror_container_->layer()->SetRoundedCornerRadius(
kBackgroundCornerRadius);
}
}
views::View* target_view = window_view_map_[target_window_];
gfx::RectF target_bounds(target_view->GetLocalBounds());
views::View::ConvertRectToTarget(target_view, mirror_container_,
&target_bounds);
gfx::Rect container_bounds(mirror_container_->GetPreferredSize());
// Case one: the container is narrower than the screen. Center the
// container.
int x_offset = (width() - container_bounds.width()) / 2;
if (x_offset < 0) {
// Case two: the container is wider than the screen. Center the target
// view by moving the list just enough to ensure the target view is in
// the center.
x_offset = width() / 2 - mirror_container_->GetMirroredXInView(
target_bounds.CenterPoint().x());
// However, the container must span the screen, i.e. the maximum x is 0
// and the minimum for its right boundary is the width of the screen.
x_offset = std::min(x_offset, 0);
x_offset = std::max(x_offset, width() - container_bounds.width());
}
container_bounds.set_x(x_offset);
mirror_container_->SetBoundsRect(container_bounds);
// Enable animations only after the first Layout() pass.
if (first_layout) {
// The preview list animates bounds changes (other animatable properties
// never change).
ui::LayerAnimator* animator = ui::LayerAnimator::CreateImplicitAnimator();
animator->AddObserver(&container_animation_observer_);
mirror_container_->layer()->SetAnimator(animator);
}
}
View* GetInitiallyFocusedView() override {
return window_view_map_[target_window_];
}
private:
std::map<aura::Window*, WindowCycleItemView*> window_view_map_;
views::View* mirror_container_;
aura::Window* target_window_;
// Metric reporter for animation.
const std::unique_ptr<WindowCycleAnimationMetricsReporter>
animation_metrics_reporter_;
WindowCycleAnimationObserver container_animation_observer_;
DISALLOW_COPY_AND_ASSIGN(WindowCycleView);
};
WindowCycleList::WindowCycleList(const WindowList& windows)
: windows_(windows) {
if (!ShouldShowUi())
Shell::Get()->mru_window_tracker()->SetIgnoreActivations(true);
for (auto* window : windows_)
window->AddObserver(this);
if (ShouldShowUi()) {
if (g_disable_initial_delay) {
InitWindowCycleView();
} else {
show_ui_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(150),
this, &WindowCycleList::InitWindowCycleView);
}
}
}
WindowCycleList::~WindowCycleList() {
if (!ShouldShowUi())
Shell::Get()->mru_window_tracker()->SetIgnoreActivations(false);
for (auto* window : windows_)
window->RemoveObserver(this);
if (cycle_ui_widget_)
cycle_ui_widget_->Close();
// |this| is responsible for notifying |cycle_view_| when windows are
// destroyed. Since |this| is going away, clobber |cycle_view_|. Otherwise
// there will be a race where a window closes after now but before the
// Widget::Close() call above actually destroys |cycle_view_|. See
// crbug.com/681207
if (cycle_view_)
cycle_view_->DestroyContents();
// While the cycler widget is shown, the windows listed in the cycler is
// marked as force-visible and don't contribute to occlusion. In order to
// work occlusion calculation properly, we need to activate a window after
// the widget has been destroyed. See b/138914552.
if (!windows_.empty() && user_did_accept_) {
auto* target_window = windows_[current_index_];
SelectWindow(target_window);
}
}
void WindowCycleList::Step(WindowCycleController::Direction direction) {
if (windows_.empty())
return;
// When there is only one window, we should give feedback to the user. If
// the window is minimized, we should also show it.
if (windows_.size() == 1) {
::wm::AnimateWindow(windows_[0], ::wm::WINDOW_ANIMATION_TYPE_BOUNCE);
SelectWindow(windows_[0]);
return;
}
DCHECK(static_cast<size_t>(current_index_) < windows_.size());
if (!cycle_view_ && current_index_ == 0) {
// Special case the situation where we're cycling forward but the MRU
// window is not active. This occurs when all windows are minimized. The
// starting window should be the first one rather than the second.
if (direction == WindowCycleController::FORWARD &&
!wm::IsActiveWindow(windows_[0]))
current_index_ = -1;
}
// We're in a valid cycle, so step forward or backward.
current_index_ += direction == WindowCycleController::FORWARD ? 1 : -1;
// Wrap to window list size.
current_index_ = (current_index_ + windows_.size()) % windows_.size();
DCHECK(windows_[current_index_]);
if (ShouldShowUi()) {
if (current_index_ > 1)
InitWindowCycleView();
if (cycle_view_)
cycle_view_->SetTargetWindow(windows_[current_index_]);
}
}
// static
void WindowCycleList::DisableInitialDelayForTesting() {
g_disable_initial_delay = true;
}
void WindowCycleList::OnWindowDestroying(aura::Window* window) {
window->RemoveObserver(this);
WindowList::iterator i = std::find(windows_.begin(), windows_.end(), window);
// TODO(oshima): Change this back to DCHECK once crbug.com/483491 is fixed.
CHECK(i != windows_.end());
int removed_index = static_cast<int>(i - windows_.begin());
windows_.erase(i);
if (current_index_ > removed_index ||
current_index_ == static_cast<int>(windows_.size())) {
current_index_--;
}
if (cycle_view_) {
auto* new_target_window =
windows_.empty() ? nullptr : windows_[current_index_];
cycle_view_->HandleWindowDestruction(window, new_target_window);
if (windows_.empty()) {
// This deletes us.
Shell::Get()->window_cycle_controller()->CancelCycling();
return;
}
}
}
void WindowCycleList::OnDisplayMetricsChanged(const display::Display& display,
uint32_t changed_metrics) {
if (cycle_ui_widget_ &&
display.id() ==
display::Screen::GetScreen()
->GetDisplayNearestWindow(cycle_ui_widget_->GetNativeWindow())
.id() &&
(changed_metrics & (DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_ROTATION))) {
Shell::Get()->window_cycle_controller()->CancelCycling();
// |this| is deleted.
return;
}
}
bool WindowCycleList::ShouldShowUi() {
return windows_.size() > 1;
}
void WindowCycleList::InitWindowCycleView() {
if (cycle_view_)
return;
cycle_view_ = new WindowCycleView(windows_);
cycle_view_->SetTargetWindow(windows_[current_index_]);
// We need to activate the widget if ChromeVox is enabled as ChromeVox
// relies on activation.
const bool spoken_feedback_enabled =
Shell::Get()->accessibility_controller()->spoken_feedback_enabled();
views::Widget* widget = new views::Widget;
views::Widget::InitParams params;
params.delegate = cycle_view_;
params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
// Don't let the alt-tab cycler be activatable. This lets the currently
// activated window continue to be in the foreground. This may affect
// things such as video automatically pausing/playing.
if (!spoken_feedback_enabled)
params.activatable = views::Widget::InitParams::ACTIVATABLE_NO;
params.accept_events = true;
params.name = "WindowCycleList (Alt+Tab)";
// TODO(estade): make sure nothing untoward happens when the lock screen
// or a system modal dialog is shown.
aura::Window* root_window = Shell::GetRootWindowForNewWindows();
params.parent = root_window->GetChildById(kShellWindowId_OverlayContainer);
gfx::Rect widget_rect = display::Screen::GetScreen()
->GetDisplayNearestWindow(root_window)
.bounds();
const int widget_height = cycle_view_->GetPreferredSize().height();
widget_rect.set_y(widget_rect.y() +
(widget_rect.height() - widget_height) / 2);
widget_rect.set_height(widget_height);
params.bounds = widget_rect;
widget->Init(std::move(params));
screen_observer_.Add(display::Screen::GetScreen());
widget->Show();
cycle_ui_widget_ = widget;
// Since this window is not activated, grab events.
if (!spoken_feedback_enabled) {
window_targeter_ = std::make_unique<aura::ScopedWindowTargeter>(
widget->GetNativeWindow()->GetRootWindow(),
std::make_unique<CustomWindowTargeter>(widget->GetNativeWindow()));
}
// Close the app list, if it's open in clamshell mode.
if (!Shell::Get()->tablet_mode_controller()->InTabletMode())
Shell::Get()->app_list_controller()->DismissAppList();
}
void WindowCycleList::SelectWindow(aura::Window* window) {
// If the list has only one window, the window can be selected twice (in
// Step() and the destructor). This causes ARC PIP windows to be restored
// twice, which leads to a wrong window state.
if (window_selected_)
return;
if (window->GetProperty(kPipOriginalWindowKey)) {
window_util::ExpandArcPipWindow();
} else {
window->Show();
WindowState::Get(window)->Activate();
}
window_selected_ = true;
}
} // namespace ash