browser_non_client_frame_view_ash.cc revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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 "chrome/browser/ui/views/frame/browser_non_client_frame_view_ash.h"
6
7#include "ash/ash_switches.h"
8#include "ash/frame/caption_buttons/frame_caption_button_container_view.h"
9#include "ash/frame/default_header_painter.h"
10#include "ash/frame/frame_border_hit_test_controller.h"
11#include "ash/frame/header_painter_util.h"
12#include "base/command_line.h"
13#include "chrome/browser/themes/theme_properties.h"
14#include "chrome/browser/ui/browser.h"
15#include "chrome/browser/ui/views/avatar_label.h"
16#include "chrome/browser/ui/views/avatar_menu_button.h"
17#include "chrome/browser/ui/views/frame/browser_frame.h"
18#include "chrome/browser/ui/views/frame/browser_header_painter_ash.h"
19#include "chrome/browser/ui/views/frame/browser_view.h"
20#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
21#include "chrome/browser/ui/views/tab_icon_view.h"
22#include "chrome/browser/ui/views/tabs/tab_strip.h"
23#include "chrome/common/chrome_switches.h"
24#include "content/public/browser/web_contents.h"
25#include "grit/ash_resources.h"
26#include "grit/theme_resources.h"
27#include "ui/accessibility/ax_view_state.h"
28#include "ui/aura/client/aura_constants.h"
29#include "ui/aura/window.h"
30#include "ui/base/hit_test.h"
31#include "ui/base/l10n/l10n_util.h"
32#include "ui/base/layout.h"
33#include "ui/base/resource/resource_bundle.h"
34#include "ui/base/theme_provider.h"
35#include "ui/compositor/layer_animator.h"
36#include "ui/gfx/canvas.h"
37#include "ui/gfx/image/image_skia.h"
38#include "ui/gfx/rect_conversions.h"
39#include "ui/views/controls/label.h"
40#include "ui/views/layout/layout_constants.h"
41#include "ui/views/widget/widget.h"
42#include "ui/views/widget/widget_delegate.h"
43
44namespace {
45
46// The avatar ends 2 px above the bottom of the tabstrip (which, given the
47// way the tabstrip draws its bottom edge, will appear like a 1 px gap to the
48// user).
49const int kAvatarBottomSpacing = 2;
50// There are 2 px on each side of the avatar (between the frame border and
51// it on the left, and between it and the tabstrip on the right).
52const int kAvatarSideSpacing = 2;
53// Space between left edge of window and tabstrip.
54const int kTabstripLeftSpacing = 0;
55// Space between right edge of tabstrip and maximize button.
56const int kTabstripRightSpacing = 10;
57// Height of the shadow of the content area, at the top of the toolbar.
58const int kContentShadowHeight = 1;
59// Space between top of window and top of tabstrip for tall headers, such as
60// for restored windows, apps, etc.
61const int kTabstripTopSpacingTall = 7;
62// Space between top of window and top of tabstrip for short headers, such as
63// for maximized windows, pop-ups, etc.
64const int kTabstripTopSpacingShort = 0;
65// Height of the shadow in the tab image, used to ensure clicks in the shadow
66// area still drag restored windows.  This keeps the clickable area large enough
67// to hit easily.
68const int kTabShadowHeight = 4;
69
70}  // namespace
71
72///////////////////////////////////////////////////////////////////////////////
73// BrowserNonClientFrameViewAsh, public:
74
75// static
76const char BrowserNonClientFrameViewAsh::kViewClassName[] =
77    "BrowserNonClientFrameViewAsh";
78
79BrowserNonClientFrameViewAsh::BrowserNonClientFrameViewAsh(
80    BrowserFrame* frame, BrowserView* browser_view)
81    : BrowserNonClientFrameView(frame, browser_view),
82      caption_button_container_(NULL),
83      window_icon_(NULL),
84      frame_border_hit_test_controller_(
85          new ash::FrameBorderHitTestController(frame)) {
86}
87
88BrowserNonClientFrameViewAsh::~BrowserNonClientFrameViewAsh() {
89}
90
91void BrowserNonClientFrameViewAsh::Init() {
92  caption_button_container_ = new ash::FrameCaptionButtonContainerView(frame(),
93      ash::FrameCaptionButtonContainerView::MINIMIZE_ALLOWED);
94  AddChildView(caption_button_container_);
95
96  // Initializing the TabIconView is expensive, so only do it if we need to.
97  if (browser_view()->ShouldShowWindowIcon()) {
98    window_icon_ = new TabIconView(this, NULL);
99    window_icon_->set_is_light(true);
100    AddChildView(window_icon_);
101    window_icon_->Update();
102  }
103
104  // Create incognito icon if necessary.
105  UpdateAvatarInfo();
106
107  // HeaderPainter handles layout.
108  if (UsePackagedAppHeaderStyle()) {
109    ash::DefaultHeaderPainter* header_painter = new ash::DefaultHeaderPainter;
110    header_painter_.reset(header_painter);
111    header_painter->Init(frame(), this, window_icon_,
112        caption_button_container_);
113  } else {
114    BrowserHeaderPainterAsh* header_painter = new BrowserHeaderPainterAsh;
115    header_painter_.reset(header_painter);
116    header_painter->Init(frame(), browser_view(), this, window_icon_,
117        caption_button_container_);
118  }
119}
120
121///////////////////////////////////////////////////////////////////////////////
122// BrowserNonClientFrameView overrides:
123
124gfx::Rect BrowserNonClientFrameViewAsh::GetBoundsForTabStrip(
125    views::View* tabstrip) const {
126  if (!tabstrip)
127    return gfx::Rect();
128
129  // When the tab strip is painted in the immersive fullscreen light bar style,
130  // the caption buttons and the avatar button are not visible. However, their
131  // bounds are still used to compute the tab strip bounds so that the tabs have
132  // the same horizontal position when the tab strip is painted in the immersive
133  // light bar style as when the top-of-window views are revealed.
134  int left_inset = GetTabStripLeftInset();
135  int right_inset = GetTabStripRightInset();
136  return gfx::Rect(left_inset,
137                   GetTopInset(),
138                   std::max(0, width() - left_inset - right_inset),
139                   tabstrip->GetPreferredSize().height());
140}
141
142int BrowserNonClientFrameViewAsh::GetTopInset() const {
143  if (!ShouldPaint() || UseImmersiveLightbarHeaderStyle())
144    return 0;
145
146  if (browser_view()->IsTabStripVisible()) {
147    if (frame()->IsMaximized() || frame()->IsFullscreen())
148      return kTabstripTopSpacingShort;
149    else
150      return kTabstripTopSpacingTall;
151  }
152
153  if (UsePackagedAppHeaderStyle())
154    return header_painter_->GetHeaderHeightForPainting();
155
156  int caption_buttons_bottom = caption_button_container_->bounds().bottom();
157
158  // The toolbar partially overlaps the caption buttons.
159  if (browser_view()->IsToolbarVisible())
160    return caption_buttons_bottom - kContentShadowHeight;
161
162  return caption_buttons_bottom + kClientEdgeThickness;
163}
164
165int BrowserNonClientFrameViewAsh::GetThemeBackgroundXInset() const {
166  return ash::HeaderPainterUtil::GetThemeBackgroundXInset();
167}
168
169void BrowserNonClientFrameViewAsh::UpdateThrobber(bool running) {
170  if (window_icon_)
171    window_icon_->Update();
172}
173
174///////////////////////////////////////////////////////////////////////////////
175// views::NonClientFrameView overrides:
176
177gfx::Rect BrowserNonClientFrameViewAsh::GetBoundsForClientView() const {
178  // The ClientView must be flush with the top edge of the widget so that the
179  // web contents can take up the entire screen in immersive fullscreen (with
180  // or without the top-of-window views revealed). When in immersive fullscreen
181  // and the top-of-window views are revealed, the TopContainerView paints the
182  // window header by redirecting paints from its background to
183  // BrowserNonClientFrameViewAsh.
184  return bounds();
185}
186
187gfx::Rect BrowserNonClientFrameViewAsh::GetWindowBoundsForClientBounds(
188    const gfx::Rect& client_bounds) const {
189  return client_bounds;
190}
191
192int BrowserNonClientFrameViewAsh::NonClientHitTest(const gfx::Point& point) {
193  int hit_test = ash::FrameBorderHitTestController::NonClientHitTest(this,
194      caption_button_container_, point);
195
196  // See if the point is actually within the avatar menu button or within
197  // the avatar label.
198  if (hit_test == HTCAPTION && ((avatar_button() &&
199       avatar_button()->GetMirroredBounds().Contains(point)) ||
200      (avatar_label() && avatar_label()->GetMirroredBounds().Contains(point))))
201      return HTCLIENT;
202
203  // When the window is restored we want a large click target above the tabs
204  // to drag the window, so redirect clicks in the tab's shadow to caption.
205  if (hit_test == HTCLIENT &&
206      !(frame()->IsMaximized() || frame()->IsFullscreen())) {
207    // Convert point to client coordinates.
208    gfx::Point client_point(point);
209    View::ConvertPointToTarget(this, frame()->client_view(), &client_point);
210    // Report hits in shadow at top of tabstrip as caption.
211    gfx::Rect tabstrip_bounds(browser_view()->tabstrip()->bounds());
212    if (client_point.y() < tabstrip_bounds.y() + kTabShadowHeight)
213      hit_test = HTCAPTION;
214  }
215  return hit_test;
216}
217
218void BrowserNonClientFrameViewAsh::GetWindowMask(const gfx::Size& size,
219                                                 gfx::Path* window_mask) {
220  // Aura does not use window masks.
221}
222
223void BrowserNonClientFrameViewAsh::ResetWindowControls() {
224  // Hide the caption buttons in immersive fullscreen when the tab light bar
225  // is visible because it's confusing when the user hovers or clicks in the
226  // top-right of the screen and hits one.
227  bool button_visibility = !UseImmersiveLightbarHeaderStyle();
228  caption_button_container_->SetVisible(button_visibility);
229
230  caption_button_container_->ResetWindowControls();
231}
232
233void BrowserNonClientFrameViewAsh::UpdateWindowIcon() {
234  if (window_icon_)
235    window_icon_->SchedulePaint();
236}
237
238void BrowserNonClientFrameViewAsh::UpdateWindowTitle() {
239  if (!frame()->IsFullscreen())
240    header_painter_->SchedulePaintForTitle();
241}
242
243///////////////////////////////////////////////////////////////////////////////
244// views::View overrides:
245
246void BrowserNonClientFrameViewAsh::OnPaint(gfx::Canvas* canvas) {
247  if (!ShouldPaint())
248    return;
249
250  if (UseImmersiveLightbarHeaderStyle()) {
251    PaintImmersiveLightbarStyleHeader(canvas);
252    return;
253  }
254
255  caption_button_container_->SetPaintAsActive(ShouldPaintAsActive());
256
257  ash::HeaderPainter::Mode header_mode = ShouldPaintAsActive() ?
258      ash::HeaderPainter::MODE_ACTIVE : ash::HeaderPainter::MODE_INACTIVE;
259  header_painter_->PaintHeader(canvas, header_mode);
260  if (browser_view()->IsToolbarVisible())
261    PaintToolbarBackground(canvas);
262  else if (!UsePackagedAppHeaderStyle())
263    PaintContentEdge(canvas);
264}
265
266void BrowserNonClientFrameViewAsh::Layout() {
267  // The header must be laid out before computing |painted_height| because the
268  // computation of |painted_height| for app and popup windows depends on the
269  // position of the window controls.
270  header_painter_->LayoutHeader();
271
272  int painted_height = 0;
273  if (browser_view()->IsTabStripVisible()) {
274    painted_height = GetTopInset() +
275        browser_view()->tabstrip()->GetPreferredSize().height();
276  } else if (browser_view()->IsToolbarVisible()) {
277    // Paint the header so that it overlaps with the top few pixels of the
278    // toolbar because the top few pixels of the toolbar are not opaque.
279    painted_height = GetTopInset() + kFrameShadowThickness * 2;
280  } else {
281    painted_height = GetTopInset();
282  }
283  header_painter_->SetHeaderHeightForPainting(painted_height);
284  if (avatar_button())
285    LayoutAvatar();
286  BrowserNonClientFrameView::Layout();
287}
288
289const char* BrowserNonClientFrameViewAsh::GetClassName() const {
290  return kViewClassName;
291}
292
293bool BrowserNonClientFrameViewAsh::HitTestRect(const gfx::Rect& rect) const {
294  if (!views::View::HitTestRect(rect)) {
295    // |rect| is outside BrowserNonClientFrameViewAsh's bounds.
296    return false;
297  }
298
299  TabStrip* tabstrip = browser_view()->tabstrip();
300  if (tabstrip && browser_view()->IsTabStripVisible()) {
301    // Claim |rect| only if it is above the bottom of the tabstrip in a non-tab
302    // portion.
303    gfx::RectF rect_in_tabstrip_coords_f(rect);
304    View::ConvertRectToTarget(this, tabstrip, &rect_in_tabstrip_coords_f);
305    gfx::Rect rect_in_tabstrip_coords = gfx::ToEnclosingRect(
306        rect_in_tabstrip_coords_f);
307
308     if (rect_in_tabstrip_coords.y() > tabstrip->height())
309       return false;
310
311    return !tabstrip->HitTestRect(rect_in_tabstrip_coords) ||
312        tabstrip->IsRectInWindowCaption(rect_in_tabstrip_coords);
313  }
314
315  // Claim |rect| if it is above the top of the topmost view in the client area.
316  return rect.y() < GetTopInset();
317}
318
319void BrowserNonClientFrameViewAsh::GetAccessibleState(
320    ui::AXViewState* state) {
321  state->role = ui::AX_ROLE_TITLE_BAR;
322}
323
324gfx::Size BrowserNonClientFrameViewAsh::GetMinimumSize() {
325  gfx::Size min_client_view_size(frame()->client_view()->GetMinimumSize());
326  int min_width = std::max(header_painter_->GetMinimumHeaderWidth(),
327                           min_client_view_size.width());
328  if (browser_view()->IsTabStripVisible()) {
329    // Ensure that the minimum width is enough to hold a minimum width tab strip
330    // at its usual insets.
331    int min_tabstrip_width =
332        browser_view()->tabstrip()->GetMinimumSize().width();
333    min_width = std::max(min_width,
334        min_tabstrip_width + GetTabStripLeftInset() + GetTabStripRightInset());
335  }
336  return gfx::Size(min_width, min_client_view_size.height());
337}
338
339///////////////////////////////////////////////////////////////////////////////
340// chrome::TabIconViewModel overrides:
341
342bool BrowserNonClientFrameViewAsh::ShouldTabIconViewAnimate() const {
343  // This function is queried during the creation of the window as the
344  // TabIconView we host is initialized, so we need to NULL check the selected
345  // WebContents because in this condition there is not yet a selected tab.
346  content::WebContents* current_tab = browser_view()->GetActiveWebContents();
347  return current_tab ? current_tab->IsLoading() : false;
348}
349
350gfx::ImageSkia BrowserNonClientFrameViewAsh::GetFaviconForTabIconView() {
351  views::WidgetDelegate* delegate = frame()->widget_delegate();
352  if (!delegate)
353    return gfx::ImageSkia();
354  return delegate->GetWindowIcon();
355}
356
357///////////////////////////////////////////////////////////////////////////////
358// BrowserNonClientFrameViewAsh, private:
359
360int BrowserNonClientFrameViewAsh::GetTabStripLeftInset() const {
361  return avatar_button() ? kAvatarSideSpacing +
362      browser_view()->GetOTRAvatarIcon().width() + kAvatarSideSpacing :
363      kTabstripLeftSpacing;
364}
365
366int BrowserNonClientFrameViewAsh::GetTabStripRightInset() const {
367  return caption_button_container_->GetPreferredSize().width() +
368      kTabstripRightSpacing;
369}
370
371bool BrowserNonClientFrameViewAsh::UseImmersiveLightbarHeaderStyle() const {
372  ImmersiveModeController* immersive_controller =
373      browser_view()->immersive_mode_controller();
374  return immersive_controller->IsEnabled() &&
375      !immersive_controller->IsRevealed() &&
376      browser_view()->IsTabStripVisible();
377}
378
379bool BrowserNonClientFrameViewAsh::UsePackagedAppHeaderStyle() const {
380  // Non streamlined hosted apps do not have a toolbar or tabstrip. Their header
381  // should look the same as the header for packaged apps. Streamlined hosted
382  // apps have a toolbar so should use the browser header style.
383  return browser_view()->browser()->is_app() &&
384      !CommandLine::ForCurrentProcess()->HasSwitch(
385          switches::kEnableStreamlinedHostedApps);
386}
387
388void BrowserNonClientFrameViewAsh::LayoutAvatar() {
389  DCHECK(avatar_button());
390  DCHECK(browser_view()->IsTabStripVisible());
391  gfx::ImageSkia incognito_icon = browser_view()->GetOTRAvatarIcon();
392
393  int avatar_bottom = GetTopInset() +
394      browser_view()->GetTabStripHeight() - kAvatarBottomSpacing;
395  int avatar_restored_y = avatar_bottom - incognito_icon.height();
396  int avatar_y = (frame()->IsMaximized() || frame()->IsFullscreen()) ?
397      GetTopInset() + kContentShadowHeight : avatar_restored_y;
398
399  // Hide the incognito icon in immersive fullscreen when the tab light bar is
400  // visible because the header is too short for the icognito icon to be
401  // recognizable.
402  bool avatar_visible = !UseImmersiveLightbarHeaderStyle();
403  int avatar_height = avatar_visible ? avatar_bottom - avatar_y : 0;
404
405  gfx::Rect avatar_bounds(kAvatarSideSpacing,
406                          avatar_y,
407                          incognito_icon.width(),
408                          avatar_height);
409  avatar_button()->SetBoundsRect(avatar_bounds);
410  avatar_button()->SetVisible(avatar_visible);
411}
412
413bool BrowserNonClientFrameViewAsh::ShouldPaint() const {
414  if (!frame()->IsFullscreen())
415    return true;
416
417  // We need to paint when in immersive fullscreen and either:
418  // - The top-of-window views are revealed.
419  // - The lightbar style tabstrip is visible.
420  ImmersiveModeController* immersive_mode_controller =
421      browser_view()->immersive_mode_controller();
422  return immersive_mode_controller->IsEnabled() &&
423      (immersive_mode_controller->IsRevealed() ||
424       UseImmersiveLightbarHeaderStyle());
425}
426
427void BrowserNonClientFrameViewAsh::PaintImmersiveLightbarStyleHeader(
428    gfx::Canvas* canvas) {
429  // The light bar header is not themed because theming it does not look good.
430  gfx::ImageSkia* frame_image = GetThemeProvider()->GetImageSkiaNamed(
431      IDR_AURA_BROWSER_WINDOW_HEADER_BASE_MAXIMIZED);
432  canvas->TileImageInt(*frame_image, 0, 0, width(), frame_image->height());
433}
434
435void BrowserNonClientFrameViewAsh::PaintToolbarBackground(gfx::Canvas* canvas) {
436  gfx::Rect toolbar_bounds(browser_view()->GetToolbarBounds());
437  if (toolbar_bounds.IsEmpty())
438    return;
439  gfx::Point toolbar_origin(toolbar_bounds.origin());
440  View::ConvertPointToTarget(browser_view(), this, &toolbar_origin);
441  toolbar_bounds.set_origin(toolbar_origin);
442
443  int x = toolbar_bounds.x();
444  int w = toolbar_bounds.width();
445  int y = toolbar_bounds.y();
446  int h = toolbar_bounds.height();
447
448  // Gross hack: We split the toolbar images into two pieces, since sometimes
449  // (popup mode) the toolbar isn't tall enough to show the whole image.  The
450  // split happens between the top shadow section and the bottom gradient
451  // section so that we never break the gradient.
452  // NOTE(pkotwicz): If the computation for |bottom_y| is changed, Layout() must
453  // be changed as well.
454  int split_point = kFrameShadowThickness * 2;
455  int bottom_y = y + split_point;
456  ui::ThemeProvider* tp = GetThemeProvider();
457  int bottom_edge_height = h - split_point;
458
459  canvas->FillRect(gfx::Rect(x, bottom_y, w, bottom_edge_height),
460                   tp->GetColor(ThemeProperties::COLOR_TOOLBAR));
461
462  // Paint the main toolbar image.  Since this image is also used to draw the
463  // tab background, we must use the tab strip offset to compute the image
464  // source y position.  If you have to debug this code use an image editor
465  // to paint a diagonal line through the toolbar image and ensure it lines up
466  // across the tab and toolbar.
467  gfx::ImageSkia* theme_toolbar = tp->GetImageSkiaNamed(IDR_THEME_TOOLBAR);
468  canvas->TileImageInt(
469      *theme_toolbar,
470      x + GetThemeBackgroundXInset(),
471      bottom_y - GetTopInset(),
472      x, bottom_y,
473      w, theme_toolbar->height());
474
475  // The content area line has a shadow that extends a couple of pixels above
476  // the toolbar bounds.
477  const int kContentShadowHeight = 2;
478  gfx::ImageSkia* toolbar_top = tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_TOP);
479  canvas->TileImageInt(*toolbar_top,
480                       0, 0,
481                       x, y - kContentShadowHeight,
482                       w, split_point + kContentShadowHeight + 1);
483
484  // Draw the "lightening" shade line around the edges of the toolbar.
485  gfx::ImageSkia* toolbar_left = tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_LEFT);
486  canvas->TileImageInt(*toolbar_left,
487                       0, 0,
488                       x + kClientEdgeThickness,
489                       y + kClientEdgeThickness + kContentShadowHeight,
490                       toolbar_left->width(), theme_toolbar->height());
491  gfx::ImageSkia* toolbar_right =
492      tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_RIGHT);
493  canvas->TileImageInt(*toolbar_right,
494                       0, 0,
495                       w - toolbar_right->width() - 2 * kClientEdgeThickness,
496                       y + kClientEdgeThickness + kContentShadowHeight,
497                       toolbar_right->width(), theme_toolbar->height());
498
499  // Draw the content/toolbar separator.
500  canvas->FillRect(
501      gfx::Rect(x + kClientEdgeThickness,
502                toolbar_bounds.bottom() - kClientEdgeThickness,
503                w - (2 * kClientEdgeThickness),
504                kClientEdgeThickness),
505      ThemeProperties::GetDefaultColor(
506          ThemeProperties::COLOR_TOOLBAR_SEPARATOR));
507}
508
509void BrowserNonClientFrameViewAsh::PaintContentEdge(gfx::Canvas* canvas) {
510  DCHECK(!UsePackagedAppHeaderStyle());
511  canvas->FillRect(gfx::Rect(0, caption_button_container_->bounds().bottom(),
512                             width(), kClientEdgeThickness),
513                   ThemeProperties::GetDefaultColor(
514                       ThemeProperties::COLOR_TOOLBAR_SEPARATOR));
515}
516