| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/tabs/tab.h" |
| |
| #include <limits> |
| |
| #include "base/command_line.h" |
| #include "base/debug/alias.h" |
| #include "base/profiler/scoped_tracker.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tab_contents/core_tab_helper.h" |
| #include "chrome/browser/ui/tabs/tab_utils.h" |
| #include "chrome/browser/ui/view_ids.h" |
| #include "chrome/browser/ui/views/layout_constants.h" |
| #include "chrome/browser/ui/views/tabs/media_indicator_button.h" |
| #include "chrome/browser/ui/views/tabs/tab_controller.h" |
| #include "chrome/browser/ui/views/theme_image_mapper.h" |
| #include "chrome/browser/ui/views/touch_uma/touch_uma.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "content/public/browser/user_metrics.h" |
| #include "grit/components_scaled_resources.h" |
| #include "grit/theme_resources.h" |
| #include "third_party/skia/include/effects/SkGradientShader.h" |
| #include "ui/accessibility/ax_view_state.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/list_selection_model.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/gfx/animation/animation_container.h" |
| #include "ui/gfx/animation/multi_animation.h" |
| #include "ui/gfx/animation/throb_animation.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_analysis.h" |
| #include "ui/gfx/favicon_size.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/gfx/path.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/gfx/vector_icons_public.h" |
| #include "ui/resources/grit/ui_resources.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/rect_based_targeting_utils.h" |
| #include "ui/views/view_targeter.h" |
| #include "ui/views/widget/tooltip_manager.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/non_client_view.h" |
| |
| #if defined(USE_AURA) |
| #include "ui/aura/env.h" |
| #endif |
| |
| using base::UserMetricsAction; |
| |
| namespace { |
| |
| // How long the pulse throb takes. |
| const int kPulseDurationMs = 200; |
| |
| // Width of touch tabs. |
| const int kTouchWidth = 120; |
| |
| const int kExtraLeftPaddingToBalanceCloseButtonPadding = 2; |
| const int kAfterTitleSpacing = 4; |
| |
| // When a non-pinned tab becomes a pinned tab the width of the tab animates. If |
| // the width of a pinned tab is at least kPinnedTabExtraWidthToRenderAsNormal |
| // larger than the desired pinned tab width then the tab is rendered as a normal |
| // tab. This is done to avoid having the title immediately disappear when |
| // transitioning a tab from normal to pinned tab. |
| const int kPinnedTabExtraWidthToRenderAsNormal = 30; |
| |
| // How opaque to make the hover state (out of 1). |
| const double kHoverOpacity = 0.33; |
| |
| // Opacity for non-active selected tabs. |
| const double kSelectedTabOpacity = .45; |
| |
| // Selected (but not active) tabs have their throb value scaled down by this. |
| const double kSelectedTabThrobScale = .5; |
| |
| // Durations for the various parts of the pinned tab title animation. |
| const int kPinnedTitleChangeAnimationDuration1MS = 1600; |
| const int kPinnedTitleChangeAnimationStart1MS = 0; |
| const int kPinnedTitleChangeAnimationEnd1MS = 1900; |
| const int kPinnedTitleChangeAnimationDuration2MS = 0; |
| const int kPinnedTitleChangeAnimationDuration3MS = 550; |
| const int kPinnedTitleChangeAnimationStart3MS = 150; |
| const int kPinnedTitleChangeAnimationEnd3MS = 800; |
| const int kPinnedTitleChangeAnimationIntervalMS = 40; |
| |
| // Offset from the right edge for the start of the pinned title change |
| // animation. |
| const int kPinnedTitleChangeInitialXOffset = 6; |
| |
| // Max number of images to cache. This has to be at least two since rounding |
| // errors may lead to tabs in the same tabstrip having different sizes. |
| const size_t kMaxImageCacheSize = 4; |
| |
| // Height of the miniature tab strip in immersive mode. |
| const int kImmersiveTabHeight = 3; |
| |
| // Height of the small tab indicator rectangles in immersive mode. |
| const int kImmersiveBarHeight = 2; |
| |
| // Color for active and inactive tabs in the immersive mode light strip. These |
| // should be a little brighter than the color of the normal art assets for tabs, |
| // which for active tabs is 230, 230, 230 and for inactive is 184, 184, 184. |
| const SkColor kImmersiveActiveTabColor = SkColorSetRGB(235, 235, 235); |
| const SkColor kImmersiveInactiveTabColor = SkColorSetRGB(190, 190, 190); |
| |
| // The minimum opacity (out of 1) when a tab (either active or inactive) is |
| // throbbing in the immersive mode light strip. |
| const double kImmersiveTabMinThrobOpacity = 0.66; |
| |
| // Number of steps in the immersive mode loading animation. |
| const int kImmersiveLoadingStepCount = 32; |
| |
| const char kTabCloseButtonName[] = "TabCloseButton"; |
| const int kTabCloseButtonSize = 16; |
| |
| chrome::HostDesktopType GetHostDesktopType(views::View* view) { |
| // Widget is NULL when tabs are detached. |
| views::Widget* widget = view->GetWidget(); |
| return chrome::GetHostDesktopTypeForNativeView( |
| widget ? widget->GetNativeView() : NULL); |
| } |
| |
| // Stop()s |animation| and then deletes it. We do this rather than just deleting |
| // so that the delegate is notified before the destruction. |
| void StopAndDeleteAnimation(scoped_ptr<gfx::Animation> animation) { |
| if (animation) |
| animation->Stop(); |
| } |
| |
| void DrawHighlight(gfx::Canvas* canvas, |
| const SkPoint& p, |
| SkScalar radius, |
| SkAlpha alpha) { |
| const SkColor colors[2] = { SkColorSetA(SK_ColorWHITE, alpha), |
| SkColorSetA(SK_ColorWHITE, 0) }; |
| skia::RefPtr<SkShader> shader = skia::AdoptRef(SkGradientShader::CreateRadial( |
| p, radius, colors, nullptr, 2, SkShader::kClamp_TileMode)); |
| SkPaint paint; |
| paint.setStyle(SkPaint::kFill_Style); |
| paint.setAntiAlias(true); |
| paint.setShader(shader.get()); |
| canvas->sk_canvas()->drawRect( |
| SkRect::MakeXYWH(p.x() - radius, p.y() - radius, radius * 2, radius * 2), |
| paint); |
| } |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // FaviconCrashAnimation |
| // |
| // A custom animation subclass to manage the favicon crash animation. |
| class Tab::FaviconCrashAnimation : public gfx::LinearAnimation, |
| public gfx::AnimationDelegate { |
| public: |
| explicit FaviconCrashAnimation(Tab* target) |
| : gfx::LinearAnimation(1000, 25, this), |
| target_(target) { |
| } |
| ~FaviconCrashAnimation() override {} |
| |
| // gfx::Animation overrides: |
| void AnimateToState(double state) override { |
| const double kHidingOffset = |
| Tab::GetMinimumInactiveSize().height() - GetLayoutInsets(TAB).height(); |
| |
| if (state < .5) { |
| // Animate the normal icon down. |
| target_->SetFaviconHidingOffset( |
| static_cast<int>(floor(kHidingOffset * 2.0 * state))); |
| } else { |
| // Animate the crashed icon up. |
| target_->set_should_display_crashed_favicon(); |
| target_->SetFaviconHidingOffset( |
| static_cast<int>( |
| floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset)))); |
| } |
| } |
| |
| private: |
| Tab* target_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // TabCloseButton |
| // |
| // This is a Button subclass that causes middle clicks to be forwarded to the |
| // parent View by explicitly not handling them in OnMousePressed. |
| class Tab::TabCloseButton : public views::ImageButton, |
| public views::MaskedTargeterDelegate { |
| public: |
| explicit TabCloseButton(Tab* tab) |
| : views::ImageButton(tab), |
| tab_(tab) { |
| SetEventTargeter( |
| scoped_ptr<views::ViewTargeter>(new views::ViewTargeter(this))); |
| } |
| |
| ~TabCloseButton() override {} |
| |
| // views::View: |
| View* GetTooltipHandlerForPoint(const gfx::Point& point) override { |
| // Tab close button has no children, so tooltip handler should be the same |
| // as the event handler. |
| // In addition, a hit test has to be performed for the point (as |
| // GetTooltipHandlerForPoint() is responsible for it). |
| if (!HitTestPoint(point)) |
| return NULL; |
| return GetEventHandlerForPoint(point); |
| } |
| |
| bool OnMousePressed(const ui::MouseEvent& event) override { |
| tab_->controller_->OnMouseEventInTab(this, event); |
| |
| bool handled = ImageButton::OnMousePressed(event); |
| // Explicitly mark midle-mouse clicks as non-handled to ensure the tab |
| // sees them. |
| return !event.IsMiddleMouseButton() && handled; |
| } |
| |
| void OnMouseMoved(const ui::MouseEvent& event) override { |
| tab_->controller_->OnMouseEventInTab(this, event); |
| CustomButton::OnMouseMoved(event); |
| } |
| |
| void OnMouseReleased(const ui::MouseEvent& event) override { |
| tab_->controller_->OnMouseEventInTab(this, event); |
| CustomButton::OnMouseReleased(event); |
| } |
| |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| // Consume all gesture events here so that the parent (Tab) does not |
| // start consuming gestures. |
| ImageButton::OnGestureEvent(event); |
| event->SetHandled(); |
| } |
| |
| const char* GetClassName() const override { return kTabCloseButtonName; } |
| |
| private: |
| // Returns the rectangular bounds of parent tab's visible region in the |
| // local coordinate space of |this|. |
| gfx::Rect GetTabBounds() const { |
| gfx::Path tab_mask; |
| tab_->GetHitTestMask(&tab_mask); |
| |
| gfx::RectF tab_bounds_f(gfx::SkRectToRectF(tab_mask.getBounds())); |
| views::View::ConvertRectToTarget(tab_, this, &tab_bounds_f); |
| return gfx::ToEnclosingRect(tab_bounds_f); |
| } |
| |
| // Returns the rectangular bounds of the tab close button in the local |
| // coordinate space of |this|, not including clipped regions on the top |
| // or bottom of the button. |tab_bounds| is the rectangular bounds of |
| // the parent tab's visible region in the local coordinate space of |this|. |
| gfx::Rect GetTabCloseButtonBounds(const gfx::Rect& tab_bounds) const { |
| gfx::Rect button_bounds(GetContentsBounds()); |
| button_bounds.set_x(GetMirroredXForRect(button_bounds)); |
| |
| int top_overflow = tab_bounds.y() - button_bounds.y(); |
| int bottom_overflow = button_bounds.bottom() - tab_bounds.bottom(); |
| if (top_overflow > 0) |
| button_bounds.set_y(tab_bounds.y()); |
| else if (bottom_overflow > 0) |
| button_bounds.set_height(button_bounds.height() - bottom_overflow); |
| |
| return button_bounds; |
| } |
| |
| // views::ViewTargeterDelegate: |
| View* TargetForRect(View* root, const gfx::Rect& rect) override { |
| CHECK_EQ(root, this); |
| |
| if (!views::UsePointBasedTargeting(rect)) |
| return ViewTargeterDelegate::TargetForRect(root, rect); |
| |
| // Ignore the padding set on the button. |
| gfx::Rect contents_bounds = GetContentsBounds(); |
| contents_bounds.set_x(GetMirroredXForRect(contents_bounds)); |
| |
| #if defined(USE_AURA) |
| // Include the padding in hit-test for touch events. |
| if (aura::Env::GetInstance()->is_touch_down()) |
| contents_bounds = GetLocalBounds(); |
| #endif |
| |
| return contents_bounds.Intersects(rect) ? this : parent(); |
| } |
| |
| // views:MaskedTargeterDelegate: |
| bool GetHitTestMask(gfx::Path* mask) const override { |
| DCHECK(mask); |
| mask->reset(); |
| |
| // The parent tab may be partially occluded by another tab if we are |
| // in stacked tab mode, which means that the tab close button may also |
| // be partially occluded. Define the hit test mask of the tab close |
| // button to be the intersection of the parent tab's visible bounds |
| // and the bounds of the tab close button. |
| gfx::Rect tab_bounds(GetTabBounds()); |
| gfx::Rect button_bounds(GetTabCloseButtonBounds(tab_bounds)); |
| gfx::Rect intersection(gfx::IntersectRects(tab_bounds, button_bounds)); |
| |
| if (!intersection.IsEmpty()) { |
| mask->addRect(RectToSkRect(intersection)); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool DoesIntersectRect(const View* target, |
| const gfx::Rect& rect) const override { |
| CHECK_EQ(target, this); |
| |
| // If the request is not made in response to a gesture, use the |
| // default implementation. |
| if (views::UsePointBasedTargeting(rect)) |
| return MaskedTargeterDelegate::DoesIntersectRect(target, rect); |
| |
| // The hit test request is in response to a gesture. Return false if any |
| // part of the tab close button is hidden from the user. |
| // TODO(tdanderson): Consider always returning the intersection if the |
| // non-rectangular shape of the tab can be accounted for. |
| gfx::Rect tab_bounds(GetTabBounds()); |
| gfx::Rect button_bounds(GetTabCloseButtonBounds(tab_bounds)); |
| if (!tab_bounds.Contains(button_bounds)) |
| return false; |
| |
| return MaskedTargeterDelegate::DoesIntersectRect(target, rect); |
| } |
| |
| Tab* tab_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TabCloseButton); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ThrobberView |
| // |
| // A Layer-backed view for updating a waiting or loading tab throbber. |
| class Tab::ThrobberView : public views::View { |
| public: |
| explicit ThrobberView(Tab* owner); |
| |
| // Resets the times tracking when the throbber changes state. |
| void ResetStartTimes(); |
| |
| private: |
| // views::View: |
| bool CanProcessEventsWithinSubtree() const override; |
| void OnPaint(gfx::Canvas* canvas) override; |
| |
| Tab* owner_; // Weak. Owns |this|. |
| |
| // The point in time when the tab icon was first painted in the waiting state. |
| base::TimeTicks waiting_start_time_; |
| |
| // The point in time when the tab icon was first painted in the loading state. |
| base::TimeTicks loading_start_time_; |
| |
| // Paint state for the throbber after the most recent waiting paint. |
| gfx::ThrobberWaitingState waiting_state_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ThrobberView); |
| }; |
| |
| Tab::ThrobberView::ThrobberView(Tab* owner) : owner_(owner) {} |
| |
| void Tab::ThrobberView::ResetStartTimes() { |
| waiting_start_time_ = base::TimeTicks(); |
| loading_start_time_ = base::TimeTicks(); |
| waiting_state_ = gfx::ThrobberWaitingState(); |
| } |
| |
| bool Tab::ThrobberView::CanProcessEventsWithinSubtree() const { |
| return false; |
| } |
| |
| void Tab::ThrobberView::OnPaint(gfx::Canvas* canvas) { |
| const TabRendererData::NetworkState state = owner_->data().network_state; |
| if (state == TabRendererData::NETWORK_STATE_NONE) |
| return; |
| |
| const ui::ThemeProvider* tp = GetThemeProvider(); |
| const gfx::Rect bounds = GetLocalBounds(); |
| if (state == TabRendererData::NETWORK_STATE_WAITING) { |
| if (waiting_start_time_ == base::TimeTicks()) |
| waiting_start_time_ = base::TimeTicks::Now(); |
| |
| waiting_state_.elapsed_time = base::TimeTicks::Now() - waiting_start_time_; |
| gfx::PaintThrobberWaiting( |
| canvas, bounds, |
| tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_WAITING), |
| waiting_state_.elapsed_time); |
| } else { |
| if (loading_start_time_ == base::TimeTicks()) |
| loading_start_time_ = base::TimeTicks::Now(); |
| |
| waiting_state_.color = |
| tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_WAITING); |
| gfx::PaintThrobberSpinningAfterWaiting( |
| canvas, bounds, |
| tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_SPINNING), |
| base::TimeTicks::Now() - loading_start_time_, &waiting_state_); |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ImageCacheEntry |
| |
| Tab::ImageCacheEntry::ImageCacheEntry() |
| : resource_id(-1), |
| scale_factor(ui::SCALE_FACTOR_NONE) { |
| } |
| |
| Tab::ImageCacheEntry::~ImageCacheEntry() {} |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, statics: |
| |
| // static |
| const char Tab::kViewClassName[] = "Tab"; |
| Tab::TabImages Tab::active_images_ = {0}; |
| Tab::TabImages Tab::inactive_images_ = {0}; |
| Tab::TabImages Tab::mask_images_ = {0}; |
| Tab::ImageCache* Tab::image_cache_ = NULL; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, public: |
| |
| Tab::Tab(TabController* controller) |
| : controller_(controller), |
| closing_(false), |
| dragging_(false), |
| detached_(false), |
| favicon_hiding_offset_(0), |
| immersive_loading_step_(0), |
| should_display_crashed_favicon_(false), |
| throbber_(nullptr), |
| media_indicator_button_(nullptr), |
| close_button_(nullptr), |
| title_(new views::Label()), |
| tab_activated_with_last_tap_down_(false), |
| hover_controller_(this), |
| showing_icon_(false), |
| showing_media_indicator_(false), |
| showing_close_button_(false), |
| button_color_(SK_ColorTRANSPARENT) { |
| DCHECK(controller); |
| InitTabResources(); |
| |
| // So we get don't get enter/exit on children and don't prematurely stop the |
| // hover. |
| set_notify_enter_exit_on_child(true); |
| |
| set_id(VIEW_ID_TAB); |
| |
| SetBorder(views::Border::CreateEmptyBorder(GetLayoutInsets(TAB))); |
| |
| title_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD); |
| title_->SetElideBehavior(gfx::FADE_TAIL); |
| title_->SetHandlesTooltips(false); |
| title_->SetAutoColorReadabilityEnabled(false); |
| title_->SetText(CoreTabHelper::GetDefaultTitle()); |
| AddChildView(title_); |
| |
| SetEventTargeter( |
| scoped_ptr<views::ViewTargeter>(new views::ViewTargeter(this))); |
| |
| throbber_ = new ThrobberView(this); |
| throbber_->SetVisible(false); |
| AddChildView(throbber_); |
| |
| media_indicator_button_ = new MediaIndicatorButton(this); |
| AddChildView(media_indicator_button_); |
| |
| close_button_ = new TabCloseButton(this); |
| close_button_->SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE)); |
| // The normal image is set by OnButtonColorMaybeChanged() because it depends |
| // on the current theme and active state. The hovered and pressed images |
| // don't depend on the these, so we can set them here. |
| const gfx::ImageSkia& hovered = gfx::CreateVectorIcon( |
| gfx::VectorIconId::TAB_CLOSE_HOVERED_PRESSED, kTabCloseButtonSize, |
| SkColorSetARGB(0xFF, 0xDB, 0x44, 0x37)); |
| const gfx::ImageSkia& pressed = gfx::CreateVectorIcon( |
| gfx::VectorIconId::TAB_CLOSE_HOVERED_PRESSED, kTabCloseButtonSize, |
| SkColorSetARGB(0xFF, 0xA8, 0x35, 0x2A)); |
| close_button_->SetImage(views::CustomButton::STATE_HOVERED, &hovered); |
| close_button_->SetImage(views::CustomButton::STATE_PRESSED, &pressed); |
| |
| // Disable animation so that the red danger sign shows up immediately |
| // to help avoid mis-clicks. |
| close_button_->SetAnimationDuration(0); |
| AddChildView(close_button_); |
| |
| set_context_menu_controller(this); |
| } |
| |
| Tab::~Tab() { |
| } |
| |
| void Tab::SetAnimationContainer(gfx::AnimationContainer* container) { |
| animation_container_ = container; |
| hover_controller_.SetAnimationContainer(container); |
| } |
| |
| bool Tab::IsActive() const { |
| return controller_->IsActiveTab(this); |
| } |
| |
| void Tab::ActiveStateChanged() { |
| OnButtonColorMaybeChanged(); |
| media_indicator_button_->UpdateEnabledForMuteToggle(); |
| } |
| |
| bool Tab::IsSelected() const { |
| return controller_->IsTabSelected(this); |
| } |
| |
| void Tab::SetData(const TabRendererData& data) { |
| DCHECK(GetWidget()); |
| |
| if (data_.Equals(data)) |
| return; |
| |
| TabRendererData old(data_); |
| UpdateLoadingAnimation(data.network_state); |
| data_ = data; |
| |
| base::string16 title = data_.title; |
| if (title.empty()) { |
| title = data_.loading ? |
| l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) : |
| CoreTabHelper::GetDefaultTitle(); |
| } else { |
| Browser::FormatTitleForDisplay(&title); |
| } |
| title_->SetText(title); |
| |
| if (data_.IsCrashed()) { |
| if (!should_display_crashed_favicon_ && !crash_icon_animation_) { |
| data_.media_state = TAB_MEDIA_STATE_NONE; |
| bool start_crash_animation = true; |
| #if defined(OS_CHROMEOS) |
| // On Chrome OS, we reload killed tabs automatically when the user |
| // switches to them. Don't display animations for these unless they're |
| // selected (i.e. in the foreground) -- we won't reload these |
| // automatically since we don't want to get into a crash loop. |
| start_crash_animation = IsSelected() || |
| (data_.crashed_status != |
| base::TERMINATION_STATUS_PROCESS_WAS_KILLED && |
| data_.crashed_status != |
| base::TERMINATION_STATUS_PROCESS_WAS_KILLED_BY_OOM); |
| #endif |
| if (start_crash_animation) { |
| crash_icon_animation_.reset(new FaviconCrashAnimation(this)); |
| crash_icon_animation_->Start(); |
| } |
| } |
| } else { |
| if (crash_icon_animation_) |
| crash_icon_animation_.reset(); |
| should_display_crashed_favicon_ = false; |
| favicon_hiding_offset_ = 0; |
| } |
| |
| if (data_.media_state != old.media_state) |
| media_indicator_button_->TransitionToMediaState(data_.media_state); |
| |
| if (old.pinned != data_.pinned) |
| StopPinnedTabTitleAnimation(); |
| |
| DataChanged(old); |
| |
| Layout(); |
| SchedulePaint(); |
| } |
| |
| void Tab::UpdateLoadingAnimation(TabRendererData::NetworkState state) { |
| if (state == data_.network_state && |
| state == TabRendererData::NETWORK_STATE_NONE) { |
| // If the network state is none and hasn't changed, do nothing. Otherwise we |
| // need to advance the animation frame. |
| return; |
| } |
| |
| data_.network_state = state; |
| AdvanceLoadingAnimation(); |
| } |
| |
| void Tab::StartPulse() { |
| pulse_animation_.reset(new gfx::ThrobAnimation(this)); |
| pulse_animation_->SetSlideDuration(kPulseDurationMs); |
| if (animation_container_.get()) |
| pulse_animation_->SetContainer(animation_container_.get()); |
| pulse_animation_->StartThrobbing(std::numeric_limits<int>::max()); |
| } |
| |
| void Tab::StopPulse() { |
| StopAndDeleteAnimation(pulse_animation_.Pass()); |
| } |
| |
| void Tab::StartPinnedTabTitleAnimation() { |
| if (!data().pinned) |
| return; |
| if (!pinned_title_change_animation_) { |
| gfx::MultiAnimation::Parts parts; |
| parts.push_back( |
| gfx::MultiAnimation::Part(kPinnedTitleChangeAnimationDuration1MS, |
| gfx::Tween::EASE_OUT)); |
| parts.push_back( |
| gfx::MultiAnimation::Part(kPinnedTitleChangeAnimationDuration2MS, |
| gfx::Tween::ZERO)); |
| parts.push_back( |
| gfx::MultiAnimation::Part(kPinnedTitleChangeAnimationDuration3MS, |
| gfx::Tween::EASE_IN)); |
| parts[0].start_time_ms = kPinnedTitleChangeAnimationStart1MS; |
| parts[0].end_time_ms = kPinnedTitleChangeAnimationEnd1MS; |
| parts[2].start_time_ms = kPinnedTitleChangeAnimationStart3MS; |
| parts[2].end_time_ms = kPinnedTitleChangeAnimationEnd3MS; |
| base::TimeDelta timeout = base::TimeDelta::FromMilliseconds( |
| kPinnedTitleChangeAnimationIntervalMS); |
| pinned_title_change_animation_.reset( |
| new gfx::MultiAnimation(parts, timeout)); |
| if (animation_container_.get()) |
| pinned_title_change_animation_->SetContainer(animation_container_.get()); |
| pinned_title_change_animation_->set_delegate(this); |
| } |
| pinned_title_change_animation_->Start(); |
| } |
| |
| void Tab::StopPinnedTabTitleAnimation() { |
| StopAndDeleteAnimation(pinned_title_change_animation_.Pass()); |
| } |
| |
| int Tab::GetWidthOfLargestSelectableRegion() const { |
| // Assume the entire region to the left of the media indicator and/or close |
| // buttons is available for click-to-select. If neither are visible, the |
| // entire tab region is available. |
| const int indicator_left = showing_media_indicator_ ? |
| media_indicator_button_->x() : width(); |
| const int close_button_left = showing_close_button_ ? |
| close_button_->x() : width(); |
| return std::min(indicator_left, close_button_left); |
| } |
| |
| gfx::Size Tab::GetMinimumInactiveSize() { |
| // Since we use images, the real minimum height of the image is |
| // defined most accurately by the height of the end cap images. |
| InitTabResources(); |
| int height = active_images_.image_l->height(); |
| return gfx::Size(GetLayoutInsets(TAB).width(), height); |
| } |
| |
| // static |
| gfx::Size Tab::GetMinimumActiveSize() { |
| gfx::Size minimum_size = GetMinimumInactiveSize(); |
| minimum_size.Enlarge(gfx::kFaviconSize, 0); |
| return minimum_size; |
| } |
| |
| // static |
| gfx::Size Tab::GetStandardSize() { |
| gfx::Size standard_size = GetMinimumInactiveSize(); |
| const int title_spacing = GetLayoutConstant(TAB_FAVICON_TITLE_SPACING); |
| const int title_width = GetLayoutConstant(TAB_MAXIMUM_TITLE_WIDTH); |
| standard_size.Enlarge(title_spacing + title_width, 0); |
| return standard_size; |
| } |
| |
| // static |
| int Tab::GetTouchWidth() { |
| return kTouchWidth; |
| } |
| |
| // static |
| int Tab::GetPinnedWidth() { |
| return GetMinimumInactiveSize().width() + |
| GetLayoutConstant(TAB_PINNED_CONTENT_WIDTH); |
| } |
| |
| // static |
| int Tab::GetImmersiveHeight() { |
| return kImmersiveTabHeight; |
| } |
| |
| // static |
| int Tab::GetYInsetForActiveTabBackground() { |
| // The computed value here is strangely less than the height of the area atop |
| // the tab that doesn't get a background painted; otherwise, we could compute |
| // the value by simply using GetLayoutInsets(TAB).top(). My suspicion is that |
| // originally there was some sort of off-by-one error in how this background |
| // was painted, and theme authors compensated; now we're stuck perpetuating it |
| // as a result. |
| return GetLayoutConstant(TAB_TOP_EXCLUSION_HEIGHT) + 1; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, AnimationDelegate overrides: |
| |
| void Tab::AnimationProgressed(const gfx::Animation* animation) { |
| // Ignore if the pulse animation is being performed on active tab because |
| // it repaints the same image. See |Tab::PaintTabBackground()|. |
| if (animation == pulse_animation_.get() && IsActive()) |
| return; |
| SchedulePaint(); |
| } |
| |
| void Tab::AnimationCanceled(const gfx::Animation* animation) { |
| SchedulePaint(); |
| } |
| |
| void Tab::AnimationEnded(const gfx::Animation* animation) { |
| SchedulePaint(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, views::ButtonListener overrides: |
| |
| void Tab::ButtonPressed(views::Button* sender, const ui::Event& event) { |
| if (media_indicator_button_ && media_indicator_button_->visible()) { |
| if (media_indicator_button_->enabled()) |
| content::RecordAction(UserMetricsAction("CloseTab_MuteToggleAvailable")); |
| else if (data_.media_state == TAB_MEDIA_STATE_AUDIO_PLAYING) |
| content::RecordAction(UserMetricsAction("CloseTab_AudioIndicator")); |
| else |
| content::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator")); |
| } else { |
| content::RecordAction(UserMetricsAction("CloseTab_NoMediaIndicator")); |
| } |
| |
| const CloseTabSource source = |
| (event.type() == ui::ET_MOUSE_RELEASED && |
| (event.flags() & ui::EF_FROM_TOUCH) == 0) ? CLOSE_TAB_FROM_MOUSE : |
| CLOSE_TAB_FROM_TOUCH; |
| DCHECK_EQ(close_button_, sender); |
| controller_->CloseTab(this, source); |
| if (event.type() == ui::ET_GESTURE_TAP) |
| TouchUMA::RecordGestureAction(TouchUMA::GESTURE_TABCLOSE_TAP); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, views::ContextMenuController overrides: |
| |
| void Tab::ShowContextMenuForView(views::View* source, |
| const gfx::Point& point, |
| ui::MenuSourceType source_type) { |
| if (!closing()) |
| controller_->ShowContextMenuForTab(this, point, source_type); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, views::MaskedTargeterDelegate overrides: |
| |
| bool Tab::GetHitTestMask(gfx::Path* mask) const { |
| DCHECK(mask); |
| |
| // When the window is maximized we don't want to shave off the edges or top |
| // shadow of the tab, such that the user can click anywhere along the top |
| // edge of the screen to select a tab. Ditto for immersive fullscreen. |
| const views::Widget* widget = GetWidget(); |
| const bool extend_to_top = |
| widget && (widget->IsMaximized() || widget->IsFullscreen()); |
| |
| // Hit mask constants. |
| const SkScalar kTabCapWidth = 15; |
| const SkScalar kTabTopCurveWidth = 4; |
| const SkScalar kTabBottomCurveWidth = 3; |
| #if defined(OS_MACOSX) |
| // Mac's Cocoa UI doesn't have shadows. |
| const SkScalar kTabInset = 0; |
| #elif defined(TOOLKIT_VIEWS) |
| // The views browser UI has shadows in the left, right and top parts of the |
| // tab. |
| const SkScalar kTabInset = 6; |
| #endif |
| |
| SkScalar left = kTabInset; |
| SkScalar top = GetLayoutConstant(TAB_TOP_EXCLUSION_HEIGHT); |
| SkScalar right = SkIntToScalar(width()) - kTabInset; |
| SkScalar bottom = SkIntToScalar(height()); |
| |
| // Start in the lower-left corner. |
| mask->moveTo(left, bottom); |
| |
| // Left end cap. |
| mask->lineTo(left + kTabBottomCurveWidth, bottom - kTabBottomCurveWidth); |
| mask->lineTo(left + kTabCapWidth - kTabTopCurveWidth, |
| top + kTabTopCurveWidth); |
| mask->lineTo(left + kTabCapWidth, top); |
| |
| // Extend over the top shadow area if we have one and the caller wants it. |
| if (top > 0 && extend_to_top) { |
| mask->lineTo(left + kTabCapWidth, 0); |
| mask->lineTo(right - kTabCapWidth, 0); |
| } |
| |
| // Connect to the right cap. |
| mask->lineTo(right - kTabCapWidth, top); |
| |
| // Right end cap. |
| mask->lineTo(right - kTabCapWidth + kTabTopCurveWidth, |
| top + kTabTopCurveWidth); |
| mask->lineTo(right - kTabBottomCurveWidth, bottom - kTabBottomCurveWidth); |
| mask->lineTo(right, bottom); |
| |
| // Close out the path. |
| mask->lineTo(left, bottom); |
| mask->close(); |
| |
| // It is possible for a portion of the tab to be occluded if tabs are |
| // stacked, so modify the hit test mask to only include the visible |
| // region of the tab. |
| gfx::Rect clip; |
| controller_->ShouldPaintTab(this, &clip); |
| if (clip.size().GetArea()) { |
| SkRect intersection(mask->getBounds()); |
| mask->reset(); |
| if (!intersection.intersect(RectToSkRect(clip))) |
| return false; |
| mask->addRect(intersection); |
| } |
| return true; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, views::View overrides: |
| |
| void Tab::ViewHierarchyChanged(const ViewHierarchyChangedDetails& details) { |
| // If this hierarchy changed has resulted in us being part of a widget |
| // hierarchy for the first time, we can now get at the theme provider, and |
| // should recalculate the button color. |
| if (details.is_add) |
| OnButtonColorMaybeChanged(); |
| } |
| |
| void Tab::OnPaint(gfx::Canvas* canvas) { |
| // Don't paint if we're narrower than we can render correctly. (This should |
| // only happen during animations). |
| if (width() < GetMinimumInactiveSize().width() && !data().pinned) |
| return; |
| |
| gfx::Rect clip; |
| if (!controller_->ShouldPaintTab(this, &clip)) |
| return; |
| if (!clip.IsEmpty()) { |
| canvas->Save(); |
| canvas->ClipRect(clip); |
| } |
| |
| if (controller_->IsImmersiveStyle()) |
| PaintImmersiveTab(canvas); |
| else |
| PaintTab(canvas); |
| |
| if (!clip.IsEmpty()) |
| canvas->Restore(); |
| } |
| |
| void Tab::Layout() { |
| gfx::Rect lb = GetContentsBounds(); |
| if (lb.IsEmpty()) |
| return; |
| |
| showing_icon_ = ShouldShowIcon(); |
| // See comments in IconCapacity(). |
| const int extra_padding = |
| (controller_->ShouldHideCloseButtonForInactiveTabs() || |
| (IconCapacity() < 3)) ? 0 : kExtraLeftPaddingToBalanceCloseButtonPadding; |
| const int start = lb.x() + extra_padding; |
| favicon_bounds_.SetRect(start, lb.y(), 0, 0); |
| if (showing_icon_) { |
| favicon_bounds_.set_size(gfx::Size(gfx::kFaviconSize, gfx::kFaviconSize)); |
| favicon_bounds_.set_y(lb.y() + (lb.height() - gfx::kFaviconSize + 1) / 2); |
| MaybeAdjustLeftForPinnedTab(&favicon_bounds_); |
| } |
| throbber_->SetBoundsRect(favicon_bounds_); |
| |
| showing_close_button_ = ShouldShowCloseBox(); |
| if (showing_close_button_) { |
| // If the ratio of the close button size to tab width exceeds the maximum. |
| // The close button should be as large as possible so that there is a larger |
| // hit-target for touch events. So the close button bounds extends to the |
| // edges of the tab. However, the larger hit-target should be active only |
| // for mouse events, and the close-image should show up in the right place. |
| // So a border is added to the button with necessary padding. The close |
| // button (BaseTab::TabCloseButton) makes sure the padding is a hit-target |
| // only for touch events. |
| close_button_->SetBorder(views::Border::NullBorder()); |
| const gfx::Size close_button_size(close_button_->GetPreferredSize()); |
| const int top = lb.y() + (lb.height() - close_button_size.height() + 1) / 2; |
| const int left = kAfterTitleSpacing; |
| const int close_button_end = lb.right() + |
| GetLayoutConstant(TAB_CLOSE_BUTTON_TRAILING_PADDING_OVERLAP); |
| close_button_->SetPosition( |
| gfx::Point(close_button_end - close_button_size.width() - left, 0)); |
| const int bottom = height() - close_button_size.height() - top; |
| const int right = width() - close_button_end; |
| close_button_->SetBorder( |
| views::Border::CreateEmptyBorder(top, left, bottom, right)); |
| close_button_->SizeToPreferredSize(); |
| } |
| close_button_->SetVisible(showing_close_button_); |
| |
| showing_media_indicator_ = ShouldShowMediaIndicator(); |
| if (showing_media_indicator_) { |
| const gfx::Size image_size(media_indicator_button_->GetPreferredSize()); |
| const int right = showing_close_button_ ? |
| close_button_->x() + close_button_->GetInsets().left() : lb.right(); |
| gfx::Rect bounds( |
| std::max(lb.x(), right - image_size.width()), |
| lb.y() + (lb.height() - image_size.height() + 1) / 2, |
| image_size.width(), |
| image_size.height()); |
| MaybeAdjustLeftForPinnedTab(&bounds); |
| media_indicator_button_->SetBoundsRect(bounds); |
| } |
| media_indicator_button_->SetVisible(showing_media_indicator_); |
| |
| // Size the title to fill the remaining width and use all available height. |
| const bool show_title = ShouldRenderAsNormalTab(); |
| if (show_title) { |
| const int title_spacing = GetLayoutConstant(TAB_FAVICON_TITLE_SPACING); |
| int title_left = showing_icon_ ? |
| (favicon_bounds_.right() + title_spacing) : start; |
| int title_width = lb.right() - title_left; |
| if (showing_media_indicator_) { |
| title_width = |
| media_indicator_button_->x() - kAfterTitleSpacing - title_left; |
| } else if (close_button_->visible()) { |
| // Allow the title to overlay the close button's empty border padding. |
| title_width = close_button_->x() + close_button_->GetInsets().left() - |
| kAfterTitleSpacing - title_left; |
| } |
| // The Label will automatically center the font's cap height within the |
| // provided vertical space. |
| title_->SetBoundsRect( |
| gfx::Rect(title_left, lb.y(), std::max(title_width, 0), lb.height())); |
| } |
| title_->SetVisible(show_title); |
| } |
| |
| void Tab::OnThemeChanged() { |
| LoadTabImages(); |
| OnButtonColorMaybeChanged(); |
| } |
| |
| const char* Tab::GetClassName() const { |
| return kViewClassName; |
| } |
| |
| bool Tab::GetTooltipText(const gfx::Point& p, base::string16* tooltip) const { |
| // Note: Anything that affects the tooltip text should be accounted for when |
| // calling TooltipTextChanged() from Tab::DataChanged(). |
| *tooltip = chrome::AssembleTabTooltipText(data_.title, data_.media_state); |
| return !tooltip->empty(); |
| } |
| |
| bool Tab::GetTooltipTextOrigin(const gfx::Point& p, gfx::Point* origin) const { |
| origin->set_x(title_->x() + 10); |
| origin->set_y(-4); |
| return true; |
| } |
| |
| bool Tab::OnMousePressed(const ui::MouseEvent& event) { |
| controller_->OnMouseEventInTab(this, event); |
| |
| // Allow a right click from touch to drag, which corresponds to a long click. |
| if (event.IsOnlyLeftMouseButton() || |
| (event.IsOnlyRightMouseButton() && event.flags() & ui::EF_FROM_TOUCH)) { |
| ui::ListSelectionModel original_selection; |
| original_selection.Copy(controller_->GetSelectionModel()); |
| // Changing the selection may cause our bounds to change. If that happens |
| // the location of the event may no longer be valid. Create a copy of the |
| // event in the parents coordinate, which won't change, and recreate an |
| // event after changing so the coordinates are correct. |
| ui::MouseEvent event_in_parent(event, static_cast<View*>(this), parent()); |
| if (controller_->SupportsMultipleSelection()) { |
| if (event.IsShiftDown() && event.IsControlDown()) { |
| controller_->AddSelectionFromAnchorTo(this); |
| } else if (event.IsShiftDown()) { |
| controller_->ExtendSelectionTo(this); |
| } else if (event.IsControlDown()) { |
| controller_->ToggleSelected(this); |
| if (!IsSelected()) { |
| // Don't allow dragging non-selected tabs. |
| return false; |
| } |
| } else if (!IsSelected()) { |
| controller_->SelectTab(this); |
| content::RecordAction(UserMetricsAction("SwitchTab_Click")); |
| } |
| } else if (!IsSelected()) { |
| controller_->SelectTab(this); |
| content::RecordAction(UserMetricsAction("SwitchTab_Click")); |
| } |
| ui::MouseEvent cloned_event(event_in_parent, parent(), |
| static_cast<View*>(this)); |
| controller_->MaybeStartDrag(this, cloned_event, original_selection); |
| } |
| return true; |
| } |
| |
| bool Tab::OnMouseDragged(const ui::MouseEvent& event) { |
| controller_->ContinueDrag(this, event); |
| return true; |
| } |
| |
| void Tab::OnMouseReleased(const ui::MouseEvent& event) { |
| controller_->OnMouseEventInTab(this, event); |
| |
| // Notify the drag helper that we're done with any potential drag operations. |
| // Clean up the drag helper, which is re-created on the next mouse press. |
| // In some cases, ending the drag will schedule the tab for destruction; if |
| // so, bail immediately, since our members are already dead and we shouldn't |
| // do anything else except drop the tab where it is. |
| if (controller_->EndDrag(END_DRAG_COMPLETE)) |
| return; |
| |
| // Close tab on middle click, but only if the button is released over the tab |
| // (normal windows behavior is to discard presses of a UI element where the |
| // releases happen off the element). |
| if (event.IsMiddleMouseButton()) { |
| if (HitTestPoint(event.location())) { |
| controller_->CloseTab(this, CLOSE_TAB_FROM_MOUSE); |
| } else if (closing_) { |
| // We're animating closed and a middle mouse button was pushed on us but |
| // we don't contain the mouse anymore. We assume the user is clicking |
| // quicker than the animation and we should close the tab that falls under |
| // the mouse. |
| Tab* closest_tab = controller_->GetTabAt(this, event.location()); |
| if (closest_tab) |
| controller_->CloseTab(closest_tab, CLOSE_TAB_FROM_MOUSE); |
| } |
| } else if (event.IsOnlyLeftMouseButton() && !event.IsShiftDown() && |
| !event.IsControlDown()) { |
| // If the tab was already selected mouse pressed doesn't change the |
| // selection. Reset it now to handle the case where multiple tabs were |
| // selected. |
| controller_->SelectTab(this); |
| |
| if (media_indicator_button_ && media_indicator_button_->visible() && |
| media_indicator_button_->bounds().Contains(event.location())) { |
| content::RecordAction(UserMetricsAction("TabMediaIndicator_Clicked")); |
| } |
| } |
| } |
| |
| void Tab::OnMouseCaptureLost() { |
| controller_->EndDrag(END_DRAG_CAPTURE_LOST); |
| } |
| |
| void Tab::OnMouseEntered(const ui::MouseEvent& event) { |
| hover_controller_.Show(views::GlowHoverController::SUBTLE); |
| } |
| |
| void Tab::OnMouseMoved(const ui::MouseEvent& event) { |
| hover_controller_.SetLocation(event.location()); |
| controller_->OnMouseEventInTab(this, event); |
| } |
| |
| void Tab::OnMouseExited(const ui::MouseEvent& event) { |
| hover_controller_.Hide(); |
| } |
| |
| void Tab::OnGestureEvent(ui::GestureEvent* event) { |
| switch (event->type()) { |
| case ui::ET_GESTURE_TAP_DOWN: { |
| // TAP_DOWN is only dispatched for the first touch point. |
| DCHECK_EQ(1, event->details().touch_points()); |
| |
| // See comment in OnMousePressed() as to why we copy the event. |
| ui::GestureEvent event_in_parent(*event, static_cast<View*>(this), |
| parent()); |
| ui::ListSelectionModel original_selection; |
| original_selection.Copy(controller_->GetSelectionModel()); |
| tab_activated_with_last_tap_down_ = !IsActive(); |
| if (!IsSelected()) |
| controller_->SelectTab(this); |
| gfx::Point loc(event->location()); |
| views::View::ConvertPointToScreen(this, &loc); |
| ui::GestureEvent cloned_event(event_in_parent, parent(), |
| static_cast<View*>(this)); |
| controller_->MaybeStartDrag(this, cloned_event, original_selection); |
| break; |
| } |
| |
| case ui::ET_GESTURE_END: |
| controller_->EndDrag(END_DRAG_COMPLETE); |
| break; |
| |
| case ui::ET_GESTURE_SCROLL_UPDATE: |
| controller_->ContinueDrag(this, *event); |
| break; |
| |
| default: |
| break; |
| } |
| event->SetHandled(); |
| } |
| |
| void Tab::GetAccessibleState(ui::AXViewState* state) { |
| state->role = ui::AX_ROLE_TAB; |
| state->name = data_.title; |
| state->AddStateFlag(ui::AX_STATE_MULTISELECTABLE); |
| state->AddStateFlag(ui::AX_STATE_SELECTABLE); |
| controller_->UpdateTabAccessibilityState(this, state); |
| if (IsSelected()) |
| state->AddStateFlag(ui::AX_STATE_SELECTED); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, private |
| |
| void Tab::MaybeAdjustLeftForPinnedTab(gfx::Rect* bounds) const { |
| if (ShouldRenderAsNormalTab()) |
| return; |
| const int ideal_delta = width() - GetPinnedWidth(); |
| const int ideal_x = (GetPinnedWidth() - bounds->width()) / 2; |
| bounds->set_x( |
| bounds->x() + static_cast<int>( |
| (1 - static_cast<float>(ideal_delta) / |
| static_cast<float>(kPinnedTabExtraWidthToRenderAsNormal)) * |
| (ideal_x - bounds->x()))); |
| } |
| |
| void Tab::DataChanged(const TabRendererData& old) { |
| if (data().media_state != old.media_state || data().title != old.title) |
| TooltipTextChanged(); |
| |
| if (data().blocked == old.blocked) |
| return; |
| |
| if (data().blocked) |
| StartPulse(); |
| else |
| StopPulse(); |
| } |
| |
| void Tab::PaintTab(gfx::Canvas* canvas) { |
| // See if the model changes whether the icons should be painted. |
| const bool show_icon = ShouldShowIcon(); |
| const bool show_media_indicator = ShouldShowMediaIndicator(); |
| const bool show_close_button = ShouldShowCloseBox(); |
| if (show_icon != showing_icon_ || |
| show_media_indicator != showing_media_indicator_ || |
| show_close_button != showing_close_button_) |
| Layout(); |
| |
| PaintTabBackground(canvas); |
| |
| if (show_icon) |
| PaintIcon(canvas); |
| } |
| |
| void Tab::PaintImmersiveTab(gfx::Canvas* canvas) { |
| // Use transparency for the draw-attention animation. |
| int alpha = 255; |
| if (pulse_animation_ && pulse_animation_->is_animating() && !data().pinned) { |
| alpha = pulse_animation_->CurrentValueBetween( |
| 255, static_cast<int>(255 * kImmersiveTabMinThrobOpacity)); |
| } |
| |
| // Draw a gray rectangle to represent the tab. This works for pinned tabs as |
| // well as regular ones. The active tab has a brigher bar. |
| SkColor color = |
| IsActive() ? kImmersiveActiveTabColor : kImmersiveInactiveTabColor; |
| gfx::Rect bar_rect = GetImmersiveBarRect(); |
| canvas->FillRect(bar_rect, SkColorSetA(color, alpha)); |
| |
| // Paint network activity indicator. |
| // TODO(jamescook): Replace this placeholder animation with a real one. |
| // For now, let's go with a Cylon eye effect, but in blue. |
| if (data().network_state != TabRendererData::NETWORK_STATE_NONE) { |
| const SkColor kEyeColor = SkColorSetARGB(alpha, 71, 138, 217); |
| int eye_width = bar_rect.width() / 3; |
| int eye_offset = bar_rect.width() * immersive_loading_step_ / |
| kImmersiveLoadingStepCount; |
| if (eye_offset + eye_width < bar_rect.width()) { |
| // Draw a single indicator strip because it fits inside |bar_rect|. |
| gfx::Rect eye_rect( |
| bar_rect.x() + eye_offset, 0, eye_width, kImmersiveBarHeight); |
| canvas->FillRect(eye_rect, kEyeColor); |
| } else { |
| // Draw two indicators to simulate the eye "wrapping around" to the left |
| // side. The first part fills the remainder of the bar. |
| int right_eye_width = bar_rect.width() - eye_offset; |
| gfx::Rect right_eye_rect( |
| bar_rect.x() + eye_offset, 0, right_eye_width, kImmersiveBarHeight); |
| canvas->FillRect(right_eye_rect, kEyeColor); |
| // The second part parts the remaining |eye_width| on the left. |
| int left_eye_width = eye_offset + eye_width - bar_rect.width(); |
| gfx::Rect left_eye_rect( |
| bar_rect.x(), 0, left_eye_width, kImmersiveBarHeight); |
| canvas->FillRect(left_eye_rect, kEyeColor); |
| } |
| } |
| } |
| |
| void Tab::PaintTabBackground(gfx::Canvas* canvas) { |
| const int kActiveTabFillId = IDR_THEME_TOOLBAR; |
| const bool has_custom_image = |
| GetThemeProvider()->HasCustomImage(kActiveTabFillId); |
| const int y_offset = -GetYInsetForActiveTabBackground(); |
| if (IsActive()) { |
| PaintTabBackgroundUsingFillId(canvas, true, kActiveTabFillId, |
| has_custom_image, y_offset); |
| } else { |
| if (pinned_title_change_animation_ && |
| pinned_title_change_animation_->is_animating()) |
| PaintInactiveTabBackgroundWithTitleChange(canvas); |
| else |
| PaintInactiveTabBackground(canvas); |
| |
| const double throb_value = GetThrobValue(); |
| if (throb_value > 0) { |
| canvas->SaveLayerAlpha(gfx::ToRoundedInt(throb_value * 0xff), |
| GetLocalBounds()); |
| PaintTabBackgroundUsingFillId(canvas, true, kActiveTabFillId, |
| has_custom_image, y_offset); |
| canvas->Restore(); |
| } |
| } |
| } |
| |
| void Tab::PaintInactiveTabBackgroundWithTitleChange(gfx::Canvas* canvas) { |
| const int kPinnedTitleChangeGradientRadius = 20; |
| const float radius = kPinnedTitleChangeGradientRadius; |
| double x = radius; |
| int alpha = 255; |
| if (pinned_title_change_animation_->current_part_index() == 0) { |
| x = pinned_title_change_animation_->CurrentValueBetween( |
| width() + radius - kPinnedTitleChangeInitialXOffset, radius); |
| } else if (pinned_title_change_animation_->current_part_index() == 2) { |
| x = pinned_title_change_animation_->CurrentValueBetween(radius, -radius); |
| alpha = pinned_title_change_animation_->CurrentValueBetween(255, 0); |
| } |
| SkPoint p; |
| p.set(SkDoubleToScalar(x), 0); |
| gfx::Canvas background_canvas(size(), canvas->image_scale(), false); |
| PaintInactiveTabBackground(&background_canvas); |
| gfx::ImageSkia background_image(background_canvas.ExtractImageRep()); |
| canvas->DrawImageInt(background_image, 0, 0); |
| gfx::Canvas hover_canvas(size(), canvas->image_scale(), false); |
| DrawHighlight(&hover_canvas, p, SkFloatToScalar(radius), alpha); |
| gfx::ImageSkia hover_image = gfx::ImageSkiaOperations::CreateMaskedImage( |
| gfx::ImageSkia(hover_canvas.ExtractImageRep()), background_image); |
| canvas->DrawImageInt(hover_image, 0, 0); |
| } |
| |
| void Tab::PaintInactiveTabBackground(gfx::Canvas* canvas) { |
| bool has_custom_image; |
| int fill_id = controller_->GetBackgroundResourceId(&has_custom_image); |
| // Explicitly map the id so we cache correctly. |
| const chrome::HostDesktopType host_desktop_type = GetHostDesktopType(this); |
| fill_id = chrome::MapThemeImage(host_desktop_type, fill_id); |
| |
| // If the theme is providing a custom background image, then its top edge |
| // should be at the top of the tab. Otherwise, we assume that the background |
| // image is a composited foreground + frame image. Note that if the theme is |
| // only providing a custom frame image, |has_custom_image| will be true, but |
| // we should use the |background_offset_| here. |
| const int y_offset = GetThemeProvider()->HasCustomImage(fill_id) ? |
| -GetLayoutConstant(TAB_TOP_EXCLUSION_HEIGHT) : background_offset_.y(); |
| |
| // We only cache the image when it's the default image and we're not hovered, |
| // to avoid caching a background image that isn't the same for all tabs. |
| if (!has_custom_image && !hover_controller_.ShouldDraw()) { |
| ui::ScaleFactor scale_factor = |
| ui::GetSupportedScaleFactor(canvas->image_scale()); |
| gfx::ImageSkia cached_image(GetCachedImage(fill_id, size(), scale_factor)); |
| if (cached_image.width() == 0) { |
| gfx::Canvas tmp_canvas(size(), canvas->image_scale(), false); |
| PaintTabBackgroundUsingFillId(&tmp_canvas, false, fill_id, false, |
| y_offset); |
| cached_image = gfx::ImageSkia(tmp_canvas.ExtractImageRep()); |
| SetCachedImage(fill_id, scale_factor, cached_image); |
| } |
| canvas->DrawImageInt(cached_image, 0, 0); |
| } else { |
| PaintTabBackgroundUsingFillId(canvas, false, fill_id, has_custom_image, |
| y_offset); |
| } |
| } |
| |
| void Tab::PaintTabBackgroundUsingFillId(gfx::Canvas* canvas, |
| bool is_active, |
| int fill_id, |
| bool has_custom_image, |
| int y_offset) { |
| gfx::ImageSkia* fill_image = GetThemeProvider()->GetImageSkiaNamed(fill_id); |
| // The tab image needs to be lined up with the background image |
| // so that it feels partially transparent. These offsets represent the tab |
| // position within the frame background image. |
| const int x_offset = GetMirroredX() + background_offset_.x(); |
| |
| const SkScalar radius = SkFloatToScalar(width() / 3.f); |
| const bool draw_hover = |
| !is_active && hover_controller_.ShouldDraw() && radius > 0; |
| SkPoint hover_location(PointToSkPoint(hover_controller_.location())); |
| const SkAlpha hover_alpha = hover_controller_.GetAlpha(); |
| |
| if (draw_hover) { |
| // Draw everything to a temporary canvas so we can extract an image for use |
| // in masking the hover glow. |
| gfx::Canvas background_canvas(size(), canvas->image_scale(), false); |
| PaintTabFill(&background_canvas, fill_image, x_offset, y_offset, is_active); |
| gfx::ImageSkia background_image(background_canvas.ExtractImageRep()); |
| canvas->DrawImageInt(background_image, 0, 0); |
| |
| gfx::Canvas hover_canvas(size(), canvas->image_scale(), false); |
| DrawHighlight(&hover_canvas, hover_location, radius, hover_alpha); |
| gfx::ImageSkia result = gfx::ImageSkiaOperations::CreateMaskedImage( |
| gfx::ImageSkia(hover_canvas.ExtractImageRep()), background_image); |
| canvas->DrawImageInt(result, 0, 0); |
| } else { |
| PaintTabFill(canvas, fill_image, x_offset, y_offset, is_active); |
| } |
| |
| // Now draw the stroke, highlights, and shadows around the tab edge. |
| TabImages* stroke_images = is_active ? &active_images_ : &inactive_images_; |
| canvas->DrawImageInt(*stroke_images->image_l, 0, 0); |
| canvas->TileImageInt( |
| *stroke_images->image_c, stroke_images->l_width, 0, |
| width() - stroke_images->l_width - stroke_images->r_width, height()); |
| canvas->DrawImageInt(*stroke_images->image_r, |
| width() - stroke_images->r_width, 0); |
| } |
| |
| void Tab::PaintTabFill(gfx::Canvas* canvas, |
| gfx::ImageSkia* fill_image, |
| int x_offset, |
| int y_offset, |
| bool is_active) { |
| const gfx::Insets tab_insets(GetLayoutInsets(TAB)); |
| // If this isn't the foreground tab, don't draw over the toolbar, but do |
| // include the 1 px divider stroke at the bottom. |
| const int toolbar_overlap = is_active ? 0 : (tab_insets.bottom() - 1); |
| |
| // Draw left edge. |
| gfx::ImageSkia tab_l = gfx::ImageSkiaOperations::CreateTiledImage( |
| *fill_image, x_offset, y_offset, mask_images_.l_width, height()); |
| gfx::ImageSkia theme_l = |
| gfx::ImageSkiaOperations::CreateMaskedImage(tab_l, *mask_images_.image_l); |
| canvas->DrawImageInt( |
| theme_l, 0, 0, theme_l.width(), theme_l.height() - toolbar_overlap, 0, 0, |
| theme_l.width(), theme_l.height() - toolbar_overlap, false); |
| |
| // Draw right edge. |
| gfx::ImageSkia tab_r = gfx::ImageSkiaOperations::CreateTiledImage( |
| *fill_image, x_offset + width() - mask_images_.r_width, y_offset, |
| mask_images_.r_width, height()); |
| gfx::ImageSkia theme_r = |
| gfx::ImageSkiaOperations::CreateMaskedImage(tab_r, *mask_images_.image_r); |
| canvas->DrawImageInt(theme_r, 0, 0, theme_r.width(), |
| theme_r.height() - toolbar_overlap, |
| width() - theme_r.width(), 0, theme_r.width(), |
| theme_r.height() - toolbar_overlap, false); |
| |
| // Draw center. Instead of masking out the top portion we simply skip over it |
| // by incrementing by the top padding, since it's a simple rectangle. |
| canvas->TileImageInt(*fill_image, x_offset + mask_images_.l_width, |
| y_offset + tab_insets.top(), mask_images_.l_width, |
| tab_insets.top(), |
| width() - mask_images_.l_width - mask_images_.r_width, |
| height() - tab_insets.top() - toolbar_overlap); |
| } |
| |
| void Tab::PaintIcon(gfx::Canvas* canvas) { |
| gfx::Rect bounds = favicon_bounds_; |
| bounds.set_x(GetMirroredXForRect(bounds)); |
| bounds.Offset(0, favicon_hiding_offset_); |
| bounds.Intersect(GetContentsBounds()); |
| if (bounds.IsEmpty()) |
| return; |
| |
| if (data().network_state != TabRendererData::NETWORK_STATE_NONE) { |
| // Throbber will do its own painting. |
| } else { |
| const gfx::ImageSkia& favicon = should_display_crashed_favicon_ ? |
| *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_CRASH_SAD_FAVICON) : |
| data().favicon; |
| if (!favicon.isNull()) { |
| canvas->DrawImageInt(favicon, 0, 0, bounds.width(), bounds.height(), |
| bounds.x(), bounds.y(), bounds.width(), |
| bounds.height(), false); |
| } |
| } |
| } |
| |
| void Tab::AdvanceLoadingAnimation() { |
| const TabRendererData::NetworkState state = data().network_state; |
| if (controller_->IsImmersiveStyle()) { |
| if (state == TabRendererData::NETWORK_STATE_WAITING) { |
| // Waiting steps backwards. |
| immersive_loading_step_ = |
| (immersive_loading_step_ - 1 + kImmersiveLoadingStepCount) % |
| kImmersiveLoadingStepCount; |
| } else if (state == TabRendererData::NETWORK_STATE_LOADING) { |
| immersive_loading_step_ = |
| (immersive_loading_step_ + 1) % kImmersiveLoadingStepCount; |
| } else { |
| immersive_loading_step_ = 0; |
| } |
| |
| SchedulePaintInRect(GetImmersiveBarRect()); |
| return; |
| } |
| |
| if (state == TabRendererData::NETWORK_STATE_NONE) { |
| throbber_->ResetStartTimes(); |
| throbber_->SetVisible(false); |
| ScheduleIconPaint(); |
| return; |
| } |
| |
| // Since the throbber can animate for a long time, paint to a separate layer |
| // when possible to reduce repaint overhead. |
| const bool paint_to_layer = controller_->CanPaintThrobberToLayer(); |
| if (paint_to_layer != !!throbber_->layer()) { |
| throbber_->SetPaintToLayer(paint_to_layer); |
| throbber_->SetFillsBoundsOpaquely(false); |
| if (paint_to_layer) |
| ScheduleIconPaint(); // Ensure the non-layered throbber goes away. |
| } |
| if (!throbber_->visible()) { |
| ScheduleIconPaint(); // Repaint the icon area to hide the favicon. |
| throbber_->SetVisible(true); |
| } |
| throbber_->SchedulePaint(); |
| } |
| |
| int Tab::IconCapacity() const { |
| const gfx::Size min_size(GetMinimumInactiveSize()); |
| if (height() < min_size.height()) |
| return 0; |
| const int available_width = std::max(0, width() - min_size.width()); |
| // All icons are the same size as the favicon. |
| const int icon_width = gfx::kFaviconSize; |
| // We need enough space to display the icons flush against each other. |
| const int visible_icons = available_width / icon_width; |
| // When the close button will be visible on inactive tabs, we add additional |
| // padding to the left of the favicon to balance the whitespace inside the |
| // non-hovered close button image; otherwise, the tab contents look too close |
| // to the left edge. If the tab close button isn't visible on inactive tabs, |
| // we let the tab contents take the full width of the tab, to maximize visible |
| // content on tiny tabs. We base the determination on the inactive tab close |
| // button state so that when a tab is activated its contents don't suddenly |
| // shift. |
| if (visible_icons < 3) |
| return visible_icons; |
| const int padding = controller_->ShouldHideCloseButtonForInactiveTabs() ? |
| 0 : kExtraLeftPaddingToBalanceCloseButtonPadding; |
| return (available_width - padding) / icon_width; |
| } |
| |
| bool Tab::ShouldShowIcon() const { |
| return chrome::ShouldTabShowFavicon( |
| IconCapacity(), data().pinned, IsActive(), data().show_icon, |
| media_indicator_button_ ? media_indicator_button_->showing_media_state() : |
| data_.media_state); |
| } |
| |
| bool Tab::ShouldShowMediaIndicator() const { |
| return chrome::ShouldTabShowMediaIndicator( |
| IconCapacity(), data().pinned, IsActive(), data().show_icon, |
| media_indicator_button_ ? media_indicator_button_->showing_media_state() : |
| data_.media_state); |
| } |
| |
| bool Tab::ShouldShowCloseBox() const { |
| if (!IsActive() && controller_->ShouldHideCloseButtonForInactiveTabs()) |
| return false; |
| |
| return chrome::ShouldTabShowCloseButton( |
| IconCapacity(), data().pinned, IsActive()); |
| } |
| |
| bool Tab::ShouldRenderAsNormalTab() const { |
| return !data().pinned || |
| (width() >= (GetPinnedWidth() + kPinnedTabExtraWidthToRenderAsNormal)); |
| } |
| |
| double Tab::GetThrobValue() { |
| const bool is_selected = IsSelected(); |
| const double min = is_selected ? kSelectedTabOpacity : 0; |
| const double scale = is_selected ? kSelectedTabThrobScale : 1; |
| |
| // Showing both the pulse and title change animation at the same time is too |
| // much. |
| if (pulse_animation_ && pulse_animation_->is_animating() && |
| (!pinned_title_change_animation_ || |
| !pinned_title_change_animation_->is_animating())) { |
| return pulse_animation_->GetCurrentValue() * kHoverOpacity * scale + min; |
| } |
| |
| if (hover_controller_.ShouldDraw()) { |
| return kHoverOpacity * hover_controller_.GetAnimationValue() * scale + |
| min; |
| } |
| |
| return is_selected ? kSelectedTabOpacity : 0; |
| } |
| |
| void Tab::SetFaviconHidingOffset(int offset) { |
| favicon_hiding_offset_ = offset; |
| ScheduleIconPaint(); |
| } |
| |
| void Tab::OnButtonColorMaybeChanged() { |
| // The theme provider may be null if we're not currently in a widget |
| // hierarchy. |
| const ui::ThemeProvider* theme_provider = GetThemeProvider(); |
| if (!theme_provider) |
| return; |
| |
| const SkColor title_color = theme_provider->GetColor(IsActive() ? |
| ThemeProperties::COLOR_TAB_TEXT : |
| ThemeProperties::COLOR_BACKGROUND_TAB_TEXT); |
| const SkColor new_button_color = SkColorSetA(title_color, 0xA0); |
| if (button_color_ != new_button_color) { |
| button_color_ = new_button_color; |
| title_->SetEnabledColor(title_color); |
| media_indicator_button_->OnParentTabButtonColorChanged(); |
| const gfx::ImageSkia& close_button_normal_image = gfx::CreateVectorIcon( |
| gfx::VectorIconId::TAB_CLOSE_NORMAL, kTabCloseButtonSize, |
| button_color_); |
| close_button_->SetImage(views::CustomButton::STATE_NORMAL, |
| &close_button_normal_image); |
| } |
| } |
| |
| void Tab::ScheduleIconPaint() { |
| gfx::Rect bounds = favicon_bounds_; |
| if (bounds.IsEmpty()) |
| return; |
| |
| // Extends the area to the bottom when the crash animation is in progress. |
| if (crash_icon_animation_) |
| bounds.set_height(height() - bounds.y()); |
| bounds.set_x(GetMirroredXForRect(bounds)); |
| SchedulePaintInRect(bounds); |
| } |
| |
| gfx::Rect Tab::GetImmersiveBarRect() const { |
| // The main bar is as wide as the normal tab's horizontal top line. |
| // This top line of the tab extends a few pixels left and right of the |
| // center image due to pixels in the rounded corner images. |
| const int kBarPadding = 1; |
| int main_bar_left = active_images_.l_width - kBarPadding; |
| int main_bar_right = width() - active_images_.r_width + kBarPadding; |
| return gfx::Rect( |
| main_bar_left, 0, main_bar_right - main_bar_left, kImmersiveBarHeight); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, private static: |
| |
| // static |
| void Tab::InitTabResources() { |
| static bool initialized = false; |
| if (initialized) |
| return; |
| |
| initialized = true; |
| image_cache_ = new ImageCache(); |
| |
| // Load the tab images once now, and maybe again later if the theme changes. |
| LoadTabImages(); |
| } |
| |
| // static |
| void Tab::LoadTabImages() { |
| // We're not letting people override tab images just yet. |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| |
| active_images_.image_l = rb.GetImageSkiaNamed(IDR_TAB_ACTIVE_LEFT); |
| active_images_.image_c = rb.GetImageSkiaNamed(IDR_TAB_ACTIVE_CENTER); |
| active_images_.image_r = rb.GetImageSkiaNamed(IDR_TAB_ACTIVE_RIGHT); |
| active_images_.l_width = active_images_.image_l->width(); |
| active_images_.r_width = active_images_.image_r->width(); |
| |
| inactive_images_.image_l = rb.GetImageSkiaNamed(IDR_TAB_INACTIVE_LEFT); |
| inactive_images_.image_c = rb.GetImageSkiaNamed(IDR_TAB_INACTIVE_CENTER); |
| inactive_images_.image_r = rb.GetImageSkiaNamed(IDR_TAB_INACTIVE_RIGHT); |
| inactive_images_.l_width = inactive_images_.image_l->width(); |
| inactive_images_.r_width = inactive_images_.image_r->width(); |
| |
| mask_images_.image_l = rb.GetImageSkiaNamed(IDR_TAB_ALPHA_LEFT); |
| mask_images_.image_r = rb.GetImageSkiaNamed(IDR_TAB_ALPHA_RIGHT); |
| mask_images_.l_width = mask_images_.image_l->width(); |
| mask_images_.r_width = mask_images_.image_r->width(); |
| } |
| |
| // static |
| gfx::ImageSkia Tab::GetCachedImage(int resource_id, |
| const gfx::Size& size, |
| ui::ScaleFactor scale_factor) { |
| for (ImageCache::const_iterator i = image_cache_->begin(); |
| i != image_cache_->end(); ++i) { |
| if (i->resource_id == resource_id && i->scale_factor == scale_factor && |
| i->image.size() == size) { |
| return i->image; |
| } |
| } |
| return gfx::ImageSkia(); |
| } |
| |
| // static |
| void Tab::SetCachedImage(int resource_id, |
| ui::ScaleFactor scale_factor, |
| const gfx::ImageSkia& image) { |
| DCHECK_NE(scale_factor, ui::SCALE_FACTOR_NONE); |
| ImageCacheEntry entry; |
| entry.resource_id = resource_id; |
| entry.scale_factor = scale_factor; |
| entry.image = image; |
| image_cache_->push_front(entry); |
| if (image_cache_->size() > kMaxImageCacheSize) |
| image_cache_->pop_back(); |
| } |