| // Copyright 2022 The Chromium Authors. All rights reserved. |
| // 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/style/ash_color_provider.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/animation/animation_builder.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/label.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // Specs for `capture_toast_widget_`. |
| constexpr int kToastSpacingFromBar = 8; |
| constexpr int kToastDefaultHeight = 36; |
| constexpr int kToastVerticalPadding = 8; |
| constexpr int kToastHorizontalPadding = 16; |
| constexpr int kToastBorderThickness = 1; |
| |
| // Animation duration for updating the visibility of `capture_toast_widget_`. |
| constexpr base::TimeDelta kCaptureToastVisibilityChangeDuration = |
| base::Milliseconds(200); |
| |
| // The duration that `capture_toast_widget_` will remain visible after it's |
| // created when there are no actions taken. After this, the toast widget will be |
| // dismissed. |
| constexpr base::TimeDelta kDelayToDismissToast = base::Seconds(6); |
| |
| std::u16string GetCaptureToastLabelOnToastType( |
| CaptureToastType capture_toast_type) { |
| const int message_id = |
| capture_toast_type == CaptureToastType::kCameraPreview |
| ? IDS_ASH_SCREEN_CAPTURE_SURFACE_TOO_SMALL_USER_NUDGE |
| : (features::IsCaptureModeSelfieCameraEnabled() |
| ? IDS_ASH_SCREEN_CAPTURE_SHOW_CAMERA_USER_NUDGE |
| : IDS_ASH_SCREEN_CAPTURE_FOLDER_SELECTION_USER_NUDGE); |
| 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, |
| const gfx::Rect& bounds) { |
| views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); |
| params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; |
| params.parent = parent; |
| params.bounds = bounds; |
| params.name = "CaptureModeToastWidget"; |
| params.accept_events = false; |
| return params; |
| } |
| |
| } // namespace |
| |
| CaptureModeToastController::CaptureModeToastController( |
| CaptureModeSession* session) |
| : capture_session_(session) {} |
| |
| CaptureModeToastController::~CaptureModeToastController() { |
| 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_label = |
| GetCaptureToastLabelOnToastType(capture_toast_type); |
| |
| if (!capture_toast_widget_) |
| BuildCaptureToastWidget(capture_toast_label); |
| else |
| toast_label_view_->SetText(capture_toast_label); |
| |
| 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, otherwise it doesn't look good. |
| layer->SetOpacity(layer->GetTargetOpacity()); |
| } |
| |
| capture_toast_widget_->SetBounds(CalculateToastWidgetScreenBounds()); |
| } |
| |
| ui::Layer* CaptureModeToastController::MaybeGetToastLayer() { |
| return capture_toast_widget_ ? capture_toast_widget_->GetLayer() : nullptr; |
| } |
| |
| void CaptureModeToastController::BuildCaptureToastWidget( |
| const std::u16string& label) { |
| // Create the widget before init it to ensure when the window gets added to |
| // the parent container, `capture_toast_widget_` is already available. |
| capture_toast_widget_ = std::make_unique<views::Widget>(); |
| const gfx::Rect toast_widget_screen_bounds = |
| CalculateToastWidgetScreenBounds(); |
| capture_toast_widget_->Init( |
| CreateWidgetParams(capture_session_->current_root()->GetChildById( |
| kShellWindowId_MenuContainer), |
| toast_widget_screen_bounds)); |
| |
| // We animate the 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); |
| |
| toast_label_view_ = capture_toast_widget_->SetContentsView( |
| std::make_unique<views::Label>(label)); |
| toast_label_view_->SetMultiLine(true); |
| auto* color_provider = AshColorProvider::Get(); |
| SkColor background_color = color_provider->GetBaseLayerColor( |
| AshColorProvider::BaseLayerType::kTransparent80); |
| toast_label_view_->SetBackground( |
| views::CreateSolidBackground(background_color)); |
| const float toast_corner_radius = toast_widget_screen_bounds.height() / 2.f; |
| toast_label_view_->SetBorder(views::CreateRoundedRectBorder( |
| kToastBorderThickness, toast_corner_radius, |
| color_provider->GetControlsLayerColor( |
| AshColorProvider::ControlsLayerType::kHighlightColor1))); |
| toast_label_view_->SetAutoColorReadabilityEnabled(false); |
| const SkColor text_color = color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kTextColorPrimary); |
| toast_label_view_->SetEnabledColor(text_color); |
| toast_label_view_->SetHorizontalAlignment(gfx::ALIGN_CENTER); |
| toast_label_view_->SetVerticalAlignment(gfx::ALIGN_MIDDLE); |
| |
| toast_label_view_->SetPaintToLayer(); |
| auto* label_layer = toast_label_view_->layer(); |
| label_layer->SetFillsBoundsOpaquely(false); |
| label_layer->SetRoundedCornerRadius( |
| gfx::RoundedCornersF(toast_corner_radius)); |
| label_layer->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma); |
| label_layer->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality); |
| |
| // The widget is created initially with 0 opacity, and will animate to be |
| // fully visible when `ShowCaptureToast` is called. |
| capture_toast_widget_->Show(); |
| auto* widget_layer = capture_toast_widget_->GetLayer(); |
| widget_layer->SetOpacity(0); |
| } |
| |
| gfx::Rect CaptureModeToastController::CalculateToastWidgetScreenBounds() const { |
| const auto bar_widget_bounds_in_screen = |
| capture_session_->capture_mode_bar_widget()->GetWindowBoundsInScreen(); |
| |
| auto bounds = bar_widget_bounds_in_screen; |
| if (toast_label_view_) { |
| const auto preferred_size = toast_label_view_->GetPreferredSize(); |
| // We don't want the toast width to go beyond the capture bar width, but if |
| // it can use a smaller width, then we align the horizontal centers of the |
| // bar the toast together. |
| const int fitted_width = |
| preferred_size.width() + 2 * kToastHorizontalPadding; |
| if (fitted_width < bar_widget_bounds_in_screen.width()) { |
| bounds.set_width(fitted_width); |
| bounds.set_x(bar_widget_bounds_in_screen.CenterPoint().x() - |
| fitted_width / 2); |
| } |
| // Note that the toast is allowed to have multiple lines if the width |
| // doesn't fit the contents. |
| bounds.set_height(toast_label_view_->GetHeightForWidth(bounds.width()) + |
| 2 * kToastVerticalPadding); |
| } else { |
| // The content view hasn't been created yet, so we use a default height. |
| // Calling Reposition() after the widget has been initialization will fix |
| // any wrong bounds. |
| bounds.set_height(kToastDefaultHeight); |
| } |
| |
| bounds.set_y(bar_widget_bounds_in_screen.y() - bounds.height() - |
| kToastSpacingFromBar); |
| |
| return bounds; |
| } |
| |
| } // namespace ash |