chromium/ash/system/power/power_button_menu_screen_view.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/power/power_button_menu_screen_view.h"

#include <utility>

#include "ash/curtain/security_curtain_controller.h"
#include "ash/shell.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/system/power/power_button_menu_curtain_view.h"
#include "ash/system/power/power_button_menu_metrics_type.h"
#include "ash/system/power/power_button_menu_view.h"
#include "ash/system/power/power_button_menu_view_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

// Opacity of the power button menu fullscreen background shield.
constexpr float kPowerButtonMenuOpacity = 0.4f;

// TODO(minch): Get the internal display size instead if needed.
// Gets the landscape size of the primary display. For landscape orientation,
// the width is always larger than height.
gfx::Size GetPrimaryDisplayLandscapeSize() {
  gfx::Rect bounds = display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
  return gfx::Size(std::max(bounds.width(), bounds.height()),
                   std::min(bounds.width(), bounds.height()));
}

// Adjust the menu's |actual_position| to be at least kMenuTransformDistanceDp
// from the edge of the display. |menu_size| means the width or height of the
// menu and |actual_position| is x-coordinate or y-coordinate of the menu.
// |display_edge| is the width or height of the display in landscape_primary
// orientation depending on the power button's posotion.
int AdjustMenuEdgeForDisplaySize(int actual_position,
                                 int display_edge,
                                 int menu_size) {
  return std::min(
      display_edge - kPowerButtonMenuTransformDistanceDp - menu_size,
      std::max(kPowerButtonMenuTransformDistanceDp, actual_position));
}

bool IsCurtainModeEnabled() {
  return ash::Shell::Get()->security_curtain_controller().IsEnabled();
}

}  // namespace

using PowerButtonPosition = PowerButtonController::PowerButtonPosition;
using TransformDirection = PowerButtonMenuView::TransformDirection;

class PowerButtonMenuScreenView::PowerButtonMenuBackgroundView
    : public views::View,
      public ui::ImplicitAnimationObserver {
  METADATA_HEADER(PowerButtonMenuBackgroundView, views::View)

 public:
  explicit PowerButtonMenuBackgroundView(
      base::RepeatingClosure show_animation_done)
      : show_animation_done_(show_animation_done) {
    SetPaintToLayer(ui::LAYER_SOLID_COLOR);
    layer()->SetOpacity(0.f);
  }
  PowerButtonMenuBackgroundView(const PowerButtonMenuBackgroundView&) = delete;
  PowerButtonMenuBackgroundView& operator=(
      const PowerButtonMenuBackgroundView&) = delete;
  ~PowerButtonMenuBackgroundView() override = default;

  void OnImplicitAnimationsCompleted() override {
    PowerButtonController* power_button_controller =
        Shell::Get()->power_button_controller();
    if (layer()->opacity() == 0.f) {
      SetVisible(false);
      power_button_controller->DismissMenu();
    }

    if (layer()->opacity() == kPowerButtonMenuOpacity) {
      show_animation_done_.Run();
    }
  }

  void ScheduleShowHideAnimation(bool show) {
    SetVisible(true);
    layer()->GetAnimator()->AbortAllAnimations();

    ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
    animation.AddObserver(this);
    animation.SetTweenType(show ? gfx::Tween::EASE_IN_2
                                : gfx::Tween::FAST_OUT_LINEAR_IN);
    animation.SetTransitionDuration(kPowerButtonMenuAnimationDuration);
    animation.SetPreemptionStrategy(
        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
    layer()->SetOpacity(show ? kPowerButtonMenuOpacity : 0.f);
  }

 private:
  // views::View:
  void OnThemeChanged() override {
    views::View::OnThemeChanged();
    layer()->SetColor(
        GetColorProvider()->GetColor(kColorAshShieldAndBaseOpaque));
  }

  // A callback for when the animation that shows the power menu has finished.
  base::RepeatingClosure show_animation_done_;
};

BEGIN_METADATA(PowerButtonMenuScreenView, PowerButtonMenuBackgroundView)
END_METADATA

PowerButtonMenuScreenView::PowerButtonMenuScreenView(
    ShutdownReason shutdown_reason,
    PowerButtonPosition power_button_position,
    double power_button_offset_percentage,
    base::RepeatingClosure show_animation_done)
    : power_button_position_(power_button_position),
      power_button_offset_percentage_(power_button_offset_percentage) {
  power_button_screen_background_shield_ =
      new PowerButtonMenuBackgroundView(show_animation_done);
  AddChildView(power_button_screen_background_shield_.get());
  power_button_menu_view_ =
      new PowerButtonMenuView(shutdown_reason, power_button_position_);
  AddChildView(power_button_menu_view_.get());

  AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
}

PowerButtonMenuScreenView::~PowerButtonMenuScreenView() = default;

void PowerButtonMenuScreenView::ScheduleShowHideAnimation(bool show) {
  power_button_screen_background_shield_->ScheduleShowHideAnimation(show);
  if (IsCurtainModeEnabled()) {
    GetOrCreateCurtainView()->ScheduleShowHideAnimation(show);
  } else {
    power_button_menu_view_->ScheduleShowHideAnimation(show);
  }
}

void PowerButtonMenuScreenView::ResetOpacity() {
  if (IsCurtainModeEnabled()) {
    for (ui::Layer* layer : {power_button_screen_background_shield_->layer(),
                             GetOrCreateCurtainView()->layer()}) {
      DCHECK(layer);
      layer->SetOpacity(0.f);
    }
  } else {
    for (ui::Layer* layer : {power_button_screen_background_shield_->layer(),
                             power_button_menu_view_->layer()}) {
      DCHECK(layer);
      layer->SetOpacity(0.f);
    }
  }
}

void PowerButtonMenuScreenView::OnWidgetShown(
    PowerButtonController::PowerButtonPosition position,
    double offset_percentage) {
  power_button_position_ = position;
  power_button_offset_percentage_ = offset_percentage;
  // The order here matters. RecreateItems() must be called before calling
  // UpdateMenuBoundsOrigins(), since the latter relies on the
  // power_button_menu_view_'s preferred size, which depends on the items added
  // to the view.
  if (!IsCurtainModeEnabled()) {
    power_button_menu_view_->RecreateItems();
  }
  if (power_button_position_ != PowerButtonPosition::NONE) {
    UpdateMenuBoundsOrigins();
  }
  DeprecatedLayoutImmediately();
}

PowerButtonMenuCurtainView*
PowerButtonMenuScreenView::GetOrCreateCurtainView() {
  if (!power_button_menu_curtain_view_) {
    power_button_menu_curtain_view_ =
        AddChildView(std::make_unique<PowerButtonMenuCurtainView>());
  }
  return power_button_menu_curtain_view_;
}

void PowerButtonMenuScreenView::Layout(PassKey) {
  power_button_screen_background_shield_->SetBoundsRect(GetContentsBounds());
  if (IsCurtainModeEnabled()) {
    LayoutMenuCurtainView();
  } else {
    LayoutMenuView();
  }
}

void PowerButtonMenuScreenView::LayoutMenuView() {
  gfx::Rect menu_bounds = GetMenuBounds();
  PowerButtonMenuView::TransformDisplacement transform_displacement =
      power_button_menu_view_->GetTransformDisplacement();
  if (transform_displacement.direction == TransformDirection::X) {
    menu_bounds.set_x(menu_bounds.x() - transform_displacement.distance);
  } else if (transform_displacement.direction == TransformDirection::Y) {
    menu_bounds.set_y(menu_bounds.y() - transform_displacement.distance);
  }

  power_button_menu_view_->SetBoundsRect(menu_bounds);
}

void PowerButtonMenuScreenView::LayoutMenuCurtainView() {
  gfx::Rect menu_bounds = GetMenuBounds();
  menu_bounds.set_y(menu_bounds.y() - kPowerButtonMenuTransformDistanceDp);
  GetOrCreateCurtainView()->SetBoundsRect(menu_bounds);
}

bool PowerButtonMenuScreenView::OnMousePressed(const ui::MouseEvent& event) {
  return true;
}

void PowerButtonMenuScreenView::OnMouseReleased(const ui::MouseEvent& event) {
  ScheduleShowHideAnimation(false);
  RecordMenuActionHistogram(PowerButtonMenuActionType::kDismissByMouse);
}

bool PowerButtonMenuScreenView::AcceleratorPressed(
    const ui::Accelerator& accelerator) {
  DCHECK_EQ(ui::VKEY_ESCAPE, accelerator.key_code());
  Shell::Get()->power_button_controller()->DismissMenu();
  RecordMenuActionHistogram(PowerButtonMenuActionType::kDismissByEsc);
  return true;
}

void PowerButtonMenuScreenView::OnGestureEvent(ui::GestureEvent* event) {
  if (event->type() != ui::EventType::kGestureTapDown) {
    return;
  }

  // Dismisses the menu if tap anywhere on the background shield.
  ScheduleShowHideAnimation(false);
  RecordMenuActionHistogram(PowerButtonMenuActionType::kDismissByTouch);
}

void PowerButtonMenuScreenView::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t changed_metrics) {
  GetWidget()->SetBounds(
      display::Screen::GetScreen()->GetPrimaryDisplay().bounds());

  LayoutWithoutTransform();
}

void PowerButtonMenuScreenView::LayoutWithoutTransform() {
  power_button_screen_background_shield_->SetBoundsRect(GetContentsBounds());
  if (IsCurtainModeEnabled()) {
    GetOrCreateCurtainView()->SetBoundsRect(GetMenuBounds());
  } else {
    power_button_menu_view_->SetTransform(gfx::Transform());
    power_button_menu_view_->SetBoundsRect(GetMenuBounds());
  }
}

void PowerButtonMenuScreenView::UpdateMenuBoundsOrigins() {
  // Power button position offset in pixels from the top when the button is at
  // the left/right of the screen after rotation.
  int left_power_button_y = 0, right_power_button_y = 0;

  // Power button position offset in pixels from the left when the button is at
  // the top/bottom of the screen after rotation.
  int top_power_button_x = 0, bottom_power_button_x = 0;

  // The screen orientation when the power button is at the
  // left/right/top/bottom of the screen after rotation.
  chromeos::OrientationType left_screen_orientation, right_screen_orientation,
      top_screen_orientation, bottom_screen_orientation;
  const gfx::Size landscape_size = GetPrimaryDisplayLandscapeSize();
  int display_width = landscape_size.width();
  int display_height = landscape_size.height();
  int display_edge_for_adjust = landscape_size.height();

  if (power_button_position_ == PowerButtonPosition::TOP ||
      power_button_position_ == PowerButtonPosition::BOTTOM) {
    std::swap(display_width, display_height);
    display_edge_for_adjust = landscape_size.width();
  }

  int power_button_offset = display_height * power_button_offset_percentage_;
  switch (power_button_position_) {
    case PowerButtonPosition::LEFT:
    case PowerButtonPosition::BOTTOM:
      left_power_button_y = bottom_power_button_x = power_button_offset;
      right_power_button_y = top_power_button_x =
          display_height - power_button_offset;
      break;
    case PowerButtonPosition::RIGHT:
    case PowerButtonPosition::TOP:
      left_power_button_y = bottom_power_button_x =
          display_height - power_button_offset;
      right_power_button_y = top_power_button_x = power_button_offset;
      break;
    default:
      NOTREACHED();
  }

  switch (power_button_position_) {
    case PowerButtonPosition::LEFT:
      left_screen_orientation = chromeos::OrientationType::kLandscapePrimary;
      right_screen_orientation = chromeos::OrientationType::kLandscapeSecondary;
      top_screen_orientation = chromeos::OrientationType::kPortraitPrimary;
      bottom_screen_orientation = chromeos::OrientationType::kPortraitSecondary;
      break;
    case PowerButtonPosition::RIGHT:
      left_screen_orientation = chromeos::OrientationType::kLandscapeSecondary;
      right_screen_orientation = chromeos::OrientationType::kLandscapePrimary;
      top_screen_orientation = chromeos::OrientationType::kPortraitSecondary;
      bottom_screen_orientation = chromeos::OrientationType::kPortraitPrimary;
      break;
    case PowerButtonPosition::TOP:
      left_screen_orientation = chromeos::OrientationType::kPortraitSecondary;
      right_screen_orientation = chromeos::OrientationType::kPortraitPrimary;
      top_screen_orientation = chromeos::OrientationType::kLandscapePrimary;
      bottom_screen_orientation =
          chromeos::OrientationType::kLandscapeSecondary;
      break;
    case PowerButtonPosition::BOTTOM:
      left_screen_orientation = chromeos::OrientationType::kPortraitPrimary;
      right_screen_orientation = chromeos::OrientationType::kPortraitSecondary;
      top_screen_orientation = chromeos::OrientationType::kLandscapeSecondary;
      bottom_screen_orientation = chromeos::OrientationType::kLandscapePrimary;
      break;
    default:
      NOTREACHED();
  }

  menu_bounds_origins_.clear();
  const gfx::Size menu_size = GetMenuViewPreferredSize();
  // Power button position offset from the left when the button is at the left
  // is always zero.
  menu_bounds_origins_.insert(std::make_pair(
      left_screen_orientation,
      gfx::Point(kPowerButtonMenuTransformDistanceDp,
                 AdjustMenuEdgeForDisplaySize(
                     left_power_button_y - menu_size.height() / 2,
                     display_edge_for_adjust, menu_size.height()))));

  menu_bounds_origins_.insert(std::make_pair(
      right_screen_orientation,
      gfx::Point(display_width - kPowerButtonMenuTransformDistanceDp -
                     menu_size.width(),
                 AdjustMenuEdgeForDisplaySize(
                     right_power_button_y - menu_size.height() / 2,
                     display_edge_for_adjust, menu_size.height()))));

  // Power button position offset from the top when the button is at the top
  // is always zero.
  menu_bounds_origins_.insert(
      std::make_pair(top_screen_orientation,
                     gfx::Point(AdjustMenuEdgeForDisplaySize(
                                    top_power_button_x - menu_size.width() / 2,
                                    display_edge_for_adjust, menu_size.width()),
                                kPowerButtonMenuTransformDistanceDp)));

  menu_bounds_origins_.insert(std::make_pair(
      bottom_screen_orientation,
      gfx::Point(AdjustMenuEdgeForDisplaySize(
                     bottom_power_button_x - menu_size.width() / 2,
                     display_edge_for_adjust, menu_size.width()),
                 display_width - kPowerButtonMenuTransformDistanceDp -
                     menu_size.height())));
}

gfx::Rect PowerButtonMenuScreenView::GetMenuBounds() {
  gfx::Rect menu_bounds;

  if (power_button_position_ == PowerButtonPosition::NONE ||
      !display::Screen::GetScreen()->InTabletMode()) {
    menu_bounds = GetContentsBounds();
    menu_bounds.ClampToCenteredSize(GetMenuViewPreferredSize());
  } else {
    menu_bounds.set_origin(
        menu_bounds_origins_[Shell::Get()
                                 ->screen_orientation_controller()
                                 ->GetCurrentOrientation()]);
    menu_bounds.set_size(GetMenuViewPreferredSize());
  }
  return menu_bounds;
}

gfx::Size PowerButtonMenuScreenView::GetMenuViewPreferredSize() {
  if (IsCurtainModeEnabled()) {
    return GetOrCreateCurtainView()->GetPreferredSize();
  } else {
    return power_button_menu_view_->GetPreferredSize();
  }
}

BEGIN_METADATA(PowerButtonMenuScreenView)
END_METADATA

}  // namespace ash