blob: 4b7ec3e1fa98ec03e9f643b0b21ca779e3afa23b [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/video_recording_watcher.h"
#include <memory>
#include "ash/accessibility/magnifier/docked_magnifier_controller.h"
#include "ash/capture_mode/capture_mode_camera_controller.h"
#include "ash/capture_mode/capture_mode_camera_preview_view.h"
#include "ash/capture_mode/capture_mode_constants.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_metrics.h"
#include "ash/capture_mode/recording_overlay_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/projector/projector_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/aura/cursor/cursor_lookup.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
// Recording is performed at a rate of 30 FPS. Any non-pressed/-released mouse
// events that are too frequent will be throttled. We use the frame duration as
// the minimum delay between any two successive such events that we use to
// update the cursor overlay.
constexpr base::TimeDelta kCursorEventsThrottleDelay = base::Hertz(30);
// Window resizes can be done on many intermediate steps. This delay is used to
// throttle these resize events so that we send the final size of the window to
// the recording service when it stabilizes.
constexpr base::TimeDelta kWindowSizeChangeThrottleDelay =
base::Milliseconds(250);
// Returns true if |window_1| and |window_2| are both windows that belong to
// the same Desk. Note that it will return false for windows that don't belong
// to any desk (such as always-on-top windows or PIPs).
bool AreWindowsOnSameDesk(aura::Window* window_1, aura::Window* window_2) {
auto* container_1 = desks_util::GetDeskContainerForContext(window_1);
auto* container_2 = desks_util::GetDeskContainerForContext(window_2);
return container_1 && container_2 && container_1 == container_2;
}
// Gets the mouse cursor location in the coordinates of the given |window|. Use
// this if a mouse event is not available.
gfx::PointF GetCursorLocationInWindow(aura::Window* window) {
gfx::PointF cursor_point(
display::Screen::GetScreen()->GetCursorScreenPoint());
wm::ConvertPointFromScreen(window, &cursor_point);
return cursor_point;
}
// Gets the location of the given mouse |event| in the coordinates of the given
// |window|.
gfx::PointF GetEventLocationInWindow(aura::Window* window,
const ui::MouseEvent& event) {
aura::Window* target = static_cast<aura::Window*>(event.target());
gfx::PointF location = event.location_f();
if (target != window)
aura::Window::ConvertPointToTarget(target, window, &location);
return location;
}
// Returns the cursor overlay bounds as defined by the documentation of the
// FrameSinkVideoCaptureOverlay. The bounds should be relative within the bounds
// of the recorded frame sink (i.e. in the range [0.f, 1.f) for both cursor
// origin and size).
gfx::RectF GetCursorOverlayBounds(
const aura::Window* recorded_window,
const gfx::PointF& location_in_recorded_window,
const gfx::Point& cursor_hotspot,
float cursor_image_scale_factor,
const SkBitmap& cursor_bitmap) {
DCHECK(recorded_window);
DCHECK_GT(cursor_image_scale_factor, 0);
// The video size, and the resolution constraints will be matching the size of
// the recorded window (whether a root or a non-root window). Hence, the
// bounds of the cursor overlay should be relative to that size.
const auto window_size = recorded_window->bounds().size();
if (window_size.IsEmpty())
return gfx::RectF();
const gfx::PointF cursor_hotspot_dip =
gfx::ConvertPointToDips(cursor_hotspot, cursor_image_scale_factor);
const gfx::SizeF cursor_size_dip = gfx::ConvertSizeToDips(
gfx::SizeF(cursor_bitmap.width(), cursor_bitmap.height()),
cursor_image_scale_factor);
gfx::RectF cursor_relative_bounds(
location_in_recorded_window - cursor_hotspot_dip.OffsetFromOrigin(),
cursor_size_dip);
cursor_relative_bounds.Scale(1.f / window_size.width(),
1.f / window_size.height());
return cursor_relative_bounds;
}
CameraPreviewView* GetCameraPreviewView() {
auto* camera_controller = CaptureModeController::Get()->camera_controller();
return camera_controller ? camera_controller->camera_preview_view() : nullptr;
}
} // namespace
// -----------------------------------------------------------------------------
// RecordedWindowRootObserver:
// Defines an observer to observe the hierarchy changes of the root window under
// which the recorded window resides. This is only constructed when performing a
// window recording type.
class RecordedWindowRootObserver : public aura::WindowObserver {
public:
RecordedWindowRootObserver(aura::Window* root, VideoRecordingWatcher* owner)
: root_(root), owner_(owner) {
DCHECK(root_);
DCHECK(owner_);
DCHECK(root_->IsRootWindow());
DCHECK_EQ(owner_->recording_source_, CaptureModeSource::kWindow);
root_->AddObserver(this);
}
RecordedWindowRootObserver(const RecordedWindowRootObserver&) = delete;
RecordedWindowRootObserver& operator=(const RecordedWindowRootObserver&) =
delete;
~RecordedWindowRootObserver() override { root_->RemoveObserver(this); }
// aura::WindowObserver:
void OnWindowHierarchyChanged(const HierarchyChangeParams& params) override {
DCHECK_EQ(params.receiver, root_);
owner_->OnRootHierarchyChanged(params.target);
}
void OnWindowDestroying(aura::Window* window) override {
// We should never get here, as the recorded window gets moved to a
// different display before the root of another is destroyed. So this root
// observer should have been destroyed already.
NOTREACHED();
}
private:
aura::Window* const root_;
VideoRecordingWatcher* const owner_;
};
// -----------------------------------------------------------------------------
// VideoRecordingWatcher:
VideoRecordingWatcher::VideoRecordingWatcher(
CaptureModeController* controller,
aura::Window* window_being_recorded,
mojo::PendingRemote<viz::mojom::FrameSinkVideoCaptureOverlay>
cursor_capture_overlay,
bool projector_mode)
: controller_(controller),
cursor_manager_(Shell::Get()->cursor_manager()),
window_being_recorded_(window_being_recorded),
current_root_(window_being_recorded->GetRootWindow()),
recording_source_(controller_->source()),
cursor_capture_overlay_remote_(std::move(cursor_capture_overlay)),
is_in_projector_mode_(projector_mode) {
DCHECK(controller_);
DCHECK(window_being_recorded_);
DCHECK(current_root_);
DCHECK(!is_in_projector_mode_ || features::IsProjectorEnabled());
if (!window_being_recorded_->IsRootWindow()) {
DCHECK_EQ(recording_source_, CaptureModeSource::kWindow);
non_root_window_capture_request_ =
window_being_recorded_->MakeWindowCapturable();
root_observer_ =
std::make_unique<RecordedWindowRootObserver>(current_root_, this);
Shell::Get()->activation_client()->AddObserver(this);
} else {
// We only need to observe the changes in the state of the software-
// composited cursor when recording a root window (i.e. fullscreen or
// partial region capture), since the software cursor is in the layer
// subtree of the root window, and will be captured by the frame sink video
// capturer automatically without the need for the cursor overlay. In this
// case we need to avoid producing a video with two overlapping cursors.
// When recording a window however, the software cursor is not in its layer
// subtree, and has to always be captured using the cursor overlay.
auto* cursor_window_controller =
Shell::Get()->window_tree_host_manager()->cursor_window_controller();
// Note that the software cursor might have already been enabled prior to
// the recording starting.
force_cursor_overlay_hidden_ =
cursor_window_controller->is_cursor_compositing_enabled();
cursor_window_controller->AddObserver(this);
}
if (recording_source_ == CaptureModeSource::kRegion)
partial_region_bounds_ = controller_->user_capture_region();
display::Screen::GetScreen()->AddObserver(this);
window_being_recorded_->AddObserver(this);
TabletModeController::Get()->AddObserver(this);
// Note the following:
// 1- We add |this| as a pre-target handler of the |window_being_recorded_| as
// opposed to |Env|. This ensures that we only get mouse events when the
// window being recorded is the target. This is more efficient since we
// won't get any event when the curosr is in a different display, or
// targeting a different window.
// 2- We use the |kAccessibility| priority to ensure that we get these events
// before other pre-target handlers can consume them (e.g. when opening a
// capture mode session to take a screenshot while recording a video).
window_being_recorded_->AddPreTargetHandler(
this, ui::EventTarget::Priority::kAccessibility);
auto* camera_controller = controller_->camera_controller();
if (camera_controller)
camera_controller->OnRecordingStarted(is_in_projector_mode_);
if (is_in_projector_mode_) {
recording_overlay_controller_ =
std::make_unique<RecordingOverlayController>(window_being_recorded_,
GetOverlayWidgetBounds());
}
if (features::IsProjectorEnabled()) {
ProjectorControllerImpl::Get()->OnRecordingStarted(current_root_,
is_in_projector_mode_);
}
}
VideoRecordingWatcher::~VideoRecordingWatcher() {
DCHECK(is_shutting_down_);
}
void VideoRecordingWatcher::ToggleRecordingOverlayEnabled() {
DCHECK(is_in_projector_mode_);
DCHECK(!is_shutting_down_);
DCHECK(recording_overlay_controller_);
recording_overlay_controller_->Toggle();
}
void VideoRecordingWatcher::ShutDown() {
is_shutting_down_ = true;
DCHECK(window_being_recorded_);
window_size_change_throttle_timer_.Stop();
cursor_events_throttle_timer_.Stop();
cursor_capture_overlay_remote_.reset();
root_observer_.reset();
recording_overlay_controller_.reset();
dimmers_.clear();
if (features::IsProjectorEnabled())
ProjectorControllerImpl::Get()->OnRecordingEnded(is_in_projector_mode_);
window_being_recorded_->RemovePreTargetHandler(this);
TabletModeController::Get()->RemoveObserver(this);
if (recording_source_ == CaptureModeSource::kWindow) {
Shell::Get()->activation_client()->RemoveObserver(this);
} else {
Shell::Get()
->window_tree_host_manager()
->cursor_window_controller()
->RemoveObserver(this);
}
// Move the `non_root_window_capture_request_` so that the
// `window_being_recorded_` is not capturable.
auto to_be_removed_request = std::move(non_root_window_capture_request_);
window_being_recorded_->RemoveObserver(this);
display::Screen::GetScreen()->RemoveObserver(this);
if (controller_->camera_controller()) {
controller_->camera_controller()->OnRecordingEnded();
}
}
aura::Window* VideoRecordingWatcher::GetCameraPreviewParentWindow() const {
DCHECK(window_being_recorded_);
return window_being_recorded_->IsRootWindow()
? window_being_recorded_->GetChildById(
kShellWindowId_MenuContainer)
: window_being_recorded_;
}
gfx::Rect VideoRecordingWatcher::GetCameraPreviewConfineBounds() const {
DCHECK(window_being_recorded_);
switch (recording_source_) {
case CaptureModeSource::kFullscreen:
return display::Screen::GetScreen()
->GetDisplayNearestWindow(window_being_recorded_)
.work_area();
case CaptureModeSource::kRegion: {
gfx::Rect capture_region = GetEffectivePartialRegionBounds();
wm::ConvertRectToScreen(current_root_, &capture_region);
return capture_region;
}
case CaptureModeSource::kWindow:
return gfx::Rect(window_being_recorded_->bounds().size());
}
}
void VideoRecordingWatcher::OnWindowParentChanged(aura::Window* window,
aura::Window* parent) {
DCHECK_EQ(window, window_being_recorded_);
DCHECK_EQ(recording_source_, CaptureModeSource::kWindow);
UpdateLayerStackingAndDimmers();
}
void VideoRecordingWatcher::OnWindowVisibilityChanged(aura::Window* window,
bool visible) {
if (window == window_being_recorded_)
UpdateShouldPaintLayer();
}
void VideoRecordingWatcher::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
if (is_in_projector_mode_)
recording_overlay_controller_->SetBounds(GetOverlayWidgetBounds());
if (recording_source_ != CaptureModeSource::kWindow)
return;
// We care only about size changes, since the location of the window won't
// affect the recorded video frames of it, however, the size of the window
// affects the size of the frames.
if (old_bounds.size() == new_bounds.size())
return;
window_size_change_throttle_timer_.Start(
FROM_HERE, kWindowSizeChangeThrottleDelay, this,
&VideoRecordingWatcher::OnWindowSizeChangeThrottleTimerFiring);
// The bounds of the camera preview should be updated if the bounds of the
// window being recorded is changed.
auto* camera_controller = controller_->camera_controller();
if (camera_controller)
camera_controller->MaybeUpdatePreviewWidget();
}
void VideoRecordingWatcher::OnWindowOpacitySet(
aura::Window* window,
ui::PropertyChangeReason reason) {
if (window == window_being_recorded_)
UpdateShouldPaintLayer();
}
void VideoRecordingWatcher::OnWindowStackingChanged(aura::Window* window) {
DCHECK_EQ(window, window_being_recorded_);
DCHECK_EQ(recording_source_, CaptureModeSource::kWindow);
UpdateLayerStackingAndDimmers();
}
void VideoRecordingWatcher::OnWindowDestroying(aura::Window* window) {
DCHECK_EQ(window, window_being_recorded_);
// EndVideoRecording() destroys |this|. No need to remove observer here, since
// it will be done in the destructor.
controller_->EndVideoRecording(EndRecordingReason::kDisplayOrWindowClosing);
}
void VideoRecordingWatcher::OnWindowDestroyed(aura::Window* window) {
DCHECK_EQ(window, window_being_recorded_);
// We should never get here, since OnWindowDestroying() calls
// EndVideoRecording() which deletes us.
NOTREACHED();
}
void VideoRecordingWatcher::OnWindowRemovingFromRootWindow(
aura::Window* window,
aura::Window* new_root) {
DCHECK_EQ(window, window_being_recorded_);
DCHECK_EQ(recording_source_, CaptureModeSource::kWindow);
root_observer_.reset();
current_root_ = new_root;
if (!new_root) {
// EndVideoRecording() destroys |this|.
controller_->EndVideoRecording(EndRecordingReason::kDisplayOrWindowClosing);
return;
}
root_observer_ =
std::make_unique<RecordedWindowRootObserver>(current_root_, this);
controller_->OnRecordedWindowChangingRoot(window_being_recorded_, new_root);
if (is_in_projector_mode_)
ProjectorControllerImpl::Get()->OnRecordedWindowChangingRoot(new_root);
}
void VideoRecordingWatcher::OnPaintLayer(const ui::PaintContext& context) {
if (!should_paint_layer_)
return;
DCHECK_NE(recording_source_, CaptureModeSource::kFullscreen);
ui::PaintRecorder recorder(context, layer()->size());
gfx::Canvas* canvas = recorder.canvas();
canvas->DrawColor(capture_mode::kDimmingShieldColor);
// We don't draw a region border around the recorded window. We just paint the
// above shield as a backdrop.
if (recording_source_ == CaptureModeSource::kWindow)
return;
gfx::ScopedCanvas scoped_canvas(canvas);
const float dsf = canvas->UndoDeviceScaleFactor();
gfx::Rect region =
gfx::ScaleToEnclosingRect(GetEffectivePartialRegionBounds(), dsf);
region.Inset(-capture_mode::kCaptureRegionBorderStrokePx);
canvas->FillRect(region, SK_ColorTRANSPARENT, SkBlendMode::kClear);
// Draw the region border.
cc::PaintFlags border_flags;
border_flags.setColor(capture_mode::kRegionBorderColor);
border_flags.setStyle(cc::PaintFlags::kStroke_Style);
border_flags.setStrokeWidth(capture_mode::kCaptureRegionBorderStrokePx);
canvas->DrawRect(gfx::RectF(region), border_flags);
}
void VideoRecordingWatcher::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
DCHECK_EQ(recording_source_, CaptureModeSource::kWindow);
UpdateLayerStackingAndDimmers();
}
void VideoRecordingWatcher::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t metrics) {
// A change in the work area, could mean that the docked magnifier state has
// changed, therefore we must update the overlay widget's bounds if any.
if (is_in_projector_mode_ && (metrics & DISPLAY_METRIC_WORK_AREA))
recording_overlay_controller_->SetBounds(GetOverlayWidgetBounds());
if (!(metrics &
(DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_ROTATION |
DISPLAY_METRIC_DEVICE_SCALE_FACTOR | DISPLAY_METRIC_WORK_AREA))) {
return;
}
const int64_t display_id =
display::Screen::GetScreen()->GetDisplayNearestWindow(current_root_).id();
if (display_id != display.id())
return;
const auto& root_bounds = current_root_->bounds();
controller_->PushNewRootSizeToRecordingService(
root_bounds.size(), current_root_->GetHost()->device_scale_factor());
// The bounds of camera preview should be updated accordingly if the display
// metrics is changed. When the capture source is `kWindow`, it will be
// handled in `OnWindowBoundsChanged`;
auto* camera_controller = controller_->camera_controller();
if (camera_controller && recording_source_ != CaptureModeSource::kWindow)
camera_controller->MaybeUpdatePreviewWidget();
// We don't show a dimming overlay when recording a fullscreen.
if (recording_source_ == CaptureModeSource::kFullscreen)
return;
DCHECK(layer());
layer()->SetBounds(root_bounds);
}
void VideoRecordingWatcher::OnDimmedWindowDestroying(
aura::Window* dimmed_window) {
dimmers_.erase(dimmed_window);
}
void VideoRecordingWatcher::OnDimmedWindowParentChanged(
aura::Window* dimmed_window) {
// If the dimmed window moves to another display or a different desk, we no
// longer dim it.
if (dimmed_window->GetRootWindow() != current_root_ ||
!AreWindowsOnSameDesk(dimmed_window, window_being_recorded_)) {
dimmers_.erase(dimmed_window);
}
}
void VideoRecordingWatcher::OnKeyEvent(ui::KeyEvent* event) {
if (event->type() != ui::ET_KEY_PRESSED)
return;
auto* camera_preview_view = GetCameraPreviewView();
if (camera_preview_view && camera_preview_view->MaybeHandleKeyEvent(event)) {
event->StopPropagation();
event->SetHandled();
return;
}
}
void VideoRecordingWatcher::OnMouseEvent(ui::MouseEvent* event) {
switch (event->type()) {
case ui::ET_MOUSEWHEEL:
case ui::ET_MOUSE_CAPTURE_CHANGED:
return;
case ui::ET_MOUSE_PRESSED: {
auto* camera_preview_view = GetCameraPreviewView();
if (camera_preview_view)
camera_preview_view->MaybeBlurFocus(*event);
}
[[fallthrough]];
case ui::ET_MOUSE_RELEASED:
// Pressed/released events are important, so we handle them immediately.
UpdateCursorOverlayNow(
GetEventLocationInWindow(window_being_recorded_, *event));
return;
default:
UpdateOrThrottleCursorOverlay(
GetEventLocationInWindow(window_being_recorded_, *event));
}
}
void VideoRecordingWatcher::OnTabletModeStarted() {
UpdateCursorOverlayNow(gfx::PointF());
}
void VideoRecordingWatcher::OnTabletModeEnded() {
UpdateCursorOverlayNow(GetCursorLocationInWindow(window_being_recorded_));
}
void VideoRecordingWatcher::OnCursorCompositingStateChanged(bool enabled) {
DCHECK_NE(recording_source_, CaptureModeSource::kWindow);
force_cursor_overlay_hidden_ = enabled;
UpdateCursorOverlayNow(
force_cursor_overlay_hidden_
? gfx::PointF()
: GetCursorLocationInWindow(window_being_recorded_));
}
gfx::Rect VideoRecordingWatcher::GetEffectivePartialRegionBounds() const {
DCHECK_EQ(recording_source_, CaptureModeSource::kRegion);
// TODO(afakhry): Consider having the region to anchor to the nearest corner,
// so that screen rotation doesn't result in the apparent change of the region
// position. Discussion with PM/UX determined that this is a low priority for
// now.
gfx::Rect result = partial_region_bounds_;
result.AdjustToFit(current_root_->bounds());
return result;
}
bool VideoRecordingWatcher::IsWindowDimmedForTesting(
aura::Window* window) const {
return dimmers_.contains(window);
}
void VideoRecordingWatcher::BindCursorOverlayForTesting(
mojo::PendingRemote<viz::mojom::FrameSinkVideoCaptureOverlay> overlay) {
cursor_capture_overlay_remote_.reset();
cursor_capture_overlay_remote_.Bind(std::move(overlay));
}
void VideoRecordingWatcher::FlushCursorOverlayForTesting() {
cursor_capture_overlay_remote_.FlushForTesting();
}
void VideoRecordingWatcher::SendThrottledWindowSizeChangedNowForTesting() {
window_size_change_throttle_timer_.FireNow();
}
void VideoRecordingWatcher::SetLayer(std::unique_ptr<ui::Layer> layer) {
if (layer) {
layer->set_delegate(this);
layer->SetName("Recording Shield");
}
LayerOwner::SetLayer(std::move(layer));
UpdateShouldPaintLayer();
UpdateLayerStackingAndDimmers();
}
void VideoRecordingWatcher::OnRootHierarchyChanged(aura::Window* target) {
DCHECK_EQ(recording_source_, CaptureModeSource::kWindow);
if (target != window_being_recorded_ && !dimmers_.contains(target) &&
CanIncludeWindowInMruList(target)) {
UpdateLayerStackingAndDimmers();
}
}
bool VideoRecordingWatcher::CalculateShouldPaintLayer() const {
if (recording_source_ == CaptureModeSource::kFullscreen)
return false;
if (recording_source_ == CaptureModeSource::kRegion)
return true;
DCHECK_EQ(recording_source_, CaptureModeSource::kWindow);
return window_being_recorded_->TargetVisibility() &&
window_being_recorded_->layer()->GetTargetVisibility() &&
window_being_recorded_->layer()->GetTargetOpacity() > 0;
}
void VideoRecordingWatcher::UpdateShouldPaintLayer() {
const bool new_value = CalculateShouldPaintLayer();
if (new_value == should_paint_layer_)
return;
should_paint_layer_ = new_value;
if (!should_paint_layer_) {
// If we're not painting the shield, we don't need the individual dimmers
// either.
dimmers_.clear();
}
if (layer())
layer()->SchedulePaint(layer()->bounds());
}
void VideoRecordingWatcher::UpdateLayerStackingAndDimmers() {
if (!layer())
return;
DCHECK_NE(recording_source_, CaptureModeSource::kFullscreen);
const bool is_recording_window =
recording_source_ == CaptureModeSource::kWindow;
ui::Layer* new_parent =
is_recording_window
? window_being_recorded_->layer()->parent()
: current_root_->GetChildById(kShellWindowId_OverlayContainer)
->layer();
ui::Layer* old_parent = layer()->parent();
DCHECK(new_parent || is_recording_window);
if (!new_parent && old_parent) {
// If the window gets removed from the hierarchy, we remove the shield layer
// too, as well as any dimming of windows we have.
old_parent->Remove(layer());
dimmers_.clear();
return;
}
if (new_parent != old_parent) {
// We get here the first time we parent the shield layer under the overlay
// container when we're recording a partial region, or when recording a
// window, and it gets moved to another display, or moved to a different
// desk.
new_parent->Add(layer());
layer()->SetBounds(current_root_->bounds());
}
// When recording a partial region, the shield layer is stacked at the top of
// everything in the overlay container.
if (!is_recording_window) {
new_parent->StackAtTop(layer());
return;
}
// However, when recording a window, we stack the shield layer below the
// recorded window's layer. This takes care of dimming any windows below the
// recorded window in the z-order.
new_parent->StackBelow(layer(), window_being_recorded_->layer());
// If the shield is not painted, all the individual dimmers should be removed.
if (!should_paint_layer_) {
dimmers_.clear();
return;
}
// For windows that are above the recorded window in the z-order and on the
// same display, they're dimmed separately.
const SkColor dimming_color = AshColorProvider::Get()->GetShieldLayerColor(
AshColorProvider::ShieldLayerType::kShield40);
// We use |kAllDesks| here for the following reasons:
// 1- A dimmed window can move out from the desk where the window being
// recorded is (either by keyboard shortcut or drag and drop in overview).
// 2- The recorded window itself can move out from the active desk to an
// inactive desk that has other windows.
// In (1), we want to remove the dimmers of those windows that moved out.
// In (2), we want to remove the dimmers of the window on the active desk, and
// create ones for the windows in the inactive desk (if any of them is above
// the recorded window).
const auto mru_windows =
Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
DesksMruType::kAllDesks);
bool did_find_recorded_window = false;
// Note that the order of |mru_windows| are from top-most first.
for (auto* window : mru_windows) {
if (window == window_being_recorded_) {
did_find_recorded_window = true;
continue;
}
// No need to dim windows that are below the window being recorded in
// z-order, or those on other displays, or other desks.
if (did_find_recorded_window || window->GetRootWindow() != current_root_ ||
!AreWindowsOnSameDesk(window, window_being_recorded_)) {
dimmers_.erase(window);
continue;
}
auto& dimmer = dimmers_[window];
if (!dimmer) {
dimmer = std::make_unique<WindowDimmer>(window, /*animate=*/false, this);
dimmer->SetDimColor(dimming_color);
dimmer->window()->Show();
}
}
}
gfx::NativeCursor VideoRecordingWatcher::GetCurrentCursor() const {
const auto cursor = cursor_manager_->GetCursor();
// See the documentation in cursor_type.mojom. |kNull| is treated exactly as
// |kPointer|.
return (cursor.type() == ui::mojom::CursorType::kNull)
? gfx::NativeCursor(ui::mojom::CursorType::kPointer)
: cursor;
}
void VideoRecordingWatcher::UpdateOrThrottleCursorOverlay(
const gfx::PointF& location) {
if (cursor_events_throttle_timer_.IsRunning()) {
throttled_cursor_location_ = location;
return;
}
UpdateCursorOverlayNow(location);
cursor_events_throttle_timer_.Start(
FROM_HERE, kCursorEventsThrottleDelay, this,
&VideoRecordingWatcher::OnCursorThrottleTimerFiring);
}
void VideoRecordingWatcher::UpdateCursorOverlayNow(
const gfx::PointF& location) {
// Cancel any pending throttled event.
cursor_events_throttle_timer_.Stop();
throttled_cursor_location_.reset();
if (!cursor_capture_overlay_remote_)
return;
if (force_cursor_overlay_hidden_ ||
TabletModeController::Get()->InTabletMode()) {
HideCursorOverlay();
return;
}
const gfx::RectF window_local_bounds(
gfx::SizeF(window_being_recorded_->bounds().size()));
if (!window_local_bounds.Contains(location)) {
HideCursorOverlay();
return;
}
const gfx::NativeCursor cursor = GetCurrentCursor();
DCHECK_NE(cursor.type(), ui::mojom::CursorType::kNull);
const float cursor_image_scale_factor = cursor.image_scale_factor();
const SkBitmap cursor_image = aura::GetCursorBitmap(cursor);
const gfx::RectF cursor_overlay_bounds = GetCursorOverlayBounds(
window_being_recorded_, location, aura::GetCursorHotspot(cursor),
cursor_image_scale_factor, cursor_image);
if (cursor != last_cursor_) {
if (cursor_image.drawsNothing()) {
last_cursor_ = gfx::NativeCursor();
HideCursorOverlay();
return;
}
last_cursor_ = cursor;
last_cursor_overlay_bounds_ = cursor_overlay_bounds;
cursor_capture_overlay_remote_->SetImageAndBounds(
cursor_image, last_cursor_overlay_bounds_);
return;
}
if (last_cursor_overlay_bounds_ == cursor_overlay_bounds)
return;
last_cursor_overlay_bounds_ = cursor_overlay_bounds;
cursor_capture_overlay_remote_->SetBounds(last_cursor_overlay_bounds_);
}
void VideoRecordingWatcher::HideCursorOverlay() {
DCHECK(cursor_capture_overlay_remote_);
// No need to rehide if already hidden.
if (last_cursor_overlay_bounds_ == gfx::RectF())
return;
last_cursor_overlay_bounds_ = gfx::RectF();
cursor_capture_overlay_remote_->SetBounds(last_cursor_overlay_bounds_);
}
void VideoRecordingWatcher::OnCursorThrottleTimerFiring() {
if (throttled_cursor_location_)
UpdateCursorOverlayNow(*throttled_cursor_location_);
}
void VideoRecordingWatcher::OnWindowSizeChangeThrottleTimerFiring() {
DCHECK_EQ(recording_source_, CaptureModeSource::kWindow);
controller_->OnRecordedWindowSizeChanged(
window_being_recorded_->bounds().size());
}
gfx::Rect VideoRecordingWatcher::GetOverlayWidgetBounds() const {
gfx::Rect bounds = recording_source_ == CaptureModeSource::kRegion
? GetEffectivePartialRegionBounds()
: gfx::Rect(window_being_recorded_->bounds().size());
bounds.Subtract(Shell::Get()
->docked_magnifier_controller()
->GetTotalMagnifierBoundsForRoot(
window_being_recorded_->GetRootWindow()));
return bounds;
}
} // namespace ash