cocoadisplay: Use hotspot read from .cur files

Previously, the cursor's hotspot defaulted to (0,0). Fixes #845.

Closes #849
This commit is contained in:
Donny Lawrence 2020-01-18 16:12:44 -05:00 committed by rdb
parent 17dddeedc4
commit 4f4b14dd2b
2 changed files with 87 additions and 28 deletions

View File

@ -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<Filename, NSImage*> IconImages;
typedef pmap<Filename, NSData*> IconImages;
IconImages _images;
public:

View File

@ -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.