1// Copyright (c) 2011 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 "chrome/browser/ui/views/frame/browser_view_layout.h"
6
7#include "chrome/browser/sidebar/sidebar_manager.h"
8#include "chrome/browser/ui/find_bar/find_bar.h"
9#include "chrome/browser/ui/find_bar/find_bar_controller.h"
10#include "chrome/browser/ui/view_ids.h"
11#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h"
12#include "chrome/browser/ui/views/download/download_shelf_view.h"
13#include "chrome/browser/ui/views/frame/browser_frame.h"
14#include "chrome/browser/ui/views/frame/browser_view.h"
15#include "chrome/browser/ui/views/frame/contents_container.h"
16#include "chrome/browser/ui/views/infobars/infobar_container_view.h"
17#include "chrome/browser/ui/views/tab_contents/tab_contents_container.h"
18#include "chrome/browser/ui/views/tabs/abstract_tab_strip_view.h"
19#include "chrome/browser/ui/views/toolbar_view.h"
20#include "ui/gfx/point.h"
21#include "ui/gfx/scrollbar_size.h"
22#include "ui/gfx/size.h"
23#include "views/controls/single_split_view.h"
24#include "views/window/window.h"
25
26#if defined(OS_LINUX)
27#include "views/window/hit_test.h"
28#endif
29
30namespace {
31
32// The visible height of the shadow above the tabs. Clicks in this area are
33// treated as clicks to the frame, rather than clicks to the tab.
34const int kTabShadowSize = 2;
35// The vertical overlap between the TabStrip and the Toolbar.
36const int kToolbarTabStripVerticalOverlap = 3;
37
38}  // namespace
39
40////////////////////////////////////////////////////////////////////////////////
41// BrowserViewLayout, public:
42
43BrowserViewLayout::BrowserViewLayout()
44    : tabstrip_(NULL),
45      toolbar_(NULL),
46      contents_split_(NULL),
47      contents_container_(NULL),
48      infobar_container_(NULL),
49      download_shelf_(NULL),
50      active_bookmark_bar_(NULL),
51      browser_view_(NULL),
52      find_bar_y_(0) {
53}
54
55BrowserViewLayout::~BrowserViewLayout() {
56}
57
58gfx::Size BrowserViewLayout::GetMinimumSize() {
59  // TODO(noname): In theory the tabstrip width should probably be
60  // (OTR + tabstrip + caption buttons) width.
61  gfx::Size tabstrip_size(
62      browser()->SupportsWindowFeature(Browser::FEATURE_TABSTRIP) ?
63      tabstrip_->GetMinimumSize() : gfx::Size());
64  gfx::Size toolbar_size(
65      (browser()->SupportsWindowFeature(Browser::FEATURE_TOOLBAR) ||
66       browser()->SupportsWindowFeature(Browser::FEATURE_LOCATIONBAR)) ?
67      toolbar_->GetMinimumSize() : gfx::Size());
68  if (tabstrip_size.height() && toolbar_size.height())
69    toolbar_size.Enlarge(0, -kToolbarTabStripVerticalOverlap);
70  gfx::Size bookmark_bar_size;
71  if (active_bookmark_bar_ &&
72      browser()->SupportsWindowFeature(Browser::FEATURE_BOOKMARKBAR)) {
73    bookmark_bar_size = active_bookmark_bar_->GetMinimumSize();
74    bookmark_bar_size.Enlarge(0,
75        -(views::NonClientFrameView::kClientEdgeThickness +
76            active_bookmark_bar_->GetToolbarOverlap(true)));
77  }
78  gfx::Size contents_size(contents_split_->GetMinimumSize());
79
80  int min_height = tabstrip_size.height() + toolbar_size.height() +
81      bookmark_bar_size.height() + contents_size.height();
82  int widths[] = { tabstrip_size.width(), toolbar_size.width(),
83                   bookmark_bar_size.width(), contents_size.width() };
84  int min_width = *std::max_element(&widths[0], &widths[arraysize(widths)]);
85  return gfx::Size(min_width, min_height);
86}
87
88gfx::Rect BrowserViewLayout::GetFindBarBoundingBox() const {
89  // This function returns the area the Find Bar can be laid out
90  // within. This basically implies the "user-perceived content
91  // area" of the browser window excluding the vertical
92  // scrollbar. This is not quite so straightforward as positioning
93  // based on the TabContentsContainer since the BookmarkBarView may
94  // be visible but not persistent (in the New Tab case) and we
95  // position the Find Bar over the top of it in that case since the
96  // BookmarkBarView is not _visually_ connected to the Toolbar.
97
98  // First determine the bounding box of the content area in Widget
99  // coordinates.
100  gfx::Rect bounding_box(contents_container_->bounds());
101
102  gfx::Point topleft;
103  views::View::ConvertPointToWidget(contents_container_, &topleft);
104  bounding_box.set_origin(topleft);
105
106  // Adjust the position and size of the bounding box by the find bar offset
107  // calculated during the last Layout.
108  int height_delta = find_bar_y_ - bounding_box.y();
109  bounding_box.set_y(find_bar_y_);
110  bounding_box.set_height(std::max(0, bounding_box.height() + height_delta));
111
112  // Finally decrease the width of the bounding box by the width of
113  // the vertical scroll bar.
114  int scrollbar_width = gfx::scrollbar_size();
115  bounding_box.set_width(std::max(0, bounding_box.width() - scrollbar_width));
116  if (base::i18n::IsRTL())
117    bounding_box.set_x(bounding_box.x() + scrollbar_width);
118
119  return bounding_box;
120}
121
122bool BrowserViewLayout::IsPositionInWindowCaption(
123    const gfx::Point& point) {
124  gfx::Point tabstrip_point(point);
125  views::View::ConvertPointToView(browser_view_, tabstrip_, &tabstrip_point);
126  return tabstrip_->IsPositionInWindowCaption(tabstrip_point);
127}
128
129int BrowserViewLayout::NonClientHitTest(
130    const gfx::Point& point) {
131  // Since the TabStrip only renders in some parts of the top of the window,
132  // the un-obscured area is considered to be part of the non-client caption
133  // area of the window. So we need to treat hit-tests in these regions as
134  // hit-tests of the titlebar.
135
136  views::View* parent = browser_view_->parent();
137
138  gfx::Point point_in_browser_view_coords(point);
139  views::View::ConvertPointToView(
140      parent, browser_view_, &point_in_browser_view_coords);
141
142  // Determine if the TabStrip exists and is capable of being clicked on. We
143  // might be a popup window without a TabStrip.
144  if (browser_view_->IsTabStripVisible()) {
145    // See if the mouse pointer is within the bounds of the TabStrip.
146    gfx::Point point_in_tabstrip_coords(point);
147    views::View::ConvertPointToView(parent, tabstrip_,
148                                    &point_in_tabstrip_coords);
149    if (tabstrip_->HitTest(point_in_tabstrip_coords)) {
150      if (tabstrip_->IsPositionInWindowCaption(point_in_tabstrip_coords))
151        return HTCAPTION;
152      return HTCLIENT;
153    }
154
155    // The top few pixels of the TabStrip are a drop-shadow - as we're pretty
156    // starved of dragable area, let's give it to window dragging (this also
157    // makes sense visually).
158    if (!browser_view_->IsMaximized() &&
159        (point_in_browser_view_coords.y() <
160         (tabstrip_->y() + kTabShadowSize))) {
161      // We return HTNOWHERE as this is a signal to our containing
162      // NonClientView that it should figure out what the correct hit-test
163      // code is given the mouse position...
164      return HTNOWHERE;
165    }
166  }
167
168  // If the point's y coordinate is below the top of the toolbar and otherwise
169  // within the bounds of this view, the point is considered to be within the
170  // client area.
171  gfx::Rect bv_bounds = browser_view_->bounds();
172  bv_bounds.Offset(0, toolbar_->y());
173  bv_bounds.set_height(bv_bounds.height() - toolbar_->y());
174  if (bv_bounds.Contains(point))
175    return HTCLIENT;
176
177  // If the point's y coordinate is above the top of the toolbar, but not in
178  // the tabstrip (per previous checking in this function), then we consider it
179  // in the window caption (e.g. the area to the right of the tabstrip
180  // underneath the window controls). However, note that we DO NOT return
181  // HTCAPTION here, because when the window is maximized the window controls
182  // will fall into this space (since the BrowserView is sized to entire size
183  // of the window at that point), and the HTCAPTION value will cause the
184  // window controls not to work. So we return HTNOWHERE so that the caller
185  // will hit-test the window controls before finally falling back to
186  // HTCAPTION.
187  bv_bounds = browser_view_->bounds();
188  bv_bounds.set_height(toolbar_->y());
189  if (bv_bounds.Contains(point))
190    return HTNOWHERE;
191
192  // If the point is somewhere else, delegate to the default implementation.
193  return browser_view_->views::ClientView::NonClientHitTest(point);
194}
195
196//////////////////////////////////////////////////////////////////////////////
197// BrowserViewLayout, views::LayoutManager implementation:
198
199void BrowserViewLayout::Installed(views::View* host) {
200  toolbar_ = NULL;
201  contents_split_ = NULL;
202  contents_container_ = NULL;
203  infobar_container_ = NULL;
204  download_shelf_ = NULL;
205  active_bookmark_bar_ = NULL;
206  tabstrip_ = NULL;
207  browser_view_ = static_cast<BrowserView*>(host);
208}
209
210void BrowserViewLayout::Uninstalled(views::View* host) {}
211
212void BrowserViewLayout::ViewAdded(views::View* host, views::View* view) {
213  switch (view->GetID()) {
214    case VIEW_ID_CONTENTS_SPLIT: {
215      contents_split_ = static_cast<views::SingleSplitView*>(view);
216      // We're installed as the LayoutManager before BrowserView creates the
217      // contents, so we have to set contents_container_ here rather than in
218      // Installed.
219      contents_container_ = browser_view_->contents_;
220      break;
221    }
222    case VIEW_ID_INFO_BAR_CONTAINER:
223      infobar_container_ = view;
224      break;
225    case VIEW_ID_DOWNLOAD_SHELF:
226      download_shelf_ = static_cast<DownloadShelfView*>(view);
227      break;
228    case VIEW_ID_BOOKMARK_BAR:
229      active_bookmark_bar_ = static_cast<BookmarkBarView*>(view);
230      break;
231    case VIEW_ID_TOOLBAR:
232      toolbar_ = static_cast<ToolbarView*>(view);
233      break;
234    case VIEW_ID_TAB_STRIP:
235      tabstrip_ = static_cast<AbstractTabStripView*>(view);
236      break;
237  }
238}
239
240void BrowserViewLayout::ViewRemoved(views::View* host, views::View* view) {
241  switch (view->GetID()) {
242    case VIEW_ID_BOOKMARK_BAR:
243      active_bookmark_bar_ = NULL;
244      break;
245  }
246}
247
248void BrowserViewLayout::Layout(views::View* host) {
249  vertical_layout_rect_ = browser_view_->GetLocalBounds();
250  int top = LayoutTabStrip();
251  if (browser_view_->IsTabStripVisible() && !browser_view_->UseVerticalTabs()) {
252    tabstrip_->SetBackgroundOffset(gfx::Point(
253        tabstrip_->GetMirroredX() + browser_view_->GetMirroredX(),
254        browser_view_->frame()->GetHorizontalTabStripVerticalOffset(false)));
255  }
256  top = LayoutToolbar(top);
257  top = LayoutBookmarkAndInfoBars(top);
258  int bottom = LayoutDownloadShelf(browser_view_->height());
259  int active_top_margin = GetTopMarginForActiveContent();
260  top -= active_top_margin;
261  contents_container_->SetActiveTopMargin(active_top_margin);
262  LayoutTabContents(top, bottom);
263  // This must be done _after_ we lay out the TabContents since this
264  // code calls back into us to find the bounding box the find bar
265  // must be laid out within, and that code depends on the
266  // TabContentsContainer's bounds being up to date.
267  if (browser()->HasFindBarController()) {
268    browser()->GetFindBarController()->find_bar()->MoveWindowIfNecessary(
269        gfx::Rect(), true);
270  }
271}
272
273// Return the preferred size which is the size required to give each
274// children their respective preferred size.
275gfx::Size BrowserViewLayout::GetPreferredSize(views::View* host) {
276  return gfx::Size();
277}
278
279//////////////////////////////////////////////////////////////////////////////
280// BrowserViewLayout, private:
281
282Browser* BrowserViewLayout::browser() {
283  return browser_view_->browser();
284}
285
286const Browser* BrowserViewLayout::browser() const {
287  return browser_view_->browser();
288}
289
290int BrowserViewLayout::LayoutTabStrip() {
291  if (!browser_view_->IsTabStripVisible()) {
292    tabstrip_->SetVisible(false);
293    tabstrip_->SetBounds(0, 0, 0, 0);
294    return 0;
295  }
296
297  gfx::Rect tabstrip_bounds(
298      browser_view_->frame()->GetBoundsForTabStrip(tabstrip_));
299  gfx::Point tabstrip_origin(tabstrip_bounds.origin());
300  views::View::ConvertPointToView(browser_view_->parent(), browser_view_,
301                                  &tabstrip_origin);
302  tabstrip_bounds.set_origin(tabstrip_origin);
303
304  if (browser_view_->UseVerticalTabs())
305    vertical_layout_rect_.Inset(tabstrip_bounds.width(), 0, 0, 0);
306
307  tabstrip_->SetVisible(true);
308  tabstrip_->SetBoundsRect(tabstrip_bounds);
309  return browser_view_->UseVerticalTabs() ?
310      tabstrip_bounds.y() : tabstrip_bounds.bottom();
311}
312
313int BrowserViewLayout::LayoutToolbar(int top) {
314  int browser_view_width = vertical_layout_rect_.width();
315  bool visible = browser_view_->IsToolbarVisible();
316  toolbar_->location_bar()->SetFocusable(visible);
317  int y = top;
318  if (!browser_view_->UseVerticalTabs()) {
319    y -= ((visible && browser_view_->IsTabStripVisible()) ?
320          kToolbarTabStripVerticalOverlap : 0);
321  }
322  int height = visible ? toolbar_->GetPreferredSize().height() : 0;
323  toolbar_->SetVisible(visible);
324  toolbar_->SetBounds(vertical_layout_rect_.x(), y, browser_view_width, height);
325  return y + height;
326}
327
328int BrowserViewLayout::LayoutBookmarkAndInfoBars(int top) {
329  find_bar_y_ = top + browser_view_->y() - 1;
330  if (active_bookmark_bar_) {
331    // If we're showing the Bookmark bar in detached style, then we
332    // need to show any Info bar _above_ the Bookmark bar, since the
333    // Bookmark bar is styled to look like it's part of the page.
334    if (active_bookmark_bar_->IsDetached())
335      return LayoutBookmarkBar(LayoutInfoBar(top));
336    // Otherwise, Bookmark bar first, Info bar second.
337    top = std::max(toolbar_->bounds().bottom(), LayoutBookmarkBar(top));
338  }
339  find_bar_y_ = top + browser_view_->y() - 1;
340  return LayoutInfoBar(top);
341}
342
343int BrowserViewLayout::LayoutBookmarkBar(int top) {
344  DCHECK(active_bookmark_bar_);
345  int y = top;
346  if (!browser_view_->IsBookmarkBarVisible()) {
347    active_bookmark_bar_->SetVisible(false);
348    active_bookmark_bar_->SetBounds(0, y, browser_view_->width(), 0);
349    return y;
350  }
351
352  active_bookmark_bar_->set_infobar_visible(InfobarVisible());
353  int bookmark_bar_height = active_bookmark_bar_->GetPreferredSize().height();
354  y -= views::NonClientFrameView::kClientEdgeThickness +
355      active_bookmark_bar_->GetToolbarOverlap(false);
356  active_bookmark_bar_->SetVisible(true);
357  active_bookmark_bar_->SetBounds(vertical_layout_rect_.x(), y,
358                                  vertical_layout_rect_.width(),
359                                  bookmark_bar_height);
360  return y + bookmark_bar_height;
361}
362
363int BrowserViewLayout::LayoutInfoBar(int top) {
364  // Raise the |infobar_container_| by its vertical overlap.
365  infobar_container_->SetVisible(InfobarVisible());
366  int height;
367  int overlapped_top = top -
368      static_cast<InfoBarContainerView*>(infobar_container_)->
369          GetVerticalOverlap(&height);
370  infobar_container_->SetBounds(vertical_layout_rect_.x(),
371                                overlapped_top,
372                                vertical_layout_rect_.width(),
373                                height);
374  return overlapped_top + height;
375}
376
377// |browser_reserved_rect| is in browser_view_ coordinates.
378// |future_source_bounds| is in |source|'s parent coordinates.
379// |future_parent_offset| is required, since parent view is not moved yet.
380// Note that |future_parent_offset| is relative to browser_view_, not to
381// the parent view.
382void BrowserViewLayout::UpdateReservedContentsRect(
383    const gfx::Rect& browser_reserved_rect,
384    TabContentsContainer* source,
385    const gfx::Rect& future_source_bounds,
386    const gfx::Point& future_parent_offset) {
387  gfx::Point resize_corner_origin(browser_reserved_rect.origin());
388  // Convert |resize_corner_origin| from browser_view_ to source's parent
389  // coordinates.
390  views::View::ConvertPointToView(browser_view_, source->parent(),
391                                  &resize_corner_origin);
392  // Create |reserved_rect| in source's parent coordinates.
393  gfx::Rect reserved_rect(resize_corner_origin, browser_reserved_rect.size());
394  // Apply source's parent future offset to it.
395  reserved_rect.Offset(-future_parent_offset.x(), -future_parent_offset.y());
396  if (future_source_bounds.Intersects(reserved_rect)) {
397    // |source| is not properly positioned yet to use ConvertPointToView,
398    // so convert it into |source|'s coordinates manually.
399    reserved_rect.Offset(-future_source_bounds.x(), -future_source_bounds.y());
400  } else {
401    reserved_rect = gfx::Rect();
402  }
403
404  source->SetReservedContentsRect(reserved_rect);
405}
406
407void BrowserViewLayout::LayoutTabContents(int top, int bottom) {
408  // The ultimate idea is to calcualte bounds and reserved areas for all
409  // contents views first and then resize them all, so every view
410  // (and its contents) is resized and laid out only once.
411
412  // The views hierarcy (see browser_view.h for more details):
413  // 1) Sidebar is not allowed:
414  //     contents_split_ -> [contents_container_ | devtools]
415  // 2) Sidebar is allowed:
416  //     contents_split_ ->
417  //         [sidebar_split -> [contents_container_ | sidebar]] | devtools
418
419  gfx::Rect sidebar_split_bounds;
420  gfx::Rect contents_bounds;
421  gfx::Rect sidebar_bounds;
422  gfx::Rect devtools_bounds;
423
424  gfx::Rect contents_split_bounds(vertical_layout_rect_.x(), top,
425                                  vertical_layout_rect_.width(),
426                                  std::max(0, bottom - top));
427  contents_split_->CalculateChildrenBounds(
428      contents_split_bounds, &sidebar_split_bounds, &devtools_bounds);
429  gfx::Point contents_split_offset(
430      contents_split_bounds.x() - contents_split_->bounds().x(),
431      contents_split_bounds.y() - contents_split_->bounds().y());
432  gfx::Point sidebar_split_offset(contents_split_offset);
433  sidebar_split_offset.Offset(sidebar_split_bounds.x(),
434                              sidebar_split_bounds.y());
435
436  views::SingleSplitView* sidebar_split = browser_view_->sidebar_split_;
437  if (sidebar_split) {
438    DCHECK(sidebar_split == contents_split_->GetChildViewAt(0));
439    sidebar_split->CalculateChildrenBounds(
440        sidebar_split_bounds, &contents_bounds, &sidebar_bounds);
441  } else {
442    contents_bounds = sidebar_split_bounds;
443  }
444
445  // Layout resize corner, sidebar mini tabs and calculate reserved contents
446  // rects here as all contents view bounds are already determined, but not yet
447  // set at this point, so contents will be laid out once at most.
448  // TODO(alekseys): layout sidebar minitabs and adjust reserved rect
449  // accordingly.
450  gfx::Rect browser_reserved_rect;
451  if (!browser_view_->frame_->GetWindow()->IsMaximized() &&
452      !browser_view_->frame_->GetWindow()->IsFullscreen()) {
453    gfx::Size resize_corner_size = browser_view_->GetResizeCornerSize();
454    if (!resize_corner_size.IsEmpty()) {
455      gfx::Rect bounds = browser_view_->GetContentsBounds();
456      gfx::Point resize_corner_origin(
457          bounds.right() - resize_corner_size.width(),
458          bounds.bottom() - resize_corner_size.height());
459      browser_reserved_rect =
460          gfx::Rect(resize_corner_origin, resize_corner_size);
461    }
462  }
463
464  UpdateReservedContentsRect(browser_reserved_rect,
465                             browser_view_->contents_container_,
466                             contents_bounds,
467                             sidebar_split_offset);
468  if (sidebar_split) {
469    UpdateReservedContentsRect(browser_reserved_rect,
470                               browser_view_->sidebar_container_,
471                               sidebar_bounds,
472                               sidebar_split_offset);
473  }
474  UpdateReservedContentsRect(browser_reserved_rect,
475                             browser_view_->devtools_container_,
476                             devtools_bounds,
477                             contents_split_offset);
478
479  // Now it's safe to actually resize all contents views in the hierarchy.
480  contents_split_->SetBoundsRect(contents_split_bounds);
481  if (sidebar_split)
482    sidebar_split->SetBoundsRect(sidebar_split_bounds);
483}
484
485int BrowserViewLayout::GetTopMarginForActiveContent() {
486  if (!active_bookmark_bar_ || !browser_view_->IsBookmarkBarVisible() ||
487      !active_bookmark_bar_->IsDetached()) {
488    return 0;
489  }
490
491  if (contents_split_->GetChildViewAt(1) &&
492      contents_split_->GetChildViewAt(1)->IsVisible())
493    return 0;
494
495  if (SidebarManager::IsSidebarAllowed()) {
496    views::View* sidebar_split = contents_split_->GetChildViewAt(0);
497    if (sidebar_split->GetChildViewAt(1) &&
498        sidebar_split->GetChildViewAt(1)->IsVisible())
499      return 0;
500  }
501
502  // Adjust for separator.
503  return active_bookmark_bar_->height() -
504      views::NonClientFrameView::kClientEdgeThickness;
505}
506
507int BrowserViewLayout::LayoutDownloadShelf(int bottom) {
508  // Re-layout the shelf either if it is visible or if it's close animation
509  // is currently running.
510  if (browser_view_->IsDownloadShelfVisible() ||
511      (download_shelf_ && download_shelf_->IsClosing())) {
512    bool visible = browser()->SupportsWindowFeature(
513        Browser::FEATURE_DOWNLOADSHELF);
514    DCHECK(download_shelf_);
515    int height = visible ? download_shelf_->GetPreferredSize().height() : 0;
516    download_shelf_->SetVisible(visible);
517    download_shelf_->SetBounds(vertical_layout_rect_.x(), bottom - height,
518                               vertical_layout_rect_.width(), height);
519    download_shelf_->Layout();
520    bottom -= height;
521  }
522  return bottom;
523}
524
525bool BrowserViewLayout::InfobarVisible() const {
526  // NOTE: Can't check if the size IsEmpty() since it's always 0-width.
527  return browser()->SupportsWindowFeature(Browser::FEATURE_INFOBAR) &&
528      (infobar_container_->GetPreferredSize().height() != 0);
529}
530