Browse Source

cocoadisplay: Use hotspot read from .cur files

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

Closes #849
Donny Lawrence 5 years ago
parent
commit
4f4b14dd2b

+ 4 - 1
panda/src/cocoadisplay/cocoaGraphicsWindow.h

@@ -79,8 +79,11 @@ protected:
   virtual void mouse_mode_relative();
   virtual void mouse_mode_relative();
 
 
 private:
 private:
+  NSData *load_image_data(const Filename &filename);
   NSImage *load_image(const Filename &filename);
   NSImage *load_image(const Filename &filename);
 
 
+  NSCursor *load_cursor(const Filename &filename);
+
   void handle_modifier(NSUInteger modifierFlags, NSUInteger mask, ButtonHandle button);
   void handle_modifier(NSUInteger modifierFlags, NSUInteger mask, ButtonHandle button);
   ButtonHandle map_key(unsigned short c) const;
   ButtonHandle map_key(unsigned short c) const;
   ButtonHandle map_raw_key(unsigned short keycode) const;
   ButtonHandle map_raw_key(unsigned short keycode) const;
@@ -104,7 +107,7 @@ private:
   CFDictionaryRef _windowed_mode;
   CFDictionaryRef _windowed_mode;
 #endif
 #endif
 
 
-  typedef pmap<Filename, NSImage*> IconImages;
+  typedef pmap<Filename, NSData*> IconImages;
   IconImages _images;
   IconImages _images;
 
 
 public:
 public:

+ 83 - 27
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -571,17 +571,17 @@ open_window() {
   }
   }
 
 
   if (_properties.has_cursor_filename()) {
   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) {
+      if (_cursor != nil) {
+        [_cursor release];
+      }
       _cursor = cursor;
       _cursor = cursor;
     } else {
     } else {
       _properties.clear_cursor_filename();
       _properties.clear_cursor_filename();
     }
     }
+
     // This will ensure that NSView's resetCursorRects gets called, which sets
     // This will ensure that NSView's resetCursorRects gets called, which sets
     // the appropriate cursor rects.
     // the appropriate cursor rects.
     [[_view window] invalidateCursorRectsForView:_view];
     [[_view window] invalidateCursorRectsForView:_view];
@@ -1045,19 +1045,15 @@ set_properties_now(WindowProperties &properties) {
       properties.set_cursor_filename(cursor_filename);
       properties.set_cursor_filename(cursor_filename);
       properties.clear_cursor_filename();
       properties.clear_cursor_filename();
     } else {
     } 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
     // 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()) {
   if (filename.empty()) {
     return nil;
     return nil;
   }
   }
@@ -1345,7 +1342,6 @@ load_image(const Filename &filename) {
   }
   }
 
 
   // Look in our index.
   // Look in our index.
-  NSImage *image = nil;
   IconImages::const_iterator it = _images.find(resolved);
   IconImages::const_iterator it = _images.find(resolved);
   if (it != _images.end()) {
   if (it != _images.end()) {
     // Found it.
     // Found it.
@@ -1373,21 +1369,81 @@ load_image(const Filename &filename) {
 
 
   NSData *data = [NSData dataWithBytesNoCopy:buffer length:size];
   NSData *data = [NSData dataWithBytesNoCopy:buffer length:size];
   if (data == nil) {
   if (data == nil) {
+    cocoadisplay_cat.error()
+      << "Could not load image data from file " << filename << "\n";
     return nil;
     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) {
   if (image == nil) {
     cocoadisplay_cat.error()
     cocoadisplay_cat.error()
       << "Could not load image from file " << filename << "\n";
       << "Could not load image from file " << filename << "\n";
     return nil;
     return nil;
   }
   }
-
-  _images[resolved] = image;
   return 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
  * Called by CocoaPandaView or the window delegate when the frame rect
  * changes.
  * changes.