Commit b156a956 authored by varkha@chromium.org's avatar varkha@chromium.org

Implement automatic layout and stacking for docked windows.

BUG=233334

Review URL: https://chromiumcodereview.appspot.com/19054013

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@213380 0039d316-1c4b-4281-b951-d872f2087c98
parent 4b2a146b
......@@ -29,6 +29,7 @@ namespace internal {
const int DockedWindowLayoutManager::kMinDockWidth = 200;
const int DockedWindowLayoutManager::kMaxDockWidth = 450;
const int DockedWindowLayoutManager::kMinDockGap = 2;
const int kWindowIdealSpacing = 4;
namespace {
......@@ -49,6 +50,11 @@ bool IsUsedByLayout(aura::Window* window) {
window->type() != aura::client::WINDOW_TYPE_POPUP);
}
bool CompareWindowPos(const aura::Window* win1, const aura::Window* win2) {
return win1->GetTargetBounds().CenterPoint().y() <
win2->GetTargetBounds().CenterPoint().y();
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
......@@ -62,7 +68,8 @@ DockedWindowLayoutManager::DockedWindowLayoutManager(
shelf_layout_manager_(NULL),
shelf_hidden_(false),
docked_width_(0),
alignment_(DOCKED_ALIGNMENT_NONE) {
alignment_(DOCKED_ALIGNMENT_NONE),
last_active_window_(NULL) {
DCHECK(dock_container);
aura::client::GetActivationClient(Shell::GetPrimaryRootWindow())->
AddObserver(this);
......@@ -127,7 +134,13 @@ void DockedWindowLayoutManager::FinishDragging() {
if (!found_docked_window)
alignment_ = AlignmentOfWindow(dragged_window_);
}
dragged_window_ = NULL;
// We no longer need to observe |dragged_window_| unless it is added back;
if (dragged_window_) {
if (dragged_window_->parent() != dock_container_)
dragged_window_->RemoveObserver(this);
dragged_window_ = NULL;
}
Relayout();
UpdateDockBounds();
......@@ -226,7 +239,10 @@ void DockedWindowLayoutManager::OnWindowAddedToLayout(aura::Window* child) {
// If this is the first window getting docked - update alignment.
if (alignment_ == DOCKED_ALIGNMENT_NONE)
alignment_ = AlignmentOfWindow(child);
child->AddObserver(this);
// We need to observe a child unless it is a |dragged_window_| that just got
// added back in which case we are already observing it.
if (child != dragged_window_)
child->AddObserver(this);
Relayout();
UpdateDockBounds();
}
......@@ -248,7 +264,14 @@ void DockedWindowLayoutManager::OnWindowRemovedFromLayout(aura::Window* child) {
if (!found_docked_window)
alignment_ = DOCKED_ALIGNMENT_NONE;
child->RemoveObserver(this);
// Keep track of former children if they are dragged as they can be reparented
// temporarily just for the drag.
if (child != dragged_window_)
child->RemoveObserver(this);
if (last_active_window_ == child)
last_active_window_ = NULL;
// Close the dock and expand workspace work area.
Relayout();
......@@ -326,6 +349,20 @@ void DockedWindowLayoutManager::OnWindowPropertyChanged(aura::Window* window,
RestoreWindow(window);
}
void DockedWindowLayoutManager::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds) {
if (window == dragged_window_)
Relayout();
}
void DockedWindowLayoutManager::OnWindowDestroying(aura::Window* window) {
if (dragged_window_ == window)
dragged_window_ = NULL;
}
////////////////////////////////////////////////////////////////////////////////
// DockLayoutManager, aura::client::ActivationChangeObserver implementation:
......@@ -408,6 +445,7 @@ void DockedWindowLayoutManager::Relayout() {
gfx::Rect dock_bounds = dock_container_->bounds();
aura::Window* active_window = NULL;
aura::Window::Windows visible_windows;
docked_width_ = 0;
for (size_t i = 0; i < dock_container_->children().size(); ++i) {
aura::Window* window(dock_container_->children()[i]);
......@@ -423,35 +461,71 @@ void DockedWindowLayoutManager::Relayout() {
window->Hide();
continue;
}
if (window->HasFocus() ||
window->Contains(
aura::client::GetFocusClient(window)->GetFocusedWindow())) {
DCHECK(!active_window);
active_window = window;
}
visible_windows.push_back(window);
}
// all docked windows other than the one currently dragged remain stuck
// to the screen edge
// Consider windows that were formerly children of the |dock_container_|
// when fanning out other child windows.
if (dragged_window_ && dragged_window_->parent() != dock_container_)
visible_windows.push_back(dragged_window_);
// Sort windows by their center positions and fan out overlapping
// windows.
std::sort(visible_windows.begin(), visible_windows.end(), CompareWindowPos);
gfx::Display display = Shell::GetScreen()->GetDisplayNearestWindow(
dock_container_);
const gfx::Rect work_area = display.work_area();
int available_room = work_area.height();
for (aura::Window::Windows::const_iterator iter = visible_windows.begin();
iter != visible_windows.end(); ++iter) {
available_room -= (*iter)->bounds().height();
}
const int num_windows = visible_windows.size();
const float delta = (float)available_room /
((available_room > 0 || num_windows <= 1) ?
num_windows + 1 : num_windows - 1);
float y_pos = (available_room > 0) ? delta : 0;
for (aura::Window::Windows::const_iterator iter = visible_windows.begin();
iter != visible_windows.end(); ++iter) {
aura::Window* window = *iter;
gfx::Rect bounds = window->GetTargetBounds();
if (window != dragged_window_) {
switch (alignment_) {
case DOCKED_ALIGNMENT_LEFT:
bounds.set_x(0);
break;
case DOCKED_ALIGNMENT_RIGHT:
bounds.set_x(dock_bounds.right() - bounds.width());
break;
case DOCKED_ALIGNMENT_NONE:
NOTREACHED() << "Relayout called when dock alignment is 'NONE'";
break;
}
// Keep the dock at least kMinDockWidth when all windows in it overhang.
docked_width_ = std::min(kMaxDockWidth,
std::max(docked_width_,
bounds.width() > kMaxDockWidth ?
kMinDockWidth : bounds.width()));
// Do not force position of a single window.
if (num_windows == 1)
y_pos = bounds.y();
// Fan out windows evenly distributing the overlap or remaining free space.
bounds.set_y(std::max(work_area.y(),
std::min(work_area.bottom() - bounds.height(),
static_cast<int>(y_pos + 0.5))));
y_pos += bounds.height() + delta;
// All docked windows other than the one currently dragged remain stuck
// to the screen edge.
if (window == dragged_window_)
continue;
switch (alignment_) {
case DOCKED_ALIGNMENT_LEFT:
bounds.set_x(0);
break;
case DOCKED_ALIGNMENT_RIGHT:
bounds.set_x(dock_bounds.right() - bounds.width());
break;
case DOCKED_ALIGNMENT_NONE:
NOTREACHED() << "Relayout called when dock alignment is 'NONE'";
break;
}
// Keep the dock at least kMinDockWidth when all windows in it overhang.
docked_width_ = std::min(kMaxDockWidth,
std::max(docked_width_,
bounds.width() > kMaxDockWidth ?
kMinDockWidth : bounds.width()));
SetChildBoundsDirect(window, bounds);
}
UpdateStacking(active_window);
......@@ -475,8 +549,57 @@ void DockedWindowLayoutManager::UpdateDockBounds() {
}
void DockedWindowLayoutManager::UpdateStacking(aura::Window* active_window) {
// TODO(varkha): Implement restacking to ensure that all docked windows are at
// least partially visible and selectable.
if (!active_window) {
if (!last_active_window_)
return;
active_window = last_active_window_;
}
// We want to to stack the windows like a deck of cards:
// ,------.
// |,------.|
// |,------.|
// | active |
// | window |
// |`------'|
// |`------'|
// `------'
// We use the middle of each window to figure out how to stack the window.
// This allows us to update the stacking when a window is being dragged around
// by the titlebar.
std::map<int, aura::Window*> window_ordering;
for (aura::Window::Windows::const_iterator it =
dock_container_->children().begin();
it != dock_container_->children().end(); ++it) {
gfx::Rect bounds = (*it)->bounds();
window_ordering.insert(std::make_pair(bounds.y() + bounds.height() / 2,
*it));
}
int active_center_y = active_window->bounds().CenterPoint().y();
aura::Window* previous_window = NULL;
for (std::map<int, aura::Window*>::const_iterator it =
window_ordering.begin();
it != window_ordering.end() && it->first < active_center_y; ++it) {
if (previous_window)
dock_container_->StackChildAbove(it->second, previous_window);
previous_window = it->second;
}
previous_window = NULL;
for (std::map<int, aura::Window*>::const_reverse_iterator it =
window_ordering.rbegin();
it != window_ordering.rend() && it->first > active_center_y; ++it) {
if (previous_window)
dock_container_->StackChildAbove(it->second, previous_window);
previous_window = it->second;
}
if (active_window->parent() == dock_container_)
dock_container_->StackChildAtTop(active_window);
if (dragged_window_ && dragged_window_->parent() == dock_container_)
dock_container_->StackChildAtTop(dragged_window_);
last_active_window_ = active_window;
}
////////////////////////////////////////////////////////////////////////////////
......
......@@ -103,6 +103,10 @@ class ASH_EXPORT DockedWindowLayoutManager
virtual void OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) OVERRIDE;
virtual void OnWindowBoundsChanged(aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds) OVERRIDE;
virtual void OnWindowDestroying(aura::Window* window) OVERRIDE;
// aura::client::ActivationChangeObserver:
virtual void OnWindowActivated(aura::Window* gained_active,
......@@ -174,6 +178,10 @@ class ASH_EXPORT DockedWindowLayoutManager
// Side of the screen that the dock is positioned at.
DockedAlignment alignment_;
// The last active window. Used to maintain stacking order even if no windows
// are currently focused.
aura::Window* last_active_window_;
// Observers of dock bounds changes.
ObserverList<DockedWindowLayoutManagerObserver> observer_list_;
......
......@@ -8,6 +8,7 @@
#include "ash/launcher/launcher.h"
#include "ash/launcher/launcher_model.h"
#include "ash/root_window_controller.h"
#include "ash/screen_ash.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shelf/shelf_types.h"
#include "ash/shelf/shelf_widget.h"
......@@ -44,7 +45,7 @@ class DockedWindowLayoutManagerTest
CommandLine::ForCurrentProcess()->AppendSwitch(
ash::switches::kAshEnableDockedWindows);
AshTestBase::SetUp();
UpdateDisplay("600x400");
UpdateDisplay("600x600");
ASSERT_TRUE(test::TestLauncherDelegate::instance());
launcher_view_test_.reset(new test::LauncherViewTestAPI(
......@@ -154,6 +155,20 @@ class DockedWindowLayoutManagerTest
window_type_ == aura::client::WINDOW_TYPE_PANEL ? -100 : 20);
}
void DragToVerticalPositionAndToEdge(DockedEdge edge,
aura::Window* window,
int y) {
DragToVerticalPositionRelativeToEdge(edge, window, 0, y);
}
void DragToVerticalPositionRelativeToEdge(DockedEdge edge,
aura::Window* window,
int dx,
int y) {
gfx::Rect initial_bounds = window->GetBoundsInScreen();
DragVerticallyAndRelativeToEdge(edge, window, dx, y - initial_bounds.y());
}
// Detach if our window is a panel, then drag it vertically by |dy| and
// horizontally to the edge with an added offset from the edge of |dx|.
void DragVerticallyAndRelativeToEdge(DockedEdge edge,
......@@ -184,7 +199,7 @@ class DockedWindowLayoutManagerTest
}
// avoid snap by clicking away from the border
ASSERT_NO_FATAL_FAILURE(DragStartAtOffsetFromwindowOrigin(window, 5, 5));
ASSERT_NO_FATAL_FAILURE(DragStartAtOffsetFromwindowOrigin(window, 25, 5));
// Drag the window left or right to the edge (or almost to it).
if (edge == DOCKED_EDGE_LEFT)
......@@ -197,8 +212,9 @@ class DockedWindowLayoutManagerTest
DragEnd();
// x-coordinate can get adjusted by snapping or sticking.
// y-coordinate should not change by possible docking.
EXPECT_EQ(initial_bounds.y() + dy, window->GetBoundsInScreen().y());
// y-coordinate could be changed by possible automatic layout if docked.
if (window->parent()->id() != internal::kShellWindowId_DockedContainer)
EXPECT_EQ(initial_bounds.y() + dy, window->GetBoundsInScreen().y());
}
private:
......@@ -228,8 +244,124 @@ TEST_P(DockedWindowLayoutManagerTest, AddOneWindow) {
EXPECT_EQ(internal::kShellWindowId_DockedContainer, window->parent()->id());
}
//TODO(varkha): Add more tests for fanning windows in the dock.
// See http://crbug.com/233334.
// Adds two windows and tests that the gaps are evenly distributed.
TEST_P(DockedWindowLayoutManagerTest, AddTwoWindows) {
if (!SupportsHostWindowResize())
return;
scoped_ptr<aura::Window> w1(CreateTestWindow(gfx::Rect(0, 0, 201, 201)));
scoped_ptr<aura::Window> w2(CreateTestWindow(gfx::Rect(0, 0, 210, 202)));
DragToVerticalPositionAndToEdge(DOCKED_EDGE_RIGHT, w1.get(), 20);
DragToVerticalPositionAndToEdge(DOCKED_EDGE_RIGHT, w2.get(), 300);
// The windows should be attached and snapped to the right side of the screen.
EXPECT_EQ(w1->GetRootWindow()->bounds().right(),
w1->GetBoundsInScreen().right());
EXPECT_EQ(internal::kShellWindowId_DockedContainer, w1->parent()->id());
EXPECT_EQ(w2->GetRootWindow()->bounds().right(),
w2->GetBoundsInScreen().right());
EXPECT_EQ(internal::kShellWindowId_DockedContainer, w2->parent()->id());
// Test that the gaps differ at most by a single pixel.
int gap1 = w1->GetBoundsInScreen().y();
int gap2 = w2->GetBoundsInScreen().y() - w1->GetBoundsInScreen().bottom();
int gap3 = ScreenAsh::GetDisplayWorkAreaBoundsInParent(w1.get()).bottom() -
w2->GetBoundsInScreen().bottom();
EXPECT_LE(abs(gap1 - gap2), 1);
EXPECT_LE(abs(gap2 - gap3), 1);
EXPECT_LE(abs(gap3 - gap1), 1);
}
// Adds two non-overlapping windows and tests layout after a drag.
TEST_P(DockedWindowLayoutManagerTest, TwoWindowsDragging) {
if (!SupportsHostWindowResize())
return;
scoped_ptr<aura::Window> w1(CreateTestWindow(gfx::Rect(0, 0, 201, 201)));
scoped_ptr<aura::Window> w2(CreateTestWindow(gfx::Rect(0, 0, 210, 202)));
DragToVerticalPositionAndToEdge(DOCKED_EDGE_RIGHT, w1.get(), 20);
DragToVerticalPositionAndToEdge(DOCKED_EDGE_RIGHT, w2.get(), 300);
// The windows should be attached and snapped to the right side of the screen.
EXPECT_EQ(w1->GetRootWindow()->bounds().right(),
w1->GetBoundsInScreen().right());
EXPECT_EQ(internal::kShellWindowId_DockedContainer, w1->parent()->id());
EXPECT_EQ(w2->GetRootWindow()->bounds().right(),
w2->GetBoundsInScreen().right());
EXPECT_EQ(internal::kShellWindowId_DockedContainer, w2->parent()->id());
// Drag w2 above w1.
ASSERT_NO_FATAL_FAILURE(DragStartAtOffsetFromwindowOrigin(w2.get(), 0, 20));
DragMove(0, w1->bounds().y() - w2->bounds().y() - 20);
DragEnd();
// Test the new windows order and that the gaps differ at most by a pixel.
int gap1 = w2->GetBoundsInScreen().y();
int gap2 = w1->GetBoundsInScreen().y() - w2->GetBoundsInScreen().bottom();
int gap3 = ScreenAsh::GetDisplayWorkAreaBoundsInParent(w1.get()).bottom() -
w1->GetBoundsInScreen().bottom();
EXPECT_LE(abs(gap1 - gap2), 1);
EXPECT_LE(abs(gap2 - gap3), 1);
EXPECT_LE(abs(gap3 - gap1), 1);
}
// Adds three overlapping windows and tests layout after a drag.
TEST_P(DockedWindowLayoutManagerTest, ThreeWindowsDragging) {
if (!SupportsHostWindowResize())
return;
scoped_ptr<aura::Window> w1(CreateTestWindow(gfx::Rect(0, 0, 201, 201)));
DragToVerticalPositionAndToEdge(DOCKED_EDGE_RIGHT, w1.get(), 20);
scoped_ptr<aura::Window> w2(CreateTestWindow(gfx::Rect(0, 0, 210, 202)));
DragToVerticalPositionAndToEdge(DOCKED_EDGE_RIGHT, w2.get(), 100);
scoped_ptr<aura::Window> w3(CreateTestWindow(gfx::Rect(0, 0, 220, 204)));
DragToVerticalPositionAndToEdge(DOCKED_EDGE_RIGHT, w3.get(), 300);
// All windows should be attached and snapped to the right side of the screen.
EXPECT_EQ(w1->GetRootWindow()->bounds().right(),
w1->GetBoundsInScreen().right());
EXPECT_EQ(internal::kShellWindowId_DockedContainer, w1->parent()->id());
EXPECT_EQ(w2->GetRootWindow()->bounds().right(),
w2->GetBoundsInScreen().right());
EXPECT_EQ(internal::kShellWindowId_DockedContainer, w2->parent()->id());
EXPECT_EQ(w3->GetRootWindow()->bounds().right(),
w3->GetBoundsInScreen().right());
EXPECT_EQ(internal::kShellWindowId_DockedContainer, w3->parent()->id());
// Test that the top and bottom windows are clamped in work area and
// that the overlaps between the windows differ at most by a pixel.
int overlap1 = w1->GetBoundsInScreen().y();
int overlap2 = w1->GetBoundsInScreen().bottom() - w2->GetBoundsInScreen().y();
int overlap3 = w2->GetBoundsInScreen().bottom() - w3->GetBoundsInScreen().y();
int overlap4 =
ScreenAsh::GetDisplayWorkAreaBoundsInParent(w3.get()).bottom() -
w3->GetBoundsInScreen().bottom();
EXPECT_EQ(0, overlap1);
EXPECT_LE(abs(overlap2 - overlap3), 1);
EXPECT_EQ(0, overlap4);
// Drag w1 below w2.
ASSERT_NO_FATAL_FAILURE(DragStartAtOffsetFromwindowOrigin(w1.get(), 0, 20));
DragMove(0, w2->bounds().y() - w1->bounds().y() + 20);
// During the drag the windows get rearranged and the top and the bottom
// should be clamped by the work area.
EXPECT_EQ(0, w2->GetBoundsInScreen().y());
EXPECT_GT(w1->GetBoundsInScreen().y(), w2->GetBoundsInScreen().y());
EXPECT_EQ(ScreenAsh::GetDisplayWorkAreaBoundsInParent(w3.get()).bottom(),
w3->GetBoundsInScreen().bottom());
DragEnd();
// Test the new windows order and that the overlaps differ at most by a pixel.
overlap1 = w2->GetBoundsInScreen().y();
overlap2 = w2->GetBoundsInScreen().bottom() - w1->GetBoundsInScreen().y();
overlap3 = w1->GetBoundsInScreen().bottom() - w3->GetBoundsInScreen().y();
overlap4 = ScreenAsh::GetDisplayWorkAreaBoundsInParent(w3.get()).bottom() -
w3->GetBoundsInScreen().bottom();
EXPECT_EQ(0, overlap1);
EXPECT_LE(abs(overlap2 - overlap3), 1);
EXPECT_EQ(0, overlap4);
}
// Tests run twice - on both panels and normal windows
INSTANTIATE_TEST_CASE_P(NormalOrPanel,
......
......@@ -224,8 +224,9 @@ class DockedWindowResizerTest
DragEnd();
// x-coordinate can get adjusted by snapping or sticking.
// y-coordinate should not change by possible docking.
EXPECT_EQ(initial_bounds.y() + dy, window->GetBoundsInScreen().y());
// y-coordinate could be changed by possible automatic layout if docked.
if (window->parent()->id() != internal::kShellWindowId_DockedContainer)
EXPECT_EQ(initial_bounds.y() + dy, window->GetBoundsInScreen().y());
}
bool test_panels() const {
......@@ -454,9 +455,10 @@ TEST_P(DockedWindowResizerTest, AttachTwoWindows)
w2->GetBoundsInScreen().right());
EXPECT_EQ(internal::kShellWindowId_DockedContainer, w2->parent()->id());
// Detach by dragging left (should get undocked)
// Detach by dragging left (should get undocked).
ASSERT_NO_FATAL_FAILURE(DragStart(w2.get()));
DragMove(-32, -10);
// Drag up as well to avoid attaching panels to launcher shelf.
DragMove(-32, -100);
// Release the mouse and the window should be no longer attached to the edge.
DragEnd();
......@@ -651,7 +653,8 @@ TEST_P(DockedWindowResizerTest, AttachTwoWindowsDetachOne)
w2.get(),
w2->bounds().width()/2 + 10,
0));
DragMove(-(w2->bounds().width()/2 + 20), 10);
// Drag up as well to avoid attaching panels to launcher shelf.
DragMove(-(w2->bounds().width()/2 + 20), -100);
// Release the mouse and the window should be no longer attached to the edge.
DragEnd();
......@@ -985,7 +988,8 @@ TEST_P(DockedWindowResizerTest, ResizeTwoWindows)
// Undock the second window. Docked area should shrink to its minimum size.
ASSERT_NO_FATAL_FAILURE(DragStart(w2.get()));
DragMove(-40, 10);
// Drag up as well to avoid attaching panels to launcher shelf.
DragMove(-40, -100);
// Alignment set to "RIGHT" since we have another window docked.
EXPECT_EQ(DOCKED_ALIGNMENT_RIGHT, manager->alignment_);
// Release the mouse and the window should be no longer attached to the edge.
......
......@@ -172,8 +172,8 @@ class ASH_EXPORT PanelLayoutManager
// Tracks the visibility of the shelf. Defaults to false when there is no
// shelf.
bool shelf_hidden_;
// The last active panel. Used to maintain stacking even if no panels are
// currently focused.
// The last active panel. Used to maintain stacking order even if no panels
// are currently focused.
aura::Window* last_active_panel_;
base::WeakPtrFactory<PanelLayoutManager> weak_factory_;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment