0

capture_selfie_cam: Show a toast when capture surface can't fit preview

This CL implemented the mechanism to show a toast when the capture
surface becomes too small to fit the camera preview to notify users.
The toast will be dismissed immedistely when there's an action taken by
the user. Or it will be auto dismissed after 6 seconds when there're no
actions taken.

Fixed: 1306563
Test: Manual, added unit-tests
Change-Id: I5545544a0248cbb86e9efa7839b28b49e552d577
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3624547
Commit-Queue: Connie Xu <conniekxu@chromium.org>
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1002394}
This commit is contained in:
conniekxu
2022-05-11 23:42:58 +00:00
committed by Chromium LUCI CQ
parent 2c0d22e8ab
commit a5ce1260b8
19 changed files with 955 additions and 283 deletions

@ -290,6 +290,8 @@ component("ash") {
"capture_mode/capture_mode_source_view.cc",
"capture_mode/capture_mode_source_view.h",
"capture_mode/capture_mode_test_api.cc",
"capture_mode/capture_mode_toast_controller.cc",
"capture_mode/capture_mode_toast_controller.h",
"capture_mode/capture_mode_toggle_button.cc",
"capture_mode/capture_mode_toggle_button.h",
"capture_mode/capture_mode_type_view.cc",

@ -4007,6 +4007,9 @@ Here are some things you can try to get started.
<message name="IDS_ASH_SCREEN_CAPTURE_SHOW_CAMERA_USER_NUDGE" desc="The message shown in a toast widget to nudge the user and alert them to check out the new settings that allow them to show camera during video recording.">
You can now record yourself and your screen at the same time
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_SURFACE_TOO_SMALL_USER_NUDGE" desc="The message shown in a toast widget to alert the user that the current capture surface is too small to show the camera preview.">
Region is too small to fit camera
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_TOOLTIP_COLLAPSE_SELFIE_CAMERA" desc="Tooltip of the collapse resize button for screen capture selfie camera.">
Collapse camera
</message>

@ -0,0 +1 @@
20b537efce6190a3c41db3f984008536ebf385b4

@ -71,8 +71,6 @@ constexpr base::TimeDelta kCameraPreviewFadeOutDuration =
constexpr base::TimeDelta kCameraPreviewFadeInDuration =
base::Milliseconds(150);
constexpr float kCameraPreviewScaleUpFactor = 0.8f;
// Defines a map type to map a camera model ID (or display name) to the number
// of cameras of that model that are currently connected.
using ModelIdToCountMap = std::map<std::string, int>;
@ -349,6 +347,14 @@ class CameraPreviewTargeter : public aura::WindowTargeter {
aura::Window* const camera_preview_window_;
};
capture_mode_util::AnimationParams BuildCameraVisibilityAnimationParams(
bool target_visibility,
bool apply_scale_up_animation) {
return {target_visibility ? kCameraPreviewFadeInDuration
: kCameraPreviewFadeOutDuration,
gfx::Tween::LINEAR, apply_scale_up_animation};
}
} // namespace
// -----------------------------------------------------------------------------
@ -547,12 +553,22 @@ void CaptureModeCameraController::MaybeUpdatePreviewWidget(bool animate) {
CalculatePreviewWidgetTargetBounds(confine_bounds, size_specs.size),
animate);
const bool did_visibility_change = SetCameraPreviewVisibility(
size_specs.should_be_visible, should_animate_visibility);
const bool did_visibility_change = capture_mode_util::SetWidgetVisibility(
camera_preview_widget_.get(), size_specs.should_be_visible,
!should_animate_visibility
? absl::nullopt
: absl::make_optional<capture_mode_util::AnimationParams>(
BuildCameraVisibilityAnimationParams(
/*target_visibility=*/size_specs.should_be_visible,
/*apply_scale_up_animation=*/is_first_bounds_update_)));
if (controller->IsActive() && (did_visibility_change || did_bounds_change)) {
if (controller->IsActive() && !controller->is_recording_in_progress()) {
controller->capture_mode_session()
->OnCameraPreviewBoundsOrVisibilityChanged();
->OnCameraPreviewBoundsOrVisibilityChanged(
/*capture_surface_became_too_small=*/size_specs
.is_surface_too_small,
/*did_bounds_or_visibility_change=*/did_visibility_change ||
did_bounds_change);
}
if (did_bounds_change) {
@ -1017,103 +1033,6 @@ void CaptureModeCameraController::RunPostRefreshCameraPreview(
}
}
bool CaptureModeCameraController::SetCameraPreviewVisibility(
bool target_visibility,
bool animate) {
DCHECK(camera_preview_widget_);
// Note that we use `aura::Window::TargetVisibility()` rather than
// `views::Widget::IsVisible()` (which in turn uses
// `aura::Window::IsVisible()`). The reason is because the latter takes into
// account whether window's layer is drawn or not. We want to calculate the
// current visibility only based on the actual visibility of the window
// itself, so that we can correctly compare it against `target_visibility`.
// Note that the preview may be a child of the unparented container (which is
// always hidden), yet the preview's window is shown.
const bool current_visibility =
camera_preview_widget_->GetNativeWindow()->TargetVisibility() &&
camera_preview_widget_->GetLayer()->GetTargetOpacity() > 0.f;
if (target_visibility == current_visibility)
return false;
if (animate) {
if (target_visibility)
FadeInCameraPreview();
else
FadeOutCameraPreview();
} else {
if (target_visibility)
camera_preview_widget_->Show();
else
camera_preview_widget_->Hide();
}
capture_mode_util::TriggerAccessibilityAlertSoon(
current_visibility ? IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_HIDDEN
: IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_ON);
return true;
}
void CaptureModeCameraController::FadeInCameraPreview() {
DCHECK(camera_preview_widget_);
auto* layer = camera_preview_widget_->GetLayer();
DCHECK(!camera_preview_widget_->GetNativeWindow()->TargetVisibility() ||
layer->GetTargetOpacity() < 1.f);
if (!camera_preview_widget_->GetNativeWindow()->TargetVisibility())
camera_preview_widget_->Show();
if (layer->opacity() == 1.f)
layer->SetOpacity(0.f);
if (is_first_bounds_update_) {
layer->SetTransform(capture_mode_util::GetScaleTransformAboutCenter(
layer, kCameraPreviewScaleUpFactor));
}
views::AnimationBuilder builder;
auto& animation_sequence_block =
builder
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(kCameraPreviewFadeInDuration)
.SetOpacity(layer, 1.f, gfx::Tween::LINEAR);
// We should only set transform here if `is_first_bounds_update_` is true,
// otherwise, it may mess up with the snap animation in
// `SetCameraPreviewBounds`.
if (is_first_bounds_update_) {
animation_sequence_block.SetTransform(layer, gfx::Transform(),
gfx::Tween::ACCEL_20_DECEL_100);
}
}
void CaptureModeCameraController::FadeOutCameraPreview() {
DCHECK(camera_preview_widget_);
DCHECK(camera_preview_widget_->GetNativeWindow()->TargetVisibility());
auto* layer = camera_preview_widget_->GetLayer();
DCHECK_EQ(layer->GetTargetOpacity(), 1.f);
views::AnimationBuilder()
.OnEnded(base::BindOnce(
[](base::WeakPtr<CaptureModeCameraController> controller) {
if (!controller || !controller->camera_preview_widget_)
return;
// Please notice, the order matters here. If we set the layer's
// opacity back to 1.f before calling `Hide`, flickering can be
// seen.
controller->camera_preview_widget_->Hide();
controller->camera_preview_widget_->GetLayer()->SetOpacity(1.f);
},
weak_ptr_factory_.GetWeakPtr()))
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(kCameraPreviewFadeOutDuration)
.SetOpacity(layer, 0.f, gfx::Tween::LINEAR);
}
bool CaptureModeCameraController::SetCameraPreviewBounds(
const gfx::Rect& target_bounds,
bool animate) {

@ -314,16 +314,6 @@ class ASH_EXPORT CaptureModeCameraController
// panels.
void RunPostRefreshCameraPreview(bool was_preview_visible_before);
// Sets the visibility of the camera preview to the given `target_visibility`
// and returns true only if the `target_visibility` is different than the
// current.
bool SetCameraPreviewVisibility(bool target_visibility, bool animate);
// Fades in or out the `camera_preview_widget_` and updates its visibility
// accordingly.
void FadeInCameraPreview();
void FadeOutCameraPreview();
// Sets the given `target_bounds` on the camera preview widget, potentially
// animating to it if `animate` is true. Returns true if the bounds actually
// changed from the current.

@ -7,6 +7,7 @@
#include "ash/accessibility/scoped_a11y_override_window_setter.h"
#include "ash/capture_mode/capture_mode_constants.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
@ -124,7 +125,11 @@ CameraPreviewView::CameraPreviewView(
UpdateResizeButtonTooltip();
}
CameraPreviewView::~CameraPreviewView() = default;
CameraPreviewView::~CameraPreviewView() {
auto* controller = CaptureModeController::Get();
if (controller->IsActive() && !controller->is_recording_in_progress())
controller->capture_mode_session()->OnCameraPreviewDestroyed();
}
void CameraPreviewView::SetIsCollapsible(bool value) {
if (value != is_collapsible_) {

@ -17,6 +17,7 @@
#include "ash/capture_mode/capture_mode_settings_test_api.h"
#include "ash/capture_mode/capture_mode_settings_view.h"
#include "ash/capture_mode/capture_mode_test_util.h"
#include "ash/capture_mode/capture_mode_toast_controller.h"
#include "ash/capture_mode/capture_mode_toggle_button.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/capture_mode_util.h"
@ -162,6 +163,12 @@ class ViewVisibilityChangeWaiter : public views::ViewObserver {
base::RunLoop wait_loop_;
};
gfx::Rect GetTooSmallToFitCameraRegion() {
return {100, 100,
capture_mode::kMinCaptureSurfaceShortSideLengthForVisibleCamera - 1,
capture_mode::kMinCaptureSurfaceShortSideLengthForVisibleCamera - 1};
}
} // namespace
class CaptureModeCameraTest : public AshTestBase {
@ -2535,6 +2542,309 @@ TEST_F(CaptureModeCameraTest, RecordingCameraPositionOnStartHistogramTest) {
}
}
TEST_F(CaptureModeCameraTest, ToastVisibilityChangeOnCaptureRegionUpdated) {
UpdateDisplay("800x600");
auto* controller =
StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
auto* capture_session = controller->capture_mode_session();
auto* camera_controller = GetCameraController();
auto* capture_toast_controller = capture_session->capture_toast_controller();
AddDefaultCamera();
camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
// Set capture region big enough to fit the camera preview. Verify the
// current capture toast is `kUserNudge`.
const gfx::Rect capture_region(100, 100, 300, 300);
SelectCaptureRegion(capture_region);
auto* capture_toast_widget = capture_toast_controller->capture_toast_widget();
EXPECT_TRUE(capture_toast_widget);
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kUserNudge);
// Update capture region small enough to not fit the camera preview. Verify
// that the capture toast is updated to `kCameraPreview` and the user nudge is
// dismissed forever.
const int delta_x =
capture_mode::kMinCaptureSurfaceShortSideLengthForVisibleCamera - 30 -
capture_region.width();
const int delta_y =
capture_mode::kMinCaptureSurfaceShortSideLengthForVisibleCamera - 30 -
capture_region.height();
const gfx::Vector2d delta(delta_x, delta_y);
auto* event_generator = GetEventGenerator();
event_generator->set_current_screen_location(capture_region.bottom_right());
event_generator->PressLeftButton();
// Verify that when drag starts, the capture toast is hidden.
EXPECT_FALSE(capture_toast_widget->IsVisible());
event_generator->MoveMouseTo(capture_region.bottom_right() + delta);
event_generator->ReleaseLeftButton();
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
EXPECT_TRUE(capture_toast_widget->IsVisible());
EXPECT_FALSE(GetUserNudgeController());
// Start dragging the capture region again to update it, but make it still
// small enough to not fit the camera preview. Verify at the beginning of the
// drag, preview toast is hidden. After the drag is released, preview toast is
// shown again.
const gfx::Vector2d delta1(delta_x + 10, delta_y + 10);
event_generator->set_current_screen_location(capture_region.bottom_right());
event_generator->PressLeftButton();
// Verify that when drag starts, the capture toast is hidden.
EXPECT_FALSE(capture_toast_widget->IsVisible());
event_generator->MoveMouseTo(capture_region.bottom_right() + delta1);
event_generator->ReleaseLeftButton();
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
EXPECT_TRUE(capture_toast_widget->IsVisible());
// Update capture region big enough to show the camera preview. Verify the
// preview toast is hidden.
event_generator->set_current_screen_location(capture_region.origin());
event_generator->PressLeftButton();
// Verify that when drag starts, the capture toast is hidden.
EXPECT_FALSE(capture_toast_widget->IsVisible());
event_generator->MoveMouseTo(capture_region.bottom_right());
event_generator->ReleaseLeftButton();
// Verify that since the capture toast is dismissed, current toast type is
// reset.
EXPECT_FALSE(capture_toast_controller->current_toast_type());
EXPECT_FALSE(capture_toast_widget->IsVisible());
}
// Tests that the capture toast will be faded out on time out when there are no
// actions taken.
TEST_F(CaptureModeCameraTest, ToastVisibilityChangeOnTimeOut) {
UpdateDisplay("800x600");
auto* controller =
StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
auto* capture_session = controller->capture_mode_session();
auto* camera_controller = GetCameraController();
auto* capture_toast_controller = capture_session->capture_toast_controller();
AddDefaultCamera();
camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
// Set capture region small enough to not fit the camera preview. Verify the
// current capture toast is `kCameraPreview`.
const gfx::Rect capture_region = GetTooSmallToFitCameraRegion();
SelectCaptureRegion(capture_region);
auto* capture_toast_widget = capture_toast_controller->capture_toast_widget();
EXPECT_TRUE(capture_toast_widget->IsVisible());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
// Verify the timer is running after the toast is shown and when the timer is
// fired, the capture toast is hidden.
base::OneShotTimer* timer =
capture_toast_controller->capture_toast_dismiss_timer_for_test();
EXPECT_TRUE(timer->IsRunning());
timer->FireNow();
EXPECT_FALSE(capture_toast_widget->IsVisible());
}
TEST_F(CaptureModeCameraTest, ToastVisibilityChangeOnSettingsMenuOpen) {
UpdateDisplay("800x600");
auto* controller =
StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
auto* capture_session = controller->capture_mode_session();
auto* camera_controller = GetCameraController();
auto* capture_toast_controller = capture_session->capture_toast_controller();
AddDefaultCamera();
camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
// Set capture region small enough to not fit the camera preview. Verify the
// current capture toast is `kCameraPreview`.
const gfx::Rect capture_region = GetTooSmallToFitCameraRegion();
SelectCaptureRegion(capture_region);
auto* capture_toast_widget = capture_toast_controller->capture_toast_widget();
EXPECT_TRUE(capture_toast_widget->IsVisible());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
// Now open settings menu, verify that preview toast is dismissed immediately
// on settings menu open.
OpenSettingsView();
EXPECT_FALSE(capture_toast_widget->IsVisible());
}
TEST_F(CaptureModeCameraTest, ToastVisibilityChangeOnCaptureRegionMoved) {
UpdateDisplay("800x600");
auto* controller =
StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
auto* capture_session = controller->capture_mode_session();
auto* camera_controller = GetCameraController();
auto* capture_toast_controller = capture_session->capture_toast_controller();
AddDefaultCamera();
camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
// Set capture region small enough to not fit the camera preview. Verify the
// current capture toast is `kCameraPreview`.
const gfx::Rect capture_region = GetTooSmallToFitCameraRegion();
SelectCaptureRegion(capture_region);
auto* capture_toast_widget = capture_toast_controller->capture_toast_widget();
EXPECT_TRUE(capture_toast_widget->IsVisible());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
// Start moving the capture region, verify the preview toast is hidden at the
// beginning of the move and is shown once the move is done.
const gfx::Vector2d delta(20, 20);
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(capture_region.origin() + delta);
event_generator->PressLeftButton();
EXPECT_FALSE(capture_toast_widget->IsVisible());
event_generator->MoveMouseTo(capture_region.CenterPoint());
event_generator->ReleaseLeftButton();
EXPECT_TRUE(capture_toast_widget->IsVisible());
}
// Tests that the preview toast shows correctly when capture mode is turned on
// through quick settings which keeps the configurations) from the previous
// session.
TEST_F(CaptureModeCameraTest, ToastVisibilityChangeOnCaptureModeTurnedOn) {
UpdateDisplay("800x600");
auto* controller =
StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
auto* capture_session = controller->capture_mode_session();
auto* camera_controller = GetCameraController();
auto* capture_toast_controller = capture_session->capture_toast_controller();
AddDefaultCamera();
camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
// Set capture region small enough to not fit the camera preview. Verify the
// current capture toast is `kCameraPreview`.
const gfx::Rect capture_region = GetTooSmallToFitCameraRegion();
SelectCaptureRegion(capture_region);
auto* capture_toast_widget = capture_toast_controller->capture_toast_widget();
EXPECT_TRUE(capture_toast_widget->IsVisible());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
// Close capture mode.
controller->Stop();
// Turn on capture mode again through the quick settings, verify that toast
// preview is visible.
controller->Start(CaptureModeEntryType::kQuickSettings);
capture_session = controller->capture_mode_session();
capture_toast_controller = capture_session->capture_toast_controller();
EXPECT_TRUE(capture_toast_controller->capture_toast_widget()->IsVisible());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
}
TEST_F(CaptureModeCameraTest, ToastVisibilityChangeOnPerformingCapture) {
UpdateDisplay("800x600");
auto* controller =
StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
auto* capture_session = controller->capture_mode_session();
auto* camera_controller = GetCameraController();
auto* capture_toast_controller = capture_session->capture_toast_controller();
AddDefaultCamera();
camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
// Set capture region small enough to not fit the camera preview. Verify the
// current capture toast is `kCameraPreview`.
const gfx::Rect capture_region = GetTooSmallToFitCameraRegion();
SelectCaptureRegion(capture_region);
auto* capture_toast_widget = capture_toast_controller->capture_toast_widget();
EXPECT_TRUE(capture_toast_widget->IsVisible());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
// Perform the screen recording, verify that the capture toast is going to be
// faded out.
controller->PerformCapture();
EXPECT_EQ(capture_toast_widget->GetLayer()->GetTargetOpacity(), 0.f);
}
TEST_F(CaptureModeCameraTest, ToastVisibilityChangeOnMultiDisplays) {
UpdateDisplay("800x700,801+0-800x700");
const gfx::Rect first_display_bounds(0, 0, 800, 700);
const gfx::Rect second_display_bounds(801, 0, 800, 700);
// Set the window's bounds small enough to not fit the camera preview.
window()->SetBoundsInScreen(
gfx::Rect(600, 500, 100, 100),
display::Screen::GetScreen()->GetDisplayNearestWindow(
Shell::GetAllRootWindows()[0]));
// Create a window in the second display and set its bounds small enough to
// not fit the camera preview.
std::unique_ptr<aura::Window> window1(CreateTestWindow());
window1->SetBoundsInScreen(
gfx::Rect(1400, 500, 100, 100),
display::Screen::GetScreen()->GetDisplayNearestWindow(
Shell::GetAllRootWindows()[1]));
// Start the capture session.
auto* controller =
StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo);
auto* camera_controller = GetCameraController();
auto* capture_session = controller->capture_mode_session();
auto* capture_toast_controller = capture_session->capture_toast_controller();
AddDefaultCamera();
camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
// Move the mouse on top of `window` to select it, since it's too small to fit
// the camera preview, verify the preview toast shows and it's on the first
// display.
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(window()->GetBoundsInScreen().CenterPoint());
EXPECT_EQ(capture_session->GetSelectedWindow(), window());
auto* capture_toast_widget = capture_toast_controller->capture_toast_widget();
EXPECT_TRUE(capture_toast_widget->IsVisible());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
first_display_bounds.Contains(
capture_toast_widget->GetWindowBoundsInScreen());
// Now move the mouse to the top of `window1`, since it's also too small to
// fit the camera preview, verify the preview toast still shows. Since
// `window1` is on the second display, verify the preview toast also shows up
// on the second display.
event_generator->MoveMouseTo(window1->GetBoundsInScreen().CenterPoint());
EXPECT_EQ(capture_session->GetSelectedWindow(), window1.get());
EXPECT_TRUE(capture_toast_widget->IsVisible());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kCameraPreview);
second_display_bounds.Contains(
capture_toast_widget->GetWindowBoundsInScreen());
// Move mouse to the outside of `window1`, verify that preview toast is
// dismissed since there's no window selected for now.
event_generator->MoveMouseTo({1300, 500});
EXPECT_FALSE(capture_toast_widget->IsVisible());
// Update the bounds of `window` big enough to fit the camera preview.
window()->SetBoundsInScreen(
gfx::Rect(100, 200, 300, 300),
display::Screen::GetScreen()->GetDisplayNearestWindow(
Shell::GetAllRootWindows()[0]));
// Now move the mouse to the top of `window` again, verify that preview toast
// is not shown, since the window is big enough to show the camera preview.
event_generator->MoveMouseTo(window()->GetBoundsInScreen().CenterPoint());
EXPECT_EQ(capture_session->GetSelectedWindow(), window());
EXPECT_FALSE(capture_toast_widget->IsVisible());
}
class CaptureModeCameraPreviewTest
: public CaptureModeCameraTest,
public testing::WithParamInterface<CaptureModeSource> {

@ -18,6 +18,7 @@
#include "ash/capture_mode/capture_mode_session_focus_cycler.h"
#include "ash/capture_mode/capture_mode_settings_view.h"
#include "ash/capture_mode/capture_mode_toggle_button.h"
#include "ash/capture_mode/capture_mode_type_view.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/capture_window_observer.h"
#include "ash/capture_mode/folder_selection_dialog_controller.h"
@ -39,7 +40,6 @@
#include "base/callback_helpers.h"
#include "base/check.h"
#include "base/memory/ptr_util.h"
#include "base/strings/stringprintf.h"
#include "cc/paint/paint_flags.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/capture_client.h"
@ -198,7 +198,7 @@ bool SetMouseWarpEnabled(bool enable) {
return old_value;
}
// Gets the overlay container inside |root|.
// Gets the menu container inside |root|.
aura::Window* GetParentContainer(aura::Window* root) {
DCHECK(root);
DCHECK(root->IsRootWindow());
@ -600,7 +600,8 @@ CaptureModeSession::CaptureModeSession(CaptureModeController* controller,
magnifier_glass_(kMagnifierParams),
is_in_projector_mode_(projector_mode),
cursor_setter_(std::make_unique<CursorSetter>()),
focus_cycler_(std::make_unique<CaptureModeSessionFocusCycler>(this)) {}
focus_cycler_(std::make_unique<CaptureModeSessionFocusCycler>(this)),
capture_toast_controller_(this) {}
CaptureModeSession::~CaptureModeSession() = default;
@ -682,6 +683,16 @@ void CaptureModeSession::Initialize() {
UpdateFloatingPanelBoundsIfNeeded();
// call `OnCaptureTypeChanged` after capture bar's initialization is done
// instead of in the initialization of the capture mode type view, since
// `OnCaptureTypeChanged` may trigger `ShowCaptureToast` which depends on the
// capture bar.
// Also please note we should call `OnCaptureTypeChanged` in
// `CaptureModeTypeView` instead of `CaptureModeSession`, since this is during
// the initialization of the capture session, the type change is not triggered
// by the user.
capture_mode_bar_view_->capture_type_view()->OnCaptureTypeChanged(
controller_->type());
MaybeCreateUserNudge();
auto* camera_controller = controller_->camera_controller();
@ -694,6 +705,7 @@ void CaptureModeSession::Shutdown() {
aura::Env::GetInstance()->RemovePreTargetHandler(this);
display_observer_.reset();
user_nudge_controller_.reset();
current_root_->RemoveObserver(this);
TabletModeController::Get()->RemoveObserver(this);
if (input_capture_window_) {
@ -847,6 +859,7 @@ void CaptureModeSession::SetSettingsMenuShown(bool shown) {
auto* parent = GetParentContainer(current_root_);
capture_mode_settings_widget_ = std::make_unique<views::Widget>();
MaybeDismissUserNudgeForever();
capture_toast_controller_.DismissCurrentToastIfAny();
capture_mode_settings_widget_->Init(CreateWidgetParams(
parent, CaptureModeSettingsView::GetBounds(capture_mode_bar_view_),
@ -905,11 +918,13 @@ void CaptureModeSession::StartCountDown(
UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
/*is_touch=*/false);
// Fade out the capture bar and capture settings if it exists.
// Fade out the capture bar, capture settings and capture toast if they exist.
std::vector<ui::Layer*> layers_to_fade_out{
capture_mode_bar_widget_->GetLayer()};
if (capture_mode_settings_widget_)
layers_to_fade_out.push_back(capture_mode_settings_widget_->GetLayer());
if (auto* toast_layer = capture_toast_controller_.MaybeGetToastLayer())
layers_to_fade_out.push_back(toast_layer);
for (auto* layer : layers_to_fade_out) {
ui::ScopedLayerAnimationSettings layer_settings(layer->GetAnimator());
@ -1504,8 +1519,35 @@ void CaptureModeSession::OnCameraPreviewDragEnded(
UpdateCursor(screen_location, is_touch);
}
void CaptureModeSession::OnCameraPreviewBoundsOrVisibilityChanged() {
MaybeUpdateCaptureUisOpacity();
void CaptureModeSession::OnCameraPreviewBoundsOrVisibilityChanged(
bool capture_surface_became_too_small,
bool did_bounds_or_visibility_change) {
auto* camera_preview_widget = GetCameraPreviewWidget();
DCHECK(camera_preview_widget);
const bool is_parented_to_unparented_container =
camera_preview_widget->GetNativeWindow()->parent()->GetId() ==
kShellWindowId_UnparentedContainer;
if (capture_surface_became_too_small && !is_drag_in_progress_ &&
!is_parented_to_unparented_container) {
// Since the user nudge toast has lower priority, if the toast for the
// camera preview needs to be shown, user nudge toast should be dismissed
// forever when applicable.
MaybeDismissUserNudgeForever();
capture_toast_controller_.ShowCaptureToast(
CaptureToastType::kCameraPreview);
} else {
capture_toast_controller_.MaybeDismissCaptureToast(
CaptureToastType::kCameraPreview);
}
if (did_bounds_or_visibility_change)
MaybeUpdateCaptureUisOpacity();
}
void CaptureModeSession::OnCameraPreviewDestroyed() {
capture_toast_controller_.MaybeDismissCaptureToast(
CaptureToastType::kCameraPreview);
}
std::vector<views::Widget*> CaptureModeSession::GetAvailableWidgets() {
@ -1518,6 +1560,8 @@ std::vector<views::Widget*> CaptureModeSession::GetAvailableWidgets() {
result.push_back(capture_mode_settings_widget_.get());
if (dimensions_label_widget_)
result.push_back(dimensions_label_widget_.get());
if (auto* toast = capture_toast_controller_.capture_toast_widget())
result.push_back(toast);
return result;
}
@ -1580,6 +1624,7 @@ void CaptureModeSession::RefreshBarWidgetBounds() {
parent->StackChildAtTop(capture_mode_bar_widget_->GetNativeWindow());
if (user_nudge_controller_)
user_nudge_controller_->Reposition();
capture_toast_controller_.MaybeRepositionCaptureToast();
}
void CaptureModeSession::MaybeCreateUserNudge() {
@ -1592,7 +1637,7 @@ void CaptureModeSession::MaybeCreateUserNudge() {
return;
user_nudge_controller_ = std::make_unique<UserNudgeController>(
capture_mode_bar_view_->settings_button());
this, capture_mode_bar_view_->settings_button());
user_nudge_controller_->SetVisible(true);
}
@ -1618,9 +1663,9 @@ void CaptureModeSession::RefreshStackingOrder() {
auto* parent_container = GetParentContainer(current_root_);
DCHECK(parent_container);
auto* overlay_layer = layer();
auto* menu_layer = layer();
auto* parent_container_layer = parent_container->layer();
parent_container_layer->StackAtTop(overlay_layer);
parent_container_layer->StackAtTop(menu_layer);
std::vector<views::Widget*> widget_in_order;
@ -1630,6 +1675,8 @@ void CaptureModeSession::RefreshStackingOrder() {
// belong to the current capture session.
if (camera_preview_widget && !controller_->is_recording_in_progress())
widget_in_order.emplace_back(camera_preview_widget);
if (auto* toast = capture_toast_controller_.capture_toast_widget())
widget_in_order.emplace_back(toast);
if (capture_label_widget_)
widget_in_order.emplace_back(capture_label_widget_.get());
if (capture_mode_bar_widget_)

@ -10,6 +10,7 @@
#include "ash/accessibility/magnifier/magnifier_glass.h"
#include "ash/ash_export.h"
#include "ash/capture_mode/capture_mode_toast_controller.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/folder_selection_dialog_controller.h"
#include "ash/public/cpp/tablet_mode_observer.h"
@ -91,6 +92,9 @@ class ASH_EXPORT CaptureModeSession
void set_is_stopping_to_start_video_recording(bool value) {
is_stopping_to_start_video_recording_ = value;
}
CaptureModeToastController* capture_toast_controller() {
return &capture_toast_controller_;
}
// Initializes the capture mode session. This should be called right after the
// object is created.
@ -221,7 +225,17 @@ class ASH_EXPORT CaptureModeSession
void OnCameraPreviewDragStarted();
void OnCameraPreviewDragEnded(const gfx::Point& screen_location,
bool is_touch);
void OnCameraPreviewBoundsOrVisibilityChanged();
// Called every time when camera preview is updated.
// `capture_surface_became_too_small` indicates whether the camera preview
// becomes invisible is due to the capture surface becoming too small.
// `did_bounds_or_visibility_change` determines whether the capture UIs'
// opacity should be updated.
void OnCameraPreviewBoundsOrVisibilityChanged(
bool capture_surface_became_too_small,
bool did_bounds_or_visibility_change);
void OnCameraPreviewDestroyed();
private:
friend class CaptureModeSettingsTestApi;
@ -540,8 +554,13 @@ class ASH_EXPORT CaptureModeSession
std::unique_ptr<FolderSelectionDialogController>
folder_selection_dialog_controller_;
// Controls the user nudge animations.
std::unique_ptr<UserNudgeController> user_nudge_controller_;
// Controls creating, destroying or updating the visibility of the capture
// toast.
CaptureModeToastController capture_toast_controller_;
base::WeakPtrFactory<CaptureModeSession> weak_ptr_factory_{this};
};

@ -157,6 +157,12 @@ CaptureModeToggleButton* GetRegionToggleButton() {
return GetCaptureModeBarView()->capture_source_view()->region_toggle_button();
}
UserNudgeController* GetUserNudgeController() {
auto* session = CaptureModeController::Get()->capture_mode_session();
DCHECK(session);
return CaptureModeSessionTestApi(session).GetUserNudgeController();
}
// -----------------------------------------------------------------------------
// ProjectorCaptureModeIntegrationHelper:

@ -8,6 +8,7 @@
#include <string>
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/user_nudge_controller.h"
#include "ash/projector/test/mock_projector_client.h"
#include "base/test/scoped_feature_list.h"
#include "ui/events/event_constants.h"
@ -99,6 +100,8 @@ CaptureModeToggleButton* GetFullscreenToggleButton();
CaptureModeToggleButton* GetRegionToggleButton();
UserNudgeController* GetUserNudgeController();
// Defines a helper class to allow setting up and testing the Projector feature
// in multiple test fixtures. Note that this helper initializes the Projector-
// related features in its constructor, so test fixtures that use this should

@ -0,0 +1,241 @@
// 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/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;
constexpr int kToastCornerRadius = 16;
constexpr gfx::RoundedCornersF kToastRoundedCorners{kToastCornerRadius};
// 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) {
capture_toast_widget_ = std::make_unique<views::Widget>(
CreateWidgetParams(capture_session_->current_root()->GetChildById(
kShellWindowId_MenuContainer),
CalculateToastWidgetScreenBounds()));
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));
toast_label_view_->SetBorder(views::CreateRoundedRectBorder(
kToastBorderThickness, kToastCornerRadius,
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(kToastRoundedCorners);
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

@ -0,0 +1,98 @@
// 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.
#ifndef ASH_CAPTURE_MODE_CAPTURE_MODE_TOAST_CONTROLLER_H_
#define ASH_CAPTURE_MODE_CAPTURE_MODE_TOAST_CONTROLLER_H_
#include "ash/ash_export.h"
#include "base/timer/timer.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/views/widget/unique_widget_ptr.h"
namespace gfx {
class Rect;
} // namespace gfx
namespace views {
class Widget;
class Label;
} // namespace views
namespace ui {
class Layer;
} // namespace ui
namespace ash {
class CaptureModeSession;
// Defines the capture toast type that Capture Mode is currently using.
enum class CaptureToastType {
kUserNudge,
kCameraPreview,
};
// Controls the capture mode toast shown in the capture session.
class ASH_EXPORT CaptureModeToastController {
public:
explicit CaptureModeToastController(CaptureModeSession* session);
CaptureModeToastController(const CaptureModeToastController&) = delete;
CaptureModeToastController& operator=(const CaptureModeToastController&) =
delete;
~CaptureModeToastController();
const CaptureToastType* current_toast_type() const {
return current_toast_type_ ? &(*current_toast_type_) : nullptr;
}
views::Widget* capture_toast_widget() { return capture_toast_widget_.get(); }
// Shows `capture_toast_widget_` with label text defined by the given
// `capture_toast_type`; if `capture_toast_widget_` doesn't exist, creates
// one. Otherwise, just updates the label text for it if needed.
void ShowCaptureToast(CaptureToastType capture_toast_type);
void MaybeDismissCaptureToast(CaptureToastType capture_toast_type,
bool animate = true);
// Called when we need to dismiss the current toast in spite of the toast
// type. For example, when the settings menu is shown, the toast should be
// dismissed no matter what type it is.
void DismissCurrentToastIfAny();
void MaybeRepositionCaptureToast();
// Return the layer of `capture_toast_widget_` if it exists.
ui::Layer* MaybeGetToastLayer();
base::OneShotTimer* capture_toast_dismiss_timer_for_test() {
return &capture_toast_dismiss_timer_;
}
private:
// Initializes the toast widget and its contents.
void BuildCaptureToastWidget(const std::u16string& label);
gfx::Rect CalculateToastWidgetScreenBounds() const;
// The session that owns `this`. Guaranteed to be non null for the lifetime of
// `this`.
CaptureModeSession* const capture_session_;
// The capture toast widget and its contents view.
views::UniqueWidgetPtr capture_toast_widget_;
views::Label* toast_label_view_ = nullptr;
// Stores the toast type of the `capture_toast_widget_` after it's created.
absl::optional<CaptureToastType> current_toast_type_;
// Started when `capture_toast_widget_` is shown for `kCameraPreview`. Runs
// MaybeDismissCaptureToast() to fade out the capture toast if possible.
base::OneShotTimer capture_toast_dismiss_timer_;
base::WeakPtrFactory<CaptureModeToastController> weak_ptr_factory_{this};
};
} // namespace ash
#endif // ASH_CAPTURE_MODE_CAPTURE_MODE_TOAST_CONTROLLER_H_

@ -83,8 +83,6 @@ CaptureModeTypeView::CaptureModeTypeView(bool projector_mode)
video_toggle_button_->SetEnabled(false);
}
OnCaptureTypeChanged(controller->type());
video_toggle_button_->SetTooltipText(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_TOOLTIP_SCREENRECORD));
}

@ -5462,12 +5462,6 @@ class CaptureModeSettingsTest : public CaptureModeTest {
return CaptureModeSessionTestApi(session).GetCaptureModeSettingsView();
}
UserNudgeController* GetUserNudgeController() const {
auto* session = CaptureModeController::Get()->capture_mode_session();
DCHECK(session);
return CaptureModeSessionTestApi(session).GetUserNudgeController();
}
void WaitForSettingsMenuToBeRefreshed() {
base::RunLoop run_loop;
CaptureModeSettingsTestApi().SetOnSettingsMenuRefreshedCallback(
@ -5535,10 +5529,15 @@ class CaptureModeNudgeDismissalTest
TEST_P(CaptureModeNudgeDismissalTest, NudgeDismissedForever) {
auto* controller = StartSession();
auto* capture_session = controller->capture_mode_session();
auto* capture_toast_controller = capture_session->capture_toast_controller();
auto* nudge_controller = GetUserNudgeController();
ASSERT_TRUE(nudge_controller);
EXPECT_TRUE(nudge_controller->is_visible());
EXPECT_TRUE(nudge_controller->toast_widget());
EXPECT_TRUE(capture_toast_controller->capture_toast_widget());
ASSERT_TRUE(capture_toast_controller->current_toast_type());
EXPECT_EQ(*(capture_toast_controller->current_toast_type()),
CaptureToastType::kUserNudge);
// Trigger the action that dismisses the nudge forever, it should be removed
// in this session (if the action doesn't stop the session) and any future
@ -5572,17 +5571,20 @@ TEST_F(CaptureModeSettingsTest, NudgeChangesRootWithBar) {
auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen,
CaptureModeType::kImage);
auto* session = controller->capture_mode_session();
auto* capture_toast_controller = session->capture_toast_controller();
EXPECT_EQ(Shell::GetAllRootWindows()[0], session->current_root());
auto* nudge_controller = GetUserNudgeController();
EXPECT_EQ(
nudge_controller->toast_widget()->GetNativeWindow()->GetRootWindow(),
session->current_root());
EXPECT_EQ(capture_toast_controller->capture_toast_widget()
->GetNativeWindow()
->GetRootWindow(),
session->current_root());
MoveMouseToAndUpdateCursorDisplay(gfx::Point(1000, 500), event_generator);
EXPECT_EQ(Shell::GetAllRootWindows()[1], session->current_root());
EXPECT_EQ(
nudge_controller->toast_widget()->GetNativeWindow()->GetRootWindow(),
session->current_root());
EXPECT_EQ(capture_toast_controller->capture_toast_widget()
->GetNativeWindow()
->GetRootWindow(),
session->current_root());
}
TEST_F(CaptureModeSettingsTest, NudgeBehaviorWhenSelectingRegion) {
@ -5607,9 +5609,11 @@ TEST_F(CaptureModeSettingsTest, NudgeBehaviorWhenSelectingRegion) {
// The nudge shows again, and is on the second display.
EXPECT_TRUE(nudge_controller->is_visible());
EXPECT_EQ(
nudge_controller->toast_widget()->GetNativeWindow()->GetRootWindow(),
session->current_root());
EXPECT_EQ(session->capture_toast_controller()
->capture_toast_widget()
->GetNativeWindow()
->GetRootWindow(),
session->current_root());
}
TEST_F(CaptureModeSettingsTest, NudgeDoesNotShowForAllUserTypes) {

@ -30,8 +30,10 @@
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/message_center/views/notification_background_painter.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/widget/widget.h"
namespace ash::capture_mode_util {
@ -39,6 +41,7 @@ namespace {
constexpr int kBannerViewTopRadius = 0;
constexpr int kBannerViewBottomRadius = 8;
constexpr float kScaleUpFactor = 0.8f;
// Returns the target visibility of the camera preview, given the
// `confine_bounds_short_side_length`. The out parameter
@ -72,6 +75,74 @@ gfx::Point GetLocalCenterPoint(ui::Layer* layer) {
return gfx::Rect(layer->GetTargetBounds().size()).CenterPoint();
}
void FadeInWidget(views::Widget* widget,
const AnimationParams& animation_params) {
DCHECK(widget);
auto* layer = widget->GetLayer();
DCHECK(!widget->GetNativeWindow()->TargetVisibility() ||
layer->GetTargetOpacity() < 1.f);
// Please notice the order matters here. When the opacity is set to 0.f, if
// there's any on-going fade out animation, the `OnEnded` in `FadeOutWidget`
// will be triggered, which will hide the widget and set its opacity to 1.f.
// So `Show` should be triggered after setting the opacity to 0 to undo the
// work done by the FadeOutWidget's OnEnded .
if (layer->opacity() == 1.f)
layer->SetOpacity(0.f);
if (!widget->GetNativeWindow()->TargetVisibility())
widget->Show();
if (animation_params.apply_scale_up_animation) {
layer->SetTransform(
capture_mode_util::GetScaleTransformAboutCenter(layer, kScaleUpFactor));
}
views::AnimationBuilder builder;
auto& animation_sequence_block =
builder
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(animation_params.animation_duration)
.SetOpacity(layer, 1.f, animation_params.tween_type);
// We should only set transform here if `apply_scale_up_animation` is true,
// otherwise, it may mess up with the snap animation in
// `SetCameraPreviewBounds`.
if (animation_params.apply_scale_up_animation) {
animation_sequence_block.SetTransform(layer, gfx::Transform(),
gfx::Tween::ACCEL_20_DECEL_100);
}
}
void FadeOutWidget(views::Widget* widget,
const AnimationParams& animation_params) {
DCHECK(widget);
DCHECK(widget->GetNativeWindow()->TargetVisibility());
auto* layer = widget->GetLayer();
DCHECK_EQ(layer->GetTargetOpacity(), 1.f);
views::AnimationBuilder()
.OnEnded(base::BindOnce(
[](base::WeakPtr<views::Widget> the_widget) {
if (!the_widget)
return;
// Please notice, the order matters here. If we set the layer's
// opacity back to 1.f before calling `Hide`, flickering can be
// seen.
the_widget->Hide();
the_widget->GetLayer()->SetOpacity(1.f);
},
widget->GetWeakPtr()))
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(animation_params.animation_duration)
.SetOpacity(layer, 0.f, animation_params.tween_type);
}
} // namespace
bool IsCaptureModeActive() {
@ -349,13 +420,52 @@ aura::Window* GetTopMostCapturableWindowAtPoint(
}
if (controller->IsActive()) {
if (auto* capture_label_widget =
controller->capture_mode_session()->capture_label_widget()) {
auto* capture_session = controller->capture_mode_session();
if (auto* capture_label_widget = capture_session->capture_label_widget())
ignore_windows.insert(capture_label_widget->GetNativeWindow());
if (auto* capture_toast_widget = capture_session->capture_toast_controller()
->capture_toast_widget()) {
ignore_windows.insert(capture_toast_widget->GetNativeWindow());
}
}
return GetTopmostWindowAtPoint(screen_point, ignore_windows);
}
bool GetWidgetCurrentVisibility(views::Widget* widget) {
// Note that we use `aura::Window::TargetVisibility()` rather than
// `views::Widget::IsVisible()` (which in turn uses
// `aura::Window::IsVisible()`). The reason is because the latter takes into
// account whether window's layer is drawn or not. We want to calculate the
// current visibility only based on the actual visibility of the window
// itself, so that we can correctly compare it against `target_visibility`.
// Note that the widget may be a child of the unparented container (which is
// always hidden), yet the native window is shown.
return widget->GetNativeWindow()->TargetVisibility() &&
widget->GetLayer()->GetTargetOpacity() > 0.f;
}
bool SetWidgetVisibility(views::Widget* widget,
bool target_visibility,
absl::optional<AnimationParams> animation_params) {
DCHECK(widget);
if (target_visibility == GetWidgetCurrentVisibility(widget))
return false;
if (animation_params) {
if (target_visibility)
FadeInWidget(widget, *animation_params);
else
FadeOutWidget(widget, *animation_params);
} else {
if (target_visibility)
widget->Show();
else
widget->Hide();
}
return true;
}
} // namespace ash::capture_mode_util

@ -9,6 +9,9 @@
#include "ash/ash_export.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "base/time/time.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/size.h"
namespace aura {
@ -27,6 +30,7 @@ class Layer;
namespace views {
class View;
class Widget;
} // namespace views
namespace ash {
@ -137,6 +141,27 @@ CalculateCameraPreviewSizeSpecs(const gfx::Size& confine_bounds_size,
// since the snapshot code tries to snap a deleted window.
aura::Window* GetTopMostCapturableWindowAtPoint(const gfx::Point& screen_point);
bool GetWidgetCurrentVisibility(views::Widget* widget);
// Defines an object to hold the animation params used for setting the widget's
// visibility.
struct AnimationParams {
const base::TimeDelta animation_duration;
const gfx::Tween::Type tween_type;
// When it's true, the scale up transform should be applied in the fade in
// animiation.
const bool apply_scale_up_animation;
};
// Sets the visibility of the given `widget` to the given `target_visibility`
// with the given `animation_params`, returns true only if the
// `target_visibility` is different than the current.
bool SetWidgetVisibility(views::Widget* widget,
bool target_visibility,
absl::optional<AnimationParams> animation_params);
} // namespace capture_mode_util
} // namespace ash

@ -5,37 +5,23 @@
#include "ash/capture_mode/user_nudge_controller.h"
#include "ash/capture_mode/capture_mode_controller.h"
#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 "base/bind.h"
#include "base/check.h"
#include "base/time/time.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/animation/animation_sequence_block.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/label.h"
namespace ash {
namespace {
constexpr int kToastSpacingFromBar = 8;
constexpr int kToastDefaultHeight = 36;
constexpr int kToastVerticalPadding = 8;
constexpr int kToastHorizontalPadding = 16;
constexpr int kToastBorderThickness = 1;
constexpr int kToastCornerRadius = 16;
constexpr gfx::RoundedCornersF kToastRoundedCorners{kToastCornerRadius};
constexpr float kBaseRingOpacity = 0.21f;
constexpr float kRippleRingOpacity = 0.5f;
@ -67,22 +53,12 @@ gfx::Rect GetViewLayerBoundsInRootNoTransform(views::View* view) {
return gfx::Rect(origin, view->layer()->size());
}
// 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 = "UserNudgeToastWidget";
params.accept_events = false;
return params;
}
} // namespace
UserNudgeController::UserNudgeController(views::View* view_to_be_highlighted)
: view_to_be_highlighted_(view_to_be_highlighted) {
UserNudgeController::UserNudgeController(CaptureModeSession* session,
views::View* view_to_be_highlighted)
: capture_session_(session),
view_to_be_highlighted_(view_to_be_highlighted) {
view_to_be_highlighted_->SetPaintToLayer();
view_to_be_highlighted_->layer()->SetFillsBoundsOpaquely(false);
@ -97,21 +73,19 @@ UserNudgeController::UserNudgeController(views::View* view_to_be_highlighted)
ripple_ring_.SetFillsBoundsOpaquely(false);
ripple_ring_.SetOpacity(0);
BuildToastWidget();
Reposition();
}
UserNudgeController::~UserNudgeController() {
DCHECK(toast_widget_);
toast_widget_->CloseNow();
if (should_dismiss_nudge_forever_)
CaptureModeController::Get()->DisableUserNudgeForever();
capture_session_->capture_toast_controller()->MaybeDismissCaptureToast(
CaptureToastType::kUserNudge,
/*animate=*/false);
}
void UserNudgeController::Reposition() {
auto* parent_window = GetParentWindow();
if (toast_widget_->GetNativeWindow()->parent() != parent_window)
parent_window->AddChild(toast_widget_->GetNativeWindow());
auto* parent_layer = parent_window->layer();
if (parent_layer != base_ring_.parent()) {
@ -127,7 +101,6 @@ void UserNudgeController::Reposition() {
ripple_ring_.SetBounds(view_bounds_in_root);
ripple_ring_.SetRoundedCornerRadius(
gfx::RoundedCornersF(view_bounds_in_root.width() / 2.f));
toast_widget_->SetBounds(CalculateToastWidgetScreenBounds());
}
void UserNudgeController::SetVisible(bool visible) {
@ -135,6 +108,7 @@ void UserNudgeController::SetVisible(bool visible) {
return;
is_visible_ = visible;
auto* capture_toast_controller = capture_session_->capture_toast_controller();
views::AnimationBuilder builder;
builder.SetPreemptionStrategy(
@ -152,8 +126,9 @@ void UserNudgeController::SetVisible(bool visible) {
builder.Once()
.SetDuration(kVisibilityChangeDuration)
.SetOpacity(&base_ring_, 0, gfx::Tween::FAST_OUT_SLOW_IN)
.SetOpacity(&ripple_ring_, 0, gfx::Tween::FAST_OUT_SLOW_IN)
.SetOpacity(toast_widget_->GetLayer(), 0, gfx::Tween::FAST_OUT_SLOW_IN);
.SetOpacity(&ripple_ring_, 0, gfx::Tween::FAST_OUT_SLOW_IN);
capture_toast_controller->MaybeDismissCaptureToast(
CaptureToastType::kUserNudge);
return;
}
@ -167,8 +142,8 @@ void UserNudgeController::SetVisible(bool visible) {
base::Unretained(this)))
.Once()
.SetDuration(kDelayToShowNudge)
.SetOpacity(&base_ring_, kBaseRingOpacity, gfx::Tween::FAST_OUT_SLOW_IN)
.SetOpacity(toast_widget_->GetLayer(), 1.f, gfx::Tween::FAST_OUT_SLOW_IN);
.SetOpacity(&base_ring_, kBaseRingOpacity, gfx::Tween::FAST_OUT_SLOW_IN);
capture_toast_controller->ShowCaptureToast(CaptureToastType::kUserNudge);
}
void UserNudgeController::PerformNudgeAnimations() {
@ -250,79 +225,4 @@ aura::Window* UserNudgeController::GetParentWindow() const {
return root_window->GetChildById(kShellWindowId_OverlayContainer);
}
gfx::Rect UserNudgeController::CalculateToastWidgetScreenBounds() const {
const auto bar_widget_bounds_in_screen =
view_to_be_highlighted_->GetWidget()->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;
}
void UserNudgeController::BuildToastWidget() {
toast_widget_->Init(CreateWidgetParams(GetParentWindow(),
CalculateToastWidgetScreenBounds()));
const int message_id =
features::IsCaptureModeSelfieCameraEnabled()
? IDS_ASH_SCREEN_CAPTURE_SHOW_CAMERA_USER_NUDGE
: IDS_ASH_SCREEN_CAPTURE_FOLDER_SELECTION_USER_NUDGE;
toast_label_view_ = toast_widget_->SetContentsView(
std::make_unique<views::Label>(l10n_util::GetStringUTF16(message_id)));
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));
toast_label_view_->SetBorder(views::CreateRoundedRectBorder(
kToastBorderThickness, kToastCornerRadius,
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(kToastRoundedCorners);
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 SetVisible() is called.
toast_widget_->Show();
auto* widget_layer = toast_widget_->GetLayer();
widget_layer->SetOpacity(0);
}
} // namespace ash

@ -7,21 +7,19 @@
#include "base/timer/timer.h"
#include "ui/compositor/layer.h"
#include "ui/views/widget/unique_widget_ptr.h"
#include "ui/views/widget/widget.h"
namespace aura {
class Window;
} // namespace aura
namespace views {
class Label;
class View;
class Widget;
} // namespace views
namespace ash {
class CaptureModeSession;
// Controls the user nudge animation and toast widget which are used to draw the
// user's attention towards the given `view_to_be_highlighted`. In the current
// iteration, this view is the settings button on the capture mode bar, and the
@ -30,23 +28,25 @@ namespace ash {
// will be shown instead once the selfie camera feature is enabled.
class UserNudgeController {
public:
explicit UserNudgeController(views::View* view_to_be_highlighted);
explicit UserNudgeController(CaptureModeSession* session,
views::View* view_to_be_highlighted);
UserNudgeController(const UserNudgeController&) = delete;
UserNudgeController& operator=(const UserNudgeController&) = delete;
~UserNudgeController();
views::Widget* toast_widget() { return toast_widget_.get(); }
bool is_visible() const { return is_visible_; }
void set_should_dismiss_nudge_forever(bool value) {
should_dismiss_nudge_forever_ = value;
}
bool should_dismiss_nudge_forever() const {
return should_dismiss_nudge_forever_;
}
// Repositions the animation layers and the toast widget such that they're
// correctly parented with the correct bounds on the correct display.
// Repositions the animation layers such that they're correctly parented with
// the correct bounds on the correct display.
void Reposition();
// Animates the animation layers and the toast widget towards the given
// visibility state `visible`.
// Animates the animation layers towards the given visibility state `visible`.
void SetVisible(bool visible);
private:
@ -69,18 +69,13 @@ class UserNudgeController {
// a repeat of this animation after a certain delay using `timer_`.
void OnBaseRingAnimationEnded();
// This is the window that will be used to as the parent of the toast widget,
// and its layer as the parent of our animation layers (`base_ring_` and
// `ripple_ring_`).
// This is the window that will be used to as the parent of our animation
// layers (`base_ring_` and `ripple_ring_`).
aura::Window* GetParentWindow() const;
// Calculates and returns the current screen bounds that should be set on the
// `toast_widget_` based on where `view_to_be_highlighted_`'s widget (which is
// the capture bar) is.
gfx::Rect CalculateToastWidgetScreenBounds() const;
// Initializes the toast widget and its contents.
void BuildToastWidget();
// The session that owns `this`. Guaranteed to be non null for the lifetime of
// `this`.
CaptureModeSession* const capture_session_;
// The view to which we're trying to grab the user's attention.
views::View* const view_to_be_highlighted_;
@ -90,10 +85,6 @@ class UserNudgeController {
ui::Layer base_ring_{ui::LAYER_SOLID_COLOR};
ui::Layer ripple_ring_{ui::LAYER_SOLID_COLOR};
// The toast widget and its contents view.
views::UniqueWidgetPtr toast_widget_ = std::make_unique<views::Widget>();
views::Label* toast_label_view_ = nullptr;
// The timer used to repeat the nudge animation after a certain delay.
base::OneShotTimer timer_;