blob: 88581e578b6e3c43afc3a53dab48d7017d57c64f [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/common/wm/window_cycle_list.h"
#include <list>
#include <map>
#include "ash/common/wm/mru_window_tracker.h"
#include "ash/common/wm/window_state.h"
#include "ash/common/wm_root_window_controller.h"
#include "ash/common/wm_shell.h"
#include "ash/common/wm_window.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "base/command_line.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/views/background.h"
#include "ui/views/border.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"
namespace ash {
namespace {
bool g_disable_initial_delay = false;
// Returns the window immediately below |window| in the current container.
WmWindow* GetWindowBelow(WmWindow* window) {
WmWindow* parent = window->GetParent();
if (!parent)
return nullptr;
const WmWindow::Windows children = parent->GetChildren();
auto iter = std::find(children.begin(), children.end(), window);
CHECK(*iter == window);
return (iter != children.begin()) ? *(iter - 1) : nullptr;
}
// This background paints a |Painter| but fills the view's layer's size rather
// than the view's size.
class LayerFillBackgroundPainter : public views::Background {
public:
explicit LayerFillBackgroundPainter(std::unique_ptr<views::Painter> painter)
: painter_(std::move(painter)) {}
~LayerFillBackgroundPainter() override {}
void Paint(gfx::Canvas* canvas, views::View* view) const override {
views::Painter::PaintPainterAt(canvas, painter_.get(),
gfx::Rect(view->layer()->size()));
}
private:
std::unique_ptr<views::Painter> painter_;
DISALLOW_COPY_AND_ASSIGN(LayerFillBackgroundPainter);
};
} // namespace
// This class restores and moves a window to the front of the stacking order for
// the duration of the class's scope.
class ScopedShowWindow : public WmWindowObserver {
public:
ScopedShowWindow();
~ScopedShowWindow() override;
// Show |window| at the top of the stacking order.
void Show(WmWindow* window);
// Cancel restoring the window on going out of scope.
void CancelRestore();
private:
// WmWindowObserver:
void OnWindowTreeChanging(WmWindow* window,
const TreeChangeParams& params) override;
// The window being shown.
WmWindow* window_;
// The window immediately below where window_ belongs.
WmWindow* stack_window_above_;
// If true, minimize window_ on going out of scope.
bool minimized_;
DISALLOW_COPY_AND_ASSIGN(ScopedShowWindow);
};
// This view represents a single WmWindow by displaying a title and a thumbnail
// of the window's contents.
class WindowPreviewView : public views::View, public WmWindowObserver {
public:
explicit WindowPreviewView(WmWindow* window)
: window_title_(new views::Label),
preview_background_(new views::View),
mirror_view_(window->CreateViewWithRecreatedLayers().release()),
window_observer_(this) {
window_observer_.Add(window);
window_title_->SetText(window->GetTitle());
window_title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
window_title_->SetEnabledColor(SK_ColorWHITE);
window_title_->SetAutoColorReadabilityEnabled(false);
// Background is not fully opaque, so subpixel rendering won't look good.
window_title_->SetSubpixelRenderingEnabled(false);
// The base font is 12pt (for English) so this comes out to 14pt.
const int kLabelSizeDelta = 2;
window_title_->SetFontList(
window_title_->font_list().DeriveWithSizeDelta(kLabelSizeDelta));
const int kAboveLabelPadding = 5;
const int kBelowLabelPadding = 10;
window_title_->SetBorder(
views::CreateEmptyBorder(kAboveLabelPadding, 0, kBelowLabelPadding, 0));
AddChildView(window_title_);
// Preview padding is black at 50% opacity.
preview_background_->set_background(
views::Background::CreateSolidBackground(
SkColorSetA(SK_ColorBLACK, 0xFF / 2)));
AddChildView(preview_background_);
AddChildView(mirror_view_);
SetFocusBehavior(FocusBehavior::ALWAYS);
}
~WindowPreviewView() override {}
// views::View:
gfx::Size GetPreferredSize() const override {
gfx::Size size = GetSizeForPreviewArea();
size.Enlarge(0, window_title_->GetPreferredSize().height());
return size;
}
void Layout() override {
const gfx::Size preview_area_size = GetSizeForPreviewArea();
// The window title is positioned above the preview area.
window_title_->SetBounds(0, 0, width(),
height() - preview_area_size.height());
gfx::Rect preview_area_bounds(preview_area_size);
preview_area_bounds.set_y(height() - preview_area_size.height());
mirror_view_->SetSize(GetMirrorViewScaledSize());
if (mirror_view_->size() == preview_area_size) {
// Padding is not needed, hide the background and set the mirror view
// to take up the entire preview area.
mirror_view_->SetPosition(preview_area_bounds.origin());
preview_background_->SetVisible(false);
return;
}
// Padding is needed, so show the background and set the mirror view to be
// centered within it.
preview_background_->SetBoundsRect(preview_area_bounds);
preview_background_->SetVisible(true);
preview_area_bounds.ClampToCenteredSize(mirror_view_->size());
mirror_view_->SetPosition(preview_area_bounds.origin());
}
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
node_data->role = ui::AX_ROLE_WINDOW;
node_data->SetName(window_title_->text());
}
// WmWindowObserver:
void OnWindowDestroying(WmWindow* window) override {
window_observer_.Remove(window);
}
void OnWindowTitleChanged(WmWindow* window) override {
window_title_->SetText(window->GetTitle());
}
private:
// The maximum width of a window preview.
static const int kMaxPreviewWidth = 512;
// All previews are the same height (this is achieved via a combination of
// scaling and padding).
static const int kFixedPreviewHeight = 256;
// Returns the size for the mirror view, scaled to fit within the max bounds.
// Scaling is always 1:1 and we only scale down, never up.
gfx::Size GetMirrorViewScaledSize() const {
gfx::Size mirror_pref_size = mirror_view_->GetPreferredSize();
if (mirror_pref_size.width() > kMaxPreviewWidth ||
mirror_pref_size.height() > kFixedPreviewHeight) {
float scale = std::min(
kMaxPreviewWidth / static_cast<float>(mirror_pref_size.width()),
kFixedPreviewHeight / static_cast<float>(mirror_pref_size.height()));
mirror_pref_size =
gfx::ScaleToFlooredSize(mirror_pref_size, scale, scale);
}
return mirror_pref_size;
}
// Returns the size for the entire preview area (mirror view and additional
// padding). All previews will be the same height, so if the mirror view isn't
// tall enough we will add top and bottom padding. Previews can range in width
// from kMaxPreviewWidth down to half that value. Again, padding will be added
// to the sides to achieve this if the preview is too narrow.
gfx::Size GetSizeForPreviewArea() const {
gfx::Size mirror_size = GetMirrorViewScaledSize();
float aspect_ratio =
static_cast<float>(mirror_size.width()) / mirror_size.height();
gfx::Size preview_size = mirror_size;
// Very narrow windows get vertical bars of padding on the sides.
if (aspect_ratio < 0.5f)
preview_size.set_width(mirror_size.height() / 2);
// All previews are the same height (this may add padding on top and
// bottom).
preview_size.set_height(kFixedPreviewHeight);
// Previews should never be narrower than half their max width (128dip).
preview_size.set_width(
std::max(preview_size.width(), kMaxPreviewWidth / 2));
return preview_size;
}
// Displays the title of the window above the preview.
views::Label* window_title_;
// When visible, shows a darkened background area behind |mirror_view_|
// (effectively padding the preview to fit the desired bounds).
views::View* preview_background_;
// The view that actually renders a thumbnail version of the window.
views::View* mirror_view_;
ScopedObserver<WmWindow, WmWindowObserver> window_observer_;
DISALLOW_COPY_AND_ASSIGN(WindowPreviewView);
};
// A view that shows a collection of windows the user can tab through.
class WindowCycleView : public views::WidgetDelegateView {
public:
WindowCycleView(const WindowCycleList::WindowList& windows,
WindowCycleController::Direction initial_direction)
: initial_direction_(initial_direction),
mirror_container_(new views::View()),
highlight_view_(new views::View()),
target_window_(nullptr) {
DCHECK(!windows.empty());
SetPaintToLayer(true);
layer()->SetFillsBoundsOpaquely(false);
layer()->SetMasksToBounds(true);
layer()->SetOpacity(0.0);
{
ui::ScopedLayerAnimationSettings animate_fade(layer()->GetAnimator());
animate_fade.SetTransitionDuration(
base::TimeDelta::FromMilliseconds(100));
layer()->SetOpacity(1.0);
}
set_background(views::Background::CreateSolidBackground(
SkColorSetA(SK_ColorBLACK, 0xE6)));
const int kInsideBorderPaddingDip = 64;
const int kBetweenChildPaddingDip = 10;
views::BoxLayout* layout = new views::BoxLayout(
views::BoxLayout::kHorizontal, kInsideBorderPaddingDip,
kInsideBorderPaddingDip, kBetweenChildPaddingDip);
layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_START);
mirror_container_->SetLayoutManager(layout);
mirror_container_->SetPaintToLayer(true);
mirror_container_->layer()->SetFillsBoundsOpaquely(false);
for (WmWindow* window : windows) {
// |mirror_container_| owns |view|.
views::View* view = new WindowPreviewView(window);
window_view_map_[window] = view;
mirror_container_->AddChildView(view);
}
const float kHighlightCornerRadius = 4;
// The background needs to be painted to fill the layer, not the View,
// because the layer animates bounds changes but the View's bounds change
// immediately.
highlight_view_->set_background(new LayerFillBackgroundPainter(
base::WrapUnique(views::Painter::CreateRoundRectWith1PxBorderPainter(
SkColorSetA(SK_ColorWHITE, 0x4D), SkColorSetA(SK_ColorWHITE, 0x33),
kHighlightCornerRadius))));
highlight_view_->SetPaintToLayer(true);
highlight_view_->layer()->SetFillsBoundsOpaquely(false);
AddChildView(highlight_view_);
AddChildView(mirror_container_);
}
~WindowCycleView() override {}
void SetTargetWindow(WmWindow* target) {
target_window_ = target;
if (GetWidget()) {
Layout();
if (target_window_) {
// In the window destruction case, we may have already removed the
// focused view and hence not be the focused window. We should still
// always be active, though.
DCHECK_EQ(ash::WmShell::Get()->GetActiveWindow()->GetInternalWidget(),
GetWidget());
window_view_map_[target_window_]->RequestFocus();
}
}
}
void HandleWindowDestruction(WmWindow* destroying_window,
WmWindow* 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);
}
// views::WidgetDelegateView overrides:
gfx::Size GetPreferredSize() 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();
// The preview list (|mirror_container_|) starts flush to the left of
// the screen but moves to the left (off the edge of the screen) as the use
// iterates over the previews. The list will move just enough to ensure the
// highlighted preview is at or to the left of the center of the workspace.
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());
int x_offset =
width() / 2 -
mirror_container_->GetMirroredXInView(target_bounds.CenterPoint().x());
if (initial_direction_ == WindowCycleController::FORWARD)
x_offset = std::min(x_offset, 0);
else
x_offset = std::max(x_offset, width() - container_bounds.width());
container_bounds.set_x(x_offset);
mirror_container_->SetBoundsRect(container_bounds);
// Calculate the target preview's bounds relative to |this|.
views::View::ConvertRectToTarget(mirror_container_, this, &target_bounds);
const int kHighlightPaddingDip = 5;
target_bounds.Inset(gfx::InsetsF(-kHighlightPaddingDip));
target_bounds.set_x(
GetMirroredXWithWidthInView(target_bounds.x(), target_bounds.width()));
highlight_view_->SetBoundsRect(gfx::ToEnclosingRect(target_bounds));
// Enable animations only after the first Layout() pass.
if (first_layout) {
// The preview list animates bounds changes (other animatable properties
// never change).
mirror_container_->layer()->SetAnimator(
ui::LayerAnimator::CreateImplicitAnimator());
// The selection highlight also animates all bounds changes and never
// changes other animatable properties.
highlight_view_->layer()->SetAnimator(
ui::LayerAnimator::CreateImplicitAnimator());
}
}
void OnMouseCaptureLost() override {
WmShell::Get()->window_cycle_controller()->StopCycling();
}
View* GetInitiallyFocusedView() override {
return window_view_map_[target_window_];
}
WmWindow* target_window() { return target_window_; }
private:
WindowCycleController::Direction initial_direction_;
std::map<WmWindow*, views::View*> window_view_map_;
views::View* mirror_container_;
views::View* highlight_view_;
WmWindow* target_window_;
DISALLOW_COPY_AND_ASSIGN(WindowCycleView);
};
ScopedShowWindow::ScopedShowWindow()
: window_(nullptr), stack_window_above_(nullptr), minimized_(false) {}
ScopedShowWindow::~ScopedShowWindow() {
if (window_) {
window_->GetParent()->RemoveObserver(this);
// Restore window's stacking position.
if (stack_window_above_)
window_->GetParent()->StackChildAbove(window_, stack_window_above_);
else
window_->GetParent()->StackChildAtBottom(window_);
// Restore minimized state.
if (minimized_)
window_->GetWindowState()->Minimize();
}
}
void ScopedShowWindow::Show(WmWindow* window) {
DCHECK(!window_);
window_ = window;
stack_window_above_ = GetWindowBelow(window);
minimized_ = window->GetWindowState()->IsMinimized();
window_->GetParent()->AddObserver(this);
window_->Show();
window_->GetWindowState()->Activate();
}
void ScopedShowWindow::CancelRestore() {
if (!window_)
return;
window_->GetParent()->RemoveObserver(this);
window_ = stack_window_above_ = nullptr;
}
void ScopedShowWindow::OnWindowTreeChanging(WmWindow* window,
const TreeChangeParams& params) {
// Only interested in removal.
if (params.new_parent != nullptr)
return;
if (params.target == window_) {
CancelRestore();
} else if (params.target == stack_window_above_) {
// If the window this window was above is removed, use the next window down
// as the restore marker.
stack_window_above_ = GetWindowBelow(stack_window_above_);
}
}
WindowCycleList::WindowCycleList(const WindowList& windows)
: windows_(windows),
current_index_(0),
initial_direction_(WindowCycleController::FORWARD),
cycle_view_(nullptr),
cycle_ui_widget_(nullptr),
screen_observer_(this) {
if (!ShouldShowUi())
WmShell::Get()->mru_window_tracker()->SetIgnoreActivations(true);
for (WmWindow* 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())
WmShell::Get()->mru_window_tracker()->SetIgnoreActivations(false);
for (WmWindow* window : windows_)
window->RemoveObserver(this);
if (showing_window_) {
showing_window_->CancelRestore();
} else if (!windows_.empty()) {
WmWindow* target_window = windows_[current_index_];
target_window->Show();
target_window->GetWindowState()->Activate();
}
if (cycle_ui_widget_)
cycle_ui_widget_->Close();
}
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) {
windows_[0]->Animate(::wm::WINDOW_ANIMATION_TYPE_BOUNCE);
windows_[0]->Show();
windows_[0]->GetWindowState()->Activate();
return;
}
DCHECK(static_cast<size_t>(current_index_) < windows_.size());
if (!cycle_view_ && current_index_ == 0) {
initial_direction_ = direction;
// 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 && !windows_[0]->IsActive())
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_]);
} else {
// Make sure the next window is visible.
showing_window_.reset(new ScopedShowWindow);
showing_window_->Show(windows_[current_index_]);
}
}
// static
void WindowCycleList::DisableInitialDelayForTesting() {
g_disable_initial_delay = true;
}
void WindowCycleList::OnWindowDestroying(WmWindow* 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_) {
WmWindow* new_target_window =
windows_.empty() ? nullptr : windows_[current_index_];
cycle_view_->HandleWindowDestruction(window, new_target_window);
if (windows_.empty()) {
// This deletes us.
WmShell::Get()->window_cycle_controller()->StopCycling();
return;
}
}
}
void WindowCycleList::OnDisplayAdded(const display::Display& new_display) {}
void WindowCycleList::OnDisplayRemoved(const display::Display& old_display) {}
void WindowCycleList::OnDisplayMetricsChanged(const display::Display& display,
uint32_t changed_metrics) {
if (cycle_ui_widget_ &&
display.id() ==
display::Screen::GetScreen()
->GetDisplayNearestWindow(cycle_ui_widget_->GetNativeView())
.id() &&
(changed_metrics & (DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_ROTATION))) {
WmShell::Get()->window_cycle_controller()->StopCycling();
// |this| is deleted.
return;
}
}
bool WindowCycleList::ShouldShowUi() {
return windows_.size() > 1;
}
void WindowCycleList::InitWindowCycleView() {
if (cycle_view_)
return;
cycle_view_ = new WindowCycleView(windows_, initial_direction_);
cycle_view_->SetTargetWindow(windows_[current_index_]);
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::TRANSLUCENT_WINDOW;
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.
WmWindow* root_window = WmShell::Get()->GetRootWindowForNewWindows();
root_window->GetRootWindowController()->ConfigureWidgetInitParamsForContainer(
widget, kShellWindowId_OverlayContainer, &params);
gfx::Rect widget_rect = root_window->GetDisplayNearestWindow().bounds();
int widget_height = cycle_view_->GetPreferredSize().height();
widget_rect.set_y((widget_rect.height() - widget_height) / 2);
widget_rect.set_height(widget_height);
params.bounds = widget_rect;
widget->Init(params);
screen_observer_.Add(display::Screen::GetScreen());
widget->Show();
widget->SetCapture(cycle_view_);
widget->set_auto_release_capture(false);
cycle_ui_widget_ = widget;
}
} // namespace ash