Chromium Code Reviews| Index: chrome/browser/ui/views/tabs/tab.cc |
| diff --git a/chrome/browser/ui/views/tabs/tab.cc b/chrome/browser/ui/views/tabs/tab.cc |
| index 441cb72677e4e493cced578cc9a8298f4c5ef68e..ee13dd30b081252b9520d4895c4c654559e94b81 100644 |
| --- a/chrome/browser/ui/views/tabs/tab.cc |
| +++ b/chrome/browser/ui/views/tabs/tab.cc |
| @@ -21,6 +21,7 @@ |
| #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/theme_resources.h" |
| #include "third_party/skia/include/effects/SkGradientShader.h" |
| #include "ui/accessibility/ax_view_state.h" |
| @@ -49,6 +50,8 @@ |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/non_client_view.h" |
| +using base::UserMetricsAction; |
| + |
| namespace { |
| // Padding around the "content" of a tab, occupied by the tab border graphics. |
| @@ -132,6 +135,7 @@ const double kImmersiveTabMinThrobOpacity = 0.66; |
| const int kImmersiveLoadingStepCount = 32; |
| const char kTabCloseButtonName[] = "TabCloseButton"; |
| +const char kMediaIndicatorButtonName[] = "MediaIndicatorButton"; |
| void DrawIconAtLocation(gfx::Canvas* canvas, |
| const gfx::ImageSkia& image, |
| @@ -378,6 +382,162 @@ class Tab::TabCloseButton : public views::ImageButton, |
| }; |
| //////////////////////////////////////////////////////////////////////////////// |
| +// MediaIndicatorButton |
| +// |
| +// This is a Button subclass that serves as both the media indicator icon |
| +// (audio, tab capture, etc.), and as a mute button. When the indicator is |
| +// transitioned to the audio playing or muting state, the button functionality |
| +// is enabled and begins handling mouse events. Otherwise, this view behaves |
| +// like an image and all mouse events will be handled by the Tab (its parent |
| +// View). |
| +class Tab::MediaIndicatorButton : public views::ImageButton, |
|
sky
2014/09/23 22:58:18
Move this into its own .h/.cc.
miu
2014/09/24 22:34:16
Done.
|
| + public views::ViewTargeterDelegate { |
| + public: |
| + explicit MediaIndicatorButton(Tab* tab) |
|
sky
2014/09/23 22:58:18
You don't seem to use tab at all here.
miu
2014/09/24 22:34:16
Done. Yep, I can downcast from View::parent() whe
|
| + : views::ImageButton(NULL), |
| + media_state_(TAB_MEDIA_STATE_NONE), |
| + showing_media_state_(TAB_MEDIA_STATE_NONE) { |
| + SetEventTargeter( |
| + scoped_ptr<views::ViewTargeter>(new views::ViewTargeter(this))); |
| + } |
| + |
| + virtual ~MediaIndicatorButton() {} |
| + |
| + // Returns the current TabMediaState except, while the indicator image is |
| + // fading out, returns the prior TabMediaState. |
| + TabMediaState showing_media_state() const { |
| + return showing_media_state_; |
| + } |
| + |
| + // Updates ImageButton images, starts fade animations, and |
| + // activates/deactivates button functionality as appropriate. |
| + void TransitionToMediaState(TabMediaState next_state) { |
| + if (next_state == media_state_) |
| + return; |
| + |
| + if (next_state != TAB_MEDIA_STATE_NONE) { |
| + const gfx::ImageSkia* const indicator_image = |
| + chrome::GetTabMediaIndicatorImage(next_state).ToImageSkia(); |
| + SetImage(views::CustomButton::STATE_NORMAL, indicator_image); |
| + SetImage(views::CustomButton::STATE_DISABLED, indicator_image); |
| + const gfx::ImageSkia* const affordance_image = |
| + chrome::GetTabMediaIndicatorAffordanceImage(next_state).ToImageSkia(); |
| + SetImage(views::CustomButton::STATE_HOVERED, affordance_image); |
| + SetImage(views::CustomButton::STATE_PRESSED, affordance_image); |
| + } |
| + |
| + if ((media_state_ == TAB_MEDIA_STATE_AUDIO_PLAYING && |
| + next_state == TAB_MEDIA_STATE_AUDIO_MUTING) || |
| + (media_state_ == TAB_MEDIA_STATE_AUDIO_MUTING && |
| + next_state == TAB_MEDIA_STATE_AUDIO_PLAYING) || |
| + (media_state_ == TAB_MEDIA_STATE_AUDIO_MUTING && |
| + next_state == TAB_MEDIA_STATE_NONE)) { |
| + // Instant user feedback: No fade animation. |
| + showing_media_state_ = next_state; |
| + media_indicator_animation_.reset(); |
| + } else { |
| + if (next_state == TAB_MEDIA_STATE_NONE) |
| + showing_media_state_ = media_state_; // Fading-out indicator. |
| + else |
| + showing_media_state_ = next_state; // Fading-in to next indicator. |
| + media_indicator_animation_ = |
| + chrome::CreateTabMediaIndicatorFadeAnimation(next_state); |
| + media_indicator_animation_->set_delegate(this); |
| + media_indicator_animation_->Start(); |
| + } |
| + |
| + SetEnabled(chrome::IsTabAudioMutingFeatureEnabled() && |
| + (next_state == TAB_MEDIA_STATE_AUDIO_PLAYING || |
| + next_state == TAB_MEDIA_STATE_AUDIO_MUTING)); |
| + |
| + // An indicator state change should be made visible immediately, instead of |
| + // the user being surprised when their mouse leaves the button. |
| + if (state() == views::CustomButton::STATE_HOVERED) { |
| + SetState(enabled() ? views::CustomButton::STATE_NORMAL : |
| + views::CustomButton::STATE_DISABLED); |
| + } |
| + |
| + media_state_ = next_state; |
| + } |
| + |
| + protected: |
| + // views::View: |
| + virtual const char* GetClassName() const OVERRIDE { |
| + return kMediaIndicatorButtonName; |
| + } |
| + |
| + virtual View* GetTooltipHandlerForPoint(const gfx::Point& point) OVERRIDE { |
| + return NULL; // Tab (the parent View) provides the tooltip. |
| + } |
| + |
| + virtual bool OnMouseDragged(const ui::MouseEvent& event) OVERRIDE { |
| + const ButtonState previous_state = state(); |
| + const bool ret = ImageButton::OnMouseDragged(event); |
| + if (previous_state != views::CustomButton::STATE_NORMAL && |
| + state() == views::CustomButton::STATE_NORMAL) |
| + content::RecordAction(UserMetricsAction("MediaIndicatorButton_Dragged")); |
| + return ret; |
| + } |
| + |
| + virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { |
| + double opaqueness = media_indicator_animation_ ? |
| + media_indicator_animation_->GetCurrentValue() : 1.0; |
| + if (media_state_ == TAB_MEDIA_STATE_NONE) |
| + opaqueness = 1.0 - opaqueness; // Fading out, not in. |
|
sky
2014/09/23 22:58:18
Can't you make the animation return the right valu
miu
2014/09/24 22:34:16
I think it goes against the intentions of gfx::Ani
sky
2014/09/25 19:25:04
Well, it depends upon the animation. In theory you
|
| + if (opaqueness < 1.0) |
| + canvas->SaveLayerAlpha(opaqueness * SK_AlphaOPAQUE); |
| + ImageButton::OnPaint(canvas); |
| + if (opaqueness < 1.0) |
| + canvas->Restore(); |
| + } |
| + |
| + // views::ViewTargeterDelegate |
| + virtual bool DoesIntersectRect(const View* target, |
| + const gfx::Rect& rect) const OVERRIDE { |
| + // If this button is not enabled, Tab (the parent View) handles all mouse |
| + // events. |
| + return enabled() && |
| + views::ViewTargeterDelegate::DoesIntersectRect(target, rect); |
| + } |
| + |
| + // views::Button: |
| + virtual void NotifyClick(const ui::Event& event) OVERRIDE { |
|
sky
2014/09/23 22:58:17
Is it going to be annoying that the behavior of th
miu
2014/09/24 22:34:16
(I assume you're referring to the code/comment at
sky
2014/09/25 19:25:04
Just in general because the state may change at an
|
| + if (media_state_ == TAB_MEDIA_STATE_AUDIO_PLAYING) |
| + content::RecordAction(UserMetricsAction("MuteTab")); |
| + else if (media_state_ == TAB_MEDIA_STATE_AUDIO_MUTING) |
| + content::RecordAction(UserMetricsAction("UnmuteTab")); |
| + else |
| + NOTREACHED(); |
| + |
| + if (Tab* const tab = static_cast<Tab*>(parent())) |
|
sky
2014/09/23 22:58:18
Can the parent really be NULL? I would expect this
miu
2014/09/24 22:34:16
Done.
|
| + tab->controller_->ToggleTabAudioMute(tab); |
| + } |
| + |
| + // gfx::AnimationDelegate |
| + virtual void AnimationCanceled(const gfx::Animation* animation) OVERRIDE { |
|
sky
2014/09/23 22:58:18
Having ImageButton be the delegate for two animati
miu
2014/09/24 22:34:16
Done.
|
| + showing_media_state_ = media_state_; |
| + ImageButton::AnimationCanceled(animation); |
| + SchedulePaint(); |
| + } |
| + |
| + virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE { |
| + showing_media_state_ = media_state_; |
| + ImageButton::AnimationEnded(animation); |
| + SchedulePaint(); |
| + } |
| + |
| + private: |
| + TabMediaState media_state_; |
| + |
| + // Media indicator fade-in/out animation (i.e., only on show/hide, not a |
| + // continuous animation). |
| + scoped_ptr<gfx::Animation> media_indicator_animation_; |
| + TabMediaState showing_media_state_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(MediaIndicatorButton); |
| +}; |
| + |
| +//////////////////////////////////////////////////////////////////////////////// |
| // ImageCacheEntry |
| Tab::ImageCacheEntry::ImageCacheEntry() |
| @@ -409,8 +569,8 @@ Tab::Tab(TabController* controller) |
| loading_animation_frame_(0), |
| immersive_loading_step_(0), |
| should_display_crashed_favicon_(false), |
| - animating_media_state_(TAB_MEDIA_STATE_NONE), |
| close_button_(NULL), |
| + media_indicator_button_(NULL), |
| title_(new views::Label()), |
| tab_activated_with_last_tap_down_(false), |
| hover_controller_(this), |
| @@ -509,11 +669,8 @@ void Tab::SetData(const TabRendererData& data) { |
| ResetCrashedFavicon(); |
| } |
| - if (data_.media_state != old.media_state) { |
| - if (data_.media_state != TAB_MEDIA_STATE_NONE) |
| - animating_media_state_ = data_.media_state; |
| - StartMediaIndicatorAnimation(); |
| - } |
| + if (data_.media_state != old.media_state) |
| + LazyGetMediaIndicatorButton()->TransitionToMediaState(data_.media_state); |
| if (old.mini != data_.mini) { |
| StopAndDeleteAnimation( |
| @@ -642,14 +799,10 @@ void Tab::AnimationProgressed(const gfx::Animation* animation) { |
| } |
| void Tab::AnimationCanceled(const gfx::Animation* animation) { |
| - if (media_indicator_animation_ == animation) |
| - animating_media_state_ = data_.media_state; |
| SchedulePaint(); |
| } |
| void Tab::AnimationEnded(const gfx::Animation* animation) { |
| - if (media_indicator_animation_ == animation) |
| - animating_media_state_ = data_.media_state; |
| SchedulePaint(); |
| } |
| @@ -657,6 +810,17 @@ void Tab::AnimationEnded(const gfx::Animation* animation) { |
| // 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()) |
|
Mark P
2014/09/23 18:56:15
Are you planning to do sequence analysis? If not,
|
| + 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_CaptureIndicator")); |
|
Mark P
2014/09/23 19:49:56
Can you please give this a better name?
miu
2014/09/24 22:34:16
Done. Named it RecordingIndicator, per offline IM
|
| + } 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 : |
| @@ -770,19 +934,21 @@ void Tab::Layout() { |
| close_button_->SetVisible(showing_close_button_); |
| showing_media_indicator_ = ShouldShowMediaIndicator(); |
| - media_indicator_bounds_.SetRect(lb.x(), lb.y(), 0, 0); |
| if (showing_media_indicator_) { |
| - const gfx::Image& media_indicator_image = |
| - chrome::GetTabMediaIndicatorImage(animating_media_state_); |
| - media_indicator_bounds_.set_width(media_indicator_image.Width()); |
| - media_indicator_bounds_.set_height(media_indicator_image.Height()); |
| - media_indicator_bounds_.set_y( |
| - lb.y() + (lb.height() - media_indicator_bounds_.height() + 1) / 2); |
| + views::ImageButton* const button = LazyGetMediaIndicatorButton(); |
| + const gfx::Size image_size(button->GetPreferredSize()); |
| const int right = showing_close_button_ ? |
| close_button_->x() + close_button_->GetInsets().left() : lb.right(); |
| - media_indicator_bounds_.set_x( |
| - std::max(lb.x(), right - media_indicator_bounds_.width())); |
| - MaybeAdjustLeftForMiniTab(&media_indicator_bounds_); |
| + 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()); |
| + MaybeAdjustLeftForMiniTab(&bounds); |
| + button->SetBoundsRect(bounds); |
|
sky
2014/09/23 22:58:18
Did you make sure this does the right thing when r
miu
2014/09/24 22:34:15
Good thing you had me check. When I first impleme
|
| + button->SetVisible(true); |
| + } else if (media_indicator_button_) { |
| + media_indicator_button_->SetVisible(false); |
| } |
| // Size the title to fill the remaining width and use all available height. |
| @@ -791,7 +957,7 @@ void Tab::Layout() { |
| int title_left = favicon_bounds_.right() + kFaviconTitleSpacing; |
| int title_width = lb.width() - title_left; |
| if (showing_media_indicator_) { |
| - title_width = media_indicator_bounds_.x() - kViewSpacing - title_left; |
| + title_width = media_indicator_button_->x() - kViewSpacing - 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() - |
| @@ -904,6 +1070,11 @@ void Tab::OnMouseReleased(const ui::MouseEvent& event) { |
| // 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")); |
| + } |
| } |
| } |
| @@ -1020,9 +1191,6 @@ void Tab::PaintTab(gfx::Canvas* canvas) { |
| if (show_icon) |
| PaintIcon(canvas); |
| - if (show_media_indicator) |
| - PaintMediaIndicator(canvas); |
| - |
| // If the close button color has changed, generate a new one. |
| if (!close_button_color_ || title_color != close_button_color_) { |
| close_button_color_ = title_color; |
| @@ -1341,28 +1509,6 @@ void Tab::PaintIcon(gfx::Canvas* canvas) { |
| } |
| } |
| -void Tab::PaintMediaIndicator(gfx::Canvas* canvas) { |
| - if (media_indicator_bounds_.IsEmpty() || !media_indicator_animation_) |
| - return; |
| - |
| - gfx::Rect bounds = media_indicator_bounds_; |
| - bounds.set_x(GetMirroredXForRect(bounds)); |
| - |
| - SkPaint paint; |
| - paint.setAntiAlias(true); |
| - double opaqueness = media_indicator_animation_->GetCurrentValue(); |
| - if (data_.media_state == TAB_MEDIA_STATE_NONE) |
| - opaqueness = 1.0 - opaqueness; // Fading out, not in. |
| - paint.setAlpha(opaqueness * SK_AlphaOPAQUE); |
| - |
| - const gfx::ImageSkia& media_indicator_image = |
| - *(chrome::GetTabMediaIndicatorImage(animating_media_state_). |
| - ToImageSkia()); |
| - DrawIconAtLocation(canvas, media_indicator_image, 0, |
| - bounds.x(), bounds.y(), media_indicator_image.width(), |
| - media_indicator_image.height(), true, paint); |
| -} |
| - |
| void Tab::AdvanceLoadingAnimation(TabRendererData::NetworkState old_state, |
| TabRendererData::NetworkState state) { |
| static bool initialized = false; |
| @@ -1439,13 +1585,15 @@ int Tab::IconCapacity() const { |
| bool Tab::ShouldShowIcon() const { |
| return chrome::ShouldTabShowFavicon( |
| IconCapacity(), data().mini, IsActive(), data().show_icon, |
| - animating_media_state_); |
| + media_indicator_button_ ? media_indicator_button_->showing_media_state() : |
| + data_.media_state); |
| } |
| bool Tab::ShouldShowMediaIndicator() const { |
| return chrome::ShouldTabShowMediaIndicator( |
| IconCapacity(), data().mini, IsActive(), data().show_icon, |
| - animating_media_state_); |
| + media_indicator_button_ ? media_indicator_button_->showing_media_state() : |
| + data_.media_state); |
| } |
| bool Tab::ShouldShowCloseBox() const { |
| @@ -1500,13 +1648,6 @@ bool Tab::IsPerformingCrashAnimation() const { |
| return crash_icon_animation_.get() && data_.IsCrashed(); |
| } |
| -void Tab::StartMediaIndicatorAnimation() { |
| - media_indicator_animation_ = |
| - chrome::CreateTabMediaIndicatorFadeAnimation(data_.media_state); |
| - media_indicator_animation_->set_delegate(this); |
| - media_indicator_animation_->Start(); |
| -} |
| - |
| void Tab::ScheduleIconPaint() { |
| gfx::Rect bounds = favicon_bounds_; |
| if (bounds.IsEmpty()) |
| @@ -1546,6 +1687,14 @@ void Tab::GetTabIdAndFrameId(views::Widget* widget, |
| } |
| } |
| +Tab::MediaIndicatorButton* Tab::LazyGetMediaIndicatorButton() { |
| + if (!media_indicator_button_) { |
| + media_indicator_button_ = new MediaIndicatorButton(this); |
| + AddChildView(media_indicator_button_); // Takes ownership. |
| + } |
| + return media_indicator_button_; |
| +} |
| + |
| //////////////////////////////////////////////////////////////////////////////// |
| // Tab, private static: |