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