diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/throbber_view.mm')
-rw-r--r-- | chrome/browser/ui/cocoa/throbber_view.mm | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/throbber_view.mm b/chrome/browser/ui/cocoa/throbber_view.mm new file mode 100644 index 0000000..c0e5dd3 --- /dev/null +++ b/chrome/browser/ui/cocoa/throbber_view.mm @@ -0,0 +1,372 @@ +// 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/throbber_view.h" + +#include <set> + +#include "base/logging.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> { + scoped_nsobject<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; + +@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 + <ThrobberDataDelegate> { + scoped_nsobject<NSImage> image1_; + scoped_nsobject<NSImage> 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<ThrobberView*> 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<ThrobberDataDelegate>)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 |