| OLD | NEW |
| (Empty) |
| 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/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 | |
| OLD | NEW |