1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ash/magnifier/docked_magnifier_controller_impl.h"
6 
7 #include <algorithm>
8 #include <utility>
9 
10 #include "ash/accessibility/accessibility_controller_impl.h"
11 #include "ash/host/ash_window_tree_host.h"
12 #include "ash/magnifier/magnifier_utils.h"
13 #include "ash/public/cpp/ash_pref_names.h"
14 #include "ash/public/cpp/shell_window_ids.h"
15 #include "ash/root_window_controller.h"
16 #include "ash/session/session_controller_impl.h"
17 #include "ash/shell.h"
18 #include "ash/wm/overview/overview_controller.h"
19 #include "ash/wm/splitview/split_view_controller.h"
20 #include "ash/wm/work_area_insets.h"
21 #include "base/bind.h"
22 #include "base/numerics/ranges.h"
23 #include "components/prefs/pref_change_registrar.h"
24 #include "components/prefs/pref_registry_simple.h"
25 #include "components/prefs/pref_service.h"
26 #include "ui/aura/client/drag_drop_client.h"
27 #include "ui/aura/window_tree_host.h"
28 #include "ui/base/ime/chromeos/ime_bridge.h"
29 #include "ui/base/ime/input_method.h"
30 #include "ui/base/ime/text_input_client.h"
31 #include "ui/compositor/layer.h"
32 #include "ui/compositor/scoped_layer_animation_settings.h"
33 #include "ui/display/screen.h"
34 #include "ui/views/widget/widget.h"
35 #include "ui/wm/core/coordinate_conversion.h"
36 
37 namespace ash {
38 
39 namespace {
40 
41 constexpr float kDefaultMagnifierScale = 4.0f;
42 constexpr float kMinMagnifierScale = 1.0f;
43 constexpr float kMaxMagnifierScale = 20.0f;
44 
45 // The factor by which the offset of scroll events are scaled.
46 constexpr float kScrollScaleFactor = 0.0125f;
47 
48 constexpr char kDockedMagnifierViewportWindowName[] =
49     "DockedMagnifierViewportWindow";
50 
51 // The duration of time to wait before moving the viewport to the caret.
52 // Allows magnifier.js to preempt the viewport moving to the caret with
53 // moveMagnifierToRect.
54 // TODO(accessibility): Remove once caret updates handled in magnifier.js.
55 constexpr base::TimeDelta kMoveMagnifierCaretDelay =
56     base::TimeDelta::FromMilliseconds(5);
57 
58 // Returns the current cursor location in screen coordinates.
GetCursorScreenPoint()59 inline gfx::Point GetCursorScreenPoint() {
60   return display::Screen::GetScreen()->GetCursorScreenPoint();
61 }
62 
63 // Updates the workarea of the display associated with |window| such that the
64 // given magnifier viewport |height| is allocated at the top of the screen.
SetViewportHeightInWorkArea(aura::Window * window,int height)65 void SetViewportHeightInWorkArea(aura::Window* window, int height) {
66   DCHECK(window);
67   WorkAreaInsets::ForWindow(window->GetRootWindow())
68       ->SetDockedMagnifierHeight(height);
69 }
70 
71 // Gets the bounds of the Docked Magnifier viewport widget when placed in the
72 // display whose root window is |root|. The bounds returned correspond to the
73 // top quarter portion of the screen.
GetViewportWidgetBoundsInRoot(aura::Window * root)74 gfx::Rect GetViewportWidgetBoundsInRoot(aura::Window* root) {
75   DCHECK(root);
76   DCHECK(root->IsRootWindow());
77 
78   auto root_bounds = root->GetBoundsInRootWindow();
79   root_bounds.set_height(root_bounds.height() /
80                          DockedMagnifierControllerImpl::kScreenHeightDivisor);
81   return root_bounds;
82 }
83 
84 // Returns the separator layer bounds from the given |viewport_bounds|. The
85 // separator layer is to be placed right below the viewport.
SeparatorBoundsFromViewportBounds(const gfx::Rect & viewport_bounds)86 inline gfx::Rect SeparatorBoundsFromViewportBounds(
87     const gfx::Rect& viewport_bounds) {
88   return gfx::Rect(viewport_bounds.x(), viewport_bounds.bottom(),
89                    viewport_bounds.width(),
90                    DockedMagnifierControllerImpl::kSeparatorHeight);
91 }
92 
93 // Returns the child container in |root| that should be used as the parent of
94 // viewport widget and the separator layer.
GetViewportParentContainerForRoot(aura::Window * root)95 aura::Window* GetViewportParentContainerForRoot(aura::Window* root) {
96   return root->GetChildById(kShellWindowId_DockedMagnifierContainer);
97 }
98 
99 }  // namespace
100 
101 // static
Get()102 DockedMagnifierController* DockedMagnifierController::Get() {
103   return Shell::Get()->docked_magnifier_controller();
104 }
105 
DockedMagnifierControllerImpl()106 DockedMagnifierControllerImpl::DockedMagnifierControllerImpl() {
107   Shell::Get()->session_controller()->AddObserver(this);
108   if (ui::IMEBridge::Get())
109     ui::IMEBridge::Get()->AddObserver(this);
110 }
111 
~DockedMagnifierControllerImpl()112 DockedMagnifierControllerImpl::~DockedMagnifierControllerImpl() {
113   if (input_method_)
114     input_method_->RemoveObserver(this);
115   input_method_ = nullptr;
116   if (ui::IMEBridge::Get())
117     ui::IMEBridge::Get()->RemoveObserver(this);
118 
119   Shell* shell = Shell::Get();
120   shell->session_controller()->RemoveObserver(this);
121 
122   if (GetEnabled()) {
123     shell->window_tree_host_manager()->RemoveObserver(this);
124     shell->RemovePreTargetHandler(this);
125   }
126   CHECK(!views::WidgetObserver::IsInObserverList());
127 }
128 
129 // static
RegisterProfilePrefs(PrefRegistrySimple * registry)130 void DockedMagnifierControllerImpl::RegisterProfilePrefs(
131     PrefRegistrySimple* registry) {
132   registry->RegisterBooleanPref(prefs::kDockedMagnifierEnabled, false);
133   registry->RegisterDoublePref(prefs::kDockedMagnifierScale,
134                                kDefaultMagnifierScale);
135 }
136 
GetEnabled() const137 bool DockedMagnifierControllerImpl::GetEnabled() const {
138   return active_user_pref_service_ &&
139          active_user_pref_service_->GetBoolean(prefs::kDockedMagnifierEnabled);
140 }
141 
GetScale() const142 float DockedMagnifierControllerImpl::GetScale() const {
143   if (active_user_pref_service_)
144     return active_user_pref_service_->GetDouble(prefs::kDockedMagnifierScale);
145 
146   return kDefaultMagnifierScale;
147 }
148 
SetEnabled(bool enabled)149 void DockedMagnifierControllerImpl::SetEnabled(bool enabled) {
150   if (active_user_pref_service_) {
151     active_user_pref_service_->SetBoolean(prefs::kDockedMagnifierEnabled,
152                                           enabled);
153   }
154 }
155 
SetScale(float scale)156 void DockedMagnifierControllerImpl::SetScale(float scale) {
157   if (active_user_pref_service_) {
158     active_user_pref_service_->SetDouble(
159         prefs::kDockedMagnifierScale,
160         base::ClampToRange(scale, kMinMagnifierScale, kMaxMagnifierScale));
161   }
162 }
163 
StepToNextScaleValue(int delta_index)164 void DockedMagnifierControllerImpl::StepToNextScaleValue(int delta_index) {
165   SetScale(magnifier_utils::GetNextMagnifierScaleValue(
166       delta_index, GetScale(), kMinMagnifierScale, kMaxMagnifierScale));
167 }
168 
MoveMagnifierToRect(const gfx::Rect & rect_in_screen)169 void DockedMagnifierControllerImpl::MoveMagnifierToRect(
170     const gfx::Rect& rect_in_screen) {
171   DCHECK(GetEnabled());
172   last_move_magnifier_to_rect_ = base::TimeTicks::Now();
173 
174   gfx::Point point_in_screen = rect_in_screen.CenterPoint();
175 
176   // If rect is too wide to fit in viewport, include as much as we can, starting
177   // with the left edge.
178   const int scaled_viewport_width =
179       current_source_root_window_->bounds().width() / GetScale();
180   if (rect_in_screen.width() > scaled_viewport_width) {
181     point_in_screen.set_x(std::max(rect_in_screen.x() +
182                                        scaled_viewport_width / 2 -
183                                        magnifier_utils::kLeftEdgeContextPadding,
184                                    0));
185   }
186 
187   CenterOnPoint(point_in_screen);
188 }
189 
CenterOnPoint(const gfx::Point & point_in_screen)190 void DockedMagnifierControllerImpl::CenterOnPoint(
191     const gfx::Point& point_in_screen) {
192   if (!GetEnabled())
193     return;
194 
195   auto* screen = display::Screen::GetScreen();
196   auto* window = screen->GetWindowAtScreenPoint(point_in_screen);
197   if (!window) {
198     // In tests and sometimes initially on signin screen, |point_in_screen|
199     // maybe invalid and doesn't belong to any existing root window. However, we
200     // are here because the Docked Magnifier is enabled. We need to create the
201     // viewport widget somewhere, so we'll use the primary root window until we
202     // get a valid cursor event.
203     window = Shell::GetPrimaryRootWindow();
204   }
205 
206   auto* root_window = window->GetRootWindow();
207   DCHECK(root_window);
208   SwitchCurrentSourceRootWindowIfNeeded(root_window,
209                                         true /* update_old_root_workarea */);
210 
211   auto* host = root_window->GetHost();
212   DCHECK(host);
213 
214   MaybeCachePointOfInterestMinimumHeight(host);
215 
216   gfx::Point point_of_interest(point_in_screen);
217   ::wm::ConvertPointFromScreen(root_window, &point_of_interest);
218 
219   // The point of interest in pixels.
220   gfx::PointF point_in_pixels(point_of_interest);
221 
222   // Before transforming to pixels, make sure its y-coordinate doesn't go below
223   // the minimum height. Do it here for this PointF since the
224   // |minimum_point_of_interest_height_| is a float, in order to avoid rounding
225   // in the transform to be able to reliably verify it in tests.
226   if (point_in_pixels.y() < minimum_point_of_interest_height_)
227     point_in_pixels.set_y(minimum_point_of_interest_height_);
228 
229   // The pixel space is the magnified space.
230   const float scale = GetScale();
231   point_in_pixels.Scale(scale);
232 
233   // Transform steps: (Note that the transform is applied in the opposite
234   // order)
235   // 1- Scale the layer by |scale|.
236   // 2- Translate the point of interest to the center point of the viewport
237   //    widget.
238   const gfx::Point viewport_center_point =
239       GetViewportWidgetBoundsInRoot(current_source_root_window_).CenterPoint();
240   gfx::Transform transform;
241   transform.Translate(viewport_center_point.x() - point_in_pixels.x(),
242                       viewport_center_point.y() - point_in_pixels.y());
243   transform.Scale(scale, scale);
244 
245   // When updating the transform, we don't want any animation, otherwise the
246   // movement of the mouse won't be very smooth. We want the magnifier layer to
247   // update immediately with the movement of the mouse (or the change in the
248   // point of interest due to input caret bounds changes ... etc.).
249   ui::ScopedLayerAnimationSettings settings(
250       viewport_magnifier_layer_->GetAnimator());
251   settings.SetTransitionDuration(base::TimeDelta::FromMilliseconds(0));
252   settings.SetTweenType(gfx::Tween::ZERO);
253   settings.SetPreemptionStrategy(ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
254   viewport_magnifier_layer_->SetTransform(transform);
255 }
256 
GetMagnifierHeightForTesting() const257 int DockedMagnifierControllerImpl::GetMagnifierHeightForTesting() const {
258   return GetTotalMagnifierHeight();
259 }
260 
OnActiveUserPrefServiceChanged(PrefService * pref_service)261 void DockedMagnifierControllerImpl::OnActiveUserPrefServiceChanged(
262     PrefService* pref_service) {
263   active_user_pref_service_ = pref_service;
264   InitFromUserPrefs();
265 }
266 
OnSigninScreenPrefServiceInitialized(PrefService * prefs)267 void DockedMagnifierControllerImpl::OnSigninScreenPrefServiceInitialized(
268     PrefService* prefs) {
269   OnActiveUserPrefServiceChanged(prefs);
270 }
271 
OnMouseEvent(ui::MouseEvent * event)272 void DockedMagnifierControllerImpl::OnMouseEvent(ui::MouseEvent* event) {
273   DCHECK(GetEnabled());
274   CenterOnPoint(GetCursorScreenPoint());
275 }
276 
OnScrollEvent(ui::ScrollEvent * event)277 void DockedMagnifierControllerImpl::OnScrollEvent(ui::ScrollEvent* event) {
278   DCHECK(GetEnabled());
279   if (!event->IsAltDown() || !event->IsControlDown())
280     return;
281 
282   if (event->type() == ui::ET_SCROLL_FLING_START ||
283       event->type() == ui::ET_SCROLL_FLING_CANCEL) {
284     event->StopPropagation();
285     return;
286   }
287 
288   if (event->type() == ui::ET_SCROLL) {
289     // Notes: - Clamping of the new scale value happens inside SetScale().
290     //        - Refreshing the viewport happens in the handler of the scale pref
291     //          changes.
292     SetScale(magnifier_utils::GetScaleFromScroll(
293         event->y_offset() * kScrollScaleFactor, GetScale(), kMaxMagnifierScale,
294         kMinMagnifierScale));
295     event->StopPropagation();
296   }
297 }
298 
OnTouchEvent(ui::TouchEvent * event)299 void DockedMagnifierControllerImpl::OnTouchEvent(ui::TouchEvent* event) {
300   DCHECK(GetEnabled());
301 
302   aura::Window* target = static_cast<aura::Window*>(event->target());
303   aura::Window* event_root = target->GetRootWindow();
304   gfx::Point event_screen_point = event->root_location();
305   ::wm::ConvertPointToScreen(event_root, &event_screen_point);
306   CenterOnPoint(event_screen_point);
307 }
308 
OnInputContextHandlerChanged()309 void DockedMagnifierControllerImpl::OnInputContextHandlerChanged() {
310   if (!GetEnabled())
311     return;
312 
313   auto* new_input_method =
314       magnifier_utils::GetInputMethod(current_source_root_window_);
315   if (new_input_method == input_method_)
316     return;
317 
318   if (input_method_)
319     input_method_->RemoveObserver(this);
320   input_method_ = new_input_method;
321   if (input_method_)
322     input_method_->AddObserver(this);
323 }
324 
OnInputMethodDestroyed(const ui::InputMethod * input_method)325 void DockedMagnifierControllerImpl::OnInputMethodDestroyed(
326     const ui::InputMethod* input_method) {
327   DCHECK_EQ(input_method, input_method_);
328   input_method_->RemoveObserver(this);
329   input_method_ = nullptr;
330 }
331 
OnCaretBoundsChanged(const ui::TextInputClient * client)332 void DockedMagnifierControllerImpl::OnCaretBoundsChanged(
333     const ui::TextInputClient* client) {
334   if (!GetEnabled()) {
335     // There is a small window between the time the "enabled" pref is updated to
336     // false, and the time we're notified with this change, upon which we remove
337     // the magnifier's viewport widget and stop observing the input method.
338     // During this short interval, if focus is in an editable element, the input
339     // method can notify us here. In this case, we should just return.
340     return;
341   }
342 
343   aura::client::DragDropClient* drag_drop_client =
344       aura::client::GetDragDropClient(current_source_root_window_);
345   if (drag_drop_client && drag_drop_client->IsDragDropInProgress()) {
346     // Ignore caret bounds change events when they result from changes in window
347     // bounds due to dragging. This is to leave the viewport centered around the
348     // cursor.
349     return;
350   }
351 
352   const gfx::Rect caret_screen_bounds = client->GetCaretBounds();
353   // In many cases, espcially text input events coming from webpages, the caret
354   // screen width is 0. Hence we can't check IsEmpty() since it returns true if
355   // either of the width or height is 0. We want to abort if only both are 0s.
356   if (!caret_screen_bounds.width() && !caret_screen_bounds.height())
357     return;
358 
359   // Wait a few milliseconds to see if any move magnifier to rect activity is
360   // occurring.
361   // TODO(accessibility): Remove once we move caret following to magnifier.js.
362   last_caret_screen_point_ = caret_screen_bounds.CenterPoint();
363   move_magnifier_timer_.Start(
364       FROM_HERE, kMoveMagnifierCaretDelay, this,
365       &DockedMagnifierControllerImpl::OnMoveMagnifierTimer);
366 }
367 
OnWidgetDestroying(views::Widget * widget)368 void DockedMagnifierControllerImpl::OnWidgetDestroying(views::Widget* widget) {
369   DCHECK_EQ(widget, viewport_widget_);
370 
371   SwitchCurrentSourceRootWindowIfNeeded(nullptr,
372                                         false /* update_old_root_workarea */);
373 }
374 
OnDisplayConfigurationChanged()375 void DockedMagnifierControllerImpl::OnDisplayConfigurationChanged() {
376   DCHECK(GetEnabled());
377 
378   // The viewport might have been on a display that just got removed, and hence
379   // the viewport widget and its associated layers are already destroyed. In
380   // that case we also cleared the |current_source_root_window_|.
381   if (current_source_root_window_) {
382     // Resolution may have changed. Update all bounds.
383     const auto viewport_bounds =
384         GetViewportWidgetBoundsInRoot(current_source_root_window_);
385     viewport_widget_->SetBounds(viewport_bounds);
386     viewport_background_layer_->SetBounds(viewport_bounds);
387     separator_layer_->SetBounds(
388         SeparatorBoundsFromViewportBounds(viewport_bounds));
389     SetViewportHeightInWorkArea(current_source_root_window_,
390                                 GetTotalMagnifierHeight());
391 
392     // Resolution changes, screen rotation, etc. can reset the host to confine
393     // the mouse cursor inside the root window. We want to make sure the cursor
394     // is confined properly outside the viewport.
395     ConfineMouseCursorOutsideViewport();
396   }
397 
398   // A change in display configuration, such as resolution, rotation, ... etc.
399   // invalidates the currently cached minimum height of the point of interest.
400   is_minimum_point_of_interest_height_valid_ = false;
401 
402   // Update the viewport magnifier layer transform.
403   CenterOnPoint(GetCursorScreenPoint());
404 }
405 
GetFullscreenMagnifierEnabled() const406 bool DockedMagnifierControllerImpl::GetFullscreenMagnifierEnabled() const {
407   return active_user_pref_service_ &&
408          active_user_pref_service_->GetBoolean(
409              prefs::kAccessibilityScreenMagnifierEnabled);
410 }
411 
SetFullscreenMagnifierEnabled(bool enabled)412 void DockedMagnifierControllerImpl::SetFullscreenMagnifierEnabled(
413     bool enabled) {
414   if (active_user_pref_service_) {
415     active_user_pref_service_->SetBoolean(
416         prefs::kAccessibilityScreenMagnifierEnabled, enabled);
417   }
418 }
419 
GetTotalMagnifierHeight() const420 int DockedMagnifierControllerImpl::GetTotalMagnifierHeight() const {
421   if (separator_layer_)
422     return separator_layer_->bounds().bottom();
423 
424   return 0;
425 }
426 
427 const views::Widget*
GetViewportWidgetForTesting() const428 DockedMagnifierControllerImpl::GetViewportWidgetForTesting() const {
429   return viewport_widget_;
430 }
431 
432 const ui::Layer*
GetViewportMagnifierLayerForTesting() const433 DockedMagnifierControllerImpl::GetViewportMagnifierLayerForTesting() const {
434   return viewport_magnifier_layer_.get();
435 }
436 
GetMinimumPointOfInterestHeightForTesting() const437 float DockedMagnifierControllerImpl::GetMinimumPointOfInterestHeightForTesting()
438     const {
439   return minimum_point_of_interest_height_;
440 }
441 
GetLastCaretScreenPointForTesting() const442 gfx::Point DockedMagnifierControllerImpl::GetLastCaretScreenPointForTesting()
443     const {
444   return last_caret_screen_point_;
445 }
446 
SwitchCurrentSourceRootWindowIfNeeded(aura::Window * new_root_window,bool update_old_root_workarea)447 void DockedMagnifierControllerImpl::SwitchCurrentSourceRootWindowIfNeeded(
448     aura::Window* new_root_window,
449     bool update_old_root_workarea) {
450   if (current_source_root_window_ == new_root_window)
451     return;
452 
453   aura::Window* old_root_window = current_source_root_window_;
454   current_source_root_window_ = new_root_window;
455 
456   // Current window changes means the minimum height of the point of interest is
457   // no longer valid.
458   is_minimum_point_of_interest_height_valid_ = false;
459 
460   if (old_root_window) {
461     // Order here matters. We should stop observing caret bounds changes before
462     // updating the work area bounds of the old root window. Otherwise, work
463     // area bounds changes will lead to caret bounds changes that recurses back
464     // here unnecessarily. https://crbug.com/1000903.
465     if (input_method_)
466       input_method_->RemoveObserver(this);
467     input_method_ = nullptr;
468 
469     if (update_old_root_workarea)
470       SetViewportHeightInWorkArea(old_root_window, 0);
471 
472     // Reset mouse cursor confinement to default.
473     RootWindowController::ForWindow(old_root_window)
474         ->ash_host()
475         ->ConfineCursorToRootWindow();
476   }
477 
478   separator_layer_ = nullptr;
479 
480   if (viewport_widget_) {
481     viewport_widget_->RemoveObserver(this);
482     viewport_widget_->Close();
483     viewport_widget_ = nullptr;
484   }
485 
486   viewport_background_layer_ = nullptr;
487   viewport_magnifier_layer_ = nullptr;
488 
489   if (!current_source_root_window_) {
490     // No need to create a new magnifier viewport.
491     return;
492   }
493 
494   CreateMagnifierViewport();
495 
496   input_method_ = magnifier_utils::GetInputMethod(current_source_root_window_);
497   if (input_method_)
498     input_method_->AddObserver(this);
499 
500   auto* magnified_container = current_source_root_window_->GetChildById(
501       kShellWindowId_MagnifiedContainer);
502   viewport_magnifier_layer_->SetShowReflectedLayerSubtree(
503       magnified_container->layer());
504 }
505 
InitFromUserPrefs()506 void DockedMagnifierControllerImpl::InitFromUserPrefs() {
507   DCHECK(active_user_pref_service_);
508 
509   pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
510   pref_change_registrar_->Init(active_user_pref_service_);
511   pref_change_registrar_->Add(
512       prefs::kDockedMagnifierEnabled,
513       base::BindRepeating(&DockedMagnifierControllerImpl::OnEnabledPrefChanged,
514                           base::Unretained(this)));
515   pref_change_registrar_->Add(
516       prefs::kDockedMagnifierScale,
517       base::BindRepeating(&DockedMagnifierControllerImpl::OnScalePrefChanged,
518                           base::Unretained(this)));
519   pref_change_registrar_->Add(
520       prefs::kAccessibilityScreenMagnifierEnabled,
521       base::BindRepeating(&DockedMagnifierControllerImpl::
522                               OnFullscreenMagnifierEnabledPrefChanged,
523                           base::Unretained(this)));
524 
525   OnEnabledPrefChanged();
526 }
527 
OnEnabledPrefChanged()528 void DockedMagnifierControllerImpl::OnEnabledPrefChanged() {
529   // When switching from the signin screen to a newly created profile while the
530   // Docked Magnifier is enabled, the prefs will be copied from the signin
531   // profile to the user profile, and the Docked Magnifier will remain enabled.
532   // We don't want to redo the below operations if the status doesn't change,
533   // for example readding the same observer to the WindowTreeHostManager will
534   // cause a crash on DCHECK on debug builds.
535   const bool current_enabled = !!current_source_root_window_;
536   const bool new_enabled = GetEnabled();
537   if (current_enabled == new_enabled)
538     return;
539 
540   // Toggling the status of the docked magnifier, changes the display's work
541   // area. However, display's work area changes are not allowed while overview
542   // mode is active (See https://crbug.com/834400). For this reason, we exit
543   // overview mode, before we actually update the state of docked magnifier
544   // below. https://crbug.com/894256.
545   Shell* shell = Shell::Get();
546   auto* overview_controller = shell->overview_controller();
547   if (overview_controller->InOverviewSession()) {
548     // |OverviewController::EndOverview| fails (returning false) in certain
549     // cases involving tablet split view mode. We can guarantee success by
550     // ensuring that tablet split view mode is not in session.
551     auto* split_view_controller =
552         SplitViewController::Get(Shell::GetPrimaryRootWindow());
553     if (split_view_controller->InTabletSplitViewMode()) {
554       split_view_controller->EndSplitView(
555           SplitViewController::EndReason::kNormal);
556     }
557     overview_controller->EndOverview();
558   }
559 
560   if (new_enabled) {
561     // Enabling the Docked Magnifier disables the Fullscreen Magnifier.
562     SetFullscreenMagnifierEnabled(false);
563     // Calling refresh will result in the creation of the magnifier viewport and
564     // its associated layers.
565     Refresh();
566     // Make sure we are in front of the fullscreen magnifier which also handles
567     // scroll events.
568     shell->AddPreTargetHandler(this, ui::EventTarget::Priority::kSystem);
569     shell->window_tree_host_manager()->AddObserver(this);
570   } else {
571     shell->window_tree_host_manager()->RemoveObserver(this);
572     shell->RemovePreTargetHandler(this);
573 
574     // Setting the current root window to |nullptr| will remove the viewport and
575     // all its associated layers.
576     SwitchCurrentSourceRootWindowIfNeeded(nullptr,
577                                           true /* update_old_root_workarea */);
578   }
579 
580   // Update the green checkmark status in the accessibility menu in the system
581   // tray.
582   shell->accessibility_controller()->NotifyAccessibilityStatusChanged();
583 
584   // We use software composited mouse cursor so that it can be mirrored into the
585   // magnifier viewport.
586   shell->UpdateCursorCompositingEnabled();
587 }
588 
OnScalePrefChanged()589 void DockedMagnifierControllerImpl::OnScalePrefChanged() {
590   if (GetEnabled()) {
591     // Invalidate the cached minimum height of the point of interest since the
592     // change in scale changes that height.
593     is_minimum_point_of_interest_height_valid_ = false;
594     Refresh();
595   }
596 }
597 
OnFullscreenMagnifierEnabledPrefChanged()598 void DockedMagnifierControllerImpl::OnFullscreenMagnifierEnabledPrefChanged() {
599   // Enabling the Fullscreen Magnifier disables the Docked Magnifier.
600   if (GetFullscreenMagnifierEnabled())
601     SetEnabled(false);
602 }
603 
Refresh()604 void DockedMagnifierControllerImpl::Refresh() {
605   DCHECK(GetEnabled());
606   CenterOnPoint(GetCursorScreenPoint());
607 }
608 
CreateMagnifierViewport()609 void DockedMagnifierControllerImpl::CreateMagnifierViewport() {
610   DCHECK(GetEnabled());
611   DCHECK(current_source_root_window_);
612 
613   const auto viewport_bounds =
614       GetViewportWidgetBoundsInRoot(current_source_root_window_);
615 
616   // 1- Create the viewport widget.
617   viewport_widget_ = new views::Widget;
618   views::Widget::InitParams params(
619       views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
620   params.activatable = views::Widget::InitParams::ACTIVATABLE_NO;
621   params.accept_events = false;
622   params.bounds = viewport_bounds;
623   params.opacity = views::Widget::InitParams::WindowOpacity::kOpaque;
624   params.parent =
625       GetViewportParentContainerForRoot(current_source_root_window_);
626   params.name = kDockedMagnifierViewportWindowName;
627   viewport_widget_->Init(std::move(params));
628 
629   // 2- Create the separator layer right below the viwport widget, parented to
630   //    the layer of the root window.
631   separator_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
632   separator_layer_->SetColor(SK_ColorBLACK);
633   separator_layer_->SetBounds(
634       SeparatorBoundsFromViewportBounds(viewport_bounds));
635   params.parent->layer()->Add(separator_layer_.get());
636 
637   // 3- Create a background layer that will show a dark gray color behind the
638   //    magnifier layer. It has the same bounds as the viewport.
639   viewport_background_layer_ =
640       std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
641   viewport_background_layer_->SetColor(SK_ColorDKGRAY);
642   viewport_background_layer_->SetBounds(viewport_bounds);
643   aura::Window* viewport_window = viewport_widget_->GetNativeView();
644   ui::Layer* viewport_layer = viewport_window->layer();
645   viewport_layer->Add(viewport_background_layer_.get());
646 
647   // 4- Create the layer in which the contents of the screen will be mirrored
648   //    and magnified.
649   viewport_magnifier_layer_ =
650       std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
651   // There are situations that the content rect for the magnified container gets
652   // larger than its bounds (e.g. shelf stretches beyond the screen to allow it
653   // being dragged up, or contents of mouse pointer might go beyond screen when
654   // the pointer is at the edges of the screen). To avoid this extra content
655   // becoming visible in the magnifier, magnifier layer should clip its contents
656   // to its bounds.
657   viewport_magnifier_layer_->SetMasksToBounds(true);
658   viewport_layer->Add(viewport_magnifier_layer_.get());
659   viewport_layer->SetMasksToBounds(true);
660 
661   // 5- Update the workarea of the current screen such that an area enough to
662   //    contain the viewport and the separator is allocated at the top of the
663   //    screen.
664   SetViewportHeightInWorkArea(current_source_root_window_,
665                               GetTotalMagnifierHeight());
666 
667   // 6- Confine the mouse cursor within the remaining part of the display.
668   ConfineMouseCursorOutsideViewport();
669 
670   // 7- Show the widget, which can trigger events to request movement of the
671   // viewport now that all internal state has been created.
672   viewport_widget_->AddObserver(this);
673   viewport_widget_->Show();
674 }
675 
MaybeCachePointOfInterestMinimumHeight(aura::WindowTreeHost * host)676 void DockedMagnifierControllerImpl::MaybeCachePointOfInterestMinimumHeight(
677     aura::WindowTreeHost* host) {
678   DCHECK(GetEnabled());
679   DCHECK(current_source_root_window_);
680   DCHECK(host);
681 
682   if (is_minimum_point_of_interest_height_valid_)
683     return;
684 
685   // Adjust the point of interest so that we don't end up magnifying the
686   // magnifier. This means we don't allow the point of interest to go beyond a
687   // minimum y-coordinate value. Here's how we find that minimum value:
688   //
689   // +-----------------+     +-----------------------------------+
690   // |     Viewport    |     |                                   |
691   // +====separator====+     |        Magnified Viewport         |
692   // |                (+) b  |                                   |
693   // |                 |     +==============separator===========(+) A
694   // |                 |     |                  Distance (D) --> |
695   // |                 |     |                                  (+) B
696   // +-----------------+     |                                   |
697   //    Screen in Non        |                                   |
698   //   Magnified Space       |                                   |
699   //                         |                                   |
700   //                         |                                   |
701   //                         +-----------------------------------+
702   //                               Screen in Magnified Space
703   //                     (the contents of |viewport_magnifier_layer_|)
704   //
705   // Problem: Find the height of the point of interest (b) in the non-magnified
706   //          coordinates space, which corresponds to the height of point (B) in
707   //          the magnified coordinates space, such that when point (A) is
708   //          translated from the magnified coordinates space to the non-
709   //          magnified coordinates space, its y coordinate is 0 (i.e. aligns
710   //          with the top of the magnifier viewport).
711   //
712   // 1- The height of Point (A) in the magnified space is the bottom of the
713   //    entire magnifier (which is actually the bottom of the separator) in the
714   //    magnified coordinates space.
715   //    Note that the magnified space is in pixels. This point should be
716   //    translated such that its y-coordiante is not greater than 0 (in the non-
717   //    magnified coordinates space), otherwise the magnifier will magnify and
718   //    mirror itself.
719   // 2- Point (B) is the scaled point of interest in the magnified space. The
720   //    point of interest is always translated to the center point of the
721   //    viewport. Hence, if point (A) goes to y = 0, and point (B) goes to a
722   //    height equals to the height of the center point of the viewport,
723   //    therefore means distance (D) = viewport_center_point.y().
724   // 3- Now that we found the height of point (B) in the magnified space,
725   //    find the the height of point (b) which is the corresponding height in
726   //    the non-magnified space. This height is the minimum height below which
727   //    the point of interest may not go.
728 
729   const gfx::Rect viewport_bounds =
730       GetViewportWidgetBoundsInRoot(current_source_root_window_);
731 
732   // 1- Point (A)'s height.
733   // Note we use a Vector3dF to actually represent a 2D point. The reason is
734   // Vector3dF provides an API to get the Length() of the vector without
735   // converting the object to another temporary object. We need to get the
736   // Length() rather than y() because screen rotation transform can make the
737   // height we are interested in either x() or y() depending on the rotation
738   // angle, so we just simply use Length().
739   // Note: Why transform the point to the magnified scale and back? The reason
740   // is that we need to go through the root window transform to go to the pixel
741   // space. This will account for device scale factors, screen rotations, and
742   // any other transforms that we cannot anticipate ourselves.
743   gfx::Vector3dF scaled_magnifier_bottom_in_pixels(
744       0.0f, viewport_bounds.bottom() + kSeparatorHeight, 0.0f);
745   const float scale = GetScale();
746   scaled_magnifier_bottom_in_pixels.Scale(scale);
747 
748   // 2- Point (B)'s height.
749   const gfx::PointF viewport_center_point(viewport_bounds.CenterPoint());
750   gfx::Vector3dF minimum_height_vector(
751       0.0f,
752       viewport_center_point.y() + scaled_magnifier_bottom_in_pixels.Length(),
753       0.0f);
754 
755   // 3- Back to non-magnified space to get point (b)'s height.
756   minimum_height_vector.Scale(1 / scale);
757   minimum_point_of_interest_height_ = minimum_height_vector.Length();
758   is_minimum_point_of_interest_height_valid_ = true;
759 }
760 
ConfineMouseCursorOutsideViewport()761 void DockedMagnifierControllerImpl::ConfineMouseCursorOutsideViewport() {
762   DCHECK(current_source_root_window_);
763 
764   gfx::Rect confine_bounds =
765       current_source_root_window_->GetBoundsInRootWindow();
766   const int docked_height = separator_layer_->bounds().bottom();
767   confine_bounds.Offset(0, docked_height);
768   confine_bounds.set_height(confine_bounds.height() - docked_height);
769   RootWindowController::ForWindow(current_source_root_window_)
770       ->ash_host()
771       ->ConfineCursorToBoundsInRoot(confine_bounds);
772 }
773 
OnMoveMagnifierTimer()774 void DockedMagnifierControllerImpl::OnMoveMagnifierTimer() {
775   // Ignore caret changes while move magnifier to rect activity is occurring.
776   if (base::TimeTicks::Now() - last_move_magnifier_to_rect_ <
777       magnifier_utils::kPauseCaretUpdateDuration) {
778     return;
779   }
780 
781   CenterOnPoint(last_caret_screen_point_);
782 }
783 
784 }  // namespace ash
785