1// Copyright (c) 2009 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/ui/cocoa/tabs/throbber_view.h" 6 7#include <set> 8 9#include "base/logging.h" 10#include "base/mac/scoped_nsobject.h" 11 12static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows 13 14@interface ThrobberView(PrivateMethods) 15- (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate; 16- (void)maintainTimer; 17- (void)animate; 18@end 19 20@protocol ThrobberDataDelegate <NSObject> 21// Is the current frame the last frame of the animation? 22- (BOOL)animationIsComplete; 23 24// Draw the current frame into the current graphics context. 25- (void)drawFrameInRect:(NSRect)rect; 26 27// Update the frame counter. 28- (void)advanceFrame; 29@end 30 31@interface ThrobberFilmstripDelegate : NSObject 32 <ThrobberDataDelegate> { 33 base::scoped_nsobject<NSImage> image_; 34 unsigned int numFrames_; // Number of frames in this animation. 35 unsigned int animationFrame_; // Current frame of the animation, 36 // [0..numFrames_) 37} 38 39- (id)initWithImage:(NSImage*)image; 40 41@end 42 43@implementation ThrobberFilmstripDelegate 44 45- (id)initWithImage:(NSImage*)image { 46 if ((self = [super init])) { 47 // Reset the animation counter so there's no chance we are off the end. 48 animationFrame_ = 0; 49 50 // Ensure that the height divides evenly into the width. Cache the 51 // number of frames in the animation for later. 52 NSSize imageSize = [image size]; 53 DCHECK(imageSize.height && imageSize.width); 54 if (!imageSize.height) 55 return nil; 56 DCHECK((int)imageSize.width % (int)imageSize.height == 0); 57 numFrames_ = (int)imageSize.width / (int)imageSize.height; 58 DCHECK(numFrames_); 59 image_.reset([image retain]); 60 } 61 return self; 62} 63 64- (BOOL)animationIsComplete { 65 return NO; 66} 67 68- (void)drawFrameInRect:(NSRect)rect { 69 float imageDimension = [image_ size].height; 70 float xOffset = animationFrame_ * imageDimension; 71 NSRect sourceImageRect = 72 NSMakeRect(xOffset, 0, imageDimension, imageDimension); 73 [image_ drawInRect:rect 74 fromRect:sourceImageRect 75 operation:NSCompositeSourceOver 76 fraction:1.0]; 77} 78 79- (void)advanceFrame { 80 animationFrame_ = ++animationFrame_ % numFrames_; 81} 82 83@end 84 85@interface ThrobberToastDelegate : NSObject 86 <ThrobberDataDelegate> { 87 base::scoped_nsobject<NSImage> image1_; 88 base::scoped_nsobject<NSImage> image2_; 89 NSSize image1Size_; 90 NSSize image2Size_; 91 int animationFrame_; // Current frame of the animation, 92} 93 94- (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2; 95 96@end 97 98@implementation ThrobberToastDelegate 99 100- (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2 { 101 if ((self = [super init])) { 102 image1_.reset([image1 retain]); 103 image2_.reset([image2 retain]); 104 image1Size_ = [image1 size]; 105 image2Size_ = [image2 size]; 106 animationFrame_ = 0; 107 } 108 return self; 109} 110 111- (BOOL)animationIsComplete { 112 if (animationFrame_ >= image1Size_.height + image2Size_.height) 113 return YES; 114 115 return NO; 116} 117 118// From [0..image1Height) we draw image1, at image1Height we draw nothing, and 119// from [image1Height+1..image1Hight+image2Height] we draw the second image. 120- (void)drawFrameInRect:(NSRect)rect { 121 NSImage* image = nil; 122 NSSize srcSize; 123 NSRect destRect; 124 125 if (animationFrame_ < image1Size_.height) { 126 image = image1_.get(); 127 srcSize = image1Size_; 128 destRect = NSMakeRect(0, -animationFrame_, 129 image1Size_.width, image1Size_.height); 130 } else if (animationFrame_ == image1Size_.height) { 131 // nothing; intermediate blank frame 132 } else { 133 image = image2_.get(); 134 srcSize = image2Size_; 135 destRect = NSMakeRect(0, animationFrame_ - 136 (image1Size_.height + image2Size_.height), 137 image2Size_.width, image2Size_.height); 138 } 139 140 if (image) { 141 NSRect sourceImageRect = 142 NSMakeRect(0, 0, srcSize.width, srcSize.height); 143 [image drawInRect:destRect 144 fromRect:sourceImageRect 145 operation:NSCompositeSourceOver 146 fraction:1.0]; 147 } 148} 149 150- (void)advanceFrame { 151 ++animationFrame_; 152} 153 154@end 155 156typedef std::set<ThrobberView*> ThrobberSet; 157 158// ThrobberTimer manages the animation of a set of ThrobberViews. It allows 159// a single timer instance to be shared among as many ThrobberViews as needed. 160@interface ThrobberTimer : NSObject { 161 @private 162 // A set of weak references to each ThrobberView that should be notified 163 // whenever the timer fires. 164 ThrobberSet throbbers_; 165 166 // Weak reference to the timer that calls back to this object. The timer 167 // retains this object. 168 NSTimer* timer_; 169 170 // Whether the timer is actively running. To avoid timer construction 171 // and destruction overhead, the timer is not invalidated when it is not 172 // needed, but its next-fire date is set to [NSDate distantFuture]. 173 // It is not possible to determine whether the timer has been suspended by 174 // comparing its fireDate to [NSDate distantFuture], though, so a separate 175 // variable is used to track this state. 176 BOOL timerRunning_; 177 178 // The thread that created this object. Used to validate that ThrobberViews 179 // are only added and removed on the same thread that the fire action will 180 // be performed on. 181 NSThread* validThread_; 182} 183 184// Returns a shared ThrobberTimer. Everyone is expected to use the same 185// instance. 186+ (ThrobberTimer*)sharedThrobberTimer; 187 188// Invalidates the timer, which will cause it to remove itself from the run 189// loop. This causes the timer to be released, and it should then release 190// this object. 191- (void)invalidate; 192 193// Adds or removes ThrobberView objects from the throbbers_ set. 194- (void)addThrobber:(ThrobberView*)throbber; 195- (void)removeThrobber:(ThrobberView*)throbber; 196@end 197 198@interface ThrobberTimer(PrivateMethods) 199// Starts or stops the timer as needed as ThrobberViews are added and removed 200// from the throbbers_ set. 201- (void)maintainTimer; 202 203// Calls animate on each ThrobberView in the throbbers_ set. 204- (void)fire:(NSTimer*)timer; 205@end 206 207@implementation ThrobberTimer 208- (id)init { 209 if ((self = [super init])) { 210 // Start out with a timer that fires at the appropriate interval, but 211 // prevent it from firing by setting its next-fire date to the distant 212 // future. Once a ThrobberView is added, the timer will be allowed to 213 // start firing. 214 timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationIntervalSeconds 215 target:self 216 selector:@selector(fire:) 217 userInfo:nil 218 repeats:YES]; 219 [timer_ setFireDate:[NSDate distantFuture]]; 220 timerRunning_ = NO; 221 222 validThread_ = [NSThread currentThread]; 223 } 224 return self; 225} 226 227+ (ThrobberTimer*)sharedThrobberTimer { 228 // Leaked. That's OK, it's scoped to the lifetime of the application. 229 static ThrobberTimer* sharedInstance = [[ThrobberTimer alloc] init]; 230 return sharedInstance; 231} 232 233- (void)invalidate { 234 [timer_ invalidate]; 235} 236 237- (void)addThrobber:(ThrobberView*)throbber { 238 DCHECK([NSThread currentThread] == validThread_); 239 throbbers_.insert(throbber); 240 [self maintainTimer]; 241} 242 243- (void)removeThrobber:(ThrobberView*)throbber { 244 DCHECK([NSThread currentThread] == validThread_); 245 throbbers_.erase(throbber); 246 [self maintainTimer]; 247} 248 249- (void)maintainTimer { 250 BOOL oldRunning = timerRunning_; 251 BOOL newRunning = throbbers_.empty() ? NO : YES; 252 253 if (oldRunning == newRunning) 254 return; 255 256 // To start the timer, set its next-fire date to an appropriate interval from 257 // now. To suspend the timer, set its next-fire date to a preposterous time 258 // in the future. 259 NSDate* fireDate; 260 if (newRunning) 261 fireDate = [NSDate dateWithTimeIntervalSinceNow:kAnimationIntervalSeconds]; 262 else 263 fireDate = [NSDate distantFuture]; 264 265 [timer_ setFireDate:fireDate]; 266 timerRunning_ = newRunning; 267} 268 269- (void)fire:(NSTimer*)timer { 270 // The call to [throbber animate] may result in the ThrobberView calling 271 // removeThrobber: if it decides it's done animating. That would invalidate 272 // the iterator, making it impossible to correctly get to the next element 273 // in the set. To prevent that from happening, a second iterator is used 274 // and incremented before calling [throbber animate]. 275 ThrobberSet::const_iterator current = throbbers_.begin(); 276 ThrobberSet::const_iterator next = current; 277 while (current != throbbers_.end()) { 278 ++next; 279 ThrobberView* throbber = *current; 280 [throbber animate]; 281 current = next; 282 } 283} 284@end 285 286@implementation ThrobberView 287 288+ (id)filmstripThrobberViewWithFrame:(NSRect)frame 289 image:(NSImage*)image { 290 ThrobberFilmstripDelegate* delegate = 291 [[[ThrobberFilmstripDelegate alloc] initWithImage:image] autorelease]; 292 if (!delegate) 293 return nil; 294 295 return [[[ThrobberView alloc] initWithFrame:frame 296 delegate:delegate] autorelease]; 297} 298 299+ (id)toastThrobberViewWithFrame:(NSRect)frame 300 beforeImage:(NSImage*)beforeImage 301 afterImage:(NSImage*)afterImage { 302 ThrobberToastDelegate* delegate = 303 [[[ThrobberToastDelegate alloc] initWithImage1:beforeImage 304 image2:afterImage] autorelease]; 305 if (!delegate) 306 return nil; 307 308 return [[[ThrobberView alloc] initWithFrame:frame 309 delegate:delegate] autorelease]; 310} 311 312- (id)initWithFrame:(NSRect)frame delegate:(id<ThrobberDataDelegate>)delegate { 313 if ((self = [super initWithFrame:frame])) { 314 dataDelegate_ = [delegate retain]; 315 } 316 return self; 317} 318 319- (void)dealloc { 320 [dataDelegate_ release]; 321 [[ThrobberTimer sharedThrobberTimer] removeThrobber:self]; 322 323 [super dealloc]; 324} 325 326// Manages this ThrobberView's membership in the shared throbber timer set on 327// the basis of its visibility and whether its animation needs to continue 328// running. 329- (void)maintainTimer { 330 ThrobberTimer* throbberTimer = [ThrobberTimer sharedThrobberTimer]; 331 332 if ([self window] && ![self isHidden] && ![dataDelegate_ animationIsComplete]) 333 [throbberTimer addThrobber:self]; 334 else 335 [throbberTimer removeThrobber:self]; 336} 337 338// A ThrobberView added to a window may need to begin animating; a ThrobberView 339// removed from a window should stop. 340- (void)viewDidMoveToWindow { 341 [self maintainTimer]; 342 [super viewDidMoveToWindow]; 343} 344 345// A hidden ThrobberView should stop animating. 346- (void)viewDidHide { 347 [self maintainTimer]; 348 [super viewDidHide]; 349} 350 351// A visible ThrobberView may need to start animating. 352- (void)viewDidUnhide { 353 [self maintainTimer]; 354 [super viewDidUnhide]; 355} 356 357// Called when the timer fires. Advance the frame, dirty the display, and remove 358// the throbber if it's no longer needed. 359- (void)animate { 360 [dataDelegate_ advanceFrame]; 361 [self setNeedsDisplay:YES]; 362 363 if ([dataDelegate_ animationIsComplete]) { 364 [[ThrobberTimer sharedThrobberTimer] removeThrobber:self]; 365 } 366} 367 368// Overridden to draw the appropriate frame in the image strip. 369- (void)drawRect:(NSRect)rect { 370 [dataDelegate_ drawFrameInRect:[self bounds]]; 371} 372 373@end 374