普通文本  |  582行  |  18.82 KB

// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/views/tabs/base_tab.h"

#include <limits>

#include "base/command_line.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/tabs/tab_controller.h"
#include "chrome/common/chrome_switches.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "grit/app_resources.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "ui/base/accessibility/accessible_view_state.h"
#include "ui/base/animation/animation_container.h"
#include "ui/base/animation/slide_animation.h"
#include "ui/base/animation/throb_animation.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/text/text_elider.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/canvas_skia.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/font.h"
#include "views/controls/button/image_button.h"

// How long the pulse throb takes.
static const int kPulseDurationMs = 200;

// How long the hover state takes.
static const int kHoverDurationMs = 400;

namespace {

////////////////////////////////////////////////////////////////////////////////
// TabCloseButton
//
//  This is a Button subclass that causes middle clicks to be forwarded to the
//  parent View by explicitly not handling them in OnMousePressed.
class TabCloseButton : public views::ImageButton {
 public:
  explicit TabCloseButton(views::ButtonListener* listener)
      : views::ImageButton(listener) {
  }
  virtual ~TabCloseButton() {}

  virtual bool OnMousePressed(const views::MouseEvent& event) OVERRIDE {
    bool handled = ImageButton::OnMousePressed(event);
    // Explicitly mark midle-mouse clicks as non-handled to ensure the tab
    // sees them.
    return event.IsOnlyMiddleMouseButton() ? false : handled;
  }

  // We need to let the parent know about mouse state so that it
  // can highlight itself appropriately. Note that Exit events
  // fire before Enter events, so this works.
  virtual void OnMouseEntered(const views::MouseEvent& event) OVERRIDE {
    CustomButton::OnMouseEntered(event);
    parent()->OnMouseEntered(event);
  }

  virtual void OnMouseExited(const views::MouseEvent& event) OVERRIDE {
    CustomButton::OnMouseExited(event);
    parent()->OnMouseExited(event);
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(TabCloseButton);
};

// Draws the icon image at the center of |bounds|.
void DrawIconCenter(gfx::Canvas* canvas,
                    const SkBitmap& image,
                    int image_offset,
                    int icon_width,
                    int icon_height,
                    const gfx::Rect& bounds,
                    bool filter) {
  // Center the image within bounds.
  int dst_x = bounds.x() - (icon_width - bounds.width()) / 2;
  int dst_y = bounds.y() - (icon_height - bounds.height()) / 2;
  // NOTE: the clipping is a work around for 69528, it shouldn't be necessary.
  canvas->Save();
  canvas->ClipRectInt(dst_x, dst_y, icon_width, icon_height);
  canvas->DrawBitmapInt(image,
                        image_offset, 0, icon_width, icon_height,
                        dst_x, dst_y, icon_width, icon_height,
                        filter);
  canvas->Restore();
}

}  // namespace

// static
gfx::Font* BaseTab::font_ = NULL;
// static
int BaseTab::font_height_ = 0;

////////////////////////////////////////////////////////////////////////////////
// FaviconCrashAnimation
//
//  A custom animation subclass to manage the favicon crash animation.
class BaseTab::FaviconCrashAnimation : public ui::LinearAnimation,
                                       public ui::AnimationDelegate {
 public:
  explicit FaviconCrashAnimation(BaseTab* target)
      : ALLOW_THIS_IN_INITIALIZER_LIST(ui::LinearAnimation(1000, 25, this)),
        target_(target) {
  }
  virtual ~FaviconCrashAnimation() {}

  // ui::Animation overrides:
  virtual void AnimateToState(double state) {
    const double kHidingOffset = 27;

    if (state < .5) {
      target_->SetFaviconHidingOffset(
          static_cast<int>(floor(kHidingOffset * 2.0 * state)));
    } else {
      target_->DisplayCrashedFavicon();
      target_->SetFaviconHidingOffset(
          static_cast<int>(
              floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset))));
    }
  }

  // ui::AnimationDelegate overrides:
  virtual void AnimationCanceled(const ui::Animation* animation) {
    target_->SetFaviconHidingOffset(0);
  }

 private:
  BaseTab* target_;

  DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation);
};

BaseTab::BaseTab(TabController* controller)
    : controller_(controller),
      closing_(false),
      dragging_(false),
      favicon_hiding_offset_(0),
      loading_animation_frame_(0),
      should_display_crashed_favicon_(false),
      throbber_disabled_(false),
      theme_provider_(NULL) {
  BaseTab::InitResources();

  SetID(VIEW_ID_TAB);

  // Add the Close Button.
  close_button_ = new TabCloseButton(this);
  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
  close_button_->SetImage(views::CustomButton::BS_NORMAL,
                          rb.GetBitmapNamed(IDR_TAB_CLOSE));
  close_button_->SetImage(views::CustomButton::BS_HOT,
                          rb.GetBitmapNamed(IDR_TAB_CLOSE_H));
  close_button_->SetImage(views::CustomButton::BS_PUSHED,
                          rb.GetBitmapNamed(IDR_TAB_CLOSE_P));
  close_button_->SetTooltipText(
      UTF16ToWide(l10n_util::GetStringUTF16(IDS_TOOLTIP_CLOSE_TAB)));
  close_button_->SetAccessibleName(
      l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE));
  // Disable animation so that the red danger sign shows up immediately
  // to help avoid mis-clicks.
  close_button_->SetAnimationDuration(0);
  AddChildView(close_button_);

  SetContextMenuController(this);
}

BaseTab::~BaseTab() {
}

void BaseTab::SetData(const TabRendererData& data) {
  if (data_.Equals(data))
    return;

  TabRendererData old(data_);
  data_ = data;

  if (data_.IsCrashed()) {
    if (!should_display_crashed_favicon_ && !IsPerformingCrashAnimation()) {
      // When --reload-killed-tabs is specified, then the idea is that
      // when tab is killed, the tab has no visual indication that it
      // died and should reload when the tab is next focused without
      // the user seeing the killed tab page.
      //
      // The only exception to this is when the tab is in the
      // foreground (i.e. when it's the selected tab), because we
      // don't want to go into an infinite loop reloading a page that
      // will constantly get killed, or if it's the only tab.  So this
      // code makes it so that the favicon will only be shown for
      // killed tabs when the tab is currently selected.
      if (CommandLine::ForCurrentProcess()->
          HasSwitch(switches::kReloadKilledTabs) && !IsSelected()) {
        // If we're reloading killed tabs, we don't want to display
        // the crashed animation at all if the process was killed and
        // the tab wasn't the current tab.
        if (data_.crashed_status != base::TERMINATION_STATUS_PROCESS_WAS_KILLED)
          StartCrashAnimation();
      } else {
        StartCrashAnimation();
      }
    }
  } else {
    if (IsPerformingCrashAnimation())
      StopCrashAnimation();
    ResetCrashedFavicon();
  }

  DataChanged(old);

  Layout();
  SchedulePaint();
}

void BaseTab::UpdateLoadingAnimation(TabRendererData::NetworkState state) {
  // If this is an extension app and a command line flag is set,
  // then disable the throbber.
  throbber_disabled_ = data().app &&
      CommandLine::ForCurrentProcess()->HasSwitch(switches::kAppsNoThrob);

  if (throbber_disabled_)
    return;

  if (state == data_.network_state &&
      state == TabRendererData::NETWORK_STATE_NONE) {
    // If the network state is none and hasn't changed, do nothing. Otherwise we
    // need to advance the animation frame.
    return;
  }

  TabRendererData::NetworkState old_state = data_.network_state;
  data_.network_state = state;
  AdvanceLoadingAnimation(old_state, state);
}

void BaseTab::StartPulse() {
  if (!pulse_animation_.get()) {
    pulse_animation_.reset(new ui::ThrobAnimation(this));
    pulse_animation_->SetSlideDuration(kPulseDurationMs);
    if (animation_container_.get())
      pulse_animation_->SetContainer(animation_container_.get());
  }
  pulse_animation_->Reset();
  pulse_animation_->StartThrobbing(std::numeric_limits<int>::max());
}

void BaseTab::StopPulse() {
  if (!pulse_animation_.get())
    return;

  pulse_animation_->Stop();  // Do stop so we get notified.
  pulse_animation_.reset(NULL);
}

void BaseTab::set_animation_container(ui::AnimationContainer* container) {
  animation_container_ = container;
}

bool BaseTab::IsCloseable() const {
  return controller() ? controller()->IsTabCloseable(this) : true;
}

bool BaseTab::IsActive() const {
  return controller() ? controller()->IsActiveTab(this) : true;
}

bool BaseTab::IsSelected() const {
  return controller() ? controller()->IsTabSelected(this) : true;
}

ui::ThemeProvider* BaseTab::GetThemeProvider() const {
  ui::ThemeProvider* tp = View::GetThemeProvider();
  return tp ? tp : theme_provider_;
}

bool BaseTab::OnMousePressed(const views::MouseEvent& event) {
  if (!controller())
    return false;

  if (event.IsOnlyLeftMouseButton()) {
    if (event.IsShiftDown() && event.IsControlDown()) {
      controller()->AddSelectionFromAnchorTo(this);
    } else if (event.IsShiftDown()) {
      controller()->ExtendSelectionTo(this);
    } else if (event.IsControlDown()) {
      controller()->ToggleSelected(this);
      if (!IsSelected()) {
        // Don't allow dragging non-selected tabs.
        return false;
      }
    } else if (!IsSelected()) {
      controller()->SelectTab(this);
    }
    controller()->MaybeStartDrag(this, event);
  }
  return true;
}

bool BaseTab::OnMouseDragged(const views::MouseEvent& event) {
  if (controller())
    controller()->ContinueDrag(event);
  return true;
}

void BaseTab::OnMouseReleased(const views::MouseEvent& event) {
  if (!controller())
    return;

  // Notify the drag helper that we're done with any potential drag operations.
  // Clean up the drag helper, which is re-created on the next mouse press.
  // In some cases, ending the drag will schedule the tab for destruction; if
  // so, bail immediately, since our members are already dead and we shouldn't
  // do anything else except drop the tab where it is.
  if (controller()->EndDrag(false))
    return;

  // Close tab on middle click, but only if the button is released over the tab
  // (normal windows behavior is to discard presses of a UI element where the
  // releases happen off the element).
  if (event.IsMiddleMouseButton()) {
    if (HitTest(event.location())) {
      controller()->CloseTab(this);
    } else if (closing_) {
      // We're animating closed and a middle mouse button was pushed on us but
      // we don't contain the mouse anymore. We assume the user is clicking
      // quicker than the animation and we should close the tab that falls under
      // the mouse.
      BaseTab* closest_tab = controller()->GetTabAt(this, event.location());
      if (closest_tab)
        controller()->CloseTab(closest_tab);
    }
  } else if (event.IsOnlyLeftMouseButton() && !event.IsShiftDown() &&
             !event.IsControlDown()) {
    // If the tab was already selected mouse pressed doesn't change the
    // selection. Reset it now to handle the case where multiple tabs were
    // selected.
    controller()->SelectTab(this);
  }
}

void BaseTab::OnMouseCaptureLost() {
  if (controller())
    controller()->EndDrag(true);
}

void BaseTab::OnMouseEntered(const views::MouseEvent& event) {
  if (!hover_animation_.get()) {
    hover_animation_.reset(new ui::SlideAnimation(this));
    hover_animation_->SetContainer(animation_container_.get());
    hover_animation_->SetSlideDuration(kHoverDurationMs);
  }
  hover_animation_->SetTweenType(ui::Tween::EASE_OUT);
  hover_animation_->Show();
}

void BaseTab::OnMouseExited(const views::MouseEvent& event) {
  hover_animation_->SetTweenType(ui::Tween::EASE_IN);
  hover_animation_->Hide();
}

bool BaseTab::GetTooltipText(const gfx::Point& p, std::wstring* tooltip) {
  if (data_.title.empty())
    return false;

  // Only show the tooltip if the title is truncated.
  if (font_->GetStringWidth(data_.title) > GetTitleBounds().width()) {
    *tooltip = UTF16ToWide(data_.title);
    return true;
  }
  return false;
}

void BaseTab::GetAccessibleState(ui::AccessibleViewState* state) {
  state->role = ui::AccessibilityTypes::ROLE_PAGETAB;
  state->name = data_.title;
}

void BaseTab::AdvanceLoadingAnimation(TabRendererData::NetworkState old_state,
                                      TabRendererData::NetworkState state) {
  static bool initialized = false;
  static int loading_animation_frame_count = 0;
  static int waiting_animation_frame_count = 0;
  static int waiting_to_loading_frame_count_ratio = 0;
  if (!initialized) {
    initialized = true;
    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
    SkBitmap loading_animation(*rb.GetBitmapNamed(IDR_THROBBER));
    loading_animation_frame_count =
        loading_animation.width() / loading_animation.height();
    SkBitmap waiting_animation(*rb.GetBitmapNamed(IDR_THROBBER_WAITING));
    waiting_animation_frame_count =
        waiting_animation.width() / waiting_animation.height();
    waiting_to_loading_frame_count_ratio =
        waiting_animation_frame_count / loading_animation_frame_count;
  }

  // The waiting animation is the reverse of the loading animation, but at a
  // different rate - the following reverses and scales the animation_frame_
  // so that the frame is at an equivalent position when going from one
  // animation to the other.
  if (state != old_state) {
    loading_animation_frame_ = loading_animation_frame_count -
        (loading_animation_frame_ / waiting_to_loading_frame_count_ratio);
  }

  if (state != TabRendererData::NETWORK_STATE_NONE) {
    loading_animation_frame_ = (loading_animation_frame_ + 1) %
        ((state == TabRendererData::NETWORK_STATE_WAITING) ?
            waiting_animation_frame_count : loading_animation_frame_count);
  } else {
    loading_animation_frame_ = 0;
  }
  ScheduleIconPaint();
}

void BaseTab::PaintIcon(gfx::Canvas* canvas) {
  gfx::Rect bounds = GetIconBounds();
  if (bounds.IsEmpty())
    return;

  // The size of bounds has to be kFaviconSize x kFaviconSize.
  DCHECK_EQ(kFaviconSize, bounds.width());
  DCHECK_EQ(kFaviconSize, bounds.height());

  bounds.set_x(GetMirroredXForRect(bounds));

  if (data().network_state != TabRendererData::NETWORK_STATE_NONE) {
    ui::ThemeProvider* tp = GetThemeProvider();
    SkBitmap frames(*tp->GetBitmapNamed(
        (data().network_state == TabRendererData::NETWORK_STATE_WAITING) ?
        IDR_THROBBER_WAITING : IDR_THROBBER));

    int icon_size = frames.height();
    int image_offset = loading_animation_frame_ * icon_size;
    DrawIconCenter(canvas, frames, image_offset,
                   icon_size, icon_size, bounds, false);
  } else {
    canvas->Save();
    canvas->ClipRectInt(0, 0, width(), height());
    if (should_display_crashed_favicon_) {
      ResourceBundle& rb = ResourceBundle::GetSharedInstance();
      SkBitmap crashed_favicon(*rb.GetBitmapNamed(IDR_SAD_FAVICON));
      bounds.set_y(bounds.y() + favicon_hiding_offset_);
      DrawIconCenter(canvas, crashed_favicon, 0,
                     crashed_favicon.width(),
                     crashed_favicon.height(), bounds, true);
    } else {
      if (!data().favicon.isNull()) {
        // TODO(pkasting): Use code in tab_icon_view.cc:PaintIcon() (or switch
        // to using that class to render the favicon).
        DrawIconCenter(canvas, data().favicon, 0,
                       data().favicon.width(),
                       data().favicon.height(),
                       bounds, true);
      }
    }
    canvas->Restore();
  }
}

void BaseTab::PaintTitle(gfx::Canvas* canvas, SkColor title_color) {
  // Paint the Title.
  const gfx::Rect& title_bounds = GetTitleBounds();
  string16 title = data().title;

  if (title.empty()) {
    title = data().loading ?
        l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) :
        TabContentsWrapper::GetDefaultTitle();
  } else {
    Browser::FormatTitleForDisplay(&title);
  }

#if defined(OS_WIN)
  canvas->AsCanvasSkia()->DrawFadeTruncatingString(title,
      gfx::CanvasSkia::TruncateFadeTail, 0, *font_, title_color, title_bounds);
#else
  canvas->DrawStringInt(title, *font_, title_color,
                        title_bounds.x(), title_bounds.y(),
                        title_bounds.width(), title_bounds.height());
#endif
}

void BaseTab::AnimationProgressed(const ui::Animation* animation) {
  SchedulePaint();
}

void BaseTab::AnimationCanceled(const ui::Animation* animation) {
  SchedulePaint();
}

void BaseTab::AnimationEnded(const ui::Animation* animation) {
  SchedulePaint();
}

void BaseTab::ButtonPressed(views::Button* sender, const views::Event& event) {
  DCHECK(sender == close_button_);
  controller()->CloseTab(this);
}

void BaseTab::ShowContextMenuForView(views::View* source,
                                     const gfx::Point& p,
                                     bool is_mouse_gesture) {
  if (controller())
    controller()->ShowContextMenuForTab(this, p);
}

int BaseTab::loading_animation_frame() const {
  return loading_animation_frame_;
}

bool BaseTab::should_display_crashed_favicon() const {
  return should_display_crashed_favicon_;
}

int BaseTab::favicon_hiding_offset() const {
  return favicon_hiding_offset_;
}

void BaseTab::SetFaviconHidingOffset(int offset) {
  favicon_hiding_offset_ = offset;
  ScheduleIconPaint();
}

void BaseTab::DisplayCrashedFavicon() {
  should_display_crashed_favicon_ = true;
}

void BaseTab::ResetCrashedFavicon() {
  should_display_crashed_favicon_ = false;
}

void BaseTab::StartCrashAnimation() {
  if (!crash_animation_.get())
    crash_animation_.reset(new FaviconCrashAnimation(this));
  crash_animation_->Stop();
  crash_animation_->Start();
}

void BaseTab::StopCrashAnimation() {
  if (!crash_animation_.get())
    return;
  crash_animation_->Stop();
}

bool BaseTab::IsPerformingCrashAnimation() const {
  return crash_animation_.get() && crash_animation_->is_animating();
}

void BaseTab::ScheduleIconPaint() {
  gfx::Rect bounds = GetIconBounds();
  if (bounds.IsEmpty())
    return;

  // Extends the area to the bottom when sad_favicon is
  // animating.
  if (IsPerformingCrashAnimation())
    bounds.set_height(height() - bounds.y());
  bounds.set_x(GetMirroredXForRect(bounds));
  SchedulePaintInRect(bounds);
}

// static
void BaseTab::InitResources() {
  static bool initialized = false;
  if (!initialized) {
    initialized = true;
    font_ = new gfx::Font(
        ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::BaseFont));
    font_height_ = font_->GetHeight();
  }
}