OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #import "chrome/browser/ui/cocoa/throbber_view.h" | |
6 | |
7 #include <set> | |
8 | |
9 #include "base/logging.h" | |
10 | |
11 static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows | |
12 | |
13 @interface ThrobberView(PrivateMethods) | |
14 - (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate; | |
15 - (void)maintainTimer; | |
16 - (void)animate; | |
17 @end | |
18 | |
19 @protocol ThrobberDataDelegate <NSObject> | |
20 // Is the current frame the last frame of the animation? | |
21 - (BOOL)animationIsComplete; | |
22 | |
23 // Draw the current frame into the current graphics context. | |
24 - (void)drawFrameInRect:(NSRect)rect; | |
25 | |
26 // Update the frame counter. | |
27 - (void)advanceFrame; | |
28 @end | |
29 | |
30 @interface ThrobberFilmstripDelegate : NSObject | |
31 <ThrobberDataDelegate> { | |
32 scoped_nsobject<NSImage> image_; | |
33 unsigned int numFrames_; // Number of frames in this animation. | |
34 unsigned int animationFrame_; // Current frame of the animation, | |
35 // [0..numFrames_) | |
36 } | |
37 | |
38 - (id)initWithImage:(NSImage*)image; | |
39 | |
40 @end | |
41 | |
42 @implementation ThrobberFilmstripDelegate | |
43 | |
44 - (id)initWithImage:(NSImage*)image { | |
45 if ((self = [super init])) { | |
46 // Reset the animation counter so there's no chance we are off the end. | |
47 animationFrame_ = 0; | |
48 | |
49 // Ensure that the height divides evenly into the width. Cache the | |
50 // number of frames in the animation for later. | |
51 NSSize imageSize = [image size]; | |
52 DCHECK(imageSize.height && imageSize.width); | |
53 if (!imageSize.height) | |
54 return nil; | |
55 DCHECK((int)imageSize.width % (int)imageSize.height == 0); | |
56 numFrames_ = (int)imageSize.width / (int)imageSize.height; | |
57 DCHECK(numFrames_); | |
58 image_.reset([image retain]); | |
59 } | |
60 return self; | |
61 } | |
62 | |
63 - (BOOL)animationIsComplete { | |
64 return NO; | |
65 } | |
66 | |
67 - (void)drawFrameInRect:(NSRect)rect { | |
68 float imageDimension = [image_ size].height; | |
69 float xOffset = animationFrame_ * imageDimension; | |
70 NSRect sourceImageRect = | |
71 NSMakeRect(xOffset, 0, imageDimension, imageDimension); | |
72 [image_ drawInRect:rect | |
73 fromRect:sourceImageRect | |
74 operation:NSCompositeSourceOver | |
75 fraction:1.0]; | |
76 } | |
77 | |
78 - (void)advanceFrame { | |
79 animationFrame_ = ++animationFrame_ % numFrames_; | |
80 } | |
81 | |
82 @end | |
83 | |
84 @interface ThrobberToastDelegate : NSObject | |
85 <ThrobberDataDelegate> { | |
86 scoped_nsobject<NSImage> image1_; | |
87 scoped_nsobject<NSImage> image2_; | |
88 NSSize image1Size_; | |
89 NSSize image2Size_; | |
90 int animationFrame_; // Current frame of the animation, | |
91 } | |
92 | |
93 - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2; | |
94 | |
95 @end | |
96 | |
97 @implementation ThrobberToastDelegate | |
98 | |
99 - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2 { | |
100 if ((self = [super init])) { | |
101 image1_.reset([image1 retain]); | |
102 image2_.reset([image2 retain]); | |
103 image1Size_ = [image1 size]; | |
104 image2Size_ = [image2 size]; | |
105 animationFrame_ = 0; | |
106 } | |
107 return self; | |
108 } | |
109 | |
110 - (BOOL)animationIsComplete { | |
111 if (animationFrame_ >= image1Size_.height + image2Size_.height) | |
112 return YES; | |
113 | |
114 return NO; | |
115 } | |
116 | |
117 // From [0..image1Height) we draw image1, at image1Height we draw nothing, and | |
118 // from [image1Height+1..image1Hight+image2Height] we draw the second image. | |
119 - (void)drawFrameInRect:(NSRect)rect { | |
120 NSImage* image = nil; | |
121 NSSize srcSize; | |
122 NSRect destRect; | |
123 | |
124 if (animationFrame_ < image1Size_.height) { | |
125 image = image1_.get(); | |
126 srcSize = image1Size_; | |
127 destRect = NSMakeRect(0, -animationFrame_, | |
128 image1Size_.width, image1Size_.height); | |
129 } else if (animationFrame_ == image1Size_.height) { | |
130 // nothing; intermediate blank frame | |
131 } else { | |
132 image = image2_.get(); | |
133 srcSize = image2Size_; | |
134 destRect = NSMakeRect(0, animationFrame_ - | |
135 (image1Size_.height + image2Size_.height), | |
136 image2Size_.width, image2Size_.height); | |
137 } | |
138 | |
139 if (image) { | |
140 NSRect sourceImageRect = | |
141 NSMakeRect(0, 0, srcSize.width, srcSize.height); | |
142 [image drawInRect:destRect | |
143 fromRect:sourceImageRect | |
144 operation:NSCompositeSourceOver | |
145 fraction:1.0]; | |
146 } | |
147 } | |
148 | |
149 - (void)advanceFrame { | |
150 ++animationFrame_; | |
151 } | |
152 | |
153 @end | |
154 | |
155 typedef std::set<ThrobberView*> ThrobberSet; | |
156 | |
157 // ThrobberTimer manages the animation of a set of ThrobberViews. It allows | |
158 // a single timer instance to be shared among as many ThrobberViews as needed. | |
159 @interface ThrobberTimer : NSObject { | |
160 @private | |
161 // A set of weak references to each ThrobberView that should be notified | |
162 // whenever the timer fires. | |
163 ThrobberSet throbbers_; | |
164 | |
165 // Weak reference to the timer that calls back to this object. The timer | |
166 // retains this object. | |
167 NSTimer* timer_; | |
168 | |
169 // Whether the timer is actively running. To avoid timer construction | |
170 // and destruction overhead, the timer is not invalidated when it is not | |
171 // needed, but its next-fire date is set to [NSDate distantFuture]. | |
172 // It is not possible to determine whether the timer has been suspended by | |
173 // comparing its fireDate to [NSDate distantFuture], though, so a separate | |
174 // variable is used to track this state. | |
175 BOOL timerRunning_; | |
176 | |
177 // The thread that created this object. Used to validate that ThrobberViews | |
178 // are only added and removed on the same thread that the fire action will | |
179 // be performed on. | |
180 NSThread* validThread_; | |
181 } | |
182 | |
183 // Returns a shared ThrobberTimer. Everyone is expected to use the same | |
184 // instance. | |
185 + (ThrobberTimer*)sharedThrobberTimer; | |
186 | |
187 // Invalidates the timer, which will cause it to remove itself from the run | |
188 // loop. This causes the timer to be released, and it should then release | |
189 // this object. | |
190 - (void)invalidate; | |
191 | |
192 // Adds or removes ThrobberView objects from the throbbers_ set. | |
193 - (void)addThrobber:(ThrobberView*)throbber; | |
194 - (void)removeThrobber:(ThrobberView*)throbber; | |
195 @end | |
196 | |
197 @interface ThrobberTimer(PrivateMethods) | |
198 // Starts or stops the timer as needed as ThrobberViews are added and removed | |
199 // from the throbbers_ set. | |
200 - (void)maintainTimer; | |
201 | |
202 // Calls animate on each ThrobberView in the throbbers_ set. | |
203 - (void)fire:(NSTimer*)timer; | |
204 @end | |
205 | |
206 @implementation ThrobberTimer | |
207 - (id)init { | |
208 if ((self = [super init])) { | |
209 // Start out with a timer that fires at the appropriate interval, but | |
210 // prevent it from firing by setting its next-fire date to the distant | |
211 // future. Once a ThrobberView is added, the timer will be allowed to | |
212 // start firing. | |
213 timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationIntervalSeconds | |
214 target:self | |
215 selector:@selector(fire:) | |
216 userInfo:nil | |
217 repeats:YES]; | |
218 [timer_ setFireDate:[NSDate distantFuture]]; | |
219 timerRunning_ = NO; | |
220 | |
221 validThread_ = [NSThread currentThread]; | |
222 } | |
223 return self; | |
224 } | |
225 | |
226 + (ThrobberTimer*)sharedThrobberTimer { | |
227 // Leaked. That's OK, it's scoped to the lifetime of the application. | |
228 static ThrobberTimer* sharedInstance = [[ThrobberTimer alloc] init]; | |
229 return sharedInstance; | |
230 } | |
231 | |
232 - (void)invalidate { | |
233 [timer_ invalidate]; | |
234 } | |
235 | |
236 - (void)addThrobber:(ThrobberView*)throbber { | |
237 DCHECK([NSThread currentThread] == validThread_); | |
238 throbbers_.insert(throbber); | |
239 [self maintainTimer]; | |
240 } | |
241 | |
242 - (void)removeThrobber:(ThrobberView*)throbber { | |
243 DCHECK([NSThread currentThread] == validThread_); | |
244 throbbers_.erase(throbber); | |
245 [self maintainTimer]; | |
246 } | |
247 | |
248 - (void)maintainTimer { | |
249 BOOL oldRunning = timerRunning_; | |
250 BOOL newRunning = throbbers_.empty() ? NO : YES; | |
251 | |
252 if (oldRunning == newRunning) | |
253 return; | |
254 | |
255 // To start the timer, set its next-fire date to an appropriate interval from | |
256 // now. To suspend the timer, set its next-fire date to a preposterous time | |
257 // in the future. | |
258 NSDate* fireDate; | |
259 if (newRunning) | |
260 fireDate = [NSDate dateWithTimeIntervalSinceNow:kAnimationIntervalSeconds]; | |
261 else | |
262 fireDate = [NSDate distantFuture]; | |
263 | |
264 [timer_ setFireDate:fireDate]; | |
265 timerRunning_ = newRunning; | |
266 } | |
267 | |
268 - (void)fire:(NSTimer*)timer { | |
269 // The call to [throbber animate] may result in the ThrobberView calling | |
270 // removeThrobber: if it decides it's done animating. That would invalidate | |
271 // the iterator, making it impossible to correctly get to the next element | |
272 // in the set. To prevent that from happening, a second iterator is used | |
273 // and incremented before calling [throbber animate]. | |
274 ThrobberSet::const_iterator current = throbbers_.begin(); | |
275 ThrobberSet::const_iterator next = current; | |
276 while (current != throbbers_.end()) { | |
277 ++next; | |
278 ThrobberView* throbber = *current; | |
279 [throbber animate]; | |
280 current = next; | |
281 } | |
282 } | |
283 @end | |
284 | |
285 @implementation ThrobberView | |
286 | |
287 + (id)filmstripThrobberViewWithFrame:(NSRect)frame | |
288 image:(NSImage*)image { | |
289 ThrobberFilmstripDelegate* delegate = | |
290 [[[ThrobberFilmstripDelegate alloc] initWithImage:image] autorelease]; | |
291 if (!delegate) | |
292 return nil; | |
293 | |
294 return [[[ThrobberView alloc] initWithFrame:frame | |
295 delegate:delegate] autorelease]; | |
296 } | |
297 | |
298 + (id)toastThrobberViewWithFrame:(NSRect)frame | |
299 beforeImage:(NSImage*)beforeImage | |
300 afterImage:(NSImage*)afterImage { | |
301 ThrobberToastDelegate* delegate = | |
302 [[[ThrobberToastDelegate alloc] initWithImage1:beforeImage | |
303 image2:afterImage] autorelease]; | |
304 if (!delegate) | |
305 return nil; | |
306 | |
307 return [[[ThrobberView alloc] initWithFrame:frame | |
308 delegate:delegate] autorelease]; | |
309 } | |
310 | |
311 - (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate { | |
312 if ((self = [super initWithFrame:frame])) { | |
313 dataDelegate_ = [delegate retain]; | |
314 } | |
315 return self; | |
316 } | |
317 | |
318 - (void)dealloc { | |
319 [dataDelegate_ release]; | |
320 [[ThrobberTimer sharedThrobberTimer] removeThrobber:self]; | |
321 | |
322 [super dealloc]; | |
323 } | |
324 | |
325 // Manages this ThrobberView's membership in the shared throbber timer set on | |
326 // the basis of its visibility and whether its animation needs to continue | |
327 // running. | |
328 - (void)maintainTimer { | |
329 ThrobberTimer* throbberTimer = [ThrobberTimer sharedThrobberTimer]; | |
330 | |
331 if ([self window] && ![self isHidden] && ![dataDelegate_ animationIsComplete]) | |
332 [throbberTimer addThrobber:self]; | |
333 else | |
334 [throbberTimer removeThrobber:self]; | |
335 } | |
336 | |
337 // A ThrobberView added to a window may need to begin animating; a ThrobberView | |
338 // removed from a window should stop. | |
339 - (void)viewDidMoveToWindow { | |
340 [self maintainTimer]; | |
341 [super viewDidMoveToWindow]; | |
342 } | |
343 | |
344 // A hidden ThrobberView should stop animating. | |
345 - (void)viewDidHide { | |
346 [self maintainTimer]; | |
347 [super viewDidHide]; | |
348 } | |
349 | |
350 // A visible ThrobberView may need to start animating. | |
351 - (void)viewDidUnhide { | |
352 [self maintainTimer]; | |
353 [super viewDidUnhide]; | |
354 } | |
355 | |
356 // Called when the timer fires. Advance the frame, dirty the display, and remove | |
357 // the throbber if it's no longer needed. | |
358 - (void)animate { | |
359 [dataDelegate_ advanceFrame]; | |
360 [self setNeedsDisplay:YES]; | |
361 | |
362 if ([dataDelegate_ animationIsComplete]) { | |
363 [[ThrobberTimer sharedThrobberTimer] removeThrobber:self]; | |
364 } | |
365 } | |
366 | |
367 // Overridden to draw the appropriate frame in the image strip. | |
368 - (void)drawRect:(NSRect)rect { | |
369 [dataDelegate_ drawFrameInRect:[self bounds]]; | |
370 } | |
371 | |
372 @end | |
OLD | NEW |