// Copyright 2022 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/unified/power_button.h"
#include <utility>
#include "ash/constants/quick_settings_catalogs.h"
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/shutdown_controller_impl.h"
#include "ash/shutdown_reason.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/style_util.h"
#include "ash/style/typography.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/unified/quick_settings_metrics_util.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "ash/system/unified/user_chooser_detailed_view_controller.h"
#include "ash/wm/lock_state_controller.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "components/user_manager/user_type.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/models/menu_separator_types.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/context_menu_controller.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/menu/menu_delegate.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_model_adapter.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view.h"
namespace ash {
namespace {
// Rounded corner constants.
static constexpr int kRoundedCornerRadius = 16;
static constexpr int kNonRoundedCornerRadius = 4;
// Size of the icon in the power button.
constexpr gfx::Size kIconSize{20, 20};
constexpr gfx::RoundedCornersF kBottomRightNonRoundedCorners(
kRoundedCornerRadius,
kRoundedCornerRadius,
kNonRoundedCornerRadius,
kRoundedCornerRadius);
constexpr gfx::RoundedCornersF kBottomLeftNonRoundedCorners(
kRoundedCornerRadius,
kRoundedCornerRadius,
kRoundedCornerRadius,
kNonRoundedCornerRadius);
constexpr gfx::RoundedCornersF kTopLeftNonRoundedCorners(
kNonRoundedCornerRadius,
kRoundedCornerRadius,
kRoundedCornerRadius,
kRoundedCornerRadius);
constexpr gfx::RoundedCornersF kTopRightNonRoundedCorners(
kRoundedCornerRadius,
kNonRoundedCornerRadius,
kRoundedCornerRadius,
kRoundedCornerRadius);
constexpr gfx::RoundedCornersF kAllRoundedCorners(kRoundedCornerRadius,
kRoundedCornerRadius,
kRoundedCornerRadius,
kRoundedCornerRadius);
// The highlight path generator for the `PowerButton`.
class HighlightPathGenerator : public views::HighlightPathGenerator {
public:
explicit HighlightPathGenerator(PowerButton* power_button)
: power_button_(power_button) {}
HighlightPathGenerator(const HighlightPathGenerator&) = delete;
HighlightPathGenerator& operator=(const HighlightPathGenerator&) = delete;
private:
// HighlightPathGenerator:
std::optional<gfx::RRectF> GetRoundRect(const gfx::RectF& rect) override {
gfx::RectF bounds(power_button_->GetLocalBounds());
gfx::RoundedCornersF rounded = kAllRoundedCorners;
if (power_button_->IsMenuShowing()) {
// Don't need to check RTL since the `HighlightPathGenerator` will auto
// adjust the shape for the RTL case.
rounded = kTopLeftNonRoundedCorners;
}
return gfx::RRectF(bounds, rounded);
}
// Owned by views hierarchy.
const raw_ptr<PowerButton> power_button_;
};
// Returns whether the user's email address should be shown in the power menu.
bool ShouldShowEmailMenuItem() {
const UserSession* user_session =
Shell::Get()->session_controller()->GetPrimaryUserSession();
// Don't show if no user is signed in.
if (!user_session) {
return false;
}
switch (user_session->user_info.type) {
case user_manager::UserType::kRegular:
case user_manager::UserType::kChild:
return true;
case user_manager::UserType::kGuest:
case user_manager::UserType::kPublicAccount:
case user_manager::UserType::kKioskApp:
case user_manager::UserType::kWebKioskApp:
return false;
}
}
// Returns the text for the email address item in the power menu.
std::u16string GetEmailMenuItemText() {
// The 0th user session is the current one.
const UserSession* user_session =
Shell::Get()->session_controller()->GetUserSession(/*index=*/0);
CHECK(user_session);
return base::UTF8ToUTF16(user_session->user_info.display_email);
}
// The menu delegate for power button menu, which overrides the fontlist for
// menu labels.
class PowerButtonMenuDelegate : public ui::SimpleMenuModel {
public:
explicit PowerButtonMenuDelegate(Delegate* delegate)
: ui::SimpleMenuModel(delegate) {
font_list_ = ash::TypographyProvider::Get()->ResolveTypographyToken(
ash::TypographyToken::kCrosButton2);
}
PowerButtonMenuDelegate(const PowerButtonMenuDelegate&) = delete;
PowerButtonMenuDelegate& operator=(const PowerButtonMenuDelegate&) = delete;
~PowerButtonMenuDelegate() override = default;
// ui::MenuModel
const gfx::FontList* GetLabelFontListAt(size_t index) const override {
return &font_list_;
}
gfx::FontList font_list_;
};
} // namespace
class PowerButton::MenuController : public ui::SimpleMenuModel::Delegate,
public views::ContextMenuController {
public:
explicit MenuController(PowerButton* button) : power_button_(button) {}
MenuController(const MenuController&) = delete;
MenuController& operator=(const MenuController&) = delete;
~MenuController() override = default;
// ui::SimpleMenuModel::Delegate:
bool IsCommandIdEnabled(int command_id) const override {
if (command_id == VIEW_ID_QS_POWER_EMAIL_MENU_BUTTON) {
// Enable the email item if OS multi-profile is available.
return UserChooserDetailedViewController::IsUserChooserEnabled();
}
return true;
}
void ExecuteCommand(int command_id, int event_flags) override {
switch (command_id) {
case VIEW_ID_QS_POWER_EMAIL_MENU_BUTTON:
quick_settings_metrics_util::RecordQsButtonActivated(
QsButtonCatalogName::kPowerEmailMenuButton);
power_button_->tray_controller_->ShowUserChooserView();
break;
case VIEW_ID_QS_POWER_OFF_MENU_BUTTON:
quick_settings_metrics_util::RecordQsButtonActivated(
QsButtonCatalogName::kPowerOffMenuButton);
Shell::Get()->lock_state_controller()->RequestShutdown(
ShutdownReason::TRAY_SHUT_DOWN_BUTTON);
break;
case VIEW_ID_QS_POWER_SIGNOUT_MENU_BUTTON:
quick_settings_metrics_util::RecordQsButtonActivated(
QsButtonCatalogName::kPowerSignoutMenuButton);
Shell::Get()->lock_state_controller()->RequestSignOut();
break;
case VIEW_ID_QS_POWER_RESTART_MENU_BUTTON:
quick_settings_metrics_util::RecordQsButtonActivated(
QsButtonCatalogName::kPowerRestartMenuButton);
Shell::Get()->lock_state_controller()->RequestRestart(
power_manager::REQUEST_RESTART_FOR_USER, "Reboot by user");
break;
case VIEW_ID_QS_POWER_LOCK_MENU_BUTTON:
quick_settings_metrics_util::RecordQsButtonActivated(
QsButtonCatalogName::kPowerLockMenuButton);
Shell::Get()->session_controller()->LockScreen();
break;
default:
NOTREACHED();
}
}
// views::ContextMenuController:
void ShowContextMenuForViewImpl(views::View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) override {
// Build the menu model and save it to `context_menu_model_`.
BuildMenuModel();
menu_model_adapter_ = std::make_unique<views::MenuModelAdapter>(
context_menu_model_.get(),
base::BindRepeating(&MenuController::OnMenuClosed,
base::Unretained(this)));
std::unique_ptr<views::MenuItemView> menu =
menu_model_adapter_->CreateMenu();
root_menu_item_view_ = menu.get();
int run_types = views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
views::MenuRunner::CONTEXT_MENU |
views::MenuRunner::FIXED_ANCHOR;
menu_runner_ =
std::make_unique<views::MenuRunner>(std::move(menu), run_types);
menu_runner_->RunMenuAt(source->GetWidget(), /*button_controller=*/nullptr,
source->GetBoundsInScreen(),
views::MenuAnchorPosition::kBubbleTopRight,
source_type, /*native_view_for_gestures=*/nullptr,
/*corners=*/
base::i18n::IsRTL() ? kBottomRightNonRoundedCorners
: kBottomLeftNonRoundedCorners);
}
// Builds and saves a SimpleMenuModel to `context_menu_model_`;
void BuildMenuModel() {
// `context_menu_model_` and the other related pointers will be live for one
// menu view's life cycle. This model will be built based on the use case
// right before the menu view is shown. For example in the non-logged in
// page, we only build power off and restart button.
context_menu_model_ =
std::make_unique<PowerButtonMenuDelegate>(/*delegate=*/this);
SessionControllerImpl* session_controller =
Shell::Get()->session_controller();
bool const is_on_login_screen =
session_controller->login_status() == LoginStatus::NOT_LOGGED_IN;
bool const can_show_settings = TrayPopupUtils::CanOpenWebUISettings();
bool const can_lock_screen = session_controller->CanLockScreen();
bool const show_power_off_button =
!Shell::Get()->shutdown_controller()->reboot_on_shutdown();
// Add the user's email address (which is also the entry point for OS-level
// multi-profile).
if (ShouldShowEmailMenuItem()) {
context_menu_model_->AddItemWithIcon(
VIEW_ID_QS_POWER_EMAIL_MENU_BUTTON, GetEmailMenuItemText(),
ui::ImageModel::FromVectorIcon(kSystemMenuNewUserIcon,
cros_tokens::kCrosSysOnSurface,
kTrayTopShortcutButtonIconSize));
context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
}
if (show_power_off_button) {
context_menu_model_->AddItemWithIcon(
VIEW_ID_QS_POWER_OFF_MENU_BUTTON,
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SHUTDOWN),
ui::ImageModel::FromVectorIcon(kSystemPowerButtonMenuPowerOffIcon,
cros_tokens::kCrosSysOnSurface,
kTrayTopShortcutButtonIconSize));
}
context_menu_model_->AddItemWithIcon(
VIEW_ID_QS_POWER_RESTART_MENU_BUTTON,
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_REBOOT),
ui::ImageModel::FromVectorIcon(kSystemPowerButtonMenuRestartIcon,
cros_tokens::kCrosSysOnSurface,
kTrayTopShortcutButtonIconSize));
if (!is_on_login_screen) {
context_menu_model_->AddItemWithIcon(
VIEW_ID_QS_POWER_SIGNOUT_MENU_BUTTON,
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SIGN_OUT),
ui::ImageModel::FromVectorIcon(kSystemPowerButtonMenuSignOutIcon,
cros_tokens::kCrosSysOnSurface,
kTrayTopShortcutButtonIconSize));
}
if (can_show_settings && can_lock_screen) {
context_menu_model_->AddItemWithIcon(
VIEW_ID_QS_POWER_LOCK_MENU_BUTTON,
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_LOCK),
ui::ImageModel::FromVectorIcon(kSystemPowerButtonMenuLockScreenIcon,
cros_tokens::kCrosSysOnSurface,
kTrayTopShortcutButtonIconSize));
}
}
// Called when the context menu is closed. Used as a callback for
// `menu_model_adapter_`.
void OnMenuClosed() {
root_menu_item_view_ = nullptr;
menu_runner_.reset();
context_menu_model_.reset();
menu_model_adapter_.reset();
power_button_->UpdateView();
}
// The context menu model and its adapter for `PowerButton`.
std::unique_ptr<ui::SimpleMenuModel> context_menu_model_;
std::unique_ptr<views::MenuModelAdapter> menu_model_adapter_;
// The menu runner that is responsible to run the menu.
std::unique_ptr<views::MenuRunner> menu_runner_;
// The root menu item view of `context_menu_model_`. Cached for testing.
raw_ptr<views::MenuItemView> root_menu_item_view_ = nullptr;
// Owned by views hierarchy.
raw_ptr<PowerButton> power_button_ = nullptr;
};
PowerButtonContainer::PowerButtonContainer(PressedCallback callback)
: Button(std::move(callback)) {
auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>());
layout->SetOrientation(views::BoxLayout::Orientation::kHorizontal);
power_icon_ = AddChildView(std::make_unique<views::ImageView>());
power_icon_->SetImage(ui::ImageModel::FromVectorIcon(
kUnifiedMenuPowerIcon, cros_tokens::kCrosSysOnSurface));
power_icon_->SetImageSize(kIconSize);
arrow_icon_ = AddChildView(std::make_unique<views::ImageView>());
arrow_icon_->SetID(VIEW_ID_QS_POWER_BUTTON_CHEVRON_ICON);
arrow_icon_->SetImage(ui::ImageModel::FromVectorIcon(
kChevronDownSmallIcon, cros_tokens::kCrosSysOnSurface));
arrow_icon_->SetImageSize(kIconSize);
SetBorder(views::CreateEmptyBorder(gfx::Insets(6)));
// Paints this view to a layer so it will be on top of the
// `background_view_`
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
GetViewAccessibility().SetName(
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_POWER_MENU));
SetTooltipText(l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_POWER_MENU));
}
PowerButtonContainer::~PowerButtonContainer() = default;
void PowerButtonContainer::UpdateIconColor(bool is_active) {
auto icon_color_id = is_active ? cros_tokens::kCrosSysSystemOnPrimaryContainer
: cros_tokens::kCrosSysOnSurface;
power_icon_->SetImage(
ui::ImageModel::FromVectorIcon(kUnifiedMenuPowerIcon, icon_color_id));
arrow_icon_->SetImage(ui::ImageModel::FromVectorIcon(
is_active ? kChevronUpSmallIcon : kChevronDownSmallIcon, icon_color_id));
}
BEGIN_METADATA(PowerButtonContainer)
END_METADATA
PowerButton::PowerButton(UnifiedSystemTrayController* tray_controller)
: background_view_(AddChildView(std::make_unique<View>())),
button_content_(AddChildView(std::make_unique<PowerButtonContainer>(
base::BindRepeating(&PowerButton::OnButtonActivated,
base::Unretained(this))))),
context_menu_(std::make_unique<MenuController>(/*button=*/this)),
tray_controller_(tray_controller) {
CHECK(tray_controller_);
SetID(VIEW_ID_QS_POWER_BUTTON);
SetLayoutManager(std::make_unique<views::FillLayout>());
// Inits the `background_view_`'s layer. This view is `SetPaintToLayer` so it
// can be set the customized rounded corner.
background_view_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
auto* background_layer = background_view_->layer();
background_layer->SetRoundedCornerRadius(kAllRoundedCorners);
background_layer->SetFillsBoundsOpaquely(false);
background_layer->SetIsFastRoundedCorner(true);
set_context_menu_controller(context_menu_.get());
// Installs the customized focus ring path generator for the button.
views::HighlightPathGenerator::Install(
button_content_,
std::make_unique<HighlightPathGenerator>(/*power_button=*/this));
views::FocusRing::Get(button_content_)
->SetColorId(cros_tokens::kCrosSysPrimary);
// Ripple.
StyleUtil::SetUpInkDropForButton(button_content_, gfx::Insets(),
/*highlight_on_hover=*/false,
/*highlight_on_focus=*/false);
}
PowerButton::~PowerButton() {
set_context_menu_controller(nullptr);
}
bool PowerButton::IsMenuShowing() {
auto* menu_runner = context_menu_->menu_runner_.get();
return menu_runner && menu_runner->IsRunning();
}
views::MenuItemView* PowerButton::GetMenuViewForTesting() {
return context_menu_->root_menu_item_view_;
}
void PowerButton::OnThemeChanged() {
views::View::OnThemeChanged();
SkColor inactive_color =
GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemOnBase);
SkColor active_color =
GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemPrimaryContainer);
background_view_->layer()->SetColor(IsMenuShowing() ? active_color
: inactive_color);
}
void PowerButton::UpdateView() {
UpdateRoundedCorners();
OnThemeChanged();
views::FocusRing* focus_ring = views::FocusRing::Get(button_content_);
if (button_content_->HasFocus() && focus_ring) {
// Updating the focus ring path, make sure the focus ring gets updated to
// match this new state.
focus_ring->InvalidateLayout();
focus_ring->SchedulePaint();
}
button_content_->UpdateIconColor(/*is_active*/ IsMenuShowing());
}
void PowerButton::UpdateRoundedCorners() {
gfx::RoundedCornersF corners = kAllRoundedCorners;
if (IsMenuShowing()) {
corners = base::i18n::IsRTL() ? kTopRightNonRoundedCorners
: kTopLeftNonRoundedCorners;
}
background_view_->layer()->SetRoundedCornerRadius(corners);
}
void PowerButton::OnButtonActivated(const ui::Event& event) {
quick_settings_metrics_util::RecordQsButtonActivated(
QsButtonCatalogName::kPowerButton);
ui::MenuSourceType type;
if (event.IsMouseEvent()) {
type = ui::MENU_SOURCE_MOUSE;
} else if (event.IsTouchEvent()) {
type = ui::MENU_SOURCE_TOUCH;
} else if (event.IsKeyEvent()) {
type = ui::MENU_SOURCE_KEYBOARD;
} else {
type = ui::MENU_SOURCE_STYLUS;
}
context_menu_->ShowContextMenuForView(
/*source=*/this, GetBoundsInScreen().CenterPoint(), type);
UpdateView();
}
BEGIN_METADATA(PowerButton)
END_METADATA
} // namespace ash