/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsMacCursor.h" #include "nsObjCExceptions.h" #include "nsDebug.h" #include "nsDirectoryServiceDefs.h" #include "nsCOMPtr.h" #include "nsIFile.h" #include "nsString.h" /*! @category nsMacCursor (PrivateMethods) @abstract Private methods internal to the nsMacCursor class. @discussion nsMacCursor is effectively an abstract class. It does not define complete behaviour in and of itself, the subclasses defined in this file provide the useful implementations. */ @interface nsMacCursor (PrivateMethods) /*! @method getNextCursorFrame @abstract get the index of the next cursor frame to display. @discussion Increments and returns the frame counter of an animated cursor. @result The index of the next frame to display in the cursor animation */ - (int)getNextCursorFrame; /*! @method numFrames @abstract Query the number of frames in this cursor's animation. @discussion Returns the number of frames in this cursor's animation. Static cursors return 1. */ - (int)numFrames; /*! @method createTimer @abstract Create a Timer to use to animate the cursor. @discussion Creates an instance of NSTimer which is used to drive the cursor animation. This method should only be called for cursors that are animated. */ - (void)createTimer; /*! @method destroyTimer @abstract Destroy any timer instance associated with this cursor. @discussion Invalidates and releases any NSTimer instance associated with this cursor. */ - (void)destroyTimer; /*! @method destroyTimer @abstract Destroy any timer instance associated with this cursor. @discussion Invalidates and releases any NSTimer instance associated with this cursor. */ /*! @method advanceAnimatedCursor: @abstract Method called by animation timer to perform animation. @discussion Called by an animated cursor's associated timer to advance the animation to the next frame. Determines which frame should occur next and sets the cursor to that frame. @param aTimer the timer causing the animation */ - (void)advanceAnimatedCursor:(NSTimer*)aTimer; /*! @method setFrame: @abstract Sets the current cursor, using an index to determine which frame in the animation to display. @discussion Sets the current cursor. The frame index determines which frame is shown if the cursor is animated. Frames and numbered from 0 to -[nsMacCursor numFrames] - 1. A static cursor has a single frame, numbered 0. @param aFrameIndex the index indicating which frame from the animation to display */ - (void)setFrame:(int)aFrameIndex; @end /*! @class nsCocoaCursor @abstract Implementation of nsMacCursor that uses Cocoa NSCursor instances. @discussion Displays a static or animated cursor, using Cocoa NSCursor instances. These can be either built-in NSCursor instances, or custom NSCursors created from images. When more than one NSCursor is provided, the cursor will use these as animation frames. */ @interface nsCocoaCursor : nsMacCursor { @private NSArray* mFrames; NSCursor* mLastSetCocoaCursor; } /*! @method initWithFrames: @abstract Create an animated cursor by specifying the frames to use for the animation. @discussion Creates a cursor that will animate by cycling through the given frames. Each element of the array must be an instance of NSCursor @param aCursorFrames an array of NSCursor, representing the frames of an animated cursor, in the order they should be played. @param aType the corresponding nsCursor constant @result an instance of nsCocoaCursor that will animate the given cursor frames */ - (id)initWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType; /*! @method initWithCursor: @abstract Create a cursor by specifying a Cocoa NSCursor. @discussion Creates a cursor representing the given Cocoa built-in cursor. @param aCursor the NSCursor to use @param aType the corresponding nsCursor constant @result an instance of nsCocoaCursor representing the given NSCursor */ - (id)initWithCursor:(NSCursor*)aCursor type:(nsCursor)aType; /*! @method initWithImageNamed:hotSpot: @abstract Create a cursor by specifying the name of an image resource to use for the cursor and a hotspot. @discussion Creates a cursor by loading the named image using the +[NSImage imageNamed:] method.

The image must be compatible with any restrictions laid down by NSCursor. These vary by operating system version.

The hotspot precisely determines the point where the user clicks when using the cursor.

@param aCursor the name of the image to use for the cursor @param aPoint the point within the cursor to use as the hotspot @param aType the corresponding nsCursor constant @result an instance of nsCocoaCursor that uses the given image and hotspot */ - (id)initWithImageNamed:(NSString*)aCursorImage hotSpot:(NSPoint)aPoint type:(nsCursor)aType; @end @implementation nsMacCursor + (nsMacCursor*)cursorWithCursor:(NSCursor*)aCursor type:(nsCursor)aType { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; return [[[nsCocoaCursor alloc] initWithCursor:aCursor type:aType] autorelease]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } + (nsMacCursor*)cursorWithImageNamed:(NSString*)aCursorImage hotSpot:(NSPoint)aPoint type:(nsCursor)aType { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; return [[[nsCocoaCursor alloc] initWithImageNamed:aCursorImage hotSpot:aPoint type:aType] autorelease]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } + (nsMacCursor*)cursorWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; return [[[nsCocoaCursor alloc] initWithFrames:aCursorFrames type:aType] autorelease]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } + (NSCursor*)cocoaCursorWithImageNamed:(NSString*)imageName hotSpot:(NSPoint)aPoint { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; nsCOMPtr resDir; nsAutoCString resPath; NSString *pathToImage, *pathToHiDpiImage; NSImage *cursorImage, *hiDpiCursorImage; nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(resDir)); if (NS_FAILED(rv)) goto INIT_FAILURE; resDir->AppendNative("res"_ns); resDir->AppendNative("cursors"_ns); rv = resDir->GetNativePath(resPath); if (NS_FAILED(rv)) goto INIT_FAILURE; pathToImage = [NSString stringWithUTF8String:(const char*)resPath.get()]; if (!pathToImage) goto INIT_FAILURE; pathToImage = [pathToImage stringByAppendingPathComponent:imageName]; pathToHiDpiImage = [pathToImage stringByAppendingString:@"@2x"]; // Add same extension to both image paths. pathToImage = [pathToImage stringByAppendingPathExtension:@"png"]; pathToHiDpiImage = [pathToHiDpiImage stringByAppendingPathExtension:@"png"]; cursorImage = [[[NSImage alloc] initWithContentsOfFile:pathToImage] autorelease]; if (!cursorImage) goto INIT_FAILURE; // Note 1: There are a few different ways to get a hidpi image via // initWithContentsOfFile. We let the OS handle this here: when the // file basename ends in "@2x", it will be displayed at native resolution // instead of being pixel-doubled. See bug 784909 comment 7 for alternates ways. // // Note 2: The OS is picky, and will ignore the hidpi representation // unless it is exactly twice the size of the lowdpi image. hiDpiCursorImage = [[[NSImage alloc] initWithContentsOfFile:pathToHiDpiImage] autorelease]; if (hiDpiCursorImage) { NSImageRep* imageRep = [[hiDpiCursorImage representations] objectAtIndex:0]; [cursorImage addRepresentation:imageRep]; } return [[[NSCursor alloc] initWithImage:cursorImage hotSpot:aPoint] autorelease]; INIT_FAILURE: NS_WARNING("Problem getting path to cursor image file!"); [self release]; return nil; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (BOOL)isSet { // implemented by subclasses return NO; } - (void)set { if ([self isAnimated]) { [self createTimer]; } // if the cursor isn't animated or the timer creation fails for any reason... if (!mTimer) { [self setFrame:0]; } } - (void)unset { [self destroyTimer]; } - (BOOL)isAnimated { return [self numFrames] > 1; } - (int)numFrames { // subclasses need to override this to support animation return 1; } - (int)getNextCursorFrame { mFrameCounter = (mFrameCounter + 1) % [self numFrames]; return mFrameCounter; } - (void)createTimer { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; if (!mTimer) { mTimer = [[NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(advanceAnimatedCursor:) userInfo:nil repeats:YES] retain]; } NS_OBJC_END_TRY_ABORT_BLOCK; } - (void)destroyTimer { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; if (mTimer) { [mTimer invalidate]; [mTimer release]; mTimer = nil; } NS_OBJC_END_TRY_ABORT_BLOCK; } - (void)advanceAnimatedCursor:(NSTimer*)aTimer { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; if ([aTimer isValid]) { [self setFrame:[self getNextCursorFrame]]; } NS_OBJC_END_TRY_ABORT_BLOCK; } - (void)setFrame:(int)aFrameIndex { // subclasses need to do something useful here } - (nsCursor)type { return mType; } - (void)dealloc { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; [self destroyTimer]; [super dealloc]; NS_OBJC_END_TRY_ABORT_BLOCK; } @end @implementation nsCocoaCursor - (id)initWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; self = [super init]; NSEnumerator* it = [aCursorFrames objectEnumerator]; NSObject* frame = nil; while ((frame = [it nextObject])) { NS_ASSERTION([frame isKindOfClass:[NSCursor class]], "Invalid argument: All frames must be of type NSCursor"); } mFrames = [aCursorFrames retain]; mFrameCounter = 0; mType = aType; return self; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (id)initWithCursor:(NSCursor*)aCursor type:(nsCursor)aType { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; NSArray* frame = [NSArray arrayWithObjects:aCursor, nil]; return [self initWithFrames:frame type:aType]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (id)initWithImageNamed:(NSString*)aCursorImage hotSpot:(NSPoint)aPoint type:(nsCursor)aType { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; return [self initWithCursor:[nsMacCursor cocoaCursorWithImageNamed:aCursorImage hotSpot:aPoint] type:aType]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (BOOL)isSet { return [NSCursor currentCursor] == mLastSetCocoaCursor; } - (void)setFrame:(int)aFrameIndex { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; NSCursor* newCursor = [mFrames objectAtIndex:aFrameIndex]; [newCursor set]; mLastSetCocoaCursor = newCursor; NS_OBJC_END_TRY_ABORT_BLOCK; } - (int)numFrames { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; return [mFrames count]; NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); } - (NSString*)description { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; return [mFrames description]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (void)dealloc { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; [mFrames release]; [super dealloc]; NS_OBJC_END_TRY_ABORT_BLOCK; } @end