browser_non_client_frame_view_ash.cc revision c5cede9ae108bb15f6b7a8aea21c7e1fefa2834c
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 "ash/shell.h"
13#include "base/command_line.h"
14#include "chrome/browser/themes/theme_properties.h"
15#include "chrome/browser/ui/browser.h"
16#include "chrome/browser/ui/views/frame/browser_frame.h"
17#include "chrome/browser/ui/views/frame/browser_header_painter_ash.h"
18#include "chrome/browser/ui/views/frame/browser_view.h"
19#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
20#include "chrome/browser/ui/views/profiles/avatar_label.h"
21#include "chrome/browser/ui/views/profiles/avatar_menu_button.h"
22#include "chrome/browser/ui/views/tab_icon_view.h"
23#include "chrome/browser/ui/views/tabs/tab_strip.h"
24#include "chrome/common/chrome_switches.h"
25#include "content/public/browser/web_contents.h"
26#include "grit/ash_resources.h"
27#include "grit/theme_resources.h"
28#include "ui/accessibility/ax_view_state.h"
29#include "ui/aura/client/aura_constants.h"
30#include "ui/aura/window.h"
31#include "ui/base/hit_test.h"
32#include "ui/base/l10n/l10n_util.h"
33#include "ui/base/layout.h"
34#include "ui/base/resource/resource_bundle.h"
35#include "ui/base/theme_provider.h"
36#include "ui/compositor/layer_animator.h"
37#include "ui/gfx/canvas.h"
38#include "ui/gfx/image/image_skia.h"
39#include "ui/gfx/rect_conversions.h"
40#include "ui/views/controls/label.h"
41#include "ui/views/layout/layout_constants.h"
42#include "ui/views/widget/widget.h"
43#include "ui/views/widget/widget_delegate.h"
44
45namespace {
46
47// The avatar ends 2 px above the bottom of the tabstrip (which, given the
48// way the tabstrip draws its bottom edge, will appear like a 1 px gap to the
49// user).
50const int kAvatarBottomSpacing = 2;
51// There are 2 px on each side of the avatar (between the frame border and
52// it on the left, and between it and the tabstrip on the right).
53const int kAvatarSideSpacing = 2;
54// Space between left edge of window and tabstrip.
55const int kTabstripLeftSpacing = 0;
56// Space between right edge of tabstrip and maximize button.
57const int kTabstripRightSpacing = 10;
58// Height of the shadow of the content area, at the top of the toolbar.
59const int kContentShadowHeight = 1;
60// Space between top of window and top of tabstrip for tall headers, such as
61// for restored windows, apps, etc.
62const int kTabstripTopSpacingTall = 7;
63// Space between top of window and top of tabstrip for short headers, such as
64// for maximized windows, pop-ups, etc.
65const int kTabstripTopSpacingShort = 0;
66// Height of the shadow in the tab image, used to ensure clicks in the shadow
67// area still drag restored windows.  This keeps the clickable area large enough
68// to hit easily.
69const int kTabShadowHeight = 4;
70
71}  // namespace
72
73///////////////////////////////////////////////////////////////////////////////
74// BrowserNonClientFrameViewAsh, public:
75
76// static
77const char BrowserNonClientFrameViewAsh::kViewClassName[] =
78    "BrowserNonClientFrameViewAsh";
79
80BrowserNonClientFrameViewAsh::BrowserNonClientFrameViewAsh(
81    BrowserFrame* frame, BrowserView* browser_view)
82    : BrowserNonClientFrameView(frame, browser_view),
83      caption_button_container_(NULL),
84      window_icon_(NULL),
85      frame_border_hit_test_controller_(
86          new ash::FrameBorderHitTestController(frame)) {
87  ash::Shell::GetInstance()->AddShellObserver(this);
88}
89
90BrowserNonClientFrameViewAsh::~BrowserNonClientFrameViewAsh() {
91  ash::Shell::GetInstance()->RemoveShellObserver(this);
92}
93
94void BrowserNonClientFrameViewAsh::Init() {
95  caption_button_container_ = new ash::FrameCaptionButtonContainerView(frame(),
96      ash::FrameCaptionButtonContainerView::MINIMIZE_ALLOWED);
97  caption_button_container_->UpdateSizeButtonVisibility(
98      ash::Shell::GetInstance()->IsMaximizeModeWindowManagerEnabled());
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
298bool BrowserNonClientFrameViewAsh::HitTestRect(const gfx::Rect& rect) const {
299  if (!views::View::HitTestRect(rect)) {
300    // |rect| is outside BrowserNonClientFrameViewAsh's bounds.
301    return false;
302  }
303
304  TabStrip* tabstrip = browser_view()->tabstrip();
305  if (tabstrip && browser_view()->IsTabStripVisible()) {
306    // Claim |rect| only if it is above the bottom of the tabstrip in a non-tab
307    // portion.
308    gfx::RectF rect_in_tabstrip_coords_f(rect);
309    View::ConvertRectToTarget(this, tabstrip, &rect_in_tabstrip_coords_f);
310    gfx::Rect rect_in_tabstrip_coords = gfx::ToEnclosingRect(
311        rect_in_tabstrip_coords_f);
312
313     if (rect_in_tabstrip_coords.y() > tabstrip->height())
314       return false;
315
316    return !tabstrip->HitTestRect(rect_in_tabstrip_coords) ||
317        tabstrip->IsRectInWindowCaption(rect_in_tabstrip_coords);
318  }
319
320  // Claim |rect| if it is above the top of the topmost view in the client area.
321  return rect.y() < GetTopInset();
322}
323
324void BrowserNonClientFrameViewAsh::GetAccessibleState(
325    ui::AXViewState* state) {
326  state->role = ui::AX_ROLE_TITLE_BAR;
327}
328
329gfx::Size BrowserNonClientFrameViewAsh::GetMinimumSize() {
330  gfx::Size min_client_view_size(frame()->client_view()->GetMinimumSize());
331  int min_width = std::max(header_painter_->GetMinimumHeaderWidth(),
332                           min_client_view_size.width());
333  if (browser_view()->IsTabStripVisible()) {
334    // Ensure that the minimum width is enough to hold a minimum width tab strip
335    // at its usual insets.
336    int min_tabstrip_width =
337        browser_view()->tabstrip()->GetMinimumSize().width();
338    min_width = std::max(min_width,
339        min_tabstrip_width + GetTabStripLeftInset() + GetTabStripRightInset());
340  }
341  return gfx::Size(min_width, min_client_view_size.height());
342}
343
344///////////////////////////////////////////////////////////////////////////////
345// ash::ShellObserver:
346
347void BrowserNonClientFrameViewAsh::OnMaximizeModeStarted() {
348  caption_button_container_->UpdateSizeButtonVisibility(true);
349  InvalidateLayout();
350  frame()->client_view()->InvalidateLayout();
351  frame()->GetRootView()->Layout();
352}
353
354void BrowserNonClientFrameViewAsh::OnMaximizeModeEnded() {
355  caption_button_container_->UpdateSizeButtonVisibility(false);
356  InvalidateLayout();
357  frame()->client_view()->InvalidateLayout();
358  frame()->GetRootView()->Layout();
359}
360
361///////////////////////////////////////////////////////////////////////////////
362// chrome::TabIconViewModel:
363
364bool BrowserNonClientFrameViewAsh::ShouldTabIconViewAnimate() const {
365  // This function is queried during the creation of the window as the
366  // TabIconView we host is initialized, so we need to NULL check the selected
367  // WebContents because in this condition there is not yet a selected tab.
368  content::WebContents* current_tab = browser_view()->GetActiveWebContents();
369  return current_tab ? current_tab->IsLoading() : false;
370}
371
372gfx::ImageSkia BrowserNonClientFrameViewAsh::GetFaviconForTabIconView() {
373  views::WidgetDelegate* delegate = frame()->widget_delegate();
374  if (!delegate)
375    return gfx::ImageSkia();
376  return delegate->GetWindowIcon();
377}
378
379///////////////////////////////////////////////////////////////////////////////
380// BrowserNonClientFrameViewAsh, private:
381
382int BrowserNonClientFrameViewAsh::GetTabStripLeftInset() const {
383  return avatar_button() ? kAvatarSideSpacing +
384      browser_view()->GetOTRAvatarIcon().width() + kAvatarSideSpacing :
385      kTabstripLeftSpacing;
386}
387
388int BrowserNonClientFrameViewAsh::GetTabStripRightInset() const {
389  return caption_button_container_->GetPreferredSize().width() +
390      kTabstripRightSpacing;
391}
392
393bool BrowserNonClientFrameViewAsh::UseImmersiveLightbarHeaderStyle() const {
394  ImmersiveModeController* immersive_controller =
395      browser_view()->immersive_mode_controller();
396  return immersive_controller->IsEnabled() &&
397      !immersive_controller->IsRevealed() &&
398      browser_view()->IsTabStripVisible();
399}
400
401bool BrowserNonClientFrameViewAsh::UsePackagedAppHeaderStyle() const {
402  // Non streamlined hosted apps do not have a toolbar or tabstrip. Their header
403  // should look the same as the header for packaged apps. Streamlined hosted
404  // apps have a toolbar so should use the browser header style.
405  return browser_view()->browser()->is_app() &&
406      !CommandLine::ForCurrentProcess()->HasSwitch(
407          switches::kEnableStreamlinedHostedApps);
408}
409
410void BrowserNonClientFrameViewAsh::LayoutAvatar() {
411  DCHECK(avatar_button());
412#if !defined(OS_CHROMEOS)
413  // ChromeOS shows avatar on V1 app.
414  DCHECK(browser_view()->IsTabStripVisible());
415#endif
416  gfx::ImageSkia incognito_icon = browser_view()->GetOTRAvatarIcon();
417
418  int avatar_bottom = GetTopInset() +
419      browser_view()->GetTabStripHeight() - kAvatarBottomSpacing;
420  int avatar_restored_y = avatar_bottom - incognito_icon.height();
421  int avatar_y =
422      (browser_view()->IsTabStripVisible() &&
423       (frame()->IsMaximized() || frame()->IsFullscreen())) ?
424      GetTopInset() + kContentShadowHeight : avatar_restored_y;
425
426  // Hide the incognito icon in immersive fullscreen when the tab light bar is
427  // visible because the header is too short for the icognito icon to be
428  // recognizable.
429  bool avatar_visible = !UseImmersiveLightbarHeaderStyle();
430  int avatar_height = avatar_visible ? avatar_bottom - avatar_y : 0;
431
432  gfx::Rect avatar_bounds(kAvatarSideSpacing,
433                          avatar_y,
434                          incognito_icon.width(),
435                          avatar_height);
436  avatar_button()->SetBoundsRect(avatar_bounds);
437  avatar_button()->SetVisible(avatar_visible);
438}
439
440bool BrowserNonClientFrameViewAsh::ShouldPaint() const {
441  if (!frame()->IsFullscreen())
442    return true;
443
444  // We need to paint when in immersive fullscreen and either:
445  // - The top-of-window views are revealed.
446  // - The lightbar style tabstrip is visible.
447  ImmersiveModeController* immersive_mode_controller =
448      browser_view()->immersive_mode_controller();
449  return immersive_mode_controller->IsEnabled() &&
450      (immersive_mode_controller->IsRevealed() ||
451       UseImmersiveLightbarHeaderStyle());
452}
453
454void BrowserNonClientFrameViewAsh::PaintImmersiveLightbarStyleHeader(
455    gfx::Canvas* canvas) {
456  // The light bar header is not themed because theming it does not look good.
457  gfx::ImageSkia* frame_image = GetThemeProvider()->GetImageSkiaNamed(
458      IDR_ASH_BROWSER_WINDOW_HEADER_BASE_MAXIMIZED);
459  canvas->TileImageInt(*frame_image, 0, 0, width(), frame_image->height());
460}
461
462void BrowserNonClientFrameViewAsh::PaintToolbarBackground(gfx::Canvas* canvas) {
463  gfx::Rect toolbar_bounds(browser_view()->GetToolbarBounds());
464  if (toolbar_bounds.IsEmpty())
465    return;
466  gfx::Point toolbar_origin(toolbar_bounds.origin());
467  View::ConvertPointToTarget(browser_view(), this, &toolbar_origin);
468  toolbar_bounds.set_origin(toolbar_origin);
469
470  int x = toolbar_bounds.x();
471  int w = toolbar_bounds.width();
472  int y = toolbar_bounds.y();
473  int h = toolbar_bounds.height();
474
475  // Gross hack: We split the toolbar images into two pieces, since sometimes
476  // (popup mode) the toolbar isn't tall enough to show the whole image.  The
477  // split happens between the top shadow section and the bottom gradient
478  // section so that we never break the gradient.
479  // NOTE(pkotwicz): If the computation for |bottom_y| is changed, Layout() must
480  // be changed as well.
481  int split_point = kFrameShadowThickness * 2;
482  int bottom_y = y + split_point;
483  ui::ThemeProvider* tp = GetThemeProvider();
484  int bottom_edge_height = h - split_point;
485
486  canvas->FillRect(gfx::Rect(x, bottom_y, w, bottom_edge_height),
487                   tp->GetColor(ThemeProperties::COLOR_TOOLBAR));
488
489  // Paint the main toolbar image.  Since this image is also used to draw the
490  // tab background, we must use the tab strip offset to compute the image
491  // source y position.  If you have to debug this code use an image editor
492  // to paint a diagonal line through the toolbar image and ensure it lines up
493  // across the tab and toolbar.
494  gfx::ImageSkia* theme_toolbar = tp->GetImageSkiaNamed(IDR_THEME_TOOLBAR);
495  canvas->TileImageInt(
496      *theme_toolbar,
497      x + GetThemeBackgroundXInset(),
498      bottom_y - GetTopInset(),
499      x, bottom_y,
500      w, theme_toolbar->height());
501
502  // The content area line has a shadow that extends a couple of pixels above
503  // the toolbar bounds.
504  const int kContentShadowHeight = 2;
505  gfx::ImageSkia* toolbar_top = tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_TOP);
506  canvas->TileImageInt(*toolbar_top,
507                       0, 0,
508                       x, y - kContentShadowHeight,
509                       w, split_point + kContentShadowHeight + 1);
510
511  // Draw the "lightening" shade line around the edges of the toolbar.
512  gfx::ImageSkia* toolbar_left = tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_LEFT);
513  canvas->TileImageInt(*toolbar_left,
514                       0, 0,
515                       x + kClientEdgeThickness,
516                       y + kClientEdgeThickness + kContentShadowHeight,
517                       toolbar_left->width(), theme_toolbar->height());
518  gfx::ImageSkia* toolbar_right =
519      tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_RIGHT);
520  canvas->TileImageInt(*toolbar_right,
521                       0, 0,
522                       w - toolbar_right->width() - 2 * kClientEdgeThickness,
523                       y + kClientEdgeThickness + kContentShadowHeight,
524                       toolbar_right->width(), theme_toolbar->height());
525
526  // Draw the content/toolbar separator.
527  canvas->FillRect(
528      gfx::Rect(x + kClientEdgeThickness,
529                toolbar_bounds.bottom() - kClientEdgeThickness,
530                w - (2 * kClientEdgeThickness),
531                kClientEdgeThickness),
532      ThemeProperties::GetDefaultColor(
533          ThemeProperties::COLOR_TOOLBAR_SEPARATOR));
534}
535
536void BrowserNonClientFrameViewAsh::PaintContentEdge(gfx::Canvas* canvas) {
537  DCHECK(!UsePackagedAppHeaderStyle());
538  canvas->FillRect(gfx::Rect(0, caption_button_container_->bounds().bottom(),
539                             width(), kClientEdgeThickness),
540                   ThemeProperties::GetDefaultColor(
541                       ThemeProperties::COLOR_TOOLBAR_SEPARATOR));
542}
543