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