// Copyright 2020 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_util.h" #include "ash/accessibility/accessibility_controller_impl.h" #include "ash/capture_mode/capture_mode_camera_controller.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/stop_recording_button_tray.h" #include "ash/constants/ash_features.h" #include "ash/public/cpp/clipboard_history_controller.h" #include "ash/public/cpp/style/scoped_light_mode_as_default.h" #include "ash/public/cpp/window_finder.h" #include "ash/resources/vector_icons/vector_icons.h" #include "ash/root_window_controller.h" #include "ash/shell.h" #include "ash/strings/grit/ash_strings.h" #include "ash/style/ash_color_provider.h" #include "ash/wm/tablet_mode/tablet_mode_controller.h" #include "base/check.h" #include "base/notreached.h" #include "ui/base/l10n/l10n_util.h" #include "ui/chromeos/events/keyboard_layout_util.h" #include "ui/compositor/layer.h" #include "ui/gfx/geometry/point.h" #include "ui/gfx/geometry/rect.h" #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 { 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 // `out_is_surface_too_small` will be set to true if the preview should be // hidden due to the surface within which it's confined is too small. Otherwise, // it's unchanged. bool CalculateCameraPreviewTargetVisibility( int confine_bounds_short_side_length, bool* out_is_surface_too_small) { DCHECK(out_is_surface_too_small); // If the short side of the bounds within which the camera preview should be // confined is too small, the camera should be hidden. if (confine_bounds_short_side_length < capture_mode::kMinCaptureSurfaceShortSideLengthForVisibleCamera) { *out_is_surface_too_small = true; return false; } // Now that we determined that its size doesn't affect its visibility, we need // to check if we're in a capture mode session that is in a state that affects // the camera preview's visibility. auto* controller = CaptureModeController::Get(); return !controller->IsActive() || controller->capture_mode_session() ->CalculateCameraPreviewTargetVisibility(); } // Returns the local center point of the given `layer`. 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 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() { return CaptureModeController::Get()->IsActive(); } gfx::Point GetLocationForFineTunePosition(const gfx::Rect& rect, FineTunePosition position) { switch (position) { case FineTunePosition::kTopLeft: return rect.origin(); case FineTunePosition::kTopCenter: return rect.top_center(); case FineTunePosition::kTopRight: return rect.top_right(); case FineTunePosition::kRightCenter: return rect.right_center(); case FineTunePosition::kBottomRight: return rect.bottom_right(); case FineTunePosition::kBottomCenter: return rect.bottom_center(); case FineTunePosition::kBottomLeft: return rect.bottom_left(); case FineTunePosition::kLeftCenter: return rect.left_center(); default: break; } NOTREACHED(); return gfx::Point(); } bool IsCornerFineTunePosition(FineTunePosition position) { switch (position) { case FineTunePosition::kTopLeft: case FineTunePosition::kTopRight: case FineTunePosition::kBottomRight: case FineTunePosition::kBottomLeft: return true; default: break; } return false; } StopRecordingButtonTray* GetStopRecordingButtonForRoot(aura::Window* root) { DCHECK(root); DCHECK(root->IsRootWindow()); // Recording can end when a display being fullscreen-captured gets removed, in // this case, we don't need to hide the button. if (root->is_destroying()) return nullptr; // Can be null while shutting down. auto* root_window_controller = RootWindowController::ForWindow(root); if (!root_window_controller) return nullptr; auto* stop_recording_button = root_window_controller->GetStatusAreaWidget() ->stop_recording_button_tray(); DCHECK(stop_recording_button); return stop_recording_button; } void SetStopRecordingButtonVisibility(aura::Window* root, bool visible) { if (auto* stop_recording_button = GetStopRecordingButtonForRoot(root)) stop_recording_button->SetVisiblePreferred(visible); } void TriggerAccessibilityAlert(const std::string& message) { Shell::Get() ->accessibility_controller() ->TriggerAccessibilityAlertWithMessage(message); } void TriggerAccessibilityAlert(int message_id) { TriggerAccessibilityAlert(l10n_util::GetStringUTF8(message_id)); } void TriggerAccessibilityAlertSoon(const std::string& message) { base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::BindOnce( &AccessibilityControllerImpl::TriggerAccessibilityAlertWithMessage, Shell::Get()->accessibility_controller()->GetWeakPtr(), message)); } void TriggerAccessibilityAlertSoon(int message_id) { TriggerAccessibilityAlertSoon(l10n_util::GetStringUTF8(message_id)); } CameraPreviewSnapPosition GetCameraNextHorizontalSnapPosition( CameraPreviewSnapPosition current, bool going_left) { switch (current) { case CameraPreviewSnapPosition::kTopLeft: return going_left ? current : CameraPreviewSnapPosition::kTopRight; case CameraPreviewSnapPosition::kTopRight: return going_left ? CameraPreviewSnapPosition::kTopLeft : current; case CameraPreviewSnapPosition::kBottomLeft: return going_left ? current : CameraPreviewSnapPosition::kBottomRight; case CameraPreviewSnapPosition::kBottomRight: return going_left ? CameraPreviewSnapPosition::kBottomLeft : current; } } CameraPreviewSnapPosition GetCameraNextVerticalSnapPosition( CameraPreviewSnapPosition current, bool going_up) { switch (current) { case CameraPreviewSnapPosition::kTopLeft: return going_up ? current : CameraPreviewSnapPosition::kBottomLeft; case CameraPreviewSnapPosition::kTopRight: return going_up ? current : CameraPreviewSnapPosition::kBottomRight; case CameraPreviewSnapPosition::kBottomLeft: return going_up ? CameraPreviewSnapPosition::kTopLeft : current; case CameraPreviewSnapPosition::kBottomRight: return going_up ? CameraPreviewSnapPosition::kTopRight : current; } } std::unique_ptr CreateClipboardShortcutView() { std::unique_ptr clipboard_shortcut_view = std::make_unique(); auto* color_provider = AshColorProvider::Get(); const SkColor background_color = color_provider->GetControlsLayerColor( AshColorProvider::ControlsLayerType::kControlBackgroundColorActive); // The text and icon are showing on the background with |background_color| // so its color is same with kButtonLabelColorPrimary although they're // not theoretically showing on a button. const SkColor text_icon_color = color_provider->GetContentLayerColor( AshColorProvider::ContentLayerType::kButtonLabelColorPrimary); clipboard_shortcut_view->SetLayoutManager(std::make_unique( views::BoxLayout::Orientation::kHorizontal)); const std::u16string shortcut_key = l10n_util::GetStringUTF16( ui::DeviceUsesKeyboardLayout2() ? IDS_ASH_SHORTCUT_MODIFIER_LAUNCHER : IDS_ASH_SHORTCUT_MODIFIER_SEARCH); const std::u16string label_text = l10n_util::GetStringFUTF16( IDS_ASH_MULTIPASTE_SCREENSHOT_NOTIFICATION_NUDGE, shortcut_key); views::Label* shortcut_label = clipboard_shortcut_view->AddChildView(std::make_unique()); shortcut_label->SetText(label_text); shortcut_label->SetBackgroundColor(background_color); shortcut_label->SetEnabledColor(text_icon_color); return clipboard_shortcut_view; } // Creates the banner view that will show on top of the notification image. std::unique_ptr CreateBannerView() { std::unique_ptr banner_view = std::make_unique(); // Use the light mode as default as notification is still using light // theme as the default theme. ScopedLightModeAsDefault scoped_light_mode_as_default; auto* color_provider = AshColorProvider::Get(); const SkColor background_color = color_provider->GetControlsLayerColor( AshColorProvider::ControlsLayerType::kControlBackgroundColorActive); // The text and icon are showing on the background with |background_color| // so its color is same with kButtonLabelColorPrimary although they're // not theoretically showing on a button. const SkColor text_icon_color = color_provider->GetContentLayerColor( AshColorProvider::ContentLayerType::kButtonLabelColorPrimary); auto* layout = banner_view->SetLayoutManager(std::make_unique( views::BoxLayout::Orientation::kHorizontal, gfx::Insets::VH(kBannerVerticalInsetDip, kBannerHorizontalInsetDip), kBannerIconTextSpacingDip)); if (features::IsNotificationsRefreshEnabled()) { banner_view->SetBackground(views::CreateBackgroundFromPainter( std::make_unique( kBannerViewTopRadius, kBannerViewBottomRadius, background_color))); } else { banner_view->SetBackground(views::CreateSolidBackground(background_color)); } views::ImageView* icon = banner_view->AddChildView(std::make_unique()); icon->SetImage(gfx::CreateVectorIcon(kCaptureModeCopiedToClipboardIcon, kBannerIconSizeDip, text_icon_color)); views::Label* label = banner_view->AddChildView( std::make_unique(l10n_util::GetStringUTF16( IDS_ASH_SCREEN_CAPTURE_SCREENSHOT_COPIED_TO_CLIPBOARD))); label->SetBackgroundColor(background_color); label->SetHorizontalAlignment(gfx::ALIGN_LEFT); label->SetEnabledColor(text_icon_color); if (!Shell::Get()->tablet_mode_controller()->InTabletMode()) { banner_view->AddChildView(CreateClipboardShortcutView()); layout->SetFlexForView(label, 1); // Notify the clipboard history of the created notification. ClipboardHistoryController::Get()->OnScreenshotNotificationCreated(); } return banner_view; } // Creates the play icon view which shows on top of the video thumbnail in the // notification. std::unique_ptr CreatePlayIconView() { auto play_view = std::make_unique(); auto* color_provider = AshColorProvider::Get(); const SkColor icon_color = color_provider->GetContentLayerColor( AshColorProvider::ContentLayerType::kIconColorPrimary); play_view->SetImage(gfx::CreateVectorIcon(kCaptureModePlayIcon, kPlayIconSizeDip, icon_color)); play_view->SetHorizontalAlignment(views::ImageView::Alignment::kCenter); play_view->SetVerticalAlignment(views::ImageView::Alignment::kCenter); const SkColor background_color = color_provider->GetBaseLayerColor( AshColorProvider::BaseLayerType::kTransparent80); play_view->SetBackground(views::CreateRoundedRectBackground( background_color, kPlayIconBackgroundCornerRadiusDip)); return play_view; } gfx::Transform GetScaleTransformAboutCenter(ui::Layer* layer, float scale) { return gfx::GetScaleTransform(GetLocalCenterPoint(layer), scale); } CameraPreviewSizeSpecs CalculateCameraPreviewSizeSpecs( const gfx::Size& confine_bounds_size, bool is_collapsed) { // We divide the shorter side of the confine bounds by a divider to calculate // the expanded diameter. Note that both expanded and collapsed diameters are // clamped at a minimum value of `kMinCameraPreviewDiameter`. const int short_side = std::min(confine_bounds_size.width(), confine_bounds_size.height()); const int expanded_diameter = std::max(short_side / capture_mode::kCaptureSurfaceShortSideDivider, capture_mode::kMinCameraPreviewDiameter); // If the expanded diameter is below a certain threshold, we consider it too // small to allow it to collapse, and in that case the resize button will be // hidden. const bool is_collapsible = expanded_diameter >= capture_mode::kMinCollapsibleCameraPreviewDiameter; // Pick the actual diameter based on whether the preview is currently expanded // or collapsed. const int diameter = !is_collapsed ? expanded_diameter : std::max(expanded_diameter / capture_mode::kCollapsedPreviewDivider, capture_mode::kMinCameraPreviewDiameter); bool is_surface_too_small = false; const bool should_be_visible = CalculateCameraPreviewTargetVisibility(short_side, &is_surface_too_small); // If the surface was determined to be too small, the preview should be // hidden. DCHECK(!is_surface_too_small || !should_be_visible); return CameraPreviewSizeSpecs{gfx::Size(diameter, diameter), is_collapsible, should_be_visible, is_surface_too_small}; } aura::Window* GetTopMostCapturableWindowAtPoint( const gfx::Point& screen_point) { auto* controller = CaptureModeController::Get(); std::set ignore_windows; auto* camera_controller = controller->camera_controller(); if (auto* camera_preview_widget = camera_controller->camera_preview_widget()) ignore_windows.insert(camera_preview_widget->GetNativeWindow()); if (controller->IsActive()) { 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 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