| // Copyright 2014 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/utility/screenshot_controller.h" |
| |
| #include <cmath> |
| |
| #include "ash/display/mouse_cursor_event_filter.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/screenshot_delegate.h" |
| #include "ash/shell.h" |
| #include "ash/wm/window_util.h" |
| #include "base/memory/ptr_util.h" |
| #include "ui/aura/client/capture_client.h" |
| #include "ui/aura/client/screen_position_client.h" |
| #include "ui/aura/window_targeter.h" |
| #include "ui/compositor/paint_recorder.h" |
| #include "ui/display/screen.h" |
| #include "ui/events/event.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/event_handler.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/cursor_manager.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| const int kCursorSize = 12; |
| |
| // This will prevent the user from taking a screenshot across multiple |
| // monitors. it will stop the mouse at the any edge of the screen. must |
| // swtich back on when the screenshot is complete. |
| void EnableMouseWarp(bool enable) { |
| Shell::GetInstance()->mouse_cursor_filter()->set_mouse_warp_enabled(enable); |
| } |
| |
| class ScreenshotWindowTargeter : public aura::WindowTargeter { |
| public: |
| ScreenshotWindowTargeter() = default; |
| ~ScreenshotWindowTargeter() override = default; |
| |
| aura::Window* FindWindowForEvent(ui::LocatedEvent* event) { |
| aura::Window* target = static_cast<aura::Window*>(event->target()); |
| aura::Window* target_root = target->GetRootWindow(); |
| |
| aura::client::ScreenPositionClient* position_client = |
| aura::client::GetScreenPositionClient(target_root); |
| gfx::Point location = event->location(); |
| position_client->ConvertPointToScreen(target, &location); |
| |
| display::Display display = |
| display::Screen::GetScreen()->GetDisplayNearestPoint(location); |
| |
| aura::Window* root_window = Shell::GetInstance() |
| ->window_tree_host_manager() |
| ->GetRootWindowForDisplayId(display.id()); |
| |
| position_client->ConvertPointFromScreen(root_window, &location); |
| |
| gfx::Point target_location = event->location(); |
| event->set_location(location); |
| |
| // Ignore capture window when finding the target for located event. |
| aura::client::CaptureClient* original_capture_client = |
| aura::client::GetCaptureClient(root_window); |
| aura::client::SetCaptureClient(root_window, nullptr); |
| |
| aura::Window* selected = |
| static_cast<aura::Window*>(FindTargetForEvent(root_window, event)); |
| |
| // Restore State. |
| aura::client::SetCaptureClient(root_window, original_capture_client); |
| event->set_location(target_location); |
| return selected; |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ScreenshotWindowTargeter); |
| }; |
| |
| } // namespace |
| |
| class ScreenshotController::ScreenshotLayer : public ui::LayerOwner, |
| public ui::LayerDelegate { |
| public: |
| ScreenshotLayer(ui::Layer* parent, bool immediate_overlay) |
| : draw_inactive_overlay_(immediate_overlay) { |
| SetLayer(base::MakeUnique<ui::Layer>(ui::LAYER_TEXTURED)); |
| layer()->SetFillsBoundsOpaquely(false); |
| layer()->SetBounds(parent->bounds()); |
| parent->Add(layer()); |
| parent->StackAtTop(layer()); |
| layer()->SetVisible(true); |
| layer()->set_delegate(this); |
| } |
| ~ScreenshotLayer() override {} |
| |
| const gfx::Rect& region() const { return region_; } |
| |
| void SetRegion(const gfx::Rect& region) { |
| // Invalidates the region which covers the current and new region. |
| gfx::Rect union_rect(region_); |
| union_rect.Union(region); |
| union_rect.Intersects(layer()->bounds()); |
| union_rect.Inset(-kCursorSize, -kCursorSize, -kCursorSize, -kCursorSize); |
| region_ = region; |
| layer()->SchedulePaint(union_rect); |
| |
| // If we are going to start drawing the inactive overlay, we need to |
| // invalidate the entire layer. |
| bool is_drawing_inactive_overlay = draw_inactive_overlay_; |
| draw_inactive_overlay_ = draw_inactive_overlay_ || !region.IsEmpty(); |
| if (draw_inactive_overlay_ && !is_drawing_inactive_overlay) |
| layer()->SchedulePaint(layer()->parent()->bounds()); |
| } |
| |
| void set_cursor_location_in_root(const gfx::Point& point) { |
| cursor_location_in_root_ = point; |
| } |
| |
| private: |
| // ui::LayerDelegate: |
| void OnPaintLayer(const ui::PaintContext& context) override { |
| const SkColor kSelectedAreaOverlayColor = 0x60000000; |
| // Screenshot area representation: transparent hole with half opaque gray |
| // overlay. |
| ui::PaintRecorder recorder(context, layer()->size()); |
| |
| if (draw_inactive_overlay_) { |
| recorder.canvas()->FillRect(gfx::Rect(layer()->size()), |
| kSelectedAreaOverlayColor); |
| } |
| |
| DrawPseudoCursor(recorder.canvas()); |
| |
| if (!region_.IsEmpty()) |
| recorder.canvas()->FillRect(region_, SK_ColorBLACK, |
| SkXfermode::kClear_Mode); |
| } |
| |
| void OnDelegatedFrameDamage(const gfx::Rect& damage_rect_in_dip) override {} |
| |
| void OnDeviceScaleFactorChanged(float device_scale_factor) override {} |
| |
| // Mouse cursor may move sub DIP, so paint pseudo cursor instead of |
| // using platform cursor so that it's aliend with the region. |
| void DrawPseudoCursor(gfx::Canvas* canvas) { |
| // Don't draw if window selection mode. |
| if (cursor_location_in_root_.IsOrigin()) |
| return; |
| |
| gfx::Point pseudo_cursor_point = cursor_location_in_root_; |
| |
| // The cursor is above/before region. |
| if (pseudo_cursor_point.x() == region_.x()) |
| pseudo_cursor_point.Offset(-1, 0); |
| |
| if (pseudo_cursor_point.y() == region_.y()) |
| pseudo_cursor_point.Offset(0, -1); |
| |
| SkPaint paint; |
| paint.setAntiAlias(false); |
| paint.setStrokeWidth(1); |
| paint.setColor(SK_ColorWHITE); |
| paint.setBlendMode(SkBlendMode::kSrc); |
| gfx::Vector2d width(kCursorSize / 2, 0); |
| gfx::Vector2d height(0, kCursorSize / 2); |
| gfx::Vector2d white_x_offset(1, -1); |
| gfx::Vector2d white_y_offset(1, -1); |
| // Horizontal |
| canvas->DrawLine(pseudo_cursor_point - width + white_x_offset, |
| pseudo_cursor_point + width + white_x_offset, paint); |
| paint.setStrokeWidth(1); |
| // Vertical |
| canvas->DrawLine(pseudo_cursor_point - height + white_y_offset, |
| pseudo_cursor_point + height + white_y_offset, paint); |
| |
| paint.setColor(SK_ColorBLACK); |
| // Horizontal |
| canvas->DrawLine(pseudo_cursor_point - width, pseudo_cursor_point + width, |
| paint); |
| // Vertical |
| canvas->DrawLine(pseudo_cursor_point - height, pseudo_cursor_point + height, |
| paint); |
| } |
| |
| bool draw_inactive_overlay_; |
| |
| gfx::Rect region_; |
| |
| gfx::Point cursor_location_in_root_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScreenshotLayer); |
| }; |
| |
| class ScreenshotController::ScopedCursorSetter { |
| public: |
| ScopedCursorSetter(::wm::CursorManager* cursor_manager, |
| gfx::NativeCursor cursor) |
| : cursor_manager_(nullptr) { |
| if (cursor_manager->IsCursorLocked()) |
| return; |
| gfx::NativeCursor original_cursor = cursor_manager->GetCursor(); |
| cursor_manager_ = cursor_manager; |
| if (cursor == ui::kCursorNone) { |
| cursor_manager_->HideCursor(); |
| } else { |
| cursor_manager_->SetCursor(cursor); |
| cursor_manager_->ShowCursor(); |
| } |
| cursor_manager_->LockCursor(); |
| // Set/ShowCursor does not make any effects at this point but it sets |
| // back to the original cursor when unlocked. |
| cursor_manager_->SetCursor(original_cursor); |
| cursor_manager_->ShowCursor(); |
| } |
| |
| ~ScopedCursorSetter() { |
| if (cursor_manager_) |
| cursor_manager_->UnlockCursor(); |
| } |
| |
| private: |
| ::wm::CursorManager* cursor_manager_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScopedCursorSetter); |
| }; |
| |
| ScreenshotController::ScreenshotController() |
| : mode_(NONE), |
| root_window_(nullptr), |
| selected_(nullptr), |
| screenshot_delegate_(nullptr) { |
| // Keep this here and don't move it to StartPartialScreenshotSession(), as it |
| // needs to be pre-pended by MouseCursorEventFilter in Shell::Init(). |
| Shell::GetInstance()->PrependPreTargetHandler(this); |
| } |
| |
| ScreenshotController::~ScreenshotController() { |
| if (screenshot_delegate_) |
| CancelScreenshotSession(); |
| Shell::GetInstance()->RemovePreTargetHandler(this); |
| } |
| |
| void ScreenshotController::StartWindowScreenshotSession( |
| ScreenshotDelegate* screenshot_delegate) { |
| if (screenshot_delegate_) { |
| DCHECK_EQ(screenshot_delegate_, screenshot_delegate); |
| return; |
| } |
| screenshot_delegate_ = screenshot_delegate; |
| mode_ = WINDOW; |
| |
| display::Screen::GetScreen()->AddObserver(this); |
| for (aura::Window* root : Shell::GetAllRootWindows()) { |
| layers_[root] = base::MakeUnique<ScreenshotLayer>( |
| Shell::GetContainer(root, kShellWindowId_OverlayContainer)->layer(), |
| true); |
| } |
| SetSelectedWindow(wm::GetActiveWindow()); |
| |
| cursor_setter_.reset(new ScopedCursorSetter( |
| Shell::GetInstance()->cursor_manager(), ui::kCursorCross)); |
| |
| EnableMouseWarp(true); |
| } |
| |
| void ScreenshotController::StartPartialScreenshotSession( |
| ScreenshotDelegate* screenshot_delegate, |
| bool draw_overlay_immediately) { |
| // Already in a screenshot session. |
| if (screenshot_delegate_) { |
| DCHECK_EQ(screenshot_delegate_, screenshot_delegate); |
| return; |
| } |
| |
| screenshot_delegate_ = screenshot_delegate; |
| mode_ = PARTIAL; |
| display::Screen::GetScreen()->AddObserver(this); |
| for (aura::Window* root : Shell::GetAllRootWindows()) { |
| layers_[root] = base::MakeUnique<ScreenshotLayer>( |
| Shell::GetContainer(root, kShellWindowId_OverlayContainer)->layer(), |
| draw_overlay_immediately); |
| } |
| |
| if (!pen_events_only_) { |
| cursor_setter_.reset(new ScopedCursorSetter( |
| Shell::GetInstance()->cursor_manager(), ui::kCursorCross)); |
| } |
| |
| EnableMouseWarp(false); |
| } |
| |
| void ScreenshotController::CancelScreenshotSession() { |
| for (aura::Window* root : Shell::GetAllRootWindows()) { |
| // Having pre-handled all mouse events, widgets that had mouse capture may |
| // now misbehave, so break any existing captures. Do this after the |
| // screenshot session is over so that it's still possible to screenshot |
| // things like menus. |
| aura::client::GetCaptureClient(root)->SetCapture(nullptr); |
| } |
| |
| mode_ = NONE; |
| pen_events_only_ = false; |
| root_window_ = nullptr; |
| SetSelectedWindow(nullptr); |
| screenshot_delegate_ = nullptr; |
| display::Screen::GetScreen()->RemoveObserver(this); |
| layers_.clear(); |
| cursor_setter_.reset(); |
| EnableMouseWarp(true); |
| |
| if (on_screenshot_session_done_) { |
| // Copy the closure to a temporary value so that if it calls |
| // CancelScreenshotSession we do not loop forever. |
| base::Closure on_done = on_screenshot_session_done_; |
| on_screenshot_session_done_.Reset(); |
| on_done.Run(); |
| } |
| } |
| |
| void ScreenshotController::MaybeStart(const ui::LocatedEvent& event) { |
| aura::Window* current_root = |
| static_cast<aura::Window*>(event.target())->GetRootWindow(); |
| if (root_window_) { |
| // It's already started. This can happen when the second finger touches |
| // the screen, or combination of the touch and mouse. We should grab the |
| // partial screenshot instead of restarting. |
| if (current_root == root_window_) { |
| Update(event); |
| CompletePartialScreenshot(); |
| } |
| } else { |
| root_window_ = current_root; |
| start_position_ = event.root_location(); |
| if (!pen_events_only_) { |
| // ScopedCursorSetter must be reset first to make sure that its dtor is |
| // called before ctor is called. |
| cursor_setter_.reset(); |
| cursor_setter_.reset(new ScopedCursorSetter( |
| Shell::GetInstance()->cursor_manager(), ui::kCursorNone)); |
| } |
| Update(event); |
| } |
| } |
| |
| void ScreenshotController::CompleteWindowScreenshot() { |
| if (selected_) |
| screenshot_delegate_->HandleTakeWindowScreenshot(selected_); |
| CancelScreenshotSession(); |
| } |
| |
| void ScreenshotController::CompletePartialScreenshot() { |
| if (!root_window_) { |
| // If we received a released event before we ever got a pressed event |
| // (resulting in setting |root_window_|), we just return without canceling |
| // to keep the screenshot session active waiting for the next press. |
| // |
| // This is to avoid a crash that used to happen when we start the screenshot |
| // session while the mouse is pressed and then release without moving the |
| // mouse. crbug.com/581432. |
| return; |
| } |
| |
| DCHECK(layers_.count(root_window_)); |
| const gfx::Rect& region = layers_.at(root_window_)->region(); |
| if (!region.IsEmpty()) { |
| screenshot_delegate_->HandleTakePartialScreenshot( |
| root_window_, gfx::IntersectRects(root_window_->bounds(), region)); |
| } |
| CancelScreenshotSession(); |
| } |
| |
| void ScreenshotController::Update(const ui::LocatedEvent& event) { |
| // Update may happen without MaybeStart() if the partial screenshot session |
| // starts when dragging. |
| if (!root_window_) |
| MaybeStart(event); |
| DCHECK(layers_.find(root_window_) != layers_.end()); |
| |
| ScreenshotLayer* layer = layers_.at(root_window_).get(); |
| layer->set_cursor_location_in_root(event.root_location()); |
| layer->SetRegion( |
| gfx::Rect(std::min(start_position_.x(), event.root_location().x()), |
| std::min(start_position_.y(), event.root_location().y()), |
| ::abs(start_position_.x() - event.root_location().x()), |
| ::abs(start_position_.y() - event.root_location().y()))); |
| } |
| |
| void ScreenshotController::UpdateSelectedWindow(ui::LocatedEvent* event) { |
| aura::Window* selected = ScreenshotWindowTargeter().FindWindowForEvent(event); |
| |
| // Find a window that is backed with a widget. |
| while (selected && (selected->type() == ui::wm::WINDOW_TYPE_CONTROL || |
| !selected->delegate())) { |
| selected = selected->parent(); |
| } |
| |
| if (selected->parent()->id() == kShellWindowId_WallpaperContainer || |
| selected->parent()->id() == kShellWindowId_LockScreenWallpaperContainer) |
| selected = nullptr; |
| |
| SetSelectedWindow(selected); |
| } |
| |
| void ScreenshotController::SetSelectedWindow(aura::Window* selected) { |
| if (selected_ == selected) |
| return; |
| |
| if (selected_) { |
| selected_->RemoveObserver(this); |
| layers_.at(selected_->GetRootWindow())->SetRegion(gfx::Rect()); |
| } |
| |
| selected_ = selected; |
| |
| if (selected_) { |
| selected_->AddObserver(this); |
| layers_.at(selected_->GetRootWindow())->SetRegion(selected_->bounds()); |
| } |
| } |
| |
| bool ScreenshotController::ShouldProcessEvent( |
| const ui::PointerDetails& pointer_details) const { |
| return !pen_events_only_ || |
| pointer_details.pointer_type == ui::EventPointerType::POINTER_TYPE_PEN; |
| } |
| |
| void ScreenshotController::OnKeyEvent(ui::KeyEvent* event) { |
| if (!screenshot_delegate_) |
| return; |
| |
| if (event->type() == ui::ET_KEY_RELEASED) { |
| if (event->key_code() == ui::VKEY_ESCAPE) { |
| CancelScreenshotSession(); |
| event->StopPropagation(); |
| } else if (event->key_code() == ui::VKEY_RETURN && mode_ == WINDOW) { |
| CompleteWindowScreenshot(); |
| event->StopPropagation(); |
| } |
| } |
| |
| // Stop all key events except if the user is using a pointer, in which case |
| // they should be able to continue manipulating the screen. |
| if (!pen_events_only_) |
| event->StopPropagation(); |
| } |
| |
| void ScreenshotController::OnMouseEvent(ui::MouseEvent* event) { |
| if (!screenshot_delegate_ || !ShouldProcessEvent(event->pointer_details())) |
| return; |
| switch (mode_) { |
| case NONE: |
| NOTREACHED(); |
| break; |
| case WINDOW: |
| switch (event->type()) { |
| case ui::ET_MOUSE_MOVED: |
| case ui::ET_MOUSE_DRAGGED: |
| UpdateSelectedWindow(event); |
| break; |
| case ui::ET_MOUSE_RELEASED: |
| CompleteWindowScreenshot(); |
| break; |
| default: |
| // Do nothing. |
| break; |
| } |
| break; |
| case PARTIAL: |
| switch (event->type()) { |
| case ui::ET_MOUSE_PRESSED: |
| MaybeStart(*event); |
| break; |
| case ui::ET_MOUSE_DRAGGED: |
| Update(*event); |
| break; |
| case ui::ET_MOUSE_RELEASED: |
| CompletePartialScreenshot(); |
| break; |
| default: |
| // Do nothing. |
| break; |
| } |
| break; |
| } |
| event->StopPropagation(); |
| } |
| |
| void ScreenshotController::OnTouchEvent(ui::TouchEvent* event) { |
| if (!screenshot_delegate_ || !ShouldProcessEvent(event->pointer_details())) |
| return; |
| switch (mode_) { |
| case NONE: |
| NOTREACHED(); |
| break; |
| case WINDOW: |
| switch (event->type()) { |
| case ui::ET_TOUCH_PRESSED: |
| case ui::ET_TOUCH_MOVED: |
| UpdateSelectedWindow(event); |
| break; |
| case ui::ET_TOUCH_RELEASED: |
| CompleteWindowScreenshot(); |
| break; |
| default: |
| // Do nothing. |
| break; |
| } |
| break; |
| case PARTIAL: |
| switch (event->type()) { |
| case ui::ET_TOUCH_PRESSED: |
| MaybeStart(*event); |
| break; |
| case ui::ET_TOUCH_MOVED: |
| Update(*event); |
| break; |
| case ui::ET_TOUCH_RELEASED: |
| CompletePartialScreenshot(); |
| break; |
| default: |
| // Do nothing. |
| break; |
| } |
| break; |
| } |
| event->StopPropagation(); |
| } |
| |
| void ScreenshotController::OnDisplayAdded(const display::Display& new_display) { |
| if (!screenshot_delegate_) |
| return; |
| CancelScreenshotSession(); |
| } |
| |
| void ScreenshotController::OnDisplayRemoved( |
| const display::Display& old_display) { |
| if (!screenshot_delegate_) |
| return; |
| CancelScreenshotSession(); |
| } |
| |
| void ScreenshotController::OnDisplayMetricsChanged( |
| const display::Display& display, |
| uint32_t changed_metrics) {} |
| |
| void ScreenshotController::OnWindowDestroying(aura::Window* window) { |
| SetSelectedWindow(nullptr); |
| } |
| |
| } // namespace ash |