• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/tabs/throbber_view.h"
6
7#include <set>
8
9#include "base/logging.h"
10
11static 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
155typedef 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
373