// Copyright 2023 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/game_dashboard/game_dashboard_context.h"
#include <memory>
#include <optional>
#include <string>
#include "ash/capture_mode/capture_mode_camera_controller.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/game_dashboard/game_dashboard_button.h"
#include "ash/game_dashboard/game_dashboard_button_reveal_controller.h"
#include "ash/game_dashboard/game_dashboard_constants.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/game_dashboard/game_dashboard_main_menu_cursor_handler.h"
#include "ash/game_dashboard/game_dashboard_main_menu_view.h"
#include "ash/game_dashboard/game_dashboard_metrics.h"
#include "ash/game_dashboard/game_dashboard_toolbar_view.h"
#include "ash/game_dashboard/game_dashboard_utils.h"
#include "ash/game_dashboard/game_dashboard_welcome_dialog.h"
#include "ash/public/cpp/app_types_util.h"
#include "ash/public/cpp/arc_game_controls_flag.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/i18n/time_formatting.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/frame_header.h"
#include "components/prefs/pref_service.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/transient_window_manager.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
constexpr base::TimeDelta kCountUpTimerRefreshInterval = base::Seconds(1);
const std::u16string& kDefaultRecordingDuration = u"00:00";
// Number of pixels to add to the top and bottom of the Game Dashboard button so
// that it's centered within the frame header.
constexpr int kGameDashboardButtonVerticalPaddingDp = 3;
// Maximum width of the game window that centers the welcome dialog in the
// window instead of right aligned.
constexpr int kMaxCenteredWelcomeDialogWidth =
1.5 * game_dashboard::kWelcomeDialogFixedWidth;
// The animation duration for the bounds change operation on the toolbar widget.
constexpr base::TimeDelta kToolbarBoundsChangeAnimationDuration =
base::Milliseconds(150);
std::unique_ptr<views::Widget> CreateTransientChildWidget(
aura::Window* game_window,
const std::string& widget_name,
std::unique_ptr<views::View> view,
views::Widget::InitParams::Activatable activatable =
views::Widget::InitParams::Activatable::kDefault) {
views::Widget::InitParams params(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
// Sets the widget as a transient child, which is actually a sibling
// of the window. This ensures that this widget will not show up in
// screenshots or screen recordings.
params.parent = game_window;
params.name = widget_name;
params.activatable = activatable;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
auto widget = std::make_unique<views::Widget>();
widget->Init(std::move(params));
wm::TransientWindowManager::GetOrCreate(widget->GetNativeWindow())
->set_parent_controls_visibility(true);
widget->SetContentsView(std::move(view));
widget->SetVisibilityAnimationTransition(views::Widget::ANIMATE_NONE);
return widget;
}
// Tells the camera preview to maybe update its position. This will ensure that
// the preview doesn't overlap with the toolbar.
void MaybeUpdateCameraPreview() {
CaptureModeController::Get()->camera_controller()->MaybeUpdatePreviewWidget(
/*animate=*/true);
}
// Determines whether a given `key_code` will interact with the toolbar.
bool WillToolbarViewProcessKeyCode(const ui::KeyboardCode key_code) {
switch (key_code) {
case ui::VKEY_RIGHT:
case ui::VKEY_LEFT:
case ui::VKEY_UP:
case ui::VKEY_DOWN:
case ui::VKEY_RETURN:
case ui::VKEY_SPACE:
return true;
default:
return false;
}
}
} // namespace
GameDashboardContext::GameDashboardContext(aura::Window* game_window)
: game_window_(game_window),
app_id_(*game_window->GetProperty(kAppIDKey)),
toolbar_snap_location_(GameDashboardToolbarSnapLocation::kTopRight) {
DCHECK(game_window_);
window_state_observation_.Observe(WindowState::Get(game_window_));
show_welcome_dialog_ = game_dashboard_utils::ShouldShowWelcomeDialog();
}
GameDashboardContext::~GameDashboardContext() {
MaybeRemovePreTargetHandler();
window_state_observation_.Reset();
game_dashboard_button_->RemoveObserver(this);
if (main_menu_widget_) {
main_menu_widget_->CloseNow();
}
CloseWelcomeDialogIfAny(/*show_toolbar=*/false);
}
const std::u16string& GameDashboardContext::GetRecordingDuration() const {
return recording_duration_.empty() ? kDefaultRecordingDuration
: recording_duration_;
}
void GameDashboardContext::EnableFeatures(
bool enable,
GameDashboardMainMenuToggleMethod main_menu_toggle_method) {
DCHECK(game_dashboard_button_widget_)
<< "Game Dashboard button doesn't exist. Make sure to call Initialize() "
"before trying to use the context.";
if (enable) {
SetGameDashboardButtonVisibility(/*visible=*/true);
SetToolbarVisibility(/*visible=*/true);
} else {
CloseWelcomeDialogIfAny();
// Hide the toolbar if the system is in tablet mode or if the window
// is undergoing a resize animation. The toolbar is still visible in
// clamshell, overview mode.
SetToolbarVisibility(
/*visible=*/!(display::Screen::GetScreen()->InTabletMode() ||
main_menu_toggle_method ==
GameDashboardMainMenuToggleMethod::kAnimation));
if (main_menu_widget_) {
CloseMainMenu(main_menu_toggle_method);
}
SetGameDashboardButtonVisibility(/*visible=*/false);
}
}
void GameDashboardContext::Initialize() {
CHECK(!game_dashboard_button_widget_)
<< "The context can only be initialized once.";
CreateAndAddGameDashboardButtonWidget();
// ARC windows handle displaying the welcome dialog once the
// `game_dashboard_button_` becomes available.
if (!IsArcWindow(game_window_)) {
MaybeShowWelcomeDialog();
}
// The pretarget handler must be added when the context is initialized, since
// `OnWindowActivated()` is called before the context was created and
// initialized.
MaybeAddPreTargetHandler();
}
void GameDashboardContext::MaybeStackAboveWidget(views::Widget* widget) {
DCHECK(widget);
DCHECK(game_dashboard_button_widget_);
game_dashboard_button_widget_->StackAboveWidget(widget);
if (welcome_dialog_widget_) {
welcome_dialog_widget_->StackAboveWidget(widget);
}
if (main_menu_widget_) {
main_menu_widget_->StackAboveWidget(widget);
}
if (toolbar_widget_) {
toolbar_widget_->StackAboveWidget(widget);
}
EnsureMainMenuAboveToolbar();
}
void GameDashboardContext::SetGameDashboardToolbarSnapLocation(
GameDashboardToolbarSnapLocation new_location) {
toolbar_snap_location_ = new_location;
AnimateToolbarWidgetBoundsChange(CalculateToolbarWidgetBounds());
MaybeUpdateCameraPreview();
RecordGameDashboardToolbarNewLocation(app_id_, new_location);
}
void GameDashboardContext::OnWindowBoundsChanged(bool from_animation) {
UpdateGameDashboardButtonWidgetBounds();
MaybeUpdateToolbarWidgetBounds();
MaybeUpdateWelcomeDialogBounds();
if (from_animation) {
EnableFeatures(
/*enable=*/!game_window_->layer()->GetAnimator()->is_animating(),
GameDashboardMainMenuToggleMethod::kAnimation);
}
}
void GameDashboardContext::UpdateForGameControlsFlags() {
CHECK(IsArcWindow(game_window_));
const bool should_enable_button =
game_dashboard_utils::ShouldEnableGameDashboardButton(game_window_);
game_dashboard_button_->SetEnabled(should_enable_button);
if (should_enable_button) {
// ARC windows handle displaying the welcome dialog once the
// `game_dashboard_button_` becomes available.
MaybeShowWelcomeDialog();
}
if (toolbar_view_) {
toolbar_view_->UpdateViewForGameControls(
game_window_->GetProperty(kArcGameControlsFlagsKey));
}
// Ensure that the main menu is above the toolbar because updating toolbar
// changes its zorder.
EnsureMainMenuAboveToolbar();
}
void GameDashboardContext::ToggleMainMenuByAccelerator() {
if (game_dashboard_button_reveal_controller_) {
// Window is in fullscreen, and `game_dashboard_button_widget_` may not be
// visible. Reset its position and make it visible. Don't animate the button
// so it and the main menu show up at the same time.
game_dashboard_button_reveal_controller_->UpdateVisibility(
/*target_visibility=*/true, /*animate=*/false);
}
ToggleMainMenu(GameDashboardMainMenuToggleMethod::kSearchPlusG);
}
void GameDashboardContext::ToggleMainMenu(
GameDashboardMainMenuToggleMethod toggle_method) {
if (!main_menu_widget_) {
// If opened, close the welcome dialog, before opening the main menu.
CloseWelcomeDialogIfAny();
auto widget_delegate = std::make_unique<GameDashboardMainMenuView>(this);
DCHECK(!main_menu_view_);
main_menu_view_ = widget_delegate.get();
main_menu_widget_ =
base::WrapUnique(views::BubbleDialogDelegateView::CreateBubble(
std::move(widget_delegate)));
main_menu_widget_->AddObserver(this);
main_menu_widget_->Show();
game_dashboard_utils::UpdateAccessibilityTree(GetTraversableWidgets());
game_dashboard_button_->SetToggled(true);
AddCursorHandler();
RecordGameDashboardToggleMainMenu(app_id_, toggle_method,
/*toggled_on=*/true);
} else {
DCHECK(main_menu_view_);
DCHECK(main_menu_widget_);
CloseMainMenu(toggle_method);
}
}
void GameDashboardContext::CloseMainMenu(
GameDashboardMainMenuToggleMethod toggle_method) {
DCHECK(main_menu_widget_);
main_menu_widget_->RemoveObserver(this);
// Reset the `main_menu_widget_` before calling `UpdateOnMainMenuClosed()` to
// ensure all Game Dashboard widgets are up-to-date.
main_menu_widget_.reset();
// Since the `WidgetObserver` has been removed, `OnWidgetDestroyed` will not
// be called. Explicitly call `UpdateOnMainMenuClosed()` to update the
// `main_menu_view_`, remove the cursor handler, and update the
// `game_dashboard_button_` UI.
UpdateOnMainMenuClosed();
RecordGameDashboardToggleMainMenu(app_id_, toggle_method,
/*toggled_on=*/false);
}
bool GameDashboardContext::ToggleToolbar() {
if (!toolbar_widget_) {
auto view = std::make_unique<GameDashboardToolbarView>(this);
DCHECK(!toolbar_view_);
toolbar_view_ = view.get();
toolbar_widget_ = CreateTransientChildWidget(
game_window_, "GameDashboardToolbar", std::move(view));
DCHECK_EQ(game_window_,
wm::GetTransientParent(toolbar_widget_->GetNativeWindow()));
toolbar_widget_->widget_delegate()->SetAccessibleTitle(
l10n_util::GetStringUTF16(
IDS_ASH_GAME_DASHBOARD_TOOLBAR_TILE_BUTTON_TITLE));
MaybeUpdateToolbarWidgetBounds();
SetToolbarVisibility(/*visible=*/true);
game_dashboard_utils::UpdateAccessibilityTree(GetTraversableWidgets());
// Display the toolbar behind the main menu view.
EnsureMainMenuAboveToolbar();
RecordGameDashboardToolbarToggleState(app_id_, /*toggled_on=*/true);
return true;
}
CloseToolbar();
return false;
}
void GameDashboardContext::CloseToolbar() {
DCHECK(toolbar_view_);
DCHECK(toolbar_widget_);
toolbar_view_ = nullptr;
toolbar_widget_.reset();
game_dashboard_utils::UpdateAccessibilityTree(GetTraversableWidgets());
RecordGameDashboardToolbarToggleState(app_id_, /*toggled_on=*/false);
}
void GameDashboardContext::MaybeUpdateToolbarWidgetBounds() {
if (toolbar_widget_) {
toolbar_widget_->SetBounds(CalculateToolbarWidgetBounds());
MaybeUpdateCameraPreview();
}
}
bool GameDashboardContext::IsToolbarVisible() const {
return toolbar_widget_ && toolbar_widget_->IsVisible();
}
gfx::Rect GameDashboardContext::GetToolbarBoundsInScreen() const {
return IsToolbarVisible() ? toolbar_widget_->GetWindowBoundsInScreen()
: gfx::Rect{};
}
void GameDashboardContext::OnRecordingStarted(bool is_recording_game_window) {
if (is_recording_game_window) {
CHECK(!recording_timer_.IsRunning());
DCHECK(recording_start_time_.is_null());
DCHECK(recording_duration_.empty());
game_dashboard_button_->OnRecordingStarted();
recording_start_time_ = base::Time::Now();
OnUpdateRecordingTimer();
recording_timer_.Start(FROM_HERE, kCountUpTimerRefreshInterval, this,
&GameDashboardContext::OnUpdateRecordingTimer);
CHECK(recording_from_main_menu_);
RecordGameDashboardRecordingStartSource(
app_id_, *recording_from_main_menu_ ? GameDashboardMenu::kMainMenu
: GameDashboardMenu::kToolbar);
// `recording_from_main_menu_` is used to record the histogram for starting
// recording only. Reset it after the histogram is recorded.
recording_from_main_menu_ = std::nullopt;
}
if (main_menu_view_) {
main_menu_view_->OnRecordingStarted(is_recording_game_window);
}
if (toolbar_view_) {
toolbar_view_->OnRecordingStarted(is_recording_game_window);
}
}
void GameDashboardContext::OnRecordingEnded() {
// Resetting the timer will stop the timer.
recording_timer_.Stop();
recording_start_time_ = base::Time();
recording_duration_.clear();
game_dashboard_button_->OnRecordingEnded();
if (main_menu_view_) {
main_menu_view_->OnRecordingEnded();
}
if (toolbar_view_) {
toolbar_view_->OnRecordingEnded();
}
}
void GameDashboardContext::OnVideoFileFinalized() {
// For now it's ok to just call `OnRecordingEnded()` to update the UI.
OnRecordingEnded();
}
void GameDashboardContext::SetGameDashboardButtonVisibility(bool visible) {
if (visible && !game_dashboard_button_widget_->IsVisible() &&
!display::Screen::GetScreen()->InTabletMode()) {
// Show the Game Dashboard button if it's not visible.
// When the top edge timer fires, it's going to try to show the Game
// Dashboard button. Because this is already showing the button, stop
// the top edge timer.
if (game_dashboard_button_reveal_controller_) {
game_dashboard_button_reveal_controller_->StopTopEdgeTimer();
}
game_dashboard_button_widget_->ShowInactive();
} else if (!visible && game_dashboard_button_widget_->IsVisible() &&
!IsMainMenuOpen()) {
// Hide the Game Dashboard button if its visible and the main menu is
// closed.
game_dashboard_button_widget_->Hide();
}
}
void GameDashboardContext::SetToolbarVisibility(bool visible) {
if (!toolbar_widget_) {
return;
}
// If the toolbar should be visible and currently is not, show it. If the
// toolbar should not be visible and currently is, hide it. Otherwise, leave
// the toolbar in whatever state it is in.
const bool is_toolbar_visible = toolbar_widget_->IsVisible();
if (visible && !is_toolbar_visible) {
// Calling `Show()` on a widget activates that given widget, which causes a
// crash when Game Dashboard widgets are added to multiple windows while
// exiting overview mode. To avoid changing the activated window after a
// user has already selected a window in overview mode, show all widgets as
// inactive.
toolbar_widget_->ShowInactive();
} else if (!visible && is_toolbar_visible) {
toolbar_widget_->Hide();
}
}
void GameDashboardContext::MaybeAddPreTargetHandler() {
if (!game_window_->Contains(
wm::GetTransientRoot(window_util::GetActiveWindow()))) {
// Don't add a pretarget handler to any window whose transient root is not
// active.
return;
}
if (!added_to_pre_target_handler_) {
added_to_pre_target_handler_ = true;
// The pretarget handler must be added to the Shell in order to be
// properly notified of all events interacting with different widgets
// within the game window.
Shell::Get()->AddPreTargetHandler(this);
}
}
void GameDashboardContext::MaybeRemovePreTargetHandler() {
if (added_to_pre_target_handler_) {
added_to_pre_target_handler_ = false;
Shell::Get()->RemovePreTargetHandler(this);
}
}
void GameDashboardContext::OnEvent(ui::Event* event) {
// Close the main menu if the user clicks outside of both the main menu
// widget and the Game Dashboard button.
auto event_type = event->type();
if (event_type == ui::EventType::kKeyPressed) {
const ui::KeyEvent* key_event = event->AsKeyEvent();
if (toolbar_widget_ && toolbar_widget_->IsActive() && main_menu_widget_ &&
WillToolbarViewProcessKeyCode(key_event->key_code())) {
// Close the main menu if the toolbar processes the given key.
CloseMainMenu(GameDashboardMainMenuToggleMethod::kOthers);
} else if (ShouldNavigateToNewWidget(key_event)) {
const auto* currently_focused = views::Widget::GetWidgetForNativeWindow(
static_cast<aura::Window*>(event->target()));
const bool reverse = event->IsShiftDown();
// Manually move focus from the currently focused widget to the next in
// the widget list. It is possible that `currently_focused` is not in the
// `GetTraversableWidgets()`. For example, `currently_focused` is the app
// window itself.
if (auto* next_focus = game_dashboard_utils::GetNextWidgetToFocus(
GetTraversableWidgets(), currently_focused, reverse)) {
MoveFocus(next_focus, event, reverse);
}
}
} else if (main_menu_widget_) {
switch (event_type) {
case ui::EventType::kTouchPressed:
case ui::EventType::kMousePressed: {
// TODO(b/328852471): Update logic to compare event target with native
// window.
const ui::LocatedEvent* located_event = event->AsLocatedEvent();
const auto event_location =
located_event->target()->GetScreenLocation(*located_event);
if (!game_dashboard_button_->GetBoundsInScreen().Contains(
event_location) &&
!main_menu_widget_->GetWindowBoundsInScreen().Contains(
event_location)) {
// Touch/Mouse event occurred outside both the main menu widget and
// the Game Dashboard button bounds. Ignore the bounds of the Game
// Dashboard button since it will already toggle the main menu when
// pressed.
CloseMainMenu(GameDashboardMainMenuToggleMethod::kOthers);
}
} break;
default:
break;
}
}
ui::EventHandler::OnEvent(event);
}
void GameDashboardContext::OnViewPreferredSizeChanged(
views::View* observed_view) {
CHECK_EQ(game_dashboard_button_, observed_view);
UpdateGameDashboardButtonWidgetBounds();
MaybeUpdateWelcomeDialogBounds();
}
void GameDashboardContext::OnWidgetDestroyed(views::Widget* widget) {
DCHECK(main_menu_view_);
DCHECK_EQ(widget, main_menu_view_->GetWidget());
UpdateOnMainMenuClosed();
// Record main menu toggle off metrics.
switch (widget->closed_reason()) {
case views::Widget::ClosedReason::kLostFocus:
// The main menu is closed explicitly in the overview mode and the
// observer is removed before this event.
DCHECK(!OverviewController::Get()->InOverviewSession());
// Close reason for clicking outside or closing game window by clicking
// close button on the caption.
RecordGameDashboardToggleMainMenu(
app_id_, GameDashboardMainMenuToggleMethod::kOthers,
/*toggled_on=*/false);
break;
case views::Widget::ClosedReason::kCancelButtonClicked:
// Close reason for key Esc pressed.
RecordGameDashboardToggleMainMenu(app_id_,
GameDashboardMainMenuToggleMethod::kEsc,
/*toggled_on=*/false);
break;
case views::Widget::ClosedReason::kUnspecified:
// Close reason when the game window is closed unspecified.
RecordGameDashboardToggleMainMenu(
app_id_, GameDashboardMainMenuToggleMethod::kOthers,
/*toggled_on=*/false);
break;
default:
break;
}
}
void GameDashboardContext::OnPreWindowStateTypeChange(
WindowState* window_state,
chromeos::WindowStateType old_type) {
// Hide the Game Dashboard button before the window switches to fullscreen.
if (window_state->IsFullscreen()) {
DCHECK(!game_dashboard_button_reveal_controller_);
game_dashboard_button_reveal_controller_ =
std::make_unique<GameDashboardButtonRevealController>(this);
if (!chromeos::IsMinimizedWindowStateType(old_type)) {
// When the window goes from minimized to fullscreen, hide the Game
// Dashboard widget.
game_dashboard_button_reveal_controller_->UpdateVisibility(
/*target_visibility=*/false, /*animate=*/false);
}
}
}
void GameDashboardContext::OnPostWindowStateTypeChange(
WindowState* window_state,
chromeos::WindowStateType old_type) {
if (!window_state->IsFullscreen() &&
game_dashboard_button_reveal_controller_) {
if (!window_state->IsMinimized()) {
// When the window exits fullscreen and goes to any state that is not
// minimize, show the Game Dashboard button widget. Otherwise do nothing.
// Changing the visibility of the widget when the window is minimized
// causes Chrome to crash.
game_dashboard_button_reveal_controller_->UpdateVisibility(
/*target_visibility=*/true, /*animate=*/false);
}
game_dashboard_button_reveal_controller_.reset();
}
}
void GameDashboardContext::AddCursorHandler() {
DCHECK(!main_menu_cursor_handler_);
main_menu_cursor_handler_ =
std::make_unique<GameDashboardMainMenuCursorHandler>(this);
game_window_->AddPreTargetHandler(main_menu_cursor_handler_.get());
}
void GameDashboardContext::RemoveCursorHandler() {
if (main_menu_cursor_handler_) {
game_window_->RemovePreTargetHandler(main_menu_cursor_handler_.get());
main_menu_cursor_handler_.reset();
}
}
void GameDashboardContext::CreateAndAddGameDashboardButtonWidget() {
auto game_dashboard_button = std::make_unique<GameDashboardButton>(
base::BindRepeating(&GameDashboardContext::OnGameDashboardButtonPressed,
weak_ptr_factory_.GetWeakPtr()));
DCHECK(!game_dashboard_button_);
game_dashboard_button_ = game_dashboard_button.get();
// Allow the Game Dashboard button to be activatable so that it can be
// focusable during tab navigation.
game_dashboard_button_widget_ = CreateTransientChildWidget(
game_window_, "GameDashboardButton", std::move(game_dashboard_button));
// Add observer after `game_dashboard_button_widget_` is created because the
// observation is to update `game_dashboard_button_widget_` bounds.
game_dashboard_button_->AddObserver(this);
DCHECK_EQ(
game_window_,
wm::GetTransientParent(game_dashboard_button_widget_->GetNativeWindow()));
UpdateGameDashboardButtonWidgetBounds();
if (game_dashboard_utils::ShouldEnableFeatures()) {
SetGameDashboardButtonVisibility(/*visible=*/true);
}
}
void GameDashboardContext::UpdateGameDashboardButtonWidgetBounds() {
DCHECK(game_dashboard_button_widget_)
<< "Game Dashboard button doesn't exist. Make sure to call Initialize() "
"before trying to use the context.";
auto preferred_size =
game_dashboard_button_widget_->GetContentsView()->GetPreferredSize();
gfx::Point origin = game_window_->GetBoundsInScreen().top_center();
const int frame_header_height =
game_dashboard_utils::GetFrameHeaderHeight(game_window_);
if (frame_header_height == 0) {
VLOG(1) << "No frame header height. Not updating main menu widget bounds.";
return;
}
// Position the button in the top center of the `FrameHeader`.
origin.set_x(origin.x() - preferred_size.width() / 2);
origin.set_y(origin.y() + kGameDashboardButtonVerticalPaddingDp);
preferred_size.set_height(frame_header_height -
2 * kGameDashboardButtonVerticalPaddingDp);
game_dashboard_button_widget_->SetBounds(gfx::Rect(origin, preferred_size));
}
void GameDashboardContext::OnGameDashboardButtonPressed() {
// Close the welcome dialog if it's open when a user opens the main menu view.
CloseWelcomeDialogIfAny();
ToggleMainMenu(GameDashboardMainMenuToggleMethod::kGameDashboardButton);
}
void GameDashboardContext::MaybeShowWelcomeDialog() {
// If the welcome dialog should not be shown, or the Game Dashboard feature is
// disabled, do not show the welcome dialog.
if (!show_welcome_dialog_ || !game_dashboard_utils::ShouldEnableFeatures()) {
MaybeShowToolbar();
return;
}
DCHECK(!welcome_dialog_widget_);
show_welcome_dialog_ = false;
auto view = std::make_unique<GameDashboardWelcomeDialog>();
GameDashboardWelcomeDialog* welcome_dialog_view = view.get();
// Activatable for accessibility screen reader.
welcome_dialog_widget_ = CreateTransientChildWidget(
game_window_, "GameDashboardWelcomeDialog", std::move(view),
/*activatable=*/views::Widget::InitParams::Activatable::kDefault);
welcome_dialog_widget_->AddObserver(this);
MaybeUpdateWelcomeDialogBounds();
welcome_dialog_widget_->SetVisibilityAnimationTransition(
views::Widget::ANIMATE_BOTH);
welcome_dialog_widget_->SetVisibilityAnimationDuration(
base::Milliseconds(700));
welcome_dialog_widget_->ShowInactive();
welcome_dialog_view->StartTimer(
base::BindOnce(&GameDashboardContext::OnWelcomeDialogTimerCompleted,
weak_ptr_factory_.GetWeakPtr()));
// Once the dialog is shown, add an announcement for screen readers since the
// dialog only shows for a short amount of time.
welcome_dialog_view->AnnounceForAccessibility();
}
void GameDashboardContext::MaybeUpdateWelcomeDialogBounds() {
if (!welcome_dialog_widget_) {
return;
}
const gfx::Rect game_bounds = game_window_->GetBoundsInScreen();
const gfx::Size preferred_size =
welcome_dialog_widget_->GetContentsView()->GetPreferredSize();
const int frame_header_height =
game_dashboard_utils::GetFrameHeaderHeight(game_window_);
int origin_x;
if (game_bounds.width() > kMaxCenteredWelcomeDialogWidth) {
// Place welcome dialog right aligned in the game window.
origin_x = game_bounds.right() - game_dashboard::kWelcomeDialogEdgePadding -
preferred_size.width();
} else {
// Place welcome dialog centered in the game window.
origin_x =
game_bounds.x() + (game_bounds.width() - preferred_size.width()) / 2;
}
welcome_dialog_widget_->SetBounds(gfx::Rect(
gfx::Point(origin_x, game_bounds.y() +
game_dashboard::kWelcomeDialogEdgePadding +
frame_header_height),
preferred_size));
}
const gfx::Rect GameDashboardContext::CalculateToolbarWidgetBounds() {
const gfx::Rect game_bounds = game_window_->GetBoundsInScreen();
const gfx::Size preferred_size =
toolbar_widget_->GetContentsView()->GetPreferredSize();
const int frame_header_height =
game_dashboard_utils::GetFrameHeaderHeight(game_window_);
gfx::Point origin;
switch (toolbar_snap_location_) {
case GameDashboardToolbarSnapLocation::kTopRight:
origin =
gfx::Point(game_bounds.right() - game_dashboard::kToolbarEdgePadding -
preferred_size.width(),
game_bounds.y() + game_dashboard::kToolbarEdgePadding +
frame_header_height);
break;
case GameDashboardToolbarSnapLocation::kTopLeft:
origin =
gfx::Point(game_bounds.x() + game_dashboard::kToolbarEdgePadding,
game_bounds.y() + game_dashboard::kToolbarEdgePadding +
frame_header_height);
break;
case GameDashboardToolbarSnapLocation::kBottomRight:
origin = gfx::Point(
game_bounds.right() - game_dashboard::kToolbarEdgePadding -
preferred_size.width(),
game_bounds.bottom() - game_dashboard::kToolbarEdgePadding -
preferred_size.height());
break;
case GameDashboardToolbarSnapLocation::kBottomLeft:
origin = gfx::Point(game_bounds.x() + game_dashboard::kToolbarEdgePadding,
game_bounds.bottom() -
game_dashboard::kToolbarEdgePadding -
preferred_size.height());
break;
}
return gfx::Rect(origin, preferred_size);
}
void GameDashboardContext::AnimateToolbarWidgetBoundsChange(
const gfx::Rect& target_screen_bounds) {
DCHECK(toolbar_widget_);
auto* toolbar_window = toolbar_widget_->GetNativeWindow();
const auto current_bounds = toolbar_window->GetBoundsInScreen();
if (target_screen_bounds == current_bounds) {
return;
}
toolbar_widget_->SetBounds(target_screen_bounds);
const auto transform = gfx::Transform::MakeTranslation(
current_bounds.CenterPoint() - target_screen_bounds.CenterPoint());
ui::Layer* layer = toolbar_window->layer();
layer->SetTransform(transform);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(kToolbarBoundsChangeAnimationDuration)
.SetTransform(layer, gfx::Transform(), gfx::Tween::ACCEL_0_80_DECEL_80);
}
void GameDashboardContext::MaybeShowToolbar() {
if (game_dashboard_utils::ShouldShowToolbar() && !toolbar_widget_ &&
!display::Screen::GetScreen()->InTabletMode()) {
// Show the toolbar, if it's not already showing.
ToggleToolbar();
DCHECK(toolbar_widget_);
}
}
void GameDashboardContext::OnUpdateRecordingTimer() {
DCHECK(!recording_start_time_.is_null());
const base::TimeDelta delta = base::Time::Now() - recording_start_time_;
std::u16string duration;
if (!base::TimeDurationFormatWithSeconds(
delta, base::DurationFormatWidth::DURATION_WIDTH_NUMERIC,
&duration)) {
VLOG(1) << "Error converting the duration to a string: " << duration;
return;
}
// Remove the leading `0:` for durations less than an hour.
if (delta < base::Hours(1)) {
base::ReplaceFirstSubstringAfterOffset(&duration, 0, u"0:", u"");
}
recording_duration_ = duration;
game_dashboard_button_->UpdateRecordingDuration(duration);
if (main_menu_view_) {
main_menu_view_->UpdateRecordingDuration(duration);
}
}
void GameDashboardContext::CloseWelcomeDialogIfAny(bool show_toolbar) {
if (welcome_dialog_widget_) {
welcome_dialog_widget_->SetVisibilityAnimationDuration(
base::Milliseconds(300));
welcome_dialog_widget_->Hide();
welcome_dialog_widget_->RemoveObserver(this);
welcome_dialog_widget_.reset();
if (show_toolbar) {
MaybeShowToolbar();
}
}
}
void GameDashboardContext::OnWelcomeDialogTimerCompleted() {
CloseWelcomeDialogIfAny();
}
void GameDashboardContext::UpdateOnMainMenuClosed() {
DCHECK(main_menu_view_);
DCHECK(!main_menu_widget_.get());
RemoveCursorHandler();
main_menu_view_ = nullptr;
// Update the accessibility tree since `main_menu_widget_` has been destroyed.
game_dashboard_utils::UpdateAccessibilityTree(GetTraversableWidgets());
game_dashboard_button_->SetToggled(false);
}
void GameDashboardContext::EnsureMainMenuAboveToolbar() {
if (main_menu_widget_ && toolbar_widget_) {
main_menu_widget_->StackAboveWidget(toolbar_widget_.get());
}
}
bool GameDashboardContext::ShouldNavigateToNewWidget(
const ui::KeyEvent* event) const {
// Tab navigation between Game Dashboard sibling widgets is only supported
// when the GD button is enabled.
if (!game_dashboard_button_->GetEnabled() ||
event->type() != ui::EventType::kKeyPressed ||
event->key_code() != ui::VKEY_TAB) {
return false;
}
if (auto* target_widget = views::Widget::GetWidgetForNativeWindow(
static_cast<aura::Window*>(event->target()))) {
if (auto* focus_manager = target_widget->GetFocusManager()) {
// If `GetNextFocusableView` returns null, navigation has reached the last
// focusable view in the given direction.
return !(focus_manager->GetNextFocusableView(
/*starting_view=*/focus_manager->GetFocusedView(),
/*starting_widget=*/target_widget,
/*reverse=*/event->IsShiftDown(),
/*dont_loop=*/true));
}
}
return false;
}
std::vector<views::Widget*> GameDashboardContext::GetTraversableWidgets()
const {
std::vector<views::Widget*> widget_list;
widget_list.emplace_back(game_dashboard_button_widget_.get());
if (main_menu_widget_) {
widget_list.emplace_back(main_menu_widget_.get());
}
if (toolbar_widget_) {
widget_list.emplace_back(toolbar_widget_.get());
}
if (widget_list.size() == 1) {
// If the toolbar and main menu widgets don't exist but focus is placed on
// the Game Dashboard button, manually move focus to the game window to
// avoid tab support looping just the Game Dashboard button.
widget_list.emplace_back(
views::Widget::GetWidgetForNativeWindow(game_window_.get()));
}
return widget_list;
}
void GameDashboardContext::MoveFocus(views::Widget* new_widget,
ui::Event* event,
bool reverse) {
CHECK(new_widget) << "Cannot move focus to a non-existent widget.";
auto* focus_manager = new_widget->GetFocusManager();
DCHECK(focus_manager) << "Cannot move focus without a focus manager";
focus_manager->ClearFocus();
// Avoid having the focus restored to the same view when the parent view
// is refocused.
focus_manager->SetStoredFocusView(nullptr);
focus_manager->AdvanceFocus(reverse);
event->StopPropagation();
event->SetHandled();
}
} // namespace ash