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