#include "ash/capture_mode/capture_mode_camera_preview_view.h"
#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"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/background.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
constexpr base::TimeDelta kResizeButtonFadeInDuration = base::Milliseconds(150);
constexpr base::TimeDelta kResizeButtonFadeOutDuration = base::Milliseconds(50);
gfx::PointF GetEventScreenLocation(const ui::LocatedEvent& event) {
return event.target()->GetScreenLocationF(event);
}
const gfx::VectorIcon& GetIconOfResizeButton(
const bool is_camera_preview_collapsed) {
return is_camera_preview_collapsed ? kCaptureModeCameraPreviewExpandIcon
: kCaptureModeCameraPreviewCollapseIcon;
}
bool IsArrowKeyEvent(const ui::KeyEvent* event) {
const ui::KeyboardCode key_code = event->key_code();
return key_code == ui::VKEY_DOWN || key_code == ui::VKEY_RIGHT ||
key_code == ui::VKEY_LEFT || key_code == ui::VKEY_UP;
}
}
CameraPreviewResizeButton::CameraPreviewResizeButton(
CameraPreviewView* camera_preview_view,
views::Button::PressedCallback callback,
const gfx::VectorIcon& icon)
: IconButton(std::move(callback),
IconButton::Type::kMediumFloating,
&icon,
u"",
false,
true),
camera_preview_view_(camera_preview_view) {}
CameraPreviewResizeButton::~CameraPreviewResizeButton() = default;
views::View* CameraPreviewResizeButton::GetView() {
return this;
}
void CameraPreviewResizeButton::PseudoFocus() {
DCHECK(camera_preview_view_->is_collapsible());
CaptureModeSessionFocusCycler::HighlightableView::PseudoFocus();
camera_preview_view_->RefreshResizeButtonVisibility();
}
void CameraPreviewResizeButton::PseudoBlur() {
CaptureModeSessionFocusCycler::HighlightableView::PseudoBlur();
camera_preview_view_->ScheduleRefreshResizeButtonVisibility();
}
BEGIN_METADATA(CameraPreviewResizeButton, IconButton)
END_METADATA
CameraPreviewView::CameraPreviewView(
CaptureModeCameraController* camera_controller,
const CameraId& camera_id,
mojo::Remote<video_capture::mojom::VideoSource> camera_video_source,
const media::VideoCaptureFormat& capture_format,
bool should_flip_frames_horizontally)
: camera_controller_(camera_controller),
camera_id_(camera_id),
camera_video_renderer_(std::move(camera_video_source),
capture_format,
should_flip_frames_horizontally),
camera_video_host_view_(
AddChildView(std::make_unique<views::NativeViewHost>())),
resize_button_(AddChildView(std::make_unique<CameraPreviewResizeButton>(
this,
base::BindRepeating(&CameraPreviewView::OnResizeButtonPressed,
base::Unretained(this)),
GetIconOfResizeButton(
camera_controller_->is_camera_preview_collapsed())))),
scoped_a11y_overrider_(
std::make_unique<ScopedA11yOverrideWindowSetter>()) {
SetBorder(views::CreateEmptyBorder(capture_mode::kCameraPreviewBorderSize));
resize_button_->SetPaintToLayer();
resize_button_->layer()->SetFillsBoundsOpaquely(false);
resize_button_->SetBackground(views::CreateThemedRoundedRectBackground(
kColorAshShieldAndBase80,
resize_button_->GetPreferredSize().height() / 2.f));
accessibility_observation_.Observe(Shell::Get()->accessibility_controller());
RefreshResizeButtonVisibility();
UpdateResizeButtonTooltip();
capture_mode_util::MaybeUpdateCaptureModePrivacyIndicators();
}
CameraPreviewView::~CameraPreviewView() {
auto* controller = CaptureModeController::Get();
if (controller->IsActive() && !controller->is_recording_in_progress())
controller->capture_mode_session()->OnCameraPreviewDestroyed();
capture_mode_util::MaybeUpdateCaptureModePrivacyIndicators();
}
void CameraPreviewView::SetIsCollapsible(bool value) {
if (value != is_collapsible_) {
is_collapsible_ = value;
RefreshResizeButtonVisibility();
}
}
bool CameraPreviewView::MaybeHandleKeyEvent(const ui::KeyEvent* event) {
if (has_focus() && event->IsControlDown() && IsArrowKeyEvent(event)) {
const CameraPreviewSnapPosition current_snap_position =
camera_controller_->camera_preview_snap_position();
CameraPreviewSnapPosition new_snap_position = current_snap_position;
const ui::KeyboardCode key_code = event->key_code();
if (key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT) {
new_snap_position =
capture_mode_util::GetCameraNextHorizontalSnapPosition(
current_snap_position, key_code == ui::VKEY_LEFT);
} else {
DCHECK(key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN);
new_snap_position = capture_mode_util::GetCameraNextVerticalSnapPosition(
current_snap_position, key_code == ui::VKEY_UP);
}
camera_controller_->SetCameraPreviewSnapPosition(new_snap_position,
true);
return new_snap_position != current_snap_position;
}
auto* controller = CaptureModeController::Get();
if (controller->IsActive() || !controller->is_recording_in_progress())
return false;
const bool camera_preview_has_focus =
has_focus() || resize_button_->has_focus();
bool should_handle_event = false;
switch (event->key_code()) {
case ui::VKEY_TAB:
if (camera_preview_has_focus) {
if (resize_button_->has_focus()) {
DCHECK(is_collapsible_);
resize_button_->PseudoBlur();
PseudoFocus();
} else if (is_collapsible_) {
PseudoBlur();
resize_button_->PseudoFocus();
}
should_handle_event = true;
}
break;
case ui::VKEY_SPACE:
if (resize_button_->has_focus()) {
resize_button_->ClickView();
should_handle_event = true;
}
break;
case ui::VKEY_ESCAPE:
if (camera_preview_has_focus) {
BlurA11yFocus();
should_handle_event = true;
}
break;
default:
break;
}
return should_handle_event;
}
void CameraPreviewView::RefreshResizeButtonVisibility() {
const float target_opacity = CalculateResizeButtonTargetOpacity();
if (target_opacity == resize_button_->layer()->GetTargetOpacity())
return;
resize_button_hide_timer_.Stop();
if (target_opacity == 1.f) {
FadeInResizeButton();
ScheduleRefreshResizeButtonVisibility();
} else {
FadeOutResizeButton();
}
}
void CameraPreviewView::UpdateA11yOverrideWindow() {
if (has_focus() || resize_button_->has_focus()) {
scoped_a11y_overrider_->MaybeUpdateA11yOverrideWindow(
GetWidget()->GetNativeWindow());
} else {
scoped_a11y_overrider_->MaybeUpdateA11yOverrideWindow(nullptr);
}
}
void CameraPreviewView::MaybeBlurFocus(const ui::MouseEvent& event) {
auto* target = static_cast<aura::Window*>(event.target());
if (!GetWidget()->GetNativeWindow()->Contains(target))
BlurA11yFocus();
}
void CameraPreviewView::AddedToWidget() {
camera_video_host_view_->Attach(camera_video_renderer_.host_window());
DisableEventHandlingInCameraVideoHostHierarchy();
camera_video_renderer_.Initialize();
}
bool CameraPreviewView::OnMousePressed(const ui::MouseEvent& event) {
camera_controller_->StartDraggingPreview(GetEventScreenLocation(event));
return true;
}
bool CameraPreviewView::OnMouseDragged(const ui::MouseEvent& event) {
camera_controller_->ContinueDraggingPreview(GetEventScreenLocation(event));
return true;
}
void CameraPreviewView::OnMouseReleased(const ui::MouseEvent& event) {
camera_controller_->EndDraggingPreview(GetEventScreenLocation(event),
false);
}
void CameraPreviewView::OnGestureEvent(ui::GestureEvent* event) {
const gfx::PointF screen_location = GetEventScreenLocation(*event);
switch (event->type()) {
case ui::ET_GESTURE_SCROLL_BEGIN:
camera_controller_->StartDraggingPreview(screen_location);
break;
case ui::ET_GESTURE_SCROLL_UPDATE:
DCHECK(camera_controller_->is_drag_in_progress());
camera_controller_->ContinueDraggingPreview(screen_location);
break;
case ui::ET_SCROLL_FLING_START:
break;
case ui::ET_GESTURE_SCROLL_END:
DCHECK(camera_controller_->is_drag_in_progress());
camera_controller_->EndDraggingPreview(screen_location,
true);
break;
case ui::ET_GESTURE_END:
if (camera_controller_->is_drag_in_progress()) {
camera_controller_->EndDraggingPreview(screen_location,
true);
}
break;
case ui::ET_GESTURE_TAP:
has_been_tapped_ = true;
RefreshResizeButtonVisibility();
has_been_tapped_ = false;
break;
default:
break;
}
event->StopPropagation();
event->SetHandled();
}
void CameraPreviewView::OnMouseEntered(const ui::MouseEvent& event) {
RefreshResizeButtonVisibility();
}
void CameraPreviewView::OnMouseExited(const ui::MouseEvent& event) {
ScheduleRefreshResizeButtonVisibility();
}
void CameraPreviewView::Layout() {
const gfx::Size resize_button_size = resize_button_->GetPreferredSize();
const gfx::Rect bounds(
(width() - resize_button_size.width()) / 2.f,
height() - resize_button_size.height() -
capture_mode::kSpaceBetweenResizeButtonAndCameraPreview -
capture_mode::kCameraPreviewBorderSize,
resize_button_size.width(), resize_button_size.height());
resize_button_->SetBoundsRect(bounds);
camera_video_host_view_->SetBoundsRect(GetContentsBounds());
DCHECK_EQ(width(), height());
auto* layer = camera_video_renderer_.host_window()->layer();
layer->SetRoundedCornerRadius(gfx::RoundedCornersF(height() / 2.f));
layer->SetIsFastRoundedCorner(true);
if (has_focus())
PseudoFocus();
}
void CameraPreviewView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
views::View::GetAccessibleNodeData(node_data);
node_data->role = ax::mojom::Role::kVideo;
node_data->SetName(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_FOCUSED));
}
views::View* CameraPreviewView::GetView() {
return this;
}
std::unique_ptr<views::HighlightPathGenerator>
CameraPreviewView::CreatePathGenerator() {
return std::make_unique<views::CircleHighlightPathGenerator>(
gfx::Insets(views::FocusRing::kDefaultHaloThickness / 2));
}
void CameraPreviewView::OnAccessibilityStatusChanged() {
RefreshResizeButtonVisibility();
}
void CameraPreviewView::OnAccessibilityControllerShutdown() {
accessibility_observation_.Reset();
}
void CameraPreviewView::OnResizeButtonPressed() {
camera_controller_->ToggleCameraPreviewSize();
UpdateResizeButton();
}
void CameraPreviewView::UpdateResizeButton() {
resize_button_->SetImageModel(
views::Button::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(
GetIconOfResizeButton(
camera_controller_->is_camera_preview_collapsed()),
kColorAshIconColorPrimary));
UpdateResizeButtonTooltip();
}
void CameraPreviewView::UpdateResizeButtonTooltip() {
resize_button_->SetTooltipText(l10n_util::GetStringUTF16(
camera_controller_->is_camera_preview_collapsed()
? IDS_ASH_SCREEN_CAPTURE_TOOLTIP_EXPAND_SELFIE_CAMERA
: IDS_ASH_SCREEN_CAPTURE_TOOLTIP_COLLAPSE_SELFIE_CAMERA));
}
void CameraPreviewView::DisableEventHandlingInCameraVideoHostHierarchy() {
camera_video_host_view_->SetCanProcessEventsWithinSubtree(false);
camera_video_host_view_->SetFocusBehavior(FocusBehavior::NEVER);
auto* const host_window = camera_video_renderer_.host_window();
auto* const widget_window = GetWidget()->GetNativeWindow();
DCHECK(host_window);
DCHECK(host_window->parent());
DCHECK(widget_window);
for (auto* window = host_window; window && window != widget_window;
window = window->parent()) {
window->SetEventTargetingPolicy(aura::EventTargetingPolicy::kNone);
}
}
void CameraPreviewView::FadeInResizeButton() {
resize_button_->SetVisible(true);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(kResizeButtonFadeInDuration)
.SetOpacity(resize_button_->layer(), 1.0f);
}
void CameraPreviewView::FadeOutResizeButton() {
views::AnimationBuilder()
.OnEnded(base::BindOnce(
[](base::WeakPtr<CameraPreviewView> camera_preview_view) {
if (camera_preview_view)
camera_preview_view->resize_button()->SetVisible(false);
},
weak_ptr_factory_.GetWeakPtr()))
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(kResizeButtonFadeOutDuration)
.SetOpacity(resize_button_->layer(), 0.0f);
}
void CameraPreviewView::ScheduleRefreshResizeButtonVisibility() {
resize_button_hide_timer_.Start(
FROM_HERE, capture_mode::kResizeButtonShowDuration, this,
&CameraPreviewView::RefreshResizeButtonVisibility);
}
float CameraPreviewView::CalculateResizeButtonTargetOpacity() {
if (!is_collapsible_ || camera_controller_->is_drag_in_progress())
return 0.f;
if (Shell::Get()->accessibility_controller()->IsSwitchAccessRunning() ||
IsMouseHovered() || resize_button_->IsMouseHovered() ||
resize_button_->has_focus() || has_been_tapped_) {
return 1.f;
}
return 0.f;
}
void CameraPreviewView::BlurA11yFocus() {
PseudoBlur();
resize_button_->PseudoBlur();
UpdateA11yOverrideWindow();
}
BEGIN_METADATA(CameraPreviewView, views::View)
END_METADATA
}