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