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