1 // Copyright (c) 2009 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/throbber_view.h"
9 #include "base/logging.h"
10 #include "base/mac/scoped_nsobject.h"
12 static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows
14 @interface ThrobberView(PrivateMethods)
15 - (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate;
16 - (void)maintainTimer;
20 @protocol ThrobberDataDelegate <NSObject>
21 // Is the current frame the last frame of the animation?
22 - (BOOL)animationIsComplete;
24 // Draw the current frame into the current graphics context.
25 - (void)drawFrameInRect:(NSRect)rect;
27 // Update the frame counter.
31 @interface ThrobberFilmstripDelegate : NSObject
32 <ThrobberDataDelegate> {
33 base::scoped_nsobject<NSImage> image_;
34 unsigned int numFrames_; // Number of frames in this animation.
35 unsigned int animationFrame_; // Current frame of the animation,
39 - (id)initWithImage:(NSImage*)image;
43 @implementation ThrobberFilmstripDelegate
45 - (id)initWithImage:(NSImage*)image {
46 if ((self = [super init])) {
47 // Reset the animation counter so there's no chance we are off the end.
50 // Ensure that the height divides evenly into the width. Cache the
51 // number of frames in the animation for later.
52 NSSize imageSize = [image size];
53 DCHECK(imageSize.height && imageSize.width);
54 if (!imageSize.height)
56 DCHECK((int)imageSize.width % (int)imageSize.height == 0);
57 numFrames_ = (int)imageSize.width / (int)imageSize.height;
59 image_.reset([image retain]);
64 - (BOOL)animationIsComplete {
68 - (void)drawFrameInRect:(NSRect)rect {
69 float imageDimension = [image_ size].height;
70 float xOffset = animationFrame_ * imageDimension;
71 NSRect sourceImageRect =
72 NSMakeRect(xOffset, 0, imageDimension, imageDimension);
73 [image_ drawInRect:rect
74 fromRect:sourceImageRect
75 operation:NSCompositeSourceOver
79 - (void)advanceFrame {
80 animationFrame_ = ++animationFrame_ % numFrames_;
85 @interface ThrobberToastDelegate : NSObject
86 <ThrobberDataDelegate> {
87 base::scoped_nsobject<NSImage> image1_;
88 base::scoped_nsobject<NSImage> image2_;
91 int animationFrame_; // Current frame of the animation,
94 - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2;
98 @implementation ThrobberToastDelegate
100 - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2 {
101 if ((self = [super init])) {
102 image1_.reset([image1 retain]);
103 image2_.reset([image2 retain]);
104 image1Size_ = [image1 size];
105 image2Size_ = [image2 size];
111 - (BOOL)animationIsComplete {
112 if (animationFrame_ >= image1Size_.height + image2Size_.height)
118 // From [0..image1Height) we draw image1, at image1Height we draw nothing, and
119 // from [image1Height+1..image1Hight+image2Height] we draw the second image.
120 - (void)drawFrameInRect:(NSRect)rect {
121 NSImage* image = nil;
125 if (animationFrame_ < image1Size_.height) {
126 image = image1_.get();
127 srcSize = image1Size_;
128 destRect = NSMakeRect(0, -animationFrame_,
129 image1Size_.width, image1Size_.height);
130 } else if (animationFrame_ == image1Size_.height) {
131 // nothing; intermediate blank frame
133 image = image2_.get();
134 srcSize = image2Size_;
135 destRect = NSMakeRect(0, animationFrame_ -
136 (image1Size_.height + image2Size_.height),
137 image2Size_.width, image2Size_.height);
141 NSRect sourceImageRect =
142 NSMakeRect(0, 0, srcSize.width, srcSize.height);
143 [image drawInRect:destRect
144 fromRect:sourceImageRect
145 operation:NSCompositeSourceOver
150 - (void)advanceFrame {
156 typedef std::set<ThrobberView*> ThrobberSet;
158 // ThrobberTimer manages the animation of a set of ThrobberViews. It allows
159 // a single timer instance to be shared among as many ThrobberViews as needed.
160 @interface ThrobberTimer : NSObject {
162 // A set of weak references to each ThrobberView that should be notified
163 // whenever the timer fires.
164 ThrobberSet throbbers_;
166 // Weak reference to the timer that calls back to this object. The timer
167 // retains this object.
170 // Whether the timer is actively running. To avoid timer construction
171 // and destruction overhead, the timer is not invalidated when it is not
172 // needed, but its next-fire date is set to [NSDate distantFuture].
173 // It is not possible to determine whether the timer has been suspended by
174 // comparing its fireDate to [NSDate distantFuture], though, so a separate
175 // variable is used to track this state.
178 // The thread that created this object. Used to validate that ThrobberViews
179 // are only added and removed on the same thread that the fire action will
181 NSThread* validThread_;
184 // Returns a shared ThrobberTimer. Everyone is expected to use the same
186 + (ThrobberTimer*)sharedThrobberTimer;
188 // Invalidates the timer, which will cause it to remove itself from the run
189 // loop. This causes the timer to be released, and it should then release
193 // Adds or removes ThrobberView objects from the throbbers_ set.
194 - (void)addThrobber:(ThrobberView*)throbber;
195 - (void)removeThrobber:(ThrobberView*)throbber;
198 @interface ThrobberTimer(PrivateMethods)
199 // Starts or stops the timer as needed as ThrobberViews are added and removed
200 // from the throbbers_ set.
201 - (void)maintainTimer;
203 // Calls animate on each ThrobberView in the throbbers_ set.
204 - (void)fire:(NSTimer*)timer;
207 @implementation ThrobberTimer
209 if ((self = [super init])) {
210 // Start out with a timer that fires at the appropriate interval, but
211 // prevent it from firing by setting its next-fire date to the distant
212 // future. Once a ThrobberView is added, the timer will be allowed to
214 timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationIntervalSeconds
216 selector:@selector(fire:)
219 [timer_ setFireDate:[NSDate distantFuture]];
222 validThread_ = [NSThread currentThread];
227 + (ThrobberTimer*)sharedThrobberTimer {
228 // Leaked. That's OK, it's scoped to the lifetime of the application.
229 static ThrobberTimer* sharedInstance = [[ThrobberTimer alloc] init];
230 return sharedInstance;
237 - (void)addThrobber:(ThrobberView*)throbber {
238 DCHECK([NSThread currentThread] == validThread_);
239 throbbers_.insert(throbber);
240 [self maintainTimer];
243 - (void)removeThrobber:(ThrobberView*)throbber {
244 DCHECK([NSThread currentThread] == validThread_);
245 throbbers_.erase(throbber);
246 [self maintainTimer];
249 - (void)maintainTimer {
250 BOOL oldRunning = timerRunning_;
251 BOOL newRunning = throbbers_.empty() ? NO : YES;
253 if (oldRunning == newRunning)
256 // To start the timer, set its next-fire date to an appropriate interval from
257 // now. To suspend the timer, set its next-fire date to a preposterous time
261 fireDate = [NSDate dateWithTimeIntervalSinceNow:kAnimationIntervalSeconds];
263 fireDate = [NSDate distantFuture];
265 [timer_ setFireDate:fireDate];
266 timerRunning_ = newRunning;
269 - (void)fire:(NSTimer*)timer {
270 // The call to [throbber animate] may result in the ThrobberView calling
271 // removeThrobber: if it decides it's done animating. That would invalidate
272 // the iterator, making it impossible to correctly get to the next element
273 // in the set. To prevent that from happening, a second iterator is used
274 // and incremented before calling [throbber animate].
275 ThrobberSet::const_iterator current = throbbers_.begin();
276 ThrobberSet::const_iterator next = current;
277 while (current != throbbers_.end()) {
279 ThrobberView* throbber = *current;
286 @implementation ThrobberView
288 + (id)filmstripThrobberViewWithFrame:(NSRect)frame
289 image:(NSImage*)image {
290 ThrobberFilmstripDelegate* delegate =
291 [[[ThrobberFilmstripDelegate alloc] initWithImage:image] autorelease];
295 return [[[ThrobberView alloc] initWithFrame:frame
296 delegate:delegate] autorelease];
299 + (id)toastThrobberViewWithFrame:(NSRect)frame
300 beforeImage:(NSImage*)beforeImage
301 afterImage:(NSImage*)afterImage {
302 ThrobberToastDelegate* delegate =
303 [[[ThrobberToastDelegate alloc] initWithImage1:beforeImage
304 image2:afterImage] autorelease];
308 return [[[ThrobberView alloc] initWithFrame:frame
309 delegate:delegate] autorelease];
312 - (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate {
313 if ((self = [super initWithFrame:frame])) {
314 dataDelegate_ = [delegate retain];
320 [dataDelegate_ release];
321 [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
326 // Manages this ThrobberView's membership in the shared throbber timer set on
327 // the basis of its visibility and whether its animation needs to continue
329 - (void)maintainTimer {
330 ThrobberTimer* throbberTimer = [ThrobberTimer sharedThrobberTimer];
332 if ([self window] && ![self isHidden] && ![dataDelegate_ animationIsComplete])
333 [throbberTimer addThrobber:self];
335 [throbberTimer removeThrobber:self];
338 // A ThrobberView added to a window may need to begin animating; a ThrobberView
339 // removed from a window should stop.
340 - (void)viewDidMoveToWindow {
341 [self maintainTimer];
342 [super viewDidMoveToWindow];
345 // A hidden ThrobberView should stop animating.
346 - (void)viewDidHide {
347 [self maintainTimer];
351 // A visible ThrobberView may need to start animating.
352 - (void)viewDidUnhide {
353 [self maintainTimer];
354 [super viewDidUnhide];
357 // Called when the timer fires. Advance the frame, dirty the display, and remove
358 // the throbber if it's no longer needed.
360 [dataDelegate_ advanceFrame];
361 [self setNeedsDisplay:YES];
363 if ([dataDelegate_ animationIsComplete]) {
364 [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
368 // Overridden to draw the appropriate frame in the image strip.
369 - (void)drawRect:(NSRect)rect {
370 [dataDelegate_ drawFrameInRect:[self bounds]];