Don't show supervised user as "already on this device" while they're being imported.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / spinner_view.mm
blob55e9c5c7d27d446ae0ba5a573f1082573210f4ba
1 // Copyright 2015 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/spinner_view.h"
7 #import <QuartzCore/QuartzCore.h>
9 #include "base/mac/mac_util.h"
10 #include "base/mac/sdk_forward_declarations.h"
11 #include "base/mac/scoped_cftyperef.h"
12 #include "skia/ext/skia_utils_mac.h"
14 namespace {
15 const CGFloat kDegrees90               = (M_PI / 2);
16 const CGFloat kDegrees180              = (M_PI);
17 const CGFloat kDegrees270              = (3 * M_PI / 2);
18 const CGFloat kDegrees360              = (2 * M_PI);
19 const CGFloat kDesignWidth             = 28.0;
20 const CGFloat kArcRadius               = 12.5;
21 const CGFloat kArcDiameter             = kArcRadius * 2.0;
22 const CGFloat kArcLength               = 58.9;
23 const CGFloat kArcStrokeWidth          = 3.0;
24 const CGFloat kArcAnimationTime        = 1.333;
25 const CGFloat kArcStartAngle           = kDegrees180;
26 const CGFloat kArcEndAngle             = (kArcStartAngle + kDegrees270);
27 const CGFloat kRotationTime            = 1.56863;
28 const SkColor kBlue                    = SkColorSetRGB(0x42, 0x85, 0xf4);
29 NSString* const kSpinnerAnimationName  = @"SpinnerAnimationName";
30 NSString* const kRotationAnimationName = @"RotationAnimationName";
33 @interface SpinnerView () {
34   base::scoped_nsobject<CAAnimationGroup> spinnerAnimation_;
35   base::scoped_nsobject<CABasicAnimation> rotationAnimation_;
36   CAShapeLayer* shapeLayer_;  // Weak.
37   CALayer* rotationLayer_;  // Weak.
39 @end
42 @implementation SpinnerView
44 - (instancetype)initWithFrame:(NSRect)frame {
45   if (self = [super initWithFrame:frame]) {
46     [self setWantsLayer:YES];
47   }
48   return self;
51 - (void)dealloc {
52   [[NSNotificationCenter defaultCenter] removeObserver:self];
53   [super dealloc];
56 // Register/unregister for window miniaturization event notifications so that
57 // the spinner can stop animating if the window is minaturized
58 // (i.e. not visible).
59 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
60   if ([self window]) {
61     [[NSNotificationCenter defaultCenter]
62         removeObserver:self
63                   name:NSWindowWillMiniaturizeNotification
64                 object:[self window]];
65     [[NSNotificationCenter defaultCenter]
66         removeObserver:self
67                   name:NSWindowDidDeminiaturizeNotification
68                 object:[self window]];
69   }
71   if (newWindow) {
72     [[NSNotificationCenter defaultCenter]
73         addObserver:self
74            selector:@selector(updateAnimation:)
75                name:NSWindowWillMiniaturizeNotification
76              object:newWindow];
77     [[NSNotificationCenter defaultCenter]
78         addObserver:self
79            selector:@selector(updateAnimation:)
80                name:NSWindowDidDeminiaturizeNotification
81              object:newWindow];
82   }
85 // Start or stop the animation whenever the view is added to or removed from a
86 // window.
87 - (void)viewDidMoveToWindow {
88   [self updateAnimation:nil];
91 - (BOOL)isAnimating {
92   return [shapeLayer_ animationForKey:kSpinnerAnimationName] != nil;
95 // Overridden to return a custom CALayer for the view (called from
96 // setWantsLayer:).
97 - (CALayer*)makeBackingLayer {
98   CGRect bounds = [self bounds];
99   // The spinner was designed to be |kDesignWidth| points wide. Compute the
100   // scale factor needed to scale design parameters like |RADIUS| so that the
101   // spinner scales to fit the view's bounds.
102   CGFloat scaleFactor = bounds.size.width / kDesignWidth;
104   shapeLayer_ = [CAShapeLayer layer];
105   [shapeLayer_ setDelegate:self];
106   [shapeLayer_ setBounds:bounds];
107   // Per the design, the line width does not scale linearly.
108   CGFloat scaledDiameter = kArcDiameter * scaleFactor;
109   CGFloat lineWidth;
110   if (scaledDiameter < kArcDiameter) {
111     lineWidth = kArcStrokeWidth - (kArcDiameter - scaledDiameter) / 16.0;
112   } else {
113     lineWidth = kArcStrokeWidth + (scaledDiameter - kArcDiameter) / 11.0;
114   }
115   [shapeLayer_ setLineWidth:lineWidth];
116   [shapeLayer_ setLineCap:kCALineCapRound];
117   [shapeLayer_ setLineDashPattern:@[ @(kArcLength * scaleFactor) ]];
118   [shapeLayer_ setFillColor:NULL];
119   CGColorRef blueColor = gfx::CGColorCreateFromSkColor(kBlue);
120   [shapeLayer_ setStrokeColor:blueColor];
121   CGColorRelease(blueColor);
123   // Create the arc that, when stroked, creates the spinner.
124   base::ScopedCFTypeRef<CGMutablePathRef> shapePath(CGPathCreateMutable());
125   CGPathAddArc(shapePath, NULL, bounds.size.width / 2.0,
126                bounds.size.height / 2.0, kArcRadius * scaleFactor,
127                kArcStartAngle, kArcEndAngle, 0);
128   [shapeLayer_ setPath:shapePath];
130   // Place |shapeLayer_| in a layer so that it's easy to rotate the entire
131   // spinner animation.
132   rotationLayer_ = [CALayer layer];
133   [rotationLayer_ setBounds:bounds];
134   [rotationLayer_ addSublayer:shapeLayer_];
135   [shapeLayer_ setPosition:CGPointMake(NSMidX(bounds), NSMidY(bounds))];
137   // Place |rotationLayer_| in a parent layer so that it's easy to rotate
138   // |rotationLayer_| around the center of the view.
139   CALayer* parentLayer = [CALayer layer];
140   [parentLayer setBounds:bounds];
141   [parentLayer addSublayer:rotationLayer_];
142   [rotationLayer_ setPosition:CGPointMake(bounds.size.width / 2.0,
143                                           bounds.size.height / 2.0)];
144   return parentLayer;
147 // Overridden to start or stop the animation whenever the view is unhidden or
148 // hidden.
149 - (void)setHidden:(BOOL)flag {
150   [super setHidden:flag];
151   [self updateAnimation:nil];
154 // Make sure the layer's backing store matches the window as the window moves
155 // between screens.
156 - (BOOL)layer:(CALayer*)layer
157     shouldInheritContentsScale:(CGFloat)newScale
158                     fromWindow:(NSWindow*)window {
159   return YES;
162 // The spinner animation consists of four cycles that it continuously repeats.
163 // Each cycle consists of one complete rotation of the spinner's arc plus a
164 // rotation adjustment at the end of each cycle (see rotation animation comment
165 // below for the reason for the adjustment). The arc's length also grows and
166 // shrinks over the course of each cycle, which the spinner achieves by drawing
167 // the arc using a (solid) dashed line pattern and animating the "lineDashPhase"
168 // property.
169 - (void)initializeAnimation {
170   CGRect bounds = [self bounds];
171   CGFloat scaleFactor = bounds.size.width / kDesignWidth;
173   // Make sure |shapeLayer_|'s content scale factor matches the window's
174   // backing depth (e.g. it's 2.0 on Retina Macs). Don't worry about adjusting
175   // any other layers because |shapeLayer_| is the only one displaying content.
176   if (base::mac::IsOSLionOrLater()) {
177     CGFloat backingScaleFactor = [[self window] backingScaleFactor];
178     [shapeLayer_ setContentsScale:backingScaleFactor];
179   }
181   // Create the first half of the arc animation, where it grows from a short
182   // block to its full length.
183   base::scoped_nsobject<CAMediaTimingFunction> timingFunction(
184       [[CAMediaTimingFunction alloc] initWithControlPoints:0.4 :0.0 :0.2 :1]);
185   base::scoped_nsobject<CAKeyframeAnimation> firstHalfAnimation(
186       [[CAKeyframeAnimation alloc] init]);
187   [firstHalfAnimation setTimingFunction:timingFunction];
188   [firstHalfAnimation setKeyPath:@"lineDashPhase"];
189   // Begin the lineDashPhase animation just short of the full arc length,
190   // otherwise the arc will be zero length at start.
191   NSArray* animationValues = @[ @(-(kArcLength - 0.4) * scaleFactor), @(0.0) ];
192   [firstHalfAnimation setValues:animationValues];
193   NSArray* keyTimes = @[ @(0.0), @(1.0) ];
194   [firstHalfAnimation setKeyTimes:keyTimes];
195   [firstHalfAnimation setDuration:kArcAnimationTime / 2.0];
196   [firstHalfAnimation setRemovedOnCompletion:NO];
197   [firstHalfAnimation setFillMode:kCAFillModeForwards];
199   // Create the second half of the arc animation, where it shrinks from full
200   // length back to a short block.
201   base::scoped_nsobject<CAKeyframeAnimation> secondHalfAnimation(
202       [[CAKeyframeAnimation alloc] init]);
203   [secondHalfAnimation setTimingFunction:timingFunction];
204   [secondHalfAnimation setKeyPath:@"lineDashPhase"];
205   // Stop the lineDashPhase animation just before it reaches the full arc
206   // length, otherwise the arc will be zero length at the end.
207   animationValues = @[ @(0.0), @((kArcLength - 0.3) * scaleFactor) ];
208   [secondHalfAnimation setValues:animationValues];
209   [secondHalfAnimation setKeyTimes:keyTimes];
210   [secondHalfAnimation setDuration:kArcAnimationTime / 2.0];
211   [secondHalfAnimation setRemovedOnCompletion:NO];
212   [secondHalfAnimation setFillMode:kCAFillModeForwards];
214   // Make four copies of the arc animations, to cover the four complete cycles
215   // of the full animation.
216   NSMutableArray* animations = [NSMutableArray array];
217   CGFloat beginTime = 0;
218   for (NSUInteger i = 0; i < 4; i++, beginTime += kArcAnimationTime) {
219     [firstHalfAnimation setBeginTime:beginTime];
220     [secondHalfAnimation setBeginTime:beginTime + kArcAnimationTime / 2.0];
221     [animations addObject:firstHalfAnimation];
222     [animations addObject:secondHalfAnimation];
223     firstHalfAnimation.reset([firstHalfAnimation copy]);
224     secondHalfAnimation.reset([secondHalfAnimation copy]);
225   }
227   // Create a step rotation animation, which rotates the arc 90 degrees on each
228   // cycle. Each arc starts as a short block at degree 0 and ends as a short
229   // block at degree -270. Without a 90 degree rotation at the end of each
230   // cycle, the short block would appear to suddenly jump from -270 degrees to
231   // -360 degrees. The full animation has to contain four of these 90 degree
232   // adjustments in order for the arc to return to its starting point, at which
233   // point the full animation can smoothly repeat.
234   CAKeyframeAnimation* stepRotationAnimation = [CAKeyframeAnimation animation];
235   [stepRotationAnimation setTimingFunction:
236       [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
237   [stepRotationAnimation setKeyPath:@"transform.rotation"];
238   animationValues = @[ @(0.0), @(0.0),
239                        @(kDegrees90),
240                        @(kDegrees90),
241                        @(kDegrees180),
242                        @(kDegrees180),
243                        @(kDegrees270),
244                        @(kDegrees270)];
245   [stepRotationAnimation setValues:animationValues];
246   keyTimes = @[ @(0.0), @(0.25), @(0.25), @(0.5), @(0.5), @(0.75), @(0.75),
247                 @(1.0) ];
248   [stepRotationAnimation setKeyTimes:keyTimes];
249   [stepRotationAnimation setDuration:kArcAnimationTime * 4.0];
250   [stepRotationAnimation setRemovedOnCompletion:NO];
251   [stepRotationAnimation setFillMode:kCAFillModeForwards];
252   [stepRotationAnimation setRepeatCount:HUGE_VALF];
253   [animations addObject:stepRotationAnimation];
255   // Use an animation group so that the animations are easier to manage, and to
256   // give them the best chance of firing synchronously.
257   CAAnimationGroup* group = [CAAnimationGroup animation];
258   [group setDuration:kArcAnimationTime * 4];
259   [group setRepeatCount:HUGE_VALF];
260   [group setFillMode:kCAFillModeForwards];
261   [group setRemovedOnCompletion:NO];
262   [group setAnimations:animations];
264   spinnerAnimation_.reset([group retain]);
266   // Finally, create an animation that rotates the entire spinner layer.
267   CABasicAnimation* rotationAnimation = [CABasicAnimation animation];
268   rotationAnimation.keyPath = @"transform.rotation";
269   [rotationAnimation setFromValue:@0];
270   [rotationAnimation setToValue:@(-kDegrees360)];
271   [rotationAnimation setDuration:kRotationTime];
272   [rotationAnimation setRemovedOnCompletion:NO];
273   [rotationAnimation setFillMode:kCAFillModeForwards];
274   [rotationAnimation setRepeatCount:HUGE_VALF];
276   rotationAnimation_.reset([rotationAnimation retain]);
279 - (void)updateAnimation:(NSNotification*)notification {
280   // Only animate the spinner if it's within a window, and that window is not
281   // currently minimized or being minimized.
282   if ([self window] && ![[self window] isMiniaturized] && ![self isHidden] &&
283       ![[notification name] isEqualToString:
284            NSWindowWillMiniaturizeNotification]) {
285     if (spinnerAnimation_.get() == nil) {
286       [self initializeAnimation];
287     }
288     if (![self isAnimating]) {
289       [shapeLayer_ addAnimation:spinnerAnimation_.get()
290                          forKey:kSpinnerAnimationName];
291       [rotationLayer_ addAnimation:rotationAnimation_.get()
292                             forKey:kRotationAnimationName];
293     }
294   } else {
295     [shapeLayer_ removeAllAnimations];
296     [rotationLayer_ removeAllAnimations];
297   }
300 @end