// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/accessibility/magnifier/docked_magnifier_controller.h"
#include <algorithm>
#include <utility>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/magnifier/magnifier_utils.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/display/cursor_window_controller.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/host/ash_window_tree_host.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/work_area_insets.h"
#include "base/functional/bind.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/window_tree_host.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
// Default, minimum, and maximum magnifier scale values (magnification levels).
constexpr float kDefaultMagnifierScale = 4.0f;
constexpr float kMinMagnifierScale = 1.0f;
constexpr float kMaxMagnifierScale = 20.0f;
// Minimum and maximum screen height divisors. These correspond to the tallest
// and shortest allowed docked magnifier viewport heights, as
// viewport_height = root_bounds.height() / screen_height_divisor.
constexpr float kMinScreenHeightDivisor = 5.0f / 4.0f;
constexpr float kMaxScreenHeightDivisor = 8.0f;
// The factor by which the offset of scroll events are scaled.
constexpr float kScrollScaleFactor = 0.0125f;
constexpr char kDockedMagnifierViewportWindowName[] =
"DockedMagnifierViewportWindow";
// Returns the current cursor location in screen coordinates.
inline gfx::Point GetCursorScreenPoint() {
return display::Screen::GetScreen()->GetCursorScreenPoint();
}
// Updates the workarea of the display associated with |window| such that the
// given magnifier viewport |height| is allocated at the top of the screen.
void SetViewportHeightInWorkArea(aura::Window* window, int height) {
DCHECK(window);
WorkAreaInsets::ForWindow(window->GetRootWindow())
->SetDockedMagnifierHeight(height);
}
// Returns the separator layer bounds from the given |viewport_bounds|. The
// separator layer is to be placed right below the viewport.
inline gfx::Rect SeparatorBoundsFromViewportBounds(
const gfx::Rect& viewport_bounds) {
return gfx::Rect(viewport_bounds.x(), viewport_bounds.bottom(),
viewport_bounds.width(),
DockedMagnifierController::kSeparatorHeight);
}
// Returns the child container in |root| that should be used as the parent of
// viewport widget.
aura::Window* GetViewportParentContainerForRoot(aura::Window* root) {
return root->GetChildById(kShellWindowId_DockedMagnifierContainer);
}
// Returns the child container in |root| that should be used as the parent of
// the separator layer.
aura::Window* GetViewportParentContainerForDivider(aura::Window* root) {
return root->GetChildById(kShellWindowId_OverlayContainer);
}
} // namespace
// static
DockedMagnifierController::DockedMagnifierController() {
Shell::Get()->session_controller()->AddObserver(this);
}
DockedMagnifierController::~DockedMagnifierController() {
Shell* shell = Shell::Get();
shell->session_controller()->RemoveObserver(this);
if (GetEnabled()) {
shell->display_manager()->RemoveDisplayManagerObserver(this);
shell->RemovePreTargetHandler(this);
}
CHECK(!views::WidgetObserver::IsInObserverList());
}
// static
void DockedMagnifierController::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kDockedMagnifierEnabled, false);
registry->RegisterDoublePref(prefs::kDockedMagnifierScale,
kDefaultMagnifierScale);
registry->RegisterDoublePref(prefs::kDockedMagnifierScreenHeightDivisor,
kDefaultScreenHeightDivisor);
}
bool DockedMagnifierController::GetEnabled() const {
return active_user_pref_service_ &&
active_user_pref_service_->GetBoolean(prefs::kDockedMagnifierEnabled);
}
float DockedMagnifierController::GetScale() const {
if (active_user_pref_service_)
return active_user_pref_service_->GetDouble(prefs::kDockedMagnifierScale);
return kDefaultMagnifierScale;
}
float DockedMagnifierController::GetScreenHeightDivisor() const {
if (active_user_pref_service_) {
return active_user_pref_service_->GetDouble(
prefs::kDockedMagnifierScreenHeightDivisor);
}
return kDefaultScreenHeightDivisor;
}
void DockedMagnifierController::SetEnabled(bool enabled) {
if (active_user_pref_service_) {
active_user_pref_service_->SetBoolean(prefs::kDockedMagnifierEnabled,
enabled);
}
}
void DockedMagnifierController::SetScale(float scale) {
if (active_user_pref_service_) {
active_user_pref_service_->SetDouble(
prefs::kDockedMagnifierScale,
std::clamp(scale, kMinMagnifierScale, kMaxMagnifierScale));
}
}
void DockedMagnifierController::SetScreenHeightDivisor(
float screen_height_divisor) {
if (active_user_pref_service_) {
active_user_pref_service_->SetDouble(
prefs::kDockedMagnifierScreenHeightDivisor,
std::clamp(screen_height_divisor, kMinScreenHeightDivisor,
kMaxScreenHeightDivisor));
}
}
void DockedMagnifierController::StepToNextScaleValue(int delta_index) {
SetScale(magnifier_utils::GetNextMagnifierScaleValue(
delta_index, GetScale(), kMinMagnifierScale, kMaxMagnifierScale));
}
void DockedMagnifierController::MoveMagnifierToRect(
const gfx::Rect& rect_in_screen) {
DCHECK(GetEnabled());
gfx::Point point_in_screen = rect_in_screen.CenterPoint();
// If rect is too wide to fit in viewport, include as much as we can, starting
// with the left edge.
const int scaled_viewport_width =
current_source_root_window_->bounds().width() / GetScale();
if (rect_in_screen.width() > scaled_viewport_width) {
point_in_screen.set_x(std::max(rect_in_screen.x() +
scaled_viewport_width / 2 -
magnifier_utils::kLeftEdgeContextPadding,
0));
}
CenterOnPoint(point_in_screen);
}
void DockedMagnifierController::CenterOnPoint(
const gfx::Point& point_in_screen) {
if (!GetEnabled())
return;
auto* screen = display::Screen::GetScreen();
auto* window = screen->GetWindowAtScreenPoint(point_in_screen);
if (!window) {
// In tests and sometimes initially on signin screen, |point_in_screen|
// maybe invalid and doesn't belong to any existing root window. However, we
// are here because the Docked Magnifier is enabled. We need to create the
// viewport widget somewhere, so we'll use the primary root window until we
// get a valid cursor event.
window = Shell::GetPrimaryRootWindow();
}
auto* root_window = window->GetRootWindow();
DCHECK(root_window);
SwitchCurrentSourceRootWindowIfNeeded(root_window,
true /* update_old_root_workarea */);
auto* host = root_window->GetHost();
DCHECK(host);
MaybeCachePointOfInterestMinimumHeight(host);
gfx::Point point_of_interest(point_in_screen);
::wm::ConvertPointFromScreen(root_window, &point_of_interest);
// The point of interest in pixels.
gfx::PointF point_in_pixels(point_of_interest);
// Before transforming to pixels, make sure its y-coordinate doesn't go below
// the minimum height. Do it here for this PointF since the
// |minimum_point_of_interest_height_| is a float, in order to avoid rounding
// in the transform to be able to reliably verify it in tests.
if (point_in_pixels.y() < minimum_point_of_interest_height_)
point_in_pixels.set_y(minimum_point_of_interest_height_);
// The pixel space is the magnified space.
const float scale = GetScale();
point_in_pixels.Scale(scale);
// Transform steps: (Note that the transform is applied in the opposite
// order)
// 1- Scale the layer by |scale|.
// 2- Translate the point of interest to the center point of the viewport
// widget.
const gfx::Point viewport_center_point =
magnifier_utils::GetViewportWidgetBoundsInRoot(
current_source_root_window_, GetScreenHeightDivisor())
.CenterPoint();
gfx::Transform transform;
transform.Translate(viewport_center_point.x() - point_in_pixels.x(),
viewport_center_point.y() - point_in_pixels.y());
transform.Scale(scale, scale);
// When updating the transform, we don't want any animation, otherwise the
// movement of the mouse won't be very smooth. We want the magnifier layer to
// update immediately with the movement of the mouse (or the change in the
// point of interest due to input caret bounds changes ... etc.).
ui::ScopedLayerAnimationSettings settings(
viewport_magnifier_layer_->GetAnimator());
settings.SetTransitionDuration(base::Milliseconds(0));
settings.SetTweenType(gfx::Tween::ZERO);
settings.SetPreemptionStrategy(ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
viewport_magnifier_layer_->SetTransform(transform);
}
int DockedMagnifierController::GetMagnifierHeightForTesting() const {
return GetTotalMagnifierHeight();
}
void DockedMagnifierController::OnActiveUserPrefServiceChanged(
PrefService* pref_service) {
active_user_pref_service_ = pref_service;
InitFromUserPrefs();
}
void DockedMagnifierController::OnSigninScreenPrefServiceInitialized(
PrefService* prefs) {
OnActiveUserPrefServiceChanged(prefs);
}
void DockedMagnifierController::OnMouseEvent(ui::MouseEvent* event) {
DCHECK(GetEnabled());
MaybePerformViewportResizing(event);
CenterOnPoint(GetCursorScreenPoint());
}
void DockedMagnifierController::OnScrollEvent(ui::ScrollEvent* event) {
DCHECK(GetEnabled());
if (!event->IsAltDown() || !event->IsControlDown())
return;
if (event->type() == ui::EventType::kScrollFlingStart ||
event->type() == ui::EventType::kScrollFlingCancel) {
event->StopPropagation();
return;
}
if (event->type() == ui::EventType::kScroll) {
// Notes: - Clamping of the new scale value happens inside SetScale().
// - Refreshing the viewport happens in the handler of the scale pref
// changes.
SetScale(magnifier_utils::GetScaleFromScroll(
event->y_offset() * kScrollScaleFactor, GetScale(), kMaxMagnifierScale,
kMinMagnifierScale));
event->StopPropagation();
}
}
void DockedMagnifierController::OnTouchEvent(ui::TouchEvent* event) {
DCHECK(GetEnabled());
aura::Window* target = static_cast<aura::Window*>(event->target());
aura::Window* event_root = target->GetRootWindow();
gfx::Point event_screen_point = event->root_location();
::wm::ConvertPointToScreen(event_root, &event_screen_point);
// Ignore touch events on virtual Keyboard, to stabilize docked magnifier.
if (keyboard::KeyboardUIController::Get()->IsEnabled() &&
keyboard::KeyboardUIController::Get()
->GetKeyboardWindow()
->GetBoundsInScreen()
.Contains(event_screen_point))
return;
CenterOnPoint(event_screen_point);
}
void DockedMagnifierController::OnWidgetDestroying(views::Widget* widget) {
DCHECK_EQ(widget, viewport_widget_);
SwitchCurrentSourceRootWindowIfNeeded(nullptr,
false /* update_old_root_workarea */);
}
void DockedMagnifierController::OnDidApplyDisplayChanges() {
DCHECK(GetEnabled());
// The viewport might have been on a display that just got removed, and hence
// the viewport widget and its associated layers are already destroyed. In
// that case we also cleared the |current_source_root_window_|.
if (current_source_root_window_) {
// Resolution may have changed. Update all bounds.
const auto viewport_bounds = magnifier_utils::GetViewportWidgetBoundsInRoot(
current_source_root_window_, GetScreenHeightDivisor());
viewport_widget_->SetBounds(viewport_bounds);
viewport_background_layer_->SetBounds(viewport_bounds);
separator_layer_->SetBounds(
SeparatorBoundsFromViewportBounds(viewport_bounds));
SetViewportHeightInWorkArea(current_source_root_window_,
GetTotalMagnifierHeight());
// Resolution changes, screen rotation, etc. can reset the host to confine
// the mouse cursor inside the root window. We want to make sure the cursor
// is confined properly outside the viewport. But don't confine mouse if
// resizing.
if (!is_resizing_)
ConfineMouseCursorOutsideViewport();
}
// A change in display configuration, such as resolution, rotation, ... etc.
// invalidates the currently cached minimum height of the point of interest.
is_minimum_point_of_interest_height_valid_ = false;
// Update the viewport magnifier layer transform.
CenterOnPoint(GetCursorScreenPoint());
}
bool DockedMagnifierController::GetFullscreenMagnifierEnabled() const {
return active_user_pref_service_ &&
active_user_pref_service_->GetBoolean(
prefs::kAccessibilityScreenMagnifierEnabled);
}
void DockedMagnifierController::SetFullscreenMagnifierEnabled(bool enabled) {
if (active_user_pref_service_) {
active_user_pref_service_->SetBoolean(
prefs::kAccessibilityScreenMagnifierEnabled, enabled);
}
}
int DockedMagnifierController::GetTotalMagnifierHeight() const {
if (separator_layer_)
return separator_layer_->bounds().bottom();
return 0;
}
gfx::Rect DockedMagnifierController::GetTotalMagnifierBoundsForRoot(
aura::Window* root) const {
DCHECK(root);
DCHECK(root->IsRootWindow());
if (viewport_widget_ && current_source_root_window_ == root) {
gfx::Rect bounds =
viewport_widget_->GetNativeWindow()->GetActualBoundsInRootWindow();
DCHECK(separator_layer_);
bounds.set_height(separator_layer_->bounds().bottom());
return bounds;
}
return gfx::Rect();
}
const views::Widget* DockedMagnifierController::GetViewportWidgetForTesting()
const {
return viewport_widget_;
}
const ui::Layer*
DockedMagnifierController::GetViewportMagnifierLayerForTesting() const {
return viewport_magnifier_layer_.get();
}
float DockedMagnifierController::GetMinimumPointOfInterestHeightForTesting()
const {
return minimum_point_of_interest_height_;
}
void DockedMagnifierController::MaybeSetCursorSize(ui::CursorSize cursor_size) {
if (Shell::Get()->accessibility_controller()->large_cursor().enabled())
return;
Shell::Get()->cursor_manager()->SetCursorSize(cursor_size);
}
void DockedMagnifierController::MaybePerformViewportResizing(
ui::MouseEvent* event) {
DCHECK(current_source_root_window_);
gfx::Rect root_bounds = current_source_root_window_->GetBoundsInRootWindow();
float magnifier_height = root_bounds.height() / GetScreenHeightDivisor();
float root_y = event->root_location_f().y();
const int separator_top = separator_layer_->bounds().y();
const int separator_bottom = separator_layer_->bounds().bottom();
bool cursor_is_over_resizer =
root_y >= separator_top - 1 && root_y <= separator_bottom;
::wm::CursorManager* cursor_manager = Shell::Get()->cursor_manager();
CursorWindowController* cursor_window_controller =
Shell::Get()->window_tree_host_manager()->cursor_window_controller();
// If cursor is over separator, change to north/south resize, move on top.
// Reset once the cursor is not over separator, and user isn't resizing.
if (cursor_is_over_resizer && !is_cursor_locked_) {
MaybeSetCursorSize(ui::CursorSize::kLarge);
cursor_manager->SetCursor(ui::mojom::CursorType::kNorthSouthResize);
cursor_manager->LockCursor();
cursor_window_controller->OnDockedMagnifierResizingStateChanged(true);
is_cursor_locked_ = true;
} else if (!cursor_is_over_resizer && !is_resizing_) {
MaybeResetResizingCursor();
}
// If user releases left mouse button, or any other mouse button is pressed,
// ignore and stop resizing.
if (!event->IsOnlyLeftMouseButton() ||
event->type() == ui::EventType::kMouseReleased) {
if (is_resizing_) {
is_resizing_ = false;
ConfineMouseCursorOutsideViewport();
}
return;
}
float new_screen_height_divisor =
root_bounds.height() / std::max(1.0f, root_y + resize_offset_);
switch (event->type()) {
case ui::EventType::kMousePressed:
// User clicks within separator to start resizing Docked Magnifier.
// Subtracting one is needed to capture when mouse is at the very top.
if (!is_resizing_ && cursor_is_over_resizer) {
resize_offset_ = magnifier_height - root_y;
is_resizing_ = true;
RootWindowController::ForWindow(current_source_root_window_)
->ash_host()
->ConfineCursorToRootWindow();
}
break;
case ui::EventType::kMouseDragged:
// User continues holding and drags separator to resize Docked Magnifier.
if (is_resizing_) {
SetScreenHeightDivisor(std::clamp(new_screen_height_divisor,
kMinScreenHeightDivisor,
kMaxScreenHeightDivisor));
OnDidApplyDisplayChanges();
}
break;
default:
break;
}
}
void DockedMagnifierController::MaybeResetResizingCursor() {
if (!is_cursor_locked_) {
return;
}
MaybeSetCursorSize(ui::CursorSize::kNormal);
Shell::Get()->cursor_manager()->UnlockCursor();
Shell::Get()
->window_tree_host_manager()
->cursor_window_controller()
->OnDockedMagnifierResizingStateChanged(false);
is_cursor_locked_ = false;
}
void DockedMagnifierController::SwitchCurrentSourceRootWindowIfNeeded(
aura::Window* new_root_window,
bool update_old_root_workarea) {
if (current_source_root_window_ == new_root_window)
return;
aura::Window* old_root_window = current_source_root_window_;
current_source_root_window_ = new_root_window;
// Current window changes means the minimum height of the point of interest is
// no longer valid.
is_minimum_point_of_interest_height_valid_ = false;
if (old_root_window) {
if (update_old_root_workarea)
SetViewportHeightInWorkArea(old_root_window, 0);
// Reset mouse cursor confinement to default.
RootWindowController::ForWindow(old_root_window)
->ash_host()
->ConfineCursorToRootWindow();
}
separator_layer_ = nullptr;
if (viewport_widget_) {
viewport_widget_->RemoveObserver(this);
viewport_widget_->Close();
viewport_widget_ = nullptr;
}
viewport_background_layer_ = nullptr;
viewport_magnifier_layer_ = nullptr;
if (!current_source_root_window_) {
// No need to create a new magnifier viewport.
return;
}
CreateMagnifierViewport();
auto* magnified_container = current_source_root_window_->GetChildById(
kShellWindowId_MagnifiedContainer);
viewport_magnifier_layer_->SetShowReflectedLayerSubtree(
magnified_container->layer());
}
void DockedMagnifierController::InitFromUserPrefs() {
DCHECK(active_user_pref_service_);
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(active_user_pref_service_);
pref_change_registrar_->Add(
prefs::kDockedMagnifierEnabled,
base::BindRepeating(&DockedMagnifierController::OnEnabledPrefChanged,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kDockedMagnifierScale,
base::BindRepeating(&DockedMagnifierController::OnScalePrefChanged,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kAccessibilityScreenMagnifierEnabled,
base::BindRepeating(
&DockedMagnifierController::OnFullscreenMagnifierEnabledPrefChanged,
base::Unretained(this)));
OnEnabledPrefChanged();
}
void DockedMagnifierController::OnEnabledPrefChanged() {
// When switching from the signin screen to a newly created profile while the
// Docked Magnifier is enabled, the prefs will be copied from the signin
// profile to the user profile, and the Docked Magnifier will remain enabled.
// We don't want to redo the below operations if the status doesn't change,
// for example readding the same observer to the WindowTreeHostManager will
// cause a crash on DCHECK on debug builds.
const bool current_enabled = !!current_source_root_window_;
const bool new_enabled = GetEnabled();
if (current_enabled == new_enabled)
return;
// Toggling the status of the docked magnifier, changes the display's work
// area. However, display's work area changes are not allowed while overview
// mode is active (See https://crbug.com/834400). For this reason, we exit
// overview mode, before we actually update the state of docked magnifier
// below. https://crbug.com/894256.
Shell* shell = Shell::Get();
auto* overview_controller = shell->overview_controller();
if (overview_controller->InOverviewSession()) {
// |OverviewController::EndOverview| fails (returning false) in certain
// cases involving tablet split view mode. We can guarantee success by
// ensuring that tablet split view mode is not in session.
auto* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
if (split_view_controller->InTabletSplitViewMode()) {
split_view_controller->EndSplitView(
SplitViewController::EndReason::kNormal);
}
overview_controller->EndOverview(
OverviewEndAction::kEnabledDockedMagnifier);
}
if (new_enabled) {
// Enabling the Docked Magnifier disables the Fullscreen Magnifier.
SetFullscreenMagnifierEnabled(false);
// Calling refresh will result in the creation of the magnifier viewport and
// its associated layers.
Refresh();
// Make sure we are in front of the fullscreen magnifier which also handles
// scroll events.
shell->AddAccessibilityEventHandler(
this, AccessibilityEventHandlerManager::HandlerType::kDockedMagnifier);
shell->display_manager()->AddDisplayManagerObserver(this);
} else {
shell->display_manager()->RemoveDisplayManagerObserver(this);
shell->RemoveAccessibilityEventHandler(this);
MaybeResetResizingCursor();
// Setting the current root window to |nullptr| will remove the viewport and
// all its associated layers.
SwitchCurrentSourceRootWindowIfNeeded(nullptr,
true /* update_old_root_workarea */);
}
// Update the green checkmark status in the accessibility menu in the system
// tray.
shell->accessibility_controller()->NotifyAccessibilityStatusChanged();
// We use software composited mouse cursor so that it can be mirrored into the
// magnifier viewport.
shell->UpdateCursorCompositingEnabled();
}
void DockedMagnifierController::OnScalePrefChanged() {
if (GetEnabled()) {
// Invalidate the cached minimum height of the point of interest since the
// change in scale changes that height.
is_minimum_point_of_interest_height_valid_ = false;
Refresh();
}
}
void DockedMagnifierController::OnFullscreenMagnifierEnabledPrefChanged() {
// Enabling the Fullscreen Magnifier disables the Docked Magnifier.
if (GetFullscreenMagnifierEnabled())
SetEnabled(false);
}
void DockedMagnifierController::Refresh() {
DCHECK(GetEnabled());
CenterOnPoint(GetCursorScreenPoint());
}
void DockedMagnifierController::CreateMagnifierViewport() {
DCHECK(GetEnabled());
DCHECK(current_source_root_window_);
const auto viewport_bounds = magnifier_utils::GetViewportWidgetBoundsInRoot(
current_source_root_window_, GetScreenHeightDivisor());
// 1- Create the viewport widget.
viewport_widget_ = new views::Widget;
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.accept_events = false;
params.bounds = viewport_bounds;
params.opacity = views::Widget::InitParams::WindowOpacity::kOpaque;
aura::Window* const viewport_parent =
GetViewportParentContainerForRoot(current_source_root_window_);
params.parent = viewport_parent;
params.name = kDockedMagnifierViewportWindowName;
viewport_widget_->Init(std::move(params));
// 2- Create the separator layer right below the viwport widget, parented to
// the layer of the root window.
separator_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
separator_layer_->SetColor(SK_ColorBLACK);
separator_layer_->SetBounds(
SeparatorBoundsFromViewportBounds(viewport_bounds));
aura::Window* const separator_parent =
GetViewportParentContainerForDivider(current_source_root_window_);
separator_parent->layer()->Add(separator_layer_.get());
// 3- Create a background layer that will show a dark gray color behind the
// magnifier layer. It has the same bounds as the viewport.
viewport_background_layer_ =
std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
viewport_background_layer_->SetColor(SK_ColorDKGRAY);
viewport_background_layer_->SetBounds(viewport_bounds);
aura::Window* viewport_window = viewport_widget_->GetNativeView();
ui::Layer* viewport_layer = viewport_window->layer();
viewport_layer->Add(viewport_background_layer_.get());
// 4- Create the layer in which the contents of the screen will be mirrored
// and magnified.
viewport_magnifier_layer_ =
std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
// There are situations that the content rect for the magnified container gets
// larger than its bounds (e.g. shelf stretches beyond the screen to allow it
// being dragged up, or contents of mouse pointer might go beyond screen when
// the pointer is at the edges of the screen). To avoid this extra content
// becoming visible in the magnifier, magnifier layer should clip its contents
// to its bounds.
viewport_magnifier_layer_->SetMasksToBounds(true);
viewport_layer->Add(viewport_magnifier_layer_.get());
viewport_layer->SetMasksToBounds(true);
// 5- Update the workarea of the current screen such that an area enough to
// contain the viewport and the separator is allocated at the top of the
// screen.
SetViewportHeightInWorkArea(current_source_root_window_,
GetTotalMagnifierHeight());
// 6- Confine the mouse cursor within the remaining part of the display.
ConfineMouseCursorOutsideViewport();
// 7- Show the widget, which can trigger events to request movement of the
// viewport now that all internal state has been created.
viewport_widget_->AddObserver(this);
viewport_widget_->Show();
}
void DockedMagnifierController::MaybeCachePointOfInterestMinimumHeight(
aura::WindowTreeHost* host) {
DCHECK(GetEnabled());
DCHECK(current_source_root_window_);
DCHECK(host);
if (is_minimum_point_of_interest_height_valid_)
return;
// Adjust the point of interest so that we don't end up magnifying the
// magnifier. This means we don't allow the point of interest to go beyond a
// minimum y-coordinate value. Here's how we find that minimum value:
//
// +-----------------+ +-----------------------------------+
// | Viewport | | |
// +====separator====+ | Magnified Viewport |
// | (+) b | |
// | | +==============separator===========(+) A
// | | | Distance (D) --> |
// | | | (+) B
// +-----------------+ | |
// Screen in Non | |
// Magnified Space | |
// | |
// | |
// +-----------------------------------+
// Screen in Magnified Space
// (the contents of |viewport_magnifier_layer_|)
//
// Problem: Find the height of the point of interest (b) in the non-magnified
// coordinates space, which corresponds to the height of point (B) in
// the magnified coordinates space, such that when point (A) is
// translated from the magnified coordinates space to the non-
// magnified coordinates space, its y coordinate is 0 (i.e. aligns
// with the top of the magnifier viewport).
//
// 1- The height of Point (A) in the magnified space is the bottom of the
// entire magnifier (which is actually the bottom of the separator) in the
// magnified coordinates space.
// Note that the magnified space is in pixels. This point should be
// translated such that its y-coordiante is not greater than 0 (in the non-
// magnified coordinates space), otherwise the magnifier will magnify and
// mirror itself.
// 2- Point (B) is the scaled point of interest in the magnified space. The
// point of interest is always translated to the center point of the
// viewport. Hence, if point (A) goes to y = 0, and point (B) goes to a
// height equals to the height of the center point of the viewport,
// therefore means distance (D) = viewport_center_point.y().
// 3- Now that we found the height of point (B) in the magnified space,
// find the the height of point (b) which is the corresponding height in
// the non-magnified space. This height is the minimum height below which
// the point of interest may not go.
const gfx::Rect viewport_bounds =
magnifier_utils::GetViewportWidgetBoundsInRoot(
current_source_root_window_, GetScreenHeightDivisor());
// 1- Point (A)'s height.
// Note we use a Vector3dF to actually represent a 2D point. The reason is
// Vector3dF provides an API to get the Length() of the vector without
// converting the object to another temporary object. We need to get the
// Length() rather than y() because screen rotation transform can make the
// height we are interested in either x() or y() depending on the rotation
// angle, so we just simply use Length().
// Note: Why transform the point to the magnified scale and back? The reason
// is that we need to go through the root window transform to go to the pixel
// space. This will account for device scale factors, screen rotations, and
// any other transforms that we cannot anticipate ourselves.
gfx::Vector3dF scaled_magnifier_bottom_in_pixels(
0.0f, viewport_bounds.bottom() + kSeparatorHeight, 0.0f);
const float scale = GetScale();
scaled_magnifier_bottom_in_pixels.Scale(scale);
// 2- Point (B)'s height.
const gfx::PointF viewport_center_point(viewport_bounds.CenterPoint());
gfx::Vector3dF minimum_height_vector(
0.0f,
viewport_center_point.y() + scaled_magnifier_bottom_in_pixels.Length(),
0.0f);
// 3- Back to non-magnified space to get point (b)'s height.
minimum_height_vector.Scale(1 / scale);
minimum_point_of_interest_height_ = minimum_height_vector.Length();
is_minimum_point_of_interest_height_valid_ = true;
}
void DockedMagnifierController::ConfineMouseCursorOutsideViewport() {
DCHECK(current_source_root_window_);
gfx::Rect confine_bounds =
current_source_root_window_->GetBoundsInRootWindow();
const auto viewport_bounds = magnifier_utils::GetViewportWidgetBoundsInRoot(
current_source_root_window_, GetScreenHeightDivisor());
const int docked_height = viewport_bounds.height();
confine_bounds.Offset(0, docked_height);
confine_bounds.set_height(confine_bounds.height() - docked_height);
RootWindowController::ForWindow(current_source_root_window_)
->ash_host()
->ConfineCursorToBoundsInRoot(confine_bounds);
}
} // namespace ash