123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- // Copyright (c) 2009 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE-chromium file.
- #import "ThrobberView.h"
- static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows
- @interface ThrobberView (PrivateMethods)
- - (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate;
- - (void)maintainTimer;
- - (void)animate;
- @end
- @protocol ThrobberDataDelegate <NSObject>
- // Is the current frame the last frame of the animation?
- - (BOOL)animationIsComplete;
- // Draw the current frame into the current graphics context.
- - (void)drawFrameInRect:(NSRect)rect;
- // Update the frame counter.
- - (void)advanceFrame;
- @end
- @interface ThrobberFilmstripDelegate : NSObject <ThrobberDataDelegate>
- - (id)initWithImage:(NSImage*)image;
- @end
- @implementation ThrobberFilmstripDelegate {
- NSImage* image_;
- unsigned int numFrames_; // Number of frames in this animation.
- unsigned int animationFrame_; // Current frame of the animation,
- // [0..numFrames_)
- }
- - (id)initWithImage:(NSImage*)image {
- if ((self = [super init])) {
- // Reset the animation counter so there's no chance we are off the end.
- animationFrame_ = 0;
-
- // Ensure that the height divides evenly into the width. Cache the
- // number of frames in the animation for later.
- NSSize imageSize = [image size];
- assert(imageSize.height && imageSize.width);
- if (!imageSize.height)
- return nil;
- assert((int)imageSize.width % (int)imageSize.height == 0);
- numFrames_ = (int)imageSize.width / (int)imageSize.height;
- assert(numFrames_);
- image_ = image;
- }
- return self;
- }
- - (BOOL)animationIsComplete {
- return NO;
- }
- - (void)drawFrameInRect:(NSRect)rect {
- float imageDimension = [image_ size].height;
- float xOffset = animationFrame_ * imageDimension;
- NSRect sourceImageRect =
- NSMakeRect(xOffset, 0, imageDimension, imageDimension);
- [image_ drawInRect:rect
- fromRect:sourceImageRect
- operation:NSCompositeSourceOver
- fraction:1.0];
- }
- - (void)advanceFrame {
- animationFrame_ = ++animationFrame_ % numFrames_;
- }
- @end
- @interface ThrobberToastDelegate : NSObject <ThrobberDataDelegate>
- - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2;
- @end
- @implementation ThrobberToastDelegate {
- NSImage* image1_;
- NSImage* image2_;
- NSSize image1Size_;
- NSSize image2Size_;
- int animationFrame_; // Current frame of the animation,
- }
- - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2 {
- if ((self = [super init])) {
- image1_ = image1;
- image2_ = image2;
- image1Size_ = [image1 size];
- image2Size_ = [image2 size];
- animationFrame_ = 0;
- }
- return self;
- }
- - (BOOL)animationIsComplete {
- if (animationFrame_ >= image1Size_.height + image2Size_.height)
- return YES;
-
- return NO;
- }
- // From [0..image1Height) we draw image1, at image1Height we draw nothing, and
- // from [image1Height+1..image1Hight+image2Height] we draw the second image.
- - (void)drawFrameInRect:(NSRect)rect {
- NSImage* image = nil;
- NSSize srcSize;
- NSRect destRect;
-
- if (animationFrame_ < image1Size_.height) {
- image = image1_;
- srcSize = image1Size_;
- destRect = NSMakeRect(0, -animationFrame_,
- image1Size_.width, image1Size_.height);
- } else if (animationFrame_ == image1Size_.height) {
- // nothing; intermediate blank frame
- } else {
- image = image2_;
- srcSize = image2Size_;
- destRect = NSMakeRect(0, animationFrame_ -
- (image1Size_.height + image2Size_.height),
- image2Size_.width, image2Size_.height);
- }
-
- if (image) {
- NSRect sourceImageRect =
- NSMakeRect(0, 0, srcSize.width, srcSize.height);
- [image drawInRect:destRect
- fromRect:sourceImageRect
- operation:NSCompositeSourceOver
- fraction:1.0];
- }
- }
- - (void)advanceFrame {
- ++animationFrame_;
- }
- @end
- // ThrobberTimer manages the animation of a set of ThrobberViews. It allows
- // a single timer instance to be shared among as many ThrobberViews as needed.
- @interface ThrobberTimer : NSObject
- // Returns a shared ThrobberTimer. Everyone is expected to use the same
- // instance.
- + (ThrobberTimer*)sharedThrobberTimer;
- // Invalidates the timer, which will cause it to remove itself from the run
- // loop. This causes the timer to be released, and it should then release
- // this object.
- - (void)invalidate;
- // Adds or removes ThrobberView objects from the throbbers_ set.
- - (void)addThrobber:(ThrobberView*)throbber;
- - (void)removeThrobber:(ThrobberView*)throbber;
- @end
- @interface ThrobberTimer(PrivateMethods)
- // Starts or stops the timer as needed as ThrobberViews are added and removed
- // from the throbbers_ set.
- - (void)maintainTimer;
- // Calls animate on each ThrobberView in the throbbers_ set.
- - (void)fire:(NSTimer*)timer;
- @end
- static ThrobberTimer* _sharedThrobberTimer;
- @implementation ThrobberTimer {
- @private
- // A set of weak references to each ThrobberView that should be notified
- // whenever the timer fires.
- NSMutableSet* throbbers_;
-
- // Weak reference to the timer that calls back to this object. The timer
- // retains this object.
- NSTimer* timer_;
-
- // Whether the timer is actively running. To avoid timer construction
- // and destruction overhead, the timer is not invalidated when it is not
- // needed, but its next-fire date is set to [NSDate distantFuture].
- // It is not possible to determine whether the timer has been suspended by
- // comparing its fireDate to [NSDate distantFuture], though, so a separate
- // variable is used to track this state.
- BOOL timerRunning_;
-
- // The thread that created this object. Used to validate that ThrobberViews
- // are only added and removed on the same thread that the fire action will
- // be performed on.
- NSThread* validThread_;
- }
- - (id)init {
- if ((self = [super init])) {
- // Start out with a timer that fires at the appropriate interval, but
- // prevent it from firing by setting its next-fire date to the distant
- // future. Once a ThrobberView is added, the timer will be allowed to
- // start firing.
- timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationIntervalSeconds
- target:self
- selector:@selector(fire:)
- userInfo:nil
- repeats:YES];
- [timer_ setFireDate:[NSDate distantFuture]];
- timerRunning_ = NO;
-
- validThread_ = [NSThread currentThread];
- throbbers_ = [NSMutableSet setWithCapacity:0];
- }
- return self;
- }
- + (ThrobberTimer *)sharedThrobberTimer {
- // Leaked. That's OK, it's scoped to the lifetime of the application.
- // static ThrobberTimer* sharedInstance = [[ThrobberTimer alloc] init];
- if (!_sharedThrobberTimer) {
- _sharedThrobberTimer = [[ThrobberTimer alloc] init];
- }
- return _sharedThrobberTimer;
- }
- - (void)invalidate {
- [timer_ invalidate];
- }
- - (void)addThrobber:(ThrobberView*)throbber {
- assert([NSThread currentThread] == validThread_);
- // throbbers_.insert(throbber);
- [throbbers_ addObject:throbber];
- [self maintainTimer];
- }
- - (void)removeThrobber:(ThrobberView*)throbber {
- assert([NSThread currentThread] == validThread_);
- // throbbers_.erase(throbber);
- [throbbers_ removeObject:throbber];
- [self maintainTimer];
- }
- - (void)maintainTimer {
- BOOL oldRunning = timerRunning_;
- // BOOL newRunning = throbbers_.empty() ? NO : YES;
- BOOL newRunning = [throbbers_ count] == 0 ? NO : YES;
-
- if (oldRunning == newRunning)
- return;
-
- // To start the timer, set its next-fire date to an appropriate interval from
- // now. To suspend the timer, set its next-fire date to a preposterous time
- // in the future.
- NSDate* fireDate;
- if (newRunning)
- fireDate = [NSDate dateWithTimeIntervalSinceNow:kAnimationIntervalSeconds];
- else
- fireDate = [NSDate distantFuture];
-
- [timer_ setFireDate:fireDate];
- timerRunning_ = newRunning;
- }
- - (void)fire:(NSTimer*)timer {
- // The call to [throbber animate] may result in the ThrobberView calling
- // removeThrobber: if it decides it's done animating. That would invalidate
- // the iterator, making it impossible to correctly get to the next element
- // in the set. To prevent that from happening, a second iterator is used
- // and incremented before calling [throbber animate].
- // ThrobberSet::const_iterator current = throbbers_.begin();
- // ThrobberSet::const_iterator next = current;
- // while (current != throbbers_.end()) {
- // ++next;
- // ThrobberView* throbber = *current;
- // [throbber animate];
- // current = next;
- // }
- for (ThrobberView* throbber in throbbers_) {
- [throbber animate];
- }
- }
- @end
- @implementation ThrobberView {
- id<ThrobberDataDelegate> dataDelegate_;
- }
- + (id)filmstripThrobberViewWithFrame:(NSRect)frame
- image:(NSImage*)image {
- ThrobberFilmstripDelegate* delegate =
- [[ThrobberFilmstripDelegate alloc] initWithImage:image];
- if (!delegate)
- return nil;
-
- return [[ThrobberView alloc] initWithFrame:frame
- delegate:delegate];
- }
- + (id)toastThrobberViewWithFrame:(NSRect)frame
- beforeImage:(NSImage*)beforeImage
- afterImage:(NSImage*)afterImage {
- ThrobberToastDelegate* delegate =
- [[ThrobberToastDelegate alloc] initWithImage1:beforeImage
- image2:afterImage];
- if (!delegate)
- return nil;
-
- return [[ThrobberView alloc] initWithFrame:frame
- delegate:delegate];
- }
- - (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate {
- if ((self = [super initWithFrame:frame])) {
- dataDelegate_ = delegate;
- }
- return self;
- }
- - (void)dealloc {
- [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
- }
- // Manages this ThrobberView's membership in the shared throbber timer set on
- // the basis of its visibility and whether its animation needs to continue
- // running.
- - (void)maintainTimer {
- ThrobberTimer* throbberTimer = [ThrobberTimer sharedThrobberTimer];
-
- if ([self window] && ![self isHidden] && ![dataDelegate_ animationIsComplete])
- [throbberTimer addThrobber:self];
- else
- [throbberTimer removeThrobber:self];
- }
- // A ThrobberView added to a window may need to begin animating; a ThrobberView
- // removed from a window should stop.
- - (void)viewDidMoveToWindow {
- [self maintainTimer];
- [super viewDidMoveToWindow];
- }
- // A hidden ThrobberView should stop animating.
- - (void)viewDidHide {
- [self maintainTimer];
- [super viewDidHide];
- }
- // A visible ThrobberView may need to start animating.
- - (void)viewDidUnhide {
- [self maintainTimer];
- [super viewDidUnhide];
- }
- // Called when the timer fires. Advance the frame, dirty the display, and remove
- // the throbber if it's no longer needed.
- - (void)animate {
- [dataDelegate_ advanceFrame];
- [self setNeedsDisplay:YES];
-
- if ([dataDelegate_ animationIsComplete]) {
- [[ThrobberTimer sharedThrobberTimer] removeThrobber:self];
- }
- }
- // Overridden to draw the appropriate frame in the image strip.
- - (void)drawRect:(NSRect)rect {
- [dataDelegate_ drawFrameInRect:[self bounds]];
- }
- @end
|