blob: 8db7968b427057de5b6881c7316ddb10a8bbdbc7 [file] [log] [blame]
// Copyright 2013 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.
#import "chrome/browser/ui/cocoa/tabs/alert_indicator_button_cocoa.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/macros.h"
#include "base/threading/thread_task_runner_handle.h"
#import "chrome/browser/ui/cocoa/tabs/tab_view.h"
#include "content/public/browser/user_metrics.h"
#include "ui/gfx/animation/animation.h"
#include "ui/gfx/animation/animation_delegate.h"
#include "ui/gfx/image/image.h"
namespace {
// The minimum required click-to-select area of an inactive tab before allowing
// the click-to-mute functionality to be enabled. This value is in terms of
// some percentage of the AlertIndicatorButton's width. See comments in the
// updateEnabledForMuteToggle method.
const int kMinMouseSelectableAreaPercent = 250;
} // namespace
@implementation AlertIndicatorButton
class FadeAnimationDelegate : public gfx::AnimationDelegate {
public:
explicit FadeAnimationDelegate(AlertIndicatorButton* button)
: button_(button) {}
~FadeAnimationDelegate() override {}
private:
// gfx::AnimationDelegate implementation.
void AnimationProgressed(const gfx::Animation* animation) override {
[button_ setNeedsDisplay:YES];
}
void AnimationCanceled(const gfx::Animation* animation) override {
AnimationEnded(animation);
}
void AnimationEnded(const gfx::Animation* animation) override {
button_->showingAlertState_ = button_->alertState_;
[button_ setNeedsDisplay:YES];
[button_->animationDoneTarget_
performSelector:button_->animationDoneAction_];
}
AlertIndicatorButton* const button_;
DISALLOW_COPY_AND_ASSIGN(FadeAnimationDelegate);
};
@synthesize showingAlertState = showingAlertState_;
- (id)init {
if ((self = [super initWithFrame:NSZeroRect])) {
alertState_ = TabAlertState::NONE;
showingAlertState_ = TabAlertState::NONE;
isDormant_ = NO;
[self setEnabled:NO];
[super setTarget:self];
[super setAction:@selector(handleClick:)];
}
return self;
}
- (void)removeFromSuperview {
fadeAnimation_.reset();
[super removeFromSuperview];
}
- (void)updateIconForState:(TabAlertState)aState {
if (aState != TabAlertState::NONE) {
TabView* const tabView = base::mac::ObjCCast<TabView>([self superview]);
SkColor iconColor = [tabView closeButtonColor];
NSImage* tabIndicatorImage =
chrome::GetTabAlertIndicatorImage(aState, iconColor).ToNSImage();
[self setImage:tabIndicatorImage];
affordanceImage_.reset(
[chrome::GetTabAlertIndicatorAffordanceImage(aState, iconColor)
.ToNSImage() retain]);
}
}
- (void)transitionToAlertState:(TabAlertState)nextState {
if (nextState == alertState_)
return;
[self updateIconForState:nextState];
if ((alertState_ == TabAlertState::AUDIO_PLAYING &&
nextState == TabAlertState::AUDIO_MUTING) ||
(alertState_ == TabAlertState::AUDIO_MUTING &&
nextState == TabAlertState::AUDIO_PLAYING) ||
(alertState_ == TabAlertState::AUDIO_MUTING &&
nextState == TabAlertState::NONE)) {
// Instant user feedback: No fade animation.
showingAlertState_ = nextState;
fadeAnimation_.reset();
} else {
if (nextState == TabAlertState::NONE)
showingAlertState_ = alertState_; // Fading-out indicator.
else
showingAlertState_ = nextState; // Fading-in to next indicator.
// gfx::Animation requires a task runner is available for the current
// thread. Generally, only certain unit tests would not instantiate a task
// runner.
if (base::ThreadTaskRunnerHandle::IsSet()) {
fadeAnimation_ = chrome::CreateTabAlertIndicatorFadeAnimation(nextState);
if (!fadeAnimationDelegate_)
fadeAnimationDelegate_.reset(new FadeAnimationDelegate(self));
fadeAnimation_->set_delegate(fadeAnimationDelegate_.get());
fadeAnimation_->Start();
}
}
alertState_ = nextState;
[self updateEnabledForMuteToggle];
[self setNeedsDisplay:YES];
}
- (void)setTarget:(id)aTarget {
NOTREACHED(); // See class-level comments.
}
- (void)setAction:(SEL)anAction {
NOTREACHED(); // See class-level comments.
}
- (void)setAnimationDoneTarget:(id)target withAction:(SEL)action {
animationDoneTarget_ = target;
animationDoneAction_ = action;
}
- (void)setClickTarget:(id)target withAction:(SEL)action {
clickTarget_ = target;
clickAction_ = action;
}
- (void)mouseDown:(NSEvent*)theEvent {
// Do not handle this left-button mouse event if any modifier keys are being
// held down. Instead, the Tab should react (e.g., selection or drag start).
if ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask ||
isDormant_) {
[self setHoverState:kHoverStateNone]; // Turn off hover.
[[self nextResponder] mouseDown:theEvent];
return;
}
[super mouseDown:theEvent];
}
- (void)mouseEntered:(NSEvent*)theEvent {
// If any modifier keys are being held down, do not turn on hover.
if ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask) {
[self setHoverState:kHoverStateNone];
return;
}
[super mouseEntered:theEvent];
}
- (void)mouseExited:(NSEvent*)theEvent {
[self exitDormantPeriod];
[super mouseExited:theEvent];
}
- (void)mouseMoved:(NSEvent*)theEvent {
// If any modifier keys are being held down, turn off hover.
if ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask) {
[self setHoverState:kHoverStateNone];
return;
}
[super mouseMoved:theEvent];
}
- (void)rightMouseDown:(NSEvent*)theEvent {
// All right-button mouse events should be handled by the Tab.
[self setHoverState:kHoverStateNone]; // Turn off hover.
[[self nextResponder] rightMouseDown:theEvent];
}
- (void)drawRect:(NSRect)dirtyRect {
NSImage* const image = ([self hoverState] == kHoverStateNone ||
![self isEnabled] || isDormant_) ?
[self image] : affordanceImage_.get();
if (!image)
return;
NSRect imageRect = NSZeroRect;
imageRect.size = [image size];
NSRect destRect = [self bounds];
destRect.origin.y =
floor((NSHeight(destRect) / 2) - (NSHeight(imageRect) / 2));
destRect.size = imageRect.size;
double opaqueness = 1.0;
if (fadeAnimation_) {
opaqueness = fadeAnimation_->GetCurrentValue();
if (alertState_ == TabAlertState::NONE)
opaqueness = 1.0 - opaqueness; // Fading out, not in.
} else if (isDormant_) {
opaqueness = 0.5;
}
[image drawInRect:destRect
fromRect:imageRect
operation:NSCompositeSourceOver
fraction:opaqueness
respectFlipped:YES
hints:nil];
}
// When disabled, the superview should receive all mouse events.
- (NSView*)hitTest:(NSPoint)aPoint {
if ([self isEnabled] && !isDormant_ && ![self isHidden])
return [super hitTest:aPoint];
else
return nil;
}
- (void)handleClick:(id)sender {
[self enterDormantPeriod];
// Call |-transitionToAlertState| to change the image, providing the user with
// instant feedback. In the very unlikely event that the mute toggle fails,
// |-transitionToAlertState| will be called again, via another code path, to
// set the image to be consistent with the final outcome.
using base::UserMetricsAction;
if (alertState_ == TabAlertState::AUDIO_PLAYING) {
content::RecordAction(UserMetricsAction("AlertIndicatorButton_Mute"));
[self transitionToAlertState:TabAlertState::AUDIO_MUTING];
} else {
DCHECK(alertState_ == TabAlertState::AUDIO_MUTING);
content::RecordAction(UserMetricsAction("AlertIndicatorButton_Unmute"));
[self transitionToAlertState:TabAlertState::AUDIO_PLAYING];
}
[clickTarget_ performSelector:clickAction_ withObject:self];
}
- (void)updateEnabledForMuteToggle {
const BOOL wasEnabled = [self isEnabled];
BOOL enable = chrome::AreExperimentalMuteControlsEnabled() &&
(alertState_ == TabAlertState::AUDIO_PLAYING ||
alertState_ == TabAlertState::AUDIO_MUTING);
// If the tab is not the currently-active tab, make sure it is wide enough
// before enabling click-to-mute. This ensures that there is enough click
// area for the user to activate a tab rather than unintentionally muting it.
TabView* const tabView = base::mac::ObjCCast<TabView>([self superview]);
if (enable && tabView && ([tabView state] != NSOnState)) {
const int requiredWidth =
NSWidth([self frame]) * kMinMouseSelectableAreaPercent / 100;
enable = ([tabView widthOfLargestSelectableRegion] >= requiredWidth);
}
if (enable == wasEnabled)
return;
[self setEnabled:enable];
// If the button has become enabled, check whether the mouse is currently
// hovering. If it is, enter a dormant period where extra user clicks are
// prevented from having an effect (i.e., before the user has realized the
// button has become enabled underneath their cursor).
if (!wasEnabled && [self hoverState] == kHoverStateMouseOver)
[self enterDormantPeriod];
else if (![self isEnabled])
[self exitDormantPeriod];
}
// Enters a temporary "dormant period" where this button will not trigger on
// clicks. The user is provided a visual affordance during this period. Sets a
// timer to call |-exitDormantPeriod|.
- (void)enterDormantPeriod {
isDormant_ = YES;
[self performSelector:@selector(exitDormantPeriod)
withObject:nil
afterDelay:[NSEvent doubleClickInterval]];
[self setNeedsDisplay:YES];
}
// Leaves the "dormant period," allowing clicks to once again trigger an enabled
// button.
- (void)exitDormantPeriod {
if (!isDormant_)
return;
isDormant_ = NO;
[self setNeedsDisplay:YES];
}
// ThemedWindowDrawing protocol support.
- (void)windowDidChangeTheme {
// Force the alert icon to update because the icon color may change based
// on the current theme.
[self updateIconForState:alertState_];
}
- (void)windowDidChangeActive {
}
@end