browser_non_client_frame_view_ash.cc revision f8ee788a64d60abd8f2d742a5fdedde054ecd910
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/l10n/l10n_util.h"
35#include "ui/base/layout.h"
36#include "ui/base/resource/resource_bundle.h"
37#include "ui/base/theme_provider.h"
38#include "ui/compositor/layer_animator.h"
39#include "ui/gfx/canvas.h"
40#include "ui/gfx/image/image_skia.h"
41#include "ui/gfx/rect_conversions.h"
42#include "ui/views/controls/label.h"
43#include "ui/views/layout/layout_constants.h"
44#include "ui/views/widget/widget.h"
45#include "ui/views/widget/widget_delegate.h"
46
47namespace {
48
49// The avatar ends 2 px above the bottom of the tabstrip (which, given the
50// way the tabstrip draws its bottom edge, will appear like a 1 px gap to the
51// user).
52const int kAvatarBottomSpacing = 2;
53// There are 2 px on each side of the avatar (between the frame border and
54// it on the left, and between it and the tabstrip on the right).
55const int kAvatarSideSpacing = 2;
56// Space between left edge of window and tabstrip.
57const int kTabstripLeftSpacing = 0;
58// Space between right edge of tabstrip and maximize button.
59const int kTabstripRightSpacing = 10;
60// Height of the shadow of the content area, at the top of the toolbar.
61const int kContentShadowHeight = 1;
62// Space between top of window and top of tabstrip for tall headers, such as
63// for restored windows, apps, etc.
64const int kTabstripTopSpacingTall = 7;
65// Space between top of window and top of tabstrip for short headers, such as
66// for maximized windows, pop-ups, etc.
67const int kTabstripTopSpacingShort = 0;
68// Height of the shadow in the tab image, used to ensure clicks in the shadow
69// area still drag restored windows.  This keeps the clickable area large enough
70// to hit easily.
71const int kTabShadowHeight = 4;
72
73}  // namespace
74
75///////////////////////////////////////////////////////////////////////////////
76// BrowserNonClientFrameViewAsh, public:
77
78// static
79const char BrowserNonClientFrameViewAsh::kViewClassName[] =
80    "BrowserNonClientFrameViewAsh";
81
82BrowserNonClientFrameViewAsh::BrowserNonClientFrameViewAsh(
83    BrowserFrame* frame, BrowserView* browser_view)
84    : BrowserNonClientFrameView(frame, browser_view),
85      caption_button_container_(NULL),
86      window_icon_(NULL),
87      frame_border_hit_test_controller_(
88          new ash::FrameBorderHitTestController(frame)) {
89  ash::Shell::GetInstance()->AddShellObserver(this);
90}
91
92BrowserNonClientFrameViewAsh::~BrowserNonClientFrameViewAsh() {
93  ash::Shell::GetInstance()->RemoveShellObserver(this);
94}
95
96void BrowserNonClientFrameViewAsh::Init() {
97  caption_button_container_ = new ash::FrameCaptionButtonContainerView(frame(),
98      ash::FrameCaptionButtonContainerView::MINIMIZE_ALLOWED);
99  caption_button_container_->UpdateSizeButtonVisibility();
100  AddChildView(caption_button_container_);
101
102  // Initializing the TabIconView is expensive, so only do it if we need to.
103  if (browser_view()->ShouldShowWindowIcon()) {
104    window_icon_ = new TabIconView(this, NULL);
105    window_icon_->set_is_light(true);
106    AddChildView(window_icon_);
107    window_icon_->Update();
108  }
109
110  // Create incognito icon if necessary.
111  UpdateAvatarInfo();
112
113  // HeaderPainter handles layout.
114  if (UsePackagedAppHeaderStyle()) {
115    ash::DefaultHeaderPainter* header_painter = new ash::DefaultHeaderPainter;
116    header_painter_.reset(header_painter);
117    header_painter->Init(frame(), this, window_icon_,
118        caption_button_container_);
119  } else {
120    BrowserHeaderPainterAsh* header_painter = new BrowserHeaderPainterAsh;
121    header_painter_.reset(header_painter);
122    header_painter->Init(frame(), browser_view(), this, window_icon_,
123        caption_button_container_);
124  }
125}
126
127///////////////////////////////////////////////////////////////////////////////
128// BrowserNonClientFrameView:
129
130gfx::Rect BrowserNonClientFrameViewAsh::GetBoundsForTabStrip(
131    views::View* tabstrip) const {
132  if (!tabstrip)
133    return gfx::Rect();
134
135  // When the tab strip is painted in the immersive fullscreen light bar style,
136  // the caption buttons and the avatar button are not visible. However, their
137  // bounds are still used to compute the tab strip bounds so that the tabs have
138  // the same horizontal position when the tab strip is painted in the immersive
139  // light bar style as when the top-of-window views are revealed.
140  int left_inset = GetTabStripLeftInset();
141  int right_inset = GetTabStripRightInset();
142  return gfx::Rect(left_inset,
143                   GetTopInset(),
144                   std::max(0, width() - left_inset - right_inset),
145                   tabstrip->GetPreferredSize().height());
146}
147
148int BrowserNonClientFrameViewAsh::GetTopInset() const {
149  if (!ShouldPaint() || UseImmersiveLightbarHeaderStyle())
150    return 0;
151
152  if (browser_view()->IsTabStripVisible()) {
153    if (frame()->IsMaximized() || frame()->IsFullscreen())
154      return kTabstripTopSpacingShort;
155    else
156      return kTabstripTopSpacingTall;
157  }
158
159  if (UsePackagedAppHeaderStyle())
160    return header_painter_->GetHeaderHeightForPainting();
161
162  int caption_buttons_bottom = caption_button_container_->bounds().bottom();
163
164  // The toolbar partially overlaps the caption buttons.
165  if (browser_view()->IsToolbarVisible())
166    return caption_buttons_bottom - kContentShadowHeight;
167
168  return caption_buttons_bottom + kClientEdgeThickness;
169}
170
171int BrowserNonClientFrameViewAsh::GetThemeBackgroundXInset() const {
172  return ash::HeaderPainterUtil::GetThemeBackgroundXInset();
173}
174
175void BrowserNonClientFrameViewAsh::UpdateThrobber(bool running) {
176  if (window_icon_)
177    window_icon_->Update();
178}
179
180///////////////////////////////////////////////////////////////////////////////
181// views::NonClientFrameView:
182
183gfx::Rect BrowserNonClientFrameViewAsh::GetBoundsForClientView() const {
184  // The ClientView must be flush with the top edge of the widget so that the
185  // web contents can take up the entire screen in immersive fullscreen (with
186  // or without the top-of-window views revealed). When in immersive fullscreen
187  // and the top-of-window views are revealed, the TopContainerView paints the
188  // window header by redirecting paints from its background to
189  // BrowserNonClientFrameViewAsh.
190  return bounds();
191}
192
193gfx::Rect BrowserNonClientFrameViewAsh::GetWindowBoundsForClientBounds(
194    const gfx::Rect& client_bounds) const {
195  return client_bounds;
196}
197
198int BrowserNonClientFrameViewAsh::NonClientHitTest(const gfx::Point& point) {
199  int hit_test = ash::FrameBorderHitTestController::NonClientHitTest(this,
200      caption_button_container_, point);
201
202  // See if the point is actually within the avatar menu button or within
203  // the avatar label.
204  if (hit_test == HTCAPTION && ((avatar_button() &&
205       avatar_button()->GetMirroredBounds().Contains(point)) ||
206      (avatar_label() && avatar_label()->GetMirroredBounds().Contains(point))))
207      return HTCLIENT;
208
209  // When the window is restored we want a large click target above the tabs
210  // to drag the window, so redirect clicks in the tab's shadow to caption.
211  if (hit_test == HTCLIENT &&
212      !(frame()->IsMaximized() || frame()->IsFullscreen())) {
213    // Convert point to client coordinates.
214    gfx::Point client_point(point);
215    View::ConvertPointToTarget(this, frame()->client_view(), &client_point);
216    // Report hits in shadow at top of tabstrip as caption.
217    gfx::Rect tabstrip_bounds(browser_view()->tabstrip()->bounds());
218    if (client_point.y() < tabstrip_bounds.y() + kTabShadowHeight)
219      hit_test = HTCAPTION;
220  }
221  return hit_test;
222}
223
224void BrowserNonClientFrameViewAsh::GetWindowMask(const gfx::Size& size,
225                                                 gfx::Path* window_mask) {
226  // Aura does not use window masks.
227}
228
229void BrowserNonClientFrameViewAsh::ResetWindowControls() {
230  // Hide the caption buttons in immersive fullscreen when the tab light bar
231  // is visible because it's confusing when the user hovers or clicks in the
232  // top-right of the screen and hits one.
233  bool button_visibility = !UseImmersiveLightbarHeaderStyle();
234  caption_button_container_->SetVisible(button_visibility);
235
236  caption_button_container_->ResetWindowControls();
237}
238
239void BrowserNonClientFrameViewAsh::UpdateWindowIcon() {
240  if (window_icon_)
241    window_icon_->SchedulePaint();
242}
243
244void BrowserNonClientFrameViewAsh::UpdateWindowTitle() {
245  if (!frame()->IsFullscreen())
246    header_painter_->SchedulePaintForTitle();
247}
248
249///////////////////////////////////////////////////////////////////////////////
250// views::View:
251
252void BrowserNonClientFrameViewAsh::OnPaint(gfx::Canvas* canvas) {
253  if (!ShouldPaint())
254    return;
255
256  if (UseImmersiveLightbarHeaderStyle()) {
257    PaintImmersiveLightbarStyleHeader(canvas);
258    return;
259  }
260
261  caption_button_container_->SetPaintAsActive(ShouldPaintAsActive());
262
263  ash::HeaderPainter::Mode header_mode = ShouldPaintAsActive() ?
264      ash::HeaderPainter::MODE_ACTIVE : ash::HeaderPainter::MODE_INACTIVE;
265  header_painter_->PaintHeader(canvas, header_mode);
266  if (browser_view()->IsToolbarVisible())
267    PaintToolbarBackground(canvas);
268  else if (!UsePackagedAppHeaderStyle())
269    PaintContentEdge(canvas);
270}
271
272void BrowserNonClientFrameViewAsh::Layout() {
273  // The header must be laid out before computing |painted_height| because the
274  // computation of |painted_height| for app and popup windows depends on the
275  // position of the window controls.
276  header_painter_->LayoutHeader();
277
278  int painted_height = 0;
279  if (browser_view()->IsTabStripVisible()) {
280    painted_height = GetTopInset() +
281        browser_view()->tabstrip()->GetPreferredSize().height();
282  } else if (browser_view()->IsToolbarVisible()) {
283    // Paint the header so that it overlaps with the top few pixels of the
284    // toolbar because the top few pixels of the toolbar are not opaque.
285    painted_height = GetTopInset() + kFrameShadowThickness * 2;
286  } else {
287    painted_height = GetTopInset();
288  }
289  header_painter_->SetHeaderHeightForPainting(painted_height);
290  if (avatar_button())
291    LayoutAvatar();
292  BrowserNonClientFrameView::Layout();
293}
294
295const char* BrowserNonClientFrameViewAsh::GetClassName() const {
296  return kViewClassName;
297}
298
299bool BrowserNonClientFrameViewAsh::HitTestRect(const gfx::Rect& rect) const {
300  if (!views::View::HitTestRect(rect)) {
301    // |rect| is outside BrowserNonClientFrameViewAsh's bounds.
302    return false;
303  }
304
305  TabStrip* tabstrip = browser_view()->tabstrip();
306  if (tabstrip && browser_view()->IsTabStripVisible()) {
307    // Claim |rect| only if it is above the bottom of the tabstrip in a non-tab
308    // portion.
309    gfx::RectF rect_in_tabstrip_coords_f(rect);
310    View::ConvertRectToTarget(this, tabstrip, &rect_in_tabstrip_coords_f);
311    gfx::Rect rect_in_tabstrip_coords = gfx::ToEnclosingRect(
312        rect_in_tabstrip_coords_f);
313
314     if (rect_in_tabstrip_coords.y() > tabstrip->height())
315       return false;
316
317    return !tabstrip->HitTestRect(rect_in_tabstrip_coords) ||
318        tabstrip->IsRectInWindowCaption(rect_in_tabstrip_coords);
319  }
320
321  // Claim |rect| if it is above the top of the topmost view in the client area.
322  return rect.y() < GetTopInset();
323}
324
325void BrowserNonClientFrameViewAsh::GetAccessibleState(
326    ui::AXViewState* state) {
327  state->role = ui::AX_ROLE_TITLE_BAR;
328}
329
330gfx::Size BrowserNonClientFrameViewAsh::GetMinimumSize() const {
331  gfx::Size min_client_view_size(frame()->client_view()->GetMinimumSize());
332  int min_width = std::max(header_painter_->GetMinimumHeaderWidth(),
333                           min_client_view_size.width());
334  if (browser_view()->IsTabStripVisible()) {
335    // Ensure that the minimum width is enough to hold a minimum width tab strip
336    // at its usual insets.
337    int min_tabstrip_width =
338        browser_view()->tabstrip()->GetMinimumSize().width();
339    min_width = std::max(min_width,
340        min_tabstrip_width + GetTabStripLeftInset() + GetTabStripRightInset());
341  }
342  return gfx::Size(min_width, min_client_view_size.height());
343}
344
345///////////////////////////////////////////////////////////////////////////////
346// ash::ShellObserver:
347
348void BrowserNonClientFrameViewAsh::OnMaximizeModeStarted() {
349  caption_button_container_->UpdateSizeButtonVisibility();
350  InvalidateLayout();
351  frame()->client_view()->InvalidateLayout();
352  frame()->GetRootView()->Layout();
353}
354
355void BrowserNonClientFrameViewAsh::OnMaximizeModeEnded() {
356  caption_button_container_->UpdateSizeButtonVisibility();
357  InvalidateLayout();
358  frame()->client_view()->InvalidateLayout();
359  frame()->GetRootView()->Layout();
360}
361
362///////////////////////////////////////////////////////////////////////////////
363// chrome::TabIconViewModel:
364
365bool BrowserNonClientFrameViewAsh::ShouldTabIconViewAnimate() const {
366  // This function is queried during the creation of the window as the
367  // TabIconView we host is initialized, so we need to NULL check the selected
368  // WebContents because in this condition there is not yet a selected tab.
369  content::WebContents* current_tab = browser_view()->GetActiveWebContents();
370  return current_tab ? current_tab->IsLoading() : false;
371}
372
373gfx::ImageSkia BrowserNonClientFrameViewAsh::GetFaviconForTabIconView() {
374  views::WidgetDelegate* delegate = frame()->widget_delegate();
375  if (!delegate)
376    return gfx::ImageSkia();
377  return delegate->GetWindowIcon();
378}
379
380///////////////////////////////////////////////////////////////////////////////
381// views::View:
382
383void BrowserNonClientFrameViewAsh::
384    ChildPreferredSizeChanged(views::View* child) {
385// FrameCaptionButtonContainerView animates the visibility changes in
386// UpdateSizeButtonVisibility(false). Due to this a new size is not available
387// until the completion of the animation. Layout in response to the preferred
388// size changes.
389  if (child != caption_button_container_)
390    return;
391  frame()->GetRootView()->Layout();
392}
393
394///////////////////////////////////////////////////////////////////////////////
395// BrowserNonClientFrameViewAsh, private:
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