chromium/ash/system/power/power_button_menu_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_view.h"

#include <memory>

#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/login/login_screen_controller.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/system_shadow.h"
#include "ash/system/power/power_button_menu_item_view.h"
#include "ash/system/power/power_button_menu_metrics_type.h"
#include "ash/system/power/power_button_menu_view_util.h"
#include "ash/system/user/login_status.h"
#include "ash/wm/lock_state_controller.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/compositor_extra/shadow.h"
#include "ui/display/screen.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/highlight_border.h"

namespace ash {

namespace {

// Horizontal and vertical padding of the menu item view.
constexpr int kMenuItemHorizontalPadding = 16;
constexpr int kMenuItemVerticalPadding = 16;

// Horizontal padding between two menu items.
constexpr int kPaddingBetweenMenuItems = 8;

}  // namespace

using PowerButtonPosition = PowerButtonController::PowerButtonPosition;

PowerButtonMenuView::PowerButtonMenuView(
    ShutdownReason shutdown_reason,
    PowerButtonPosition power_button_position)
    : shutdown_reason_(shutdown_reason),
      power_button_position_(power_button_position) {
  SetFocusBehavior(FocusBehavior::ALWAYS);
  SetPaintToLayer();
  SetBorder(std::make_unique<views::HighlightBorder>(
      kPowerButtonMenuCornerRadius,
      chromeos::features::IsJellyrollEnabled()
          ? views::HighlightBorder::Type::kHighlightBorderOnShadow
          : kPowerButtonMenuBorderType));
  SetBackground(
      views::CreateThemedSolidBackground(kPowerButtonMenuBackgroundColorId));

  layer()->SetFillsBoundsOpaquely(false);
  layer()->SetRoundedCornerRadius(
      gfx::RoundedCornersF(kPowerButtonMenuCornerRadius));
  if (features::IsBackgroundBlurEnabled()) {
    layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
    layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
  }
  GetViewAccessibility().SetRole(ax::mojom::Role::kMenu);
  GetViewAccessibility().SetName(
      l10n_util::GetStringUTF16(IDS_ASH_POWER_BUTTON_MENU_ACCESSIBLE),
      ax::mojom::NameFrom::kAttribute);
  RecreateItems();

  // Create a system shadow for current view.
  shadow_ = SystemShadow::CreateShadowOnNinePatchLayerForView(
      this, SystemShadow::Type::kElevation12);
  shadow_->SetRoundedCornerRadius(kPowerButtonMenuCornerRadius);
}

PowerButtonMenuView::~PowerButtonMenuView() = default;

void PowerButtonMenuView::FocusPowerOffButton() {
  power_off_item_->RequestFocus();
}

void PowerButtonMenuView::ScheduleShowHideAnimation(bool show) {
  // Set initial state.
  SetVisible(true);

  // Calculate transform of menu view and shadow bounds.
  gfx::Transform transform;
  if (show) {
    TransformDisplacement transform_displacement = GetTransformDisplacement();
    if (transform_displacement.direction == TransformDirection::X) {
      transform.Translate(transform_displacement.distance, 0);
    } else if (transform_displacement.direction == TransformDirection::Y) {
      transform.Translate(0, transform_displacement.distance);
    }
  }

  SetLayerAnimation(layer(), this, show, transform);
  SetLayerAnimation(shadow_->GetLayer(), nullptr, show, transform);
}

PowerButtonMenuView::TransformDisplacement
PowerButtonMenuView::GetTransformDisplacement() const {
  TransformDisplacement transform_displacement;
  if (power_button_position_ == PowerButtonPosition::NONE ||
      !display::Screen::GetScreen()->InTabletMode()) {
    transform_displacement.direction = TransformDirection::Y;
    transform_displacement.distance = kPowerButtonMenuTransformDistanceDp;
    return transform_displacement;
  }

  chromeos::OrientationType screen_orientation =
      Shell::Get()->screen_orientation_controller()->GetCurrentOrientation();
  bool is_left_or_right = power_button_position_ == PowerButtonPosition::LEFT ||
                          power_button_position_ == PowerButtonPosition::RIGHT;

  if (chromeos::IsLandscapeOrientation(screen_orientation)) {
    transform_displacement.direction =
        is_left_or_right ? TransformDirection::X : TransformDirection::Y;
  } else {
    transform_displacement.direction =
        is_left_or_right ? TransformDirection::Y : TransformDirection::X;
  }

  bool positive_transform = false;
  if (is_left_or_right) {
    bool is_primary = chromeos::IsPrimaryOrientation(screen_orientation);
    positive_transform = power_button_position_ == PowerButtonPosition::LEFT
                             ? is_primary
                             : !is_primary;
  } else {
    bool is_landscape_primary_or_portrait_secondary =
        screen_orientation == chromeos::OrientationType::kLandscapePrimary ||
        screen_orientation == chromeos::OrientationType::kPortraitSecondary;

    positive_transform = power_button_position_ == PowerButtonPosition::TOP
                             ? is_landscape_primary_or_portrait_secondary
                             : !is_landscape_primary_or_portrait_secondary;
  }
  transform_displacement.distance = positive_transform
                                        ? kPowerButtonMenuTransformDistanceDp
                                        : -kPowerButtonMenuTransformDistanceDp;
  return transform_displacement;
}

void PowerButtonMenuView::RecreateItems() {
  // Helper to add or remove a menu item from |this|. Stores weak pointer to
  // |out_item_ptr|.
  auto add_remove_item =
      [this](bool create, PowerButtonMenuActionType action,
             base::RepeatingClosure callback, const gfx::VectorIcon& icon,
             const std::u16string& string,
             raw_ptr<PowerButtonMenuItemView>* out_item_ptr) -> void {
    // If an item needs to be created and exists, or needs to be destroyed but
    // does not exist, there is nothing to be done.
    if (create && *out_item_ptr) {
      return;
    }
    if (!create && !*out_item_ptr) {
      return;
    }

    if (create) {
      *out_item_ptr = AddChildView(std::make_unique<PowerButtonMenuItemView>(
          base::BindRepeating(&PowerButtonMenuView::ButtonPressed,
                              base::Unretained(this), action,
                              std::move(callback)),
          icon, string));
    } else {
      std::unique_ptr<PowerButtonMenuItemView> to_delete =
          RemoveChildViewT(*out_item_ptr);
      *out_item_ptr = nullptr;
    }
  };

  const SessionControllerImpl* const session_controller =
      Shell::Get()->session_controller();
  const LoginStatus login_status = session_controller->login_status();
  const bool create_sign_out = login_status != LoginStatus::NOT_LOGGED_IN;
  const bool create_lock_screen = login_status != LoginStatus::LOCKED &&
                                  session_controller->CanLockScreen();
  const bool create_capture_mode =
      display::Screen::GetScreen()->InTabletMode() &&
      !session_controller->IsUserSessionBlocked() &&
      login_status != LoginStatus::KIOSK_APP;
  const bool create_feedback = login_status != LoginStatus::LOCKED &&
                               login_status != LoginStatus::KIOSK_APP;

  add_remove_item(
      true, PowerButtonMenuActionType::kPowerOff,
      base::BindRepeating(
          &LockStateController::RequestShutdown,
          base::Unretained(Shell::Get()->lock_state_controller()),
          shutdown_reason_),
      kSystemPowerButtonMenuPowerOffIcon,
      l10n_util::GetStringUTF16(IDS_ASH_POWER_BUTTON_MENU_POWER_OFF_BUTTON),
      &power_off_item_);
  add_remove_item(create_sign_out, PowerButtonMenuActionType::kSignOut,
                  base::BindRepeating(
                      &LockStateController::RequestSignOut,
                      base::Unretained(Shell::Get()->lock_state_controller())),
                  kSystemPowerButtonMenuSignOutIcon,
                  user::GetLocalizedSignOutStringForStatus(login_status, false),
                  &sign_out_item_);
  add_remove_item(
      create_lock_screen, PowerButtonMenuActionType::kLockScreen,
      base::BindRepeating(&SessionControllerImpl::LockScreen,
                          base::Unretained(Shell::Get()->session_controller())),
      kSystemPowerButtonMenuLockScreenIcon,
      l10n_util::GetStringUTF16(IDS_ASH_POWER_BUTTON_MENU_LOCK_SCREEN_BUTTON),
      &lock_screen_item_);
  add_remove_item(
      create_capture_mode, PowerButtonMenuActionType::kCaptureMode,
      base::BindRepeating(&CaptureModeController::Start,
                          base::Unretained(CaptureModeController::Get()),
                          CaptureModeEntryType::kPowerMenu, base::DoNothing()),
      kCaptureModeIcon,
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAPTURE_MODE_BUTTON_LABEL),
      &capture_mode_item_);
  add_remove_item(
      create_feedback, PowerButtonMenuActionType::kFeedback,
      base::BindRepeating(
          [](Shell* shell) {
            if (shell->session_controller()->login_status() ==
                LoginStatus::NOT_LOGGED_IN) {
              // There is a special flow for feedback while in login screen,
              // therefore we trigger the same handler associated with the
              // feedback accelerator from the login screen to bring up the
              // feedback dialog.
              shell->login_screen_controller()->HandleAccelerator(
                  LoginAcceleratorAction::kShowFeedback);
            } else {
              NewWindowDelegate::GetInstance()->OpenFeedbackPage();
            }
          },
          Shell::Get()),
      kSystemPowerButtonMenuFeedbackIcon,
      l10n_util::GetStringUTF16(IDS_ASH_POWER_BUTTON_MENU_FEEDBACK_BUTTON),
      &feedback_item_);
}

void PowerButtonMenuView::Layout(PassKey) {
  gfx::Rect rect(GetContentsBounds().origin(),
                 power_off_item_->GetPreferredSize());
  const int y_offset =
      kMenuItemVerticalPadding - PowerButtonMenuItemView::kItemBorderThickness;
  int x_offset = kMenuItemHorizontalPadding -
                 PowerButtonMenuItemView::kItemBorderThickness;
  rect.Offset(x_offset, y_offset);
  power_off_item_->SetBoundsRect(rect);

  const int padding_between_items_with_border =
      kPaddingBetweenMenuItems -
      2 * PowerButtonMenuItemView::kItemBorderThickness;
  x_offset = rect.width() + padding_between_items_with_border;

  if (sign_out_item_) {
    rect.Offset(x_offset, 0);
    sign_out_item_->SetBoundsRect(rect);

    if (lock_screen_item_) {
      rect.Offset(x_offset, 0);
      lock_screen_item_->SetBoundsRect(rect);
    }
  }
  if (capture_mode_item_) {
    rect.Offset(x_offset, 0);
    capture_mode_item_->SetBoundsRect(rect);
  }
  if (feedback_item_) {
    rect.Offset(x_offset, 0);
    feedback_item_->SetBoundsRect(rect);
  }
}

gfx::Size PowerButtonMenuView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  gfx::Size menu_size;
  DCHECK(power_off_item_);
  menu_size = gfx::Size(0, PowerButtonMenuItemView::kMenuItemHeight +
                               2 * kMenuItemVerticalPadding);

  int width =
      PowerButtonMenuItemView::kMenuItemWidth + 2 * kMenuItemHorizontalPadding;
  const int one_item_x_offset =
      PowerButtonMenuItemView::kMenuItemWidth + kPaddingBetweenMenuItems;
  if (sign_out_item_) {
    width += one_item_x_offset;
    if (lock_screen_item_) {
      width += one_item_x_offset;
    }
  }
  if (capture_mode_item_) {
    width += one_item_x_offset;
  }
  if (feedback_item_) {
    width += one_item_x_offset;
  }
  menu_size.set_width(width);
  return menu_size;
}

void PowerButtonMenuView::OnImplicitAnimationsCompleted() {
  if (layer()->opacity() == 0.f) {
    SetVisible(false);
  }

  if (layer()->opacity() == 1.0f) {
    RequestFocus();
  }
}

void PowerButtonMenuView::ButtonPressed(PowerButtonMenuActionType action,
                                        base::RepeatingClosure callback) {
  RecordMenuActionHistogram(action);
  std::move(callback).Run();
  Shell::Get()->power_button_controller()->DismissMenu();
}

BEGIN_METADATA(PowerButtonMenuView)
END_METADATA

}  // namespace ash