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 #include "base/mac/scoped_nsobject.h"
     11 
     12 static 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 
    156 typedef 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