blob: ad1458b4a642b3b00056bb9da04cff1d18046448 [file] [log] [blame]
// Copyright (c) 2012 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/magnifier/magnification_controller.h"
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "ash/accelerators/accelerator_controller.h"
#include "ash/accessibility/accessibility_delegate.h"
#include "ash/display/root_window_transformers.h"
#include "ash/host/ash_window_tree_host.h"
#include "ash/host/root_window_transformer.h"
#include "ash/magnifier/magnifier_utils.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "base/numerics/ranges.h"
#include "base/synchronization/waitable_event.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/ime_bridge.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/compositor/dip_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/events/gestures/gesture_provider_aura.h"
#include "ui/gfx/geometry/point3_f.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/keyboard/keyboard_controller.h"
#include "ui/wm/core/compound_event_filter.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
constexpr float kMaxMagnifiedScale = 20.0f;
constexpr float kMinMagnifiedScaleThreshold = 1.1f;
constexpr float kNonMagnifiedScale = 1.0f;
constexpr float kInitialMagnifiedScale = 2.0f;
constexpr float kScrollScaleChangeFactor = 0.00125f;
// Default animation parameters for redrawing the magnification window.
constexpr gfx::Tween::Type kDefaultAnimationTweenType = gfx::Tween::EASE_OUT;
constexpr int kDefaultAnimationDurationInMs = 100;
// Use linear transformation to make the magnifier window move smoothly
// to center the focus when user types in a text input field.
constexpr gfx::Tween::Type kCenterCaretAnimationTweenType = gfx::Tween::LINEAR;
// The delay of the timer for moving magnifier window for centering the text
// input focus.
constexpr int kMoveMagnifierDelayInMs = 10;
// Threshold of panning. If the cursor moves to within pixels (in DIP) of
// |kCursorPanningMargin| from the edge, the view-port moves.
constexpr int kCursorPanningMargin = 100;
// Threshold of panning at the bottom when the virtual keyboard is up. If the
// cursor moves to within pixels (in DIP) of |kKeyboardBottomPanningMargin| from
// the bottom edge, the view-port moves. This is only used by
// MoveMagnifierWindowFollowPoint() when |reduce_bottom_margin| is true.
constexpr int kKeyboardBottomPanningMargin = 10;
// Threadshold of panning. If the caret moves to within pixels (in DIP) of
// |kCaretPanningMargin| from the edge, the view-port moves.
constexpr int kCaretPanningMargin = 50;
void MoveCursorTo(aura::WindowTreeHost* host, const gfx::Point& root_location) {
auto host_location_3f = gfx::Point3F(gfx::PointF(root_location));
host->GetRootTransform().TransformPoint(&host_location_3f);
host->MoveCursorToLocationInPixels(
gfx::ToCeiledPoint(host_location_3f.AsPointF()));
}
} // namespace
class MagnificationController::GestureProviderClient
: public ui::GestureProviderAuraClient {
public:
GestureProviderClient() = default;
~GestureProviderClient() override = default;
// ui::GestureProviderAuraClient overrides:
void OnGestureEvent(GestureConsumer* consumer,
ui::GestureEvent* event) override {
// Do nothing. OnGestureEvent is for timer based gesture events, e.g. tap.
// MagnificationController is interested only in pinch and scroll
// gestures.
DCHECK_NE(ui::ET_GESTURE_SCROLL_BEGIN, event->type());
DCHECK_NE(ui::ET_GESTURE_SCROLL_END, event->type());
DCHECK_NE(ui::ET_GESTURE_SCROLL_UPDATE, event->type());
DCHECK_NE(ui::ET_GESTURE_PINCH_BEGIN, event->type());
DCHECK_NE(ui::ET_GESTURE_PINCH_END, event->type());
DCHECK_NE(ui::ET_GESTURE_PINCH_UPDATE, event->type());
}
private:
DISALLOW_COPY_AND_ASSIGN(GestureProviderClient);
};
MagnificationController::MagnificationController()
: root_window_(Shell::GetPrimaryRootWindow()),
scale_(kNonMagnifiedScale),
original_scale_(kNonMagnifiedScale) {
Shell::Get()->AddPreTargetHandler(this);
root_window_->AddObserver(this);
root_window_->GetHost()->GetEventSource()->AddEventRewriter(this);
if (ui::IMEBridge::Get())
ui::IMEBridge::Get()->AddObserver(this);
point_of_interest_in_root_ = root_window_->bounds().CenterPoint();
gesture_provider_client_ = std::make_unique<GestureProviderClient>();
gesture_provider_ = std::make_unique<ui::GestureProviderAura>(
this, gesture_provider_client_.get());
}
MagnificationController::~MagnificationController() {
if (input_method_)
input_method_->RemoveObserver(this);
input_method_ = nullptr;
if (ui::IMEBridge::Get())
ui::IMEBridge::Get()->RemoveObserver(this);
root_window_->GetHost()->GetEventSource()->RemoveEventRewriter(this);
root_window_->RemoveObserver(this);
Shell::Get()->RemovePreTargetHandler(this);
}
void MagnificationController::SetEnabled(bool enabled) {
if (enabled) {
if (!is_enabled_) {
input_method_ = magnifier_utils::GetInputMethod(root_window_);
if (input_method_)
input_method_->AddObserver(this);
}
Shell* shell = Shell::Get();
float scale =
shell->accessibility_delegate()->GetSavedScreenMagnifierScale();
if (scale <= 0.0f)
scale = kInitialMagnifiedScale;
ValidateScale(&scale);
// Do nothing, if already enabled with same scale.
if (is_enabled_ && scale == scale_)
return;
is_enabled_ = enabled;
RedrawKeepingMousePosition(scale, true, false);
shell->accessibility_delegate()->SaveScreenMagnifierScale(scale);
} else {
// Do nothing, if already disabled.
if (!is_enabled_)
return;
if (input_method_)
input_method_->RemoveObserver(this);
input_method_ = nullptr;
RedrawKeepingMousePosition(kNonMagnifiedScale, true, false);
is_enabled_ = enabled;
}
// Keyboard overscroll creates layout issues with fullscreen magnification
// so it needs to be disabled when magnification is enabled.
// TODO(spqchan): Fix the keyboard overscroll issues.
auto config = keyboard::KeyboardController::Get()->keyboard_config();
config.overscroll_behavior =
is_enabled_ ? keyboard::mojom::KeyboardOverscrollBehavior::kDisabled
: keyboard::mojom::KeyboardOverscrollBehavior::kDefault;
keyboard::KeyboardController::Get()->UpdateKeyboardConfig(config);
}
bool MagnificationController::IsEnabled() const {
return is_enabled_;
}
void MagnificationController::SetKeepFocusCentered(bool keep_focus_centered) {
keep_focus_centered_ = keep_focus_centered;
}
bool MagnificationController::KeepFocusCentered() const {
return keep_focus_centered_;
}
void MagnificationController::SetScale(float scale, bool animate) {
if (!is_enabled_)
return;
ValidateScale(&scale);
Shell::Get()->accessibility_delegate()->SaveScreenMagnifierScale(scale);
RedrawKeepingMousePosition(scale, animate, false);
}
void MagnificationController::StepToNextScaleValue(int delta_index) {
SetScale(magnifier_utils::GetNextMagnifierScaleValue(
delta_index, GetScale(), kNonMagnifiedScale, kMaxMagnifiedScale),
true /* animate */);
}
void MagnificationController::MoveWindow(int x, int y, bool animate) {
if (!is_enabled_)
return;
Redraw(gfx::PointF(x, y), scale_, animate);
}
void MagnificationController::MoveWindow(const gfx::Point& point,
bool animate) {
if (!is_enabled_)
return;
Redraw(gfx::PointF(point), scale_, animate);
}
gfx::Point MagnificationController::GetWindowPosition() const {
return gfx::ToFlooredPoint(origin_);
}
void MagnificationController::SetScrollDirection(ScrollDirection direction) {
scroll_direction_ = direction;
StartOrStopScrollIfNecessary();
}
gfx::Rect MagnificationController::GetViewportRect() const {
return gfx::ToEnclosingRect(GetWindowRectDIP(scale_));
}
void MagnificationController::CenterOnPoint(const gfx::Point& point_in_screen) {
gfx::Point point_in_root = point_in_screen;
::wm::ConvertPointFromScreen(root_window_, &point_in_root);
MoveMagnifierWindowCenterPoint(point_in_root);
}
void MagnificationController::HandleFocusedNodeChanged(
bool is_editable_node,
const gfx::Rect& node_bounds_in_screen) {
// The editable node is handled by OnCaretBoundsChanged.
if (is_editable_node)
return;
// Nothing to recenter on.
if (node_bounds_in_screen.IsEmpty())
return;
gfx::Rect node_bounds_in_root = node_bounds_in_screen;
::wm::ConvertRectFromScreen(root_window_, &node_bounds_in_root);
if (GetViewportRect().Contains(node_bounds_in_root))
return;
MoveMagnifierWindowFollowRect(node_bounds_in_root);
}
void MagnificationController::SwitchTargetRootWindow(
aura::Window* new_root_window,
bool redraw_original_root_window) {
DCHECK(new_root_window);
if (new_root_window == root_window_)
return;
// Stores the previous scale.
float scale = GetScale();
// Unmagnify the previous root window.
root_window_->RemoveObserver(this);
// TODO: This may need to remove the IME observer from the old root window
// and add it to the new root window. https://crbug.com/820464
// Do not move mouse back to its original position (point at border of the
// root window) after redrawing as doing so will trigger root window switch
// again.
if (redraw_original_root_window)
RedrawKeepingMousePosition(1.0f, true, true);
root_window_ = new_root_window;
RedrawKeepingMousePosition(scale, true, true);
root_window_->AddObserver(this);
}
gfx::Transform MagnificationController::GetMagnifierTransform() const {
gfx::Transform transform;
if (IsEnabled()) {
transform.Scale(scale_, scale_);
gfx::Point offset = GetWindowPosition();
transform.Translate(-offset.x(), -offset.y());
}
return transform;
}
void MagnificationController::OnInputContextHandlerChanged() {
if (!is_enabled_)
return;
auto* new_input_method = magnifier_utils::GetInputMethod(root_window_);
if (new_input_method == input_method_)
return;
if (input_method_)
input_method_->RemoveObserver(this);
input_method_ = new_input_method;
if (input_method_)
input_method_->AddObserver(this);
}
void MagnificationController::OnCaretBoundsChanged(
const ui::TextInputClient* client) {
// caret bounds in screen coordinates.
const gfx::Rect caret_bounds = client->GetCaretBounds();
// Note: OnCaretBoundsChanged could be fired OnTextInputTypeChanged during
// which the caret position is not set a meaning position, and we do not
// need to adjust the view port position based on the bogus caret position.
// This is only a transition period, the caret position will be fixed upon
// focusing right after.
if (caret_bounds.width() == 0 && caret_bounds.height() == 0)
return;
gfx::Point new_caret_point = caret_bounds.CenterPoint();
// |caret_point_| in |root_window_| coordinates.
::wm::ConvertPointFromScreen(root_window_, &new_caret_point);
// When the caret point was not actually changed, nothing should happen.
// OnCaretBoundsChanged could be fired on every event that may change the
// caret bounds, in particular a window creation/movement, that may not result
// in an actual movement.
if (new_caret_point == caret_point_)
return;
caret_point_ = new_caret_point;
// If the feature for centering the text input focus is disabled, the
// magnifier window will be moved to follow the focus with a panning margin.
if (!KeepFocusCentered()) {
// Visible window_rect in |root_window_| coordinates.
const gfx::Rect visible_window_rect = GetViewportRect();
const int panning_margin = kCaretPanningMargin / scale_;
MoveMagnifierWindowFollowPoint(caret_point_, panning_margin, panning_margin,
visible_window_rect.width() / 2,
visible_window_rect.height() / 2,
false /* reduce_bottom_margin */);
return;
}
// Move the magnifier window to center the focus with a little delay.
// In Gmail compose window, when user types a blank space, it will insert
// a non-breaking space(NBSP). NBSP will be replaced with a blank space
// character when user types a non-blank space character later, which causes
// OnCaretBoundsChanged be called twice. The first call moves the caret back
// to the character position just before NBSP, replaces the NBSP with blank
// space plus the new character, then the second call will move caret to the
// position after the new character. In order to avoid the magnifier window
// being moved back and forth with these two OnCaretBoundsChanged events, we
// defer moving magnifier window until the |move_magnifier_timer_| fires,
// when the caret settles eventually.
move_magnifier_timer_.Stop();
move_magnifier_timer_.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(
disable_move_magnifier_delay_ ? 0 : kMoveMagnifierDelayInMs),
this, &MagnificationController::OnMoveMagnifierTimer);
}
void MagnificationController::OnInputMethodDestroyed(
const ui::InputMethod* input_method) {
DCHECK_EQ(input_method, input_method_);
input_method_->RemoveObserver(this);
input_method_ = nullptr;
}
void MagnificationController::OnImplicitAnimationsCompleted() {
if (!is_on_animation_)
return;
if (move_cursor_after_animation_) {
MoveCursorTo(root_window_->GetHost(), position_after_animation_);
move_cursor_after_animation_ = false;
aura::client::CursorClient* cursor_client =
aura::client::GetCursorClient(root_window_);
if (cursor_client)
cursor_client->EnableMouseEvents();
}
is_on_animation_ = false;
StartOrStopScrollIfNecessary();
}
void MagnificationController::OnWindowDestroying(aura::Window* root_window) {
if (root_window == root_window_) {
// There must be at least one root window because this controller is
// destroyed before the root windows get destroyed.
DCHECK(root_window);
aura::Window* target_root_window = Shell::GetRootWindowForNewWindows();
CHECK(target_root_window);
// The destroyed root window must not be target.
CHECK_NE(target_root_window, root_window);
// Don't redraw the old root window as it's being destroyed.
SwitchTargetRootWindow(target_root_window, false);
point_of_interest_in_root_ = target_root_window->bounds().CenterPoint();
}
}
void MagnificationController::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
// TODO(yoshiki): implement here. crbug.com/230979
}
void MagnificationController::OnMouseEvent(ui::MouseEvent* event) {
aura::Window* target = static_cast<aura::Window*>(event->target());
aura::Window* current_root = target->GetRootWindow();
gfx::Rect root_bounds = current_root->bounds();
if (root_bounds.Contains(event->root_location())) {
// This must be before |SwitchTargetRootWindow()|.
if (event->type() != ui::ET_MOUSE_CAPTURE_CHANGED)
point_of_interest_in_root_ = event->root_location();
if (current_root != root_window_) {
DCHECK(current_root);
SwitchTargetRootWindow(current_root, true);
}
if (IsMagnified() && event->type() == ui::ET_MOUSE_MOVED &&
event->pointer_details().pointer_type !=
ui::EventPointerType::POINTER_TYPE_PEN) {
OnMouseMove(event->root_location());
}
}
}
void MagnificationController::OnScrollEvent(ui::ScrollEvent* event) {
if (event->IsAltDown() && event->IsControlDown()) {
if (event->type() == ui::ET_SCROLL_FLING_START) {
event->StopPropagation();
return;
} else if (event->type() == ui::ET_SCROLL_FLING_CANCEL) {
float scale = GetScale();
// Jump back to exactly 1.0 if we are just a tiny bit zoomed in.
// TODO(katie): These events are not fired after every scroll, which means
// we don't always jump back to 1.0. Look into why they are missing.
if (scale < kMinMagnifiedScaleThreshold) {
scale = kNonMagnifiedScale;
SetScale(scale, true);
}
event->StopPropagation();
return;
}
if (event->type() == ui::ET_SCROLL) {
SetScale(magnifier_utils::GetScaleFromScroll(
event->y_offset() * kScrollScaleChangeFactor, GetScale(),
kMaxMagnifiedScale, kNonMagnifiedScale),
false /* animate */);
event->StopPropagation();
return;
}
}
}
void MagnificationController::OnTouchEvent(ui::TouchEvent* event) {
aura::Window* target = static_cast<aura::Window*>(event->target());
aura::Window* current_root = target->GetRootWindow();
gfx::Rect root_bounds = current_root->bounds();
if (!root_bounds.Contains(event->root_location()))
return;
point_of_interest_in_root_ = event->root_location();
if (current_root != root_window_)
SwitchTargetRootWindow(current_root, true);
}
ui::EventDispatchDetails MagnificationController::RewriteEvent(
const ui::Event& event,
const Continuation continuation) {
if (!IsEnabled())
return SendEvent(continuation, &event);
if (!event.IsTouchEvent())
return SendEvent(continuation, &event);
const ui::TouchEvent* touch_event = event.AsTouchEvent();
if (touch_event->type() == ui::ET_TOUCH_PRESSED) {
touch_points_++;
press_event_map_[touch_event->pointer_details().id] =
std::make_unique<ui::TouchEvent>(*touch_event);
} else if (touch_event->type() == ui::ET_TOUCH_RELEASED) {
touch_points_--;
press_event_map_.erase(touch_event->pointer_details().id);
}
ui::TouchEvent touch_event_copy = *touch_event;
if (gesture_provider_->OnTouchEvent(&touch_event_copy)) {
gesture_provider_->OnTouchEventAck(
touch_event_copy.unique_event_id(), false /* event_consumed */,
false /* is_source_touch_event_set_non_blocking */);
} else {
return DiscardEvent(continuation);
}
// User can change zoom level with two fingers pinch and pan around with two
// fingers scroll. Once MagnificationController detects one of those two
// gestures, it starts consuming all touch events with cancelling existing
// touches. If cancel_pressed_touches is set to true, ET_TOUCH_CANCELLED
// events are dispatched for existing touches after the next for-loop.
bool cancel_pressed_touches = ProcessGestures();
if (cancel_pressed_touches) {
DCHECK_EQ(2u, press_event_map_.size());
// MagnificationController starts consuming all touch events after it
// cancells existing touches.
consume_touch_event_ = true;
for (const auto& it : press_event_map_) {
ui::TouchEvent touch_cancel_event(ui::ET_TOUCH_CANCELLED, gfx::Point(),
touch_event->time_stamp(),
it.second->pointer_details());
touch_cancel_event.set_location_f(it.second->location_f());
touch_cancel_event.set_root_location_f(it.second->root_location_f());
touch_cancel_event.set_flags(it.second->flags());
// TouchExplorationController is watching event stream and managing its
// internal state. If an event rewriter (MagnificationController) rewrites
// event stream, the next event rewriter won't get the event, which makes
// TouchExplorationController confused. Send cancelled event for recorded
// touch events to the next event rewriter here instead of rewriting an
// event in the stream.
ui::EventDispatchDetails details =
SendEvent(continuation, &touch_cancel_event);
if (details.dispatcher_destroyed || details.target_destroyed)
return details;
}
press_event_map_.clear();
}
bool discard = consume_touch_event_;
// Reset state once no point is touched on the screen.
if (touch_points_ == 0) {
consume_touch_event_ = false;
// Jump back to exactly 1.0 if we are just a tiny bit zoomed in.
if (scale_ < kMinMagnifiedScaleThreshold) {
SetScale(kNonMagnifiedScale, true /* animate */);
} else {
// Store current magnifier scale in pref. We don't need to call this if we
// call SetScale (the above case) as SetScale does this.
Shell::Get()->accessibility_delegate()->SaveScreenMagnifierScale(scale_);
}
}
if (discard)
return DiscardEvent(continuation);
return SendEvent(continuation, &event);
}
bool MagnificationController::Redraw(const gfx::PointF& position,
float scale,
bool animate) {
const gfx::PointF position_in_dip =
ui::ConvertPointToDIP(root_window_->layer(), position);
return RedrawDIP(position_in_dip, scale,
animate ? kDefaultAnimationDurationInMs : 0,
kDefaultAnimationTweenType);
}
bool MagnificationController::RedrawDIP(const gfx::PointF& position_in_dip,
float scale,
int duration_in_ms,
gfx::Tween::Type tween_type) {
DCHECK(root_window_);
float x = position_in_dip.x();
float y = position_in_dip.y();
ValidateScale(&scale);
if (x < 0)
x = 0;
if (y < 0)
y = 0;
const gfx::Size host_size_in_dip = GetHostSizeDIP();
const gfx::SizeF window_size_in_dip = GetWindowRectDIP(scale).size();
float max_x = host_size_in_dip.width() - window_size_in_dip.width();
float max_y = host_size_in_dip.height() - window_size_in_dip.height();
if (x > max_x)
x = max_x;
if (y > max_y)
y = max_y;
// Does nothing if both the origin and the scale are not changed.
if (origin_.x() == x && origin_.y() == y && scale == scale_) {
return false;
}
origin_.set_x(x);
origin_.set_y(y);
scale_ = scale;
const ui::LayerAnimator::PreemptionStrategy strategy =
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET;
const base::TimeDelta duration =
base::TimeDelta::FromMilliseconds(duration_in_ms);
ui::ScopedLayerAnimationSettings root_layer_settings(
root_window_->layer()->GetAnimator());
root_layer_settings.AddObserver(this);
root_layer_settings.SetPreemptionStrategy(strategy);
root_layer_settings.SetTweenType(tween_type);
root_layer_settings.SetTransitionDuration(duration);
display::Display display =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_window_);
std::unique_ptr<RootWindowTransformer> transformer(
CreateRootWindowTransformerForDisplay(root_window_, display));
// Inverse the transformation on the keyboard container so the keyboard will
// remain zoomed out. Apply the same animation settings to it.
// Note: if |scale_| is 1.0f, the transform matrix will be an identity matrix.
// Applying the inverse of an identity matrix will not change the
// transformation.
// TODO(spqchan): Find a way to sync the layer animations together.
aura::Window* virtual_keyboard_container =
root_window_->GetChildById(kShellWindowId_ImeWindowParentContainer);
gfx::Transform vk_transform;
if (GetMagnifierTransform().GetInverse(&vk_transform)) {
ui::ScopedLayerAnimationSettings vk_layer_settings(
virtual_keyboard_container->layer()->GetAnimator());
vk_layer_settings.SetPreemptionStrategy(strategy);
vk_layer_settings.SetTweenType(tween_type);
vk_layer_settings.SetTransitionDuration(duration);
virtual_keyboard_container->SetTransform(vk_transform);
}
RootWindowController::ForWindow(root_window_)
->ash_host()
->SetRootWindowTransformer(std::move(transformer));
if (duration_in_ms > 0)
is_on_animation_ = true;
return true;
}
void MagnificationController::StartOrStopScrollIfNecessary() {
// This value controls the scrolling speed.
const int kMoveOffset = 40;
if (is_on_animation_) {
if (scroll_direction_ == SCROLL_NONE)
root_window_->layer()->GetAnimator()->StopAnimating();
return;
}
gfx::PointF new_origin = origin_;
switch (scroll_direction_) {
case SCROLL_NONE:
// No need to take action.
return;
case SCROLL_LEFT:
new_origin.Offset(-kMoveOffset, 0);
break;
case SCROLL_RIGHT:
new_origin.Offset(kMoveOffset, 0);
break;
case SCROLL_UP:
new_origin.Offset(0, -kMoveOffset);
break;
case SCROLL_DOWN:
new_origin.Offset(0, kMoveOffset);
break;
}
RedrawDIP(new_origin, scale_, kDefaultAnimationDurationInMs,
kDefaultAnimationTweenType);
}
void MagnificationController::RedrawKeepingMousePosition(
float scale,
bool animate,
bool ignore_mouse_change) {
gfx::Point mouse_in_root = point_of_interest_in_root_;
// mouse_in_root is invalid value when the cursor is hidden.
if (!root_window_->bounds().Contains(mouse_in_root))
mouse_in_root = root_window_->bounds().CenterPoint();
const gfx::PointF origin = gfx::PointF(
mouse_in_root.x() - (scale_ / scale) * (mouse_in_root.x() - origin_.x()),
mouse_in_root.y() - (scale_ / scale) * (mouse_in_root.y() - origin_.y()));
bool changed =
RedrawDIP(origin, scale, animate ? kDefaultAnimationDurationInMs : 0,
kDefaultAnimationTweenType);
if (!ignore_mouse_change && changed)
AfterAnimationMoveCursorTo(mouse_in_root);
}
void MagnificationController::OnMouseMove(const gfx::Point& location) {
DCHECK(root_window_);
gfx::Point mouse(location);
int margin = kCursorPanningMargin / scale_; // No need to consider DPI.
// Reduce the bottom margin if the keyboard is visible.
bool reduce_bottom_margin =
keyboard::KeyboardController::Get()->IsKeyboardVisible();
MoveMagnifierWindowFollowPoint(mouse, margin, margin, margin, margin,
reduce_bottom_margin);
}
void MagnificationController::AfterAnimationMoveCursorTo(
const gfx::Point& location) {
DCHECK(root_window_);
aura::client::CursorClient* cursor_client =
aura::client::GetCursorClient(root_window_);
if (cursor_client) {
// When cursor is invisible, do not move or show the cursor after the
// animation.
if (!cursor_client->IsCursorVisible())
return;
cursor_client->DisableMouseEvents();
}
move_cursor_after_animation_ = true;
position_after_animation_ = location;
}
bool MagnificationController::IsMagnified() const {
return scale_ >= kMinMagnifiedScaleThreshold;
}
gfx::RectF MagnificationController::GetWindowRectDIP(float scale) const {
const gfx::Size size_in_dip = root_window_->bounds().size();
const float width = size_in_dip.width() / scale;
const float height = size_in_dip.height() / scale;
return gfx::RectF(origin_.x(), origin_.y(), width, height);
}
gfx::Size MagnificationController::GetHostSizeDIP() const {
return root_window_->bounds().size();
}
void MagnificationController::ValidateScale(float* scale) {
*scale = base::ClampToRange(*scale, kNonMagnifiedScale, kMaxMagnifiedScale);
DCHECK(kNonMagnifiedScale <= *scale && *scale <= kMaxMagnifiedScale);
}
bool MagnificationController::ProcessGestures() {
bool cancel_pressed_touches = false;
std::vector<std::unique_ptr<ui::GestureEvent>> gestures =
gesture_provider_->GetAndResetPendingGestures();
for (const auto& gesture : gestures) {
const ui::GestureEventDetails& details = gesture->details();
if (details.touch_points() != 2)
continue;
if (gesture->type() == ui::ET_GESTURE_PINCH_BEGIN) {
original_scale_ = scale_;
// Start consuming touch events with cancelling existing touches.
if (!consume_touch_event_)
cancel_pressed_touches = true;
} else if (gesture->type() == ui::ET_GESTURE_PINCH_UPDATE) {
float scale = GetScale() * details.scale();
ValidateScale(&scale);
// |details.bounding_box().CenterPoint()| return center of touch points
// of gesture in non-dip screen coordinate.
gfx::PointF gesture_center =
gfx::PointF(details.bounding_box().CenterPoint());
// Root transform does dip scaling, screen magnification scaling and
// translation. Apply inverse transform to convert non-dip screen
// coordinate to dip logical coordinate.
root_window_->GetHost()->GetInverseRootTransform().TransformPoint(
&gesture_center);
// Calcualte new origin to keep the distance between |gesture_center|
// and |origin| same in screen coordinate. This means the following
// equation.
// (gesture_center.x - origin_.x) * scale_ =
// (gesture_center.x - new_origin.x) * scale
// If you solve it for |new_origin|, you will get the following formula.
const gfx::PointF origin = gfx::PointF(
gesture_center.x() -
(scale_ / scale) * (gesture_center.x() - origin_.x()),
gesture_center.y() -
(scale_ / scale) * (gesture_center.y() - origin_.y()));
RedrawDIP(origin, scale, 0, kDefaultAnimationTweenType);
} else if (gesture->type() == ui::ET_GESTURE_SCROLL_BEGIN) {
original_origin_ = origin_;
// Start consuming all touch events with cancelling existing touches.
if (!consume_touch_event_)
cancel_pressed_touches = true;
} else if (gesture->type() == ui::ET_GESTURE_SCROLL_UPDATE) {
// Divide by scale to keep scroll speed same at any scale.
float new_x = origin_.x() + (-1.0f * details.scroll_x() / scale_);
float new_y = origin_.y() + (-1.0f * details.scroll_y() / scale_);
RedrawDIP(gfx::PointF(new_x, new_y), scale_, 0,
kDefaultAnimationTweenType);
}
}
return cancel_pressed_touches;
}
void MagnificationController::MoveMagnifierWindowFollowPoint(
const gfx::Point& point,
int x_panning_margin,
int y_panning_margin,
int x_target_margin,
int y_target_margin,
bool reduce_bottom_margin) {
DCHECK(root_window_);
bool start_zoom = false;
const gfx::Rect window_rect = GetViewportRect();
const int left = window_rect.x();
const int right = window_rect.right();
int x_diff = 0;
if (point.x() < left + x_panning_margin) {
// Panning left.
x_diff = point.x() - (left + x_target_margin);
start_zoom = true;
} else if (right - x_panning_margin < point.x()) {
// Panning right.
x_diff = point.x() - (right - x_target_margin);
start_zoom = true;
}
int x = left + x_diff;
const int top = window_rect.y();
const int bottom = window_rect.bottom();
// If |reduce_bottom_margin| is true, use kKeyboardBottomPanningMargin instead
// of |y_panning_margin|. This is to prevent the magnifier from panning when
// the user is trying to interact with the bottom of the keyboard.
const int bottom_panning_margin = reduce_bottom_margin
? kKeyboardBottomPanningMargin / scale_
: y_panning_margin;
int y_diff = 0;
if (point.y() < top + y_panning_margin) {
// Panning up.
y_diff = point.y() - (top + y_target_margin);
start_zoom = true;
} else if (bottom - bottom_panning_margin < point.y()) {
// Panning down.
const int bottom_target_margin =
reduce_bottom_margin ? std::min(bottom_panning_margin, y_target_margin)
: y_target_margin;
y_diff = point.y() - (bottom - bottom_target_margin);
start_zoom = true;
}
int y = top + y_diff;
if (start_zoom && !is_on_animation_) {
bool ret = RedrawDIP(gfx::PointF(x, y), scale_,
0, // No animation on panning.
kDefaultAnimationTweenType);
if (ret) {
// If the magnified region is moved, hides the mouse cursor and moves it.
if (x_diff != 0 || y_diff != 0)
MoveCursorTo(root_window_->GetHost(), point);
}
}
}
void MagnificationController::MoveMagnifierWindowCenterPoint(
const gfx::Point& point) {
DCHECK(root_window_);
gfx::Rect window_rect = GetViewportRect();
// Reduce the viewport bounds if the keyboard is up.
if (keyboard::KeyboardController::Get()->IsEnabled()) {
gfx::Rect keyboard_rect = keyboard::KeyboardController::Get()
->GetKeyboardWindow()
->GetBoundsInScreen();
window_rect.set_height(window_rect.height() -
keyboard_rect.height() / scale_);
}
if (point == window_rect.CenterPoint())
return;
if (!is_on_animation_) {
// With animation on panning.
RedrawDIP(
gfx::PointF(window_rect.origin() + (point - window_rect.CenterPoint())),
scale_, kDefaultAnimationDurationInMs, kCenterCaretAnimationTweenType);
}
}
void MagnificationController::MoveMagnifierWindowFollowRect(
const gfx::Rect& rect) {
DCHECK(root_window_);
bool should_pan = false;
const gfx::Rect viewport_rect = GetViewportRect();
const int left = viewport_rect.x();
const int right = viewport_rect.right();
const gfx::Point rect_center = rect.CenterPoint();
int x = left;
if (rect.x() < left || right < rect.right()) {
// Panning horizontally.
x = rect_center.x() - viewport_rect.width() / 2;
should_pan = true;
}
const int top = viewport_rect.y();
const int bottom = viewport_rect.bottom();
int y = top;
if (rect.y() < top || bottom < rect.bottom()) {
// Panning vertically.
y = rect_center.y() - viewport_rect.height() / 2;
should_pan = true;
}
if (should_pan) {
if (is_on_animation_) {
root_window_->layer()->GetAnimator()->StopAnimating();
is_on_animation_ = false;
}
RedrawDIP(gfx::PointF(x, y), scale_,
0, // No animation on panning.
kDefaultAnimationTweenType);
}
}
void MagnificationController::OnMoveMagnifierTimer() {
MoveMagnifierWindowCenterPoint(caret_point_);
}
} // namespace ash