1 // Copyright 2017 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 #ifndef ASH_WM_SPLITVIEW_SPLIT_VIEW_CONTROLLER_H_
6 #define ASH_WM_SPLITVIEW_SPLIT_VIEW_CONTROLLER_H_
7 
8 #include <limits>
9 #include <memory>
10 
11 #include "ash/accessibility/accessibility_observer.h"
12 #include "ash/ash_export.h"
13 #include "ash/public/cpp/tablet_mode_observer.h"
14 #include "ash/shell_observer.h"
15 #include "ash/wm/overview/overview_observer.h"
16 #include "ash/wm/tablet_mode/tablet_mode_controller.h"
17 #include "ash/wm/window_state_observer.h"
18 #include "base/containers/flat_map.h"
19 #include "base/macros.h"
20 #include "base/observer_list.h"
21 #include "base/time/time.h"
22 #include "ui/aura/window_observer.h"
23 #include "ui/display/display.h"
24 #include "ui/display/display_observer.h"
25 #include "ui/gfx/geometry/point.h"
26 
27 namespace ui {
28 class Layer;
29 }  // namespace ui
30 
31 namespace ash {
32 class PresentationTimeRecorder;
33 class OverviewSession;
34 class SplitViewControllerTest;
35 class SplitViewDivider;
36 class SplitViewObserver;
37 class SplitViewOverviewSessionTest;
38 
39 // The controller for the split view. It snaps a window to left/right side of
40 // the screen. It also observes the two snapped windows and decides when to exit
41 // the split view mode.
42 // TODO(xdai): Make it work for multi-display non mirror environment.
43 class ASH_EXPORT SplitViewController : public aura::WindowObserver,
44                                        public WindowStateObserver,
45                                        public ShellObserver,
46                                        public OverviewObserver,
47                                        public display::DisplayObserver,
48                                        public TabletModeObserver,
49                                        public AccessibilityObserver {
50  public:
51   // |LEFT| and |RIGHT| are named for the positions to which they correspond in
52   // clamshell mode or primary-landscape-oriented tablet mode. In portrait-
53   // oriented tablet mode, we actually snap windows on the top and bottom, but
54   // in clamshell mode, although the display orientation may sometimes be
55   // portrait, we always snap windows on the left and right (see
56   // |IsLayoutHorizontal|). The snap positions are swapped in secondary-oriented
57   // tablet mode (see |IsLayoutRightSideUp|).
58   enum SnapPosition { NONE, LEFT, RIGHT };
59 
60   // Why splitview was ended.
61   enum class EndReason {
62     kNormal = 0,
63     kHomeLauncherPressed,
64     kUnsnappableWindowActivated,
65     kActiveUserChanged,
66     kWindowDragStarted,
67     kExitTabletMode,
68     // Splitview is being ended due to a change in Virtual Desks, such as
69     // switching desks or removing a desk.
70     kDesksChange,
71   };
72 
73   // The behaviors of split view are very different when in tablet mode and in
74   // clamshell mode. In tablet mode, split view mode will stay active until the
75   // user explicitly ends it (e.g., by pressing home launcher, or long pressing
76   // the overview button, or sliding the divider bar to the edge, etc). However,
77   // in clamshell mode, there is no divider bar, and split view mode only stays
78   // active during overview snapping, i.e., it's only possible that split view
79   // is active when overview is active. Once the user has selected two windows
80   // to snap to both side of the screen, split view mode is no longer active.
81   enum class SplitViewType {
82     kTabletType = 0,
83     kClamshellType,
84   };
85 
86   enum class State {
87     kNoSnap,
88     kLeftSnapped,
89     kRightSnapped,
90     kBothSnapped,
91   };
92 
93   // Gets the |SplitViewController| for the root window of |window|. |window| is
94   // important in clamshell mode. In tablet mode, the working assumption for now
95   // is mirror mode (or just one display), and so |window| can be almost any
96   // window and it does not matter. For code that only applies to tablet mode,
97   // you may simply use the primary root (see |Shell::GetPrimaryRootWindow|).
98   // The user actually can go to the display settings while in tablet mode and
99   // choose extend; we just are not yet trying to support it really well. When
100   // the |ash::features::kMultiDisplayOverviewAndSplitView| feature flag is
101   // disabled, |window| is ignored as there is only one |SplitViewController|.
102   static SplitViewController* Get(const aura::Window* window);
103 
104   // The return values of these two functions together indicate what actual
105   // positions correspond to |LEFT| and |RIGHT|:
106   // |IsLayoutHorizontal|  |IsLayoutRightSideUp|  |LEFT|               |RIGHT|
107   // -------------------------------------------------------------------------
108   // true                  true                   left                 right
109   // true                  false                  right                left
110   // false                 true                   top                  bottom
111   // false                 false                  bottom               top
112   // In tablet mode, these functions return values based on display orientation.
113   // In clamshell mode, these functions return true.
114   static bool IsLayoutHorizontal();
115   static bool IsLayoutRightSideUp();
116 
117   // Returns true if |position| actually signifies a left or top position,
118   // according to the return values of |IsLayoutHorizontal| and
119   // |IsLayoutRightSideUp|.
120   static bool IsPhysicalLeftOrTop(SnapPosition position);
121 
122   explicit SplitViewController(aura::Window* root_window);
123   ~SplitViewController() override;
124 
125   // Returns true if split view mode is active. Please see SplitViewType above
126   // to see the difference between tablet mode and clamshell mode splitview
127   // mode.
128   bool InSplitViewMode() const;
129   bool InClamshellSplitViewMode() const;
130   bool InTabletSplitViewMode() const;
131 
132   // Checks the following criteria:
133   // 1. Split view mode is supported (see |ShouldAllowSplitView|).
134   // 2. |window| can be activated (see |wm::CanActivateWindow|).
135   // 3. The |WindowState| of |window| can snap (see |WindowState::CanSnap|).
136   // 4. |window|'s minimum size, if any, fits into the left or top with the
137   //    default divider position. (If the work area length is odd, then the
138   //    right or bottom will be one pixel larger.)
139   // See also the |DCHECK|s in |SnapWindow|.
140   bool CanSnapWindow(aura::Window* window) const;
141 
142   // Snaps window to left/right. It will try to remove |window| from the
143   // overview window grid first before snapping it if |window| is currently
144   // showing in the overview window grid. If split view mode is not already
145   // active, and if |window| is not minimized, |use_divider_spawn_animation|
146   // causes the divider to show up with an animation that adds a finishing touch
147   // to the snap animation of |window|. Use true when |window| is snapped by
148   // dragging, except for tab dragging.
149   void SnapWindow(aura::Window* window,
150                   SnapPosition snap_position,
151                   bool use_divider_spawn_animation = false);
152 
153   // Swaps the left and right windows. This will do nothing if one of the
154   // windows is not snapped.
155   void SwapWindows();
156 
157   // |window| should be |left_window_| or |right_window_|, and this function
158   // returns |LEFT| or |RIGHT| accordingly.
159   SnapPosition GetPositionOfSnappedWindow(const aura::Window* window) const;
160 
161   // |position| should be |LEFT| or |RIGHT|, and this function returns
162   // |left_window_| or |right_window_| accordingly.
163   aura::Window* GetSnappedWindow(SnapPosition position);
164 
165   // Returns the default snapped window. It's the window that remains open until
166   // the split mode ends. It's decided by |default_snap_position_|. E.g., If
167   // |default_snap_position_| equals LEFT, then the default snapped window is
168   // |left_window_|. All the other window will open on the right side.
169   aura::Window* GetDefaultSnappedWindow();
170 
171   // Gets snapped bounds based on |snap_position| and |divider_position_|,
172   // adjusted to accommodate the minimum size of |window_for_minimum_size| if
173   // |window_for_minimum_size| is not null.
174   gfx::Rect GetSnappedWindowBoundsInParent(
175       SnapPosition snap_position,
176       aura::Window* window_for_minimum_size);
177   gfx::Rect GetSnappedWindowBoundsInScreen(
178       SnapPosition snap_position,
179       aura::Window* window_for_minimum_size);
180 
181   // Gets the default value of |divider_position_|.
182   int GetDefaultDividerPosition() const;
183 
184   // Returns true during the divider snap animation.
185   bool IsDividerAnimating() const;
186 
187   void StartResize(const gfx::Point& location_in_screen);
188   void Resize(const gfx::Point& location_in_screen);
189   void EndResize(const gfx::Point& location_in_screen);
190 
191   // Ends the split view mode.
192   void EndSplitView(EndReason end_reason = EndReason::kNormal);
193 
194   // Returns true if |window| is a snapped window in splitview.
195   bool IsWindowInSplitView(const aura::Window* window) const;
196 
197   // This function is only supposed to be called during clamshell <-> tablet
198   // transition or multi-user transition, when we need to carry over one/two
199   // snapped windows into splitview, we calculate the divider position based on
200   // the one or two to-be-snapped windows' bounds so that we can keep the
201   // snapped windows' bounds after transition (instead of putting them always
202   // on the middle split position).
203   void InitDividerPositionForTransition(int divider_position);
204 
205   // Returns true if |window| is in a transitinal state which means that
206   // |SplitViewController| has already changed its internal snapped state for
207   // |window| but the snapped state has not been applied to |window|'s window
208   // state yet. The transional state can be happen in some clients (e.g. ARC
209   // app) which handle window states asynchronously.
210   bool IsWindowInTransitionalState(const aura::Window* window) const;
211 
212   // Called when the overview button tray has been long pressed. Enters
213   // splitview mode if the active window is snappable. Also enters overview mode
214   // if device is not currently in overview mode.
215   void OnOverviewButtonTrayLongPressed(const gfx::Point& event_location);
216 
217   // Called when a window (either it's browser window or an app window) start/
218   // end being dragged.
219   void OnWindowDragStarted(aura::Window* dragged_window);
220   void OnWindowDragEnded(aura::Window* dragged_window,
221                          SnapPosition desired_snap_position,
222                          const gfx::Point& last_location_in_screen);
223   void OnWindowDragCanceled();
224 
225   void AddObserver(SplitViewObserver* observer);
226   void RemoveObserver(SplitViewObserver* observer);
227 
228   // aura::WindowObserver:
229   void OnWindowPropertyChanged(aura::Window* window,
230                                const void* key,
231                                intptr_t old) override;
232   void OnWindowBoundsChanged(aura::Window* window,
233                              const gfx::Rect& old_bounds,
234                              const gfx::Rect& new_bounds,
235                              ui::PropertyChangeReason reason) override;
236   void OnWindowDestroyed(aura::Window* window) override;
237   void OnResizeLoopStarted(aura::Window* window) override;
238   void OnResizeLoopEnded(aura::Window* window) override;
239 
240   // WindowStateObserver:
241   void OnPostWindowStateTypeChange(WindowState* window_state,
242                                    chromeos::WindowStateType old_type) override;
243 
244   // ShellObserver:
245   void OnPinnedStateChanged(aura::Window* pinned_window) override;
246 
247   // OverviewObserver:
248   void OnOverviewModeStarting() override;
249   void OnOverviewModeEnding(OverviewSession* overview_session) override;
250   void OnOverviewModeEnded() override;
251 
252   // display::DisplayObserver:
253   void OnDisplayRemoved(const display::Display& old_display) override;
254   void OnDisplayMetricsChanged(const display::Display& display,
255                                uint32_t metrics) override;
256 
257   // TabletModeObserver:
258   void OnTabletModeStarting() override;
259   void OnTabletModeStarted() override;
260   void OnTabletModeEnding() override;
261   void OnTabletModeEnded() override;
262   void OnTabletControllerDestroyed() override;
263 
264   // AccessibilityObserver:
265   void OnAccessibilityStatusChanged() override;
266   void OnAccessibilityControllerShutdown() override;
267 
root_window()268   aura::Window* root_window() const { return root_window_; }
left_window()269   aura::Window* left_window() { return left_window_; }
right_window()270   aura::Window* right_window() { return right_window_; }
divider_position()271   int divider_position() const { return divider_position_; }
state()272   State state() const { return state_; }
default_snap_position()273   SnapPosition default_snap_position() const { return default_snap_position_; }
split_view_divider()274   SplitViewDivider* split_view_divider() { return split_view_divider_.get(); }
is_resizing()275   bool is_resizing() const { return is_resizing_; }
end_reason()276   EndReason end_reason() const { return end_reason_; }
277 
278  private:
279   friend class SplitViewControllerTest;
280   friend class SplitViewOverviewSessionTest;
281   class TabDraggedWindowObserver;
282   class DividerSnapAnimation;
283   class AutoSnapController;
284 
285   // These functions return |left_window_| and |right_window_|, swapped in
286   // nonprimary screen orientations. Note that they may return null.
287   aura::Window* GetPhysicalLeftOrTopWindow();
288   aura::Window* GetPhysicalRightOrBottomWindow();
289 
290   // Start observing |window|.
291   void StartObserving(aura::Window* window);
292   // Stop observing the window at associated with |snap_position|. Also updates
293   // shadows and sets |left_window_| or |right_window_| to nullptr.
294   void StopObserving(SnapPosition snap_position);
295 
296   // Update split view state and notify its observer about the change.
297   void UpdateStateAndNotifyObservers();
298 
299   // Notifies observers that the split view divider position has been changed.
300   void NotifyDividerPositionChanged();
301 
302   // Updates the black scrim layer's bounds and opacity while dragging the
303   // divider. The opacity increases as the split divider gets closer to the edge
304   // of the screen.
305   void UpdateBlackScrim(const gfx::Point& location_in_screen);
306 
307   // Updates the bounds for the snapped windows and divider according to the
308   // current snap direction.
309   void UpdateSnappedWindowsAndDividerBounds();
310 
311   // Gets the position where the black scrim should show.
312   SnapPosition GetBlackScrimPosition(const gfx::Point& location_in_screen);
313 
314   // Updates |divider_position_| according to the current event location during
315   // resizing.
316   void UpdateDividerPosition(const gfx::Point& location_in_screen);
317 
318   // Returns the closest fix location for |divider_position_|.
319   int GetClosestFixedDividerPosition();
320 
321   // While the divider is animating to somewhere, stop it and shove it there.
322   void StopAndShoveAnimatedDivider();
323 
324   // Returns true if we should end tablet split view after resizing, i.e. the
325   // split view divider is at an edge of the work area.
326   bool ShouldEndTabletSplitViewAfterResizing();
327 
328   // Ends split view if |ShouldEndTabletSplitViewAfterResizing| returns true.
329   // Handles extra details associated with dragging the divider off the screen.
330   void EndTabletSplitViewAfterResizingIfAppropriate();
331 
332   // After resizing, if we should end split view mode, returns the window that
333   // needs to be activated. Returns nullptr if there is no such window.
334   aura::Window* GetActiveWindowAfterResizingUponExit();
335 
336   // Returns the maximum value of the |divider_position_|. It is the width of
337   // the current display's work area bounds in landscape orientation, or height
338   // of the current display's work area bounds in portrait orientation.
339   int GetDividerEndPosition() const;
340 
341   // Called after a to-be-snapped window |window| got snapped. It updates the
342   // split view states and notifies observers about the change. It also restore
343   // the snapped window's transform if it's not identity and activate it.
344   void OnWindowSnapped(aura::Window* window);
345 
346   // If there are two snapped windows, closing/minimizing/tab-dragging one of
347   // them will open overview window grid on the closed/minimized/tab-dragged
348   // window side of the screen. If there is only one snapped windows, closing/
349   // minimizing/tab-dragging the sanpped window will end split view mode and
350   // adjust the overview window grid bounds if the overview mode is active at
351   // that moment. |window_drag| is true if the window was detached as a result
352   // of dragging.
353   void OnSnappedWindowDetached(aura::Window* window, bool window_drag);
354 
355   // Returns the closest position ratio based on |distance| and |length|.
356   float FindClosestPositionRatio(float distance, float length);
357 
358   // Gets the divider optional position ratios. The divider can always be
359   // moved to the positions in |kFixedPositionRatios|. Whether the divider can
360   // be moved to |kOneThirdPositionRatio| or |kTwoThirdPositionRatio| depends
361   // on the minimum size of current snapped windows.
362   void GetDividerOptionalPositionRatios(
363       std::vector<float>* out_position_ratios);
364 
365   // Gets the expected window component depending on current screen orientation
366   // for resizing purpose.
367   int GetWindowComponentForResize(aura::Window* window);
368   // Gets the expected end drag position for |window| depending on current
369   // screen orientation and split divider position.
370   gfx::Point GetEndDragLocationInScreen(aura::Window* window,
371                                         const gfx::Point& location_in_screen);
372 
373   // Restores |window| transform to identity transform if applicable.
374   void RestoreTransformIfApplicable(aura::Window* window);
375 
376   // Called after |newly_snapped| gets snapped. Updates window stacking.
377   void UpdateWindowStackingAfterSnap(aura::Window* newly_snapped);
378 
379   // During resizing, it's possible that the resizing bounds of the snapped
380   // window is smaller than its minimum bounds, in this case we apply a
381   // translation to the snapped window to make it visually be placed outside of
382   // the workspace area.
383   void SetWindowsTransformDuringResizing();
384 
385   // Restore the snapped windows transform to identity transform after resizing.
386   void RestoreWindowsTransformAfterResizing();
387 
388   // Animates to |target_transform| for |window| and its transient descendants.
389   // |window| will be applied |start_transform| first and then animate to
390   // |target_transform|. Note |start_transform| and |end_transform| are for
391   // |window| and need to be adjusted for its transient child windows.
392   void SetTransformWithAnimation(aura::Window* window,
393                                  const gfx::Transform& start_transform,
394                                  const gfx::Transform& target_transform);
395 
396   // Updates the |snapping_window_transformed_bounds_map_| on |window|. It
397   // should be called before trying to snap the window.
398   void UpdateSnappingWindowTransformedBounds(aura::Window* window);
399 
400   // Inserts |window| into overview window grid if overview mode is active. Do
401   // nothing if overview mode is inactive at the moment.
402   void InsertWindowToOverview(aura::Window* window, bool animate = true);
403 
404   // Finalizes and cleans up after stopping dragging the divider bar to resize
405   // snapped windows.
406   void FinishWindowResizing(aura::Window* window);
407 
408   // Finalizes and cleans up divider dragging/animating. Called when the divider
409   // snapping animation completes or is interrupted or totally skipped.
410   void EndResizeImpl();
411 
412   // Called by OnWindowDragEnded to do the actual work of finishing the window
413   // dragging. If |is_being_destroyed| equals true, the dragged window is to be
414   // destroyed, and SplitViewController should not try to put it in splitview.
415   void EndWindowDragImpl(aura::Window* window,
416                          bool is_being_destroyed,
417                          SnapPosition desired_snap_position,
418                          const gfx::Point& last_location_in_screen);
419 
420   // Computes the snap position for a dragged window, based on the last
421   // mouse/gesture event location. Called by |EndWindowDragImpl| when
422   // desired_snap_position is |NONE| but because split view is already active,
423   // the dragged window needs to be snapped anyway.
424   SplitViewController::SnapPosition ComputeSnapPosition(
425       const gfx::Point& last_location_in_screen);
426 
427   // Root window the split view is in.
428   aura::Window* root_window_;
429 
430   // The current left/right snapped window.
431   aura::Window* left_window_ = nullptr;
432   aura::Window* right_window_ = nullptr;
433 
434   // Split view divider widget. Only exist in tablet splitview mode. It's a
435   // black bar stretching from one edge of the screen to the other, containing a
436   // small white drag bar in the middle. As the user presses on it and drag it
437   // to left or right, the left and right window will be resized accordingly.
438   std::unique_ptr<SplitViewDivider> split_view_divider_;
439 
440   // A black scrim layer that fades in over a window when its width drops under
441   // 1/3 of the width of the screen, increasing in opacity as the divider gets
442   // closer to the edge of the screen.
443   std::unique_ptr<ui::Layer> black_scrim_layer_;
444 
445   // The window observer that obseves the tab-dragged window in tablet mode.
446   std::unique_ptr<TabDraggedWindowObserver> dragged_window_observer_;
447 
448   // The distance between the origin of the divider and the origin of the
449   // current display's work area in screen coordinates.
450   //     |<---     divider_position_    --->|
451   //     ----------------------------------------------------------
452   //     |                                  | |                    |
453   //     |        left_window_              | |   right_window_    |
454   //     |                                  | |                    |
455   //     ----------------------------------------------------------
456   int divider_position_ = -1;
457 
458   // The closest position ratio of divider among kFixedPositionRatios,
459   // kOneThirdPositionRatio and kTwoThirdPositionRatio based on current
460   // |divider_position_|. Used to update |divider_position_| on work area
461   // changes.
462   float divider_closest_ratio_ = std::numeric_limits<float>::quiet_NaN();
463 
464   // The location of the previous mouse/gesture event in screen coordinates.
465   gfx::Point previous_event_location_;
466 
467   // The animation that animates the divider to a fixed position after resizing.
468   std::unique_ptr<DividerSnapAnimation> divider_snap_animation_;
469 
470   // Current snap state.
471   State state_ = State::kNoSnap;
472 
473   // The default snap position. It's decided by the first snapped window. If the
474   // first window was snapped left, then |default_snap_position_| equals LEFT,
475   // i.e., all the other windows will open snapped on the right side - and vice
476   // versa.
477   SnapPosition default_snap_position_ = NONE;
478 
479   // Whether the previous layout is right-side-up (see |IsLayoutRightSideUp|).
480   // Consistent with |IsLayoutRightSideUp|, |is_previous_layout_right_side_up_|
481   // is always true in clamshell mode. It is not really used in clamshell mode,
482   // but it is kept up to date in anticipation that future code changes could
483   // introduce a bug similar to https://crbug.com/1029181 which could be
484   // overlooked for years while occasionally irritating or confusing real users.
485   bool is_previous_layout_right_side_up_ = true;
486 
487   // True when the divider is being dragged (not during its snap animation).
488   bool is_resizing_ = false;
489 
490   // Stores the reason which cause splitview to end.
491   EndReason end_reason_ = EndReason::kNormal;
492 
493   // The split view type. See SplitViewType for the differences between tablet
494   // split view and clamshell split view.
495   SplitViewType split_view_type_ = SplitViewType::kTabletType;
496 
497   // The time when splitview starts. Used for metric collection purpose.
498   base::Time splitview_start_time_;
499 
500   // The map from a to-be-snapped window to its transformed bounds.
501   base::flat_map<aura::Window*, gfx::Rect>
502       snapping_window_transformed_bounds_map_;
503 
504   base::ObserverList<SplitViewObserver>::Unchecked observers_;
505 
506   ScopedObserver<TabletModeController, TabletModeObserver>
507       tablet_mode_observer_{this};
508 
509   // Records the presentation time of resize operation in split view mode.
510   std::unique_ptr<PresentationTimeRecorder> presentation_time_recorder_;
511 
512   // Observes windows and performs auto snapping if needed.
513   std::unique_ptr<AutoSnapController> auto_snap_controller_;
514 
515   DISALLOW_COPY_AND_ASSIGN(SplitViewController);
516 };
517 
518 }  // namespace ash
519 
520 #endif  // ASH_WM_SPLITVIEW_SPLIT_VIEW_CONTROLLER_H_
521