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