chromium/ash/capture_mode/capture_mode_toast_controller.cc

// 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/capture_mode/capture_mode_toast_controller.h"

#include <memory>

#include "ash/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/toast/system_toast_view.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/rect.h"

namespace ash {

namespace {

constexpr int kToastSpacingFromBar = 8;

// Animation duration for updating the visibility of `capture_toast_widget_`.
constexpr base::TimeDelta kCaptureToastVisibilityChangeDuration =
    base::Milliseconds(200);

// The duration that `capture_toast_widget_` remains visible after been created
// and there are no actions taken, after which the toast widget will be
// dismissed.
constexpr base::TimeDelta kDelayToDismissToast = base::Seconds(6);

std::u16string GetCaptureToastTextOnToastType(
    CaptureToastType capture_toast_type) {
  const int nudge_message_id =
      IDS_ASH_SCREEN_CAPTURE_SHOW_DEMO_TOOLS_USER_NUDGE;

  const int message_id =
      capture_toast_type == CaptureToastType::kCameraPreview
          ? IDS_ASH_SCREEN_CAPTURE_SURFACE_TOO_SMALL_USER_NUDGE
          : nudge_message_id;
  return l10n_util::GetStringUTF16(message_id);
}

// Returns the init params that will be used for the toast widget.
views::Widget::InitParams CreateWidgetParams(aura::Window* parent) {
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_POPUP);
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.parent = parent;
  params.name = "CaptureModeToastWidget";
  params.accept_events = false;
  return params;
}

}  // namespace

CaptureModeToastController::CaptureModeToastController(
    CaptureModeSession* session)
    : capture_session_(session) {}

CaptureModeToastController::~CaptureModeToastController() {
  // Widget needs to be closed immediately so it does not show in the
  // screenshot.
  if (capture_toast_widget_) {
    capture_toast_widget_->CloseNow();
  }
}

void CaptureModeToastController::ShowCaptureToast(
    CaptureToastType capture_toast_type) {
  current_toast_type_ = capture_toast_type;
  const std::u16string capture_toast_text =
      GetCaptureToastTextOnToastType(capture_toast_type);

  if (!capture_toast_widget_) {
    BuildCaptureToastWidget(capture_toast_text);
  } else {
    toast_contents_view_->SetText(capture_toast_text);
  }

  capture_mode_util::TriggerAccessibilityAlertSoon(
      base::UTF16ToUTF8(capture_toast_text));

  MaybeRepositionCaptureToast();
  const bool did_visibility_change = capture_mode_util::SetWidgetVisibility(
      capture_toast_widget_.get(), /*target_visibility=*/true,
      capture_mode_util::AnimationParams{kCaptureToastVisibilityChangeDuration,
                                         gfx::Tween::FAST_OUT_SLOW_IN,
                                         /*apply_scale_up_animation=*/false});

  // Only if the capture toast type is the `kCameraPreview`, the capture toast
  // should be auto dismissed after `kDelayToDismissToast`.
  if (did_visibility_change &&
      capture_toast_type == CaptureToastType::kCameraPreview) {
    capture_toast_dismiss_timer_.Start(
        FROM_HERE, kDelayToDismissToast,
        base::BindOnce(&CaptureModeToastController::MaybeDismissCaptureToast,
                       base::Unretained(this), capture_toast_type,
                       /*animate=*/true));
  }
}

void CaptureModeToastController::MaybeDismissCaptureToast(
    CaptureToastType capture_toast_type,
    bool animate) {
  if (!current_toast_type_) {
    DCHECK(!capture_toast_widget_ ||
           !capture_mode_util::GetWidgetCurrentVisibility(
               capture_toast_widget_.get()));
    return;
  }

  if (!capture_toast_widget_) {
    DCHECK(!current_toast_type_);
    return;
  }

  if (capture_toast_type != current_toast_type_)
    return;

  capture_toast_dismiss_timer_.Stop();

  current_toast_type_.reset();
  if (animate) {
    capture_mode_util::SetWidgetVisibility(
        capture_toast_widget_.get(), /*target_visibility=*/false,
        capture_mode_util::AnimationParams{
            kCaptureToastVisibilityChangeDuration, gfx::Tween::FAST_OUT_SLOW_IN,
            /*apply_scale_up_animation=*/false});
    return;
  }

  capture_toast_widget_->Hide();
}

void CaptureModeToastController::DismissCurrentToastIfAny() {
  if (current_toast_type_)
    MaybeDismissCaptureToast(*current_toast_type_, /*animate=*/false);
}

void CaptureModeToastController::MaybeRepositionCaptureToast() {
  if (!capture_toast_widget_)
    return;

  auto* parent_window = capture_session_->current_root()->GetChildById(
      kShellWindowId_MenuContainer);

  if (capture_toast_widget_->GetNativeWindow()->parent() != parent_window) {
    parent_window->AddChild(capture_toast_widget_->GetNativeWindow());
    auto* layer = capture_toast_widget_->GetLayer();
    // Any ongoing opacity animation should be committed when we reparent the
    // toast in case that the residual animation continues in the old position
    // after been repositioned to a new bounds.
    layer->SetOpacity(layer->GetTargetOpacity());
  }

  capture_toast_widget_->SetBounds(CalculateToastWidgetBoundsInScreen());
}

ui::Layer* CaptureModeToastController::MaybeGetToastLayer() {
  return capture_toast_widget_ ? capture_toast_widget_->GetLayer() : nullptr;
}

void CaptureModeToastController::OnWidgetDestroying(views::Widget* widget) {
  toast_contents_view_ = nullptr;
  if (capture_toast_widget_) {
    capture_toast_widget_->RemoveObserver(this);
  }
}

void CaptureModeToastController::BuildCaptureToastWidget(
    const std::u16string& text) {
  // Create the widget before init it to ensure that the `capture_toast_widget_`
  // is available when the window gets added to the parent container.
  capture_toast_widget_ = std::make_unique<views::Widget>();
  capture_toast_widget_->Init(
      CreateWidgetParams(capture_session_->current_root()->GetChildById(
          kShellWindowId_MenuContainer)));
  capture_toast_widget_->AddObserver(this);
  toast_contents_view_ = capture_toast_widget_->SetContentsView(
      std::make_unique<SystemToastView>(text));

  // We animate the `capture_toast_widget_` explicitly in `ShowCaptureToast()`
  // and `MaybeDismissCaptureToast()`. Any default visibility animations added
  // by the widget's window should be disabled.
  capture_toast_widget_->SetVisibilityAnimationTransition(
      views::Widget::ANIMATE_NONE);
  const auto toast_bounds_in_screen = CalculateToastWidgetBoundsInScreen();
  capture_toast_widget_->SetBounds(toast_bounds_in_screen);
  toast_contents_view_->SetPaintToLayer();
  toast_contents_view_->layer()->SetRoundedCornerRadius(
      gfx::RoundedCornersF(toast_bounds_in_screen.height() / 2.f));
  capture_toast_widget_->Show();

  // The widget is created initially with 0 opacity, and will animate to be
  // fully visible when `ShowCaptureToast` is called.
  capture_toast_widget_->GetLayer()->SetOpacity(0);
}

gfx::Rect CaptureModeToastController::CalculateToastWidgetBoundsInScreen()
    const {
  DCHECK(toast_contents_view_);

  gfx::Rect bounds;
  const auto preferred_size = toast_contents_view_->GetPreferredSize();
  bounds = gfx::Rect(preferred_size);

  // Align the centers of the capture mode bar and the toast horizontally.
  const auto bar_widget_bounds_in_screen =
      capture_session_->GetCaptureModeBarWidget()->GetWindowBoundsInScreen();
  bounds.set_x(bar_widget_bounds_in_screen.CenterPoint().x() -
               preferred_size.width() / 2);
  bounds.set_y(bar_widget_bounds_in_screen.y() - bounds.height() -
               kToastSpacingFromBar);
  return bounds;
}

}  // namespace ash