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