Home | History | Annotate | Download | only in tabs
      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