1// Copyright (c) 2012 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/wm/workspace/workspace_layout_manager.h" 6 7#include "ash/display/display_layout.h" 8#include "ash/display/display_manager.h" 9#include "ash/root_window_controller.h" 10#include "ash/screen_ash.h" 11#include "ash/shelf/shelf_layout_manager.h" 12#include "ash/shelf/shelf_widget.h" 13#include "ash/shell.h" 14#include "ash/shell_observer.h" 15#include "ash/test/ash_test_base.h" 16#include "ash/wm/window_state.h" 17#include "ash/wm/window_util.h" 18#include "ui/aura/client/aura_constants.h" 19#include "ui/aura/root_window.h" 20#include "ui/aura/test/test_windows.h" 21#include "ui/aura/window.h" 22#include "ui/gfx/insets.h" 23#include "ui/views/widget/widget.h" 24#include "ui/views/widget/widget_delegate.h" 25 26namespace ash { 27namespace { 28 29class MaximizeDelegateView : public views::WidgetDelegateView { 30 public: 31 MaximizeDelegateView(const gfx::Rect& initial_bounds) 32 : initial_bounds_(initial_bounds) { 33 } 34 virtual ~MaximizeDelegateView() {} 35 36 virtual bool GetSavedWindowPlacement( 37 const views::Widget* widget, 38 gfx::Rect* bounds, 39 ui::WindowShowState* show_state) const OVERRIDE { 40 *bounds = initial_bounds_; 41 *show_state = ui::SHOW_STATE_MAXIMIZED; 42 return true; 43 } 44 45 private: 46 const gfx::Rect initial_bounds_; 47 48 DISALLOW_COPY_AND_ASSIGN(MaximizeDelegateView); 49}; 50 51class TestShellObserver : public ShellObserver { 52 public: 53 TestShellObserver() : call_count_(0), 54 is_fullscreen_(false) { 55 Shell::GetInstance()->AddShellObserver(this); 56 } 57 58 virtual ~TestShellObserver() { 59 Shell::GetInstance()->RemoveShellObserver(this); 60 } 61 62 virtual void OnFullscreenStateChanged(bool is_fullscreen, 63 aura::Window* root_window) OVERRIDE { 64 call_count_++; 65 is_fullscreen_ = is_fullscreen; 66 } 67 68 int call_count() const { 69 return call_count_; 70 } 71 72 bool is_fullscreen() const { 73 return is_fullscreen_; 74 } 75 76 private: 77 int call_count_; 78 bool is_fullscreen_; 79 80 DISALLOW_COPY_AND_ASSIGN(TestShellObserver); 81}; 82 83} // namespace 84 85typedef test::AshTestBase WorkspaceLayoutManagerTest; 86 87// Verifies that a window containing a restore coordinate will be restored to 88// to the size prior to minimize, keeping the restore rectangle in tact (if 89// there is one). 90TEST_F(WorkspaceLayoutManagerTest, RestoreFromMinimizeKeepsRestore) { 91 scoped_ptr<aura::Window> window( 92 CreateTestWindowInShellWithBounds(gfx::Rect(1, 2, 3, 4))); 93 gfx::Rect bounds(10, 15, 25, 35); 94 window->SetBounds(bounds); 95 96 wm::WindowState* window_state = wm::GetWindowState(window.get()); 97 98 // This will not be used for un-minimizing window. 99 window_state->SetRestoreBoundsInScreen(gfx::Rect(0, 0, 100, 100)); 100 window_state->Minimize(); 101 window_state->Restore(); 102 EXPECT_EQ("0,0 100x100", window_state->GetRestoreBoundsInScreen().ToString()); 103 EXPECT_EQ("10,15 25x35", window.get()->bounds().ToString()); 104 105 if (!SupportsMultipleDisplays()) 106 return; 107 108 UpdateDisplay("400x300,500x400"); 109 window->SetBoundsInScreen(gfx::Rect(600, 0, 100, 100), 110 ScreenAsh::GetSecondaryDisplay()); 111 EXPECT_EQ(Shell::GetAllRootWindows()[1], window->GetRootWindow()); 112 window_state->Minimize(); 113 // This will not be used for un-minimizing window. 114 window_state->SetRestoreBoundsInScreen(gfx::Rect(0, 0, 100, 100)); 115 window_state->Restore(); 116 EXPECT_EQ("600,0 100x100", window->GetBoundsInScreen().ToString()); 117 118 // Make sure the unminimized window moves inside the display when 119 // 2nd display is disconnected. 120 window_state->Minimize(); 121 UpdateDisplay("400x300"); 122 window_state->Restore(); 123 EXPECT_EQ(Shell::GetPrimaryRootWindow(), window->GetRootWindow()); 124 EXPECT_TRUE( 125 Shell::GetPrimaryRootWindow()->bounds().Intersects(window->bounds())); 126} 127 128TEST_F(WorkspaceLayoutManagerTest, KeepMinimumVisibilityInDisplays) { 129 if (!SupportsMultipleDisplays()) 130 return; 131 132 UpdateDisplay("300x400,400x500"); 133 aura::Window::Windows root_windows = Shell::GetAllRootWindows(); 134 135 DisplayLayout layout(DisplayLayout::TOP, 0); 136 Shell::GetInstance()->display_manager()-> 137 SetLayoutForCurrentDisplays(layout); 138 EXPECT_EQ("0,-500 400x500", root_windows[1]->GetBoundsInScreen().ToString()); 139 140 scoped_ptr<aura::Window> window1( 141 CreateTestWindowInShellWithBounds(gfx::Rect(10, -400, 200, 200))); 142 EXPECT_EQ("10,-400 200x200", window1->GetBoundsInScreen().ToString()); 143 144 // Make sure the caption is visible. 145 scoped_ptr<aura::Window> window2( 146 CreateTestWindowInShellWithBounds(gfx::Rect(10, -600, 200, 200))); 147 EXPECT_EQ("10,-500 200x200", window2->GetBoundsInScreen().ToString()); 148} 149 150TEST_F(WorkspaceLayoutManagerTest, KeepRestoredWindowInDisplay) { 151 if (!SupportsHostWindowResize()) 152 return; 153 scoped_ptr<aura::Window> window( 154 CreateTestWindowInShellWithBounds(gfx::Rect(1, 2, 30, 40))); 155 wm::WindowState* window_state = wm::GetWindowState(window.get()); 156 157 // Maximized -> Normal transition. 158 window_state->Maximize(); 159 window_state->SetRestoreBoundsInScreen(gfx::Rect(-100, -100, 30, 40)); 160 window_state->Restore(); 161 EXPECT_TRUE( 162 Shell::GetPrimaryRootWindow()->bounds().Intersects(window->bounds())); 163 // Y bounds should not be negative. 164 EXPECT_EQ("-20,0 30x40", window->bounds().ToString()); 165 166 // Minimized -> Normal transition. 167 window->SetBounds(gfx::Rect(-100, -100, 30, 40)); 168 window_state->Minimize(); 169 EXPECT_FALSE( 170 Shell::GetPrimaryRootWindow()->bounds().Intersects(window->bounds())); 171 EXPECT_EQ("-100,-100 30x40", window->bounds().ToString()); 172 window->Show(); 173 EXPECT_TRUE( 174 Shell::GetPrimaryRootWindow()->bounds().Intersects(window->bounds())); 175 // Y bounds should not be negative. 176 EXPECT_EQ("-20,0 30x40", window->bounds().ToString()); 177 178 // Fullscreen -> Normal transition. 179 window->SetBounds(gfx::Rect(0, 0, 30, 40)); // reset bounds. 180 ASSERT_EQ("0,0 30x40", window->bounds().ToString()); 181 window->SetProperty(aura::client::kShowStateKey, ui::SHOW_STATE_FULLSCREEN); 182 EXPECT_EQ(window->bounds(), window->GetRootWindow()->bounds()); 183 window_state->SetRestoreBoundsInScreen(gfx::Rect(-100, -100, 30, 40)); 184 window_state->Restore(); 185 EXPECT_TRUE( 186 Shell::GetPrimaryRootWindow()->bounds().Intersects(window->bounds())); 187 // Y bounds should not be negative. 188 EXPECT_EQ("-20,0 30x40", window->bounds().ToString()); 189} 190 191TEST_F(WorkspaceLayoutManagerTest, MaximizeInDisplayToBeRestored) { 192 if (!SupportsMultipleDisplays()) 193 return; 194 UpdateDisplay("300x400,400x500"); 195 196 aura::Window::Windows root_windows = Shell::GetAllRootWindows(); 197 198 scoped_ptr<aura::Window> window( 199 CreateTestWindowInShellWithBounds(gfx::Rect(1, 2, 30, 40))); 200 EXPECT_EQ(root_windows[0], window->GetRootWindow()); 201 202 wm::WindowState* window_state = wm::GetWindowState(window.get()); 203 window_state->SetRestoreBoundsInScreen(gfx::Rect(400, 0, 30, 40)); 204 // Maximize the window in 2nd display as the restore bounds 205 // is inside 2nd display. 206 window_state->Maximize(); 207 EXPECT_EQ(root_windows[1], window->GetRootWindow()); 208 EXPECT_EQ("300,0 400x453", window->GetBoundsInScreen().ToString()); 209 210 window_state->Restore(); 211 EXPECT_EQ(root_windows[1], window->GetRootWindow()); 212 EXPECT_EQ("400,0 30x40", window->GetBoundsInScreen().ToString()); 213 214 // If the restore bounds intersects with the current display, 215 // don't move. 216 window_state->SetRestoreBoundsInScreen(gfx::Rect(280, 0, 30, 40)); 217 window_state->Maximize(); 218 EXPECT_EQ(root_windows[1], window->GetRootWindow()); 219 EXPECT_EQ("300,0 400x453", window->GetBoundsInScreen().ToString()); 220 221 window_state->Restore(); 222 EXPECT_EQ(root_windows[1], window->GetRootWindow()); 223 EXPECT_EQ("280,0 30x40", window->GetBoundsInScreen().ToString()); 224 225 // Restoring widget state. 226 scoped_ptr<views::Widget> w1(new views::Widget); 227 views::Widget::InitParams params; 228 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 229 params.delegate = new MaximizeDelegateView(gfx::Rect(400, 0, 30, 40)); 230 params.context = root_windows[0]; 231 w1->Init(params); 232 w1->Show(); 233 EXPECT_TRUE(w1->IsMaximized()); 234 EXPECT_EQ(root_windows[1], w1->GetNativeView()->GetRootWindow()); 235 EXPECT_EQ("300,0 400x453", w1->GetWindowBoundsInScreen().ToString()); 236 w1->Restore(); 237 EXPECT_EQ(root_windows[1], w1->GetNativeView()->GetRootWindow()); 238 EXPECT_EQ("400,0 30x40", w1->GetWindowBoundsInScreen().ToString()); 239} 240 241TEST_F(WorkspaceLayoutManagerTest, FullscreenInDisplayToBeRestored) { 242 if (!SupportsMultipleDisplays()) 243 return; 244 UpdateDisplay("300x400,400x500"); 245 246 aura::Window::Windows root_windows = Shell::GetAllRootWindows(); 247 248 scoped_ptr<aura::Window> window( 249 CreateTestWindowInShellWithBounds(gfx::Rect(1, 2, 30, 40))); 250 EXPECT_EQ(root_windows[0], window->GetRootWindow()); 251 252 wm::WindowState* window_state = wm::GetWindowState(window.get()); 253 window_state->SetRestoreBoundsInScreen(gfx::Rect(400, 0, 30, 40)); 254 // Maximize the window in 2nd display as the restore bounds 255 // is inside 2nd display. 256 window->SetProperty(aura::client::kShowStateKey, 257 ui::SHOW_STATE_FULLSCREEN); 258 EXPECT_EQ(root_windows[1], window->GetRootWindow()); 259 EXPECT_EQ("300,0 400x500", window->GetBoundsInScreen().ToString()); 260 261 window_state->Restore(); 262 EXPECT_EQ(root_windows[1], window->GetRootWindow()); 263 EXPECT_EQ("400,0 30x40", window->GetBoundsInScreen().ToString()); 264 265 // If the restore bounds intersects with the current display, 266 // don't move. 267 window_state->SetRestoreBoundsInScreen(gfx::Rect(280, 0, 30, 40)); 268 window->SetProperty(aura::client::kShowStateKey, 269 ui::SHOW_STATE_FULLSCREEN); 270 EXPECT_EQ(root_windows[1], window->GetRootWindow()); 271 EXPECT_EQ("300,0 400x500", window->GetBoundsInScreen().ToString()); 272 273 window_state->Restore(); 274 EXPECT_EQ(root_windows[1], window->GetRootWindow()); 275 EXPECT_EQ("280,0 30x40", window->GetBoundsInScreen().ToString()); 276} 277 278// WindowObserver implementation used by DontClobberRestoreBoundsWindowObserver. 279// This code mirrors what BrowserFrameAsh does. In particular when this code 280// sees the window was maximized it changes the bounds of a secondary 281// window. The secondary window mirrors the status window. 282class DontClobberRestoreBoundsWindowObserver : public aura::WindowObserver { 283 public: 284 DontClobberRestoreBoundsWindowObserver() : window_(NULL) {} 285 286 void set_window(aura::Window* window) { window_ = window; } 287 288 virtual void OnWindowPropertyChanged(aura::Window* window, 289 const void* key, 290 intptr_t old) OVERRIDE { 291 if (!window_) 292 return; 293 294 if (wm::GetWindowState(window)->IsMaximized()) { 295 aura::Window* w = window_; 296 window_ = NULL; 297 298 gfx::Rect shelf_bounds(Shell::GetPrimaryRootWindowController()-> 299 GetShelfLayoutManager()->GetIdealBounds()); 300 const gfx::Rect& window_bounds(w->bounds()); 301 w->SetBounds(gfx::Rect(window_bounds.x(), shelf_bounds.y() - 1, 302 window_bounds.width(), window_bounds.height())); 303 } 304 } 305 306 private: 307 aura::Window* window_; 308 309 DISALLOW_COPY_AND_ASSIGN(DontClobberRestoreBoundsWindowObserver); 310}; 311 312// Creates a window, maximized the window and from within the maximized 313// notification sets the bounds of a window to overlap the shelf. Verifies this 314// doesn't effect the restore bounds. 315TEST_F(WorkspaceLayoutManagerTest, DontClobberRestoreBounds) { 316 DontClobberRestoreBoundsWindowObserver window_observer; 317 scoped_ptr<aura::Window> window(new aura::Window(NULL)); 318 window->SetType(aura::client::WINDOW_TYPE_NORMAL); 319 window->Init(ui::LAYER_TEXTURED); 320 window->SetBounds(gfx::Rect(10, 20, 30, 40)); 321 // NOTE: for this test to exercise the failure the observer needs to be added 322 // before the parent set. This mimics what BrowserFrameAsh does. 323 window->AddObserver(&window_observer); 324 ParentWindowInPrimaryRootWindow(window.get()); 325 window->Show(); 326 327 wm::WindowState* window_state = wm::GetWindowState(window.get()); 328 window_state->Activate(); 329 330 scoped_ptr<aura::Window> window2( 331 CreateTestWindowInShellWithBounds(gfx::Rect(12, 20, 30, 40))); 332 window->AddTransientChild(window2.get()); 333 window2->Show(); 334 335 window_observer.set_window(window2.get()); 336 window_state->Maximize(); 337 EXPECT_EQ("10,20 30x40", 338 window_state->GetRestoreBoundsInScreen().ToString()); 339 window->RemoveObserver(&window_observer); 340} 341 342// Verifies when a window is maximized all descendant windows have a size. 343TEST_F(WorkspaceLayoutManagerTest, ChildBoundsResetOnMaximize) { 344 scoped_ptr<aura::Window> window( 345 CreateTestWindowInShellWithBounds(gfx::Rect(10, 20, 30, 40))); 346 window->Show(); 347 wm::WindowState* window_state = wm::GetWindowState(window.get()); 348 window_state->Activate(); 349 scoped_ptr<aura::Window> child_window( 350 aura::test::CreateTestWindowWithBounds(gfx::Rect(5, 6, 7, 8), 351 window.get())); 352 child_window->Show(); 353 window_state->Maximize(); 354 EXPECT_EQ("5,6 7x8", child_window->bounds().ToString()); 355} 356 357TEST_F(WorkspaceLayoutManagerTest, WindowShouldBeOnScreenWhenAdded) { 358 // Normal window bounds shouldn't be changed. 359 gfx::Rect window_bounds(100, 100, 200, 200); 360 scoped_ptr<aura::Window> window( 361 CreateTestWindowInShellWithBounds(window_bounds)); 362 EXPECT_EQ(window_bounds, window->bounds()); 363 364 // If the window is out of the workspace, it would be moved on screen. 365 gfx::Rect root_window_bounds = 366 Shell::GetInstance()->GetPrimaryRootWindow()->bounds(); 367 window_bounds.Offset(root_window_bounds.width(), root_window_bounds.height()); 368 ASSERT_FALSE(window_bounds.Intersects(root_window_bounds)); 369 scoped_ptr<aura::Window> out_window( 370 CreateTestWindowInShellWithBounds(window_bounds)); 371 EXPECT_EQ(window_bounds.size(), out_window->bounds().size()); 372 gfx::Rect bounds = out_window->bounds(); 373 bounds.Intersect(root_window_bounds); 374 375 // 30% of the window edge must be visible. 376 EXPECT_GT(bounds.width(), out_window->bounds().width() * 0.29); 377 EXPECT_GT(bounds.height(), out_window->bounds().height() * 0.29); 378 379 aura::Window* parent = out_window->parent(); 380 parent->RemoveChild(out_window.get()); 381 out_window->SetBounds(gfx::Rect(-200, -200, 200, 200)); 382 // UserHasChangedWindowPositionOrSize flag shouldn't turn off this behavior. 383 wm::GetWindowState(window.get())->set_bounds_changed_by_user(true); 384 parent->AddChild(out_window.get()); 385 EXPECT_GT(bounds.width(), out_window->bounds().width() * 0.29); 386 EXPECT_GT(bounds.height(), out_window->bounds().height() * 0.29); 387 388 // Make sure we always make more than 1/3 of the window edge visible even 389 // if the initial bounds intersects with display. 390 window_bounds.SetRect(-150, -150, 200, 200); 391 bounds = window_bounds; 392 bounds.Intersect(root_window_bounds); 393 394 // Make sure that the initial bounds' visible area is less than 26% 395 // so that the auto adjustment logic kicks in. 396 ASSERT_LT(bounds.width(), out_window->bounds().width() * 0.26); 397 ASSERT_LT(bounds.height(), out_window->bounds().height() * 0.26); 398 ASSERT_TRUE(window_bounds.Intersects(root_window_bounds)); 399 400 scoped_ptr<aura::Window> partially_out_window( 401 CreateTestWindowInShellWithBounds(window_bounds)); 402 EXPECT_EQ(window_bounds.size(), partially_out_window->bounds().size()); 403 bounds = partially_out_window->bounds(); 404 bounds.Intersect(root_window_bounds); 405 EXPECT_GT(bounds.width(), out_window->bounds().width() * 0.29); 406 EXPECT_GT(bounds.height(), out_window->bounds().height() * 0.29); 407 408 // Make sure the window whose 30% width/height is bigger than display 409 // will be placed correctly. 410 window_bounds.SetRect(-1900, -1900, 3000, 3000); 411 scoped_ptr<aura::Window> window_bigger_than_display( 412 CreateTestWindowInShellWithBounds(window_bounds)); 413 EXPECT_GE(root_window_bounds.width(), 414 window_bigger_than_display->bounds().width()); 415 EXPECT_GE(root_window_bounds.height(), 416 window_bigger_than_display->bounds().height()); 417 418 bounds = window_bigger_than_display->bounds(); 419 bounds.Intersect(root_window_bounds); 420 EXPECT_GT(bounds.width(), out_window->bounds().width() * 0.29); 421 EXPECT_GT(bounds.height(), out_window->bounds().height() * 0.29); 422} 423 424// Verifies the size of a window is enforced to be smaller than the work area. 425TEST_F(WorkspaceLayoutManagerTest, SizeToWorkArea) { 426 // Normal window bounds shouldn't be changed. 427 gfx::Size work_area( 428 Shell::GetScreen()->GetPrimaryDisplay().work_area().size()); 429 const gfx::Rect window_bounds( 430 100, 101, work_area.width() + 1, work_area.height() + 2); 431 scoped_ptr<aura::Window> window( 432 CreateTestWindowInShellWithBounds(window_bounds)); 433 EXPECT_EQ(gfx::Rect(gfx::Point(100, 101), work_area).ToString(), 434 window->bounds().ToString()); 435 436 // Directly setting the bounds triggers a slightly different code path. Verify 437 // that too. 438 window->SetBounds(window_bounds); 439 EXPECT_EQ(gfx::Rect(gfx::Point(100, 101), work_area).ToString(), 440 window->bounds().ToString()); 441} 442 443TEST_F(WorkspaceLayoutManagerTest, NotifyFullscreenChanges) { 444 TestShellObserver observer; 445 scoped_ptr<aura::Window> window1( 446 CreateTestWindowInShellWithBounds(gfx::Rect(1, 2, 30, 40))); 447 scoped_ptr<aura::Window> window2( 448 CreateTestWindowInShellWithBounds(gfx::Rect(1, 2, 30, 40))); 449 wm::WindowState* window_state1 = wm::GetWindowState(window1.get()); 450 wm::WindowState* window_state2 = wm::GetWindowState(window2.get()); 451 window_state2->Activate(); 452 453 window_state2->ToggleFullscreen(); 454 EXPECT_EQ(1, observer.call_count()); 455 EXPECT_TRUE(observer.is_fullscreen()); 456 457 // When window1 moves to the front the fullscreen state should change. 458 window_state1->Activate(); 459 EXPECT_EQ(2, observer.call_count()); 460 EXPECT_FALSE(observer.is_fullscreen()); 461 462 // It should change back if window2 becomes active again. 463 window_state2->Activate(); 464 EXPECT_EQ(3, observer.call_count()); 465 EXPECT_TRUE(observer.is_fullscreen()); 466 467 window_state2->ToggleFullscreen(); 468 EXPECT_EQ(4, observer.call_count()); 469 EXPECT_FALSE(observer.is_fullscreen()); 470 471 window_state2->ToggleFullscreen(); 472 EXPECT_EQ(5, observer.call_count()); 473 EXPECT_TRUE(observer.is_fullscreen()); 474 475 // Closing the window should change the fullscreen state. 476 window2.reset(); 477 EXPECT_EQ(6, observer.call_count()); 478 EXPECT_FALSE(observer.is_fullscreen()); 479} 480 481} // namespace ash 482