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