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.
34static const int kPulseDurationMs = 200;
35
36// How long the hover state takes.
37static const int kHoverDurationMs = 400;
38
39namespace {
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.
46class 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|.
78void 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
101gfx::Font* BaseTab::font_ = NULL;
102// static
103int BaseTab::font_height_ = 0;
104
105////////////////////////////////////////////////////////////////////////////////
106// FaviconCrashAnimation
107//
108//  A custom animation subclass to manage the favicon crash animation.
109class 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
144BaseTab::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
178BaseTab::~BaseTab() {
179}
180
181void 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
224void 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
245void 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
256void 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
264void BaseTab::set_animation_container(ui::AnimationContainer* container) {
265  animation_container_ = container;
266}
267
268bool BaseTab::IsCloseable() const {
269  return controller() ? controller()->IsTabCloseable(this) : true;
270}
271
272bool BaseTab::IsActive() const {
273  return controller() ? controller()->IsActiveTab(this) : true;
274}
275
276bool BaseTab::IsSelected() const {
277  return controller() ? controller()->IsTabSelected(this) : true;
278}
279
280ui::ThemeProvider* BaseTab::GetThemeProvider() const {
281  ui::ThemeProvider* tp = View::GetThemeProvider();
282  return tp ? tp : theme_provider_;
283}
284
285bool 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
308bool BaseTab::OnMouseDragged(const views::MouseEvent& event) {
309  if (controller())
310    controller()->ContinueDrag(event);
311  return true;
312}
313
314void 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
350void BaseTab::OnMouseCaptureLost() {
351  if (controller())
352    controller()->EndDrag(true);
353}
354
355void 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
365void BaseTab::OnMouseExited(const views::MouseEvent& event) {
366  hover_animation_->SetTweenType(ui::Tween::EASE_IN);
367  hover_animation_->Hide();
368}
369
370bool 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
382void BaseTab::GetAccessibleState(ui::AccessibleViewState* state) {
383  state->role = ui::AccessibilityTypes::ROLE_PAGETAB;
384  state->name = data_.title;
385}
386
387void 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
425void 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
470void 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
493void BaseTab::AnimationProgressed(const ui::Animation* animation) {
494  SchedulePaint();
495}
496
497void BaseTab::AnimationCanceled(const ui::Animation* animation) {
498  SchedulePaint();
499}
500
501void BaseTab::AnimationEnded(const ui::Animation* animation) {
502  SchedulePaint();
503}
504
505void BaseTab::ButtonPressed(views::Button* sender, const views::Event& event) {
506  DCHECK(sender == close_button_);
507  controller()->CloseTab(this);
508}
509
510void 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
517int BaseTab::loading_animation_frame() const {
518  return loading_animation_frame_;
519}
520
521bool BaseTab::should_display_crashed_favicon() const {
522  return should_display_crashed_favicon_;
523}
524
525int BaseTab::favicon_hiding_offset() const {
526  return favicon_hiding_offset_;
527}
528
529void BaseTab::SetFaviconHidingOffset(int offset) {
530  favicon_hiding_offset_ = offset;
531  ScheduleIconPaint();
532}
533
534void BaseTab::DisplayCrashedFavicon() {
535  should_display_crashed_favicon_ = true;
536}
537
538void BaseTab::ResetCrashedFavicon() {
539  should_display_crashed_favicon_ = false;
540}
541
542void BaseTab::StartCrashAnimation() {
543  if (!crash_animation_.get())
544    crash_animation_.reset(new FaviconCrashAnimation(this));
545  crash_animation_->Stop();
546  crash_animation_->Start();
547}
548
549void BaseTab::StopCrashAnimation() {
550  if (!crash_animation_.get())
551    return;
552  crash_animation_->Stop();
553}
554
555bool BaseTab::IsPerformingCrashAnimation() const {
556  return crash_animation_.get() && crash_animation_->is_animating();
557}
558
559void 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
573void 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