chromium/ash/game_dashboard/game_dashboard_context.cc

// 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