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 12 static 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 156 typedef 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