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