• 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#include "base/mac/scoped_nsobject.h"
11
12static const float kAnimationIntervalSeconds = 0.03;  // 30ms, same as windows
13
14@interface ThrobberView(PrivateMethods)
15- (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate;
16- (void)maintainTimer;
17- (void)animate;
18@end
19
20@protocol ThrobberDataDelegate <NSObject>
21// Is the current frame the last frame of the animation?
22- (BOOL)animationIsComplete;
23
24// Draw the current frame into the current graphics context.
25- (void)drawFrameInRect:(NSRect)rect;
26
27// Update the frame counter.
28- (void)advanceFrame;
29@end
30
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_)
37}
38
39- (id)initWithImage:(NSImage*)image;
40
41@end
42
43@implementation ThrobberFilmstripDelegate
44
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;
49
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;
62}
63
64- (BOOL)animationIsComplete {
65  return NO;
66}
67
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];
77}
78
79- (void)advanceFrame {
80  animationFrame_ = ++animationFrame_ % numFrames_;
81}
82
83@end
84
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,
92}
93
94- (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2;
95
96@end
97
98@implementation ThrobberToastDelegate
99
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;
109}
110
111- (BOOL)animationIsComplete {
112  if (animationFrame_ >= image1Size_.height + image2Size_.height)
113    return YES;
114
115  return NO;
116}
117
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;
124
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  }
139
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  }
148}
149
150- (void)advanceFrame {
151  ++animationFrame_;
152}
153
154@end
155
156typedef std::set<ThrobberView*> ThrobberSet;
157
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_;
165
166  // Weak reference to the timer that calls back to this object.  The timer
167  // retains this object.
168  NSTimer* timer_;
169
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_;
177
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_;
182}
183
184// Returns a shared ThrobberTimer.  Everyone is expected to use the same
185// instance.
186+ (ThrobberTimer*)sharedThrobberTimer;
187
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;
192
193// Adds or removes ThrobberView objects from the throbbers_ set.
194- (void)addThrobber:(ThrobberView*)throbber;
195- (void)removeThrobber:(ThrobberView*)throbber;
196@end
197
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;
202
203// Calls animate on each ThrobberView in the throbbers_ set.
204- (void)fire:(NSTimer*)timer;
205@end
206
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;
221
222    validThread_ = [NSThread currentThread];
223  }
224  return self;
225}
226
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;
231}
232
233- (void)invalidate {
234  [timer_ invalidate];
235}
236
237- (void)addThrobber:(ThrobberView*)throbber {
238  DCHECK([NSThread currentThread] == validThread_);
239  throbbers_.insert(throbber);
240  [self maintainTimer];
241}
242
243- (void)removeThrobber:(ThrobberView*)throbber {
244  DCHECK([NSThread currentThread] == validThread_);
245  throbbers_.erase(throbber);
246  [self maintainTimer];
247}
248
249- (void)maintainTimer {
250  BOOL oldRunning = timerRunning_;
251  BOOL newRunning = throbbers_.empty() ? NO : YES;
252
253  if (oldRunning == newRunning)
254    return;
255
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];
264
265  [timer_ setFireDate:fireDate];
266  timerRunning_ = newRunning;
267}
268
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  }
283}
284@end
285
286@implementation ThrobberView
287
288+ (id)filmstripThrobberViewWithFrame:(NSRect)frame
289                               image:(NSImage*)image {
290  ThrobberFilmstripDelegate* delegate =
291      [[[ThrobberFilmstripDelegate alloc] initWithImage:image] autorelease];
292  if (!delegate)
293    return nil;
294
295  return [[[ThrobberView alloc] initWithFrame:frame
296                                     delegate:delegate] autorelease];
297}
298
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;
307
308  return [[[ThrobberView alloc] initWithFrame:frame
309                                     delegate:delegate] autorelease];
310}
311
312- (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate {
313  if ((self = [super initWithFrame:frame])) {
314    dataDelegate_ = [delegate retain];
315  }
316  return self;
317}
318
319- (void)dealloc {
320  [dataDelegate_ release];
321  [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
322
323  [super dealloc];
324}
325
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];
331
332  if ([self window] && ![self isHidden] && ![dataDelegate_ animationIsComplete])
333    [throbberTimer addThrobber:self];
334  else
335    [throbberTimer removeThrobber:self];
336}
337
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];
343}
344
345// A hidden ThrobberView should stop animating.
346- (void)viewDidHide {
347  [self maintainTimer];
348  [super viewDidHide];
349}
350
351// A visible ThrobberView may need to start animating.
352- (void)viewDidUnhide {
353  [self maintainTimer];
354  [super viewDidUnhide];
355}
356
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];
362
363  if ([dataDelegate_ animationIsComplete]) {
364    [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
365  }
366}
367
368// Overridden to draw the appropriate frame in the image strip.
369- (void)drawRect:(NSRect)rect {
370  [dataDelegate_ drawFrameInRect:[self bounds]];
371}
372
373@end
374