Custom mouse cursor. [Committed]

I’ve looked around for an implementation to use custom cursors and I couldn’t find anything. The only thing I found that looked like it was using the gui node and I don’t really like that to be honest. I knew that was possible to do this in LWJGL so I modified jME3 to make that a reality.



For the following video I used a little something I found on The Cursor Library. :wink:



The changes are done in 5 files.



setMouseCursor in InputManager.java ← method called by the user where the cursor is actually changed.



MouseInput.java

LwjglMouseInput.java

AwtMouseInput.java

DummyInput.java



I haven’t implemented the functionality in AwtMouseInput and DummyInput though.



Works fine on my end as you can see below.

http://www.youtube.com/watch?v=32KcKY9I-r0



If there is interest I can post the changes and/or post the code for approval for integration.

7 Likes

cough ?

Can you provide a test build for OS X? I’ve tried a custom cursor once but it didn’t work… did not put that much effort in it though…

Sure.



First, you have to create the cursor. Here’s how I did it with a static cursor. (I’m currently checking on using animated cursors, unsure when/if I’ll implement that though.)



This is rather quick and dirty to be honest. All I wanted at this point was to have it work, so nothing fancy.

[java]

private void setMouseCursor() {



Texture tex = assetManager.loadTexture(“Textures/GUI/Cursors/derp.png”);



Image img = tex.getImage();



ByteBuffer data = img.getData(0);

data.rewind();

IntBuffer image = BufferUtils.createIntBuffer(img.getHeight() * img.getWidth());

for (int y = 0; y < img.getHeight(); y++){

for (int x = 0; x < img.getWidth(); x++){

int rgba = data.getInt();

image.put(rgba);

}

}

image.rewind();



Cursor cur = null;

try {

cur = new Cursor(img.getWidth(), img.getHeight(), 1, img.getHeight() - 1, 1, image, null);

} catch (LWJGLException ex) {

Logger.getLogger(Disenthral.class.getName()).log(Level.SEVERE, null, ex);

}



inputManager.setMouseCursor(cur);

image.clear();

}

[/java]



Here’s the diffs for each file.

LwjglMouseInput.java

[patch]

— Base (BASE)

+++ Locally Modified (Based On LOCAL)

@@ -150,4 +150,13 @@

return Sys.getTime() * LwjglTimer.LWJGL_TIME_TO_NANOS;

}


  • public Cursor setNativeCursor(Cursor cursor) {
  •    try {<br />
    
  •        return Mouse.setNativeCursor(cursor);<br />
    
  •    } catch (LWJGLException ex) {<br />
    
  •        Logger.getLogger(LwjglMouseInput.class.getName()).log(Level.SEVERE, null, ex);<br />
    

}

  •    return null;<br />
    
  • }

    +

    +}

    [/patch]



    AwtMouseInput.java

    [patch]

    — Base (BASE)

    +++ Locally Modified (Based On LOCAL)

    @@ -312,4 +312,8 @@

    }

    return index;

    }

    +
  • public org.lwjgl.input.Cursor setNativeCursor(org.lwjgl.input.Cursor cursor) {
  •    throw new UnsupportedOperationException(&quot;Not supported yet.&quot;);<br />
    

}

+}

[/patch]



MouseInput.java

[patch]

— Base (BASE)

+++ Locally Modified (Based On LOCAL)

@@ -32,6 +32,8 @@



package com.jme3.input;



+import org.lwjgl.input.Cursor;

+

/**

  • A specific API for interfacing with the mouse.

    */

    @@ -80,4 +82,11 @@
  • @return the number of buttons the mouse has.

    */

    public int getButtonCount();

    +
  • /**
  • * Sets the cursor to use.<br />
    
  • * @param cursor The cursor to use.<br />
    
  • * @return The previous Cursor object set. May be null.<br />
    
  • */<br />
    
  • public Cursor setNativeCursor(Cursor cursor);

    }

    [/patch]



    InputManager.java

    [patch]

    — Base (BASE)

    +++ Locally Modified (Based On LOCAL)

    @@ -42,6 +42,7 @@

    import java.util.HashMap;

    import java.util.logging.Level;

    import java.util.logging.Logger;

    +import org.lwjgl.input.Cursor;



    /**
  • The <code>InputManager</code> is responsible for converting input events

    @@ -386,6 +387,10 @@

    }

    }


  • public void setMouseCursor(Cursor cursor) {
  •    mouse.setNativeCursor(cursor);<br />
    
  • }

    +

    /**
  • Callback from RawInputListener. Do not use.

    */

    [/patch]



    and finally DummyMouseInput.java

    [patch]

    — Base (BASE)

    +++ Locally Modified (Based On LOCAL)

    @@ -33,6 +33,7 @@

    package com.jme3.input.dummy;



    import com.jme3.input.MouseInput;

    +import org.lwjgl.input.Cursor;



    /**
  • DummyMouseInput as an implementation of <code>MouseInput</code> that raises no

    @@ -51,4 +52,8 @@

    return 0;

    }


  • public Cursor setNativeCursor(Cursor cursor) {
  •    throw new UnsupportedOperationException(&quot;Not supported yet.&quot;);<br />
    

}

+

+}

[/patch]



That should work.



Note that some png files might not work 100%. I have some idea why that might be, but as of now, I haven’t put my finger on it. You might want to try simple cursors, black and white for example.

We cannot accept this patch because it will crash on Android (LWJGL dependency in InputManager).

@Momoko_Fan said:
We cannot accept this patch because it will crash on Android (LWJGL dependency in InputManager).


What do you suggest? I've never touched any part of Android, so I'm clueless on that front.
@madjack said:
What do you suggest? I've never touched any part of Android, so I'm clueless on that front.

You'd have to find a similar function for android and abstract it so far that you can plug one or the other in.
@normen said:
You'd have to find a similar function for android and abstract it so far that you can plug one or the other in.


So if I get this right, I'll have to find how they change the cursors on Android (pure openGL) and reflect the changes I've done on the "desktop" part of jME3 to the Android part of jME3. Right?

I think I can figure this out. :)
@madjack said:
So if I get this right, I'll have to find how they change the cursors on Android (pure openGL) and reflect the changes I've done on the "desktop" part of jME3 to the Android part of jME3. Right?

Yeah, basically. Just think about the fact that other backends would have to be able to implement this as well so the interface should be slick.
@madjack said:
So if I get this right, I'll have to find how they change the cursors on Android (pure openGL) and reflect the changes I've done on the "desktop" part of jME3 to the Android part of jME3. Right?

I think I can figure this out. :)

Normally, yes.
But since Android doesn't seem to have an API for changing cursor, you don't have to do that. Simply make available a stub on Android that does nothing for now.

@madjack: Keep going! There are some pirates who would be very happy to have that. ^^

@ceiphren, if this doesn’t work out, I have a code I can give you. You must first disable the cursor (setCursorVisible(false), then using the mouse’s value calculate a new position for an image on the guiNode. I doubt if this is as efficient as madjack’s, but if you want it, just tell me.

Just had time to check up on this and I couldn’t find any dependency for the changes above except for the following in OGLESContext.java:



[java]

@Override

public MouseInput getMouseInput() {

return new DummyMouseInput();

}

[/java]



But I already added the method in DummyMouseInput and it returns null without doing anything.



There’s also the possibility someone could call something along the line of :



[java]androidHarness.getJmeApplication().getInputManager().setMouseCursor(someCursor);[/java]



And I have no clue from inside InputManager how I could detect if the application is running on Android or not. Actually, I could use something like

[java]

public void setMouseCursor(Cursor cursor) {

if (!(mouse instanceof DummyMouseInput)) {

mouse.setNativeCursor(cursor);

}

}

[/java]

… But since that method in DummyMouseInput already returns null without doing anything, that’s moot.



In short, I need more information please. I’m pretty much clueless here. :cry:

Sounds like a neat feature! May the force be with you madjack :slight_smile:

The issue is that you’re importing a class in org.lwjgl into InputManager, which is supposed to be independent of context implementation. If the user doesn’t include jME3-lwjgl.jar in their Android app, it will crash. So in order for this to work we’ll need a jME3 Cursor class which is then converted to a LWJGL cursor when used in the LWJGL implementation of MouseInput.

Suggestion: change the signature to setMouseCursor(String file) or. Pass the Image. Then move the cursor creation stuff down into the implementations.

Gotcha. I think what I’m working on right now might be the answer to that actually, but it’s not working yet.



If E3 isn’t too distracting today I might have some result by the “end of the day”. :open_mouth:

Hey how about (as a template on how to do this, for a jme patch a little bit more is needed in terms of code quality, especially that currently code needs to be changed to support other cursor, since the constructor has hard coded image files), no changes to jme core classes however, also support for animated.

Also deals with buggy transparency in windows (only 1 bit alpha supported),

Currently limited to png, (and any other image delivering ABGR8), since else the creation of the animated Cursor TextureAtlas lwjgl uses would be somewhat difficult.



Applications can simply decide in logic (wich os) if they do the call, or not (android).





AnimatedCursorLoader

Code:
package online.newhorizons.gui.cursor;

import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
import com.jme3.texture.Image;

public class AnimationCursorLoader implements AssetLoader {

@Override
public Object load(final AssetInfo assetInfo) throws IOException {
	final InputStream descriptor = assetInfo.openStream();
	final Scanner c = new Scanner(descriptor);
	int lines = 0;
	while (c.hasNextLine()) {
		c.nextLine();
		lines++;
	}
	descriptor.close();

	final InputStream descriptor2 = assetInfo.openStream();
	final Scanner sin = new Scanner(descriptor2);
	final AnimationData returnvalue = new AnimationData();
	
	returnvalue.images = new Image[lines];
	returnvalue.delays = new int[lines];

	lines = 0;
	while (sin.hasNextLine()) {
		final String line = sin.nextLine();
		final String[] parts = line.split(&quot; &quot;);
		final int delay = Integer.parseInt(parts[0]);
		final String image = parts[1];
		returnvalue.images[lines] = assetInfo.getManager().loadTexture(assetInfo.getKey().getFolder() + image).getImage();
		returnvalue.delays[lines] = delay;
		lines++;
	}
	descriptor2.close();
	return returnvalue;
}

}


AnimationDataStruct
Code:
package online.newhorizons.gui.cursor;

import com.jme3.texture.Image;

public class AnimationData {
public Image[] images;
public int[] delays;
}


Cursor Manager
Code:
package online.newhorizons.gui.cursor;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;

import org.lwjgl.LWJGLException;
import org.lwjgl.input.Cursor;
import org.lwjgl.input.Mouse;

import com.jme3.asset.AssetManager;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.util.BufferUtils;

public class CursorManager {
public enum CursorType {

	Default, Text, NW_Resize, SE_Resize, SW_Resize, NE_Resize, N_Resize, S_Resize, E_Resize, W_Resize, Hyperlink, Move, Scroll_Right, Scroll_RightFast, Scroll_Left, Scroll_LeftFast
};

private static AssetManager assetManager;
private static int currentCursor;
private final static Cursor[] CURSOR = new Cursor[CursorType.values().length];

/**
 * Loads and sets a hardware cursor
 * 
 * @param url
 *            to imagefile
 * @param xHotspot
 *            from image left
 * @param yHotspot
 *            from image bottom
 */
public CursorManager(final AssetManager assetmanager) {

	CursorManager.currentCursor = -1;
	CursorManager.assetManager = assetmanager;
	CursorManager.assetManager.registerLoader(AnimationCursorLoader.class, &quot;anim&quot;);

	CursorManager.CURSOR[CursorType.Default.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_standard_trans.png&quot;, 0, 15);
	CursorManager.CURSOR[CursorType.Text.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_text.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.NW_Resize.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_nwse.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.SE_Resize.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_nwse.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.SW_Resize.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_nesw.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.NE_Resize.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_nesw.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.N_Resize.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_topdown.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.S_Resize.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_topdown.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.E_Resize.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_leftright.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.W_Resize.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_leftright.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.Scroll_Left.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_leftscrollslow.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.Scroll_LeftFast.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_leftscrollfast.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.Scroll_Right.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_rightscrollslow.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.Scroll_RightFast.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_rightscrollfast.png&quot;, 8, 8);
	CursorManager.CURSOR[CursorType.Move.ordinal()] = CursorManager.precache(&quot;/assets/cursor/cursor_move.anim&quot;, 32, 32);
}

private static synchronized Cursor precache(final String file, final int xHotspot, final int yHotspot) {
	if (CursorManager.assetManager == null) {
		throw new RuntimeException(&quot;CursorManager not initialized&quot;);
	}

	Image[] animation = null;
	int[] delay = null;

	if (file.contains(&quot;.anim&quot;)) {
		final Object asset = CursorManager.assetManager.loadAsset(file);
		final AnimationData data = (AnimationData) asset;
		animation = data.images;
		delay = data.delays;
	} else {
		animation = new Image[1];
		animation[0] = CursorManager.assetManager.loadTexture(file).getImage();
	}

	final int imageWidth = animation[0].getWidth();
	final int imageHeight = animation[0].getHeight();

	final ByteBuffer imageDataCopy = BufferUtils.createByteBuffer(imageWidth * imageHeight * animation.length * 4);

	for (final Image frame : animation) {
		assert frame.getFormat().equals(Format.ABGR8) : &quot;False format &quot; + file;
		assert frame.getWidth() == imageWidth : &quot;All animation Frames must have same width&quot;;
		assert frame.getHeight() == imageHeight : &quot;All animation Frames must have same width&quot;;

		final ByteBuffer imageData = frame.getData(0);
		imageData.rewind();
		for (int y = 0; y &lt; imageHeight; y++) {
			for (int x = 0; x &lt; imageWidth; x++) {
				byte alpha = imageData.get();
				// b-g-r-a trial and error format discovery
				imageDataCopy.put(imageData.get());
				imageDataCopy.put(imageData.get());
				imageDataCopy.put(imageData.get());
				if (alpha &lt; 0) {
					// i don't even want to think about this (windows hack
					// since windows only supports 1bit alpha)
					alpha = (byte) 255f;
				} else {
					alpha = 0;
				}

				imageDataCopy.put(alpha);
			}
		}
	}
	imageDataCopy.rewind();
	try {
		if (delay != null) {
			final IntBuffer delays = BufferUtils.createIntBuffer(delay.length);
			for (final int d : delay) {
				delays.put(d);
			}
			delays.rewind();
			return new Cursor(imageWidth, imageHeight, xHotspot, yHotspot, delay.length, imageDataCopy.asIntBuffer(), delays);
		}
		return new Cursor(imageWidth, imageHeight, xHotspot, yHotspot, 1, imageDataCopy.asIntBuffer(), null);

	} catch (final LWJGLException e) {
		throw new RuntimeException(e);
	}

}

public static synchronized void setHardwareCursor(final CursorType cursortype) {
	if (CursorManager.currentCursor != cursortype.ordinal()) {
		final Cursor cursor = CursorManager.CURSOR[cursortype.ordinal()];
		if (!cursor.equals(Mouse.getNativeCursor())) {
			try {
				Mouse.setNativeCursor(cursor);
			} catch (final LWJGLException e) {
				throw new RuntimeException(e);
			}
		}
	}
}

}

NEWS!



What I’ve done is implemented a new JmeCursor class that can be used to change the cursor. That effectively removes the lwjgl dependency in InputManager as it is this cursor you will need to pass to the InputManager.



What I really wanted was being able to use any .ICO or .CUR or .ANI that follows Microsoft’s standards. This isn’t really hard in itself for .ICO and .CUR as those contain only either .bmp or .png image data plus some basic info about the cursor itself. The meat of the work was on .ANI, the animated cursors format. It’s now done and working.



What you need to know is the .ani you might want to use HAS to follow the RIFF format and be LittleEndien because that’s the way the format works and I’m not aware if there is any other format (I’m sure there is, but it’s not supported as of now and might not be in the future).



Anyway, I’ve tested my modifications and it works fine with a nice animated cursor I found on the link I pasted above a couple of days ago.



So, as you can imagine, I’m at the testing phase now. I’ll be getting several animated icons and test them out to see if they’ll work or not. Hopefully they will.



Because of that I’ve had to implement a CursorLoader, obviously. I’ve also made a new package com.jme3.cursors and put those two classes there.



I’ll post a little video showcasing the animated cursor in a bit.

1 Like

Gah. Ran into a bone.



There’ll be a bit of a delay.



Sorry.