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 11 static 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 155 typedef 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