1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5#include "nsMacCursor.h"
6#include "nsObjCExceptions.h"
7#include "nsDebug.h"
8#include "nsDirectoryServiceDefs.h"
9#include "nsCOMPtr.h"
10#include "nsIFile.h"
11#include "nsString.h"
12
13/*! @category   nsMacCursor (PrivateMethods)
14    @abstract   Private methods internal to the nsMacCursor class.
15    @discussion <code>nsMacCursor</code> is effectively an abstract class. It does not define
16   complete behaviour in and of itself, the subclasses defined in this file provide the useful
17   implementations.
18*/
19@interface nsMacCursor (PrivateMethods)
20
21/*! @method     getNextCursorFrame
22    @abstract   get the index of the next cursor frame to display.
23    @discussion Increments and returns the frame counter of an animated cursor.
24    @result     The index of the next frame to display in the cursor animation
25*/
26- (int)getNextCursorFrame;
27
28/*! @method     numFrames
29    @abstract   Query the number of frames in this cursor's animation.
30    @discussion Returns the number of frames in this cursor's animation. Static cursors return 1.
31*/
32- (int)numFrames;
33
34/*! @method     createTimer
35    @abstract   Create a Timer to use to animate the cursor.
36    @discussion Creates an instance of <code>NSTimer</code> which is used to drive the cursor
37   animation. This method should only be called for cursors that are animated.
38*/
39- (void)createTimer;
40
41/*! @method     destroyTimer
42    @abstract   Destroy any timer instance associated with this cursor.
43    @discussion Invalidates and releases any <code>NSTimer</code> instance associated with this
44   cursor.
45 */
46- (void)destroyTimer;
47/*! @method     destroyTimer
48    @abstract   Destroy any timer instance associated with this cursor.
49    @discussion Invalidates and releases any <code>NSTimer</code> instance associated with this
50   cursor.
51*/
52
53/*! @method     advanceAnimatedCursor:
54    @abstract   Method called by animation timer to perform animation.
55    @discussion Called by an animated cursor's associated timer to advance the animation to the next
56   frame. Determines which frame should occur next and sets the cursor to that frame.
57    @param      aTimer the timer causing the animation
58*/
59- (void)advanceAnimatedCursor:(NSTimer*)aTimer;
60
61/*! @method     setFrame:
62    @abstract   Sets the current cursor, using an index to determine which frame in the animation to
63   display.
64    @discussion Sets the current cursor. The frame index determines which frame is shown if the
65   cursor is animated. Frames and numbered from <code>0</code> to <code>-[nsMacCursor numFrames] -
66   1</code>. A static cursor has a single frame, numbered 0.
67    @param      aFrameIndex the index indicating which frame from the animation to display
68*/
69- (void)setFrame:(int)aFrameIndex;
70
71@end
72
73/*! @class      nsCocoaCursor
74    @abstract   Implementation of <code>nsMacCursor</code> that uses Cocoa <code>NSCursor</code>
75   instances.
76    @discussion Displays a static or animated cursor, using Cocoa <code>NSCursor</code> instances.
77   These can be either built-in <code>NSCursor</code> instances, or custom <code>NSCursor</code>s
78   created from images. When more than one <code>NSCursor</code> is provided, the cursor will use
79   these as animation frames.
80*/
81@interface nsCocoaCursor : nsMacCursor {
82 @private
83  NSArray* mFrames;
84  NSCursor* mLastSetCocoaCursor;
85}
86
87/*! @method     initWithFrames:
88    @abstract   Create an animated cursor by specifying the frames to use for the animation.
89    @discussion Creates a cursor that will animate by cycling through the given frames. Each element
90   of the array must be an instance of <code>NSCursor</code>
91    @param      aCursorFrames an array of <code>NSCursor</code>, representing the frames of an
92   animated cursor, in the order they should be played.
93    @param      aType the corresponding <code>nsCursor</code> constant
94    @result     an instance of <code>nsCocoaCursor</code> that will animate the given cursor frames
95 */
96- (id)initWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType;
97
98/*! @method     initWithCursor:
99    @abstract   Create a cursor by specifying a Cocoa <code>NSCursor</code>.
100    @discussion Creates a cursor representing the given Cocoa built-in cursor.
101    @param      aCursor the <code>NSCursor</code> to use
102    @param      aType the corresponding <code>nsCursor</code> constant
103    @result     an instance of <code>nsCocoaCursor</code> representing the given
104   <code>NSCursor</code>
105*/
106- (id)initWithCursor:(NSCursor*)aCursor type:(nsCursor)aType;
107
108/*! @method     initWithImageNamed:hotSpot:
109    @abstract   Create a cursor by specifying the name of an image resource to use for the cursor
110   and a hotspot.
111    @discussion Creates a cursor by loading the named image using the <code>+[NSImage
112   imageNamed:]</code> method. <p>The image must be compatible with any restrictions laid down by
113   <code>NSCursor</code>. These vary by operating system version.</p> <p>The hotspot precisely
114   determines the point where the user clicks when using the cursor.</p>
115    @param      aCursor the name of the image to use for the cursor
116    @param      aPoint the point within the cursor to use as the hotspot
117    @param      aType the corresponding <code>nsCursor</code> constant
118    @result     an instance of <code>nsCocoaCursor</code> that uses the given image and hotspot
119*/
120- (id)initWithImageNamed:(NSString*)aCursorImage hotSpot:(NSPoint)aPoint type:(nsCursor)aType;
121
122@end
123
124@implementation nsMacCursor
125
126+ (nsMacCursor*)cursorWithCursor:(NSCursor*)aCursor type:(nsCursor)aType {
127  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
128
129  return [[[nsCocoaCursor alloc] initWithCursor:aCursor type:aType] autorelease];
130
131  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
132}
133
134+ (nsMacCursor*)cursorWithImageNamed:(NSString*)aCursorImage
135                             hotSpot:(NSPoint)aPoint
136                                type:(nsCursor)aType {
137  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
138
139  return [[[nsCocoaCursor alloc] initWithImageNamed:aCursorImage hotSpot:aPoint
140                                               type:aType] autorelease];
141
142  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
143}
144
145+ (nsMacCursor*)cursorWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType {
146  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
147
148  return [[[nsCocoaCursor alloc] initWithFrames:aCursorFrames type:aType] autorelease];
149
150  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
151}
152
153+ (NSCursor*)cocoaCursorWithImageNamed:(NSString*)imageName hotSpot:(NSPoint)aPoint {
154  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
155
156  nsCOMPtr<nsIFile> resDir;
157  nsAutoCString resPath;
158  NSString *pathToImage, *pathToHiDpiImage;
159  NSImage *cursorImage, *hiDpiCursorImage;
160
161  nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(resDir));
162  if (NS_FAILED(rv)) goto INIT_FAILURE;
163  resDir->AppendNative("res"_ns);
164  resDir->AppendNative("cursors"_ns);
165
166  rv = resDir->GetNativePath(resPath);
167  if (NS_FAILED(rv)) goto INIT_FAILURE;
168
169  pathToImage = [NSString stringWithUTF8String:(const char*)resPath.get()];
170  if (!pathToImage) goto INIT_FAILURE;
171  pathToImage = [pathToImage stringByAppendingPathComponent:imageName];
172  pathToHiDpiImage = [pathToImage stringByAppendingString:@"@2x"];
173  // Add same extension to both image paths.
174  pathToImage = [pathToImage stringByAppendingPathExtension:@"png"];
175  pathToHiDpiImage = [pathToHiDpiImage stringByAppendingPathExtension:@"png"];
176
177  cursorImage = [[[NSImage alloc] initWithContentsOfFile:pathToImage] autorelease];
178  if (!cursorImage) goto INIT_FAILURE;
179
180  // Note 1: There are a few different ways to get a hidpi image via
181  // initWithContentsOfFile. We let the OS handle this here: when the
182  // file basename ends in "@2x", it will be displayed at native resolution
183  // instead of being pixel-doubled. See bug 784909 comment 7 for alternates ways.
184  //
185  // Note 2: The OS is picky, and will ignore the hidpi representation
186  // unless it is exactly twice the size of the lowdpi image.
187  hiDpiCursorImage = [[[NSImage alloc] initWithContentsOfFile:pathToHiDpiImage] autorelease];
188  if (hiDpiCursorImage) {
189    NSImageRep* imageRep = [[hiDpiCursorImage representations] objectAtIndex:0];
190    [cursorImage addRepresentation:imageRep];
191  }
192  return [[[NSCursor alloc] initWithImage:cursorImage hotSpot:aPoint] autorelease];
193
194INIT_FAILURE:
195  NS_WARNING("Problem getting path to cursor image file!");
196  [self release];
197  return nil;
198
199  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
200}
201
202- (BOOL)isSet {
203  // implemented by subclasses
204  return NO;
205}
206
207- (void)set {
208  if ([self isAnimated]) {
209    [self createTimer];
210  }
211  // if the cursor isn't animated or the timer creation fails for any reason...
212  if (!mTimer) {
213    [self setFrame:0];
214  }
215}
216
217- (void)unset {
218  [self destroyTimer];
219}
220
221- (BOOL)isAnimated {
222  return [self numFrames] > 1;
223}
224
225- (int)numFrames {
226  // subclasses need to override this to support animation
227  return 1;
228}
229
230- (int)getNextCursorFrame {
231  mFrameCounter = (mFrameCounter + 1) % [self numFrames];
232  return mFrameCounter;
233}
234
235- (void)createTimer {
236  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
237
238  if (!mTimer) {
239    mTimer = [[NSTimer scheduledTimerWithTimeInterval:0.25
240                                               target:self
241                                             selector:@selector(advanceAnimatedCursor:)
242                                             userInfo:nil
243                                              repeats:YES] retain];
244  }
245
246  NS_OBJC_END_TRY_IGNORE_BLOCK;
247}
248
249- (void)destroyTimer {
250  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
251
252  if (mTimer) {
253    [mTimer invalidate];
254    [mTimer release];
255    mTimer = nil;
256  }
257
258  NS_OBJC_END_TRY_IGNORE_BLOCK;
259}
260
261- (void)advanceAnimatedCursor:(NSTimer*)aTimer {
262  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
263
264  if ([aTimer isValid]) {
265    [self setFrame:[self getNextCursorFrame]];
266  }
267
268  NS_OBJC_END_TRY_IGNORE_BLOCK;
269}
270
271- (void)setFrame:(int)aFrameIndex {
272  // subclasses need to do something useful here
273}
274
275- (nsCursor)type {
276  return mType;
277}
278
279- (void)dealloc {
280  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
281
282  [self destroyTimer];
283  [super dealloc];
284
285  NS_OBJC_END_TRY_IGNORE_BLOCK;
286}
287
288@end
289
290@implementation nsCocoaCursor
291
292- (id)initWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType {
293  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
294
295  self = [super init];
296  NSEnumerator* it = [aCursorFrames objectEnumerator];
297  NSObject* frame = nil;
298  while ((frame = [it nextObject])) {
299    NS_ASSERTION([frame isKindOfClass:[NSCursor class]],
300                 "Invalid argument: All frames must be of type NSCursor");
301  }
302  mFrames = [aCursorFrames retain];
303  mFrameCounter = 0;
304  mType = aType;
305  return self;
306
307  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
308}
309
310- (id)initWithCursor:(NSCursor*)aCursor type:(nsCursor)aType {
311  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
312
313  NSArray* frame = [NSArray arrayWithObjects:aCursor, nil];
314  return [self initWithFrames:frame type:aType];
315
316  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
317}
318
319- (id)initWithImageNamed:(NSString*)aCursorImage hotSpot:(NSPoint)aPoint type:(nsCursor)aType {
320  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
321
322  return [self initWithCursor:[nsMacCursor cocoaCursorWithImageNamed:aCursorImage hotSpot:aPoint]
323                         type:aType];
324
325  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
326}
327
328- (BOOL)isSet {
329  return [NSCursor currentCursor] == mLastSetCocoaCursor;
330}
331
332- (void)setFrame:(int)aFrameIndex {
333  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
334
335  NSCursor* newCursor = [mFrames objectAtIndex:aFrameIndex];
336  [newCursor set];
337  mLastSetCocoaCursor = newCursor;
338
339  NS_OBJC_END_TRY_IGNORE_BLOCK;
340}
341
342- (int)numFrames {
343  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
344
345  return [mFrames count];
346
347  NS_OBJC_END_TRY_BLOCK_RETURN(0);
348}
349
350- (NSString*)description {
351  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
352
353  return [mFrames description];
354
355  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
356}
357
358- (void)dealloc {
359  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
360
361  [mFrames release];
362  [super dealloc];
363
364  NS_OBJC_END_TRY_IGNORE_BLOCK;
365}
366
367@end
368