Home | History | Annotate | Download | only in tabs
      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 
     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
    373