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