summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/throbber_view.mm
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/ui/cocoa/throbber_view.mm')
-rw-r--r--chrome/browser/ui/cocoa/throbber_view.mm372
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