| // 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) { |
| return capture_toast_type == CaptureToastType::kCameraPreview |
| ? l10n_util::GetStringUTF16( |
| IDS_ASH_SCREEN_CAPTURE_SURFACE_TOO_SMALL_USER_NUDGE) |
| : l10n_util::GetStringUTF16(IDS_ASH_SUNFISH_EDUCATE_TOAST_MESSAGE); |
| } |
| |
| // 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 |