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/tabs/tab.h" 6 7#include <limits> 8 9#include "base/command_line.h" 10#include "base/debug/alias.h" 11#include "base/strings/utf_string_conversions.h" 12#include "chrome/browser/defaults.h" 13#include "chrome/browser/themes/theme_properties.h" 14#include "chrome/browser/ui/browser.h" 15#include "chrome/browser/ui/tab_contents/core_tab_helper.h" 16#include "chrome/browser/ui/tabs/tab_resources.h" 17#include "chrome/browser/ui/tabs/tab_utils.h" 18#include "chrome/browser/ui/view_ids.h" 19#include "chrome/browser/ui/views/tabs/media_indicator_button.h" 20#include "chrome/browser/ui/views/tabs/tab_controller.h" 21#include "chrome/browser/ui/views/theme_image_mapper.h" 22#include "chrome/browser/ui/views/touch_uma/touch_uma.h" 23#include "chrome/common/chrome_switches.h" 24#include "chrome/grit/generated_resources.h" 25#include "content/public/browser/user_metrics.h" 26#include "grit/theme_resources.h" 27#include "third_party/skia/include/effects/SkGradientShader.h" 28#include "ui/accessibility/ax_view_state.h" 29#include "ui/aura/env.h" 30#include "ui/base/l10n/l10n_util.h" 31#include "ui/base/models/list_selection_model.h" 32#include "ui/base/resource/resource_bundle.h" 33#include "ui/base/theme_provider.h" 34#include "ui/gfx/animation/animation_container.h" 35#include "ui/gfx/animation/multi_animation.h" 36#include "ui/gfx/animation/throb_animation.h" 37#include "ui/gfx/canvas.h" 38#include "ui/gfx/color_analysis.h" 39#include "ui/gfx/favicon_size.h" 40#include "ui/gfx/image/image_skia_operations.h" 41#include "ui/gfx/path.h" 42#include "ui/gfx/rect_conversions.h" 43#include "ui/gfx/skia_util.h" 44#include "ui/resources/grit/ui_resources.h" 45#include "ui/views/border.h" 46#include "ui/views/controls/button/image_button.h" 47#include "ui/views/controls/label.h" 48#include "ui/views/rect_based_targeting_utils.h" 49#include "ui/views/view_targeter.h" 50#include "ui/views/widget/tooltip_manager.h" 51#include "ui/views/widget/widget.h" 52#include "ui/views/window/non_client_view.h" 53 54using base::UserMetricsAction; 55 56namespace { 57 58// Padding around the "content" of a tab, occupied by the tab border graphics. 59const int kLeftPadding = 22; 60const int kTopPadding = 4; 61const int kRightPadding = 17; 62const int kBottomPadding = 2; 63 64// Height of the shadow at the top of the tab image assets. 65const int kDropShadowHeight = 4; 66 67// How long the pulse throb takes. 68const int kPulseDurationMs = 200; 69 70// Width of touch tabs. 71static const int kTouchWidth = 120; 72 73static const int kToolbarOverlap = 1; 74static const int kFaviconTitleSpacing = 4; 75static const int kViewSpacing = 3; 76static const int kStandardTitleWidth = 175; 77 78// When a non-mini-tab becomes a mini-tab the width of the tab animates. If 79// the width of a mini-tab is >= kMiniTabRendererAsNormalTabWidth then the tab 80// is rendered as a normal tab. This is done to avoid having the title 81// immediately disappear when transitioning a tab from normal to mini-tab. 82static const int kMiniTabRendererAsNormalTabWidth = 83 browser_defaults::kMiniTabWidth + 30; 84 85// How opaque to make the hover state (out of 1). 86static const double kHoverOpacity = 0.33; 87 88// Opacity for non-active selected tabs. 89static const double kSelectedTabOpacity = .45; 90 91// Selected (but not active) tabs have their throb value scaled down by this. 92static const double kSelectedTabThrobScale = .5; 93 94// Durations for the various parts of the mini tab title animation. 95static const int kMiniTitleChangeAnimationDuration1MS = 1600; 96static const int kMiniTitleChangeAnimationStart1MS = 0; 97static const int kMiniTitleChangeAnimationEnd1MS = 1900; 98static const int kMiniTitleChangeAnimationDuration2MS = 0; 99static const int kMiniTitleChangeAnimationDuration3MS = 550; 100static const int kMiniTitleChangeAnimationStart3MS = 150; 101static const int kMiniTitleChangeAnimationEnd3MS = 800; 102static const int kMiniTitleChangeAnimationIntervalMS = 40; 103 104// Offset from the right edge for the start of the mini title change animation. 105static const int kMiniTitleChangeInitialXOffset = 6; 106 107// Radius of the radial gradient used for mini title change animation. 108static const int kMiniTitleChangeGradientRadius = 20; 109 110// Colors of the gradient used during the mini title change animation. 111static const SkColor kMiniTitleChangeGradientColor1 = SK_ColorWHITE; 112static const SkColor kMiniTitleChangeGradientColor2 = 113 SkColorSetARGB(0, 255, 255, 255); 114 115// Max number of images to cache. This has to be at least two since rounding 116// errors may lead to tabs in the same tabstrip having different sizes. 117const size_t kMaxImageCacheSize = 4; 118 119// Height of the miniature tab strip in immersive mode. 120const int kImmersiveTabHeight = 3; 121 122// Height of the small tab indicator rectangles in immersive mode. 123const int kImmersiveBarHeight = 2; 124 125// Color for active and inactive tabs in the immersive mode light strip. These 126// should be a little brighter than the color of the normal art assets for tabs, 127// which for active tabs is 230, 230, 230 and for inactive is 184, 184, 184. 128const SkColor kImmersiveActiveTabColor = SkColorSetRGB(235, 235, 235); 129const SkColor kImmersiveInactiveTabColor = SkColorSetRGB(190, 190, 190); 130 131// The minimum opacity (out of 1) when a tab (either active or inactive) is 132// throbbing in the immersive mode light strip. 133const double kImmersiveTabMinThrobOpacity = 0.66; 134 135// Number of steps in the immersive mode loading animation. 136const int kImmersiveLoadingStepCount = 32; 137 138const char kTabCloseButtonName[] = "TabCloseButton"; 139 140void DrawIconAtLocation(gfx::Canvas* canvas, 141 const gfx::ImageSkia& image, 142 int image_offset, 143 int dst_x, 144 int dst_y, 145 int icon_width, 146 int icon_height, 147 bool filter, 148 const SkPaint& paint) { 149 // NOTE: the clipping is a work around for 69528, it shouldn't be necessary. 150 canvas->Save(); 151 canvas->ClipRect(gfx::Rect(dst_x, dst_y, icon_width, icon_height)); 152 canvas->DrawImageInt(image, 153 image_offset, 0, icon_width, icon_height, 154 dst_x, dst_y, icon_width, icon_height, 155 filter, paint); 156 canvas->Restore(); 157} 158 159// Draws the icon image at the center of |bounds|. 160void DrawIconCenter(gfx::Canvas* canvas, 161 const gfx::ImageSkia& image, 162 int image_offset, 163 int icon_width, 164 int icon_height, 165 const gfx::Rect& bounds, 166 bool filter, 167 const SkPaint& paint) { 168 // Center the image within bounds. 169 int dst_x = bounds.x() - (icon_width - bounds.width()) / 2; 170 int dst_y = bounds.y() - (icon_height - bounds.height()) / 2; 171 DrawIconAtLocation(canvas, image, image_offset, dst_x, dst_y, icon_width, 172 icon_height, filter, paint); 173} 174 175chrome::HostDesktopType GetHostDesktopType(views::View* view) { 176 // Widget is NULL when tabs are detached. 177 views::Widget* widget = view->GetWidget(); 178 return chrome::GetHostDesktopTypeForNativeView( 179 widget ? widget->GetNativeView() : NULL); 180} 181 182// Stop()s |animation| and then deletes it. We do this rather than just deleting 183// so that the delegate is notified before the destruction. 184void StopAndDeleteAnimation(scoped_ptr<gfx::Animation> animation) { 185 if (animation) 186 animation->Stop(); 187} 188 189} // namespace 190 191//////////////////////////////////////////////////////////////////////////////// 192// FaviconCrashAnimation 193// 194// A custom animation subclass to manage the favicon crash animation. 195class Tab::FaviconCrashAnimation : public gfx::LinearAnimation, 196 public gfx::AnimationDelegate { 197 public: 198 explicit FaviconCrashAnimation(Tab* target) 199 : gfx::LinearAnimation(1000, 25, this), 200 target_(target) { 201 } 202 virtual ~FaviconCrashAnimation() {} 203 204 // gfx::Animation overrides: 205 virtual void AnimateToState(double state) OVERRIDE { 206 const double kHidingOffset = 27; 207 208 if (state < .5) { 209 target_->SetFaviconHidingOffset( 210 static_cast<int>(floor(kHidingOffset * 2.0 * state))); 211 } else { 212 target_->DisplayCrashedFavicon(); 213 target_->SetFaviconHidingOffset( 214 static_cast<int>( 215 floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset)))); 216 } 217 } 218 219 // gfx::AnimationDelegate overrides: 220 virtual void AnimationCanceled(const gfx::Animation* animation) OVERRIDE { 221 target_->SetFaviconHidingOffset(0); 222 } 223 224 private: 225 Tab* target_; 226 227 DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation); 228}; 229 230//////////////////////////////////////////////////////////////////////////////// 231// TabCloseButton 232// 233// This is a Button subclass that causes middle clicks to be forwarded to the 234// parent View by explicitly not handling them in OnMousePressed. 235class Tab::TabCloseButton : public views::ImageButton, 236 public views::MaskedTargeterDelegate { 237 public: 238 explicit TabCloseButton(Tab* tab) 239 : views::ImageButton(tab), 240 tab_(tab) { 241 SetEventTargeter( 242 scoped_ptr<views::ViewTargeter>(new views::ViewTargeter(this))); 243 } 244 245 virtual ~TabCloseButton() {} 246 247 // views::View: 248 virtual View* GetTooltipHandlerForPoint(const gfx::Point& point) OVERRIDE { 249 // Tab close button has no children, so tooltip handler should be the same 250 // as the event handler. 251 // In addition, a hit test has to be performed for the point (as 252 // GetTooltipHandlerForPoint() is responsible for it). 253 if (!HitTestPoint(point)) 254 return NULL; 255 return GetEventHandlerForPoint(point); 256 } 257 258 virtual bool OnMousePressed(const ui::MouseEvent& event) OVERRIDE { 259 tab_->controller_->OnMouseEventInTab(this, event); 260 261 bool handled = ImageButton::OnMousePressed(event); 262 // Explicitly mark midle-mouse clicks as non-handled to ensure the tab 263 // sees them. 264 return event.IsOnlyMiddleMouseButton() ? false : handled; 265 } 266 267 virtual void OnMouseMoved(const ui::MouseEvent& event) OVERRIDE { 268 tab_->controller_->OnMouseEventInTab(this, event); 269 CustomButton::OnMouseMoved(event); 270 } 271 272 virtual void OnMouseReleased(const ui::MouseEvent& event) OVERRIDE { 273 tab_->controller_->OnMouseEventInTab(this, event); 274 CustomButton::OnMouseReleased(event); 275 } 276 277 virtual void OnGestureEvent(ui::GestureEvent* event) OVERRIDE { 278 // Consume all gesture events here so that the parent (Tab) does not 279 // start consuming gestures. 280 ImageButton::OnGestureEvent(event); 281 event->SetHandled(); 282 } 283 284 virtual const char* GetClassName() const OVERRIDE { 285 return kTabCloseButtonName; 286 } 287 288 private: 289 // Returns the rectangular bounds of parent tab's visible region in the 290 // local coordinate space of |this|. 291 gfx::Rect GetTabBounds() const { 292 gfx::Path tab_mask; 293 tab_->GetHitTestMask(&tab_mask); 294 295 gfx::RectF tab_bounds_f(gfx::SkRectToRectF(tab_mask.getBounds())); 296 views::View::ConvertRectToTarget(tab_, this, &tab_bounds_f); 297 return gfx::ToEnclosingRect(tab_bounds_f); 298 } 299 300 // Returns the rectangular bounds of the tab close button in the local 301 // coordinate space of |this|, not including clipped regions on the top 302 // or bottom of the button. |tab_bounds| is the rectangular bounds of 303 // the parent tab's visible region in the local coordinate space of |this|. 304 gfx::Rect GetTabCloseButtonBounds(const gfx::Rect& tab_bounds) const { 305 gfx::Rect button_bounds(GetContentsBounds()); 306 button_bounds.set_x(GetMirroredXForRect(button_bounds)); 307 308 int top_overflow = tab_bounds.y() - button_bounds.y(); 309 int bottom_overflow = button_bounds.bottom() - tab_bounds.bottom(); 310 if (top_overflow > 0) 311 button_bounds.set_y(tab_bounds.y()); 312 else if (bottom_overflow > 0) 313 button_bounds.set_height(button_bounds.height() - bottom_overflow); 314 315 return button_bounds; 316 } 317 318 // views::ViewTargeterDelegate: 319 virtual View* TargetForRect(View* root, const gfx::Rect& rect) OVERRIDE { 320 CHECK_EQ(root, this); 321 322 if (!views::UsePointBasedTargeting(rect)) 323 return ViewTargeterDelegate::TargetForRect(root, rect); 324 325 // Ignore the padding set on the button. 326 gfx::Rect contents_bounds = GetContentsBounds(); 327 contents_bounds.set_x(GetMirroredXForRect(contents_bounds)); 328 329 // Include the padding in hit-test for touch events. 330 if (aura::Env::GetInstance()->is_touch_down()) 331 contents_bounds = GetLocalBounds(); 332 333 return contents_bounds.Intersects(rect) ? this : parent(); 334 } 335 336 // views:MaskedTargeterDelegate: 337 virtual bool GetHitTestMask(gfx::Path* mask) const OVERRIDE { 338 DCHECK(mask); 339 mask->reset(); 340 341 // The parent tab may be partially occluded by another tab if we are 342 // in stacked tab mode, which means that the tab close button may also 343 // be partially occluded. Define the hit test mask of the tab close 344 // button to be the intersection of the parent tab's visible bounds 345 // and the bounds of the tab close button. 346 gfx::Rect tab_bounds(GetTabBounds()); 347 gfx::Rect button_bounds(GetTabCloseButtonBounds(tab_bounds)); 348 gfx::Rect intersection(gfx::IntersectRects(tab_bounds, button_bounds)); 349 350 if (!intersection.IsEmpty()) { 351 mask->addRect(RectToSkRect(intersection)); 352 return true; 353 } 354 355 return false; 356 } 357 358 virtual bool DoesIntersectRect(const View* target, 359 const gfx::Rect& rect) const OVERRIDE { 360 CHECK_EQ(target, this); 361 362 // If the request is not made in response to a gesture, use the 363 // default implementation. 364 if (views::UsePointBasedTargeting(rect)) 365 return MaskedTargeterDelegate::DoesIntersectRect(target, rect); 366 367 // The hit test request is in response to a gesture. Return false if any 368 // part of the tab close button is hidden from the user. 369 // TODO(tdanderson): Consider always returning the intersection if the 370 // non-rectangular shape of the tab can be accounted for. 371 gfx::Rect tab_bounds(GetTabBounds()); 372 gfx::Rect button_bounds(GetTabCloseButtonBounds(tab_bounds)); 373 if (!tab_bounds.Contains(button_bounds)) 374 return false; 375 376 return MaskedTargeterDelegate::DoesIntersectRect(target, rect); 377 } 378 379 Tab* tab_; 380 381 DISALLOW_COPY_AND_ASSIGN(TabCloseButton); 382}; 383 384//////////////////////////////////////////////////////////////////////////////// 385// ImageCacheEntry 386 387Tab::ImageCacheEntry::ImageCacheEntry() 388 : resource_id(-1), 389 scale_factor(ui::SCALE_FACTOR_NONE) { 390} 391 392Tab::ImageCacheEntry::~ImageCacheEntry() {} 393 394//////////////////////////////////////////////////////////////////////////////// 395// Tab, statics: 396 397// static 398const char Tab::kViewClassName[] = "Tab"; 399Tab::TabImage Tab::tab_active_ = {0}; 400Tab::TabImage Tab::tab_inactive_ = {0}; 401Tab::TabImage Tab::tab_alpha_ = {0}; 402Tab::ImageCache* Tab::image_cache_ = NULL; 403 404//////////////////////////////////////////////////////////////////////////////// 405// Tab, public: 406 407Tab::Tab(TabController* controller) 408 : controller_(controller), 409 closing_(false), 410 dragging_(false), 411 detached_(false), 412 favicon_hiding_offset_(0), 413 loading_animation_frame_(0), 414 immersive_loading_step_(0), 415 should_display_crashed_favicon_(false), 416 close_button_(NULL), 417 media_indicator_button_(NULL), 418 title_(new views::Label()), 419 tab_activated_with_last_tap_down_(false), 420 hover_controller_(this), 421 showing_icon_(false), 422 showing_media_indicator_(false), 423 showing_close_button_(false), 424 close_button_color_(0) { 425 DCHECK(controller); 426 InitTabResources(); 427 428 // So we get don't get enter/exit on children and don't prematurely stop the 429 // hover. 430 set_notify_enter_exit_on_child(true); 431 432 set_id(VIEW_ID_TAB); 433 434 title_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD); 435 title_->SetElideBehavior(gfx::FADE_TAIL); 436 title_->SetAutoColorReadabilityEnabled(false); 437 title_->SetText(CoreTabHelper::GetDefaultTitle()); 438 AddChildView(title_); 439 440 SetEventTargeter( 441 scoped_ptr<views::ViewTargeter>(new views::ViewTargeter(this))); 442 443 // Add the Close Button. 444 close_button_ = new TabCloseButton(this); 445 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 446 close_button_->SetImage(views::CustomButton::STATE_NORMAL, 447 rb.GetImageSkiaNamed(IDR_CLOSE_1)); 448 close_button_->SetImage(views::CustomButton::STATE_HOVERED, 449 rb.GetImageSkiaNamed(IDR_CLOSE_1_H)); 450 close_button_->SetImage(views::CustomButton::STATE_PRESSED, 451 rb.GetImageSkiaNamed(IDR_CLOSE_1_P)); 452 close_button_->SetAccessibleName( 453 l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE)); 454 // Disable animation so that the red danger sign shows up immediately 455 // to help avoid mis-clicks. 456 close_button_->SetAnimationDuration(0); 457 AddChildView(close_button_); 458 459 set_context_menu_controller(this); 460} 461 462Tab::~Tab() { 463} 464 465void Tab::set_animation_container(gfx::AnimationContainer* container) { 466 animation_container_ = container; 467 hover_controller_.SetAnimationContainer(container); 468} 469 470bool Tab::IsActive() const { 471 return controller_->IsActiveTab(this); 472} 473 474bool Tab::IsSelected() const { 475 return controller_->IsTabSelected(this); 476} 477 478void Tab::SetData(const TabRendererData& data) { 479 if (data_.Equals(data)) 480 return; 481 482 TabRendererData old(data_); 483 data_ = data; 484 485 base::string16 title = data_.title; 486 if (title.empty()) { 487 title = data_.loading ? 488 l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) : 489 CoreTabHelper::GetDefaultTitle(); 490 } else { 491 Browser::FormatTitleForDisplay(&title); 492 } 493 title_->SetText(title); 494 495 if (data_.IsCrashed()) { 496 if (!should_display_crashed_favicon_ && !IsPerformingCrashAnimation()) { 497 data_.media_state = TAB_MEDIA_STATE_NONE; 498#if defined(OS_CHROMEOS) 499 // On Chrome OS, we reload killed tabs automatically when the user 500 // switches to them. Don't display animations for these unless they're 501 // selected (i.e. in the foreground) -- we won't reload these 502 // automatically since we don't want to get into a crash loop. 503 if (IsSelected() || 504 data_.crashed_status != base::TERMINATION_STATUS_PROCESS_WAS_KILLED) 505 StartCrashAnimation(); 506#else 507 StartCrashAnimation(); 508#endif 509 } 510 } else { 511 if (IsPerformingCrashAnimation()) 512 StopCrashAnimation(); 513 ResetCrashedFavicon(); 514 } 515 516 if (data_.media_state != old.media_state) 517 GetMediaIndicatorButton()->TransitionToMediaState(data_.media_state); 518 519 if (old.mini != data_.mini) { 520 StopAndDeleteAnimation( 521 mini_title_change_animation_.PassAs<gfx::Animation>()); 522 } 523 524 DataChanged(old); 525 526 Layout(); 527 SchedulePaint(); 528} 529 530void Tab::UpdateLoadingAnimation(TabRendererData::NetworkState state) { 531 if (state == data_.network_state && 532 state == TabRendererData::NETWORK_STATE_NONE) { 533 // If the network state is none and hasn't changed, do nothing. Otherwise we 534 // need to advance the animation frame. 535 return; 536 } 537 538 TabRendererData::NetworkState old_state = data_.network_state; 539 data_.network_state = state; 540 AdvanceLoadingAnimation(old_state, state); 541} 542 543void Tab::StartPulse() { 544 pulse_animation_.reset(new gfx::ThrobAnimation(this)); 545 pulse_animation_->SetSlideDuration(kPulseDurationMs); 546 if (animation_container_.get()) 547 pulse_animation_->SetContainer(animation_container_.get()); 548 pulse_animation_->StartThrobbing(std::numeric_limits<int>::max()); 549} 550 551void Tab::StopPulse() { 552 StopAndDeleteAnimation(pulse_animation_.PassAs<gfx::Animation>()); 553} 554 555void Tab::StartMiniTabTitleAnimation() { 556 if (!data().mini) 557 return; 558 if (!mini_title_change_animation_) { 559 gfx::MultiAnimation::Parts parts; 560 parts.push_back( 561 gfx::MultiAnimation::Part(kMiniTitleChangeAnimationDuration1MS, 562 gfx::Tween::EASE_OUT)); 563 parts.push_back( 564 gfx::MultiAnimation::Part(kMiniTitleChangeAnimationDuration2MS, 565 gfx::Tween::ZERO)); 566 parts.push_back( 567 gfx::MultiAnimation::Part(kMiniTitleChangeAnimationDuration3MS, 568 gfx::Tween::EASE_IN)); 569 parts[0].start_time_ms = kMiniTitleChangeAnimationStart1MS; 570 parts[0].end_time_ms = kMiniTitleChangeAnimationEnd1MS; 571 parts[2].start_time_ms = kMiniTitleChangeAnimationStart3MS; 572 parts[2].end_time_ms = kMiniTitleChangeAnimationEnd3MS; 573 base::TimeDelta timeout = 574 base::TimeDelta::FromMilliseconds(kMiniTitleChangeAnimationIntervalMS); 575 mini_title_change_animation_.reset(new gfx::MultiAnimation(parts, timeout)); 576 if (animation_container_.get()) 577 mini_title_change_animation_->SetContainer(animation_container_.get()); 578 mini_title_change_animation_->set_delegate(this); 579 } 580 mini_title_change_animation_->Start(); 581} 582 583void Tab::StopMiniTabTitleAnimation() { 584 StopAndDeleteAnimation(mini_title_change_animation_.PassAs<gfx::Animation>()); 585} 586 587// static 588gfx::Size Tab::GetBasicMinimumUnselectedSize() { 589 InitTabResources(); 590 591 gfx::Size minimum_size; 592 minimum_size.set_width(kLeftPadding + kRightPadding); 593 // Since we use image images, the real minimum height of the image is 594 // defined most accurately by the height of the end cap images. 595 minimum_size.set_height(tab_active_.image_l->height()); 596 return minimum_size; 597} 598 599gfx::Size Tab::GetMinimumUnselectedSize() { 600 return GetBasicMinimumUnselectedSize(); 601} 602 603// static 604gfx::Size Tab::GetMinimumSelectedSize() { 605 gfx::Size minimum_size = GetBasicMinimumUnselectedSize(); 606 minimum_size.set_width( 607 kLeftPadding + gfx::kFaviconSize + kRightPadding); 608 return minimum_size; 609} 610 611// static 612gfx::Size Tab::GetStandardSize() { 613 gfx::Size standard_size = GetBasicMinimumUnselectedSize(); 614 standard_size.set_width( 615 standard_size.width() + kFaviconTitleSpacing + kStandardTitleWidth); 616 return standard_size; 617} 618 619// static 620int Tab::GetTouchWidth() { 621 return kTouchWidth; 622} 623 624// static 625int Tab::GetMiniWidth() { 626 return browser_defaults::kMiniTabWidth; 627} 628 629// static 630int Tab::GetImmersiveHeight() { 631 return kImmersiveTabHeight; 632} 633 634//////////////////////////////////////////////////////////////////////////////// 635// Tab, AnimationDelegate overrides: 636 637void Tab::AnimationProgressed(const gfx::Animation* animation) { 638 // Ignore if the pulse animation is being performed on active tab because 639 // it repaints the same image. See |Tab::PaintTabBackground()|. 640 if (animation == pulse_animation_.get() && IsActive()) 641 return; 642 SchedulePaint(); 643} 644 645void Tab::AnimationCanceled(const gfx::Animation* animation) { 646 SchedulePaint(); 647} 648 649void Tab::AnimationEnded(const gfx::Animation* animation) { 650 SchedulePaint(); 651} 652 653//////////////////////////////////////////////////////////////////////////////// 654// Tab, views::ButtonListener overrides: 655 656void Tab::ButtonPressed(views::Button* sender, const ui::Event& event) { 657 if (media_indicator_button_ && media_indicator_button_->visible()) { 658 if (media_indicator_button_->enabled()) 659 content::RecordAction(UserMetricsAction("CloseTab_MuteToggleAvailable")); 660 else if (data_.media_state == TAB_MEDIA_STATE_AUDIO_PLAYING) 661 content::RecordAction(UserMetricsAction("CloseTab_AudioIndicator")); 662 else 663 content::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator")); 664 } else { 665 content::RecordAction(UserMetricsAction("CloseTab_NoMediaIndicator")); 666 } 667 668 const CloseTabSource source = 669 (event.type() == ui::ET_MOUSE_RELEASED && 670 (event.flags() & ui::EF_FROM_TOUCH) == 0) ? CLOSE_TAB_FROM_MOUSE : 671 CLOSE_TAB_FROM_TOUCH; 672 DCHECK_EQ(close_button_, sender); 673 controller_->CloseTab(this, source); 674 if (event.type() == ui::ET_GESTURE_TAP) 675 TouchUMA::RecordGestureAction(TouchUMA::GESTURE_TABCLOSE_TAP); 676} 677 678//////////////////////////////////////////////////////////////////////////////// 679// Tab, views::ContextMenuController overrides: 680 681void Tab::ShowContextMenuForView(views::View* source, 682 const gfx::Point& point, 683 ui::MenuSourceType source_type) { 684 if (!closing()) 685 controller_->ShowContextMenuForTab(this, point, source_type); 686} 687 688//////////////////////////////////////////////////////////////////////////////// 689// Tab, views::MaskedTargeterDelegate overrides: 690 691bool Tab::GetHitTestMask(gfx::Path* mask) const { 692 DCHECK(mask); 693 694 // When the window is maximized we don't want to shave off the edges or top 695 // shadow of the tab, such that the user can click anywhere along the top 696 // edge of the screen to select a tab. Ditto for immersive fullscreen. 697 const views::Widget* widget = GetWidget(); 698 bool include_top_shadow = 699 widget && (widget->IsMaximized() || widget->IsFullscreen()); 700 TabResources::GetHitTestMask(width(), height(), include_top_shadow, mask); 701 702 // It is possible for a portion of the tab to be occluded if tabs are 703 // stacked, so modify the hit test mask to only include the visible 704 // region of the tab. 705 gfx::Rect clip; 706 controller_->ShouldPaintTab(this, &clip); 707 if (clip.size().GetArea()) { 708 SkRect intersection(mask->getBounds()); 709 intersection.intersect(RectToSkRect(clip)); 710 mask->reset(); 711 mask->addRect(intersection); 712 } 713 714 return true; 715} 716 717//////////////////////////////////////////////////////////////////////////////// 718// Tab, views::View overrides: 719 720void Tab::OnPaint(gfx::Canvas* canvas) { 721 // Don't paint if we're narrower than we can render correctly. (This should 722 // only happen during animations). 723 if (width() < GetMinimumUnselectedSize().width() && !data().mini) 724 return; 725 726 gfx::Rect clip; 727 if (!controller_->ShouldPaintTab(this, &clip)) 728 return; 729 if (!clip.IsEmpty()) { 730 canvas->Save(); 731 canvas->ClipRect(clip); 732 } 733 734 if (controller_->IsImmersiveStyle()) 735 PaintImmersiveTab(canvas); 736 else 737 PaintTab(canvas); 738 739 if (!clip.IsEmpty()) 740 canvas->Restore(); 741} 742 743void Tab::Layout() { 744 gfx::Rect lb = GetContentsBounds(); 745 if (lb.IsEmpty()) 746 return; 747 748 lb.Inset(kLeftPadding, kTopPadding, kRightPadding, kBottomPadding); 749 showing_icon_ = ShouldShowIcon(); 750 favicon_bounds_.SetRect(lb.x(), lb.y(), 0, 0); 751 if (showing_icon_) { 752 favicon_bounds_.set_size(gfx::Size(gfx::kFaviconSize, gfx::kFaviconSize)); 753 favicon_bounds_.set_y(lb.y() + (lb.height() - gfx::kFaviconSize + 1) / 2); 754 MaybeAdjustLeftForMiniTab(&favicon_bounds_); 755 } 756 757 showing_close_button_ = ShouldShowCloseBox(); 758 if (showing_close_button_) { 759 // If the ratio of the close button size to tab width exceeds the maximum. 760 // The close button should be as large as possible so that there is a larger 761 // hit-target for touch events. So the close button bounds extends to the 762 // edges of the tab. However, the larger hit-target should be active only 763 // for mouse events, and the close-image should show up in the right place. 764 // So a border is added to the button with necessary padding. The close 765 // button (BaseTab::TabCloseButton) makes sure the padding is a hit-target 766 // only for touch events. 767 close_button_->SetBorder(views::Border::NullBorder()); 768 const gfx::Size close_button_size(close_button_->GetPreferredSize()); 769 const int top = lb.y() + (lb.height() - close_button_size.height() + 1) / 2; 770 const int bottom = height() - (close_button_size.height() + top); 771 const int left = kViewSpacing; 772 const int right = width() - (lb.width() + close_button_size.width() + left); 773 close_button_->SetBorder( 774 views::Border::CreateEmptyBorder(top, left, bottom, right)); 775 close_button_->SetPosition(gfx::Point(lb.width(), 0)); 776 close_button_->SizeToPreferredSize(); 777 } 778 close_button_->SetVisible(showing_close_button_); 779 780 showing_media_indicator_ = ShouldShowMediaIndicator(); 781 if (showing_media_indicator_) { 782 views::ImageButton* const button = GetMediaIndicatorButton(); 783 const gfx::Size image_size(button->GetPreferredSize()); 784 const int right = showing_close_button_ ? 785 close_button_->x() + close_button_->GetInsets().left() : lb.right(); 786 gfx::Rect bounds( 787 std::max(lb.x(), right - image_size.width()), 788 lb.y() + (lb.height() - image_size.height() + 1) / 2, 789 image_size.width(), 790 image_size.height()); 791 MaybeAdjustLeftForMiniTab(&bounds); 792 button->SetBoundsRect(bounds); 793 button->SetVisible(true); 794 } else if (media_indicator_button_) { 795 media_indicator_button_->SetVisible(false); 796 } 797 798 // Size the title to fill the remaining width and use all available height. 799 bool show_title = !data().mini || width() >= kMiniTabRendererAsNormalTabWidth; 800 if (show_title) { 801 int title_left = favicon_bounds_.right() + kFaviconTitleSpacing; 802 int title_width = lb.width() - title_left; 803 if (showing_media_indicator_) { 804 title_width = media_indicator_button_->x() - kViewSpacing - title_left; 805 } else if (close_button_->visible()) { 806 // Allow the title to overlay the close button's empty border padding. 807 title_width = close_button_->x() + close_button_->GetInsets().left() - 808 kViewSpacing - title_left; 809 } 810 gfx::Rect rect(title_left, lb.y(), std::max(title_width, 0), lb.height()); 811 const int title_height = title_->GetPreferredSize().height(); 812 if (title_height > rect.height()) { 813 rect.set_y(lb.y() - (title_height - rect.height()) / 2); 814 rect.set_height(title_height); 815 } 816 title_->SetBoundsRect(rect); 817 } 818 title_->SetVisible(show_title); 819} 820 821void Tab::OnThemeChanged() { 822 LoadTabImages(); 823} 824 825const char* Tab::GetClassName() const { 826 return kViewClassName; 827} 828 829bool Tab::GetTooltipText(const gfx::Point& p, base::string16* tooltip) const { 830 // Note: Anything that affects the tooltip text should be accounted for when 831 // calling TooltipTextChanged() from Tab::DataChanged(). 832 *tooltip = chrome::AssembleTabTooltipText(data_.title, data_.media_state); 833 return !tooltip->empty(); 834} 835 836bool Tab::GetTooltipTextOrigin(const gfx::Point& p, gfx::Point* origin) const { 837 origin->set_x(title_->x() + 10); 838 origin->set_y(-views::TooltipManager::GetTooltipHeight() - 4); 839 return true; 840} 841 842bool Tab::OnMousePressed(const ui::MouseEvent& event) { 843 controller_->OnMouseEventInTab(this, event); 844 845 // Allow a right click from touch to drag, which corresponds to a long click. 846 if (event.IsOnlyLeftMouseButton() || 847 (event.IsOnlyRightMouseButton() && event.flags() & ui::EF_FROM_TOUCH)) { 848 ui::ListSelectionModel original_selection; 849 original_selection.Copy(controller_->GetSelectionModel()); 850 // Changing the selection may cause our bounds to change. If that happens 851 // the location of the event may no longer be valid. Create a copy of the 852 // event in the parents coordinate, which won't change, and recreate an 853 // event after changing so the coordinates are correct. 854 ui::MouseEvent event_in_parent(event, static_cast<View*>(this), parent()); 855 if (controller_->SupportsMultipleSelection()) { 856 if (event.IsShiftDown() && event.IsControlDown()) { 857 controller_->AddSelectionFromAnchorTo(this); 858 } else if (event.IsShiftDown()) { 859 controller_->ExtendSelectionTo(this); 860 } else if (event.IsControlDown()) { 861 controller_->ToggleSelected(this); 862 if (!IsSelected()) { 863 // Don't allow dragging non-selected tabs. 864 return false; 865 } 866 } else if (!IsSelected()) { 867 controller_->SelectTab(this); 868 } 869 } else if (!IsSelected()) { 870 controller_->SelectTab(this); 871 } 872 ui::MouseEvent cloned_event(event_in_parent, parent(), 873 static_cast<View*>(this)); 874 controller_->MaybeStartDrag(this, cloned_event, original_selection); 875 } 876 return true; 877} 878 879bool Tab::OnMouseDragged(const ui::MouseEvent& event) { 880 controller_->ContinueDrag(this, event); 881 return true; 882} 883 884void Tab::OnMouseReleased(const ui::MouseEvent& event) { 885 controller_->OnMouseEventInTab(this, event); 886 887 // Notify the drag helper that we're done with any potential drag operations. 888 // Clean up the drag helper, which is re-created on the next mouse press. 889 // In some cases, ending the drag will schedule the tab for destruction; if 890 // so, bail immediately, since our members are already dead and we shouldn't 891 // do anything else except drop the tab where it is. 892 if (controller_->EndDrag(END_DRAG_COMPLETE)) 893 return; 894 895 // Close tab on middle click, but only if the button is released over the tab 896 // (normal windows behavior is to discard presses of a UI element where the 897 // releases happen off the element). 898 if (event.IsMiddleMouseButton()) { 899 if (HitTestPoint(event.location())) { 900 controller_->CloseTab(this, CLOSE_TAB_FROM_MOUSE); 901 } else if (closing_) { 902 // We're animating closed and a middle mouse button was pushed on us but 903 // we don't contain the mouse anymore. We assume the user is clicking 904 // quicker than the animation and we should close the tab that falls under 905 // the mouse. 906 Tab* closest_tab = controller_->GetTabAt(this, event.location()); 907 if (closest_tab) 908 controller_->CloseTab(closest_tab, CLOSE_TAB_FROM_MOUSE); 909 } 910 } else if (event.IsOnlyLeftMouseButton() && !event.IsShiftDown() && 911 !event.IsControlDown()) { 912 // If the tab was already selected mouse pressed doesn't change the 913 // selection. Reset it now to handle the case where multiple tabs were 914 // selected. 915 controller_->SelectTab(this); 916 917 if (media_indicator_button_ && media_indicator_button_->visible() && 918 media_indicator_button_->bounds().Contains(event.location())) { 919 content::RecordAction(UserMetricsAction("TabMediaIndicator_Clicked")); 920 } 921 } 922} 923 924void Tab::OnMouseCaptureLost() { 925 controller_->EndDrag(END_DRAG_CAPTURE_LOST); 926} 927 928void Tab::OnMouseEntered(const ui::MouseEvent& event) { 929 hover_controller_.Show(views::GlowHoverController::SUBTLE); 930} 931 932void Tab::OnMouseMoved(const ui::MouseEvent& event) { 933 hover_controller_.SetLocation(event.location()); 934 controller_->OnMouseEventInTab(this, event); 935} 936 937void Tab::OnMouseExited(const ui::MouseEvent& event) { 938 hover_controller_.Hide(); 939} 940 941void Tab::OnGestureEvent(ui::GestureEvent* event) { 942 switch (event->type()) { 943 case ui::ET_GESTURE_TAP_DOWN: { 944 // TAP_DOWN is only dispatched for the first touch point. 945 DCHECK_EQ(1, event->details().touch_points()); 946 947 // See comment in OnMousePressed() as to why we copy the event. 948 ui::GestureEvent event_in_parent(*event, static_cast<View*>(this), 949 parent()); 950 ui::ListSelectionModel original_selection; 951 original_selection.Copy(controller_->GetSelectionModel()); 952 tab_activated_with_last_tap_down_ = !IsActive(); 953 if (!IsSelected()) 954 controller_->SelectTab(this); 955 gfx::Point loc(event->location()); 956 views::View::ConvertPointToScreen(this, &loc); 957 ui::GestureEvent cloned_event(event_in_parent, parent(), 958 static_cast<View*>(this)); 959 controller_->MaybeStartDrag(this, cloned_event, original_selection); 960 break; 961 } 962 963 case ui::ET_GESTURE_END: 964 controller_->EndDrag(END_DRAG_COMPLETE); 965 break; 966 967 case ui::ET_GESTURE_SCROLL_UPDATE: 968 controller_->ContinueDrag(this, *event); 969 break; 970 971 default: 972 break; 973 } 974 event->SetHandled(); 975} 976 977void Tab::GetAccessibleState(ui::AXViewState* state) { 978 state->role = ui::AX_ROLE_TAB; 979 state->name = data_.title; 980 state->AddStateFlag(ui::AX_STATE_MULTISELECTABLE); 981 state->AddStateFlag(ui::AX_STATE_SELECTABLE); 982 controller_->UpdateTabAccessibilityState(this, state); 983 if (IsSelected()) 984 state->AddStateFlag(ui::AX_STATE_SELECTED); 985} 986 987//////////////////////////////////////////////////////////////////////////////// 988// Tab, private 989 990void Tab::MaybeAdjustLeftForMiniTab(gfx::Rect* bounds) const { 991 if (!data().mini || width() >= kMiniTabRendererAsNormalTabWidth) 992 return; 993 const int mini_delta = kMiniTabRendererAsNormalTabWidth - GetMiniWidth(); 994 const int ideal_delta = width() - GetMiniWidth(); 995 const int ideal_x = (GetMiniWidth() - bounds->width()) / 2; 996 bounds->set_x(bounds->x() + static_cast<int>( 997 (1 - static_cast<float>(ideal_delta) / static_cast<float>(mini_delta)) * 998 (ideal_x - bounds->x()))); 999} 1000 1001void Tab::DataChanged(const TabRendererData& old) { 1002 if (data().media_state != old.media_state || data().title != old.title) 1003 TooltipTextChanged(); 1004 1005 if (data().blocked == old.blocked) 1006 return; 1007 1008 if (data().blocked) 1009 StartPulse(); 1010 else 1011 StopPulse(); 1012} 1013 1014void Tab::PaintTab(gfx::Canvas* canvas) { 1015 // See if the model changes whether the icons should be painted. 1016 const bool show_icon = ShouldShowIcon(); 1017 const bool show_media_indicator = ShouldShowMediaIndicator(); 1018 const bool show_close_button = ShouldShowCloseBox(); 1019 if (show_icon != showing_icon_ || 1020 show_media_indicator != showing_media_indicator_ || 1021 show_close_button != showing_close_button_) { 1022 Layout(); 1023 } 1024 1025 PaintTabBackground(canvas); 1026 1027 const SkColor title_color = GetThemeProvider()->GetColor(IsSelected() ? 1028 ThemeProperties::COLOR_TAB_TEXT : 1029 ThemeProperties::COLOR_BACKGROUND_TAB_TEXT); 1030 title_->SetVisible(!data().mini || 1031 width() > kMiniTabRendererAsNormalTabWidth); 1032 title_->SetEnabledColor(title_color); 1033 1034 if (show_icon) 1035 PaintIcon(canvas); 1036 1037 // If the close button color has changed, generate a new one. 1038 if (!close_button_color_ || title_color != close_button_color_) { 1039 close_button_color_ = title_color; 1040 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 1041 close_button_->SetBackground(close_button_color_, 1042 rb.GetImageSkiaNamed(IDR_CLOSE_1), 1043 rb.GetImageSkiaNamed(IDR_CLOSE_1_MASK)); 1044 } 1045} 1046 1047void Tab::PaintImmersiveTab(gfx::Canvas* canvas) { 1048 // Use transparency for the draw-attention animation. 1049 int alpha = 255; 1050 if (pulse_animation_ && pulse_animation_->is_animating() && !data().mini) { 1051 alpha = pulse_animation_->CurrentValueBetween( 1052 255, static_cast<int>(255 * kImmersiveTabMinThrobOpacity)); 1053 } 1054 1055 // Draw a gray rectangle to represent the tab. This works for mini-tabs as 1056 // well as regular ones. The active tab has a brigher bar. 1057 SkColor color = 1058 IsActive() ? kImmersiveActiveTabColor : kImmersiveInactiveTabColor; 1059 gfx::Rect bar_rect = GetImmersiveBarRect(); 1060 canvas->FillRect(bar_rect, SkColorSetA(color, alpha)); 1061 1062 // Paint network activity indicator. 1063 // TODO(jamescook): Replace this placeholder animation with a real one. 1064 // For now, let's go with a Cylon eye effect, but in blue. 1065 if (data().network_state != TabRendererData::NETWORK_STATE_NONE) { 1066 const SkColor kEyeColor = SkColorSetARGB(alpha, 71, 138, 217); 1067 int eye_width = bar_rect.width() / 3; 1068 int eye_offset = bar_rect.width() * immersive_loading_step_ / 1069 kImmersiveLoadingStepCount; 1070 if (eye_offset + eye_width < bar_rect.width()) { 1071 // Draw a single indicator strip because it fits inside |bar_rect|. 1072 gfx::Rect eye_rect( 1073 bar_rect.x() + eye_offset, 0, eye_width, kImmersiveBarHeight); 1074 canvas->FillRect(eye_rect, kEyeColor); 1075 } else { 1076 // Draw two indicators to simulate the eye "wrapping around" to the left 1077 // side. The first part fills the remainder of the bar. 1078 int right_eye_width = bar_rect.width() - eye_offset; 1079 gfx::Rect right_eye_rect( 1080 bar_rect.x() + eye_offset, 0, right_eye_width, kImmersiveBarHeight); 1081 canvas->FillRect(right_eye_rect, kEyeColor); 1082 // The second part parts the remaining |eye_width| on the left. 1083 int left_eye_width = eye_offset + eye_width - bar_rect.width(); 1084 gfx::Rect left_eye_rect( 1085 bar_rect.x(), 0, left_eye_width, kImmersiveBarHeight); 1086 canvas->FillRect(left_eye_rect, kEyeColor); 1087 } 1088 } 1089} 1090 1091void Tab::PaintTabBackground(gfx::Canvas* canvas) { 1092 if (IsActive()) { 1093 PaintActiveTabBackground(canvas); 1094 } else { 1095 if (mini_title_change_animation_ && 1096 mini_title_change_animation_->is_animating()) { 1097 PaintInactiveTabBackgroundWithTitleChange(canvas); 1098 } else { 1099 PaintInactiveTabBackground(canvas); 1100 } 1101 1102 double throb_value = GetThrobValue(); 1103 if (throb_value > 0) { 1104 canvas->SaveLayerAlpha(static_cast<int>(throb_value * 0xff), 1105 GetLocalBounds()); 1106 PaintActiveTabBackground(canvas); 1107 canvas->Restore(); 1108 } 1109 } 1110} 1111 1112void Tab::PaintInactiveTabBackgroundWithTitleChange(gfx::Canvas* canvas) { 1113 // Render the inactive tab background. We'll use this for clipping. 1114 gfx::Canvas background_canvas(size(), canvas->image_scale(), false); 1115 PaintInactiveTabBackground(&background_canvas); 1116 1117 gfx::ImageSkia background_image(background_canvas.ExtractImageRep()); 1118 1119 // Draw a radial gradient to hover_canvas. 1120 gfx::Canvas hover_canvas(size(), canvas->image_scale(), false); 1121 int radius = kMiniTitleChangeGradientRadius; 1122 int x0 = width() + radius - kMiniTitleChangeInitialXOffset; 1123 int x1 = radius; 1124 int x2 = -radius; 1125 int x; 1126 if (mini_title_change_animation_->current_part_index() == 0) { 1127 x = mini_title_change_animation_->CurrentValueBetween(x0, x1); 1128 } else if (mini_title_change_animation_->current_part_index() == 1) { 1129 x = x1; 1130 } else { 1131 x = mini_title_change_animation_->CurrentValueBetween(x1, x2); 1132 } 1133 SkPoint center_point; 1134 center_point.iset(x, 0); 1135 SkColor colors[2] = { kMiniTitleChangeGradientColor1, 1136 kMiniTitleChangeGradientColor2 }; 1137 skia::RefPtr<SkShader> shader = skia::AdoptRef( 1138 SkGradientShader::CreateRadial( 1139 center_point, SkIntToScalar(radius), colors, NULL, 2, 1140 SkShader::kClamp_TileMode)); 1141 SkPaint paint; 1142 paint.setShader(shader.get()); 1143 hover_canvas.DrawRect(gfx::Rect(x - radius, -radius, radius * 2, radius * 2), 1144 paint); 1145 1146 // Draw the radial gradient clipped to the background into hover_image. 1147 gfx::ImageSkia hover_image = gfx::ImageSkiaOperations::CreateMaskedImage( 1148 gfx::ImageSkia(hover_canvas.ExtractImageRep()), background_image); 1149 1150 // Draw the tab background to the canvas. 1151 canvas->DrawImageInt(background_image, 0, 0); 1152 1153 // And then the gradient on top of that. 1154 if (mini_title_change_animation_->current_part_index() == 2) { 1155 uint8 alpha = mini_title_change_animation_->CurrentValueBetween(255, 0); 1156 canvas->DrawImageInt(hover_image, 0, 0, alpha); 1157 } else { 1158 canvas->DrawImageInt(hover_image, 0, 0); 1159 } 1160} 1161 1162void Tab::PaintInactiveTabBackground(gfx::Canvas* canvas) { 1163 int tab_id; 1164 int frame_id; 1165 views::Widget* widget = GetWidget(); 1166 GetTabIdAndFrameId(widget, &tab_id, &frame_id); 1167 1168 // Explicitly map the id so we cache correctly. 1169 const chrome::HostDesktopType host_desktop_type = GetHostDesktopType(this); 1170 tab_id = chrome::MapThemeImage(host_desktop_type, tab_id); 1171 1172 // HasCustomImage() is only true if the theme provides the image. However, 1173 // even if the theme does not provide a tab background, the theme machinery 1174 // will make one if given a frame image. 1175 ui::ThemeProvider* theme_provider = GetThemeProvider(); 1176 const bool theme_provided_image = theme_provider->HasCustomImage(tab_id) || 1177 (frame_id != 0 && theme_provider->HasCustomImage(frame_id)); 1178 1179 const bool can_cache = !theme_provided_image && 1180 !hover_controller_.ShouldDraw(); 1181 1182 if (can_cache) { 1183 ui::ScaleFactor scale_factor = 1184 ui::GetSupportedScaleFactor(canvas->image_scale()); 1185 gfx::ImageSkia cached_image(GetCachedImage(tab_id, size(), scale_factor)); 1186 if (cached_image.width() == 0) { 1187 gfx::Canvas tmp_canvas(size(), canvas->image_scale(), false); 1188 PaintInactiveTabBackgroundUsingResourceId(&tmp_canvas, tab_id); 1189 cached_image = gfx::ImageSkia(tmp_canvas.ExtractImageRep()); 1190 SetCachedImage(tab_id, scale_factor, cached_image); 1191 } 1192 canvas->DrawImageInt(cached_image, 0, 0); 1193 } else { 1194 PaintInactiveTabBackgroundUsingResourceId(canvas, tab_id); 1195 } 1196} 1197 1198void Tab::PaintInactiveTabBackgroundUsingResourceId(gfx::Canvas* canvas, 1199 int tab_id) { 1200 // WARNING: the inactive tab background may be cached. If you change what it 1201 // is drawn from you may need to update whether it can be cached. 1202 1203 // The tab image needs to be lined up with the background image 1204 // so that it feels partially transparent. These offsets represent the tab 1205 // position within the frame background image. 1206 int offset = GetMirroredX() + background_offset_.x(); 1207 1208 gfx::ImageSkia* tab_bg = GetThemeProvider()->GetImageSkiaNamed(tab_id); 1209 1210 TabImage* tab_image = &tab_active_; 1211 TabImage* tab_inactive_image = &tab_inactive_; 1212 TabImage* alpha = &tab_alpha_; 1213 1214 // If the theme is providing a custom background image, then its top edge 1215 // should be at the top of the tab. Otherwise, we assume that the background 1216 // image is a composited foreground + frame image. 1217 int bg_offset_y = GetThemeProvider()->HasCustomImage(tab_id) ? 1218 0 : background_offset_.y(); 1219 1220 // We need a gfx::Canvas object to be able to extract the image from. 1221 // We draw everything to this canvas and then output it to the canvas 1222 // parameter in addition to using it to mask the hover glow if needed. 1223 gfx::Canvas background_canvas(size(), canvas->image_scale(), false); 1224 1225 // Draw left edge. Don't draw over the toolbar, as we're not the foreground 1226 // tab. 1227 gfx::ImageSkia tab_l = gfx::ImageSkiaOperations::CreateTiledImage( 1228 *tab_bg, offset, bg_offset_y, tab_image->l_width, height()); 1229 gfx::ImageSkia theme_l = 1230 gfx::ImageSkiaOperations::CreateMaskedImage(tab_l, *alpha->image_l); 1231 background_canvas.DrawImageInt(theme_l, 1232 0, 0, theme_l.width(), theme_l.height() - kToolbarOverlap, 1233 0, 0, theme_l.width(), theme_l.height() - kToolbarOverlap, 1234 false); 1235 1236 // Draw right edge. Again, don't draw over the toolbar. 1237 gfx::ImageSkia tab_r = gfx::ImageSkiaOperations::CreateTiledImage(*tab_bg, 1238 offset + width() - tab_image->r_width, bg_offset_y, 1239 tab_image->r_width, height()); 1240 gfx::ImageSkia theme_r = 1241 gfx::ImageSkiaOperations::CreateMaskedImage(tab_r, *alpha->image_r); 1242 background_canvas.DrawImageInt(theme_r, 1243 0, 0, theme_r.width(), theme_r.height() - kToolbarOverlap, 1244 width() - theme_r.width(), 0, theme_r.width(), 1245 theme_r.height() - kToolbarOverlap, false); 1246 1247 // Draw center. Instead of masking out the top portion we simply skip over 1248 // it by incrementing by GetDropShadowHeight(), since it's a simple 1249 // rectangle. And again, don't draw over the toolbar. 1250 background_canvas.TileImageInt(*tab_bg, 1251 offset + tab_image->l_width, 1252 bg_offset_y + kDropShadowHeight, 1253 tab_image->l_width, 1254 kDropShadowHeight, 1255 width() - tab_image->l_width - tab_image->r_width, 1256 height() - kDropShadowHeight - kToolbarOverlap); 1257 1258 canvas->DrawImageInt( 1259 gfx::ImageSkia(background_canvas.ExtractImageRep()), 0, 0); 1260 1261 if (!GetThemeProvider()->HasCustomImage(tab_id) && 1262 hover_controller_.ShouldDraw()) { 1263 hover_controller_.Draw(canvas, gfx::ImageSkia( 1264 background_canvas.ExtractImageRep())); 1265 } 1266 1267 // Now draw the highlights/shadows around the tab edge. 1268 canvas->DrawImageInt(*tab_inactive_image->image_l, 0, 0); 1269 canvas->TileImageInt(*tab_inactive_image->image_c, 1270 tab_inactive_image->l_width, 0, 1271 width() - tab_inactive_image->l_width - 1272 tab_inactive_image->r_width, 1273 height()); 1274 canvas->DrawImageInt(*tab_inactive_image->image_r, 1275 width() - tab_inactive_image->r_width, 0); 1276} 1277 1278void Tab::PaintActiveTabBackground(gfx::Canvas* canvas) { 1279 gfx::ImageSkia* tab_background = 1280 GetThemeProvider()->GetImageSkiaNamed(IDR_THEME_TOOLBAR); 1281 int offset = GetMirroredX() + background_offset_.x(); 1282 1283 TabImage* tab_image = &tab_active_; 1284 TabImage* alpha = &tab_alpha_; 1285 1286 // Draw left edge. 1287 gfx::ImageSkia tab_l = gfx::ImageSkiaOperations::CreateTiledImage( 1288 *tab_background, offset, 0, tab_image->l_width, height()); 1289 gfx::ImageSkia theme_l = 1290 gfx::ImageSkiaOperations::CreateMaskedImage(tab_l, *alpha->image_l); 1291 canvas->DrawImageInt(theme_l, 0, 0); 1292 1293 // Draw right edge. 1294 gfx::ImageSkia tab_r = gfx::ImageSkiaOperations::CreateTiledImage( 1295 *tab_background, 1296 offset + width() - tab_image->r_width, 0, tab_image->r_width, height()); 1297 gfx::ImageSkia theme_r = 1298 gfx::ImageSkiaOperations::CreateMaskedImage(tab_r, *alpha->image_r); 1299 canvas->DrawImageInt(theme_r, width() - tab_image->r_width, 0); 1300 1301 // Draw center. Instead of masking out the top portion we simply skip over it 1302 // by incrementing by GetDropShadowHeight(), since it's a simple rectangle. 1303 canvas->TileImageInt(*tab_background, 1304 offset + tab_image->l_width, 1305 kDropShadowHeight, 1306 tab_image->l_width, 1307 kDropShadowHeight, 1308 width() - tab_image->l_width - tab_image->r_width, 1309 height() - kDropShadowHeight); 1310 1311 // Now draw the highlights/shadows around the tab edge. 1312 canvas->DrawImageInt(*tab_image->image_l, 0, 0); 1313 canvas->TileImageInt(*tab_image->image_c, tab_image->l_width, 0, 1314 width() - tab_image->l_width - tab_image->r_width, height()); 1315 canvas->DrawImageInt(*tab_image->image_r, width() - tab_image->r_width, 0); 1316} 1317 1318void Tab::PaintIcon(gfx::Canvas* canvas) { 1319 gfx::Rect bounds = favicon_bounds_; 1320 if (bounds.IsEmpty()) 1321 return; 1322 1323 bounds.set_x(GetMirroredXForRect(bounds)); 1324 1325 if (data().network_state != TabRendererData::NETWORK_STATE_NONE) { 1326 // Paint network activity (aka throbber) animation frame. 1327 ui::ThemeProvider* tp = GetThemeProvider(); 1328 gfx::ImageSkia frames(*tp->GetImageSkiaNamed( 1329 (data().network_state == TabRendererData::NETWORK_STATE_WAITING) ? 1330 IDR_THROBBER_WAITING : IDR_THROBBER)); 1331 1332 int icon_size = frames.height(); 1333 int image_offset = loading_animation_frame_ * icon_size; 1334 DrawIconCenter(canvas, frames, image_offset, 1335 icon_size, icon_size, 1336 bounds, false, SkPaint()); 1337 } else if (should_display_crashed_favicon_) { 1338 // Paint crash favicon. 1339 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 1340 gfx::ImageSkia crashed_favicon(*rb.GetImageSkiaNamed(IDR_SAD_FAVICON)); 1341 bounds.set_y(bounds.y() + favicon_hiding_offset_); 1342 DrawIconCenter(canvas, crashed_favicon, 0, 1343 crashed_favicon.width(), 1344 crashed_favicon.height(), 1345 bounds, true, SkPaint()); 1346 } else if (!data().favicon.isNull()) { 1347 // Paint the normal favicon. 1348 DrawIconCenter(canvas, data().favicon, 0, 1349 data().favicon.width(), 1350 data().favicon.height(), 1351 bounds, true, SkPaint()); 1352 } 1353} 1354 1355void Tab::AdvanceLoadingAnimation(TabRendererData::NetworkState old_state, 1356 TabRendererData::NetworkState state) { 1357 static bool initialized = false; 1358 static int loading_animation_frame_count = 0; 1359 static int waiting_animation_frame_count = 0; 1360 static int waiting_to_loading_frame_count_ratio = 0; 1361 if (!initialized) { 1362 initialized = true; 1363 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 1364 gfx::ImageSkia loading_animation(*rb.GetImageSkiaNamed(IDR_THROBBER)); 1365 loading_animation_frame_count = 1366 loading_animation.width() / loading_animation.height(); 1367 gfx::ImageSkia waiting_animation(*rb.GetImageSkiaNamed( 1368 IDR_THROBBER_WAITING)); 1369 waiting_animation_frame_count = 1370 waiting_animation.width() / waiting_animation.height(); 1371 waiting_to_loading_frame_count_ratio = 1372 waiting_animation_frame_count / loading_animation_frame_count; 1373 1374 base::debug::Alias(&loading_animation_frame_count); 1375 base::debug::Alias(&waiting_animation_frame_count); 1376 CHECK_NE(0, waiting_to_loading_frame_count_ratio) << 1377 "Number of frames in IDR_THROBBER must be equal to or greater " << 1378 "than the number of frames in IDR_THROBBER_WAITING. Please " << 1379 "investigate how this happened and update http://crbug.com/132590, " << 1380 "this is causing crashes in the wild."; 1381 } 1382 1383 // The waiting animation is the reverse of the loading animation, but at a 1384 // different rate - the following reverses and scales the animation_frame_ 1385 // so that the frame is at an equivalent position when going from one 1386 // animation to the other. 1387 if (state != old_state) { 1388 loading_animation_frame_ = loading_animation_frame_count - 1389 (loading_animation_frame_ / waiting_to_loading_frame_count_ratio); 1390 } 1391 1392 if (state == TabRendererData::NETWORK_STATE_WAITING) { 1393 loading_animation_frame_ = (loading_animation_frame_ + 1) % 1394 waiting_animation_frame_count; 1395 // Waiting steps backwards. 1396 immersive_loading_step_ = 1397 (immersive_loading_step_ - 1 + kImmersiveLoadingStepCount) % 1398 kImmersiveLoadingStepCount; 1399 } else if (state == TabRendererData::NETWORK_STATE_LOADING) { 1400 loading_animation_frame_ = (loading_animation_frame_ + 1) % 1401 loading_animation_frame_count; 1402 immersive_loading_step_ = (immersive_loading_step_ + 1) % 1403 kImmersiveLoadingStepCount; 1404 } else { 1405 loading_animation_frame_ = 0; 1406 immersive_loading_step_ = 0; 1407 } 1408 if (controller_->IsImmersiveStyle()) 1409 SchedulePaintInRect(GetImmersiveBarRect()); 1410 else 1411 ScheduleIconPaint(); 1412} 1413 1414int Tab::IconCapacity() const { 1415 if (height() < GetMinimumUnselectedSize().height()) 1416 return 0; 1417 const int available_width = 1418 std::max(0, width() - kLeftPadding - kRightPadding); 1419 const int width_per_icon = gfx::kFaviconSize; 1420 const int kPaddingBetweenIcons = 2; 1421 if (available_width >= width_per_icon && 1422 available_width < (width_per_icon + kPaddingBetweenIcons)) { 1423 return 1; 1424 } 1425 return available_width / (width_per_icon + kPaddingBetweenIcons); 1426} 1427 1428bool Tab::ShouldShowIcon() const { 1429 return chrome::ShouldTabShowFavicon( 1430 IconCapacity(), data().mini, IsActive(), data().show_icon, 1431 media_indicator_button_ ? media_indicator_button_->showing_media_state() : 1432 data_.media_state); 1433} 1434 1435bool Tab::ShouldShowMediaIndicator() const { 1436 return chrome::ShouldTabShowMediaIndicator( 1437 IconCapacity(), data().mini, IsActive(), data().show_icon, 1438 media_indicator_button_ ? media_indicator_button_->showing_media_state() : 1439 data_.media_state); 1440} 1441 1442bool Tab::ShouldShowCloseBox() const { 1443 return chrome::ShouldTabShowCloseButton( 1444 IconCapacity(), data().mini, IsActive()); 1445} 1446 1447double Tab::GetThrobValue() { 1448 const bool is_selected = IsSelected(); 1449 const double min = is_selected ? kSelectedTabOpacity : 0; 1450 const double scale = is_selected ? kSelectedTabThrobScale : 1; 1451 1452 // Showing both the pulse and title change animation at the same time is too 1453 // much. 1454 if (pulse_animation_ && pulse_animation_->is_animating() && 1455 (!mini_title_change_animation_ || 1456 !mini_title_change_animation_->is_animating())) { 1457 return pulse_animation_->GetCurrentValue() * kHoverOpacity * scale + min; 1458 } 1459 1460 if (hover_controller_.ShouldDraw()) { 1461 return kHoverOpacity * hover_controller_.GetAnimationValue() * scale + 1462 min; 1463 } 1464 1465 return is_selected ? kSelectedTabOpacity : 0; 1466} 1467 1468void Tab::SetFaviconHidingOffset(int offset) { 1469 favicon_hiding_offset_ = offset; 1470 ScheduleIconPaint(); 1471} 1472 1473void Tab::DisplayCrashedFavicon() { 1474 should_display_crashed_favicon_ = true; 1475} 1476 1477void Tab::ResetCrashedFavicon() { 1478 should_display_crashed_favicon_ = false; 1479} 1480 1481void Tab::StopCrashAnimation() { 1482 crash_icon_animation_.reset(); 1483} 1484 1485void Tab::StartCrashAnimation() { 1486 crash_icon_animation_.reset(new FaviconCrashAnimation(this)); 1487 crash_icon_animation_->Start(); 1488} 1489 1490bool Tab::IsPerformingCrashAnimation() const { 1491 return crash_icon_animation_.get() && data_.IsCrashed(); 1492} 1493 1494void Tab::ScheduleIconPaint() { 1495 gfx::Rect bounds = favicon_bounds_; 1496 if (bounds.IsEmpty()) 1497 return; 1498 1499 // Extends the area to the bottom when sad_favicon is animating. 1500 if (IsPerformingCrashAnimation()) 1501 bounds.set_height(height() - bounds.y()); 1502 bounds.set_x(GetMirroredXForRect(bounds)); 1503 SchedulePaintInRect(bounds); 1504} 1505 1506gfx::Rect Tab::GetImmersiveBarRect() const { 1507 // The main bar is as wide as the normal tab's horizontal top line. 1508 // This top line of the tab extends a few pixels left and right of the 1509 // center image due to pixels in the rounded corner images. 1510 const int kBarPadding = 1; 1511 int main_bar_left = tab_active_.l_width - kBarPadding; 1512 int main_bar_right = width() - tab_active_.r_width + kBarPadding; 1513 return gfx::Rect( 1514 main_bar_left, 0, main_bar_right - main_bar_left, kImmersiveBarHeight); 1515} 1516 1517void Tab::GetTabIdAndFrameId(views::Widget* widget, 1518 int* tab_id, 1519 int* frame_id) const { 1520 if (widget && 1521 widget->GetTopLevelWidget()->ShouldWindowContentsBeTransparent()) { 1522 *tab_id = IDR_THEME_TAB_BACKGROUND_V; 1523 *frame_id = 0; 1524 } else if (data().incognito) { 1525 *tab_id = IDR_THEME_TAB_BACKGROUND_INCOGNITO; 1526 *frame_id = IDR_THEME_FRAME_INCOGNITO; 1527 } else { 1528 *tab_id = IDR_THEME_TAB_BACKGROUND; 1529 *frame_id = IDR_THEME_FRAME; 1530 } 1531} 1532 1533MediaIndicatorButton* Tab::GetMediaIndicatorButton() { 1534 if (!media_indicator_button_) { 1535 media_indicator_button_ = new MediaIndicatorButton(); 1536 AddChildView(media_indicator_button_); // Takes ownership. 1537 } 1538 return media_indicator_button_; 1539} 1540 1541//////////////////////////////////////////////////////////////////////////////// 1542// Tab, private static: 1543 1544// static 1545void Tab::InitTabResources() { 1546 static bool initialized = false; 1547 if (initialized) 1548 return; 1549 1550 initialized = true; 1551 image_cache_ = new ImageCache(); 1552 1553 // Load the tab images once now, and maybe again later if the theme changes. 1554 LoadTabImages(); 1555} 1556 1557// static 1558void Tab::LoadTabImages() { 1559 // We're not letting people override tab images just yet. 1560 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 1561 1562 tab_alpha_.image_l = rb.GetImageSkiaNamed(IDR_TAB_ALPHA_LEFT); 1563 tab_alpha_.image_r = rb.GetImageSkiaNamed(IDR_TAB_ALPHA_RIGHT); 1564 1565 tab_active_.image_l = rb.GetImageSkiaNamed(IDR_TAB_ACTIVE_LEFT); 1566 tab_active_.image_c = rb.GetImageSkiaNamed(IDR_TAB_ACTIVE_CENTER); 1567 tab_active_.image_r = rb.GetImageSkiaNamed(IDR_TAB_ACTIVE_RIGHT); 1568 tab_active_.l_width = tab_active_.image_l->width(); 1569 tab_active_.r_width = tab_active_.image_r->width(); 1570 1571 tab_inactive_.image_l = rb.GetImageSkiaNamed(IDR_TAB_INACTIVE_LEFT); 1572 tab_inactive_.image_c = rb.GetImageSkiaNamed(IDR_TAB_INACTIVE_CENTER); 1573 tab_inactive_.image_r = rb.GetImageSkiaNamed(IDR_TAB_INACTIVE_RIGHT); 1574 tab_inactive_.l_width = tab_inactive_.image_l->width(); 1575 tab_inactive_.r_width = tab_inactive_.image_r->width(); 1576} 1577 1578// static 1579gfx::ImageSkia Tab::GetCachedImage(int resource_id, 1580 const gfx::Size& size, 1581 ui::ScaleFactor scale_factor) { 1582 for (ImageCache::const_iterator i = image_cache_->begin(); 1583 i != image_cache_->end(); ++i) { 1584 if (i->resource_id == resource_id && i->scale_factor == scale_factor && 1585 i->image.size() == size) { 1586 return i->image; 1587 } 1588 } 1589 return gfx::ImageSkia(); 1590} 1591 1592// static 1593void Tab::SetCachedImage(int resource_id, 1594 ui::ScaleFactor scale_factor, 1595 const gfx::ImageSkia& image) { 1596 DCHECK_NE(scale_factor, ui::SCALE_FACTOR_NONE); 1597 ImageCacheEntry entry; 1598 entry.resource_id = resource_id; 1599 entry.scale_factor = scale_factor; 1600 entry.image = image; 1601 image_cache_->push_front(entry); 1602 if (image_cache_->size() > kMaxImageCacheSize) 1603 image_cache_->pop_back(); 1604} 1605