1 // Copyright 2014 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.
5 #include "athena/wm/split_view_controller.h"
9 #include "athena/screen/public/screen_manager.h"
10 #include "athena/wm/public/window_manager.h"
11 #include "athena/wm/window_list_provider_impl.h"
12 #include "base/bind.h"
13 #include "ui/aura/scoped_window_targeter.h"
14 #include "ui/aura/window.h"
15 #include "ui/aura/window_targeter.h"
16 #include "ui/compositor/closure_animation_observer.h"
17 #include "ui/compositor/layer.h"
18 #include "ui/compositor/scoped_layer_animation_settings.h"
19 #include "ui/events/event_handler.h"
20 #include "ui/gfx/display.h"
21 #include "ui/gfx/screen.h"
22 #include "ui/views/background.h"
23 #include "ui/views/layout/box_layout.h"
24 #include "ui/views/widget/root_view.h"
25 #include "ui/views/widget/root_view_targeter.h"
26 #include "ui/views/widget/widget.h"
27 #include "ui/wm/core/window_util.h"
28 #include "ui/wm/public/activation_client.h"
34 const int kDragHandleWidth
= 4;
35 const int kDragHandleHeight
= 80;
36 const int kDragHandleMargin
= 1;
37 const int kDividerWidth
= kDragHandleWidth
+ 2 * kDragHandleMargin
;
39 // Max distance from the scroll end position to the middle of the screen where
40 // we would go into the split view mode.
41 const float kMaxDistanceFromMiddle
= 120.0f
;
43 // The minimum x-velocity required for a fling to disengage split view mode
44 // when targeted to the drag handle.
45 const float kMinFlingVelocity
= 800.0f
;
47 enum WindowToActivate
{
48 // Do not activate either of |left_window_| or |right_window_|.
50 // Activate |left_window_|.
52 // Activate |right_window_|.
56 // Always returns the same target.
57 class StaticViewTargeterDelegate
: public views::ViewTargeterDelegate
{
59 explicit StaticViewTargeterDelegate(views::View
* target
) : target_(target
) {}
61 ~StaticViewTargeterDelegate() override
{}
64 // views::ViewTargeterDelegate:
65 virtual views::View
* TargetForRect(views::View
* root
,
66 const gfx::Rect
& rect
) override
{
73 DISALLOW_COPY_AND_ASSIGN(StaticViewTargeterDelegate
);
76 // Expands the effective target area of the window of the widget containing the
77 // specified view. If the view is large enough to begin with, there should be
78 // no change from the default targeting behavior.
79 class PriorityWindowTargeter
: public aura::WindowTargeter
,
80 public aura::WindowObserver
{
82 explicit PriorityWindowTargeter(views::View
* priority_view
)
83 : priority_view_(priority_view
) {
84 CHECK(priority_view
->GetWidget());
85 window_
= priority_view
->GetWidget()->GetNativeWindow();
87 window_
->AddObserver(this);
90 ~PriorityWindowTargeter() override
{ window_
->RemoveObserver(this); }
93 // aura::WindowTargeter:
94 ui::EventTarget
* FindTargetForLocatedEvent(ui::EventTarget
* root
,
95 ui::LocatedEvent
* event
) override
{
96 if (!window_
|| (event
->type() != ui::ET_TOUCH_PRESSED
))
97 return WindowTargeter::FindTargetForLocatedEvent(root
, event
);
98 CHECK_EQ(window_
, priority_view_
->GetWidget()->GetNativeWindow());
100 // Bounds of the view in root window's coordinates.
101 gfx::Rect view_bounds
= priority_view_
->GetBoundsInScreen();
102 // If there is a transform on the window's layer - apply it.
103 gfx::Transform window_transform
= window_
->layer()->transform();
104 gfx::RectF transformed_bounds_f
= view_bounds
;
105 window_transform
.TransformRect(&transformed_bounds_f
);
106 gfx::Rect transformed_bounds
= gfx::Rect(transformed_bounds_f
.x(),
107 transformed_bounds_f
.y(),
108 transformed_bounds_f
.width(),
109 transformed_bounds_f
.height());
110 // Now expand the bounds to be at least
111 // kMinTouchDimension x kMinTouchDimension and target the event to the
112 // window if it falls within the expanded bounds
113 gfx::Point center
= transformed_bounds
.CenterPoint();
114 gfx::Rect extension_rect
= gfx::Rect(
115 center
.x() - kMinTouchDimension
/ 2,
116 center
.y() - kMinTouchDimension
/ 2,
119 gfx::Rect extended_bounds
=
120 gfx::UnionRects(transformed_bounds
, extension_rect
);
121 if (extended_bounds
.Contains(event
->root_location())) {
122 root
->ConvertEventToTarget(window_
, event
);
126 return WindowTargeter::FindTargetForLocatedEvent(root
, event
);
129 // aura::WindowObserver:
130 void OnWindowDestroying(aura::Window
* window
) override
{
131 DCHECK_EQ(window
, window_
);
132 window_
->RemoveObserver(this);
136 // Minimum dimension of a target to be comfortably touchable.
137 // The effective touch target area of |priority_window_| gets expanded so
138 // that it's width and height is ayt least |kMinTouchDimension|.
139 int const kMinTouchDimension
= 26;
141 aura::Window
* window_
;
142 views::View
* priority_view_
;
144 DISALLOW_COPY_AND_ASSIGN(PriorityWindowTargeter
);
147 // Returns a target transform required to transform |from| to |to|.
148 gfx::Transform
GetTransformForBounds(const gfx::Rect
& from
,
149 const gfx::Rect
& to
) {
150 gfx::Transform transform
;
151 transform
.Translate(to
.x() - from
.x(), to
.y() - from
.y());
152 transform
.Scale(to
.width() / static_cast<float>(from
.width()),
153 to
.height() / static_cast<float>(from
.height()));
157 bool IsLandscapeOrientation(gfx::Display::Rotation rotation
) {
158 return rotation
== gfx::Display::ROTATE_0
||
159 rotation
== gfx::Display::ROTATE_180
;
164 SplitViewController::SplitViewController(
165 aura::Window
* container
,
166 WindowListProviderImpl
* window_list_provider
)
168 container_(container
),
169 window_list_provider_(window_list_provider
),
170 left_window_(nullptr),
171 right_window_(nullptr),
172 divider_position_(0),
173 divider_scroll_start_position_(0),
174 divider_widget_(nullptr),
175 drag_handle_(nullptr),
176 weak_factory_(this) {
177 window_list_provider_
->AddObserver(this);
180 SplitViewController::~SplitViewController() {
181 window_list_provider_
->RemoveObserver(this);
184 bool SplitViewController::CanActivateSplitViewMode() const {
185 // TODO(mfomitchev): return false in full screen.
186 return (!IsSplitViewModeActive() &&
187 window_list_provider_
->GetWindowList().size() >= 2 &&
188 IsLandscapeOrientation(gfx::Screen::GetNativeScreen()->
189 GetDisplayNearestWindow(container_
).rotation()));
192 bool SplitViewController::IsSplitViewModeActive() const {
193 return state_
== ACTIVE
;
196 void SplitViewController::ActivateSplitMode(aura::Window
* left
,
198 aura::Window
* to_activate
) {
199 const aura::Window::Windows
& windows
= window_list_provider_
->GetWindowList();
200 aura::Window::Windows::const_reverse_iterator iter
= windows
.rbegin();
201 if (state_
== ACTIVE
) {
202 if (!left
&& left_window_
!= right
)
204 if (!right
&& right_window_
!= left
)
205 right
= right_window_
;
208 if (!left
&& iter
!= windows
.rend()) {
211 if (left
== right
&& iter
!= windows
.rend()) {
217 if (!right
&& iter
!= windows
.rend()) {
220 if (right
== left
&& iter
!= windows
.rend()) {
227 if (left_window_
&& left_window_
!= left
&& left_window_
!= right
)
228 to_hide_
.push_back(left_window_
);
229 if (right_window_
&& right_window_
!= left
&& right_window_
!= right
)
230 to_hide_
.push_back(right_window_
);
233 right_window_
= right
;
235 divider_position_
= GetDefaultDividerPosition();
239 aura::client::ActivationClient
* activation_client
=
240 aura::client::GetActivationClient(container_
->GetRootWindow());
241 aura::Window
* active_window
= activation_client
->GetActiveWindow();
243 CHECK(to_activate
== left_window_
|| to_activate
== right_window_
);
244 wm::ActivateWindow(to_activate
);
245 } else if (active_window
!= left_window_
&&
246 active_window
!= right_window_
) {
247 // A window which does not belong to an activity could be active.
248 wm::ActivateWindow(left_window_
);
250 active_window
= activation_client
->GetActiveWindow();
252 if (active_window
== left_window_
)
253 window_list_provider_
->StackWindowBehindTo(right_window_
, left_window_
);
255 window_list_provider_
->StackWindowBehindTo(left_window_
, right_window_
);
258 void SplitViewController::ReplaceWindow(aura::Window
* window
,
259 aura::Window
* replace_with
) {
260 CHECK(IsSplitViewModeActive());
262 CHECK(window
== left_window_
|| window
== right_window_
);
263 CHECK(replace_with
!= left_window_
&& replace_with
!= right_window_
);
264 DCHECK(window_list_provider_
->IsValidWindow(replace_with
));
266 aura::Window
* not_replaced
= nullptr;
267 if (window
== left_window_
) {
268 left_window_
= replace_with
;
269 not_replaced
= right_window_
;
271 right_window_
= replace_with
;
272 not_replaced
= left_window_
;
276 wm::ActivateWindow(replace_with
);
277 window_list_provider_
->StackWindowBehindTo(not_replaced
, replace_with
);
279 window
->SetTransform(gfx::Transform());
283 void SplitViewController::DeactivateSplitMode() {
284 CHECK_EQ(ACTIVE
, state_
);
287 left_window_
= right_window_
= nullptr;
290 void SplitViewController::InitializeDivider() {
291 CHECK(!divider_widget_
);
292 CHECK(!drag_handle_
);
294 drag_handle_
= CreateDragHandleView(DRAG_HANDLE_HORIZONTAL
,
298 views::View
* content_view
= new views::View
;
299 content_view
->set_background(
300 views::Background::CreateSolidBackground(SK_ColorBLACK
));
301 views::BoxLayout
* layout
=
302 new views::BoxLayout(views::BoxLayout::kHorizontal
,
306 layout
->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER
);
307 layout
->set_cross_axis_alignment(
308 views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER
);
309 content_view
->SetLayoutManager(layout
);
310 content_view
->AddChildView(drag_handle_
);
312 divider_widget_
= new views::Widget();
313 views::Widget::InitParams
params(views::Widget::InitParams::TYPE_POPUP
);
314 params
.parent
= container_
;
315 params
.bounds
= gfx::Rect(-kDividerWidth
/ 2,
318 container_
->bounds().height());
319 divider_widget_
->Init(params
);
320 divider_widget_
->SetContentsView(content_view
);
322 // Install a static view targeter on the root view which always targets
324 // TODO(mfomitchev,tdanderson): This should not be needed:
325 // 1. crbug.com/414339 - divider_view is the only view and it completely
326 // overlaps the root view.
327 // 2. The logic in ViewTargeterDelegate::TargetForRect could be improved to
328 // work better for views that are narrow in one dimension and long in
329 // another dimension.
330 views::internal::RootView
* root_view
=
331 static_cast<views::internal::RootView
*>(divider_widget_
->GetRootView());
332 view_targeter_delegate_
.reset(new StaticViewTargeterDelegate(drag_handle_
));
333 views::ViewTargeter
* targeter
=
334 new views::RootViewTargeter(view_targeter_delegate_
.get(), root_view
);
335 divider_widget_
->GetRootView()->SetEventTargeter(
336 scoped_ptr
<views::ViewTargeter
>(targeter
));
339 void SplitViewController::HideDivider() {
340 divider_widget_
->Hide();
341 window_targeter_
.reset();
344 void SplitViewController::ShowDivider() {
345 divider_widget_
->Show();
346 if (!window_targeter_
) {
347 scoped_ptr
<ui::EventTargeter
> window_targeter
=
348 scoped_ptr
<ui::EventTargeter
>(new PriorityWindowTargeter(drag_handle_
));
349 window_targeter_
.reset(
350 new aura::ScopedWindowTargeter(container_
, window_targeter
.Pass()));
354 gfx::Rect
SplitViewController::GetLeftAreaBounds() {
355 gfx::Rect work_area
=
356 gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area();
358 0, 0, divider_position_
- kDividerWidth
/ 2, work_area
.height());
361 gfx::Rect
SplitViewController::GetRightAreaBounds() {
362 gfx::Rect work_area
=
363 gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area();
364 int container_width
= container_
->bounds().width();
365 return gfx::Rect(divider_position_
+ kDividerWidth
/ 2,
367 container_width
- divider_position_
- kDividerWidth
/ 2,
371 void SplitViewController::OnWindowAddedToList(aura::Window
* added_window
) {
374 void SplitViewController::OnWindowRemovedFromList(aura::Window
* removed_window
,
376 if (!IsSplitViewModeActive() ||
377 (removed_window
!= left_window_
&& removed_window
!= right_window_
)) {
381 DCHECK(!window_list_provider_
->IsWindowInList(removed_window
));
383 const aura::Window::Windows windows
= window_list_provider_
->GetWindowList();
384 CHECK_GE(static_cast<int>(windows
.size()), 1);
385 DCHECK_GE(index
, static_cast<int>(windows
.size() - 1));
386 DCHECK_LE(index
, static_cast<int>(windows
.size()));
388 if (windows
.size() == 1) {
389 DeactivateSplitMode();
393 aura::Window
* next_window
= *(windows
.rbegin() + 1);
394 if (removed_window
== left_window_
) {
395 CHECK(right_window_
== windows
.back());
396 left_window_
= next_window
;
398 CHECK(left_window_
== windows
.back());
399 CHECK(removed_window
== right_window_
);
400 right_window_
= next_window
;
405 void SplitViewController::SetState(SplitViewController::State state
) {
409 if (divider_widget_
== nullptr)
414 ScreenManager::Get()->SetRotationLocked(state_
!= INACTIVE
);
415 if (state
== INACTIVE
)
421 void SplitViewController::UpdateLayout(bool animate
) {
423 CHECK(right_window_
);
424 // Splitview can be activated from SplitViewController::ActivateSplitMode or
425 // SplitViewController::ScrollEnd. Additionally we don't want to rotate the
426 // screen while engaging splitview (i.e. state_ == SCROLLING).
427 if (state_
== INACTIVE
&& !animate
) {
428 gfx::Rect work_area
=
429 gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area();
430 aura::Window
* top_window
= window_list_provider_
->GetWindowList().back();
431 if (top_window
!= left_window_
) {
432 // TODO(mfomitchev): Use to_hide_ instead
433 left_window_
->Hide();
434 right_window_
->SetBounds(gfx::Rect(work_area
.size()));
436 if (top_window
!= right_window_
) {
437 left_window_
->SetBounds(gfx::Rect(work_area
.size()));
438 // TODO(mfomitchev): Use to_hide_ instead
439 right_window_
->Hide();
442 gfx::Transform(), gfx::Transform(), gfx::Transform(), false);
446 left_window_
->Show();
447 right_window_
->Show();
449 gfx::Transform divider_transform
;
450 divider_transform
.Translate(divider_position_
, 0);
451 if (state_
== ACTIVE
) {
453 gfx::Transform left_transform
=
454 GetTransformForBounds(left_window_
->bounds(), GetLeftAreaBounds());
455 gfx::Transform right_transform
=
456 GetTransformForBounds(right_window_
->bounds(), GetRightAreaBounds());
458 left_transform
, right_transform
, divider_transform
, true);
460 left_window_
->SetBounds(GetLeftAreaBounds());
461 right_window_
->SetBounds(GetRightAreaBounds());
463 gfx::Transform(), gfx::Transform(), divider_transform
, false);
466 gfx::Transform left_transform
;
467 gfx::Transform right_transform
;
468 gfx::Rect left_area_bounds
= GetLeftAreaBounds();
469 gfx::Rect right_area_bounds
= GetRightAreaBounds();
470 // If the width of the window is greater than the width of the area which it
471 // is supposed to occupy - translate the window. Otherwise scale the window
472 // up to fill the target area.
473 if (left_window_
->bounds().width() >= left_area_bounds
.width()) {
474 left_transform
.Translate(
475 left_area_bounds
.right() - left_window_
->bounds().right(), 0);
478 GetTransformForBounds(left_window_
->bounds(), left_area_bounds
);
480 if (right_window_
->bounds().width() >= right_area_bounds
.width()) {
481 right_transform
.Translate(
482 right_area_bounds
.x() - right_window_
->bounds().x(), 0);
485 GetTransformForBounds(right_window_
->bounds(), right_area_bounds
);
488 left_transform
, right_transform
, divider_transform
, animate
);
490 // Note: |left_window_| and |right_window_| may be nullptr if calling
491 // SetWindowTransforms():
492 // - caused the in-progress animation to abort.
493 // - started a zero duration animation.
496 void SplitViewController::SetWindowTransforms(
497 const gfx::Transform
& left_transform
,
498 const gfx::Transform
& right_transform
,
499 const gfx::Transform
& divider_transform
,
502 ui::ScopedLayerAnimationSettings
left_settings(
503 left_window_
->layer()->GetAnimator());
504 left_settings
.SetPreemptionStrategy(
505 ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET
);
506 left_window_
->SetTransform(left_transform
);
508 ui::ScopedLayerAnimationSettings
divider_widget_settings(
509 divider_widget_
->GetNativeWindow()->layer()->GetAnimator());
510 divider_widget_settings
.SetPreemptionStrategy(
511 ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET
);
512 divider_widget_
->GetNativeWindow()->SetTransform(divider_transform
);
514 ui::ScopedLayerAnimationSettings
right_settings(
515 right_window_
->layer()->GetAnimator());
516 right_settings
.SetPreemptionStrategy(
517 ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET
);
518 right_settings
.AddObserver(new ui::ClosureAnimationObserver(
519 base::Bind(&SplitViewController::OnAnimationCompleted
,
520 weak_factory_
.GetWeakPtr())));
521 right_window_
->SetTransform(right_transform
);
523 left_window_
->SetTransform(left_transform
);
524 divider_widget_
->GetNativeWindow()->SetTransform(divider_transform
);
525 right_window_
->SetTransform(right_transform
);
529 void SplitViewController::OnAnimationCompleted() {
530 // Animation can be cancelled when deactivated.
531 if (left_window_
== nullptr)
535 for (size_t i
= 0; i
< to_hide_
.size(); ++i
)
539 if (state_
== INACTIVE
) {
540 left_window_
= nullptr;
541 right_window_
= nullptr;
545 int SplitViewController::GetDefaultDividerPosition() {
546 return container_
->GetBoundsInScreen().width() / 2;
549 float SplitViewController::GetMaxDistanceFromMiddleForTest() const {
550 return kMaxDistanceFromMiddle
;
553 float SplitViewController::GetMinFlingVelocityForTest() const {
554 return kMinFlingVelocity
;
557 ///////////////////////////////////////////////////////////////////////////////
558 // DragHandleScrollDelegate:
560 void SplitViewController::HandleScrollBegin(float delta
) {
561 CHECK(state_
== ACTIVE
);
563 divider_scroll_start_position_
= GetDefaultDividerPosition();
564 divider_position_
= divider_scroll_start_position_
+ delta
;
568 void SplitViewController::HandleScrollEnd(float velocity
) {
569 if (state_
!= SCROLLING
)
572 int delta
= GetDefaultDividerPosition() - divider_position_
;
573 WindowToActivate window
= WINDOW_NONE
;
574 if (std::abs(velocity
) > kMinFlingVelocity
)
575 window
= velocity
> 0 ? WINDOW_LEFT
: WINDOW_RIGHT
;
576 else if (std::abs(delta
) > kMaxDistanceFromMiddle
)
577 window
= delta
> 0 ? WINDOW_RIGHT
: WINDOW_LEFT
;
581 divider_position_
= GetDefaultDividerPosition();
585 divider_position_
= container_
->GetBoundsInScreen().width();
587 wm::ActivateWindow(left_window_
);
590 divider_position_
= 0;
592 wm::ActivateWindow(right_window_
);
599 void SplitViewController::HandleScrollUpdate(float delta
) {
600 if (state_
!= SCROLLING
)
602 divider_position_
= divider_scroll_start_position_
+ delta
;
606 ///////////////////////////////////////////////////////////////////////////////
607 // WindowManagerObserver:
609 void SplitViewController::OnOverviewModeEnter() {
614 void SplitViewController::OnOverviewModeExit() {
615 if (state_
!= INACTIVE
)
619 void SplitViewController::OnSplitViewModeEnter() {
622 void SplitViewController::OnSplitViewModeExit() {
625 } // namespace athena