blob: 3cb9413ae7b0cf1b6bd2280552ff81bca75a561b [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 "chrome/browser/image_editor/screenshot_flow.h"
#include <memory>
#include "base/logging.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/compositor/paint_recorder.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 defined(OS_MAC)
#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(0x30, 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;
ScreenshotFlow::ScreenshotFlow(content::WebContents* 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 defined(OS_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());
views::Widget* top_widget =
views::Widget::GetTopLevelWidgetForNativeView(web_contents_view);
views::View* root_view = top_widget->GetRootView();
root_view->AddPreTargetHandler(this);
#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.
// TODO(skare): We should exit from this mode when moving between tabs,
// clicking on browser chrome, etc.
native_window->AddPreTargetHandler(this);
#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);
}
void ScreenshotFlow::RemoveUIOverlay() {
if (!web_contents_ || !screen_capture_layer_)
return;
#if defined(OS_MAC)
views::Widget* widget = views::Widget::GetWidgetForNativeView(
web_contents_->GetContentNativeView());
ui::Layer* content_layer = widget->GetLayer();
views::View* root_view = widget->GetRootView();
root_view->RemovePreTargetHandler(this);
#else
const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
native_window->RemovePreTargetHandler(this);
ui::Layer* content_layer = native_window->layer();
#endif
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(gfx::Rect(web_contents_->GetSize()));
}
void ScreenshotFlow::CaptureAndRunScreenshotCompleteCallback(gfx::Rect region) {
if (region.IsEmpty()) {
RunScreenshotCompleteCallback(gfx::Rect(), gfx::Image());
return;
}
gfx::Rect bounds = web_contents_->GetViewBounds();
#if defined(OS_MAC)
const gfx::NativeView& native_view = web_contents_->GetContentNativeView();
gfx::Image img;
bool rval = ui::GrabViewSnapshot(native_view, region, &img);
// If |img| is empty, clients should treat it as a canceled action, but
// we have a DCHECK for development as we expected this call to succeed.
DCHECK(rval);
RunScreenshotCompleteCallback(bounds, img);
#else
ui::GrabWindowSnapshotAsyncCallback screenshot_callback = base::BindOnce(
&ScreenshotFlow::RunScreenshotCompleteCallback, weak_this_, bounds);
const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
ui::GrabWindowSnapshotAsync(native_window, region,
std::move(screenshot_callback));
#endif
}
void ScreenshotFlow::CancelCapture() {
RemoveUIOverlay();
}
void ScreenshotFlow::OnKeyEvent(ui::KeyEvent* event) {
if (event->type() == ui::ET_KEY_PRESSED &&
event->key_code() == ui::VKEY_ESCAPE) {
CompleteCapture(gfx::Rect());
event->StopPropagation();
}
}
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();
switch (event->type()) {
case ui::ET_MOUSE_MOVED:
SetCursor(ui::mojom::CursorType::kCross);
break;
case ui::ET_MOUSE_PRESSED:
if (event->IsLeftMouseButton()) {
capture_mode_ = CaptureMode::SELECTION_RECTANGLE;
drag_start_ = location;
drag_end_ = location;
event->SetHandled();
}
break;
case ui::ET_MOUSE_DRAGGED:
if (event->IsLeftMouseButton()) {
drag_end_ = location;
RequestRepaint(gfx::Rect());
event->SetHandled();
}
break;
case ui::ET_MOUSE_RELEASED:
if (capture_mode_ == CaptureMode::SELECTION_RECTANGLE ||
capture_mode_ == CaptureMode::SELECTION_ELEMENT) {
capture_mode_ = CaptureMode::NOT_CAPTURING;
event->SetHandled();
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(selection);
} else {
RequestRepaint(gfx::Rect());
}
}
break;
default:
break;
}
}
void ScreenshotFlow::CompleteCapture(const gfx::Rect& region) {
RemoveUIOverlay();
CaptureAndRunScreenshotCompleteCallback(region);
}
void ScreenshotFlow::RunScreenshotCompleteCallback(gfx::Rect bounds,
gfx::Image image) {
ScreenshotCaptureResult result;
result.image = image;
result.screen_bounds = bounds;
std::move(flow_callback_).Run(result);
}
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_);
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;
}
content::RenderWidgetHost* host =
web_contents_->GetMainFrame()->GetRenderWidgetHost();
if (host) {
ui::Cursor cursor(cursor_type);
host->SetCursor(cursor);
}
}
// 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 {
screenshot_flow_->CancelCapture();
}
private:
ScreenshotFlow* screenshot_flow_;
};
} // namespace image_editor