| // 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::kArcKioskApp: |
| 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()->session_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); |
| |
| SetAccessibleName(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 |