blob: 78c1da3c957e0ca29b41318fa3a19937dc8e4a26 [file] [log] [blame]
// Copyright 2020 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/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_label_view.h"
#include "ash/capture_mode/capture_mode_bar_view.h"
#include "ash/capture_mode/capture_mode_constants.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_settings_view.h"
#include "ash/capture_mode/capture_mode_toggle_button.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/capture_window_observer.h"
#include "ash/display/mouse_cursor_event_filter.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/magnifier/magnifier_glass.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_dimmer.h"
#include "base/memory/ptr_util.h"
#include "base/stl_util.h"
#include "cc/paint/paint_flags.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/window.h"
#include "ui/base/cursor/cursor_factory.h"
#include "ui/base/cursor/cursor_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_type.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/gfx/shadow_value.h"
#include "ui/gfx/skia_paint_util.h"
#include "ui/gfx/transform_util.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
constexpr int kCaptureRegionBorderStrokePx = 1;
// The visual radius of the drag affordance circles which are shown while
// resizing a drag region.
constexpr int kAffordanceCircleRadiusDp = 4;
// The hit radius of the drag affordance circles touch events.
constexpr int kAffordanceCircleTouchHitRadiusDp = 16;
// Capture region magnifier parameters.
constexpr MagnifierGlass::Params kMagnifierParams{
/*scale=*/2.f,
/*radius=*/60,
/*border_size=*/2,
/*border_outline_thickness=*/0,
/*border_color=*/SK_ColorWHITE,
/*border_outline_color=*/SK_ColorTRANSPARENT,
/*bottom_shadow=*/
gfx::ShadowValue(gfx::Vector2d(0, 1),
2,
SkColorSetARGB(0x4C, 0x00, 0x00, 0x00)),
/*top_shadow=*/
gfx::ShadowValue(gfx::Vector2d(0, 1),
3,
SkColorSetARGB(0x26, 0x00, 0x00, 0x00))};
constexpr int kSizeLabelBorderRadius = 4;
constexpr int kSizeLabelHorizontalPadding = 8;
constexpr SkColor kRegionBorderColor = SK_ColorWHITE;
// Blue300 at 30%.
constexpr SkColor kCaptureRegionColor = SkColorSetA(gfx::kGoogleBlue300, 77);
// Values for the shadows of the capture region components.
constexpr int kRegionAffordanceCircleShadow2Blur = 6;
constexpr gfx::ShadowValue kRegionOutlineShadow(gfx::Vector2d(0, 0),
2,
SkColorSetARGB(41, 0, 0, 0));
constexpr gfx::ShadowValue kRegionAffordanceCircleShadow1(
gfx::Vector2d(0, 1),
2,
SkColorSetARGB(76, 0, 0, 0));
constexpr gfx::ShadowValue kRegionAffordanceCircleShadow2(
gfx::Vector2d(0, 2),
kRegionAffordanceCircleShadow2Blur,
SkColorSetARGB(38, 0, 0, 0));
// Values of the focus ring draw around the region or affordance circles.
constexpr int kFocusRingStrokeWidthDp = 2;
constexpr int kFocusRingSpacingDp = 2;
// When updating the capture region, request a repaint on the region and inset
// such that the border, affordance circles and affordance circle shadows are
// all repainted as well.
constexpr int kDamageInsetDp = kCaptureRegionBorderStrokePx +
kAffordanceCircleRadiusDp +
kRegionAffordanceCircleShadow2Blur;
// The minimum padding on each side of the capture region. If the capture button
// cannot be placed in the center of the capture region and maintain this
// padding, it will be placed below or above the capture region.
constexpr int kCaptureRegionMinimumPaddingDp = 16;
// Animation parameters needed when countdown starts.
// The animation duration that the label fades out and scales down before count
// down starts.
constexpr base::TimeDelta kCaptureLabelAnimationDuration =
base::TimeDelta::FromMilliseconds(267);
// The animation duration that the capture bar fades out before count down
// starts.
constexpr base::TimeDelta kCaptureBarFadeOutDuration =
base::TimeDelta::FromMilliseconds(167);
// The animation duration that the fullscreen shield fades out before count down
// starts.
constexpr base::TimeDelta kCaptureShieldFadeOutDuration =
base::TimeDelta::FromMilliseconds(333);
// If there is no text message was showing when count down starts, the label
// widget will shrink down from 120% -> 100% and fade in.
constexpr float kLabelScaleUpOnCountdown = 1.2;
// Animation parameters for capture bar overlapping the user capture region.
// The default animation duration for opacity changes to the capture bar.
constexpr base::TimeDelta kCaptureBarOpacityChangeDuration =
base::TimeDelta::FromMilliseconds(100);
// The animation duration for showing the capture bar on mouse/touch release.
constexpr base::TimeDelta kCaptureBarOnReleaseOpacityChangeDuration =
base::TimeDelta::FromMilliseconds(167);
// When the capture bar and user capture region overlap and the mouse is not
// hovering over the capture bar, drop the opacity to this value to make the
// region easier to see.
constexpr float kCaptureBarOverlapOpacity = 0.1;
// If the user is using keyboard only and they are on the selecting region
// phase, they can create default region which is centered and sized to this
// value times the root window's width and height.
constexpr float kRegionDefaultRatio = 0.12f;
// Mouse cursor warping is disabled when the capture source is a custom region.
// Sets the mouse warp status to |enable| and return the original value.
bool SetMouseWarpEnabled(bool enable) {
auto* mouse_cursor_filter = Shell::Get()->mouse_cursor_filter();
const bool old_value = mouse_cursor_filter->mouse_warp_enabled();
mouse_cursor_filter->set_mouse_warp_enabled(enable);
return old_value;
}
// Gets the overlay container inside |root|.
aura::Window* GetParentContainer(aura::Window* root) {
DCHECK(root);
DCHECK(root->IsRootWindow());
return root->GetChildById(kShellWindowId_MenuContainer);
}
// Returns the smallest rect that contains all of |points|.
gfx::Rect GetRectEnclosingPoints(const std::vector<gfx::Point>& points) {
DCHECK_GE(points.size(), 2u);
int x = INT_MAX;
int y = INT_MAX;
int right = INT_MIN;
int bottom = INT_MIN;
for (const gfx::Point& point : points) {
x = std::min(point.x(), x);
y = std::min(point.y(), y);
right = std::max(point.x(), right);
bottom = std::max(point.y(), bottom);
}
return gfx::Rect(x, y, right - x, bottom - y);
}
// Returns the widget init params needed to create a widget associated with a
// capture session.
views::Widget::InitParams CreateWidgetParams(aura::Window* parent,
const gfx::Rect& bounds,
const std::string& name) {
// Use a popup widget to get transient properties, such as not needing to
// click on the widget first to get capture before receiving events.
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.parent = parent;
params.bounds = bounds;
params.name = name;
return params;
}
// Gets the root window associated |location_in_screen| if given, otherwise gets
// the root window associated with the CursorManager.
aura::Window* GetPreferredRootWindow(
base::Optional<gfx::Point> location_in_screen = base::nullopt) {
int64_t display_id =
(location_in_screen
? display::Screen::GetScreen()->GetDisplayNearestPoint(
*location_in_screen)
: Shell::Get()->cursor_manager()->GetDisplay())
.id();
// The Display object returned by CursorManager::GetDisplay may be stale, but
// will have the correct id.
DCHECK_NE(display::kInvalidDisplayId, display_id);
return Shell::GetRootWindowForDisplayId(display_id);
}
// In fullscreen or window capture mode, the mouse will change to a camera
// image icon if we're capturing image, or a video record image icon if we're
// capturing video.
ui::Cursor GetCursorForFullscreenOrWindowCapture(bool capture_image) {
ui::Cursor cursor(ui::mojom::CursorType::kCustom);
const display::Display display =
display::Screen::GetScreen()->GetDisplayNearestWindow(
GetPreferredRootWindow());
const float device_scale_factor = display.device_scale_factor();
// TODO: Adjust the icon color after spec is updated.
const gfx::ImageSkia icon = gfx::CreateVectorIcon(
capture_image ? kCaptureModeImageIcon : kCaptureModeVideoIcon,
SK_ColorBLACK);
SkBitmap bitmap = *icon.bitmap();
gfx::Point hotspot(bitmap.width() / 2, bitmap.height() / 2);
ui::ScaleAndRotateCursorBitmapAndHotpoint(
device_scale_factor, display.panel_rotation(), &bitmap, &hotspot);
auto* cursor_factory = ui::CursorFactory::GetInstance();
ui::PlatformCursor platform_cursor =
cursor_factory->CreateImageCursor(cursor.type(), bitmap, hotspot);
cursor.SetPlatformCursor(platform_cursor);
cursor.set_custom_bitmap(bitmap);
cursor.set_custom_hotspot(hotspot);
cursor_factory->UnrefImageCursor(platform_cursor);
return cursor;
}
// Returns the expected cursor type for |position| in region capture.
ui::mojom::CursorType GetCursorTypeForFineTunePosition(
FineTunePosition position) {
switch (position) {
case FineTunePosition::kTopLeft:
return ui::mojom::CursorType::kNorthWestResize;
case FineTunePosition::kBottomRight:
return ui::mojom::CursorType::kSouthEastResize;
case FineTunePosition::kTopCenter:
case FineTunePosition::kBottomCenter:
return ui::mojom::CursorType::kNorthSouthResize;
case FineTunePosition::kTopRight:
return ui::mojom::CursorType::kNorthEastResize;
case FineTunePosition::kBottomLeft:
return ui::mojom::CursorType::kSouthWestResize;
case FineTunePosition::kLeftCenter:
case FineTunePosition::kRightCenter:
return ui::mojom::CursorType::kEastWestResize;
case FineTunePosition::kCenter:
return ui::mojom::CursorType::kMove;
default:
return ui::mojom::CursorType::kCell;
}
}
int GetArrowKeyPressChange(bool is_shift_down) {
return is_shift_down ? capture_mode::kShiftArrowKeyboardRegionChangeDp
: capture_mode::kArrowKeyboardRegionChangeDp;
}
// Clips |out_bounds| to fit |rect|. Similar to
// gfx::Rect::AdjustToFit() but does not shift the output rect to maintain the
// rect size.
void ClipRectToFit(gfx::Rect* out_bounds, const gfx::Rect& rect) {
out_bounds->SetByBounds(std::max(rect.x(), out_bounds->x()),
std::max(rect.y(), out_bounds->y()),
std::min(rect.right(), out_bounds->right()),
std::min(rect.bottom(), out_bounds->bottom()));
}
// Returns the appropriate |message_id| for a chromevox alert.
// |for_toggle_alert| helps differentiate between the session start and when a
// user toggles the source.
int GetMessageIdForCaptureSource(CaptureModeSource source,
bool for_toggle_alert) {
switch (source) {
case CaptureModeSource::kFullscreen:
return for_toggle_alert
? IDS_ASH_SCREEN_CAPTURE_ALERT_SELECT_SOURCE_FULLSCREEN
: IDS_ASH_SCREEN_CAPTURE_SOURCE_FULLSCREEN;
case CaptureModeSource::kRegion:
return for_toggle_alert
? IDS_ASH_SCREEN_CAPTURE_ALERT_SELECT_SOURCE_REGION
: IDS_ASH_SCREEN_CAPTURE_SOURCE_PARTIAL;
default:
return for_toggle_alert
? IDS_ASH_SCREEN_CAPTURE_ALERT_SELECT_SOURCE_WINDOW
: IDS_ASH_SCREEN_CAPTURE_SOURCE_WINDOW;
}
}
} // namespace
class CaptureModeSession::CursorSetter {
public:
CursorSetter()
: cursor_manager_(Shell::Get()->cursor_manager()),
original_cursor_(cursor_manager_->GetCursor()),
original_cursor_visible_(cursor_manager_->IsCursorVisible()),
original_cursor_locked_(cursor_manager_->IsCursorLocked()),
current_orientation_(GetCurrentScreenOrientation()) {}
CursorSetter(const CursorSetter&) = delete;
CursorSetter& operator=(const CursorSetter&) = delete;
~CursorSetter() { ResetCursor(); }
// Note that this will always make the cursor visible if it is not |kNone|.
void UpdateCursor(const ui::Cursor& cursor) {
if (original_cursor_locked_)
return;
if (in_cursor_update_)
return;
base::AutoReset<bool> auto_reset_in_cursor_update(&in_cursor_update_, true);
const ui::mojom::CursorType current_cursor_type =
cursor_manager_->GetCursor().type();
const ui::mojom::CursorType new_cursor_type = cursor.type();
const CaptureModeType capture_type = CaptureModeController::Get()->type();
// For custom cursor, update the cursor if we need to change between image
// capture and video capture or the screen orientation changes.
const OrientationLockType orientation = GetCurrentScreenOrientation();
const bool is_cursor_changed =
current_cursor_type != new_cursor_type ||
(current_cursor_type == ui::mojom::CursorType::kCustom &&
(custom_cursor_capture_type_ != capture_type ||
current_orientation_ != orientation));
const bool is_cursor_visibility_changed =
cursor_manager_->IsCursorVisible() !=
(new_cursor_type != ui::mojom::CursorType::kNone);
if (new_cursor_type == ui::mojom::CursorType::kCustom)
custom_cursor_capture_type_ = capture_type;
current_orientation_ = orientation;
if (!is_cursor_changed && !is_cursor_visibility_changed)
return;
if (cursor_manager_->IsCursorLocked())
cursor_manager_->UnlockCursor();
if (new_cursor_type == ui::mojom::CursorType::kNone) {
cursor_manager_->HideCursor();
} else {
cursor_manager_->SetCursor(cursor);
cursor_manager_->ShowCursor();
}
cursor_manager_->LockCursor();
was_cursor_reset_to_original_ = false;
}
// Resets to its original cursor.
void ResetCursor() {
// Only unlock the cursor if it wasn't locked before.
if (original_cursor_locked_)
return;
// Only reset cursor if it hasn't been reset before.
if (was_cursor_reset_to_original_)
return;
if (cursor_manager_->IsCursorLocked())
cursor_manager_->UnlockCursor();
cursor_manager_->SetCursor(original_cursor_);
if (original_cursor_visible_)
cursor_manager_->ShowCursor();
else
cursor_manager_->HideCursor();
was_cursor_reset_to_original_ = true;
}
bool IsCursorVisible() const { return cursor_manager_->IsCursorVisible(); }
void HideCursor() {
if (original_cursor_locked_ || !IsCursorVisible())
return;
if (cursor_manager_->IsCursorLocked())
cursor_manager_->UnlockCursor();
cursor_manager_->HideCursor();
cursor_manager_->LockCursor();
was_cursor_reset_to_original_ = false;
}
bool IsUsingCustomCursor(CaptureModeType type) const {
return cursor_manager_->GetCursor().type() ==
ui::mojom::CursorType::kCustom &&
custom_cursor_capture_type_ == type;
}
private:
wm::CursorManager* const cursor_manager_;
const gfx::NativeCursor original_cursor_;
const bool original_cursor_visible_;
// If the original cursor is already locked, don't make any changes to it.
const bool original_cursor_locked_;
// The current custom cursor type. kImage if we're using image capture icon as
// the mouse cursor, and kVideo if we're using video record icon as the mouse
// cursor.
CaptureModeType custom_cursor_capture_type_ = CaptureModeType::kImage;
// Records the current screen orientation. If screen orientation changes, we
// will need to update the cursor if we're using custom cursor.
OrientationLockType current_orientation_;
// True if the cursor has reset back to its original cursor. It's to prevent
// Reset() from setting the cursor to |original_cursor_| more than once.
bool was_cursor_reset_to_original_ = true;
// True if the cursor is currently being updated. This is to prevent
// UpdateCursor() is called nestly more than once and the mouse is locked
// multiple times.
bool in_cursor_update_ = false;
};
CaptureModeSession::CaptureModeSession(CaptureModeController* controller)
: controller_(controller),
current_root_(GetPreferredRootWindow()),
magnifier_glass_(kMagnifierParams),
cursor_setter_(std::make_unique<CursorSetter>()) {
Shell::Get()->AddPreTargetHandler(this);
SetLayer(std::make_unique<ui::Layer>(ui::LAYER_TEXTURED));
layer()->SetFillsBoundsOpaquely(false);
layer()->set_delegate(this);
auto* parent = GetParentContainer(current_root_);
parent->layer()->Add(layer());
layer()->SetBounds(parent->bounds());
// The last region selected could have been on a larger display. Ensure that
// the region is not larger than the current display.
ClampCaptureRegionToRootWindowSize();
capture_mode_bar_widget_->Init(
CreateWidgetParams(parent, CaptureModeBarView::GetBounds(current_root_),
"CaptureModeBarWidget"));
capture_mode_bar_view_ = capture_mode_bar_widget_->SetContentsView(
std::make_unique<CaptureModeBarView>());
capture_mode_bar_widget_->Show();
UpdateCaptureLabelWidget();
RefreshStackingOrder(parent);
UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
/*is_touch=*/false);
if (controller_->source() == CaptureModeSource::kWindow)
capture_window_observer_ = std::make_unique<CaptureWindowObserver>(this);
UpdateRootWindowDimmers();
// A context menu may have input capture when entering a session. Remove
// capture from it, otherwise subsequent mouse events will cause it to close,
// and then we won't be able to take a screenshot of the menu. Store it so we
// can return capture to it when exiting the session.
auto* capture_client = aura::client::GetCaptureClient(current_root_);
input_capture_window_ = capture_client->GetCaptureWindow();
if (input_capture_window_) {
capture_client->ReleaseCapture(input_capture_window_);
input_capture_window_->AddObserver(this);
}
TabletModeController::Get()->AddObserver(this);
current_root_->AddObserver(this);
display::Screen::GetScreen()->AddObserver(this);
capture_mode_util::TriggerAccessibilityAlert(l10n_util::GetStringFUTF8(
IDS_ASH_SCREEN_CAPTURE_ALERT_OPEN,
l10n_util::GetStringUTF16(GetMessageIdForCaptureSource(
controller_->source(), /*for_toggle_alert=*/false)),
l10n_util::GetStringUTF16(
controller_->type() == CaptureModeType::kImage
? IDS_ASH_SCREEN_CAPTURE_TYPE_SCREENSHOT
: IDS_ASH_SCREEN_CAPTURE_TYPE_SCREEN_RECORDING)));
}
CaptureModeSession::~CaptureModeSession() {
if (input_capture_window_) {
input_capture_window_->RemoveObserver(this);
aura::client::GetCaptureClient(current_root_)
->SetCapture(input_capture_window_);
}
display::Screen::GetScreen()->RemoveObserver(this);
current_root_->RemoveObserver(this);
TabletModeController::Get()->RemoveObserver(this);
Shell::Get()->RemovePreTargetHandler(this);
// This may happen if we hit esc while dragging.
if (old_mouse_warp_status_)
SetMouseWarpEnabled(*old_mouse_warp_status_);
// Close these widgets immediately to avoid having them show up in the
// captured screenshots or video.
if (capture_label_widget_)
capture_label_widget_->CloseNow();
if (dimensions_label_widget_)
dimensions_label_widget_->CloseNow();
if (capture_mode_settings_widget_)
capture_mode_settings_widget_->CloseNow();
DCHECK(capture_mode_bar_widget_);
capture_mode_bar_widget_->CloseNow();
if (a11y_alert_on_session_exit_) {
capture_mode_util::TriggerAccessibilityAlert(
IDS_ASH_SCREEN_CAPTURE_ALERT_CLOSE);
}
}
aura::Window* CaptureModeSession::GetSelectedWindow() const {
return capture_window_observer_ ? capture_window_observer_->window()
: nullptr;
}
void CaptureModeSession::OnCaptureSourceChanged(CaptureModeSource new_source) {
capture_source_changed_ = true;
if (new_source == CaptureModeSource::kWindow)
capture_window_observer_ = std::make_unique<CaptureWindowObserver>(this);
else
capture_window_observer_.reset();
if (new_source == CaptureModeSource::kRegion)
num_capture_region_adjusted_ = 0;
capture_mode_bar_view_->OnCaptureSourceChanged(new_source);
UpdateDimensionsLabelWidget(/*is_resizing=*/false);
layer()->SchedulePaint(layer()->bounds());
UpdateCaptureLabelWidget();
UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
/*is_touch=*/false);
capture_mode_util::TriggerAccessibilityAlert(
GetMessageIdForCaptureSource(new_source, /*for_toggle_alert=*/true));
}
void CaptureModeSession::OnCaptureTypeChanged(CaptureModeType new_type) {
capture_mode_bar_view_->OnCaptureTypeChanged(new_type);
UpdateCaptureLabelWidget();
UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
/*is_touch=*/false);
capture_mode_util::TriggerAccessibilityAlert(
new_type == CaptureModeType::kImage
? IDS_ASH_SCREEN_CAPTURE_ALERT_SELECT_TYPE_IMAGE
: IDS_ASH_SCREEN_CAPTURE_ALERT_SELECT_TYPE_VIDEO);
}
void CaptureModeSession::SetSettingsMenuShown(bool shown) {
capture_mode_bar_view_->SetSettingsMenuShown(shown);
if (!shown) {
capture_mode_settings_widget_.reset();
capture_mode_settings_view_ = nullptr;
return;
}
if (!capture_mode_settings_widget_) {
auto* parent = GetParentContainer(current_root_);
capture_mode_settings_widget_ = std::make_unique<views::Widget>();
capture_mode_settings_widget_->Init(CreateWidgetParams(
parent, CaptureModeSettingsView::GetBounds(capture_mode_bar_view_),
"CaptureModeSettingsWidget"));
capture_mode_settings_view_ =
capture_mode_settings_widget_->SetContentsView(
std::make_unique<CaptureModeSettingsView>());
parent->layer()->StackAtTop(capture_mode_settings_widget_->GetLayer());
capture_mode_settings_widget_->Show();
}
}
void CaptureModeSession::OnMicrophoneChanged(bool microphone_enabled) {
DCHECK(capture_mode_settings_view_);
capture_mode_settings_view_->OnMicrophoneChanged(microphone_enabled);
}
void CaptureModeSession::ReportSessionHistograms() {
if (controller_->source() == CaptureModeSource::kRegion)
RecordNumberOfCaptureRegionAdjustments(num_capture_region_adjusted_);
num_capture_region_adjusted_ = 0;
RecordCaptureModeSwitchesFromInitialMode(capture_source_changed_);
RecordCaptureModeConfiguration(controller_->type(), controller_->source());
}
void CaptureModeSession::StartCountDown(
base::OnceClosure countdown_finished_callback) {
DCHECK(capture_label_widget_);
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
label_view->StartCountDown(std::move(countdown_finished_callback));
UpdateCaptureLabelWidgetBounds(/*animate=*/true);
// Fade out the capture bar.
ui::Layer* capture_bar_layer = capture_mode_bar_widget_->GetLayer();
ui::ScopedLayerAnimationSettings capture_bar_settings(
capture_bar_layer->GetAnimator());
capture_bar_settings.SetTransitionDuration(kCaptureBarFadeOutDuration);
capture_bar_settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
capture_bar_settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
capture_bar_layer->SetOpacity(0.f);
// Fade out the shield if it's recording fullscreen.
if (controller_->source() == CaptureModeSource::kFullscreen) {
ui::ScopedLayerAnimationSettings shield_settings(layer()->GetAnimator());
shield_settings.SetTransitionDuration(kCaptureShieldFadeOutDuration);
shield_settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
shield_settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
layer()->SetOpacity(0.f);
}
}
void CaptureModeSession::OnPaintLayer(const ui::PaintContext& context) {
ui::PaintRecorder recorder(context, layer()->size());
auto* color_provider = AshColorProvider::Get();
const SkColor dimming_color = color_provider->GetShieldLayerColor(
AshColorProvider::ShieldLayerType::kShield40);
recorder.canvas()->DrawColor(dimming_color);
PaintCaptureRegion(recorder.canvas());
}
void CaptureModeSession::OnKeyEvent(ui::KeyEvent* event) {
if (event->type() != ui::ET_KEY_PRESSED)
return;
ui::KeyboardCode key_code = event->key_code();
switch (key_code) {
case ui::VKEY_ESCAPE:
event->StopPropagation();
controller_->Stop(); // |this| is destroyed here.
return;
case ui::VKEY_RETURN:
event->StopPropagation();
if (!IsInCountDownAnimation())
controller_->PerformCapture(); // |this| is destroyed here.
return;
case ui::VKEY_SPACE:
event->StopPropagation();
event->SetHandled();
if (controller_->source() == CaptureModeSource::kRegion)
SelectDefaultRegion();
return;
case ui::VKEY_TAB: {
// Eat tab events always to prevent application windows from getting them.
event->StopPropagation();
event->SetHandled();
if (!is_selecting_region_ &&
controller_->source() == CaptureModeSource::kRegion) {
// Update the position to the next one in |tabbing_order| and then
// schedule a paint to repaint the focus. |kNone| means nothing has
// keyboard focus, and arrow keys will have no effect.
// TODO(richui|sammiequon): Once the capture bar and button are
// focusable, create a class which handles tabbing for all of capture
// session.
static const std::vector<FineTunePosition> tabbing_order = {
FineTunePosition::kNone, FineTunePosition::kCenter,
FineTunePosition::kTopLeft, FineTunePosition::kTopCenter,
FineTunePosition::kTopRight, FineTunePosition::kRightCenter,
FineTunePosition::kBottomRight, FineTunePosition::kBottomCenter,
FineTunePosition::kBottomLeft, FineTunePosition::kLeftCenter};
auto it = std::find(tabbing_order.begin(), tabbing_order.end(),
focused_fine_tune_position_);
DCHECK(it != tabbing_order.end());
int index = it - tabbing_order.begin();
const int array_size = int{tabbing_order.size()};
// Adding |array_size| ensures if |index| starts at 0 and decrements we
// wrap around and get n-1 as expected. For example, -1 % 5 returns -1,
// but what we want is 4.
index = ((index + array_size + (event->IsShiftDown() ? -1 : 1)) %
array_size);
focused_fine_tune_position_ = tabbing_order[index];
RepaintRegion();
}
return;
}
case ui::VKEY_UP:
case ui::VKEY_DOWN: {
event->StopPropagation();
event->SetHandled();
UpdateRegionVertically(/*up=*/key_code == ui::VKEY_UP,
event->IsShiftDown());
return;
}
case ui::VKEY_LEFT:
case ui::VKEY_RIGHT: {
event->StopPropagation();
event->SetHandled();
UpdateRegionHorizontally(/*left=*/key_code == ui::VKEY_LEFT,
event->IsShiftDown());
return;
}
default:
return;
}
}
void CaptureModeSession::OnMouseEvent(ui::MouseEvent* event) {
OnLocatedEvent(event, /*is_touch=*/false);
}
void CaptureModeSession::OnTouchEvent(ui::TouchEvent* event) {
OnLocatedEvent(event, /*is_touch=*/true);
}
void CaptureModeSession::OnTabletModeStarted() {
UpdateCaptureLabelWidget();
UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
/*is_touch=*/false);
}
void CaptureModeSession::OnTabletModeEnded() {
UpdateCaptureLabelWidget();
UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
/*is_touch=*/false);
}
void CaptureModeSession::OnWindowDestroying(aura::Window* window) {
if (window == input_capture_window_) {
input_capture_window_->RemoveObserver(this);
input_capture_window_ = nullptr;
return;
}
DCHECK_EQ(current_root_, window);
MaybeChangeRoot(Shell::GetPrimaryRootWindow());
}
void CaptureModeSession::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t metrics) {
if (!(metrics & (DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_ROTATION |
DISPLAY_METRIC_DEVICE_SCALE_FACTOR))) {
return;
}
EndSelection(/*is_event_on_capture_bar_or_menu=*/false,
/*region_intersects_capture_bar=*/false);
UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
/*is_touch=*/false);
// Ensure the region still fits the root window after display changes.
ClampCaptureRegionToRootWindowSize();
// Update the bounds of all created widgets and repaint the entire layer.
auto* parent = GetParentContainer(current_root_);
DCHECK_EQ(parent->layer(), layer()->parent());
layer()->SetBounds(parent->bounds());
DCHECK(capture_mode_bar_widget_);
capture_mode_bar_widget_->SetBounds(
CaptureModeBarView::GetBounds(current_root_));
if (capture_label_widget_)
UpdateCaptureLabelWidget();
layer()->SchedulePaint(layer()->bounds());
}
gfx::Rect CaptureModeSession::GetSelectedWindowBounds() const {
auto* window = GetSelectedWindow();
return window ? window->bounds() : gfx::Rect();
}
void CaptureModeSession::RefreshStackingOrder(aura::Window* parent_container) {
DCHECK(parent_container);
auto* capture_mode_bar_layer = capture_mode_bar_widget_->GetLayer();
auto* overlay_layer = layer();
auto* parent_container_layer = parent_container->layer();
parent_container_layer->StackAtTop(overlay_layer);
parent_container_layer->StackAtTop(capture_label_widget_->GetLayer());
parent_container_layer->StackAtTop(capture_mode_bar_layer);
}
void CaptureModeSession::PaintCaptureRegion(gfx::Canvas* canvas) {
gfx::Rect region;
bool adjustable_region = false;
switch (controller_->source()) {
case CaptureModeSource::kFullscreen:
region = current_root_->bounds();
break;
case CaptureModeSource::kWindow:
region = GetSelectedWindowBounds();
break;
case CaptureModeSource::kRegion:
region = controller_->user_capture_region();
adjustable_region = true;
break;
}
if (region.IsEmpty())
return;
gfx::ScopedCanvas scoped_canvas(canvas);
const float dsf = canvas->UndoDeviceScaleFactor();
region = gfx::ScaleToEnclosingRect(region, dsf);
if (!adjustable_region) {
canvas->FillRect(region, SK_ColorTRANSPARENT, SkBlendMode::kClear);
canvas->FillRect(region, kCaptureRegionColor);
return;
}
region.Inset(-kCaptureRegionBorderStrokePx, -kCaptureRegionBorderStrokePx);
canvas->FillRect(region, SK_ColorTRANSPARENT, SkBlendMode::kClear);
// Draw the region border.
cc::PaintFlags border_flags;
border_flags.setColor(kRegionBorderColor);
border_flags.setStyle(cc::PaintFlags::kStroke_Style);
border_flags.setStrokeWidth(kCaptureRegionBorderStrokePx);
border_flags.setLooper(gfx::CreateShadowDrawLooper({kRegionOutlineShadow}));
canvas->DrawRect(gfx::RectF(region), border_flags);
// Draws the focus ring if the region or one of the affordance circles
// currently has focus.
auto maybe_draw_focus_ring = [&canvas, &region,
&dsf](FineTunePosition position) {
if (position == FineTunePosition::kNone)
return;
cc::PaintFlags focus_ring_flags;
focus_ring_flags.setColor(AshColorProvider::Get()->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kFocusRingColor));
focus_ring_flags.setStyle(cc::PaintFlags::kStroke_Style);
focus_ring_flags.setStrokeWidth(kFocusRingStrokeWidthDp);
if (position == FineTunePosition::kCenter) {
gfx::RectF focus_rect(region);
focus_rect.Inset(
gfx::InsetsF(-kFocusRingSpacingDp - kFocusRingStrokeWidthDp / 2));
canvas->DrawRect(focus_rect, focus_ring_flags);
return;
}
const float radius =
dsf * (kAffordanceCircleRadiusDp + kFocusRingSpacingDp +
kFocusRingStrokeWidthDp / 2);
canvas->DrawCircle(
capture_mode_util::GetLocationForFineTunePosition(region, position),
radius, focus_ring_flags);
};
if (is_selecting_region_ ||
capture_mode_util::ShouldHideDragAffordance(fine_tune_position_)) {
maybe_draw_focus_ring(focused_fine_tune_position_);
return;
}
// Draw the drag affordance circles.
cc::PaintFlags circle_flags;
circle_flags.setColor(kRegionBorderColor);
circle_flags.setStyle(cc::PaintFlags::kFill_Style);
circle_flags.setAntiAlias(true);
circle_flags.setLooper(gfx::CreateShadowDrawLooper(
{kRegionAffordanceCircleShadow1, kRegionAffordanceCircleShadow2}));
auto draw_circle = [&canvas, &circle_flags,
&dsf](const gfx::Point& location) {
canvas->DrawCircle(location, dsf * kAffordanceCircleRadiusDp, circle_flags);
};
draw_circle(region.origin());
draw_circle(region.top_center());
draw_circle(region.top_right());
draw_circle(region.right_center());
draw_circle(region.bottom_right());
draw_circle(region.bottom_center());
draw_circle(region.bottom_left());
draw_circle(region.left_center());
maybe_draw_focus_ring(focused_fine_tune_position_);
}
void CaptureModeSession::OnLocatedEvent(ui::LocatedEvent* event,
bool is_touch) {
// If we're currently in countdown animation, don't further handle any
// located events. However we should stop the event propagation here to
// prevent other event handlers from handling this event.
if (IsInCountDownAnimation()) {
event->StopPropagation();
return;
}
gfx::Point screen_location = event->location();
aura::Window* event_target = static_cast<aura::Window*>(event->target());
wm::ConvertPointToScreen(event_target, &screen_location);
// For fullscreen/window mode, change the root window as soon as we detect the
// cursor on a new display. For region mode, wait until the user taps down to
// try to select a new region on the new display.
const CaptureModeSource source = controller_->source();
const bool is_press_event = event->type() == ui::ET_MOUSE_PRESSED ||
event->type() == ui::ET_TOUCH_PRESSED;
const bool can_change_root =
source != CaptureModeSource::kRegion ||
(source == CaptureModeSource::kRegion && is_press_event);
if (can_change_root)
MaybeChangeRoot(GetPreferredRootWindow(screen_location));
// The root may have switched while pressing the mouse down. Move the capture
// bar to the current display if that is the case and make sure it is stacked
// at the top. The dimensions label and capture button have been moved and
// stacked on tap down so manually stack at top instead of calling
// RefreshStackingOrder.
const bool is_release_event = event->type() == ui::ET_MOUSE_RELEASED ||
event->type() == ui::ET_TOUCH_RELEASED;
if (is_release_event && source == CaptureModeSource::kRegion &&
current_root_ !=
capture_mode_bar_widget_->GetNativeWindow()->GetRootWindow()) {
capture_mode_bar_widget_->SetBounds(
CaptureModeBarView::GetBounds(current_root_));
auto* parent = GetParentContainer(current_root_);
parent->StackChildAtTop(capture_mode_bar_widget_->GetNativeWindow());
}
const bool is_event_on_settings_menu =
IsEventOnSettingsWidget(screen_location);
// Hide the settings menu if the user presses anywhere outside of the menu.
// Skip if the event is on the settings button, since the button will handle
// toggling the menu separately.
if (is_press_event && !is_event_on_settings_menu &&
!capture_mode_bar_view_->settings_button()->GetBoundsInScreen().Contains(
screen_location)) {
SetSettingsMenuShown(/*shown=*/false);
}
// Let the capture button handle any events it can handle first.
if (ShouldCaptureLabelHandleEvent(event_target)) {
UpdateCursor(screen_location, is_touch);
return;
}
const bool is_event_on_capture_bar_or_menu =
capture_mode_bar_widget_->GetWindowBoundsInScreen().Contains(
screen_location) ||
is_event_on_settings_menu;
const CaptureModeSource capture_source = controller_->source();
const bool is_capture_fullscreen =
capture_source == CaptureModeSource::kFullscreen;
const bool is_capture_window = capture_source == CaptureModeSource::kWindow;
if (is_capture_fullscreen || is_capture_window) {
// Do not handle any event located on the capture mode bar or settings menu.
if (is_event_on_capture_bar_or_menu) {
UpdateCursor(screen_location, is_touch);
return;
}
event->SetHandled();
event->StopPropagation();
switch (event->type()) {
case ui::ET_MOUSE_MOVED:
case ui::ET_TOUCH_PRESSED:
case ui::ET_TOUCH_MOVED: {
if (is_capture_window) {
// Make sure the capture label widget will not get picked up by the
// get topmost window algorithm otherwise a crash will happen since
// the snapshot code tries snap a deleted window.
std::set<aura::Window*> ignore_windows;
if (capture_label_widget_)
ignore_windows.insert(capture_label_widget_->GetNativeWindow());
capture_window_observer_->UpdateSelectedWindowAtPosition(
screen_location, ignore_windows);
}
UpdateCursor(screen_location, is_touch);
break;
}
case ui::ET_MOUSE_RELEASED:
case ui::ET_TOUCH_RELEASED:
if (is_capture_fullscreen || (is_capture_window && GetSelectedWindow()))
controller_->PerformCapture();
break;
default:
break;
}
return;
}
DCHECK_EQ(CaptureModeSource::kRegion, capture_source);
DCHECK(cursor_setter_);
// Allow events that are located on the capture mode bar or settings menu to
// pass through so we can click the buttons.
if (!is_event_on_capture_bar_or_menu) {
event->SetHandled();
event->StopPropagation();
}
// OnLocatedEventPressed() and OnLocatedEventDragged used root locations since
// CaptureModeController::user_capture_region() is stored in root
// coordinates..
// TODO(sammiequon): Update CaptureModeController::user_capture_region() to
// store screen coordinates.
gfx::Point location_in_root = event->location();
aura::Window::ConvertPointToTarget(event_target, current_root_,
&location_in_root);
const bool region_intersects_capture_bar =
capture_mode_bar_widget_->GetWindowBoundsInScreen().Intersects(
controller_->user_capture_region());
switch (event->type()) {
case ui::ET_MOUSE_PRESSED:
case ui::ET_TOUCH_PRESSED:
old_mouse_warp_status_ = SetMouseWarpEnabled(false);
OnLocatedEventPressed(location_in_root, is_touch,
is_event_on_capture_bar_or_menu);
break;
case ui::ET_MOUSE_DRAGGED:
case ui::ET_TOUCH_MOVED:
OnLocatedEventDragged(location_in_root);
break;
case ui::ET_MOUSE_RELEASED:
case ui::ET_TOUCH_RELEASED:
// Reenable mouse warping.
if (old_mouse_warp_status_)
SetMouseWarpEnabled(*old_mouse_warp_status_);
old_mouse_warp_status_.reset();
OnLocatedEventReleased(is_event_on_capture_bar_or_menu,
region_intersects_capture_bar);
break;
case ui::ET_MOUSE_MOVED:
if (!capture_mode_settings_widget_ && region_intersects_capture_bar) {
UpdateCaptureBarWidgetOpacity(
is_event_on_capture_bar_or_menu ? 1.f : kCaptureBarOverlapOpacity,
/*on_release=*/false);
}
break;
default:
break;
}
UpdateCursor(screen_location, is_touch);
}
FineTunePosition CaptureModeSession::GetFineTunePosition(
const gfx::Point& location_in_root,
bool is_touch) const {
// In the case of overlapping affordances, prioritize the bottomm right
// corner, then the rest of the corners, then the edges.
static const std::vector<FineTunePosition> drag_positions = {
FineTunePosition::kBottomRight, FineTunePosition::kBottomLeft,
FineTunePosition::kTopLeft, FineTunePosition::kTopRight,
FineTunePosition::kBottomCenter, FineTunePosition::kLeftCenter,
FineTunePosition::kTopCenter, FineTunePosition::kRightCenter};
const int hit_radius =
is_touch ? kAffordanceCircleTouchHitRadiusDp : kAffordanceCircleRadiusDp;
const int hit_radius_squared = hit_radius * hit_radius;
for (FineTunePosition position : drag_positions) {
const gfx::Point position_location =
capture_mode_util::GetLocationForFineTunePosition(
controller_->user_capture_region(), position);
// If |location_in_root| is within |hit_radius| of |position_location| for
// both x and y, then |position| is the current pressed down affordance.
if ((position_location - location_in_root).LengthSquared() <=
hit_radius_squared) {
return position;
}
}
if (controller_->user_capture_region().Contains(location_in_root))
return FineTunePosition::kCenter;
return FineTunePosition::kNone;
}
void CaptureModeSession::OnLocatedEventPressed(
const gfx::Point& location_in_root,
bool is_touch,
bool is_event_on_capture_bar_or_menu) {
initial_location_in_root_ = location_in_root;
previous_location_in_root_ = location_in_root;
// Use cursor compositing instead of the platform cursor when dragging to
// ensure the cursor is aligned with the region.
is_drag_in_progress_ = true;
Shell::Get()->UpdateCursorCompositingEnabled();
if (!is_event_on_capture_bar_or_menu)
UpdateCaptureBarWidgetOpacity(0.f, /*on_release=*/false);
if (is_selecting_region_)
return;
fine_tune_position_ = GetFineTunePosition(location_in_root, is_touch);
if (fine_tune_position_ == FineTunePosition::kNone &&
!is_event_on_capture_bar_or_menu) {
// If the point is outside the capture region and not on the capture bar or
// settings menu, restart to the select phase.
is_selecting_region_ = true;
UpdateCaptureRegion(gfx::Rect(), /*is_resizing=*/true, /*by_user=*/true);
num_capture_region_adjusted_ = 0;
return;
}
if (fine_tune_position_ != FineTunePosition::kNone)
++num_capture_region_adjusted_;
// In order to hide the drag affordance circles on click, we need to repaint
// the capture region.
if (capture_mode_util::ShouldHideDragAffordance(fine_tune_position_))
RepaintRegion();
if (fine_tune_position_ != FineTunePosition::kCenter &&
fine_tune_position_ != FineTunePosition::kNone) {
anchor_points_ = GetAnchorPointsForPosition(fine_tune_position_);
const gfx::Point position_location =
capture_mode_util::GetLocationForFineTunePosition(
controller_->user_capture_region(), fine_tune_position_);
MaybeShowMagnifierGlassAtPoint(position_location);
}
}
void CaptureModeSession::OnLocatedEventDragged(
const gfx::Point& location_in_root) {
const gfx::Point previous_location_in_root = previous_location_in_root_;
previous_location_in_root_ = location_in_root;
// For the select phase, the select region is the rectangle formed by the
// press location and the current location.
if (is_selecting_region_) {
UpdateCaptureRegion(
GetRectEnclosingPoints({initial_location_in_root_, location_in_root}),
/*is_resizing=*/true, /*by_user=*/true);
return;
}
if (fine_tune_position_ == FineTunePosition::kNone)
return;
// For a reposition, offset the old select region by the difference between
// the current location and the previous location, but do not let the select
// region go offscreen.
if (fine_tune_position_ == FineTunePosition::kCenter) {
gfx::Rect new_capture_region = controller_->user_capture_region();
new_capture_region.Offset(location_in_root - previous_location_in_root);
new_capture_region.AdjustToFit(current_root_->bounds());
UpdateCaptureRegion(new_capture_region, /*is_resizing=*/false,
/*by_user=*/true);
return;
}
// The new region is defined by the rectangle which encloses the anchor
// point(s) and |resizing_point|, which is based off of |location_in_root| but
// prevents edge drags from resizing the region in the non-desired direction.
std::vector<gfx::Point> points = anchor_points_;
DCHECK(!points.empty());
gfx::Point resizing_point = location_in_root;
// For edge dragging, there will be two anchor points with the same primary
// axis value. Setting |resizing_point|'s secondary axis value to match either
// one of the anchor points secondary axis value will ensure that for the
// duration of a drag, GetRectEnclosingPoints will return a rect whose
// secondary dimension does not change.
if (fine_tune_position_ == FineTunePosition::kLeftCenter ||
fine_tune_position_ == FineTunePosition::kRightCenter) {
resizing_point.set_y(points.front().y());
} else if (fine_tune_position_ == FineTunePosition::kTopCenter ||
fine_tune_position_ == FineTunePosition::kBottomCenter) {
resizing_point.set_x(points.front().x());
}
points.push_back(resizing_point);
UpdateCaptureRegion(GetRectEnclosingPoints(points), /*is_resizing=*/true,
/*by_user=*/true);
MaybeShowMagnifierGlassAtPoint(location_in_root);
}
void CaptureModeSession::OnLocatedEventReleased(
bool is_event_on_capture_bar_or_menu,
bool region_intersects_capture_bar) {
EndSelection(is_event_on_capture_bar_or_menu, region_intersects_capture_bar);
// Do a repaint to show the affordance circles.
RepaintRegion();
if (!is_selecting_region_)
return;
// After first release event, we advance to the next phase.
is_selecting_region_ = false;
UpdateCaptureLabelWidget();
}
void CaptureModeSession::UpdateCaptureRegion(
const gfx::Rect& new_capture_region,
bool is_resizing,
bool by_user) {
const gfx::Rect old_capture_region = controller_->user_capture_region();
if (old_capture_region == new_capture_region)
return;
// Calculate the region that has been damaged and repaint the layer. Add some
// extra padding to make sure the border and affordance circles are also
// repainted.
gfx::Rect damage_region = old_capture_region;
damage_region.Union(new_capture_region);
damage_region.Inset(gfx::Insets(-kDamageInsetDp));
layer()->SchedulePaint(damage_region);
controller_->SetUserCaptureRegion(new_capture_region, by_user);
UpdateDimensionsLabelWidget(is_resizing);
UpdateCaptureLabelWidget();
}
void CaptureModeSession::UpdateDimensionsLabelWidget(bool is_resizing) {
const bool should_not_show =
!is_resizing || controller_->source() != CaptureModeSource::kRegion ||
controller_->user_capture_region().IsEmpty();
if (should_not_show) {
dimensions_label_widget_.reset();
return;
}
if (!dimensions_label_widget_) {
auto* parent = GetParentContainer(current_root_);
dimensions_label_widget_ = std::make_unique<views::Widget>();
dimensions_label_widget_->Init(
CreateWidgetParams(parent, gfx::Rect(), "CaptureModeDimensionsLabel"));
auto size_label = std::make_unique<views::Label>();
auto* color_provider = AshColorProvider::Get();
size_label->SetEnabledColor(color_provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kTextColorPrimary));
size_label->SetBackground(views::CreateRoundedRectBackground(
color_provider->GetBaseLayerColor(
AshColorProvider::BaseLayerType::kTransparent80),
kSizeLabelBorderRadius));
size_label->SetAutoColorReadabilityEnabled(false);
dimensions_label_widget_->SetContentsView(std::move(size_label));
dimensions_label_widget_->Show();
// When moving to a new display, the dimensions label gets created/moved
// onto the new display on press, while the capture bar gets moved on
// release. In this case, we do not have to stack the dimensions label.
if (parent == capture_mode_bar_widget_->GetNativeWindow()->parent()) {
parent->StackChildBelow(dimensions_label_widget_->GetNativeWindow(),
capture_mode_bar_widget_->GetNativeWindow());
}
}
views::Label* size_label =
static_cast<views::Label*>(dimensions_label_widget_->GetContentsView());
const gfx::Rect capture_region = controller_->user_capture_region();
size_label->SetText(base::UTF8ToUTF16(base::StringPrintf(
"%d x %d", capture_region.width(), capture_region.height())));
UpdateDimensionsLabelBounds();
}
void CaptureModeSession::UpdateDimensionsLabelBounds() {
DCHECK(dimensions_label_widget_ &&
dimensions_label_widget_->GetContentsView());
gfx::Rect bounds(
dimensions_label_widget_->GetContentsView()->GetPreferredSize());
const gfx::Rect capture_region = controller_->user_capture_region();
gfx::Rect screen_region = current_root_->bounds();
bounds.set_width(bounds.width() + 2 * kSizeLabelHorizontalPadding);
bounds.set_x(capture_region.CenterPoint().x() - bounds.width() / 2);
bounds.set_y(capture_region.bottom() + kSizeLabelYDistanceFromRegionDp);
// The dimension label should always be within the screen and at the bottom of
// the capture region. If it does not fit below the bottom edge fo the region,
// move it above the bottom edge into the capture region.
screen_region.Inset(0, 0, 0, kSizeLabelYDistanceFromRegionDp);
bounds.AdjustToFit(screen_region);
wm::ConvertRectToScreen(current_root_, &bounds);
dimensions_label_widget_->SetBounds(bounds);
}
void CaptureModeSession::MaybeShowMagnifierGlassAtPoint(
const gfx::Point& location_in_root) {
if (!capture_mode_util::IsCornerFineTunePosition(fine_tune_position_))
return;
magnifier_glass_.ShowFor(current_root_, location_in_root);
}
void CaptureModeSession::CloseMagnifierGlass() {
magnifier_glass_.Close();
}
std::vector<gfx::Point> CaptureModeSession::GetAnchorPointsForPosition(
FineTunePosition position) {
std::vector<gfx::Point> anchor_points;
// For a vertex, the anchor point is the opposite vertex on the rectangle
// (ex. bottom left vertex -> top right vertex anchor point). For an edge, the
// anchor points are the two vertices of the opposite edge (ex. bottom edge ->
// top left and top right anchor points).
const gfx::Rect rect = controller_->user_capture_region();
switch (position) {
case FineTunePosition::kNone:
case FineTunePosition::kCenter:
break;
case FineTunePosition::kTopLeft:
anchor_points.push_back(rect.bottom_right());
break;
case FineTunePosition::kTopCenter:
anchor_points.push_back(rect.bottom_left());
anchor_points.push_back(rect.bottom_right());
break;
case FineTunePosition::kTopRight:
anchor_points.push_back(rect.bottom_left());
break;
case FineTunePosition::kLeftCenter:
anchor_points.push_back(rect.top_right());
anchor_points.push_back(rect.bottom_right());
break;
case FineTunePosition::kRightCenter:
anchor_points.push_back(rect.origin());
anchor_points.push_back(rect.bottom_left());
break;
case FineTunePosition::kBottomLeft:
anchor_points.push_back(rect.top_right());
break;
case FineTunePosition::kBottomCenter:
anchor_points.push_back(rect.origin());
anchor_points.push_back(rect.top_right());
break;
case FineTunePosition::kBottomRight:
anchor_points.push_back(rect.origin());
break;
}
DCHECK(!anchor_points.empty());
DCHECK_LE(anchor_points.size(), 2u);
return anchor_points;
}
void CaptureModeSession::UpdateCaptureLabelWidget() {
if (!capture_label_widget_) {
capture_label_widget_ = std::make_unique<views::Widget>();
auto* parent = GetParentContainer(current_root_);
capture_label_widget_->Init(
CreateWidgetParams(parent, gfx::Rect(), "CaptureLabel"));
capture_label_widget_->SetContentsView(
std::make_unique<CaptureLabelView>(this));
capture_label_widget_->Show();
}
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
label_view->UpdateIconAndText();
UpdateCaptureLabelWidgetBounds(/*animate=*/false);
}
void CaptureModeSession::UpdateCaptureLabelWidgetBounds(bool animate) {
DCHECK(capture_label_widget_);
const gfx::Rect bounds = CalculateCaptureLabelWidgetBounds();
const gfx::Rect old_bounds =
capture_label_widget_->GetNativeWindow()->GetBoundsInScreen();
if (old_bounds == bounds)
return;
if (!animate) {
capture_label_widget_->SetBounds(bounds);
return;
}
ui::Layer* layer = capture_label_widget_->GetLayer();
if (!old_bounds.IsEmpty()) {
// This happens if there is a label or a label button showing when count
// down starts. In this case we'll do a bounds change animation.
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetTweenType(gfx::Tween::LINEAR_OUT_SLOW_IN);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
settings.SetTransitionDuration(kCaptureLabelAnimationDuration);
capture_label_widget_->SetBounds(bounds);
} else {
// This happens when no text message was showing when count down starts, in
// this case we'll do a fade in + shrinking down animation.
capture_label_widget_->SetBounds(bounds);
const gfx::Point center_point = bounds.CenterPoint();
layer->SetTransform(
gfx::GetScaleTransform(gfx::Point(center_point.x() - bounds.x(),
center_point.y() - bounds.y()),
kLabelScaleUpOnCountdown));
layer->SetOpacity(0.f);
// Fade in.
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetTransitionDuration(kCaptureLabelAnimationDuration);
settings.SetTweenType(gfx::Tween::LINEAR);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
layer->SetOpacity(1.f);
// Scale down from 120% -> 100%.
settings.SetTweenType(gfx::Tween::LINEAR_OUT_SLOW_IN);
layer->SetTransform(gfx::Transform());
}
}
gfx::Rect CaptureModeSession::CalculateCaptureLabelWidgetBounds() {
DCHECK(capture_label_widget_);
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
// For fullscreen and window capture mode, the capture label is placed in the
// middle of the screen. For region capture mode, if it's in select phase, the
// capture label is also placed in the middle of the screen, and if it's in
// fine tune phase, the capture label is ideally placed in the middle of the
// capture region. If it cannot fit, then it will be placed slightly above or
// below the capture region.
gfx::Rect bounds(current_root_->bounds());
const gfx::Rect capture_region = controller_->user_capture_region();
const gfx::Size preferred_size = label_view->GetPreferredSize();
if (controller_->source() == CaptureModeSource::kRegion &&
!is_selecting_region_ && !capture_region.IsEmpty()) {
if (label_view->IsInCountDownAnimation()) {
// If countdown starts, calculate the bounds based on the old capture
// label's position, otherwise, since the countdown label bounds is
// smaller than the label bounds and may fit into the capture region even
// if the old capture label doesn't fit thus was place outside of the
// capture region, it's possible that we see the countdown label animates
// to inside of the capture region from outside of the capture region.
bounds = capture_label_widget_->GetNativeWindow()->bounds();
bounds.ClampToCenteredSize(preferred_size);
} else {
bounds = capture_region;
// The capture region must be at least the size of |preferred_size| plus
// some padding for the capture label to be centered inside it.
gfx::Size capture_region_min_size = preferred_size;
capture_region_min_size.Enlarge(kCaptureRegionMinimumPaddingDp,
kCaptureRegionMinimumPaddingDp);
if (bounds.width() > capture_region_min_size.width() &&
bounds.height() > capture_region_min_size.height()) {
bounds.ClampToCenteredSize(preferred_size);
} else {
// The capture region is too small for the capture label to be inside
// it. Align |bounds| so that its horizontal centerpoint aligns with the
// capture regions centerpoint.
bounds.set_size(preferred_size);
bounds.set_x(capture_region.CenterPoint().x() -
preferred_size.width() / 2);
// Try to put the capture label slightly below the capture region. If it
// does not fully fit in the root window bounds, place the capture label
// slightly above.
const int under_region_label_y =
capture_region.bottom() + kCaptureButtonDistanceFromRegionDp;
if (under_region_label_y + preferred_size.height() <
current_root_->bounds().bottom()) {
bounds.set_y(under_region_label_y);
} else {
bounds.set_y(capture_region.y() - kCaptureButtonDistanceFromRegionDp -
preferred_size.height());
}
}
}
} else {
bounds.ClampToCenteredSize(preferred_size);
}
// User capture region bounds are in root window coordinates so convert them
// here.
wm::ConvertRectToScreen(current_root_, &bounds);
return bounds;
}
bool CaptureModeSession::ShouldCaptureLabelHandleEvent(
aura::Window* event_target) {
if (!capture_label_widget_ ||
capture_label_widget_->GetNativeWindow() != event_target) {
return false;
}
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
return label_view->ShouldHandleEvent();
}
void CaptureModeSession::MaybeChangeRoot(aura::Window* new_root) {
DCHECK(new_root->IsRootWindow());
if (new_root == current_root_)
return;
current_root_->RemoveObserver(this);
new_root->AddObserver(this);
auto* new_parent = GetParentContainer(new_root);
new_parent->layer()->Add(layer());
layer()->SetBounds(new_parent->bounds());
current_root_ = new_root;
// Update the bounds of the widgets after setting the new root. For region
// capture, the capture bar will move at a later time, when the mouse is
// released.
if (controller_->source() != CaptureModeSource::kRegion) {
capture_mode_bar_widget_->SetBounds(
CaptureModeBarView::GetBounds(current_root_));
}
// The following call to UpdateCaptureRegion will update the capture label
// bounds, moving it onto the correct display, but will early return if the
// region is already empty.
if (controller_->user_capture_region().IsEmpty())
UpdateCaptureLabelWidgetBounds(/*animate=*/false);
// Start with a new region when we switch displays.
is_selecting_region_ = true;
UpdateCaptureRegion(gfx::Rect(), /*is_resizing=*/false, /*by_user=*/false);
UpdateRootWindowDimmers();
}
void CaptureModeSession::UpdateRootWindowDimmers() {
root_window_dimmers_.clear();
// Add dimmers for all root windows except |current_root_| if needed.
for (aura::Window* root_window : Shell::GetAllRootWindows()) {
if (root_window == current_root_)
continue;
auto dimmer = std::make_unique<WindowDimmer>(root_window);
dimmer->window()->Show();
root_window_dimmers_.emplace(std::move(dimmer));
}
}
bool CaptureModeSession::IsInCountDownAnimation() const {
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
return label_view->IsInCountDownAnimation();
}
void CaptureModeSession::UpdateCursor(const gfx::Point& location_in_screen,
bool is_touch) {
// Hide mouse cursor in tablet mode.
auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller();
if (tablet_mode_controller->InTabletMode() &&
!tablet_mode_controller->IsInDevTabletMode()) {
cursor_setter_->HideCursor();
return;
}
// If the current mouse is on capture bar or settings menu, use the pointer
// mouse cursor.
const bool is_event_on_capture_bar_or_menu =
capture_mode_bar_widget_->GetWindowBoundsInScreen().Contains(
location_in_screen) ||
IsEventOnSettingsWidget(location_in_screen);
if (is_event_on_capture_bar_or_menu) {
cursor_setter_->UpdateCursor(ui::mojom::CursorType::kPointer);
return;
}
// If the current mouse event is on capture label button, and capture label
// button can handle the event, show the hand mouse cursor.
const bool is_event_on_capture_button =
capture_label_widget_->GetWindowBoundsInScreen().Contains(
location_in_screen) &&
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView())
->ShouldHandleEvent();
if (is_event_on_capture_button) {
cursor_setter_->UpdateCursor(ui::mojom::CursorType::kHand);
return;
}
const CaptureModeSource source = controller_->source();
if (source == CaptureModeSource::kWindow && !GetSelectedWindow()) {
// If we're in window capture mode and there is no select window at the
// moment, we should use the original mouse.
cursor_setter_->ResetCursor();
return;
}
if (source == CaptureModeSource::kFullscreen ||
source == CaptureModeSource::kWindow) {
// For fullscreen and other window capture cases, we should either use
// image capture icon or screen record icon as the mouse icon.
cursor_setter_->UpdateCursor(GetCursorForFullscreenOrWindowCapture(
controller_->type() == CaptureModeType::kImage));
return;
}
DCHECK_EQ(source, CaptureModeSource::kRegion);
if (fine_tune_position_ != FineTunePosition::kNone) {
// We're in fine tuning process.
if (capture_mode_util::IsCornerFineTunePosition(fine_tune_position_)) {
cursor_setter_->HideCursor();
} else {
cursor_setter_->UpdateCursor(
GetCursorTypeForFineTunePosition(fine_tune_position_));
}
} else {
// Otherwise update the cursor depending on the current cursor location.
cursor_setter_->UpdateCursor(GetCursorTypeForFineTunePosition(
GetFineTunePosition(location_in_screen, is_touch)));
}
}
bool CaptureModeSession::IsUsingCustomCursor(CaptureModeType type) const {
return cursor_setter_->IsUsingCustomCursor(type);
}
void CaptureModeSession::UpdateCaptureBarWidgetOpacity(float opacity,
bool on_release) {
DCHECK(capture_mode_bar_view_);
DCHECK(capture_mode_bar_widget_->GetLayer());
ui::Layer* capture_bar_layer = capture_mode_bar_widget_->GetLayer();
if (capture_bar_layer->GetTargetOpacity() == opacity)
return;
ui::ScopedLayerAnimationSettings capture_bar_settings(
capture_bar_layer->GetAnimator());
capture_bar_settings.SetTransitionDuration(
on_release ? kCaptureBarOnReleaseOpacityChangeDuration
: kCaptureBarOpacityChangeDuration);
capture_bar_settings.SetTweenType(on_release ? gfx::Tween::FAST_OUT_SLOW_IN
: gfx::Tween::LINEAR);
capture_bar_settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
capture_bar_layer->SetOpacity(opacity);
}
void CaptureModeSession::ClampCaptureRegionToRootWindowSize() {
gfx::Rect new_capture_region = controller_->user_capture_region();
new_capture_region.AdjustToFit(current_root_->bounds());
controller_->SetUserCaptureRegion(new_capture_region, /*by_user=*/false);
}
void CaptureModeSession::EndSelection(bool is_event_on_capture_bar_or_menu,
bool region_intersects_capture_bar) {
fine_tune_position_ = FineTunePosition::kNone;
anchor_points_.clear();
is_drag_in_progress_ = false;
Shell::Get()->UpdateCursorCompositingEnabled();
// TODO(richui): Update this for tablet mode.
UpdateCaptureBarWidgetOpacity(
region_intersects_capture_bar && !is_event_on_capture_bar_or_menu
? kCaptureBarOverlapOpacity
: 1.f,
/*on_release=*/true);
UpdateDimensionsLabelWidget(/*is_resizing=*/false);
CloseMagnifierGlass();
}
void CaptureModeSession::RepaintRegion() {
gfx::Rect damage_region = controller_->user_capture_region();
damage_region.Inset(gfx::Insets(-kDamageInsetDp));
layer()->SchedulePaint(damage_region);
}
void CaptureModeSession::SelectDefaultRegion() {
is_selecting_region_ = false;
// Default is centered in the root, and its width and height are
// |kRegionDefaultRatio| size of the root.
gfx::Rect default_capture_region = current_root_->bounds();
default_capture_region.ClampToCenteredSize(gfx::ScaleToCeiledSize(
default_capture_region.size(), kRegionDefaultRatio));
UpdateCaptureRegion(default_capture_region, /*is_resizing=*/false,
/*by_user=*/true);
}
void CaptureModeSession::UpdateRegionHorizontally(bool left,
bool is_shift_down) {
if (focused_fine_tune_position_ == FineTunePosition::kNone ||
focused_fine_tune_position_ == FineTunePosition::kTopCenter ||
focused_fine_tune_position_ == FineTunePosition::kBottomCenter) {
return;
}
const int change = GetArrowKeyPressChange(is_shift_down);
gfx::Rect new_capture_region = controller_->user_capture_region();
if (focused_fine_tune_position_ == FineTunePosition::kCenter) {
new_capture_region.Offset(left ? -change : change, 0);
new_capture_region.AdjustToFit(current_root_->bounds());
} else {
const gfx::Point location =
capture_mode_util::GetLocationForFineTunePosition(
new_capture_region, focused_fine_tune_position_);
// If an affordance circle on the left side of the capture region is
// focused, left presses will enlarge the existing region and right presses
// will shrink the existing region. If it is on the right side, right
// presses will enlarge and left presses will shrink.
const bool affordance_on_left = location.x() == new_capture_region.x();
const bool shrink = affordance_on_left ^ left;
if (shrink && new_capture_region.width() < change)
return;
const int inset = shrink ? change : -change;
gfx::Insets insets(0, affordance_on_left ? inset : 0, 0,
affordance_on_left ? 0 : inset);
new_capture_region.Inset(insets);
ClipRectToFit(&new_capture_region, current_root_->bounds());
}
UpdateCaptureRegion(new_capture_region, /*is_resizing=*/false,
/*by_user=*/true);
}
void CaptureModeSession::UpdateRegionVertically(bool up, bool is_shift_down) {
if (focused_fine_tune_position_ == FineTunePosition::kNone ||
focused_fine_tune_position_ == FineTunePosition::kLeftCenter ||
focused_fine_tune_position_ == FineTunePosition::kRightCenter) {
return;
}
const int change = GetArrowKeyPressChange(is_shift_down);
gfx::Rect new_capture_region = controller_->user_capture_region();
// TODO(sammiequon): The below is similar to UpdateRegionHorizontally() except
// we are acting on the y-axis. Investigate if we can remove the duplication.
if (focused_fine_tune_position_ == FineTunePosition::kCenter) {
new_capture_region.Offset(0, up ? -change : change);
new_capture_region.AdjustToFit(current_root_->bounds());
} else {
const gfx::Point location =
capture_mode_util::GetLocationForFineTunePosition(
new_capture_region, focused_fine_tune_position_);
// If an affordance circle on the top side of the capture region is
// focused, up presses will enlarge the existing region and down presses
// will shrink the existing region. If it is on the bottom side, down
// presses will enlarge and up presses will shrink.
const bool affordance_on_top = location.y() == new_capture_region.y();
const bool shrink = affordance_on_top ^ up;
if (shrink && new_capture_region.height() < change)
return;
const int inset = shrink ? change : -change;
gfx::Insets insets(affordance_on_top ? inset : 0, 0,
affordance_on_top ? 0 : inset, 0);
new_capture_region.Inset(insets);
ClipRectToFit(&new_capture_region, current_root_->bounds());
}
UpdateCaptureRegion(new_capture_region, /*is_resizing=*/false,
/*by_user=*/true);
}
bool CaptureModeSession::IsEventOnSettingsWidget(
const gfx::Point& location_in_screen) {
return capture_mode_settings_widget_ &&
capture_mode_settings_widget_->GetWindowBoundsInScreen().Contains(
location_in_screen);
}
} // namespace ash