blob: 0d838a3a43d6788c06a9671ed955e9a650657abb [file] [log] [blame]
// 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 <set>
#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<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> {
base::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> {
base::scoped_nsobject<NSImage> image1_;
base::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