Fix for CursorLoader: Hotspot loading for .cur files

Well the loader does not seperate between cur and ico formats, wich is fine for the image part, as both are specified identically,
however there are 4 bytes that have a different meaning in cur and containg the hotspot ICO_(file_format)

So I added a case for loading hotspots from cur files (note for ani format the hotspot kinda works, this is only for .cur)

This is not the nicest patch, but then the whole class is kinda chaotic, and it gets the job done, and does not make stuff worse, so I would like seeing this getting merged into the trunk (As I’m most probably not the only person needing cursors with a custom hotspot).

Following a pseudo dif of the CursorLoader.java
[java]
/*

  • Copyright © 2009-2012 jMonkeyEngine
  • All rights reserved.
  • Redistribution and use in source and binary forms, with or without
  • modification, are permitted provided that the following conditions are
  • met:
    • Redistributions of source code must retain the above copyright
  • notice, this list of conditions and the following disclaimer.
    • Redistributions in binary form must reproduce the above copyright
  • notice, this list of conditions and the following disclaimer in the
  • documentation and/or other materials provided with the distribution.
    • Neither the name of ‘jMonkeyEngine’ nor the names of its contributors
  • may be used to endorse or promote products derived from this software
  • without specific prior written permission.
  • THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  • “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  • TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  • PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  • CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  • EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  • PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  • PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  • SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    */
    package com.jme3.cursors.plugins;

import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.IntBuffer;
import java.util.ArrayList;

import javax.imageio.ImageIO;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
import com.jme3.util.BufferUtils;
import com.jme3.util.LittleEndien;

/**
*

  • @author MadJack

  • @creation Jun 5, 2012 9:45:58 AM
    */
    public class CursorLoader implements AssetLoader {

    private boolean isIco;
    private boolean isAni;

    /**

    • Loads and return a cursor file of one of the following format: .ani, .cur and .ico.
    • @param info
    •        The {@link AssetInfo} describing the cursor file.
      
    • @return A JmeCursor representation of the LWJGL’s Cursor.
    • @throws IOException
    •         if the file is not found.
      

    */
    @Override
    public JmeCursor load(final AssetInfo info) throws IOException {

     this.isIco = false;
     this.isAni = false;
    
     this.isIco = info.getKey().getExtension().equals("ico");
     if (!this.isIco) {
     	this.isIco = info.getKey().getExtension().equals("cur");
     	if (!this.isIco) {
     		this.isAni = info.getKey().getExtension().equals("ani");
     	}
     }
     if (!this.isAni && !this.isIco) {
     	throw new IllegalArgumentException("Cursors supported are .ico, .cur or .ani");
     }
    
     InputStream in = null;
     try {
     	in = info.openStream();
    

— return this.loadCursor(in);
+++ return this.loadCursor(in, info);
} finally {
if (in != null) {
in.close();
}
}
}
— private JmeCursor loadCursor(final InputStream inStream) throws IOException {
+++ private JmeCursor loadCursor(final InputStream inStream, final AssetInfo info) throws IOException {

	byte[] icoimages = new byte[0]; // new byte [0] facilitates read()

	if (this.isAni) {
		final CursorImageData ciDat = new CursorImageData();
		int numIcons = 0;
		int jiffy = 0;
		// not using those but keeping references for now.
		int steps = 0;
		int width = 0;
		int height = 0;
		int flag = 0; // we don't use that.
		int[] rate = null;
		int[] animSeq = null;
		ArrayList<byte[]> icons;

		final DataInput leIn = new LittleEndien(inStream);
		final int riff = leIn.readInt();
		if (riff == 0x46464952) { // RIFF
			// read next int (file length), discarding it, we don't need that.
			leIn.readInt();

			int nextInt = 0;

			nextInt = this.getNext(leIn);
			if (nextInt == 0x4e4f4341) {
				// We have ACON, we do nothing
				// System.out.println("We have ACON. Next!");
				nextInt = this.getNext(leIn);
				while (nextInt >= 0) {
					if (nextInt == 0x68696e61) {
						// System.out.println("we have 'anih' header");
						leIn.skipBytes(8); // internal struct length (always 36)
						numIcons = leIn.readInt();
						steps = leIn.readInt(); // number of blits for ani cycles
						width = leIn.readInt();
						height = leIn.readInt();
						leIn.skipBytes(8);
						jiffy = leIn.readInt();
						flag = leIn.readInt();
						nextInt = leIn.readInt();
					} else if (nextInt == 0x65746172) { // found a 'rate' of animation
						// System.out.println("we have 'rate'.");
						// Fill rate here.
						// Rate is synchronous with frames.
						final int length = leIn.readInt();
						rate = new int[length / 4];
						for (int i = 0; i < length / 4; i++) {
							rate[i] = leIn.readInt();
						}
						nextInt = leIn.readInt();
					} else if (nextInt == 0x20716573) { // found a 'seq ' of animation
						// System.out.println("we have 'seq '.");
						// Fill animation sequence here
						final int length = leIn.readInt();
						animSeq = new int[length / 4];
						for (int i = 0; i < length / 4; i++) {
							animSeq[i] = leIn.readInt();
						}
						nextInt = leIn.readInt();
					} else if (nextInt == 0x5453494c) { // Found a LIST
						// System.out.println("we have 'LIST'.");
						final int length = leIn.readInt();
						nextInt = leIn.readInt();
						if (nextInt == 0x4f464e49) { // Got an INFO, skip its length
							// this part consist of Author, title, etc
							leIn.skipBytes(length - 4);
							// System.out.println(" Discarding INFO (skipped = " + skipped + ")");
							nextInt = leIn.readInt();
						} else if (nextInt == 0x6d617266) { // found a 'fram' for animation
							// System.out.println("we have 'fram'.");
							if (leIn.readInt() == 0x6e6f6369) { // we have 'icon'
								// We have an icon and from this point on
								// the rest is only icons.
								final int icoLength = leIn.readInt();
								ciDat.numImages = numIcons;
								icons = new ArrayList<byte[]>(numIcons);
								for (int i = 0; i < numIcons; i++) {
									if (i > 0) {
										// skip 'icon' header and length as they are
										// known already and won't change.
										leIn.skipBytes(8);
									}
									final byte[] data = new byte[icoLength];
									((InputStream) leIn).read(data, 0, icoLength);
									// in case the header didn't have width or height
									// get it from first image.
									if (width == 0 || height == 0 && i == 1) {
										width = data[6];
										height = data[7];
									}
									icons.add(data);
								}
								// at this point we have the icons, rates (either
								// through jiffy or rate array, the sequence (if
								// applicable) and the ani header info.
								// Put things together.
								ciDat.assembleCursor(icons, rate, animSeq, jiffy, steps, width, height);
								ciDat.completeCursor();
								nextInt = leIn.readInt();
								// if for some reason there's JUNK (nextInt > -1)
								// bail out.
								nextInt = nextInt > -1 ? -1 : nextInt;
							}
						}
					}
				}
			}
			return this.setJmeCursor(ciDat);

		} else if (riff == 0x58464952) {
			throw new IllegalArgumentException("Big-Endian RIFX is not supported. Sorry.");
		} else {
			throw new IllegalArgumentException("Unknown format.");
		}
	} else if (this.isIco) {
		final DataInputStream in = new DataInputStream(inStream);
		int bytesToRead;
		while ((bytesToRead = in.available()) != 0) {
			final byte[] icoimage2 = new byte[icoimages.length + bytesToRead];
			System.arraycopy(icoimages, 0, icoimage2, 0, icoimages.length);
			in.read(icoimage2, icoimages.length, bytesToRead);
			icoimages = icoimage2;
		}
	}

	final BufferedImage bi[] = this.parseICOImage(icoimages);

	final CursorImageData cid = new CursorImageData(bi, 0, 0, 0, 0);
	// only cur does store hotspots NOTE: untested for extremly large images

+++ if (info.getKey().getExtension().equals(“cur”)) {
+++ final int FDE_OFFSET = 6; // first directory entry offset
+++ int hotspotX = icoimages[FDE_OFFSET + 4];
+++ hotspotX = hotspotX + icoimages[FDE_OFFSET + 5] * 255;
+++ int hotspotY = icoimages[FDE_OFFSET + 6];
+++ hotspotY = hotspotY + icoimages[FDE_OFFSET + 7] * 255;
+++ cid.xHotSpot = hotspotX;
+++ cid.yHotSpot = bi[0].getHeight() - 1 - hotspotY;
+++ }

	cid.completeCursor();

	return this.setJmeCursor(cid);
}

private JmeCursor setJmeCursor(final CursorImageData cid) {
	final JmeCursor jmeCursor = new JmeCursor();

	// set cursor's params.
	jmeCursor.setWidth(cid.width);
	jmeCursor.setHeight(cid.height);
	jmeCursor.setxHotSpot(cid.xHotSpot);
	jmeCursor.setyHotSpot(cid.yHotSpot);
	jmeCursor.setNumImages(cid.numImages);
	jmeCursor.setImagesDelay(cid.imgDelay);
	jmeCursor.setImagesData(cid.data);
	// System.out.println("Width = " + cid.width);
	// System.out.println("Height = " + cid.height);
	// System.out.println("HSx = " + cid.xHotSpot);
	// System.out.println("HSy = " + cid.yHotSpot);
	// System.out.println("# img = " + cid.numImages);

	return jmeCursor;
}

private BufferedImage[] parseICOImage(byte[] icoimage) throws IOException {
	/*
	 * Most of this is original code by Jeff Friesen at http://www.informit.com/articles/article.aspx?p=1186882&seqNum=3
	 */

	BufferedImage[] bi;
	// Check resource type field.
	final int FDE_OFFSET = 6; // first directory entry offset
	final int DE_LENGTH = 16; // directory entry length
	final int BMIH_LENGTH = 40; // BITMAPINFOHEADER length

	if (icoimage[2] != 1 && icoimage[2] != 2 || icoimage[3] != 0) {
		throw new IllegalArgumentException("Bad data in ICO/CUR file. ImageType has to be either 1 or 2.");
	}

	int numImages = this.ubyte(icoimage[5]);
	numImages <<= 8;
	numImages |= icoimage[4];
	bi = new BufferedImage[numImages];
	final int[] colorCount = new int[numImages];

	for (int i = 0; i < numImages; i++) {
		int width = this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH]);

		int height = this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 1]);

		colorCount[i] = this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 2]);

		int bytesInRes = this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 11]);
		bytesInRes <<= 8;
		bytesInRes |= this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 10]);
		bytesInRes <<= 8;
		bytesInRes |= this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 9]);
		bytesInRes <<= 8;
		bytesInRes |= this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 8]);

		int imageOffset = this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 15]);
		imageOffset <<= 8;
		imageOffset |= this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 14]);
		imageOffset <<= 8;
		imageOffset |= this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 13]);
		imageOffset <<= 8;
		imageOffset |= this.ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 12]);

		if (icoimage[imageOffset] == 40 && icoimage[imageOffset + 1] == 0 && icoimage[imageOffset + 2] == 0
				&& icoimage[imageOffset + 3] == 0) {
			// BITMAPINFOHEADER detected

			int _width = this.ubyte(icoimage[imageOffset + 7]);
			_width <<= 8;
			_width |= this.ubyte(icoimage[imageOffset + 6]);
			_width <<= 8;
			_width |= this.ubyte(icoimage[imageOffset + 5]);
			_width <<= 8;
			_width |= this.ubyte(icoimage[imageOffset + 4]);

			// If width is 0 (for 256 pixels or higher), _width contains
			// actual width.

			if (width == 0) {
				width = _width;
			}

			int _height = this.ubyte(icoimage[imageOffset + 11]);
			_height <<= 8;
			_height |= this.ubyte(icoimage[imageOffset + 10]);
			_height <<= 8;
			_height |= this.ubyte(icoimage[imageOffset + 9]);
			_height <<= 8;
			_height |= this.ubyte(icoimage[imageOffset + 8]);

			// If height is 0 (for 256 pixels or higher), _height contains
			// actual height times 2.

			if (height == 0) {
				height = _height >> 1; // Divide by 2.
			}
			int planes = this.ubyte(icoimage[imageOffset + 13]);
			planes <<= 8;
			planes |= this.ubyte(icoimage[imageOffset + 12]);

			int bitCount = this.ubyte(icoimage[imageOffset + 15]);
			bitCount <<= 8;
			bitCount |= this.ubyte(icoimage[imageOffset + 14]);

			// If colorCount [i] is 0, the number of colors is determined
			// from the planes and bitCount values. For example, the number
			// of colors is 256 when planes is 1 and bitCount is 8. Leave
			// colorCount [i] set to 0 when planes is 1 and bitCount is 32.

			if (colorCount[i] == 0) {
				if (planes == 1) {
					if (bitCount == 1) {
						colorCount[i] = 2;
					} else if (bitCount == 4) {
						colorCount[i] = 16;
					} else if (bitCount == 8) {
						colorCount[i] = 256;
					} else if (bitCount != 32) {
						colorCount[i] = (int) Math.pow(2, bitCount);
					}
				} else {
					colorCount[i] = (int) Math.pow(2, bitCount * planes);
				}
			}

			bi[i] = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

			// Parse image to image buffer.

			final int colorTableOffset = imageOffset + BMIH_LENGTH;

			if (colorCount[i] == 2) {
				final int xorImageOffset = colorTableOffset + 2 * 4;

				final int scanlineBytes = this.calcScanlineBytes(width, 1);
				final int andImageOffset = xorImageOffset + scanlineBytes * height;

				final int[] masks = { 128, 64, 32, 16, 8, 4, 2, 1 };

				for (int row = 0; row < height; row++) {
					for (int col = 0; col < width; col++) {
						int index;

						if ((this.ubyte(icoimage[xorImageOffset + row * scanlineBytes + col / 8]) & masks[col % 8]) != 0) {
							index = 1;
						} else {
							index = 0;
						}

						int rgb = 0;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4 + 2]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4 + 1]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4]);

						if ((this.ubyte(icoimage[andImageOffset + row * scanlineBytes + col / 8]) & masks[col % 8]) != 0) {
							bi[i].setRGB(col, height - 1 - row, rgb);
						} else {
							bi[i].setRGB(col, height - 1 - row, 0xff000000 | rgb);
						}
					}
				}
			} else if (colorCount[i] == 16) {
				final int xorImageOffset = colorTableOffset + 16 * 4;

				final int scanlineBytes = this.calcScanlineBytes(width, 4);
				final int andImageOffset = xorImageOffset + scanlineBytes * height;

				final int[] masks = { 128, 64, 32, 16, 8, 4, 2, 1 };

				for (int row = 0; row < height; row++) {
					for (int col = 0; col < width; col++) {
						int index;
						if ((col & 1) == 0) // even
						{
							index = this.ubyte(icoimage[xorImageOffset + row * scanlineBytes + col / 2]);
							index >>= 4;
						} else {
							index = this.ubyte(icoimage[xorImageOffset + row * scanlineBytes + col / 2]) & 15;
						}

						int rgb = 0;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4 + 2]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4 + 1]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4]);

						if ((this
								.ubyte(icoimage[andImageOffset + row * this.calcScanlineBytes(width, 1) + col / 8]) & masks[col % 8]) != 0) {
							bi[i].setRGB(col, height - 1 - row, rgb);
						} else {
							bi[i].setRGB(col, height - 1 - row, 0xff000000 | rgb);
						}
					}
				}
			} else if (colorCount[i] == 256) {
				final int xorImageOffset = colorTableOffset + 256 * 4;

				final int scanlineBytes = this.calcScanlineBytes(width, 8);
				final int andImageOffset = xorImageOffset + scanlineBytes * height;

				final int[] masks = { 128, 64, 32, 16, 8, 4, 2, 1 };

				for (int row = 0; row < height; row++) {
					for (int col = 0; col < width; col++) {
						int index;
						index = this.ubyte(icoimage[xorImageOffset + row * scanlineBytes + col]);

						int rgb = 0;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4 + 2]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4 + 1]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + index * 4]);

						if ((this
								.ubyte(icoimage[andImageOffset + row * this.calcScanlineBytes(width, 1) + col / 8]) & masks[col % 8]) != 0) {
							bi[i].setRGB(col, height - 1 - row, rgb);
						} else {
							bi[i].setRGB(col, height - 1 - row, 0xff000000 | rgb);
						}
					}
				}
			} else if (colorCount[i] == 0) {
				final int scanlineBytes = this.calcScanlineBytes(width, 32);

				for (int row = 0; row < height; row++) {
					for (int col = 0; col < width; col++) {
						int rgb = this.ubyte(icoimage[colorTableOffset + row * scanlineBytes + col * 4 + 3]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + row * scanlineBytes + col * 4 + 2]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + row * scanlineBytes + col * 4 + 1]);
						rgb <<= 8;
						rgb |= this.ubyte(icoimage[colorTableOffset + row * scanlineBytes + col * 4]);

						bi[i].setRGB(col, height - 1 - row, rgb);
					}
				}
			}
		} else if (this.ubyte(icoimage[imageOffset]) == 0x89 && icoimage[imageOffset + 1] == 0x50
				&& icoimage[imageOffset + 2] == 0x4e && icoimage[imageOffset + 3] == 0x47
				&& icoimage[imageOffset + 4] == 0x0d && icoimage[imageOffset + 5] == 0x0a
				&& icoimage[imageOffset + 6] == 0x1a && icoimage[imageOffset + 7] == 0x0a) {
			// PNG detected

			ByteArrayInputStream bais;
			bais = new ByteArrayInputStream(icoimage, imageOffset, bytesInRes);
			bi[i] = ImageIO.read(bais);
		} else {
			throw new IllegalArgumentException("Bad data in ICO/CUR file. BITMAPINFOHEADER or PNG " + "expected");
		}
	}
	icoimage = null; // This array can now be garbage collected.

	return bi;
}

private int ubyte(final byte b) {
	return b < 0 ? 256 + b : b; // Convert byte to unsigned byte.
}

private int calcScanlineBytes(final int width, final int bitCount) {
	// Calculate minimum number of double-words required to store width
	// pixels where each pixel occupies bitCount bits. XOR and AND bitmaps
	// are stored such that each scanline is aligned on a double-word
	// boundary.

	return (width * bitCount + 31) / 32 * 4;
}

private int getNext(final DataInput in) throws IOException {
	return in.readInt();
}

private class CursorImageData {

	int			width;
	int			height;
	int			xHotSpot;
	int			yHotSpot;
	int			numImages;
	IntBuffer	imgDelay;
	IntBuffer	data;

	public CursorImageData() {
	}

	CursorImageData(final BufferedImage[] bi, final int delay, int hsX, int hsY, final int curType) {
		// cursor type
		// 0 - Undefined (an array of images inside an ICO)
		// 1 - ICO
		// 2 - CUR
		IntBuffer singleCursor = null;
		final ArrayList<IntBuffer> cursors = new ArrayList<IntBuffer>();
		int bwidth = 0;
		int bheight = 0;
		boolean multIcons = false;

		// make the cursor image
		for (int i = 0; i < bi.length; i++) {
			BufferedImage img = bi[i];
			bwidth = img.getWidth();
			bheight = img.getHeight();
			if (curType == 1) {
				hsX = 0;
				hsY = bheight - 1;
			} else if (curType == 2) {
				if (hsY == 0) {
					// make sure we flip if 0
					hsY = bheight - 1;
				}
			} else {
				// We force to choose 32x32 icon from
				// the array of icons in that ICO file.
				if (bwidth != 32 && bheight != 32) {
					multIcons = true;
					continue;
				} else {
					if (img.getType() != 2) {
						continue;
					} else {
						// force hotspot
						hsY = bheight - 1;
					}
				}
			}

			// We flip our image because .ICO and .CUR will always be reversed.
			final AffineTransform trans = AffineTransform.getScaleInstance(1, -1);
			trans.translate(0, -img.getHeight(null));
			final AffineTransformOp op = new AffineTransformOp(trans, AffineTransformOp.TYPE_BILINEAR);
			img = op.filter(img, null);

			singleCursor = BufferUtils.createIntBuffer(img.getWidth() * img.getHeight());
			final DataBufferInt dataIntBuf = (DataBufferInt) img.getData().getDataBuffer();
			singleCursor = IntBuffer.wrap(dataIntBuf.getData());
			cursors.add(singleCursor);
		}

		int count;
		if (multIcons) {
			bwidth = 32;
			bheight = 32;
			count = 1;
		} else {
			count = cursors.size();
		}
		// put the image in the IntBuffer
		this.data = BufferUtils.createIntBuffer(bwidth * bheight);
		this.imgDelay = BufferUtils.createIntBuffer(bi.length);
		for (int i = 0; i < count; i++) {
			this.data.put(cursors.get(i));
			if (delay > 0) {
				this.imgDelay.put(delay);
			}
		}
		this.width = bwidth;
		this.height = bheight;
		this.xHotSpot = hsX;
		this.yHotSpot = hsY;
		this.numImages = count;
		this.data.rewind();
		if (this.imgDelay != null) {
			this.imgDelay.rewind();
		}
	}

	private void addFrame(final byte[] imgData, int rate, final int jiffy, final int width, final int height,
			final int numSeq) throws IOException {
		final BufferedImage bi[] = CursorLoader.this.parseICOImage(imgData);
		int hotspotx = 0;
		int hotspoty = 0;
		final int type = imgData[2] | imgData[3];
		if (type == 2) {
			// CUR type, hotspot might be stored.
			hotspotx = imgData[10] | imgData[11];
			hotspoty = imgData[12] | imgData[13];
		} else if (type == 1) {
			// ICO type, hotspot not stored. Put at 0, height - 1
			// because it's flipped.
			hotspotx = 0;
			hotspoty = height - 1;
		}
		// System.out.println("Image type = " + (type == 1 ? "CUR" : "ICO"));
		if (rate == 0) {
			rate = jiffy;
		}
		CursorImageData cid = new CursorImageData(bi, rate, hotspotx, hotspoty, type);
		if (width == 0) {
			this.width = cid.width;
		} else {
			this.width = width;
		}
		if (height == 0) {
			this.height = cid.height;
		} else {
			this.height = height;
		}
		if (this.data == null) {
			if (numSeq > this.numImages) {
				this.data = BufferUtils.createIntBuffer(this.width * this.height * numSeq);
			} else {
				this.data = BufferUtils.createIntBuffer(this.width * this.height * this.numImages);
			}
			this.data.put(cid.data);
		} else {
			this.data.put(cid.data);
		}
		if (this.imgDelay == null && (this.numImages > 1 || numSeq > 1)) {
			if (numSeq > this.numImages) {
				this.imgDelay = BufferUtils.createIntBuffer(numSeq);
			} else {
				this.imgDelay = BufferUtils.createIntBuffer(this.numImages);
			}
			this.imgDelay.put(cid.imgDelay);
		} else if (imgData != null) {
			this.imgDelay.put(cid.imgDelay);
		}
		this.xHotSpot = cid.xHotSpot;
		this.yHotSpot = cid.yHotSpot;
		cid = null;
	}

	void assembleCursor(final ArrayList<byte[]> icons, final int[] rate, final int[] animSeq, final int jiffy,
			final int steps, final int width, final int height) throws IOException {
		// Jiffy multiplicator for LWJGL's delay, which is in milisecond.
		final int MULT = 17;
		this.numImages = icons.size();
		int frRate = 0;
		byte[] frame = new byte[0];
		// if we have an animation sequence we use that
		// since the sequence can be larger than the number
		// of images in the ani if it reuses one or more of those
		// images.
		if (animSeq != null && animSeq.length > 0) {
			for (int i = 0; i < animSeq.length; i++) {
				if (rate != null) {
					frRate = rate[i] * MULT;
				} else {
					frRate = jiffy * MULT;
				}
				// the frame # is the one in the animation sequence
				frame = icons.get(animSeq[i]);
				this.addFrame(frame, frRate, jiffy, width, height, animSeq.length);
				// System.out.println("delay of " + frRate);
			}
		} else {
			for (int i = 0; i < icons.size(); i++) {
				frame = icons.get(i);
				if (rate == null) {
					frRate = jiffy * MULT;
				} else {
					frRate = rate[i] * MULT;
				}
				this.addFrame(frame, frRate, jiffy, width, height, 0);
				// System.out.println("delay of " + frRate);
			}
		}
	}

	/**
	 * Called to rewind the buffers after filling them.
	 */
	void completeCursor() {
		if (this.numImages == 1) {
			this.imgDelay = null;
		} else {
			this.imgDelay.rewind();
		}
		this.data.rewind();
	}
}

}

[/java]

And last but not least a simple TestCase, so it is easily verifiable
[java]
package com.jme3x.jfx;

import org.lwjgl.input.Mouse;

import com.jme3.app.SimpleApplication;
import com.jme3.cursors.plugins.JmeCursor;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Quad;

public class CursorHotspotTest extends SimpleApplication {
private Geometry hotspotDebug;

public static void main(final String[] args) {
	new CursorHotspotTest().start();
}

@Override
public void simpleInitApp() {
	this.flyCam.setEnabled(false);
	final JmeCursor loaded = (JmeCursor) this.assetManager.loadAsset("com/jme3x/jfx/cursor/proton/aero_cross.cur");
	this.inputManager.setMouseCursor(loaded);

	final Quad hotspot = new Quad(2, 2);
	this.hotspotDebug = new Geometry("gurke", hotspot);
	final Material hotspotMaterial = new Material(this.assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
	hotspotMaterial.setColor("Color", ColorRGBA.Cyan);
	this.hotspotDebug.setMaterial(hotspotMaterial);

	this.guiNode.attachChild(this.hotspotDebug);
	this.hotspotDebug.setLocalTranslation(500, 500, 0);
}

@Override
public void simpleUpdate(final float tpf) {
	this.hotspotDebug.setLocalTranslation(Mouse.getX(), Mouse.getY(), 0);
}

}

[/java]

2 Likes

Oooo
 thanks for this.

Been wondering about this for a while
 figured it was just me doing something wrong :wink:

EDIT: Just to further his point about others needing this. When using custom cursor in the tonegodGUI, the text pointer (caret looking thing) is just all sorts of ganked and confused since the hotspot is always at 0,0.

Would love to see this implemented as well.

Have you tried something like
 oh I don’t know, setting the hotspot manually?

[java]
JmeCursor cur = (JmeCursor) assetManager.loadAsset(“Textures/GUI/Cursors/myOwnUglyCursor.ico”);
cur.setxHotSpot(10);
cur.setyHotSpot(10);
[/java]

That’s supposed to work.

1 Like
@madjack said: Have you tried something like... oh I don't know, setting the hotspot manually?

[java]
JmeCursor cur = (JmeCursor) assetManager.loadAsset(“Textures/GUI/Cursors/myOwnUglyCursor.ico”);
cur.setxHotSpot(10);
cur.setyHotSpot(10);
[/java]

That’s supposed to work.

I tried this (iirc) and it didn’t work. But, I’ll go try again now and let you know if I was wrong.

And I was wrong
 this works fine.

Thanks @madjack I’ll update according until the patch above is implemented.

EDIT: Problem with setting it manually is
 it works fine in my particular case because it is the direct center of the cursor. Since you can add any cursors you like to the library
 these will always act improperly (unless the hotspot is supposed to be 0,0) as I can’t guess what the original intent was.

@t0neg0d said: EDIT: Problem with setting it manually is... it works fine in my particular case because it is the direct center of the cursor. Since you can add any cursors you like to the library... these will always act improperly (unless the hotspot is supposed to be 0,0) as I can't guess what the original intent was.

What has the middle of the icon has to do with anything? Not sure I follow.

WellI know I can do this, but seriously if I have 3 cursor themes with 15cursors each, this sucks.
It’s always better to just do what the format is supposed to do.

Yes I agree, but about 15% of the cursors I tested when I did that seem to have their own ways of doing things and ended up broken and unworking. That’s not my fault if they’re using a different format while using the same extension.

The working extensions are those that are following Microsoft’s definition. The others I have no intention of supporting them. So either you redo the whole import part in a non-chaotic way to support all those weird formats, or you use the proper format that has been tested and shown to work.

Your choice.

@madjack said: What has the middle of the icon has to do with anything? Not sure I follow.

I meant cursor
 the specific case I was using can be solved without actually having to know where the person who created the cursor placed the hotspot. This would be the rare exception, however.

Sooo
 maybe someone could suggest the proper tool for creating cursors that will read the hotspot correctly? And a note in the JavaDocs about what standard is being used might also be helpful.

I don’t really care what the fix is, as long as I have an answer for myself and that I can pass along to other people.

@t0neg0d said: I meant cursor... the specific case I was using can be solved without actually having to know where the person who created the cursor placed the hotspot. This would be the rare exception, however.

That’s the drawback. Fortunately it doesn’t have to be exact to the pixel and it’s usually fairly easy to figure out.

@madjack said: That's the drawback. Fortunately it doesn't have to be exact to the pixel and it's usually fairly easy to figure out.

I fail to see how I could figure it out when I’m not the one creating or adding the cursors to the GUI theme. I guess I could force the user to define the hotspot coords in the XML as a failsafe. And if not defined than default to 0,0.

Yeah
 that’s good enough for me.

I’m just happy the hotspot can be adjusted.

Thanks =)

@t0neg0d said: I fail to see how I could figure it out when I'm not the one creating or adding the cursors to the GUI theme.

Cursors are usually very intuitive when it comes to figuring out where the hotspot is. Look at the default windows cursors. Hint: it’s the tip of the arrow. :wink:

In my mind, if it’s not easy then the author did it wrong. :wink:

Edit:
Forgot to mention that I can’t remember the name of the application I used to test those ico, cur and ani. I’ll try to find it again and repost.

@madjack said: Yes I agree, but about 15% of the cursors I tested when I did that seem to have their own ways of doing things and ended up broken and unworking. That's not my fault if they're using a different format while using the same extension.

The working extensions are those that are following Microsoft’s definition. The others I have no intention of supporting them. So either you redo the whole import part in a non-chaotic way to support all those weird formats, or you use the proper format that has been tested and shown to work.

Your choice.

The hotspot position as read by me is 100% exactly what the microsoft specification says.

Btw for the determining issue, see the posted testCursorHotspot testcase, it shows with a small 2x2 pixel quad where the percieved cursor position is.

Ps. For some reason I read your answer really toxic, I just hope this is a misinterpretion, all I want is to get some kind of cursor format into jme with hotspots, it’s not that much asked.

@madjack said: Cursors are usually very intuitive when it comes to figuring out where the hotspot is. Look at the default windows cursors. Hint: it's the tip of the arrow. ;)


I see the arrow
 though
 god knows where the point is going to be


Which arrow? Which point? And am I sure the hotspot falls inside a non-transparent pixel?

This is the problem I’d be faced with if hotspots are not defined by either a) the importer or b) in the xml defining the cursor =(

But, I think defining them in XML is a workable solution.

@t0neg0d said: I see the arrow... though... god knows where the point is going to be
Eyeballing it says: 3, 3 (from top left)
Which arrow? Which point? And am I sure the hotspot falls inside a non-transparent pixel?
This one should be middle of the image. Most people won't make a difference whether it's pixel perfect or not. Stop worrying. :P

The software I used was AniTuner. I uninstalled it like last week, as things like that happen.

Animated Cursor Software

1 Like
@madjack said: Eyeballing it says: 3, 3 (from top left)

This one should be middle of the image. Most people won’t make a difference whether it’s pixel perfect or not. Stop worrying. :stuck_out_tongue:

I think you missed what the problem was
 these cursors aren’t being added by me. I’d know exactly what pixel the hotspot was otherwise :wink:

Not sure if you’re telling me your users could be doorknob-dumb? :stuck_out_tongue: But I guess you’re right, giving them a way to specify (or override) the hotspot would be ideal.

As I said, some of those cursors won’t work at all despite the code. They don’t show up, some won’t animate properly or are just oversized or broken. Due diligence should be applied by whoever wants to use the given cursor.

I dont get this, what is the actually problem with just using the provided patch and just load them from the cursor, as it is supposed to be?

As long as the cursors are valid .cur file it will work.

And it’s not like this would break the ability to manually override the hotspot., So nothing lost, only a gain.

2 Likes