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