Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / tabs / throbber_view.mm
blob0d838a3a43d6788c06a9671ed955e9a650657abb
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"
7 #include <set>
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;
17 - (void)animate;
18 @end
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.
28 - (void)advanceFrame;
29 @end
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,
36                                  // [0..numFrames_)
39 - (id)initWithImage:(NSImage*)image;
41 @end
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.
48     animationFrame_ = 0;
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)
55       return nil;
56     DCHECK((int)imageSize.width % (int)imageSize.height == 0);
57     numFrames_ = (int)imageSize.width / (int)imageSize.height;
58     DCHECK(numFrames_);
59     image_.reset([image retain]);
60   }
61   return self;
64 - (BOOL)animationIsComplete {
65   return NO;
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
76             fraction:1.0];
79 - (void)advanceFrame {
80   animationFrame_ = ++animationFrame_ % numFrames_;
83 @end
85 @interface ThrobberToastDelegate : NSObject
86                                    <ThrobberDataDelegate> {
87   base::scoped_nsobject<NSImage> image1_;
88   base::scoped_nsobject<NSImage> image2_;
89   NSSize image1Size_;
90   NSSize image2Size_;
91   int animationFrame_;  // Current frame of the animation,
94 - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2;
96 @end
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];
106     animationFrame_ = 0;
107   }
108   return self;
111 - (BOOL)animationIsComplete {
112   if (animationFrame_ >= image1Size_.height + image2Size_.height)
113     return YES;
115   return NO;
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;
122   NSSize srcSize;
123   NSRect destRect;
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
132   } else {
133     image = image2_.get();
134     srcSize = image2Size_;
135     destRect = NSMakeRect(0, animationFrame_ -
136                                  (image1Size_.height + image2Size_.height),
137                           image2Size_.width, image2Size_.height);
138   }
140   if (image) {
141     NSRect sourceImageRect =
142         NSMakeRect(0, 0, srcSize.width, srcSize.height);
143     [image drawInRect:destRect
144              fromRect:sourceImageRect
145             operation:NSCompositeSourceOver
146              fraction:1.0];
147   }
150 - (void)advanceFrame {
151   ++animationFrame_;
154 @end
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 {
161  @private
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.
168   NSTimer* timer_;
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.
176   BOOL timerRunning_;
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
180   // be performed on.
181   NSThread* validThread_;
184 // Returns a shared ThrobberTimer.  Everyone is expected to use the same
185 // instance.
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
190 // this object.
191 - (void)invalidate;
193 // Adds or removes ThrobberView objects from the throbbers_ set.
194 - (void)addThrobber:(ThrobberView*)throbber;
195 - (void)removeThrobber:(ThrobberView*)throbber;
196 @end
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;
205 @end
207 @implementation ThrobberTimer
208 - (id)init {
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
213     // start firing.
214     timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationIntervalSeconds
215                                               target:self
216                                             selector:@selector(fire:)
217                                             userInfo:nil
218                                              repeats:YES];
219     [timer_ setFireDate:[NSDate distantFuture]];
220     timerRunning_ = NO;
222     validThread_ = [NSThread currentThread];
223   }
224   return self;
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;
233 - (void)invalidate {
234   [timer_ invalidate];
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)
254     return;
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
258   // in the future.
259   NSDate* fireDate;
260   if (newRunning)
261     fireDate = [NSDate dateWithTimeIntervalSinceNow:kAnimationIntervalSeconds];
262   else
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()) {
278     ++next;
279     ThrobberView* throbber = *current;
280     [throbber animate];
281     current = next;
282   }
284 @end
286 @implementation ThrobberView
288 + (id)filmstripThrobberViewWithFrame:(NSRect)frame
289                                image:(NSImage*)image {
290   ThrobberFilmstripDelegate* delegate =
291       [[[ThrobberFilmstripDelegate alloc] initWithImage:image] autorelease];
292   if (!delegate)
293     return nil;
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];
305   if (!delegate)
306     return nil;
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];
315   }
316   return self;
319 - (void)dealloc {
320   [dataDelegate_ release];
321   [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
323   [super dealloc];
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
328 // running.
329 - (void)maintainTimer {
330   ThrobberTimer* throbberTimer = [ThrobberTimer sharedThrobberTimer];
332   if ([self window] && ![self isHidden] && ![dataDelegate_ animationIsComplete])
333     [throbberTimer addThrobber:self];
334   else
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];
348   [super viewDidHide];
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.
359 - (void)animate {
360   [dataDelegate_ advanceFrame];
361   [self setNeedsDisplay:YES];
363   if ([dataDelegate_ animationIsComplete]) {
364     [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
365   }
368 // Overridden to draw the appropriate frame in the image strip.
369 - (void)drawRect:(NSRect)rect {
370   [dataDelegate_ drawFrameInRect:[self bounds]];
373 @end