blob: b6cecfc5fef30a9b98f9d06c14403397f7e1123f [file] [log] [blame]
// Copyright 2021 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/recording_overlay_controller.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/stop_recording_button_tray.h"
#include "ash/projector/projector_annotation_tray.h"
#include "ash/public/cpp/capture_mode/recording_overlay_view.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/system/status_area_widget.h"
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#include "ui/compositor/layer_type.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/window_properties.h"
namespace ash {
namespace {
// When recording a non-root window (i.e. kWindow recording source), the overlay
// is added as a direct child of the window being recorded, and stacked on top
// of all children. This is so that the overlay contents show up in the
// recording above everything else.
//
// + window_being_recorded
// |
// + (Some other child windows hosting contents of the window)
// |
// + Recording overlay widget
//
// (Note that bottom-most child are the top-most child in terms of z-order).
//
// However, when recording the root window (i.e. kFullscreen or kRegion
// recording sources), the overlay is added as a child of the menu container.
// The menu container is high enough in terms of z-order, making the overlay on
// top of most things. However, it's also the same container used by the
// projector bar (which we want to be on top of the overlay, since it has the
// button to toggle the overlay off, and we don't want the overlay to block
// events going to that button). Therefore, the overlay is stacked at the bottom
// of the menu container's children. See UpdateWidgetStacking() below.
//
// + Menu container
// |
// + Recording overlay widget
// |
// + Projector bar widget
//
// TODO(https://crbug.com/1253011): Revise this parenting and z-ordering once
// the deprecated Projector toolbar is removed and replaced by the shelf-pod
// based new tools.
aura::Window* GetWidgetParent(aura::Window* window_being_recorded) {
return window_being_recorded->IsRootWindow()
? window_being_recorded->GetChildById(kShellWindowId_MenuContainer)
: window_being_recorded;
}
// Given the `bounds_in_parent` of the overlay widget, returns the bounds in the
// correct coordinate system depending on whether the `overlay_window_parent`
// uses screen coordinates or not.
gfx::Rect MaybeAdjustOverlayBounds(const gfx::Rect& bounds_in_parent,
aura::Window* overlay_window_parent) {
DCHECK(overlay_window_parent);
if (!overlay_window_parent->GetProperty(wm::kUsesScreenCoordinatesKey))
return bounds_in_parent;
gfx::Rect bounds_in_screen = bounds_in_parent;
wm::ConvertRectToScreen(overlay_window_parent, &bounds_in_screen);
return bounds_in_screen;
}
// Defines a window targeter that will be installed on the overlay widget's
// window so that we can allow located events over the projector shelf pod or
// its associated bubble widget to go through and not be consumed by the
// overlay. This enables the user to interact with the annotation tools while
// annotating.
class OverlayTargeter : public aura::WindowTargeter {
public:
explicit OverlayTargeter(aura::Window* overlay_window)
: overlay_window_(overlay_window) {}
OverlayTargeter(const OverlayTargeter&) = delete;
OverlayTargeter& operator=(const OverlayTargeter&) = delete;
~OverlayTargeter() override = default;
// aura::WindowTargeter:
ui::EventTarget* FindTargetForEvent(ui::EventTarget* root,
ui::Event* event) override {
if (event->IsLocatedEvent()) {
auto* root_window = overlay_window_->GetRootWindow();
auto* status_area_widget =
RootWindowController::ForWindow(root_window)->GetStatusAreaWidget();
StopRecordingButtonTray* stop_recording_button =
status_area_widget->stop_recording_button_tray();
auto screen_location = event->AsLocatedEvent()->root_location();
wm::ConvertPointToScreen(root_window, &screen_location);
Shelf* shelf = RootWindowController::ForWindow(root_window)->shelf();
// To be able to bring the auto-hidden shelf back even while annotation is
// active, we expose a slim 1dp region at the edge of the screen in which
// the shelf is aligned. Events in that region will not be consumed so
// that they can be used to show the auto-hidden shelf.
if (!shelf->IsVisible()) {
gfx::Rect root_window_bounds_in_screen =
root_window->GetBoundsInScreen();
const int display_width = root_window_bounds_in_screen.width();
const int display_height = root_window_bounds_in_screen.height();
const gfx::Rect shelf_activation_bounds =
shelf->SelectValueForShelfAlignment(
gfx::Rect(0, display_height - 1, display_width, 1),
gfx::Rect(0, 0, 1, display_height),
gfx::Rect(display_width - 1, 0, 1, display_height));
if (shelf_activation_bounds.Contains(screen_location))
return nullptr;
}
// To be able to end video recording even while annotation is active,
// let events over the stop recording button to go through.
if (stop_recording_button && stop_recording_button->visible_preferred() &&
stop_recording_button->GetBoundsInScreen().Contains(
screen_location)) {
return nullptr;
}
ProjectorAnnotationTray* annotations =
status_area_widget->projector_annotation_tray();
if (annotations && annotations->visible_preferred()) {
// Let events over the projector shelf pod to go through.
if (annotations->GetBoundsInScreen().Contains(screen_location))
return nullptr;
// Let events over the projector bubble widget (if shown) to go through.
views::Widget* bubble_widget = annotations->GetBubbleWidget();
if (bubble_widget && bubble_widget->IsVisible() &&
bubble_widget->GetWindowBoundsInScreen().Contains(
screen_location)) {
return nullptr;
}
// Ensure that the annotator bubble is closed when a press event is
// triggered.
if (event->type() == ui::ET_MOUSE_PRESSED ||
event->type() == ui::ET_TOUCH_PRESSED) {
annotations->ClickedOutsideBubble();
}
}
}
return aura::WindowTargeter::FindTargetForEvent(root, event);
}
private:
aura::Window* const overlay_window_;
};
} // namespace
RecordingOverlayController::RecordingOverlayController(
aura::Window* window_being_recorded,
const gfx::Rect& initial_bounds_in_parent) {
DCHECK(window_being_recorded);
views::Widget::InitParams params(
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.name = "RecordingOverlayWidget";
params.child = true;
params.parent = GetWidgetParent(window_being_recorded);
// The overlay hosts transparent contents so actual contents of the window
// being recorded shows up underneath.
params.layer_type = ui::LAYER_NOT_DRAWN;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.bounds =
MaybeAdjustOverlayBounds(initial_bounds_in_parent, params.parent);
// The overlay window does not receive any events until it's shown and
// enabled. See |Start()| below.
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.accept_events = false;
overlay_widget_->Init(std::move(params));
recording_overlay_view_ = overlay_widget_->SetContentsView(
CaptureModeController::Get()->CreateRecordingOverlayView());
auto* overlay_window = overlay_widget_->GetNativeWindow();
overlay_window->SetEventTargeter(
std::make_unique<OverlayTargeter>(overlay_window));
UpdateWidgetStacking();
}
void RecordingOverlayController::Toggle() {
is_enabled_ = !is_enabled_;
if (is_enabled_)
Start();
else
Stop();
}
void RecordingOverlayController::SetBounds(const gfx::Rect& bounds_in_parent) {
overlay_widget_->SetBounds(MaybeAdjustOverlayBounds(
bounds_in_parent, overlay_widget_->GetNativeWindow()));
}
aura::Window* RecordingOverlayController::GetOverlayNativeWindow() {
return overlay_widget_->GetNativeWindow();
}
void RecordingOverlayController::Start() {
DCHECK(is_enabled_);
overlay_widget_->GetNativeWindow()->SetEventTargetingPolicy(
aura::EventTargetingPolicy::kTargetAndDescendants);
overlay_widget_->Show();
}
void RecordingOverlayController::Stop() {
DCHECK(!is_enabled_);
overlay_widget_->GetNativeWindow()->SetEventTargetingPolicy(
aura::EventTargetingPolicy::kNone);
overlay_widget_->Hide();
}
void RecordingOverlayController::UpdateWidgetStacking() {
auto* overlay_window = overlay_widget_->GetNativeWindow();
auto* parent = overlay_window->parent();
DCHECK(parent);
// See more info in the docs of GetWidgetParent() above.
if (parent->GetId() == kShellWindowId_MenuContainer)
parent->StackChildAtBottom(overlay_window);
else
parent->StackChildAtTop(overlay_window);
}
} // namespace ash