1 // Copyright 2013 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.
5 #import "chrome/browser/ui/cocoa/tabs/media_indicator_button_cocoa.h"
7 #include "base/logging.h"
8 #include "base/mac/foundation_util.h"
9 #include "base/thread_task_runner_handle.h"
10 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
11 #include "content/public/browser/user_metrics.h"
12 #include "ui/gfx/animation/animation.h"
13 #include "ui/gfx/animation/animation_delegate.h"
14 #include "ui/gfx/image/image.h"
18 // The minimum required click-to-select area of an inactive tab before allowing
19 // the click-to-mute functionality to be enabled. This value is in terms of
20 // some percentage of the MediaIndicatorButton's width. See comments in the
21 // updateEnabledForMuteToggle method.
22 const int kMinMouseSelectableAreaPercent = 250;
26 @implementation MediaIndicatorButton
28 class FadeAnimationDelegate : public gfx::AnimationDelegate {
30 explicit FadeAnimationDelegate(MediaIndicatorButton* button)
32 ~FadeAnimationDelegate() override {}
35 // gfx::AnimationDelegate implementation.
36 void AnimationProgressed(const gfx::Animation* animation) override {
37 [button_ setNeedsDisplay:YES];
40 void AnimationCanceled(const gfx::Animation* animation) override {
41 AnimationEnded(animation);
44 void AnimationEnded(const gfx::Animation* animation) override {
45 button_->showingMediaState_ = button_->mediaState_;
46 [button_ setNeedsDisplay:YES];
47 [button_->animationDoneTarget_
48 performSelector:button_->animationDoneAction_];
51 MediaIndicatorButton* const button_;
53 DISALLOW_COPY_AND_ASSIGN(FadeAnimationDelegate);
56 @synthesize showingMediaState = showingMediaState_;
59 if ((self = [super initWithFrame:NSZeroRect])) {
60 mediaState_ = TAB_MEDIA_STATE_NONE;
61 showingMediaState_ = TAB_MEDIA_STATE_NONE;
63 [super setTarget:self];
64 [super setAction:@selector(handleClick:)];
69 - (void)removeFromSuperview {
70 fadeAnimation_.reset();
71 [super removeFromSuperview];
74 - (void)transitionToMediaState:(TabMediaState)nextState {
75 if (nextState == mediaState_)
78 if (nextState != TAB_MEDIA_STATE_NONE) {
80 setImage:chrome::GetTabMediaIndicatorImage(nextState, nil).ToNSImage()];
81 affordanceImage_.reset(
82 [chrome::GetTabMediaIndicatorAffordanceImage(nextState, nil)
83 .ToNSImage() retain]);
86 if ((mediaState_ == TAB_MEDIA_STATE_AUDIO_PLAYING &&
87 nextState == TAB_MEDIA_STATE_AUDIO_MUTING) ||
88 (mediaState_ == TAB_MEDIA_STATE_AUDIO_MUTING &&
89 nextState == TAB_MEDIA_STATE_AUDIO_PLAYING) ||
90 (mediaState_ == TAB_MEDIA_STATE_AUDIO_MUTING &&
91 nextState == TAB_MEDIA_STATE_NONE)) {
92 // Instant user feedback: No fade animation.
93 showingMediaState_ = nextState;
94 fadeAnimation_.reset();
96 if (nextState == TAB_MEDIA_STATE_NONE)
97 showingMediaState_ = mediaState_; // Fading-out indicator.
99 showingMediaState_ = nextState; // Fading-in to next indicator.
100 // gfx::Animation requires a task runner is available for the current
101 // thread. Generally, only certain unit tests would not instantiate a task
103 if (base::ThreadTaskRunnerHandle::IsSet()) {
104 fadeAnimation_ = chrome::CreateTabMediaIndicatorFadeAnimation(nextState);
105 if (!fadeAnimationDelegate_)
106 fadeAnimationDelegate_.reset(new FadeAnimationDelegate(self));
107 fadeAnimation_->set_delegate(fadeAnimationDelegate_.get());
108 fadeAnimation_->Start();
112 mediaState_ = nextState;
114 [self updateEnabledForMuteToggle];
116 // An indicator state change should be made visible immediately, instead of
117 // the user being surprised when their mouse leaves the button.
118 if ([self hoverState] == kHoverStateMouseOver)
119 [self setHoverState:kHoverStateNone];
121 [self setNeedsDisplay:YES];
124 - (void)setTarget:(id)aTarget {
125 NOTREACHED(); // See class-level comments.
128 - (void)setAction:(SEL)anAction {
129 NOTREACHED(); // See class-level comments.
132 - (void)setAnimationDoneTarget:(id)target withAction:(SEL)action {
133 animationDoneTarget_ = target;
134 animationDoneAction_ = action;
137 - (void)setClickTarget:(id)target withAction:(SEL)action {
138 clickTarget_ = target;
139 clickAction_ = action;
142 - (void)mouseDown:(NSEvent*)theEvent {
143 // Do not handle this left-button mouse event if any modifier keys are being
144 // held down. Instead, the Tab should react (e.g., selection or drag start).
145 if ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask) {
146 [self setHoverState:kHoverStateNone]; // Turn off hover.
147 [[self nextResponder] mouseDown:theEvent];
150 [super mouseDown:theEvent];
153 - (void)mouseEntered:(NSEvent*)theEvent {
154 // If any modifier keys are being held down, do not turn on hover.
155 if ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask) {
156 [self setHoverState:kHoverStateNone];
159 [super mouseEntered:theEvent];
162 - (void)mouseMoved:(NSEvent*)theEvent {
163 // If any modifier keys are being held down, turn off hover.
164 if ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask) {
165 [self setHoverState:kHoverStateNone];
168 [super mouseMoved:theEvent];
171 - (void)rightMouseDown:(NSEvent*)theEvent {
172 // All right-button mouse events should be handled by the Tab.
173 [self setHoverState:kHoverStateNone]; // Turn off hover.
174 [[self nextResponder] rightMouseDown:theEvent];
177 - (void)drawRect:(NSRect)dirtyRect {
178 NSImage* image = ([self hoverState] == kHoverStateNone || ![self isEnabled]) ?
179 [self image] : affordanceImage_.get();
182 NSRect imageRect = NSZeroRect;
183 imageRect.size = [image size];
184 NSRect destRect = [self bounds];
186 floor((NSHeight(destRect) / 2) - (NSHeight(imageRect) / 2));
187 destRect.size = imageRect.size;
189 fadeAnimation_ ? fadeAnimation_->GetCurrentValue() : 1.0;
190 if (mediaState_ == TAB_MEDIA_STATE_NONE)
191 opaqueness = 1.0 - opaqueness; // Fading out, not in.
192 [image drawInRect:destRect
194 operation:NSCompositeSourceOver
200 // When disabled, the superview should receive all mouse events.
201 - (NSView*)hitTest:(NSPoint)aPoint {
202 if ([self isEnabled] && ![self isHidden])
203 return [super hitTest:aPoint];
208 - (void)handleClick:(id)sender {
209 using base::UserMetricsAction;
211 if (mediaState_ == TAB_MEDIA_STATE_AUDIO_PLAYING)
212 content::RecordAction(UserMetricsAction("MediaIndicatorButton_Mute"));
213 else if (mediaState_ == TAB_MEDIA_STATE_AUDIO_MUTING)
214 content::RecordAction(UserMetricsAction("MediaIndicatorButton_Unmute"));
218 [clickTarget_ performSelector:clickAction_ withObject:self];
221 - (void)updateEnabledForMuteToggle {
222 BOOL enable = chrome::AreExperimentalMuteControlsEnabled() &&
223 (mediaState_ == TAB_MEDIA_STATE_AUDIO_PLAYING ||
224 mediaState_ == TAB_MEDIA_STATE_AUDIO_MUTING);
226 // If the tab is not the currently-active tab, make sure it is wide enough
227 // before enabling click-to-mute. This ensures that there is enough click
228 // area for the user to activate a tab rather than unintentionally muting it.
229 TabView* const tabView = base::mac::ObjCCast<TabView>([self superview]);
230 if (enable && tabView && ([tabView state] != NSOnState)) {
231 const int requiredWidth =
232 NSWidth([self frame]) * kMinMouseSelectableAreaPercent / 100;
233 enable = ([tabView widthOfLargestSelectableRegion] >= requiredWidth);
236 [self setEnabled:enable];