| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 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/media_indicator_button.h" | |
| 6 | |
| 7 #include "base/macros.h" | |
| 8 #include "chrome/browser/ui/views/tabs/tab.h" | |
| 9 #include "chrome/browser/ui/views/tabs/tab_controller.h" | |
| 10 #include "chrome/browser/ui/views/tabs/tab_renderer_data.h" | |
| 11 #include "content/public/browser/user_metrics.h" | |
| 12 #include "ui/gfx/animation/animation_delegate.h" | |
| 13 #include "ui/gfx/canvas.h" | |
| 14 #include "ui/gfx/image/image.h" | |
| 15 | |
| 16 using base::UserMetricsAction; | |
| 17 | |
| 18 namespace { | |
| 19 | |
| 20 // The minimum required click-to-select area of an inactive Tab before allowing | |
| 21 // the click-to-mute functionality to be enabled. These values are in terms of | |
| 22 // some percentage of the MediaIndicatorButton's width. See comments in | |
| 23 // UpdateEnabledForMuteToggle(). | |
| 24 const int kMinMouseSelectableAreaPercent = 250; | |
| 25 const int kMinGestureSelectableAreaPercent = 400; | |
| 26 | |
| 27 // Returns true if either Shift or Control are being held down. In this case, | |
| 28 // mouse events are delegated to the Tab, to perform tab selection in the tab | |
| 29 // strip instead. | |
| 30 bool IsShiftOrControlDown(const ui::Event& event) { | |
| 31 return (event.flags() & (ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN)) != 0; | |
| 32 } | |
| 33 | |
| 34 } // namespace | |
| 35 | |
| 36 const char MediaIndicatorButton::kViewClassName[] = "MediaIndicatorButton"; | |
| 37 | |
| 38 class MediaIndicatorButton::FadeAnimationDelegate | |
| 39 : public gfx::AnimationDelegate { | |
| 40 public: | |
| 41 explicit FadeAnimationDelegate(MediaIndicatorButton* button) | |
| 42 : button_(button) {} | |
| 43 ~FadeAnimationDelegate() override {} | |
| 44 | |
| 45 private: | |
| 46 // gfx::AnimationDelegate | |
| 47 void AnimationProgressed(const gfx::Animation* animation) override { | |
| 48 button_->SchedulePaint(); | |
| 49 } | |
| 50 | |
| 51 void AnimationCanceled(const gfx::Animation* animation) override { | |
| 52 AnimationEnded(animation); | |
| 53 } | |
| 54 | |
| 55 void AnimationEnded(const gfx::Animation* animation) override { | |
| 56 button_->showing_media_state_ = button_->media_state_; | |
| 57 button_->parent_tab_->MediaStateChanged(); | |
| 58 } | |
| 59 | |
| 60 MediaIndicatorButton* const button_; | |
| 61 | |
| 62 DISALLOW_COPY_AND_ASSIGN(FadeAnimationDelegate); | |
| 63 }; | |
| 64 | |
| 65 MediaIndicatorButton::MediaIndicatorButton(Tab* parent_tab) | |
| 66 : views::ImageButton(NULL), | |
| 67 parent_tab_(parent_tab), | |
| 68 media_state_(TAB_MEDIA_STATE_NONE), | |
| 69 showing_media_state_(TAB_MEDIA_STATE_NONE) { | |
| 70 DCHECK(parent_tab_); | |
| 71 SetEventTargeter( | |
| 72 scoped_ptr<views::ViewTargeter>(new views::ViewTargeter(this))); | |
| 73 } | |
| 74 | |
| 75 MediaIndicatorButton::~MediaIndicatorButton() {} | |
| 76 | |
| 77 void MediaIndicatorButton::TransitionToMediaState(TabMediaState next_state) { | |
| 78 if (next_state == media_state_) | |
| 79 return; | |
| 80 | |
| 81 TabMediaState previous_media_showing_state = showing_media_state_; | |
| 82 | |
| 83 if (next_state != TAB_MEDIA_STATE_NONE) | |
| 84 ResetImages(next_state); | |
| 85 | |
| 86 if ((media_state_ == TAB_MEDIA_STATE_AUDIO_PLAYING && | |
| 87 next_state == TAB_MEDIA_STATE_AUDIO_MUTING) || | |
| 88 (media_state_ == TAB_MEDIA_STATE_AUDIO_MUTING && | |
| 89 next_state == TAB_MEDIA_STATE_AUDIO_PLAYING) || | |
| 90 (media_state_ == TAB_MEDIA_STATE_AUDIO_MUTING && | |
| 91 next_state == TAB_MEDIA_STATE_NONE)) { | |
| 92 // Instant user feedback: No fade animation. | |
| 93 showing_media_state_ = next_state; | |
| 94 fade_animation_.reset(); | |
| 95 } else { | |
| 96 if (next_state == TAB_MEDIA_STATE_NONE) | |
| 97 showing_media_state_ = media_state_; // Fading-out indicator. | |
| 98 else | |
| 99 showing_media_state_ = next_state; // Fading-in to next indicator. | |
| 100 fade_animation_ = chrome::CreateTabMediaIndicatorFadeAnimation(next_state); | |
| 101 if (!fade_animation_delegate_) | |
| 102 fade_animation_delegate_.reset(new FadeAnimationDelegate(this)); | |
| 103 fade_animation_->set_delegate(fade_animation_delegate_.get()); | |
| 104 fade_animation_->Start(); | |
| 105 } | |
| 106 | |
| 107 media_state_ = next_state; | |
| 108 | |
| 109 if (previous_media_showing_state != showing_media_state_) | |
| 110 parent_tab_->MediaStateChanged(); | |
| 111 | |
| 112 UpdateEnabledForMuteToggle(); | |
| 113 | |
| 114 // An indicator state change should be made visible immediately, instead of | |
| 115 // the user being surprised when their mouse leaves the button. | |
| 116 if (state() == views::CustomButton::STATE_HOVERED) { | |
| 117 SetState(enabled() ? views::CustomButton::STATE_NORMAL : | |
| 118 views::CustomButton::STATE_DISABLED); | |
| 119 } | |
| 120 | |
| 121 // Note: The calls to SetImage(), SetEnabled(), and SetState() above will call | |
| 122 // SchedulePaint() if necessary. | |
| 123 } | |
| 124 | |
| 125 void MediaIndicatorButton::UpdateEnabledForMuteToggle() { | |
| 126 bool enable = chrome::AreExperimentalMuteControlsEnabled() && | |
| 127 (media_state_ == TAB_MEDIA_STATE_AUDIO_PLAYING || | |
| 128 media_state_ == TAB_MEDIA_STATE_AUDIO_MUTING); | |
| 129 | |
| 130 // If the tab is not the currently-active tab, make sure it is wide enough | |
| 131 // before enabling click-to-mute. This ensures that there is enough click | |
| 132 // area for the user to activate a tab rather than unintentionally muting it. | |
| 133 // Note that IsTriggerableEvent() is also overridden to provide an even wider | |
| 134 // requirement for tap gestures. | |
| 135 if (enable && !GetTab()->IsActive()) { | |
| 136 const int required_width = width() * kMinMouseSelectableAreaPercent / 100; | |
| 137 enable = (GetTab()->GetWidthOfLargestSelectableRegion() >= required_width); | |
| 138 } | |
| 139 | |
| 140 SetEnabled(enable); | |
| 141 } | |
| 142 | |
| 143 void MediaIndicatorButton::OnParentTabButtonColorChanged() { | |
| 144 if (media_state_ == TAB_MEDIA_STATE_AUDIO_PLAYING || | |
| 145 media_state_ == TAB_MEDIA_STATE_AUDIO_MUTING) | |
| 146 ResetImages(media_state_); | |
| 147 } | |
| 148 | |
| 149 const char* MediaIndicatorButton::GetClassName() const { | |
| 150 return kViewClassName; | |
| 151 } | |
| 152 | |
| 153 views::View* MediaIndicatorButton::GetTooltipHandlerForPoint( | |
| 154 const gfx::Point& point) { | |
| 155 return NULL; // Tab (the parent View) provides the tooltip. | |
| 156 } | |
| 157 | |
| 158 bool MediaIndicatorButton::OnMousePressed(const ui::MouseEvent& event) { | |
| 159 // Do not handle this mouse event when anything but the left mouse button is | |
| 160 // pressed or when any modifier keys are being held down. Instead, the Tab | |
| 161 // should react (e.g., middle-click for close, right-click for context menu). | |
| 162 if (!event.IsOnlyLeftMouseButton() || IsShiftOrControlDown(event)) { | |
| 163 if (state() != views::CustomButton::STATE_DISABLED) | |
| 164 SetState(views::CustomButton::STATE_NORMAL); // Turn off hover. | |
| 165 return false; // Event to be handled by Tab. | |
| 166 } | |
| 167 return ImageButton::OnMousePressed(event); | |
| 168 } | |
| 169 | |
| 170 bool MediaIndicatorButton::OnMouseDragged(const ui::MouseEvent& event) { | |
| 171 const ButtonState previous_state = state(); | |
| 172 const bool ret = ImageButton::OnMouseDragged(event); | |
| 173 if (previous_state != views::CustomButton::STATE_NORMAL && | |
| 174 state() == views::CustomButton::STATE_NORMAL) | |
| 175 content::RecordAction(UserMetricsAction("MediaIndicatorButton_Dragged")); | |
| 176 return ret; | |
| 177 } | |
| 178 | |
| 179 void MediaIndicatorButton::OnMouseEntered(const ui::MouseEvent& event) { | |
| 180 // If any modifier keys are being held down, do not turn on hover. | |
| 181 if (state() != views::CustomButton::STATE_DISABLED && | |
| 182 IsShiftOrControlDown(event)) { | |
| 183 SetState(views::CustomButton::STATE_NORMAL); | |
| 184 return; | |
| 185 } | |
| 186 ImageButton::OnMouseEntered(event); | |
| 187 } | |
| 188 | |
| 189 void MediaIndicatorButton::OnMouseMoved(const ui::MouseEvent& event) { | |
| 190 // If any modifier keys are being held down, turn off hover. | |
| 191 if (state() != views::CustomButton::STATE_DISABLED && | |
| 192 IsShiftOrControlDown(event)) { | |
| 193 SetState(views::CustomButton::STATE_NORMAL); | |
| 194 return; | |
| 195 } | |
| 196 ImageButton::OnMouseMoved(event); | |
| 197 } | |
| 198 | |
| 199 void MediaIndicatorButton::OnBoundsChanged(const gfx::Rect& previous_bounds) { | |
| 200 UpdateEnabledForMuteToggle(); | |
| 201 } | |
| 202 | |
| 203 void MediaIndicatorButton::OnPaint(gfx::Canvas* canvas) { | |
| 204 double opaqueness = | |
| 205 fade_animation_ ? fade_animation_->GetCurrentValue() : 1.0; | |
| 206 if (media_state_ == TAB_MEDIA_STATE_NONE) | |
| 207 opaqueness = 1.0 - opaqueness; // Fading out, not in. | |
| 208 if (opaqueness < 1.0) | |
| 209 canvas->SaveLayerAlpha(opaqueness * SK_AlphaOPAQUE); | |
| 210 ImageButton::OnPaint(canvas); | |
| 211 if (opaqueness < 1.0) | |
| 212 canvas->Restore(); | |
| 213 } | |
| 214 | |
| 215 bool MediaIndicatorButton::DoesIntersectRect(const views::View* target, | |
| 216 const gfx::Rect& rect) const { | |
| 217 // If this button is not enabled, Tab (the parent View) handles all mouse | |
| 218 // events. | |
| 219 return enabled() && | |
| 220 views::ViewTargeterDelegate::DoesIntersectRect(target, rect); | |
| 221 } | |
| 222 | |
| 223 void MediaIndicatorButton::NotifyClick(const ui::Event& event) { | |
| 224 if (media_state_ == TAB_MEDIA_STATE_AUDIO_PLAYING) | |
| 225 content::RecordAction(UserMetricsAction("MediaIndicatorButton_Mute")); | |
| 226 else if (media_state_ == TAB_MEDIA_STATE_AUDIO_MUTING) | |
| 227 content::RecordAction(UserMetricsAction("MediaIndicatorButton_Unmute")); | |
| 228 else | |
| 229 NOTREACHED(); | |
| 230 | |
| 231 GetTab()->controller()->ToggleTabAudioMute(GetTab()); | |
| 232 } | |
| 233 | |
| 234 bool MediaIndicatorButton::IsTriggerableEvent(const ui::Event& event) { | |
| 235 // For mouse events, only trigger on the left mouse button and when no | |
| 236 // modifier keys are being held down. | |
| 237 if (event.IsMouseEvent() && | |
| 238 (!static_cast<const ui::MouseEvent*>(&event)->IsOnlyLeftMouseButton() || | |
| 239 IsShiftOrControlDown(event))) | |
| 240 return false; | |
| 241 | |
| 242 // For gesture events on an inactive tab, require an even wider tab before | |
| 243 // click-to-mute can be triggered. See comments in | |
| 244 // UpdateEnabledForMuteToggle(). | |
| 245 if (event.IsGestureEvent() && !GetTab()->IsActive()) { | |
| 246 const int required_width = width() * kMinGestureSelectableAreaPercent / 100; | |
| 247 if (GetTab()->GetWidthOfLargestSelectableRegion() < required_width) | |
| 248 return false; | |
| 249 } | |
| 250 | |
| 251 return views::ImageButton::IsTriggerableEvent(event); | |
| 252 } | |
| 253 | |
| 254 Tab* MediaIndicatorButton::GetTab() const { | |
| 255 DCHECK_EQ(static_cast<views::View*>(parent_tab_), parent()); | |
| 256 return parent_tab_; | |
| 257 } | |
| 258 | |
| 259 void MediaIndicatorButton::ResetImages(TabMediaState state) { | |
| 260 SkColor color = parent_tab_->button_color(); | |
| 261 gfx::ImageSkia indicator_image = | |
| 262 chrome::GetTabMediaIndicatorImage(state, color).AsImageSkia(); | |
| 263 SetImage(views::CustomButton::STATE_NORMAL, &indicator_image); | |
| 264 SetImage(views::CustomButton::STATE_DISABLED, &indicator_image); | |
| 265 gfx::ImageSkia affordance_image = | |
| 266 chrome::GetTabMediaIndicatorAffordanceImage(state, color).AsImageSkia(); | |
| 267 SetImage(views::CustomButton::STATE_HOVERED, &affordance_image); | |
| 268 SetImage(views::CustomButton::STATE_PRESSED, &affordance_image); | |
| 269 } | |
| OLD | NEW |