diff --git a/panda/src/cocoadisplay/cocoaGraphicsWindow.h b/panda/src/cocoadisplay/cocoaGraphicsWindow.h index a053cb11b9..bec277e1b6 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsWindow.h +++ b/panda/src/cocoadisplay/cocoaGraphicsWindow.h @@ -79,8 +79,11 @@ protected: virtual void mouse_mode_relative(); private: + NSData *load_image_data(const Filename &filename); NSImage *load_image(const Filename &filename); + NSCursor *load_cursor(const Filename &filename); + void handle_modifier(NSUInteger modifierFlags, NSUInteger mask, ButtonHandle button); ButtonHandle map_key(unsigned short c) const; ButtonHandle map_raw_key(unsigned short keycode) const; @@ -104,7 +107,7 @@ private: CFDictionaryRef _windowed_mode; #endif - typedef pmap IconImages; + typedef pmap IconImages; IconImages _images; public: diff --git a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm index 53e5f6f6a2..e7d25c29d7 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm @@ -571,17 +571,17 @@ open_window() { } if (_properties.has_cursor_filename()) { - NSImage *image = load_image(_properties.get_cursor_filename()); - NSCursor *cursor = nil; - // TODO: allow setting the hotspot, read it from file when loading .cur. - if (image != nil) { - cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint(0, 0)]; - } + NSCursor *cursor = load_cursor(_properties.get_cursor_filename()); + if (cursor != nil) { + if (_cursor != nil) { + [_cursor release]; + } _cursor = cursor; } else { _properties.clear_cursor_filename(); } + // This will ensure that NSView's resetCursorRects gets called, which sets // the appropriate cursor rects. [[_view window] invalidateCursorRectsForView:_view]; @@ -1045,19 +1045,15 @@ set_properties_now(WindowProperties &properties) { properties.set_cursor_filename(cursor_filename); properties.clear_cursor_filename(); } else { - NSImage *image = load_image(cursor_filename); - if (image != nil) { - NSCursor *cursor; - cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint(0, 0)]; - if (cursor != nil) { - // Replace the existing cursor. - if (_cursor != nil) { - [_cursor release]; - } - _cursor = cursor; - _properties.set_cursor_filename(cursor_filename); - properties.clear_cursor_filename(); + NSCursor *cursor = load_cursor(cursor_filename); + if (cursor != nil) { + // Replace the existing cursor. + if (_cursor != nil) { + [_cursor release]; } + _cursor = cursor; + _properties.set_cursor_filename(cursor_filename); + properties.clear_cursor_filename(); } } // This will ensure that NSView's resetCursorRects gets called, which sets @@ -1321,11 +1317,12 @@ do_switch_fullscreen(CFDictionaryRef mode) { } /** - * Loads the indicated filename and returns an NSImage pointer, or NULL on - * failure. Must be called from the window thread. + * Loads the indicated filename and returns an NSData pointer (which can then + * be used to create a CGImageSource or NSImage), or NULL on failure. Must be + * called from the window thread. May return nil. */ -NSImage *CocoaGraphicsWindow:: -load_image(const Filename &filename) { +NSData *CocoaGraphicsWindow:: +load_image_data(const Filename &filename) { if (filename.empty()) { return nil; } @@ -1345,7 +1342,6 @@ load_image(const Filename &filename) { } // Look in our index. - NSImage *image = nil; IconImages::const_iterator it = _images.find(resolved); if (it != _images.end()) { // Found it. @@ -1373,21 +1369,81 @@ load_image(const Filename &filename) { NSData *data = [NSData dataWithBytesNoCopy:buffer length:size]; if (data == nil) { + cocoadisplay_cat.error() + << "Could not load image data from file " << filename << "\n"; return nil; } - image = [[NSImage alloc] initWithData:data]; - [data release]; + _images[resolved] = data; + return data; +} + +/** + * Wraps image data loaded by load_image_data with an NSImage. The returned + * pointer is autoreleased. May return nil. + */ +NSImage *CocoaGraphicsWindow:: +load_image(const Filename &filename) { + NSData *image_data = load_image_data(filename); + NSImage *image = [[[NSImage alloc] initWithData:image_data] autorelease]; if (image == nil) { cocoadisplay_cat.error() << "Could not load image from file " << filename << "\n"; return nil; } - - _images[resolved] = image; return image; } +/** + * Returns a cursor with the proper hotspot if a .cur filename is passed in. + * You must release the returned pointer. May return nil. + */ +NSCursor *CocoaGraphicsWindow:: +load_cursor(const Filename &filename) { + NSData *image_data = load_image_data(cursor_filename); + if (image_data == nil) { + return nil; + } + + // Read the metadata from the image, which should contain hotspotX and + // hotspotY properties. + CGImageSourceRef cg_image = CGImageSourceCreateWithData((CFDataRef)image_data, nullptr); + if (cg_image == NULL) { + return nil; + } + + NSDictionary *image_props = (NSDictionary *)CGImageSourceCopyPropertiesAtIndex(cg_image, 0, nil); + CFRelease(cg_image); + + if (image_props == nil) { + return nil; + } + + CGFloat hotspot_x = 0.0f; + CGFloat hotspot_y = 0.0f; + if (image_props[@"hotspotX"] != nil) { + hotspot_x = [(NSNumber *)image_props[@"hotspotX"] floatValue]; + } + if (image_props[@"hotspotY"] != nil) { + hotspot_y = [(NSNumber *)image_props[@"hotspotY"] floatValue]; + } + [image_props release]; + + NSImage *image = [[NSImage alloc] initWithData:image_data]; + + NSCursor *cursor; + if (image != nil) { + // Apple recognizes that hotspots are usually specified from a .cur + // file, whose origin is in the top-left, so there's no need to flip + // it like most other Cocoa coordinates. + cursor = [[NSCursor alloc] initWithImage:image + hotSpot:NSMakePoint(hotspot_x, hotspot_y)]; + [image release]; + } + + return cursor; +} + /** * Called by CocoaPandaView or the window delegate when the frame rect * changes.