blob: b0aeb45900af93133e535240782da7e9d1605a3f [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/image_editor/screenshot_flow.h"
#include <memory>
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/supports_user_data.h"
#include "build/build_config.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/cursor/cursor.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/events/event_target.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/gfx/render_text.h"
#include "ui/snapshot/snapshot.h"
#include "ui/views/background.h"
#if BUILDFLAG(IS_MAC)
#include "chrome/browser/image_editor/event_capture_mac.h"
#include "components/lens/lens_features.h"
#include "content/public/browser/render_view_host.h"
#include "ui/views/widget/widget.h"
#endif
#if defined(USE_AURA)
#include "ui/aura/window.h"
#include "ui/wm/core/window_util.h"
#endif
namespace image_editor {
// Colors for semitransparent overlay.
static constexpr SkColor kColorSemitransparentOverlayMask =
SkColorSetARGB(0x9F, 0x00, 0x00, 0x00);
static constexpr SkColor kColorSemitransparentOverlayVisible =
SkColorSetARGB(0x00, 0x00, 0x00, 0x00);
static constexpr SkColor kColorSelectionRect = SkColorSetRGB(0xEE, 0xEE, 0xEE);
// Minimum selection rect edge size to treat as a valid capture region.
static constexpr int kMinimumValidSelectionEdgePixels = 30;
ScreenshotCapturedData::ScreenshotCapturedData() = default;
ScreenshotCapturedData::~ScreenshotCapturedData() = default;
ScreenshotFlow::ScreenshotFlow(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
web_contents_(web_contents->GetWeakPtr()) {
weak_this_ = weak_factory_.GetWeakPtr();
}
ScreenshotFlow::~ScreenshotFlow() {
RemoveUIOverlay();
}
void ScreenshotFlow::CreateAndAddUIOverlay() {
if (screen_capture_layer_)
return;
web_contents_observer_ = std::make_unique<UnderlyingWebContentsObserver>(
web_contents_.get(), this);
screen_capture_layer_ =
std::make_unique<ui::Layer>(ui::LayerType::LAYER_TEXTURED);
screen_capture_layer_->SetName("ScreenshotRegionSelectionLayer");
screen_capture_layer_->SetFillsBoundsOpaquely(false);
screen_capture_layer_->set_delegate(this);
#if BUILDFLAG(IS_MAC)
gfx::Rect bounds = web_contents_->GetViewBounds();
const gfx::NativeView web_contents_view =
web_contents_->GetContentNativeView();
views::Widget* widget =
views::Widget::GetWidgetForNativeView(web_contents_view);
ui::Layer* content_layer = widget->GetLayer();
const gfx::Rect offset_bounds = widget->GetWindowBoundsInScreen();
bounds.Offset(-offset_bounds.x(), -offset_bounds.y());
event_capture_mac_ = std::make_unique<EventCaptureMac>(
this,
base::BindOnce(&ScreenshotFlow::CancelCapture, base::Unretained(this)),
web_contents_view, web_contents_->GetTopLevelNativeWindow());
#else
const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
ui::Layer* content_layer = native_window->layer();
const gfx::Rect bounds = native_window->bounds();
// Capture mouse down and drag events on our window.
event_capture_.Observe(native_window);
#endif
content_layer->Add(screen_capture_layer_.get());
content_layer->StackAtTop(screen_capture_layer_.get());
screen_capture_layer_->SetBounds(bounds);
screen_capture_layer_->SetVisible(true);
SetCursor(ui::mojom::CursorType::kCross);
// After setup is done, we should set the capture mode to active.
capture_mode_ = CaptureMode::SELECTION_RECTANGLE;
}
void ScreenshotFlow::RemoveUIOverlay() {
if (capture_mode_ == CaptureMode::NOT_CAPTURING)
return;
is_dragging_ = false;
capture_mode_ = CaptureMode::NOT_CAPTURING;
if (!web_contents_ || !screen_capture_layer_)
return;
#if BUILDFLAG(IS_MAC)
views::Widget* widget = views::Widget::GetWidgetForNativeView(
web_contents_->GetContentNativeView());
if (!widget)
return;
ui::Layer* content_layer = widget->GetLayer();
event_capture_mac_.reset();
#else
const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
#if BUILDFLAG(IS_WIN)
// This handles cases where the overlay is removed while a drag is still in
// progress, including if ScreenshotFlow is destroyed. It is safe to call
// ReleaseCapture() even if the capture has not been set or has already been
// released.
native_window->ReleaseCapture();
#endif // BUILDFLAG(IS_WIN)
event_capture_.Reset();
ui::Layer* content_layer = native_window->layer();
#endif // else
content_layer->Remove(screen_capture_layer_.get());
screen_capture_layer_->set_delegate(nullptr);
screen_capture_layer_.reset();
// Restore the cursor to pointer; there's no corresponding GetCursor()
// to store the pre-capture-mode cursor, and the pointer will have moved
// in the meantime.
SetCursor(ui::mojom::CursorType::kPointer);
}
void ScreenshotFlow::Start(ScreenshotCaptureCallback flow_callback) {
flow_callback_ = std::move(flow_callback);
CreateAndAddUIOverlay();
RequestRepaint(gfx::Rect());
}
void ScreenshotFlow::StartFullscreenCapture(
ScreenshotCaptureCallback flow_callback) {
// Start and finish the capture process by screenshotting the full window.
// There is no region selection step in this mode.
flow_callback_ = std::move(flow_callback);
CaptureAndRunScreenshotCompleteCallback(ScreenshotCaptureResultCode::SUCCESS,
gfx::Rect(web_contents_->GetSize()));
}
void ScreenshotFlow::CaptureAndRunScreenshotCompleteCallback(
ScreenshotCaptureResultCode result_code,
gfx::Rect region) {
if (region.IsEmpty()) {
RunScreenshotCompleteCallback(result_code, gfx::Rect(), gfx::Image());
return;
}
gfx::Rect bounds = web_contents_->GetViewBounds();
ui::GrabSnapshotImageCallback screenshot_callback =
base::BindOnce(&ScreenshotFlow::RunScreenshotCompleteCallback, weak_this_,
result_code, bounds);
ui::GrabViewSnapshot(web_contents_->GetNativeView(), region,
std::move(screenshot_callback));
}
void ScreenshotFlow::CancelCapture() {
RemoveUIOverlay();
CaptureAndRunScreenshotCompleteCallback(
ScreenshotCaptureResultCode::USER_NAVIGATED_EXIT, gfx::Rect());
}
void ScreenshotFlow::OnKeyEvent(ui::KeyEvent* event) {
if (event->type() == ui::EventType::kKeyPressed &&
event->key_code() == ui::VKEY_ESCAPE) {
event->StopPropagation();
CompleteCapture(ScreenshotCaptureResultCode::USER_ESCAPE_EXIT, gfx::Rect());
}
}
void ScreenshotFlow::OnMouseEvent(ui::MouseEvent* event) {
if (!event->IsLocatedEvent())
return;
const ui::LocatedEvent* located_event = ui::LocatedEvent::FromIfValid(event);
if (!located_event)
return;
gfx::Point location = located_event->location();
gfx::Rect web_contents_bounds = web_contents_->GetViewBounds();
#if BUILDFLAG(IS_MAC)
// Offset |location| be relative to the WebContents widget, vs the parent
// window, recomputed rather than cached in case e.g. user disables
// bookmarks bar from another window.
const gfx::NativeView web_contents_view =
web_contents_->GetContentNativeView();
views::Widget* widget =
views::Widget::GetWidgetForNativeView(web_contents_view);
if (!widget)
return;
const gfx::Rect widget_bounds = widget->GetWindowBoundsInScreen();
location.set_x(location.x() + (widget_bounds.x() - web_contents_bounds.x()));
location.set_y(location.y() + (widget_bounds.y() - web_contents_bounds.y()));
#endif
switch (event->type()) {
case ui::EventType::kMouseMoved:
SetCursor(ui::mojom::CursorType::kCross);
event->SetHandled();
break;
case ui::EventType::kMousePressed:
if (event->IsOnlyLeftMouseButton()) {
// Don't capture initial clicks on browser ui outside the webcontents.
if (location.x() < 0 || location.y() < 0 ||
location.x() > web_contents_bounds.width() ||
location.y() > web_contents_bounds.height()) {
return;
}
SetIsDragging(true);
drag_start_ = location;
drag_end_ = location;
event->SetHandled();
}
break;
case ui::EventType::kMouseDragged:
if (event->IsOnlyLeftMouseButton() && is_dragging_) {
drag_end_ = location;
RequestRepaint(gfx::Rect());
event->SetHandled();
}
break;
case ui::EventType::kMouseReleased:
if ((capture_mode_ == CaptureMode::SELECTION_RECTANGLE ||
capture_mode_ == CaptureMode::SELECTION_ELEMENT) &&
is_dragging_) {
SetIsDragging(false);
AttemptRegionCapture(web_contents_bounds);
event->SetHandled();
}
break;
// This event type is never called on Mac.
case ui::EventType::kMousewheel:
if ((capture_mode_ == CaptureMode::SELECTION_RECTANGLE ||
capture_mode_ == CaptureMode::SELECTION_ELEMENT) &&
event->AsMouseWheelEvent()->y_offset() > 0 && is_dragging_) {
SetIsDragging(false);
AttemptRegionCapture(web_contents_bounds);
event->SetHandled();
}
break;
default:
break;
}
}
void ScreenshotFlow::OnScrollEvent(ui::ScrollEvent* event) {
// A single tap can create a scroll event, so ignore scroll starts and
// cancels but complete capture when scrolls actually occur.
if (event->type() == ui::EventType::kScrollFlingStart ||
event->type() == ui::EventType::kScrollFlingCancel) {
return;
}
gfx::Rect web_contents_bounds = web_contents_->GetViewBounds();
if ((capture_mode_ == CaptureMode::SELECTION_RECTANGLE ||
capture_mode_ == CaptureMode::SELECTION_ELEMENT) &&
event->y_offset() > 0 && is_dragging_) {
SetIsDragging(false);
AttemptRegionCapture(web_contents_bounds);
event->SetHandled();
}
}
void ScreenshotFlow::CompleteCapture(ScreenshotCaptureResultCode result_code,
const gfx::Rect& region) {
RemoveUIOverlay();
CaptureAndRunScreenshotCompleteCallback(result_code, region);
}
void ScreenshotFlow::RunScreenshotCompleteCallback(
ScreenshotCaptureResultCode result_code,
gfx::Rect bounds,
gfx::Image image) {
if (flow_callback_.is_null())
return;
ScreenshotCaptureResult result;
result.result_code = result_code;
result.image = image;
result.screen_bounds = bounds;
std::move(flow_callback_).Run(result);
}
bool ScreenshotFlow::IsUIOverlayShown() {
return screen_capture_layer_ != nullptr && screen_capture_layer_->visible();
}
void ScreenshotFlow::ResetUIOverlayBounds() {
#if BUILDFLAG(IS_MAC)
gfx::Rect bounds = web_contents_->GetViewBounds();
const gfx::NativeView web_contents_view =
web_contents_->GetContentNativeView();
views::Widget* widget =
views::Widget::GetWidgetForNativeView(web_contents_view);
if (widget != nullptr) {
const gfx::Rect offset_bounds = widget->GetWindowBoundsInScreen();
bounds.Offset(-offset_bounds.x(), -offset_bounds.y());
}
#else
const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
const gfx::Rect bounds = native_window->bounds();
#endif
screen_capture_layer_->SetBounds(bounds);
}
void ScreenshotFlow::OnPaintLayer(const ui::PaintContext& context) {
if (!screen_capture_layer_)
return;
const gfx::Rect& screen_bounds(screen_capture_layer_->bounds());
ui::PaintRecorder recorder(context, screen_bounds.size());
gfx::Canvas* canvas = recorder.canvas();
auto selection_rect = gfx::BoundingRect(drag_start_, drag_end_);
// Draw border exclusively outside selected region.
selection_rect.Inset(gfx::Insets::TLBR(-1, -1, 0, 0));
PaintSelectionLayer(canvas, selection_rect, gfx::Rect());
paint_invalidation_ = gfx::Rect();
}
void ScreenshotFlow::RequestRepaint(gfx::Rect region) {
if (!screen_capture_layer_)
return;
if (region.IsEmpty()) {
const gfx::Size& layer_size = screen_capture_layer_->size();
region = gfx::Rect(0, 0, layer_size.width(), layer_size.height());
}
paint_invalidation_.Union(region);
screen_capture_layer_->SchedulePaint(region);
}
void ScreenshotFlow::PaintSelectionLayer(gfx::Canvas* canvas,
const gfx::Rect& selection,
const gfx::Rect& invalidation_region) {
// Adjust for hidpi and lodpi support.
canvas->UndoDeviceScaleFactor();
// Clear the canvas with our mask color.
canvas->DrawColor(kColorSemitransparentOverlayMask);
// Allow the user's selection to show through, and add a border around it.
if (!selection.IsEmpty()) {
float scale_factor = screen_capture_layer_->device_scale_factor();
gfx::Rect selection_scaled =
gfx::ScaleToEnclosingRect(selection, scale_factor);
canvas->FillRect(selection_scaled, kColorSemitransparentOverlayVisible,
SkBlendMode::kClear);
canvas->DrawRect(gfx::RectF(selection_scaled), kColorSelectionRect);
}
}
void ScreenshotFlow::SetCursor(ui::mojom::CursorType cursor_type) {
if (!web_contents_) {
return;
}
#if BUILDFLAG(IS_MAC)
if (cursor_type == ui::mojom::CursorType::kCross) {
EventCaptureMac::SetCrossCursor();
return;
}
#endif
content::RenderWidgetHost* host =
web_contents_->GetPrimaryMainFrame()->GetRenderWidgetHost();
if (host) {
ui::Cursor cursor(cursor_type);
host->SetCursor(cursor);
}
}
void ScreenshotFlow::SetIsDragging(bool value) {
is_dragging_ = value;
#if BUILDFLAG(IS_WIN)
const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
if (value) {
native_window->SetCapture();
} else {
native_window->ReleaseCapture();
}
#endif
}
bool ScreenshotFlow::IsCaptureModeActive() {
return capture_mode_ != CaptureMode::NOT_CAPTURING;
}
void ScreenshotFlow::WebContentsDestroyed() {
if (IsCaptureModeActive()) {
CancelCapture();
}
}
void ScreenshotFlow::OnVisibilityChanged(content::Visibility visibility) {
if (IsCaptureModeActive()) {
CancelCapture();
}
}
void ScreenshotFlow::AttemptRegionCapture(gfx::Rect view_bounds) {
// On Mac, it is possible for drag_end_ to end up outside of the
// Browser UI. This causes an error when completing the region
// capture. Update drag_end_ to be within the web content bounds.
drag_end_.SetToMin(gfx::Point(view_bounds.width(), view_bounds.height()));
drag_end_.SetToMax(gfx::Point(0, 0));
gfx::Rect selection = gfx::BoundingRect(drag_start_, drag_end_);
drag_start_.SetPoint(0, 0);
drag_end_.SetPoint(0, 0);
if (selection.width() >= kMinimumValidSelectionEdgePixels &&
selection.height() >= kMinimumValidSelectionEdgePixels) {
CompleteCapture(ScreenshotCaptureResultCode::SUCCESS, selection);
} else {
RequestRepaint(gfx::Rect());
}
}
// UnderlyingWebContentsObserver monitors the WebContents and exits screen
// capture mode if a navigation occurs.
class ScreenshotFlow::UnderlyingWebContentsObserver
: public content::WebContentsObserver {
public:
UnderlyingWebContentsObserver(content::WebContents* web_contents,
ScreenshotFlow* screenshot_flow)
: content::WebContentsObserver(web_contents),
screenshot_flow_(screenshot_flow) {}
~UnderlyingWebContentsObserver() override = default;
UnderlyingWebContentsObserver(const UnderlyingWebContentsObserver&) = delete;
UnderlyingWebContentsObserver& operator=(
const UnderlyingWebContentsObserver&) = delete;
// content::WebContentsObserver
void PrimaryPageChanged(content::Page& page) override {
// We only care to complete/cancel a capture if the capture mode is
// currently active.
if (screenshot_flow_->IsCaptureModeActive())
screenshot_flow_->CompleteCapture(
ScreenshotCaptureResultCode::USER_NAVIGATED_EXIT, gfx::Rect());
}
// content::WebContentsObserver
void FrameSizeChanged(content::RenderFrameHost* render_frame_host,
const gfx::Size& frame_size) override {
// We only care to resize the UI overlay when it's visible to the user.
if (screenshot_flow_->IsUIOverlayShown()) {
screenshot_flow_->ResetUIOverlayBounds();
}
}
private:
raw_ptr<ScreenshotFlow> screenshot_flow_;
};
} // namespace image_editor