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