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