1 // Copyright (c) 2011 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/base_tab.h" 6 7 #include <limits> 8 9 #include "base/command_line.h" 10 #include "base/utf_string_conversions.h" 11 #include "chrome/browser/ui/browser.h" 12 #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" 13 #include "chrome/browser/ui/view_ids.h" 14 #include "chrome/browser/ui/views/tabs/tab_controller.h" 15 #include "chrome/common/chrome_switches.h" 16 #include "content/browser/tab_contents/tab_contents.h" 17 #include "grit/app_resources.h" 18 #include "grit/generated_resources.h" 19 #include "grit/theme_resources.h" 20 #include "ui/base/accessibility/accessible_view_state.h" 21 #include "ui/base/animation/animation_container.h" 22 #include "ui/base/animation/slide_animation.h" 23 #include "ui/base/animation/throb_animation.h" 24 #include "ui/base/l10n/l10n_util.h" 25 #include "ui/base/resource/resource_bundle.h" 26 #include "ui/base/text/text_elider.h" 27 #include "ui/base/theme_provider.h" 28 #include "ui/gfx/canvas_skia.h" 29 #include "ui/gfx/favicon_size.h" 30 #include "ui/gfx/font.h" 31 #include "views/controls/button/image_button.h" 32 33 // How long the pulse throb takes. 34 static const int kPulseDurationMs = 200; 35 36 // How long the hover state takes. 37 static const int kHoverDurationMs = 400; 38 39 namespace { 40 41 //////////////////////////////////////////////////////////////////////////////// 42 // TabCloseButton 43 // 44 // This is a Button subclass that causes middle clicks to be forwarded to the 45 // parent View by explicitly not handling them in OnMousePressed. 46 class TabCloseButton : public views::ImageButton { 47 public: 48 explicit TabCloseButton(views::ButtonListener* listener) 49 : views::ImageButton(listener) { 50 } 51 virtual ~TabCloseButton() {} 52 53 virtual bool OnMousePressed(const views::MouseEvent& event) OVERRIDE { 54 bool handled = ImageButton::OnMousePressed(event); 55 // Explicitly mark midle-mouse clicks as non-handled to ensure the tab 56 // sees them. 57 return event.IsOnlyMiddleMouseButton() ? false : handled; 58 } 59 60 // We need to let the parent know about mouse state so that it 61 // can highlight itself appropriately. Note that Exit events 62 // fire before Enter events, so this works. 63 virtual void OnMouseEntered(const views::MouseEvent& event) OVERRIDE { 64 CustomButton::OnMouseEntered(event); 65 parent()->OnMouseEntered(event); 66 } 67 68 virtual void OnMouseExited(const views::MouseEvent& event) OVERRIDE { 69 CustomButton::OnMouseExited(event); 70 parent()->OnMouseExited(event); 71 } 72 73 private: 74 DISALLOW_COPY_AND_ASSIGN(TabCloseButton); 75 }; 76 77 // Draws the icon image at the center of |bounds|. 78 void DrawIconCenter(gfx::Canvas* canvas, 79 const SkBitmap& image, 80 int image_offset, 81 int icon_width, 82 int icon_height, 83 const gfx::Rect& bounds, 84 bool filter) { 85 // Center the image within bounds. 86 int dst_x = bounds.x() - (icon_width - bounds.width()) / 2; 87 int dst_y = bounds.y() - (icon_height - bounds.height()) / 2; 88 // NOTE: the clipping is a work around for 69528, it shouldn't be necessary. 89 canvas->Save(); 90 canvas->ClipRectInt(dst_x, dst_y, icon_width, icon_height); 91 canvas->DrawBitmapInt(image, 92 image_offset, 0, icon_width, icon_height, 93 dst_x, dst_y, icon_width, icon_height, 94 filter); 95 canvas->Restore(); 96 } 97 98 } // namespace 99 100 // static 101 gfx::Font* BaseTab::font_ = NULL; 102 // static 103 int BaseTab::font_height_ = 0; 104 105 //////////////////////////////////////////////////////////////////////////////// 106 // FaviconCrashAnimation 107 // 108 // A custom animation subclass to manage the favicon crash animation. 109 class BaseTab::FaviconCrashAnimation : public ui::LinearAnimation, 110 public ui::AnimationDelegate { 111 public: 112 explicit FaviconCrashAnimation(BaseTab* target) 113 : ALLOW_THIS_IN_INITIALIZER_LIST(ui::LinearAnimation(1000, 25, this)), 114 target_(target) { 115 } 116 virtual ~FaviconCrashAnimation() {} 117 118 // ui::Animation overrides: 119 virtual void AnimateToState(double state) { 120 const double kHidingOffset = 27; 121 122 if (state < .5) { 123 target_->SetFaviconHidingOffset( 124 static_cast<int>(floor(kHidingOffset * 2.0 * state))); 125 } else { 126 target_->DisplayCrashedFavicon(); 127 target_->SetFaviconHidingOffset( 128 static_cast<int>( 129 floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset)))); 130 } 131 } 132 133 // ui::AnimationDelegate overrides: 134 virtual void AnimationCanceled(const ui::Animation* animation) { 135 target_->SetFaviconHidingOffset(0); 136 } 137 138 private: 139 BaseTab* target_; 140 141 DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation); 142 }; 143 144 BaseTab::BaseTab(TabController* controller) 145 : controller_(controller), 146 closing_(false), 147 dragging_(false), 148 favicon_hiding_offset_(0), 149 loading_animation_frame_(0), 150 should_display_crashed_favicon_(false), 151 throbber_disabled_(false), 152 theme_provider_(NULL) { 153 BaseTab::InitResources(); 154 155 SetID(VIEW_ID_TAB); 156 157 // Add the Close Button. 158 close_button_ = new TabCloseButton(this); 159 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 160 close_button_->SetImage(views::CustomButton::BS_NORMAL, 161 rb.GetBitmapNamed(IDR_TAB_CLOSE)); 162 close_button_->SetImage(views::CustomButton::BS_HOT, 163 rb.GetBitmapNamed(IDR_TAB_CLOSE_H)); 164 close_button_->SetImage(views::CustomButton::BS_PUSHED, 165 rb.GetBitmapNamed(IDR_TAB_CLOSE_P)); 166 close_button_->SetTooltipText( 167 UTF16ToWide(l10n_util::GetStringUTF16(IDS_TOOLTIP_CLOSE_TAB))); 168 close_button_->SetAccessibleName( 169 l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE)); 170 // Disable animation so that the red danger sign shows up immediately 171 // to help avoid mis-clicks. 172 close_button_->SetAnimationDuration(0); 173 AddChildView(close_button_); 174 175 SetContextMenuController(this); 176 } 177 178 BaseTab::~BaseTab() { 179 } 180 181 void BaseTab::SetData(const TabRendererData& data) { 182 if (data_.Equals(data)) 183 return; 184 185 TabRendererData old(data_); 186 data_ = data; 187 188 if (data_.IsCrashed()) { 189 if (!should_display_crashed_favicon_ && !IsPerformingCrashAnimation()) { 190 // When --reload-killed-tabs is specified, then the idea is that 191 // when tab is killed, the tab has no visual indication that it 192 // died and should reload when the tab is next focused without 193 // the user seeing the killed tab page. 194 // 195 // The only exception to this is when the tab is in the 196 // foreground (i.e. when it's the selected tab), because we 197 // don't want to go into an infinite loop reloading a page that 198 // will constantly get killed, or if it's the only tab. So this 199 // code makes it so that the favicon will only be shown for 200 // killed tabs when the tab is currently selected. 201 if (CommandLine::ForCurrentProcess()-> 202 HasSwitch(switches::kReloadKilledTabs) && !IsSelected()) { 203 // If we're reloading killed tabs, we don't want to display 204 // the crashed animation at all if the process was killed and 205 // the tab wasn't the current tab. 206 if (data_.crashed_status != base::TERMINATION_STATUS_PROCESS_WAS_KILLED) 207 StartCrashAnimation(); 208 } else { 209 StartCrashAnimation(); 210 } 211 } 212 } else { 213 if (IsPerformingCrashAnimation()) 214 StopCrashAnimation(); 215 ResetCrashedFavicon(); 216 } 217 218 DataChanged(old); 219 220 Layout(); 221 SchedulePaint(); 222 } 223 224 void BaseTab::UpdateLoadingAnimation(TabRendererData::NetworkState state) { 225 // If this is an extension app and a command line flag is set, 226 // then disable the throbber. 227 throbber_disabled_ = data().app && 228 CommandLine::ForCurrentProcess()->HasSwitch(switches::kAppsNoThrob); 229 230 if (throbber_disabled_) 231 return; 232 233 if (state == data_.network_state && 234 state == TabRendererData::NETWORK_STATE_NONE) { 235 // If the network state is none and hasn't changed, do nothing. Otherwise we 236 // need to advance the animation frame. 237 return; 238 } 239 240 TabRendererData::NetworkState old_state = data_.network_state; 241 data_.network_state = state; 242 AdvanceLoadingAnimation(old_state, state); 243 } 244 245 void BaseTab::StartPulse() { 246 if (!pulse_animation_.get()) { 247 pulse_animation_.reset(new ui::ThrobAnimation(this)); 248 pulse_animation_->SetSlideDuration(kPulseDurationMs); 249 if (animation_container_.get()) 250 pulse_animation_->SetContainer(animation_container_.get()); 251 } 252 pulse_animation_->Reset(); 253 pulse_animation_->StartThrobbing(std::numeric_limits<int>::max()); 254 } 255 256 void BaseTab::StopPulse() { 257 if (!pulse_animation_.get()) 258 return; 259 260 pulse_animation_->Stop(); // Do stop so we get notified. 261 pulse_animation_.reset(NULL); 262 } 263 264 void BaseTab::set_animation_container(ui::AnimationContainer* container) { 265 animation_container_ = container; 266 } 267 268 bool BaseTab::IsCloseable() const { 269 return controller() ? controller()->IsTabCloseable(this) : true; 270 } 271 272 bool BaseTab::IsActive() const { 273 return controller() ? controller()->IsActiveTab(this) : true; 274 } 275 276 bool BaseTab::IsSelected() const { 277 return controller() ? controller()->IsTabSelected(this) : true; 278 } 279 280 ui::ThemeProvider* BaseTab::GetThemeProvider() const { 281 ui::ThemeProvider* tp = View::GetThemeProvider(); 282 return tp ? tp : theme_provider_; 283 } 284 285 bool BaseTab::OnMousePressed(const views::MouseEvent& event) { 286 if (!controller()) 287 return false; 288 289 if (event.IsOnlyLeftMouseButton()) { 290 if (event.IsShiftDown() && event.IsControlDown()) { 291 controller()->AddSelectionFromAnchorTo(this); 292 } else if (event.IsShiftDown()) { 293 controller()->ExtendSelectionTo(this); 294 } else if (event.IsControlDown()) { 295 controller()->ToggleSelected(this); 296 if (!IsSelected()) { 297 // Don't allow dragging non-selected tabs. 298 return false; 299 } 300 } else if (!IsSelected()) { 301 controller()->SelectTab(this); 302 } 303 controller()->MaybeStartDrag(this, event); 304 } 305 return true; 306 } 307 308 bool BaseTab::OnMouseDragged(const views::MouseEvent& event) { 309 if (controller()) 310 controller()->ContinueDrag(event); 311 return true; 312 } 313 314 void BaseTab::OnMouseReleased(const views::MouseEvent& event) { 315 if (!controller()) 316 return; 317 318 // Notify the drag helper that we're done with any potential drag operations. 319 // Clean up the drag helper, which is re-created on the next mouse press. 320 // In some cases, ending the drag will schedule the tab for destruction; if 321 // so, bail immediately, since our members are already dead and we shouldn't 322 // do anything else except drop the tab where it is. 323 if (controller()->EndDrag(false)) 324 return; 325 326 // Close tab on middle click, but only if the button is released over the tab 327 // (normal windows behavior is to discard presses of a UI element where the 328 // releases happen off the element). 329 if (event.IsMiddleMouseButton()) { 330 if (HitTest(event.location())) { 331 controller()->CloseTab(this); 332 } else if (closing_) { 333 // We're animating closed and a middle mouse button was pushed on us but 334 // we don't contain the mouse anymore. We assume the user is clicking 335 // quicker than the animation and we should close the tab that falls under 336 // the mouse. 337 BaseTab* closest_tab = controller()->GetTabAt(this, event.location()); 338 if (closest_tab) 339 controller()->CloseTab(closest_tab); 340 } 341 } else if (event.IsOnlyLeftMouseButton() && !event.IsShiftDown() && 342 !event.IsControlDown()) { 343 // If the tab was already selected mouse pressed doesn't change the 344 // selection. Reset it now to handle the case where multiple tabs were 345 // selected. 346 controller()->SelectTab(this); 347 } 348 } 349 350 void BaseTab::OnMouseCaptureLost() { 351 if (controller()) 352 controller()->EndDrag(true); 353 } 354 355 void BaseTab::OnMouseEntered(const views::MouseEvent& event) { 356 if (!hover_animation_.get()) { 357 hover_animation_.reset(new ui::SlideAnimation(this)); 358 hover_animation_->SetContainer(animation_container_.get()); 359 hover_animation_->SetSlideDuration(kHoverDurationMs); 360 } 361 hover_animation_->SetTweenType(ui::Tween::EASE_OUT); 362 hover_animation_->Show(); 363 } 364 365 void BaseTab::OnMouseExited(const views::MouseEvent& event) { 366 hover_animation_->SetTweenType(ui::Tween::EASE_IN); 367 hover_animation_->Hide(); 368 } 369 370 bool BaseTab::GetTooltipText(const gfx::Point& p, std::wstring* tooltip) { 371 if (data_.title.empty()) 372 return false; 373 374 // Only show the tooltip if the title is truncated. 375 if (font_->GetStringWidth(data_.title) > GetTitleBounds().width()) { 376 *tooltip = UTF16ToWide(data_.title); 377 return true; 378 } 379 return false; 380 } 381 382 void BaseTab::GetAccessibleState(ui::AccessibleViewState* state) { 383 state->role = ui::AccessibilityTypes::ROLE_PAGETAB; 384 state->name = data_.title; 385 } 386 387 void BaseTab::AdvanceLoadingAnimation(TabRendererData::NetworkState old_state, 388 TabRendererData::NetworkState state) { 389 static bool initialized = false; 390 static int loading_animation_frame_count = 0; 391 static int waiting_animation_frame_count = 0; 392 static int waiting_to_loading_frame_count_ratio = 0; 393 if (!initialized) { 394 initialized = true; 395 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 396 SkBitmap loading_animation(*rb.GetBitmapNamed(IDR_THROBBER)); 397 loading_animation_frame_count = 398 loading_animation.width() / loading_animation.height(); 399 SkBitmap waiting_animation(*rb.GetBitmapNamed(IDR_THROBBER_WAITING)); 400 waiting_animation_frame_count = 401 waiting_animation.width() / waiting_animation.height(); 402 waiting_to_loading_frame_count_ratio = 403 waiting_animation_frame_count / loading_animation_frame_count; 404 } 405 406 // The waiting animation is the reverse of the loading animation, but at a 407 // different rate - the following reverses and scales the animation_frame_ 408 // so that the frame is at an equivalent position when going from one 409 // animation to the other. 410 if (state != old_state) { 411 loading_animation_frame_ = loading_animation_frame_count - 412 (loading_animation_frame_ / waiting_to_loading_frame_count_ratio); 413 } 414 415 if (state != TabRendererData::NETWORK_STATE_NONE) { 416 loading_animation_frame_ = (loading_animation_frame_ + 1) % 417 ((state == TabRendererData::NETWORK_STATE_WAITING) ? 418 waiting_animation_frame_count : loading_animation_frame_count); 419 } else { 420 loading_animation_frame_ = 0; 421 } 422 ScheduleIconPaint(); 423 } 424 425 void BaseTab::PaintIcon(gfx::Canvas* canvas) { 426 gfx::Rect bounds = GetIconBounds(); 427 if (bounds.IsEmpty()) 428 return; 429 430 // The size of bounds has to be kFaviconSize x kFaviconSize. 431 DCHECK_EQ(kFaviconSize, bounds.width()); 432 DCHECK_EQ(kFaviconSize, bounds.height()); 433 434 bounds.set_x(GetMirroredXForRect(bounds)); 435 436 if (data().network_state != TabRendererData::NETWORK_STATE_NONE) { 437 ui::ThemeProvider* tp = GetThemeProvider(); 438 SkBitmap frames(*tp->GetBitmapNamed( 439 (data().network_state == TabRendererData::NETWORK_STATE_WAITING) ? 440 IDR_THROBBER_WAITING : IDR_THROBBER)); 441 442 int icon_size = frames.height(); 443 int image_offset = loading_animation_frame_ * icon_size; 444 DrawIconCenter(canvas, frames, image_offset, 445 icon_size, icon_size, bounds, false); 446 } else { 447 canvas->Save(); 448 canvas->ClipRectInt(0, 0, width(), height()); 449 if (should_display_crashed_favicon_) { 450 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 451 SkBitmap crashed_favicon(*rb.GetBitmapNamed(IDR_SAD_FAVICON)); 452 bounds.set_y(bounds.y() + favicon_hiding_offset_); 453 DrawIconCenter(canvas, crashed_favicon, 0, 454 crashed_favicon.width(), 455 crashed_favicon.height(), bounds, true); 456 } else { 457 if (!data().favicon.isNull()) { 458 // TODO(pkasting): Use code in tab_icon_view.cc:PaintIcon() (or switch 459 // to using that class to render the favicon). 460 DrawIconCenter(canvas, data().favicon, 0, 461 data().favicon.width(), 462 data().favicon.height(), 463 bounds, true); 464 } 465 } 466 canvas->Restore(); 467 } 468 } 469 470 void BaseTab::PaintTitle(gfx::Canvas* canvas, SkColor title_color) { 471 // Paint the Title. 472 const gfx::Rect& title_bounds = GetTitleBounds(); 473 string16 title = data().title; 474 475 if (title.empty()) { 476 title = data().loading ? 477 l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) : 478 TabContentsWrapper::GetDefaultTitle(); 479 } else { 480 Browser::FormatTitleForDisplay(&title); 481 } 482 483 #if defined(OS_WIN) 484 canvas->AsCanvasSkia()->DrawFadeTruncatingString(title, 485 gfx::CanvasSkia::TruncateFadeTail, 0, *font_, title_color, title_bounds); 486 #else 487 canvas->DrawStringInt(title, *font_, title_color, 488 title_bounds.x(), title_bounds.y(), 489 title_bounds.width(), title_bounds.height()); 490 #endif 491 } 492 493 void BaseTab::AnimationProgressed(const ui::Animation* animation) { 494 SchedulePaint(); 495 } 496 497 void BaseTab::AnimationCanceled(const ui::Animation* animation) { 498 SchedulePaint(); 499 } 500 501 void BaseTab::AnimationEnded(const ui::Animation* animation) { 502 SchedulePaint(); 503 } 504 505 void BaseTab::ButtonPressed(views::Button* sender, const views::Event& event) { 506 DCHECK(sender == close_button_); 507 controller()->CloseTab(this); 508 } 509 510 void BaseTab::ShowContextMenuForView(views::View* source, 511 const gfx::Point& p, 512 bool is_mouse_gesture) { 513 if (controller()) 514 controller()->ShowContextMenuForTab(this, p); 515 } 516 517 int BaseTab::loading_animation_frame() const { 518 return loading_animation_frame_; 519 } 520 521 bool BaseTab::should_display_crashed_favicon() const { 522 return should_display_crashed_favicon_; 523 } 524 525 int BaseTab::favicon_hiding_offset() const { 526 return favicon_hiding_offset_; 527 } 528 529 void BaseTab::SetFaviconHidingOffset(int offset) { 530 favicon_hiding_offset_ = offset; 531 ScheduleIconPaint(); 532 } 533 534 void BaseTab::DisplayCrashedFavicon() { 535 should_display_crashed_favicon_ = true; 536 } 537 538 void BaseTab::ResetCrashedFavicon() { 539 should_display_crashed_favicon_ = false; 540 } 541 542 void BaseTab::StartCrashAnimation() { 543 if (!crash_animation_.get()) 544 crash_animation_.reset(new FaviconCrashAnimation(this)); 545 crash_animation_->Stop(); 546 crash_animation_->Start(); 547 } 548 549 void BaseTab::StopCrashAnimation() { 550 if (!crash_animation_.get()) 551 return; 552 crash_animation_->Stop(); 553 } 554 555 bool BaseTab::IsPerformingCrashAnimation() const { 556 return crash_animation_.get() && crash_animation_->is_animating(); 557 } 558 559 void BaseTab::ScheduleIconPaint() { 560 gfx::Rect bounds = GetIconBounds(); 561 if (bounds.IsEmpty()) 562 return; 563 564 // Extends the area to the bottom when sad_favicon is 565 // animating. 566 if (IsPerformingCrashAnimation()) 567 bounds.set_height(height() - bounds.y()); 568 bounds.set_x(GetMirroredXForRect(bounds)); 569 SchedulePaintInRect(bounds); 570 } 571 572 // static 573 void BaseTab::InitResources() { 574 static bool initialized = false; 575 if (!initialized) { 576 initialized = true; 577 font_ = new gfx::Font( 578 ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::BaseFont)); 579 font_height_ = font_->GetHeight(); 580 } 581 } 582