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