// 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 file. #import "chrome/browser/ui/cocoa/tabs/throbber_view.h" #include #include "base/logging.h" #include "base/mac/scoped_nsobject.h" static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows @interface ThrobberView(PrivateMethods) - (id)initWithFrame:(NSRect)frame delegate:(id)delegate; - (void)maintainTimer; - (void)animate; @end @protocol ThrobberDataDelegate // 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 { base::scoped_nsobject image_; unsigned int numFrames_; // Number of frames in this animation. unsigned int animationFrame_; // Current frame of the animation, // [0..numFrames_) } - (id)initWithImage:(NSImage*)image; @end @implementation ThrobberFilmstripDelegate - (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]; DCHECK(imageSize.height && imageSize.width); if (!imageSize.height) return nil; DCHECK((int)imageSize.width % (int)imageSize.height == 0); numFrames_ = (int)imageSize.width / (int)imageSize.height; DCHECK(numFrames_); image_.reset([image retain]); } 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 { base::scoped_nsobject image1_; base::scoped_nsobject image2_; NSSize image1Size_; NSSize image2Size_; int animationFrame_; // Current frame of the animation, } - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2; @end @implementation ThrobberToastDelegate - (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2 { if ((self = [super init])) { image1_.reset([image1 retain]); image2_.reset([image2 retain]); 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_.get(); srcSize = image1Size_; destRect = NSMakeRect(0, -animationFrame_, image1Size_.width, image1Size_.height); } else if (animationFrame_ == image1Size_.height) { // nothing; intermediate blank frame } else { image = image2_.get(); 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 typedef std::set ThrobberSet; // 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 { @private // A set of weak references to each ThrobberView that should be notified // whenever the timer fires. ThrobberSet 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_; } // 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 @implementation ThrobberTimer - (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]; } return self; } + (ThrobberTimer*)sharedThrobberTimer { // Leaked. That's OK, it's scoped to the lifetime of the application. static ThrobberTimer* sharedInstance = [[ThrobberTimer alloc] init]; return sharedInstance; } - (void)invalidate { [timer_ invalidate]; } - (void)addThrobber:(ThrobberView*)throbber { DCHECK([NSThread currentThread] == validThread_); throbbers_.insert(throbber); [self maintainTimer]; } - (void)removeThrobber:(ThrobberView*)throbber { DCHECK([NSThread currentThread] == validThread_); throbbers_.erase(throbber); [self maintainTimer]; } - (void)maintainTimer { BOOL oldRunning = timerRunning_; BOOL newRunning = throbbers_.empty() ? 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; } } @end @implementation ThrobberView + (id)filmstripThrobberViewWithFrame:(NSRect)frame image:(NSImage*)image { ThrobberFilmstripDelegate* delegate = [[[ThrobberFilmstripDelegate alloc] initWithImage:image] autorelease]; if (!delegate) return nil; return [[[ThrobberView alloc] initWithFrame:frame delegate:delegate] autorelease]; } + (id)toastThrobberViewWithFrame:(NSRect)frame beforeImage:(NSImage*)beforeImage afterImage:(NSImage*)afterImage { ThrobberToastDelegate* delegate = [[[ThrobberToastDelegate alloc] initWithImage1:beforeImage image2:afterImage] autorelease]; if (!delegate) return nil; return [[[ThrobberView alloc] initWithFrame:frame delegate:delegate] autorelease]; } - (id)initWithFrame:(NSRect)frame delegate:(id)delegate { if ((self = [super initWithFrame:frame])) { dataDelegate_ = [delegate retain]; } return self; } - (void)dealloc { [dataDelegate_ release]; [[ThrobberTimer sharedThrobberTimer] removeThrobber:self]; [super dealloc]; } // 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