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