ThrobberView.m 11 KB

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