| // 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 <memory> |
| |
| #include "ash/accessibility/magnifier/magnifier_glass.h" |
| #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/metrics/histogram_functions.h" |
| #include "base/optional.h" |
| #include "ui/aura/client/capture_client.h" |
| #include "ui/aura/client/screen_position_client.h" |
| #include "ui/aura/window_targeter.h" |
| #include "ui/base/cursor/mojom/cursor_type.mojom-shared.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/events/pointer_details.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/cursor_manager.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| constexpr 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 |
| // switch back on when the screenshot is complete. |
| void EnableMouseWarp(bool enable) { |
| Shell::Get()->mouse_cursor_filter()->set_mouse_warp_enabled(enable); |
| } |
| |
| // Returns the target for the specified event ignoring any capture windows. |
| aura::Window* FindWindowForEvent(const ui::LocatedEvent& event) { |
| gfx::Point location = event.target()->GetScreenLocation(event); |
| display::Display display = |
| display::Screen::GetScreen()->GetDisplayNearestPoint(location); |
| |
| aura::Window* root = Shell::GetRootWindowForDisplayId(display.id()); |
| auto* screen_position_client = aura::client::GetScreenPositionClient(root); |
| screen_position_client->ConvertPointFromScreen(root, &location); |
| |
| std::unique_ptr<ui::Event> cloned_event = ui::Event::Clone(event); |
| ui::LocatedEvent* cloned_located_event = cloned_event->AsLocatedEvent(); |
| cloned_located_event->set_location(location); |
| |
| // Ignore capture window when finding the target for located event. |
| aura::client::CaptureClient* original_capture_client = |
| aura::client::GetCaptureClient(root); |
| aura::client::SetCaptureClient(root, nullptr); |
| |
| aura::Window* selected = static_cast<aura::Window*>( |
| aura::WindowTargeter().FindTargetForEvent(root, cloned_located_event)); |
| |
| // Restore State. |
| aura::client::SetCaptureClient(root, original_capture_client); |
| return selected; |
| } |
| |
| // Returns true if the |window| is top-level. |
| bool IsTopLevelWindow(aura::Window* window) { |
| if (!window) |
| return false; |
| if (window->type() == aura::client::WINDOW_TYPE_CONTROL || |
| !window->delegate()) { |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| class ScreenshotController::ScreenshotLayer : public ui::LayerOwner, |
| public ui::LayerDelegate { |
| public: |
| ScreenshotLayer(ScreenshotController* controller, |
| ui::Layer* parent, |
| bool immediate_overlay) |
| : controller_(controller), draw_inactive_overlay_(immediate_overlay) { |
| SetLayer(std::make_unique<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 = default; |
| |
| ScreenshotController* controller() { return controller_; } |
| |
| 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); |
| 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()); |
| } |
| |
| const gfx::Point& cursor_location_in_root() const { |
| return cursor_location_in_root_; |
| } |
| void set_cursor_location_in_root(const gfx::Point& point) { |
| cursor_location_in_root_ = point; |
| } |
| |
| bool draw_inactive_overlay() const { return draw_inactive_overlay_; } |
| |
| const base::Optional<gfx::Point>& start_position() const { |
| return start_position_; |
| } |
| |
| void OnLocatedEvent(const ui::LocatedEvent& event) { |
| DCHECK_EQ(ScreenshotController::PARTIAL, controller()->mode_); |
| |
| switch (event.type()) { |
| case ui::ET_MOUSE_PRESSED: |
| case ui::ET_TOUCH_PRESSED: |
| OnPointerPressed(event); |
| break; |
| case ui::ET_MOUSE_MOVED: |
| OnPointerMoved(event); |
| break; |
| case ui::ET_MOUSE_DRAGGED: |
| case ui::ET_TOUCH_MOVED: |
| OnPointerDragged(event); |
| break; |
| case ui::ET_MOUSE_RELEASED: |
| case ui::ET_TOUCH_RELEASED: |
| OnPointerReleased(event); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| virtual void OnPointerPressed(const ui::LocatedEvent& event) { |
| if (start_position_.has_value()) { |
| // 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. |
| OnPointerDragged(event); |
| controller()->CompletePartialScreenshot(); |
| return; |
| } |
| |
| MaybeChangeCursor(ui::mojom::CursorType::kNone); |
| Update(event.root_location()); |
| } |
| |
| virtual void OnPointerMoved(const ui::LocatedEvent& event) {} |
| |
| virtual void OnPointerDragged(const ui::LocatedEvent& event) { |
| Update(event.root_location()); |
| } |
| |
| virtual void OnPointerReleased(const ui::LocatedEvent& event) { |
| controller()->CompletePartialScreenshot(); |
| } |
| |
| protected: |
| void MaybeChangeCursor(ui::mojom::CursorType cursor) { |
| if (controller()->pen_events_only_) |
| return; |
| |
| // ScopedCursorSetter must be reset first to make sure that its dtor is |
| // called before ctor is called. |
| controller()->cursor_setter_.reset(); |
| controller()->cursor_setter_ = std::make_unique<ScopedCursorSetter>(cursor); |
| } |
| |
| private: |
| void Update(const gfx::Point& cursor_root_location) { |
| if (!start_position_.has_value()) |
| start_position_ = cursor_root_location; |
| |
| set_cursor_location_in_root(cursor_root_location); |
| SetRegion( |
| gfx::Rect(std::min(start_position_->x(), cursor_root_location.x()), |
| std::min(start_position_->y(), cursor_root_location.y()), |
| ::abs(start_position_->x() - cursor_root_location.x()), |
| ::abs(start_position_->y() - cursor_root_location.y()))); |
| } |
| |
| // 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(), context.device_scale_factor()); |
| |
| if (!region_.IsEmpty()) |
| recorder.canvas()->FillRect(region_, SK_ColorBLACK, SkBlendMode::kClear); |
| } |
| |
| void OnDeviceScaleFactorChanged(float old_device_scale_factor, |
| float new_device_scale_factor) override {} |
| |
| // Mouse cursor may move sub DIP, so paint pseudo cursor instead of |
| // using platform cursor so that it's aligned with the region. |
| void DrawPseudoCursor(gfx::Canvas* canvas, float device_scale_factor) { |
| // 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); |
| |
| cc::PaintFlags flags; |
| flags.setBlendMode(SkBlendMode::kSrc); |
| |
| // Circle fill. |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| flags.setColor(SK_ColorGRAY); |
| flags.setAntiAlias(true); |
| const int stroke_width = 1; |
| flags.setStrokeWidth(stroke_width); |
| gfx::PointF circle_center(pseudo_cursor_point); |
| // For the circle to be exactly centered in the middle of the crosshairs, we |
| // need to take into account the stroke width of the crosshair as well as |
| // the device scale factor. |
| const float center_offset = |
| stroke_width / (2.0f * device_scale_factor * device_scale_factor); |
| circle_center.Offset(center_offset, center_offset); |
| const float circle_radius = (kCursorSize / 2.0f) - 2.5f; |
| canvas->DrawCircle(circle_center, circle_radius, flags); |
| |
| flags.setAntiAlias(false); |
| flags.setColor(SK_ColorWHITE); |
| 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, flags); |
| // Vertical |
| canvas->DrawLine(pseudo_cursor_point - height + white_y_offset, |
| pseudo_cursor_point + height + white_y_offset, flags); |
| |
| flags.setColor(SK_ColorBLACK); |
| // Horizontal |
| canvas->DrawLine(pseudo_cursor_point - width, pseudo_cursor_point + width, |
| flags); |
| // Vertical |
| canvas->DrawLine(pseudo_cursor_point - height, pseudo_cursor_point + height, |
| flags); |
| |
| // Circle stroke. |
| flags.setColor(SK_ColorDKGRAY); |
| flags.setStyle(cc::PaintFlags::kStroke_Style); |
| flags.setAntiAlias(true); |
| canvas->DrawCircle(circle_center, circle_radius, flags); |
| } |
| |
| ScreenshotController* const controller_; |
| |
| bool draw_inactive_overlay_; |
| |
| gfx::Rect region_; |
| |
| gfx::Point cursor_location_in_root_; |
| |
| base::Optional<gfx::Point> start_position_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScreenshotLayer); |
| }; |
| |
| class ScreenshotController::ScopedCursorSetter { |
| public: |
| explicit ScopedCursorSetter(ui::mojom::CursorType cursor) { |
| ::wm::CursorManager* cursor_manager = Shell::Get()->cursor_manager(); |
| if (cursor_manager->IsCursorLocked()) { |
| already_locked_ = true; |
| return; |
| } |
| gfx::NativeCursor original_cursor = cursor_manager->GetCursor(); |
| if (cursor == ui::mojom::CursorType::kNone) { |
| 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() { |
| // Only unlock the cursor if it wasn't locked before. |
| if (!already_locked_) |
| Shell::Get()->cursor_manager()->UnlockCursor(); |
| } |
| |
| private: |
| // If the cursor is already locked, don't try to lock it again. |
| bool already_locked_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScopedCursorSetter); |
| }; |
| |
| ScreenshotController::ScreenshotController( |
| std::unique_ptr<ScreenshotDelegate> delegate) |
| : mode_(NONE), |
| root_window_(nullptr), |
| selected_(nullptr), |
| screenshot_delegate_(std::move(delegate)) { |
| // Keep this here and don't move it to StartPartialScreenshotSession(), as it |
| // needs to be pre-pended by MouseCursorEventFilter in Shell::Init(). |
| Shell::Get()->AddPreTargetHandler(this, ui::EventTarget::Priority::kSystem); |
| |
| // Schedule recording of the number of screenshots taken per day. |
| num_screenshots_taken_in_last_day_scheduler_.Start( |
| FROM_HERE, base::TimeDelta::FromDays(1), |
| base::BindRepeating( |
| &ScreenshotController::RecordNumberOfScreenshotsTakenInLastDay, |
| weak_factory_.GetWeakPtr())); |
| |
| // Schedule recording of the number of screenshots taken per week. |
| num_screenshots_taken_in_last_week_scheduler_.Start( |
| FROM_HERE, base::TimeDelta::FromDays(7), |
| base::BindRepeating( |
| &ScreenshotController::RecordNumberOfScreenshotsTakenInLastWeek, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| ScreenshotController::~ScreenshotController() { |
| if (in_screenshot_session_) |
| CancelScreenshotSession(); |
| Shell::Get()->RemovePreTargetHandler(this); |
| } |
| |
| void ScreenshotController::TakeScreenshotForAllRootWindows() { |
| DCHECK(screenshot_delegate_); |
| if (screenshot_delegate_->CanTakeScreenshot()) { |
| screenshot_delegate_->HandleTakeScreenshotForAllRootWindows(); |
| ++num_screenshots_taken_in_last_day_; |
| ++num_screenshots_taken_in_last_week_; |
| } |
| } |
| |
| void ScreenshotController::StartWindowScreenshotSession() { |
| DCHECK(screenshot_delegate_); |
| // Already in a screenshot session. |
| if (in_screenshot_session_) |
| return; |
| in_screenshot_session_ = true; |
| mode_ = WINDOW; |
| |
| display::Screen::GetScreen()->AddObserver(this); |
| for (aura::Window* root : Shell::GetAllRootWindows()) { |
| layers_[root] = std::make_unique<ScreenshotLayer>( |
| this, |
| Shell::GetContainer(root, kShellWindowId_OverlayContainer)->layer(), |
| true); |
| } |
| SetSelectedWindow(window_util::GetActiveWindow()); |
| |
| cursor_setter_ = |
| std::make_unique<ScopedCursorSetter>(ui::mojom::CursorType::kCross); |
| |
| EnableMouseWarp(true); |
| } |
| |
| void ScreenshotController::StartPartialScreenshotSession( |
| bool draw_overlay_immediately) { |
| DCHECK(screenshot_delegate_); |
| // Already in a screenshot session. |
| if (in_screenshot_session_) |
| return; |
| in_screenshot_session_ = true; |
| mode_ = PARTIAL; |
| display::Screen::GetScreen()->AddObserver(this); |
| for (aura::Window* root : Shell::GetAllRootWindows()) { |
| layers_[root] = std::make_unique<ScreenshotLayer>( |
| this, |
| Shell::GetContainer(root, kShellWindowId_OverlayContainer)->layer(), |
| draw_overlay_immediately); |
| } |
| |
| if (!pen_events_only_) { |
| cursor_setter_ = |
| std::make_unique<ScopedCursorSetter>(ui::mojom::CursorType::kCross); |
| } |
| |
| 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); |
| in_screenshot_session_ = false; |
| display::Screen::GetScreen()->RemoveObserver(this); |
| layers_.clear(); |
| cursor_setter_.reset(); |
| EnableMouseWarp(true); |
| |
| if (on_screenshot_session_done_) |
| std::move(on_screenshot_session_done_).Run(); |
| } |
| |
| void ScreenshotController::CompleteWindowScreenshot() { |
| if (selected_) { |
| screenshot_delegate_->HandleTakeWindowScreenshot(selected_); |
| ++num_screenshots_taken_in_last_day_; |
| ++num_screenshots_taken_in_last_week_; |
| } |
| 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)); |
| ++num_screenshots_taken_in_last_day_; |
| ++num_screenshots_taken_in_last_week_; |
| } |
| CancelScreenshotSession(); |
| } |
| |
| void ScreenshotController::Update(const ui::LocatedEvent& event) { |
| aura::Window* current_root = |
| static_cast<aura::Window*>(event.target())->GetRootWindow(); |
| |
| // Settle a root window if |event| is not a pointer release event. That is, |
| // pointer release events received before pointer press and drag events are |
| // ignored. |
| if (!root_window_ && event.type() != ui::ET_MOUSE_RELEASED && |
| event.type() != ui::ET_TOUCH_RELEASED) { |
| root_window_ = current_root; |
| } |
| |
| if (current_root != root_window_) |
| return; |
| |
| DCHECK(layers_.find(root_window_) != layers_.end()); |
| layers_.at(root_window_)->OnLocatedEvent(event); |
| } |
| |
| void ScreenshotController::UpdateSelectedWindow(const ui::LocatedEvent& event) { |
| aura::Window* selected = FindWindowForEvent(event); |
| |
| // Find a window that is backed with a widget. |
| while (selected && !IsTopLevelWindow(selected)) |
| 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::kPen; |
| } |
| |
| void ScreenshotController::OnKeyEvent(ui::KeyEvent* event) { |
| if (!in_screenshot_session_) |
| 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 (!in_screenshot_session_ || !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: |
| case ui::ET_MOUSE_MOVED: |
| case ui::ET_MOUSE_DRAGGED: |
| case ui::ET_MOUSE_RELEASED: |
| Update(*event); |
| break; |
| default: |
| // Do nothing. |
| break; |
| } |
| break; |
| } |
| event->StopPropagation(); |
| } |
| |
| void ScreenshotController::OnTouchEvent(ui::TouchEvent* event) { |
| if (!in_screenshot_session_ || !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: |
| case ui::ET_TOUCH_MOVED: |
| case ui::ET_TOUCH_RELEASED: |
| Update(*event); |
| break; |
| default: |
| // Do nothing. |
| break; |
| } |
| break; |
| } |
| event->StopPropagation(); |
| } |
| |
| void ScreenshotController::OnDisplayAdded(const display::Display& new_display) { |
| if (!in_screenshot_session_) |
| return; |
| CancelScreenshotSession(); |
| } |
| |
| void ScreenshotController::OnDisplayRemoved( |
| const display::Display& old_display) { |
| if (!in_screenshot_session_) |
| return; |
| CancelScreenshotSession(); |
| } |
| |
| void ScreenshotController::OnDisplayMetricsChanged( |
| const display::Display& display, |
| uint32_t changed_metrics) {} |
| |
| void ScreenshotController::OnWindowDestroying(aura::Window* window) { |
| SetSelectedWindow(nullptr); |
| } |
| |
| void ScreenshotController::RecordNumberOfScreenshotsTakenInLastDay() { |
| base::UmaHistogramCounts100("Ash.ScreenshotController.ScreenshotsPerDay", |
| num_screenshots_taken_in_last_day_); |
| num_screenshots_taken_in_last_day_ = 0; |
| } |
| |
| void ScreenshotController::RecordNumberOfScreenshotsTakenInLastWeek() { |
| base::UmaHistogramCounts1000("Ash.ScreenshotController.ScreenshotsPerWeek", |
| num_screenshots_taken_in_last_week_); |
| num_screenshots_taken_in_last_week_ = 0; |
| } |
| |
| gfx::Point ScreenshotController::GetStartPositionForTest() const { |
| for (const auto& pair : layers_) { |
| const auto& start_position = pair.second->start_position(); |
| if (start_position.has_value()) |
| return start_position.value(); |
| } |
| |
| return gfx::Point(); |
| } |
| |
| } // namespace ash |