From 84f024c021c2a8e009026516483897546faf86fa Mon Sep 17 00:00:00 2001 From: "alekseys@chromium.org" Date: Wed, 12 Jan 2011 20:57:44 +0000 Subject: [PATCH] Streamline the layout of the BrowserView's children TabContents views. Modify SingleSplitView to calculate its children view's bounds, but do not actually resize them and change BrowserViewLayout accordingly (BrowserViewLayout resizes all views now). Do all reserved contents rect calculations before resizing TabContents views. Rationale: to do all BrowserView layout related actions in the context of BrowserViewLayout::Layout call and to minimize actual contents re-layouts. BUG=51084 TEST=All tests should pass Review URL: http://codereview.chromium.org/5606012 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@71230 0039d316-1c4b-4281-b951-d872f2087c98 --- chrome/browser/ui/views/frame/browser_view.cc | 70 +++---- chrome/browser/ui/views/frame/browser_view.h | 12 +- .../ui/views/frame/browser_view_layout.cc | 113 ++++++++++- .../ui/views/frame/browser_view_layout.h | 16 +- .../tab_contents/tab_contents_container.cc | 40 ++-- .../tab_contents/tab_contents_container.h | 28 +-- chrome/chrome_tests.gypi | 1 + views/controls/single_split_view.cc | 149 +++++++++------ views/controls/single_split_view.h | 58 +++++- views/controls/single_split_view_unittest.cc | 176 ++++++++++++++++++ views/examples/single_split_view_example.h | 3 +- views/views.gyp | 1 + 12 files changed, 510 insertions(+), 157 deletions(-) create mode 100644 views/controls/single_split_view_unittest.cc diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc index 199337427e0b..a52745b78ddc 100644 --- a/chrome/browser/ui/views/frame/browser_view.cc +++ b/chrome/browser/ui/views/frame/browser_view.cc @@ -1348,10 +1348,8 @@ void BrowserView::PrepareForInstant() { } void BrowserView::ShowInstant(TabContents* preview_contents) { - if (!preview_container_) { + if (!preview_container_) preview_container_ = new TabContentsContainer(); - preview_container_->set_reserved_area_delegate(this); - } contents_->SetPreview(preview_container_, preview_contents); preview_container_->ChangeTabContents(preview_contents); @@ -1825,41 +1823,12 @@ void BrowserView::InfoBarSizeChanged(bool is_animating) { SelectedTabToolbarSizeChanged(is_animating); } -void BrowserView::UpdateReservedContentsRect( - const TabContentsContainer* source) { - RenderWidgetHostView* render_widget_host_view = - source->tab_contents() ? source->tab_contents()->GetRenderWidgetHostView() - : NULL; - if (!render_widget_host_view) - return; - - gfx::Rect reserved_rect; - - if (!frame_->GetWindow()->IsMaximized() && - !frame_->GetWindow()->IsFullscreen()) { - gfx::Size resize_corner_size = ResizeCorner::GetSize(); - if (!resize_corner_size.IsEmpty()) { - gfx::Point resize_corner_origin; - gfx::Rect bounds = GetLocalBounds(false); - resize_corner_origin.set_x(bounds.right() - resize_corner_size.width()); - resize_corner_origin.set_y(bounds.bottom() - resize_corner_size.height()); - - View::ConvertPointToView(this, source, &resize_corner_origin); - - gfx::Size container_size = source->size(); - - if (resize_corner_origin.x() < container_size.width() && - resize_corner_origin.y() < container_size.height()) { - reserved_rect = gfx::Rect(resize_corner_origin, resize_corner_size); - } - } - } - - // TODO(alekseys): for source == contents_container_, consult SidebarTabView - // for the current size to reserve. Something like this: - // reserved_rect = reserved_rect.Union(SidebarTabView::GetCurrentBounds()); - - render_widget_host_view->set_reserved_contents_rect(reserved_rect); +bool BrowserView::SplitHandleMoved(views::SingleSplitView* view) { + for (int i = 0; i < view->GetChildViewCount(); ++i) + view->GetChildViewAt(i)->InvalidateLayout(); + SchedulePaint(); + Layout(); + return false; } views::LayoutManager* BrowserView::CreateLayoutManager() const { @@ -1922,7 +1891,6 @@ void BrowserView::Init() { AddChildView(infobar_container_); contents_container_ = new TabContentsContainer; - contents_container_->set_reserved_area_delegate(this); contents_ = new ContentsContainer(contents_container_); SkColor bg_color = GetWidget()->GetThemeProvider()-> @@ -1931,14 +1899,14 @@ void BrowserView::Init() { bool sidebar_allowed = SidebarManager::IsSidebarAllowed(); if (sidebar_allowed) { sidebar_container_ = new TabContentsContainer; - sidebar_container_->set_reserved_area_delegate(this); sidebar_container_->SetID(VIEW_ID_SIDE_BAR_CONTAINER); sidebar_container_->SetVisible(false); sidebar_split_ = new views::SingleSplitView( contents_, sidebar_container_, - views::SingleSplitView::HORIZONTAL_SPLIT); + views::SingleSplitView::HORIZONTAL_SPLIT, + this); sidebar_split_->SetID(VIEW_ID_SIDE_BAR_SPLIT); sidebar_split_->SetAccessibleName( UTF16ToWide(l10n_util::GetStringUTF16(IDS_ACCNAME_SIDE_BAR))); @@ -1947,7 +1915,6 @@ void BrowserView::Init() { } devtools_container_ = new TabContentsContainer; - devtools_container_->set_reserved_area_delegate(this); devtools_container_->SetID(VIEW_ID_DEV_TOOLS_DOCKED); devtools_container_->SetVisible(false); @@ -1958,7 +1925,8 @@ void BrowserView::Init() { contents_split_ = new views::SingleSplitView( contents_view, devtools_container_, - views::SingleSplitView::VERTICAL_SPLIT); + views::SingleSplitView::VERTICAL_SPLIT, + this); contents_split_->SetID(VIEW_ID_CONTENTS_SPLIT); contents_split_->SetAccessibleName( UTF16ToWide(l10n_util::GetStringUTF16(IDS_ACCNAME_WEB_CONTENTS))); @@ -2102,7 +2070,8 @@ void BrowserView::UpdateSidebarForContents(TabContentsWrapper* tab_contents) { sidebar_split_->width() - sidebar_width); sidebar_container_->SetVisible(true); - sidebar_split_->Layout(); + sidebar_split_->InvalidateLayout(); + Layout(); } else if (should_hide) { // Store split offset when hiding sidebar only. g_browser_process->local_state()->SetInteger( @@ -2110,7 +2079,8 @@ void BrowserView::UpdateSidebarForContents(TabContentsWrapper* tab_contents) { sidebar_split_->width() - sidebar_split_->divider_offset()); sidebar_container_->SetVisible(false); - sidebar_split_->Layout(); + sidebar_split_->InvalidateLayout(); + Layout(); } } @@ -2147,7 +2117,8 @@ void BrowserView::UpdateDevToolsForContents(TabContentsWrapper* wrapper) { contents_split_->set_divider_offset(split_offset); devtools_container_->SetVisible(true); - contents_split_->Layout(); + contents_split_->InvalidateLayout(); + Layout(); } else if (should_hide) { // Store split offset when hiding devtools window only. g_browser_process->local_state()->SetInteger( @@ -2157,7 +2128,8 @@ void BrowserView::UpdateDevToolsForContents(TabContentsWrapper* wrapper) { devtools_focus_tracker_->FocusLastFocusedExternalView(); devtools_container_->SetVisible(false); - contents_split_->Layout(); + contents_split_->InvalidateLayout(); + Layout(); } } @@ -2567,6 +2539,10 @@ void BrowserView::ProcessTabSelected(TabContentsWrapper* new_contents, UpdateUIForContents(new_contents); } +gfx::Size BrowserView::GetResizeCornerSize() const { + return ResizeCorner::GetSize(); +} + #if !defined(OS_CHROMEOS) // static BrowserWindow* BrowserWindow::CreateBrowserWindow(Browser* browser) { diff --git a/chrome/browser/ui/views/frame/browser_view.h b/chrome/browser/ui/views/frame/browser_view.h index 79b0859fe09c..1825d64ac016 100644 --- a/chrome/browser/ui/views/frame/browser_view.h +++ b/chrome/browser/ui/views/frame/browser_view.h @@ -26,6 +26,7 @@ #include "chrome/browser/ui/views/unhandled_keyboard_event_handler.h" #include "chrome/common/notification_registrar.h" #include "gfx/native_widget_types.h" +#include "views/controls/single_split_view.h" #include "views/window/client_view.h" #include "views/window/window_delegate.h" @@ -53,6 +54,7 @@ class InfoBarContainer; class LocationBarView; class SideTabStrip; class StatusBubbleViews; +class TabContentsContainer; class TabStripModel; class ToolbarView; class ZoomMenuModel; @@ -66,7 +68,6 @@ class JumpList; namespace views { class ExternalFocusTracker; class Menu; -class SingleSplitView; } /////////////////////////////////////////////////////////////////////////////// @@ -84,7 +85,7 @@ class BrowserView : public BrowserBubbleHost, public views::WindowDelegate, public views::ClientView, public InfoBarContainer::Delegate, - public TabContentsContainer::ReservedAreaDelegate { + public views::SingleSplitView::Observer { public: // The browser view's class name. static const char kViewClassName[]; @@ -396,8 +397,8 @@ class BrowserView : public BrowserBubbleHost, // InfoBarContainer::Delegate overrides virtual void InfoBarSizeChanged(bool is_animating); - // TabContentsContainer::ReservedAreaDelegate overrides. - virtual void UpdateReservedContentsRect(const TabContentsContainer* source); + // views::SingleSplitView::Observer overrides: + virtual bool SplitHandleMoved(views::SingleSplitView* view); protected: // Appends to |toolbars| a pointer to each AccessiblePaneView that @@ -522,6 +523,9 @@ class BrowserView : public BrowserBubbleHost, void ProcessTabSelected(TabContentsWrapper* new_contents, bool change_tab_contents); + // Exposes resize corner size to BrowserViewLayout. + gfx::Size GetResizeCornerSize() const; + // Last focused view that issued a tab traversal. int last_focused_view_storage_id_; diff --git a/chrome/browser/ui/views/frame/browser_view_layout.cc b/chrome/browser/ui/views/frame/browser_view_layout.cc index 34549bd9f6ca..6dc8cd040712 100644 --- a/chrome/browser/ui/views/frame/browser_view_layout.cc +++ b/chrome/browser/ui/views/frame/browser_view_layout.cc @@ -13,10 +13,12 @@ #include "chrome/browser/ui/views/frame/browser_frame.h" #include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/views/frame/contents_container.h" +#include "chrome/browser/ui/views/tab_contents/tab_contents_container.h" #include "chrome/browser/ui/views/tabs/side_tab_strip.h" #include "chrome/browser/ui/views/tabs/tab_strip.h" #include "chrome/browser/ui/views/toolbar_view.h" #include "gfx/scrollbar_size.h" +#include "views/controls/single_split_view.h" #include "views/window/window.h" #if defined(OS_LINUX) @@ -207,9 +209,9 @@ void BrowserViewLayout::Uninstalled(views::View* host) {} void BrowserViewLayout::ViewAdded(views::View* host, views::View* view) { switch (view->GetID()) { case VIEW_ID_CONTENTS_SPLIT: { - contents_split_ = view; + contents_split_ = static_cast(view); // We're installed as the LayoutManager before BrowserView creates the - // contents, so we have to set contents_container_ here rather than + // contents, so we have to set contents_container_ here rather than in // Installed. contents_container_ = browser_view_->contents_; break; @@ -363,9 +365,112 @@ int BrowserViewLayout::LayoutInfoBar(int top) { return top + height; } +// |browser_reserved_rect| is in browser_view_ coordinates. +// |future_source_bounds| is in |source|'s parent coordinates. +// |future_parent_offset| is required, since parent view is not moved yet. +// Note that |future_parent_offset| is relative to browser_view_, not to +// the parent view. +void BrowserViewLayout::UpdateReservedContentsRect( + const gfx::Rect& browser_reserved_rect, + TabContentsContainer* source, + const gfx::Rect& future_source_bounds, + const gfx::Point& future_parent_offset) { + gfx::Point resize_corner_origin(browser_reserved_rect.origin()); + // Convert |resize_corner_origin| from browser_view_ to source's parent + // coordinates. + views::View::ConvertPointToView(browser_view_, source->GetParent(), + &resize_corner_origin); + // Create |reserved_rect| in source's parent coordinates. + gfx::Rect reserved_rect(resize_corner_origin, browser_reserved_rect.size()); + // Apply source's parent future offset to it. + reserved_rect.Offset(-future_parent_offset.x(), -future_parent_offset.y()); + if (future_source_bounds.Intersects(reserved_rect)) { + // |source| is not properly positioned yet to use ConvertPointToView, + // so convert it into |source|'s coordinates manually. + reserved_rect.Offset(-future_source_bounds.x(), -future_source_bounds.y()); + } else { + reserved_rect = gfx::Rect(); + } + + source->SetReservedContentsRect(reserved_rect); +} + void BrowserViewLayout::LayoutTabContents(int top, int bottom) { - contents_split_->SetBounds(vertical_layout_rect_.x(), top, - vertical_layout_rect_.width(), bottom - top); + // The ultimate idea is to calcualte bounds and reserved areas for all + // contents views first and then resize them all, so every view + // (and its contents) is resized and laid out only once. + + // The views hierarcy (see browser_view.h for more details): + // 1) Sidebar is not allowed: + // contents_split_ -> [contents_container_ | devtools] + // 2) Sidebar is allowed: + // contents_split_ -> + // [sidebar_split -> [contents_container_ | sidebar]] | devtools + + gfx::Rect sidebar_split_bounds; + gfx::Rect contents_bounds; + gfx::Rect sidebar_bounds; + gfx::Rect devtools_bounds; + + gfx::Rect contents_split_bounds(vertical_layout_rect_.x(), top, + vertical_layout_rect_.width(), + std::max(0, bottom - top)); + contents_split_->CalculateChildrenBounds( + contents_split_bounds, &sidebar_split_bounds, &devtools_bounds); + gfx::Point contents_split_offset( + contents_split_bounds.x() - contents_split_->bounds().x(), + contents_split_bounds.y() - contents_split_->bounds().y()); + gfx::Point sidebar_split_offset(contents_split_offset); + sidebar_split_offset.Offset(sidebar_split_bounds.x(), + sidebar_split_bounds.y()); + + views::SingleSplitView* sidebar_split = browser_view_->sidebar_split_; + if (sidebar_split) { + DCHECK(sidebar_split == contents_split_->GetChildViewAt(0)); + sidebar_split->CalculateChildrenBounds( + sidebar_split_bounds, &contents_bounds, &sidebar_bounds); + } else { + contents_bounds = sidebar_split_bounds; + } + + // Layout resize corner, sidebar mini tabs and calculate reserved contents + // rects here as all contents view bounds are already determined, but not yet + // set at this point, so contents will be laid out once at most. + // TODO(alekseys): layout sidebar minitabs and adjust reserved rect + // accordingly. + gfx::Rect browser_reserved_rect; + if (!browser_view_->frame_->GetWindow()->IsMaximized() && + !browser_view_->frame_->GetWindow()->IsFullscreen()) { + gfx::Size resize_corner_size = browser_view_->GetResizeCornerSize(); + if (!resize_corner_size.IsEmpty()) { + gfx::Rect bounds = browser_view_->GetLocalBounds(false); + gfx::Point resize_corner_origin( + bounds.right() - resize_corner_size.width(), + bounds.bottom() - resize_corner_size.height()); + browser_reserved_rect = + gfx::Rect(resize_corner_origin, resize_corner_size); + } + } + + UpdateReservedContentsRect(browser_reserved_rect, + browser_view_->contents_container_, + contents_bounds, + sidebar_split_offset); + if (sidebar_split) { + UpdateReservedContentsRect(browser_reserved_rect, + browser_view_->sidebar_container_, + sidebar_bounds, + sidebar_split_offset); + } + UpdateReservedContentsRect(browser_reserved_rect, + browser_view_->devtools_container_, + devtools_bounds, + contents_split_offset); + + // Now it's safe to actually resize all contents views in the hierarchy. + contents_split_->SetBounds(contents_split_bounds); + if (sidebar_split) + sidebar_split->SetBounds(sidebar_split_bounds); } int BrowserViewLayout::GetTopMarginForActiveContent() { diff --git a/chrome/browser/ui/views/frame/browser_view_layout.h b/chrome/browser/ui/views/frame/browser_view_layout.h index 279803c09a3f..1b680a1b1dc4 100644 --- a/chrome/browser/ui/views/frame/browser_view_layout.h +++ b/chrome/browser/ui/views/frame/browser_view_layout.h @@ -14,8 +14,13 @@ class Browser; class BrowserView; class ContentsContainer; class DownloadShelfView; +class TabContentsContainer; class ToolbarView; +namespace views { +class SingleSplitView; +} + // The layout manager used in chrome browser. class BrowserViewLayout : public views::LayoutManager { public: @@ -61,6 +66,15 @@ class BrowserViewLayout : public views::LayoutManager { int LayoutBookmarkBar(int top); int LayoutInfoBar(int top); + // Updates |source|'s reserved contents rect by mapping BrowserView's + // |browser_reserved_rect| into |future_source_bounds| taking into + // account |source|'s |future_parent_offset| (offset is relative to + // browser_view_). + void UpdateReservedContentsRect(const gfx::Rect& browser_reserved_rect, + TabContentsContainer* source, + const gfx::Rect& future_source_bounds, + const gfx::Point& future_parent_offset); + // Layout the TabContents container, between the coordinates |top| and // |bottom|. void LayoutTabContents(int top, int bottom); @@ -88,7 +102,7 @@ class BrowserViewLayout : public views::LayoutManager { // Child views that the layout manager manages. BaseTabStrip* tabstrip_; ToolbarView* toolbar_; - views::View* contents_split_; + views::SingleSplitView* contents_split_; ContentsContainer* contents_container_; views::View* infobar_container_; DownloadShelfView* download_shelf_; diff --git a/chrome/browser/ui/views/tab_contents/tab_contents_container.cc b/chrome/browser/ui/views/tab_contents/tab_contents_container.cc index 06c34002c3e5..90af9978f0e2 100644 --- a/chrome/browser/ui/views/tab_contents/tab_contents_container.cc +++ b/chrome/browser/ui/views/tab_contents/tab_contents_container.cc @@ -25,8 +25,7 @@ TabContentsContainer::TabContentsContainer() : native_container_(NULL), - tab_contents_(NULL), - reserved_area_delegate_(NULL) { + tab_contents_(NULL) { SetID(VIEW_ID_TAB_CONTAINER); } @@ -46,9 +45,6 @@ void TabContentsContainer::ChangeTabContents(TabContents* contents) { tab_contents_->WasHidden(); RemoveObservers(); } -#if !defined(TOUCH_UI) - TabContents* old_contents = tab_contents_; -#endif tab_contents_ = contents; // When detaching the last tab of the browser ChangeTabContents is invoked // with NULL. Don't attempt to do anything in that case. @@ -63,9 +59,7 @@ void TabContentsContainer::ChangeTabContents(TabContents* contents) { Layout(); } #else - RenderWidgetHostViewChanged( - old_contents ? old_contents->GetRenderWidgetHostView() : NULL, - tab_contents_->GetRenderWidgetHostView()); + RenderWidgetHostViewChanged(tab_contents_->GetRenderWidgetHostView()); native_container_->AttachContents(tab_contents_); #endif AddObservers(); @@ -82,6 +76,17 @@ void TabContentsContainer::SetFastResize(bool fast_resize) { native_container_->SetFastResize(fast_resize); } +void TabContentsContainer::SetReservedContentsRect( + const gfx::Rect& reserved_rect) { + cached_reserved_rect_ = reserved_rect; +#if !defined(TOUCH_UI) + if (tab_contents_ && tab_contents_->GetRenderWidgetHostView()) { + tab_contents_->GetRenderWidgetHostView()->set_reserved_contents_rect( + reserved_rect); + } +#endif +} + //////////////////////////////////////////////////////////////////////////////// // TabContentsContainer, NotificationObserver implementation: @@ -108,8 +113,6 @@ void TabContentsContainer::Layout() { views::View::Layout(); #else if (native_container_) { - if (reserved_area_delegate_) - reserved_area_delegate_->UpdateReservedContentsRect(this); native_container_->GetView()->SetBounds(0, 0, width(), height()); native_container_->GetView()->Layout(); } @@ -158,10 +161,8 @@ void TabContentsContainer::RenderViewHostChanged(RenderViewHost* old_host, #if defined(TOUCH_UI) NOTIMPLEMENTED(); // TODO(anicolao) #else - if (new_host) { - RenderWidgetHostViewChanged( - old_host ? old_host->view() : NULL, new_host->view()); - } + if (new_host) + RenderWidgetHostViewChanged(new_host->view()); native_container_->RenderViewHostChanged(old_host, new_host); #endif } @@ -174,12 +175,7 @@ void TabContentsContainer::TabContentsDestroyed(TabContents* contents) { } void TabContentsContainer::RenderWidgetHostViewChanged( - RenderWidgetHostView* old_view, RenderWidgetHostView* new_view) { - // Carry over the reserved rect, if possible. - if (old_view && new_view) { - new_view->set_reserved_contents_rect(old_view->reserved_contents_rect()); - } else { - if (reserved_area_delegate_) - reserved_area_delegate_->UpdateReservedContentsRect(this); - } + RenderWidgetHostView* new_view) { + if (new_view) + new_view->set_reserved_contents_rect(cached_reserved_rect_); } diff --git a/chrome/browser/ui/views/tab_contents/tab_contents_container.h b/chrome/browser/ui/views/tab_contents/tab_contents_container.h index 83633e8c1290..1ba30ed6d712 100644 --- a/chrome/browser/ui/views/tab_contents/tab_contents_container.h +++ b/chrome/browser/ui/views/tab_contents/tab_contents_container.h @@ -19,19 +19,6 @@ class TabContents; class TabContentsContainer : public views::View, public NotificationObserver { public: - // Interface to request the reserved contents area updates. - class ReservedAreaDelegate { - public: - // Notifies that |source|'s reserved contents area should be updated. - // Reserved contents area is a rect in tab contents view coordinates where - // contents should not be rendered (to display the resize corner, sidebar - // mini tabs or any other UI elements overlaying this container). - virtual void UpdateReservedContentsRect( - const TabContentsContainer* source) = 0; - protected: - virtual ~ReservedAreaDelegate() {} - }; - TabContentsContainer(); virtual ~TabContentsContainer(); @@ -50,9 +37,9 @@ class TabContentsContainer : public views::View, // so performance is better. void SetFastResize(bool fast_resize); - void set_reserved_area_delegate(ReservedAreaDelegate* delegate) { - reserved_area_delegate_ = delegate; - } + // Updates the current reserved rect in view coordinates where contents + // should not be rendered to draw the resize corner, sidebar mini tabs etc. + void SetReservedContentsRect(const gfx::Rect& reserved_rect); // Overridden from NotificationObserver: virtual void Observe(NotificationType type, @@ -84,8 +71,7 @@ class TabContentsContainer : public views::View, void TabContentsDestroyed(TabContents* contents); // Called when the RenderWidgetHostView of the hosted TabContents has changed. - void RenderWidgetHostViewChanged(RenderWidgetHostView* old_view, - RenderWidgetHostView* new_view); + void RenderWidgetHostViewChanged(RenderWidgetHostView* new_view); // An instance of a NativeTabContentsContainer object that holds the native // view handle associated with the attached TabContents. @@ -97,8 +83,10 @@ class TabContentsContainer : public views::View, // Handles registering for our notifications. NotificationRegistrar registrar_; - // Delegate for enquiring reserved contents area. Not owned by us. - ReservedAreaDelegate* reserved_area_delegate_; + // The current reserved rect in view coordinates where contents should not be + // rendered to draw the resize corner, sidebar mini tabs etc. + // Cached here to update ever changing renderers. + gfx::Rect cached_reserved_rect_; DISALLOW_COPY_AND_ASSIGN(TabContentsContainer); }; diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 54572cfca005..1ec65dd7bd44 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1723,6 +1723,7 @@ '../views/box_layout_unittest.cc', '../views/controls/label_unittest.cc', '../views/controls/progress_bar_unittest.cc', + '../views/controls/single_split_view.cc', '../views/controls/tabbed_pane/tabbed_pane_unittest.cc', '../views/controls/table/table_view_unittest.cc', '../views/controls/textfield/textfield_views_model_unittest.cc', diff --git a/views/controls/single_split_view.cc b/views/controls/single_split_view.cc index f997f31b3641..7248200b970e 100644 --- a/views/controls/single_split_view.cc +++ b/views/controls/single_split_view.cc @@ -23,10 +23,12 @@ static const int kDividerSize = 4; SingleSplitView::SingleSplitView(View* leading, View* trailing, - Orientation orientation) + Orientation orientation, + Observer* observer) : is_horizontal_(orientation == HORIZONTAL_SPLIT), divider_offset_(-1), - resize_leading_on_bounds_change_(true) { + resize_leading_on_bounds_change_(true), + observer_(observer) { AddChildView(leading); AddChildView(trailing); #if defined(OS_WIN) @@ -38,57 +40,21 @@ SingleSplitView::SingleSplitView(View* leading, void SingleSplitView::DidChangeBounds(const gfx::Rect& previous, const gfx::Rect& current) { - if (resize_leading_on_bounds_change_) { - // We do not update divider_offset_ on minimize (to zero) and on restore - // (to largest value). As a result we get back to the original value upon - // window restore. - bool is_minimize_or_restore = - previous.height() == 0 || current.height() == 0; - if (!is_minimize_or_restore) { - if (is_horizontal_) - divider_offset_ += current.width() - previous.width(); - else - divider_offset_ += current.height() - previous.height(); - - if (divider_offset_ < 0) - divider_offset_ = kDividerSize; - } - } + divider_offset_ = CalculateDividerOffset(divider_offset_, previous, current); View::DidChangeBounds(previous, current); } void SingleSplitView::Layout() { - if (GetChildViewCount() != 2) - return; - - View* leading = GetChildViewAt(0); - View* trailing = GetChildViewAt(1); - - if (!leading->IsVisible() && !trailing->IsVisible()) - return; - - if (width() == 0 || height() == 0) { - // We are most likely minimized - do not touch divider offset. - return; - } else if (!trailing->IsVisible()) { - leading->SetBounds(0, 0, width(), height()); - } else if (!leading->IsVisible()) { - trailing->SetBounds(0, 0, width(), height()); - } else { - if (divider_offset_ < 0) - divider_offset_ = (GetPrimaryAxisSize() - kDividerSize) / 2; - else - divider_offset_ = std::min(divider_offset_, - GetPrimaryAxisSize() - kDividerSize); - - if (is_horizontal_) { - leading->SetBounds(0, 0, divider_offset_, height()); - trailing->SetBounds(divider_offset_ + kDividerSize, 0, - width() - divider_offset_ - kDividerSize, height()); - } else { - leading->SetBounds(0, 0, width(), divider_offset_); - trailing->SetBounds(0, divider_offset_ + kDividerSize, - width(), height() - divider_offset_ - kDividerSize); + gfx::Rect leading_bounds; + gfx::Rect trailing_bounds; + CalculateChildrenBounds(bounds(), &leading_bounds, &trailing_bounds); + + if (GetChildViewCount() > 0) { + if (GetChildViewAt(0)->IsVisible()) + GetChildViewAt(0)->SetBounds(leading_bounds); + if (GetChildViewCount() > 1) { + if (GetChildViewAt(1)->IsVisible()) + GetChildViewAt(1)->SetBounds(trailing_bounds); } } @@ -140,11 +106,56 @@ gfx::NativeCursor SingleSplitView::GetCursorForPoint( return NULL; } +void SingleSplitView::CalculateChildrenBounds( + const gfx::Rect& bounds, + gfx::Rect* leading_bounds, + gfx::Rect* trailing_bounds) const { + bool is_leading_visible = + GetChildViewCount() > 0 && GetChildViewAt(0)->IsVisible(); + bool is_trailing_visible = + GetChildViewCount() > 1 && GetChildViewAt(1)->IsVisible(); + + if (!is_leading_visible && !is_trailing_visible) { + *leading_bounds = gfx::Rect(); + *trailing_bounds = gfx::Rect(); + return; + } + + int divider_at; + + if (!is_trailing_visible) { + divider_at = GetPrimaryAxisSize(bounds.width(), bounds.height()); + } else if (!is_leading_visible) { + divider_at = 0; + } else { + divider_at = + CalculateDividerOffset(divider_offset_, this->bounds(), bounds); + divider_at = NormalizeDividerOffset(divider_at, bounds); + } + + int divider_size = + !is_leading_visible || !is_trailing_visible ? 0 : kDividerSize; + + if (is_horizontal_) { + *leading_bounds = gfx::Rect(0, 0, divider_at, bounds.height()); + *trailing_bounds = + gfx::Rect(divider_at + divider_size, 0, + std::max(0, bounds.width() - divider_at - divider_size), + bounds.height()); + } else { + *leading_bounds = gfx::Rect(0, 0, bounds.width(), divider_at); + *trailing_bounds = + gfx::Rect(0, divider_at + divider_size, bounds.width(), + std::max(0, bounds.height() - divider_at - divider_size)); + } +} + bool SingleSplitView::OnMousePressed(const MouseEvent& event) { if (!IsPointInDivider(event.location())) return false; drag_info_.initial_mouse_offset = GetPrimaryAxisSize(event.x(), event.y()); - drag_info_.initial_divider_offset = divider_offset_; + drag_info_.initial_divider_offset = + NormalizeDividerOffset(divider_offset_, bounds()); return true; } @@ -166,7 +177,8 @@ bool SingleSplitView::OnMouseDragged(const MouseEvent& event) { if (new_size != divider_offset_) { set_divider_offset(new_size); - Layout(); + if (!observer_ || observer_->SplitHandleMoved(this)) + Layout(); } return true; } @@ -177,7 +189,8 @@ void SingleSplitView::OnMouseReleased(const MouseEvent& event, bool canceled) { if (canceled && drag_info_.initial_divider_offset != divider_offset_) { set_divider_offset(drag_info_.initial_divider_offset); - Layout(); + if (!observer_ || observer_->SplitHandleMoved(this)) + Layout(); } } @@ -199,4 +212,36 @@ bool SingleSplitView::IsPointInDivider(const gfx::Point& p) { divider_relative_offset < kDividerSize); } +int SingleSplitView::CalculateDividerOffset( + int divider_offset, + const gfx::Rect& previous_bounds, + const gfx::Rect& new_bounds) const { + if (resize_leading_on_bounds_change_ && divider_offset != -1) { + // We do not update divider_offset on minimize (to zero) and on restore + // (to largest value). As a result we get back to the original value upon + // window restore. + bool is_minimize_or_restore = + previous_bounds.height() == 0 || new_bounds.height() == 0; + if (!is_minimize_or_restore) { + if (is_horizontal_) + divider_offset += new_bounds.width() - previous_bounds.width(); + else + divider_offset += new_bounds.height() - previous_bounds.height(); + + if (divider_offset < 0) + divider_offset = kDividerSize; + } + } + return divider_offset; +} + +int SingleSplitView::NormalizeDividerOffset(int divider_offset, + const gfx::Rect& bounds) const { + int primary_axis_size = GetPrimaryAxisSize(bounds.width(), bounds.height()); + if (divider_offset < 0) + return (primary_axis_size - kDividerSize) / 2; + return std::min(divider_offset, + std::max(primary_axis_size - kDividerSize, 0)); +} + } // namespace views diff --git a/views/controls/single_split_view.h b/views/controls/single_split_view.h index c3814b1e88f9..a1b0534661a5 100644 --- a/views/controls/single_split_view.h +++ b/views/controls/single_split_view.h @@ -6,12 +6,16 @@ #define VIEWS_CONTROLS_SINGLE_SPLIT_VIEW_H_ #pragma once +#include "base/gtest_prod_util.h" #include "views/view.h" namespace views { -// SingleSplitView lays out two views horizontally. A splitter exists between -// the two views that the user can drag around to resize the views. +// SingleSplitView lays out two views next to each other, either horizontally +// or vertically. A splitter exists between the two views that the user can +// drag around to resize the views. +// Observer's SplitHandleMoved notification helps to monitor user initiated +// layout changes. class SingleSplitView : public views::View { public: enum Orientation { @@ -19,7 +23,21 @@ class SingleSplitView : public views::View { VERTICAL_SPLIT }; - SingleSplitView(View* leading, View* trailing, Orientation orientation); + class Observer { + public: + // Invoked when split handle is moved by the user. |source|'s divider_offset + // is already set to the new value, but Layout has not happened yet. + // Returns false if the layout has been handled by the observer, returns + // true if |source| should do it by itself. + virtual bool SplitHandleMoved(SingleSplitView* source) = 0; + protected: + virtual ~Observer() {} + }; + + SingleSplitView(View* leading, + View* trailing, + Orientation orientation, + Observer* observer); virtual void DidChangeBounds(const gfx::Rect& previous, const gfx::Rect& current); @@ -36,10 +54,14 @@ class SingleSplitView : public views::View { virtual gfx::NativeCursor GetCursorForPoint(Event::EventType event_type, const gfx::Point& p); + Orientation orientation() const { + return is_horizontal_ ? HORIZONTAL_SPLIT : VERTICAL_SPLIT; + } + void set_divider_offset(int divider_offset) { divider_offset_ = divider_offset; } - int divider_offset() { return divider_offset_; } + int divider_offset() const { return divider_offset_; } // Sets whether the leading component is resized when the split views size // changes. The default is true. A value of false results in the trailing @@ -48,21 +70,42 @@ class SingleSplitView : public views::View { resize_leading_on_bounds_change_ = resize; } + // Calculates ideal leading and trailing view bounds according to the given + // split view |bounds|, current divider offset and children visiblity. + // Does not change children view bounds. + void CalculateChildrenBounds(const gfx::Rect& bounds, + gfx::Rect* leading_bounds, + gfx::Rect* trailing_bounds) const; + protected: virtual bool OnMousePressed(const MouseEvent& event); virtual bool OnMouseDragged(const MouseEvent& event); virtual void OnMouseReleased(const MouseEvent& event, bool canceled); private: + // This test calls OnMouse* functions. + FRIEND_TEST_ALL_PREFIXES(SingleSplitViewTest, MouseDrag); + // Returns true if |x| or |y| is over the divider. bool IsPointInDivider(const gfx::Point& p); + // Calculates the new |divider_offset| based on the changes of split view + // bounds. + int CalculateDividerOffset( + int divider_offset, + const gfx::Rect& previous_bounds, + const gfx::Rect& new_bounds) const; + + // Returns divider offset within primary axis size range for given split + // view |bounds|. + int NormalizeDividerOffset(int divider_offset, const gfx::Rect& bounds) const; + // Returns width in case of horizontal split and height otherwise. - int GetPrimaryAxisSize() { + int GetPrimaryAxisSize() const { return GetPrimaryAxisSize(width(), height()); } - int GetPrimaryAxisSize(int h, int v) { + int GetPrimaryAxisSize(int h, int v) const { return is_horizontal_ ? h : v; } @@ -84,6 +127,9 @@ class SingleSplitView : public views::View { bool resize_leading_on_bounds_change_; + // Observer to notify about user initiated handle movements. Not own by us. + Observer* observer_; + DISALLOW_COPY_AND_ASSIGN(SingleSplitView); }; diff --git a/views/controls/single_split_view_unittest.cc b/views/controls/single_split_view_unittest.cc new file mode 100644 index 000000000000..b788a3774b87 --- /dev/null +++ b/views/controls/single_split_view_unittest.cc @@ -0,0 +1,176 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/logging.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "views/controls/single_split_view.h" + +using ::testing::_; +using ::testing::Return; + +namespace { + +static void VerifySplitViewLayout(const views::SingleSplitView& split) { + ASSERT_EQ(2, split.GetChildViewCount()); + + views::View* leading = split.GetChildViewAt(0); + views::View* trailing = split.GetChildViewAt(1); + + if (split.bounds().IsEmpty()) { + EXPECT_TRUE(leading->bounds().IsEmpty()); + EXPECT_TRUE(trailing->bounds().IsEmpty()); + return; + } + + EXPECT_FALSE(leading->bounds().IsEmpty()); + EXPECT_FALSE(trailing->bounds().IsEmpty()); + EXPECT_FALSE(leading->bounds().Intersects(trailing->bounds())); + + if (split.orientation() == views::SingleSplitView::HORIZONTAL_SPLIT) { + EXPECT_EQ(leading->bounds().height(), split.bounds().height()); + EXPECT_EQ(trailing->bounds().height(), split.bounds().height()); + EXPECT_LT(leading->bounds().width() + trailing->bounds().width(), + split.bounds().width()); + } else if (split.orientation() == views::SingleSplitView::VERTICAL_SPLIT) { + EXPECT_EQ(leading->bounds().width(), split.bounds().width()); + EXPECT_EQ(trailing->bounds().width(), split.bounds().width()); + EXPECT_LT(leading->bounds().height() + trailing->bounds().height(), + split.bounds().height()); + } else { + NOTREACHED(); + } +} + +class MockObserver : public views::SingleSplitView::Observer { + public: + MOCK_METHOD1(SplitHandleMoved, bool(views::SingleSplitView*)); +}; + +} // namespace + +namespace views { + +TEST(SingleSplitViewTest, Resize) { + // Test cases to iterate through for horizontal and vertical split views. + struct TestCase { + // Split view resize policy for this test case. + bool resize_leading_on_bounds_change; + // Split view size to set. + int primary_axis_size; + int secondary_axis_size; + // Expected divider offset. + int divider_offset; + } test_cases[] = { + // The initial split size is 100x100, divider at 33. + { true, 100, 100, 33 }, + // Grow the split view, leading view should grow. + { true, 1000, 100, 933 }, + // Shrink the split view, leading view should shrink. + { true, 200, 100, 133 }, + // Minimize the split view, divider should not move. + { true, 0, 0, 133 }, + // Restore the split view, divider should not move. + { false, 500, 100, 133 }, + // Resize the split view by secondary axis, divider should not move. + { false, 500, 600, 133 } + }; + + SingleSplitView::Orientation orientations[] = { + SingleSplitView::HORIZONTAL_SPLIT, + SingleSplitView::VERTICAL_SPLIT + }; + + for (size_t orientation = 0; orientation < arraysize(orientations); + ++orientation) { + // Create a split view. + SingleSplitView split( + new View(), new View(), orientations[orientation], NULL); + + // Set initial size and divider offset. + EXPECT_EQ(test_cases[0].primary_axis_size, + test_cases[0].secondary_axis_size); + split.SetBounds(0, 0, test_cases[0].primary_axis_size, + test_cases[0].secondary_axis_size); + split.set_divider_offset(test_cases[0].divider_offset); + split.Layout(); + + // Run all test cases. + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { + split.set_resize_leading_on_bounds_change( + test_cases[i].resize_leading_on_bounds_change); + if (split.orientation() == SingleSplitView::HORIZONTAL_SPLIT) { + split.SetBounds(0, 0, test_cases[i].primary_axis_size, + test_cases[i].secondary_axis_size); + } else { + split.SetBounds(0, 0, test_cases[i].secondary_axis_size, + test_cases[i].primary_axis_size); + } + + EXPECT_EQ(test_cases[i].divider_offset, split.divider_offset()); + VerifySplitViewLayout(split); + } + + // Special cases, one of the child views is hidden. + split.GetChildViewAt(0)->SetVisible(false); + split.Layout(); + + EXPECT_EQ(split.bounds().size(), + split.GetChildViewAt(1)->bounds().size()); + + split.GetChildViewAt(0)->SetVisible(true); + split.GetChildViewAt(1)->SetVisible(false); + split.Layout(); + + EXPECT_EQ(split.bounds().size(), + split.GetChildViewAt(0)->bounds().size()); + } +} + +TEST(SingleSplitViewTest, MouseDrag) { + MockObserver observer; + SingleSplitView split( + new View(), new View(), SingleSplitView::VERTICAL_SPLIT, &observer); + + ON_CALL(observer, SplitHandleMoved(_)) + .WillByDefault(Return(true)); + // SplitHandleMoved is expected to be called once for every mouse move. + EXPECT_CALL(observer, SplitHandleMoved(_)) + .Times(2); + + split.SetBounds(0, 0, 10, 100); + const int kInitialDividerOffset = 33; + const int kMouseOffset = 2; // Mouse offset in the divider. + const int kMouseMoveDelta = 7; + split.set_divider_offset(kInitialDividerOffset); + split.Layout(); + + // Drag divider to the right, in 2 steps. + MouseEvent mouse_pressed( + Event::ET_MOUSE_PRESSED, 7, kInitialDividerOffset + kMouseOffset, 0); + ASSERT_TRUE(split.OnMousePressed(mouse_pressed)); + EXPECT_EQ(kInitialDividerOffset, split.divider_offset()); + + MouseEvent mouse_dragged_1( + Event::ET_MOUSE_DRAGGED, 5, + kInitialDividerOffset + kMouseOffset + kMouseMoveDelta, 0); + ASSERT_TRUE(split.OnMouseDragged(mouse_dragged_1)); + EXPECT_EQ(kInitialDividerOffset + kMouseMoveDelta, split.divider_offset()); + + MouseEvent mouse_dragged_2( + Event::ET_MOUSE_DRAGGED, 6, + kInitialDividerOffset + kMouseOffset + kMouseMoveDelta * 2, 0); + ASSERT_TRUE(split.OnMouseDragged(mouse_dragged_2)); + EXPECT_EQ(kInitialDividerOffset + kMouseMoveDelta * 2, + split.divider_offset()); + + MouseEvent mouse_released( + Event::ET_MOUSE_RELEASED, 7, + kInitialDividerOffset + kMouseOffset + kMouseMoveDelta * 2, 0); + split.OnMouseReleased(mouse_released, false); + EXPECT_EQ(kInitialDividerOffset + kMouseMoveDelta * 2, + split.divider_offset()); +} + +} // namespace views diff --git a/views/examples/single_split_view_example.h b/views/examples/single_split_view_example.h index 3eaea7ca3e29..73d69a6718ad 100644 --- a/views/examples/single_split_view_example.h +++ b/views/examples/single_split_view_example.h @@ -29,7 +29,8 @@ class SingleSplitViewExample : public ExampleBase { single_split_view_ = new views::SingleSplitView( splitted_view_1, splitted_view_2, - views::SingleSplitView::HORIZONTAL_SPLIT); + views::SingleSplitView::HORIZONTAL_SPLIT, + NULL); splitted_view_1->SetColor(SK_ColorYELLOW, SK_ColorCYAN); diff --git a/views/views.gyp b/views/views.gyp index b8d75f9a8add..191fc7dde997 100644 --- a/views/views.gyp +++ b/views/views.gyp @@ -428,6 +428,7 @@ 'box_layout_unittest.cc', 'controls/label_unittest.cc', 'controls/progress_bar_unittest.cc', + 'controls/single_split_view_unittest.cc', 'controls/tabbed_pane/tabbed_pane_unittest.cc', 'controls/table/table_view_unittest.cc', 'controls/textfield/native_textfield_views_unittest.cc',