chromium/ash/annotator/annotations_overlay_controller.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/annotator/annotations_overlay_controller.h"

#include "ash/accessibility/magnifier/docked_magnifier_controller.h"
#include "ash/annotator/annotation_tray.h"
#include "ash/annotator/annotator_controller.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/stop_recording_button_tray.h"
#include "ash/public/cpp/annotator/annotations_overlay_view.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/system/status_area_widget.h"
#include "base/memory/raw_ptr.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/gfx/geometry/rect_f.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 annotating on a non-root window, the overlay is added as a direct
// child of the window, and stacked on top of all children. This is so that
// the overlay contents show up above everything else.
//
//   + window
//       |
//       + (Some other child windows hosting contents of the window)
//       |
//       + Annotations overlay widget
//
// (Note that bottom-most child are the top-most child in terms of z-order).
//
// However, when annotating on the root window, 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
//     |
//     + Annotations overlay widget
//     |
//     + Projector bar widget
//
// TODO(crbug.com/40199022): 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) {
  return window->IsRootWindow()
             ? window->GetChildById(kShellWindowId_MenuContainer)
             : window;
}

// 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;
      }

      AnnotationTray* annotations = status_area_widget->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->IsLocatedEvent() &&
            (event->type() == ui::EventType::kMousePressed ||
             event->type() == ui::EventType::kTouchPressed)) {
          annotations->ClickedOutsideBubble(*event->AsLocatedEvent());
        }
      }
    }

    return aura::WindowTargeter::FindTargetForEvent(root, event);
  }

 private:
  const raw_ptr<aura::Window> overlay_window_;
};

}  // namespace

AnnotationsOverlayController::AnnotationsOverlayController(
    aura::Window* window,
    std::optional<gfx::Rect> partial_region_bounds)
    : window_(window), partial_region_bounds_(partial_region_bounds) {
  DCHECK(window_);
  const gfx::Rect initial_bounds_in_parent = GetOverlayWidgetBounds();
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
  params.name = "AnnotationsOverlayWidget";
  params.child = true;
  params.parent = GetWidgetParent(window_);

  // The overlay hosts transparent contents so actual contents of the window
  // 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));
  annotations_overlay_view_ = overlay_widget_->SetContentsView(
      Shell::Get()->annotator_controller()->CreateAnnotationsOverlayView());
  auto* overlay_window = overlay_widget_->GetNativeWindow();
  overlay_window->SetEventTargeter(
      std::make_unique<OverlayTargeter>(overlay_window));
  UpdateWidgetStacking();
  display_observation_.Observe(display::Screen::GetScreen());
  window_observation_.Observe(window_);
}

AnnotationsOverlayController::~AnnotationsOverlayController() {
  Reset();
}

void AnnotationsOverlayController::Toggle() {
  is_enabled_ = !is_enabled_;
  if (is_enabled_)
    Start();
  else
    Stop();
}

aura::Window* AnnotationsOverlayController::GetOverlayNativeWindow() {
  return overlay_widget_->GetNativeWindow();
}

void AnnotationsOverlayController::OnWindowBoundsChanged(
    aura::Window* window,
    const gfx::Rect& old_bounds,
    const gfx::Rect& new_bounds,
    ui::PropertyChangeReason reason) {
  SetBounds(GetOverlayWidgetBounds());
}

void AnnotationsOverlayController::OnWindowDestroying(aura::Window* window) {
  Reset();
}

void AnnotationsOverlayController::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t metrics) {
  // TODO(b/342104047): Check if DISPLAY_METRIC_BOUNDS and
  // DISPLAY_METRIC_ROTATION need to be considered here as well.
  if (metrics & DISPLAY_METRIC_WORK_AREA) {
    SetBounds(GetOverlayWidgetBounds());
  }
}

void AnnotationsOverlayController::Start() {
  DCHECK(is_enabled_);

  overlay_widget_->GetNativeWindow()->SetEventTargetingPolicy(
      aura::EventTargetingPolicy::kTargetAndDescendants);
  overlay_widget_->Show();
}

void AnnotationsOverlayController::Stop() {
  DCHECK(!is_enabled_);

  overlay_widget_->GetNativeWindow()->SetEventTargetingPolicy(
      aura::EventTargetingPolicy::kNone);
  overlay_widget_->Hide();
}

void AnnotationsOverlayController::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);
}

void AnnotationsOverlayController::SetBounds(
    const gfx::Rect& bounds_in_parent) {
  overlay_widget_->SetBounds(MaybeAdjustOverlayBounds(
      bounds_in_parent, overlay_widget_->GetNativeWindow()));
}

gfx::Rect AnnotationsOverlayController::GetOverlayWidgetBounds() const {
  gfx::Rect bounds =
      partial_region_bounds_.has_value()
          ? capture_mode_util::GetEffectivePartialRegionBounds(
                partial_region_bounds_.value(), window_->GetRootWindow())
          : gfx::Rect(window_->bounds().size());
  bounds.Subtract(
      Shell::Get()
          ->docked_magnifier_controller()
          ->GetTotalMagnifierBoundsForRoot(window_->GetRootWindow()));
  return bounds;
}

void AnnotationsOverlayController::Reset() {
  window_observation_.Reset();
  display_observation_.Reset();
  overlay_widget_.reset();
  annotations_overlay_view_ = nullptr;
  window_ = nullptr;
}

}  // namespace ash