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