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