blob: b7ff611aa66e77c385d2728bdb408ad021ea6025 [file] [log] [blame]
// 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<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() {
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<views::View> CreateClipboardShortcutView() {
std::unique_ptr<views::View> clipboard_shortcut_view =
std::make_unique<views::View>();
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>(
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<views::Label>());
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<views::View> CreateBannerView() {
std::unique_ptr<views::View> banner_view = std::make_unique<views::View>();
// 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>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets::VH(kBannerVerticalInsetDip, kBannerHorizontalInsetDip),
kBannerIconTextSpacingDip));
if (features::IsNotificationsRefreshEnabled()) {
banner_view->SetBackground(views::CreateBackgroundFromPainter(
std::make_unique<message_center::NotificationBackgroundPainter>(
kBannerViewTopRadius, kBannerViewBottomRadius, background_color)));
} else {
banner_view->SetBackground(views::CreateSolidBackground(background_color));
}
views::ImageView* icon =
banner_view->AddChildView(std::make_unique<views::ImageView>());
icon->SetImage(gfx::CreateVectorIcon(kCaptureModeCopiedToClipboardIcon,
kBannerIconSizeDip, text_icon_color));
views::Label* label = banner_view->AddChildView(
std::make_unique<views::Label>(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<views::View> CreatePlayIconView() {
auto play_view = std::make_unique<views::ImageView>();
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<aura::Window*> ignore_windows;
auto* camera_controller = controller->camera_controller();
if (camera_controller && camera_controller->camera_preview_widget()) {
ignore_windows.insert(
camera_controller->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<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