From 5ac9953f62beee50f04661bde25557041906a48a Mon Sep 17 00:00:00 2001 From: "dan..om" Date: Mon, 11 Jun 2012 21:18:52 +0000 Subject: [PATCH] Implemented the ability to change the mouse cursor. The way to do this is by calling inputManager.setMouseCursor("path/to/cursor/in/assets/cursor.ani"). NOTE: Supported formats are "RIFF-wrapped" .ani, .cur and .ico. Those can be found everywhere on the web and are the majority of icons found. If the file format is unrecognized the loader will crash with an appropriate message (hopefully). git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@9484 75d07b2b-3a1a-0410-a2c5-0572b91ccdca --- .../jme3/cursors/plugins/CursorLoader.java | 672 ++++++++++++++++++ .../com/jme3/cursors/plugins/JmeCursor.java | 151 ++++ engine/src/core/com/jme3/asset/Desktop.cfg | 3 +- .../src/core/com/jme3/input/InputManager.java | 147 ++-- .../src/core/com/jme3/input/MouseInput.java | 20 +- .../com/jme3/input/dummy/DummyMouseInput.java | 6 +- .../com/jme3/input/awt/AwtMouseInput.java | 10 +- .../com/jme3/input/lwjgl/LwjglMouseInput.java | 21 +- .../com/jme3/niftygui/InputSystemJme.java | 4 +- 9 files changed, 948 insertions(+), 86 deletions(-) create mode 100644 engine/src/core-plugins/com/jme3/cursors/plugins/CursorLoader.java create mode 100644 engine/src/core-plugins/com/jme3/cursors/plugins/JmeCursor.java diff --git a/engine/src/core-plugins/com/jme3/cursors/plugins/CursorLoader.java b/engine/src/core-plugins/com/jme3/cursors/plugins/CursorLoader.java new file mode 100644 index 000000000..36fbbecd4 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/cursors/plugins/CursorLoader.java @@ -0,0 +1,672 @@ +package com.jme3.cursors.plugins; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetLoader; +import com.jme3.util.BufferUtils; +import com.jme3.util.LittleEndien; +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; + +/** + * + * @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. + */ + public JmeCursor load(AssetInfo info) throws IOException { + + isIco = ((AssetKey) info.getKey()).getExtension().equals("ico"); + if (!isIco) { + isIco = ((AssetKey) info.getKey()).getExtension().equals("cur"); + if (!isIco) { + isAni = ((AssetKey) info.getKey()).getExtension().equals("ani"); + } + } + if (!isAni && !isIco) { + throw new IllegalArgumentException("Cursors supported are .ico, .cur or .ani"); + } + + InputStream in = null; + try { + in = info.openStream(); + return loadCursor(in); + } finally { + if (in != null) { + in.close(); + } + } + } + + private JmeCursor loadCursor(InputStream inStream) throws IOException { + + byte[] icoimages = new byte[0]; // new byte [0] facilitates read() + + if (isAni) { + 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 icons; + + DataInput leIn = new LittleEndien(inStream); + 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 = getNext(leIn); + if (nextInt == 0x4e4f4341) { + // We have ACON, we do nothing +// System.out.println("We have ACON. Next!"); + nextInt = 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. + 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 + 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'."); + int length = leIn.readInt(); + nextInt = leIn.readInt(); + if (nextInt == 0x4f464e49) { // Got an INFO, skip its length + // this part consist of Author, title, etc +// int skipped = 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. + int icoLength = leIn.readInt(); + ciDat.numImages = numIcons; + icons = new ArrayList(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); + } + byte[] data = new byte[icoLength]; + ((InputStream) leIn).read(data, 0, icoLength); + 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 setJmeCursor(ciDat); + + } else if (riff == 0x58464952) { + throw new IllegalArgumentException("Big-Endian RIFX is not supported. Sorry."); + } else { + throw new IllegalArgumentException("Unknown format."); + } + } else if (isIco) { + DataInputStream in = new DataInputStream(inStream); + int bytesToRead; + while ((bytesToRead = in.available()) != 0) { + byte[] icoimage2 = new byte[icoimages.length + bytesToRead]; + System.arraycopy(icoimages, 0, icoimage2, 0, icoimages.length); + in.read(icoimage2, icoimages.length, bytesToRead); + icoimages = icoimage2; + } + } + + BufferedImage bi[] = parseICOImage(icoimages); + CursorImageData cid = new CursorImageData(bi, 0, 0, 0); + cid.completeCursor(); + + return setJmeCursor(cid); + } + + private JmeCursor setJmeCursor(CursorImageData cid) { + 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. + int FDE_OFFSET = 6; // first directory entry offset + int DE_LENGTH = 16; // directory entry length + 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 = ubyte(icoimage[5]); + numImages <<= 8; + numImages |= icoimage[4]; + bi = new BufferedImage[numImages]; + int[] colorCount = new int[numImages]; + + for (int i = 0; i < numImages; i++) { + int width = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH]); + + int height = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 1]); + + colorCount[i] = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 2]); + + int bytesInRes = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 11]); + bytesInRes <<= 8; + bytesInRes |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 10]); + bytesInRes <<= 8; + bytesInRes |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 9]); + bytesInRes <<= 8; + bytesInRes |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 8]); + + int imageOffset = ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 15]); + imageOffset <<= 8; + imageOffset |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 14]); + imageOffset <<= 8; + imageOffset |= ubyte(icoimage[FDE_OFFSET + i * DE_LENGTH + 13]); + imageOffset <<= 8; + imageOffset |= 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 = ubyte(icoimage[imageOffset + 7]); + _width <<= 8; + _width |= ubyte(icoimage[imageOffset + 6]); + _width <<= 8; + _width |= ubyte(icoimage[imageOffset + 5]); + _width <<= 8; + _width |= ubyte(icoimage[imageOffset + 4]); + + // If width is 0 (for 256 pixels or higher), _width contains + // actual width. + + if (width == 0) { + width = _width; + } + + int _height = ubyte(icoimage[imageOffset + 11]); + _height <<= 8; + _height |= ubyte(icoimage[imageOffset + 10]); + _height <<= 8; + _height |= ubyte(icoimage[imageOffset + 9]); + _height <<= 8; + _height |= 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 = ubyte(icoimage[imageOffset + 13]); + planes <<= 8; + planes |= ubyte(icoimage[imageOffset + 12]); + + int bitCount = ubyte(icoimage[imageOffset + 15]); + bitCount <<= 8; + bitCount |= 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. + + int colorTableOffset = imageOffset + BMIH_LENGTH; + + if (colorCount[i] == 2) { + int xorImageOffset = colorTableOffset + 2 * 4; + + int scanlineBytes = calcScanlineBytes(width, 1); + int andImageOffset = xorImageOffset + scanlineBytes * height; + + 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 ((ubyte(icoimage[xorImageOffset + row + * scanlineBytes + col / 8]) + & masks[col % 8]) != 0) { + index = 1; + } else { + index = 0; + } + + int rgb = 0; + rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + + 2])); + rgb <<= 8; + rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + + 1])); + rgb <<= 8; + rgb |= (ubyte(icoimage[colorTableOffset + index + * 4])); + + if ((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) { + int xorImageOffset = colorTableOffset + 16 * 4; + + int scanlineBytes = calcScanlineBytes(width, 4); + int andImageOffset = xorImageOffset + scanlineBytes * height; + + 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 = ubyte(icoimage[xorImageOffset + row + * scanlineBytes + col / 2]); + index >>= 4; + } else { + index = ubyte(icoimage[xorImageOffset + row + * scanlineBytes + col / 2]) + & 15; + } + + int rgb = 0; + rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + + 2])); + rgb <<= 8; + rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + + 1])); + rgb <<= 8; + rgb |= (ubyte(icoimage[colorTableOffset + index + * 4])); + + if ((ubyte(icoimage[andImageOffset + row + * 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) { + int xorImageOffset = colorTableOffset + 256 * 4; + + int scanlineBytes = calcScanlineBytes(width, 8); + int andImageOffset = xorImageOffset + scanlineBytes * height; + + 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 = ubyte(icoimage[xorImageOffset + row + * scanlineBytes + col]); + + int rgb = 0; + rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + + 2])); + rgb <<= 8; + rgb |= (ubyte(icoimage[colorTableOffset + index * 4 + + 1])); + rgb <<= 8; + rgb |= (ubyte(icoimage[colorTableOffset + index * 4])); + + if ((ubyte(icoimage[andImageOffset + row + * 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) { + int scanlineBytes = calcScanlineBytes(width, 32); + + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + int rgb = ubyte(icoimage[colorTableOffset + row + * scanlineBytes + col * 4 + 3]); + rgb <<= 8; + rgb |= ubyte(icoimage[colorTableOffset + row + * scanlineBytes + col * 4 + 2]); + rgb <<= 8; + rgb |= ubyte(icoimage[colorTableOffset + row + * scanlineBytes + col * 4 + 1]); + rgb <<= 8; + rgb |= ubyte(icoimage[colorTableOffset + row + * scanlineBytes + col * 4]); + + bi[i].setRGB(col, height - 1 - row, rgb); + } + } + } + } else if (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(byte b) { + return (b < 0) ? 256 + b : b; // Convert byte to unsigned byte. + } + + private int calcScanlineBytes(int width, 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(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(BufferedImage[] bi, int delay, int hsX, int hsY) { + IntBuffer singleCursor = null; + ArrayList cursors = new ArrayList(); + int bwidth = 0; + int bheight = 0; + // cursor type + // 1 - ICO + // 2 - CUR + // Anything else is invalid. + int curType = 0; + + // make the cursor image + for (int i = 0; i < bi.length; i++) { + BufferedImage img = bi[i]; + bwidth = img.getWidth(); + bheight = img.getHeight(); + curType = img.getType(); + 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 { + throw new IllegalArgumentException( + "An image contained is not of the right type! Only proper ICO and CUR formats are valid."); + } + + // We flip our image because .ICO and .CUR will always be reversed. + AffineTransform trans = AffineTransform.getScaleInstance(1, -1); + trans.translate(0, -img.getHeight(null)); + AffineTransformOp op = new AffineTransformOp(trans, AffineTransformOp.TYPE_BILINEAR); + img = op.filter(img, null); + + singleCursor = BufferUtils.createIntBuffer(img.getWidth() * img.getHeight()); + DataBufferInt dataIntBuf = (DataBufferInt) img.getData().getDataBuffer(); + singleCursor = IntBuffer.wrap(dataIntBuf.getData()); + cursors.add(singleCursor); + } + + // put the image in the IntBuffer + data = BufferUtils.createIntBuffer(bwidth * bheight); + imgDelay = BufferUtils.createIntBuffer(bi.length); + for (int i = 0; i < cursors.size(); i++) { + data.put(cursors.get(i)); + if (delay > 0) { + imgDelay.put(delay); + } + } + width = bwidth; + height = bheight; + xHotSpot = hsX; + yHotSpot = hsY; + numImages = cursors.size(); + data.rewind(); + if (imgDelay != null) { + imgDelay.rewind(); + } + } + + private void addFrame(byte[] imgData, int rate, int jiffy, int width, int height) throws IOException { + BufferedImage bi[] = parseICOImage(imgData); + int hotspotx = 0; + int hotspoty = 0; + 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. Leave at 0; + hotspotx = 0; + hotspoty = 0; + } +// System.out.println("Image type = " + (type == 1 ? "CUR" : "ICO")); + if (rate == 0) { + rate = jiffy; + } + CursorImageData cid = new CursorImageData(bi, rate, hotspotx, hotspoty); + if (width == 0) { + this.width = cid.width; + } else { + this.width = width; + } + if (height == 0) { + this.height = cid.height; + } else { + this.height = height; + } + if (data == null) { + data = BufferUtils.createIntBuffer(this.width * this.height * numImages); + data.put(cid.data); + } else { + data.put(cid.data); + } + if (imgDelay == null && numImages > 1) { + imgDelay = BufferUtils.createIntBuffer(numImages); + imgDelay.put(cid.imgDelay); + } else if (imgData != null) { + imgDelay.put(cid.imgDelay); + } + xHotSpot = cid.xHotSpot; + yHotSpot = cid.yHotSpot; + cid = null; + } + + void assembleCursor(ArrayList icons, int[] rate, int[] animSeq, int jiffy, int steps, int width, int height) throws IOException { + // Jiffy multiplicator for LWJGL's delay, which is in milisecond. + final int MULT = 17; + 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) { + 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]); + addFrame(frame, frRate, jiffy, width, height); +// 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; + } + addFrame(frame, frRate, jiffy, width, height); +// System.out.println("delay of " + frRate); + } + } + } + + /** + * Called to rewind the buffers after filling them. + */ + void completeCursor() { + if (numImages == 1) { + imgDelay = null; + } else { + imgDelay.rewind(); + } + data.rewind(); + } + } +} diff --git a/engine/src/core-plugins/com/jme3/cursors/plugins/JmeCursor.java b/engine/src/core-plugins/com/jme3/cursors/plugins/JmeCursor.java new file mode 100644 index 000000000..3cc4b62c9 --- /dev/null +++ b/engine/src/core-plugins/com/jme3/cursors/plugins/JmeCursor.java @@ -0,0 +1,151 @@ +package com.jme3.cursors.plugins; + +import java.nio.IntBuffer; + +/** + * A Jme representation of the LWJGL Cursor class. + * + * @author MadJack + * @creation Jun 6, 2012 12:12:38 PM + */ +public class JmeCursor { + + private int width; + private int height; + private int xHotSpot; + private int yHotSpot; + private int numImages; + private IntBuffer imagesData; + private IntBuffer imagesDelay; + + /** + * Queries the cursor's height. Note that + * the coordinate system is the same as OpenGL. 0, 0 being lower left. + * @return the height in pixel. + */ + public int getHeight() { + return height; + } + + /** + * Queries the cursor's images' data. + * @return An {@link IntBuffer} containing the cursor's image(s) data in + * sequence. + */ + public IntBuffer getImagesData() { + return imagesData; + } + + /** + * Queries the cursor's delay for each frame. + * @return An {@link IntBuffer} containing the cursor's delay in + * sequence. The delay is expressed in milliseconds. + */ + public IntBuffer getImagesDelay() { + return imagesDelay; + } + + /** + * Queries the number of images contained in the cursor. Static cursors should + * contain only 1 image. + * @return The number of image(s) composing the cursor. 1 if the cursor is + * static. + */ + public int getNumImages() { + return numImages; + } + + /** + * Queries the cursor's width. Note that + * the coordinate system is the same as OpenGL. 0, 0 being lower left. + * @return the width of the cursor in pixel. + */ + public int getWidth() { + return width; + } + + /** + * Queries the cursor's X hotspot coordinate. Note that + * the coordinate system is the same as OpenGL. 0, 0 being lower left. + * @return the coordinate on the cursor's X axis where the hotspot is located. + */ + public int getXHotSpot() { + return xHotSpot; + } + + /** + * Queries the cursor's Y hotspot coordinate. Note that + * the coordinate system is the same as OpenGL. 0, 0 being lower left. + * @return the coordinate on the cursor's Y axis where the hotspot is located. + */ + public int getYHotSpot() { + return yHotSpot; + } + + /** + * Sets the cursor's height. + * @param height The height of the cursor in pixels. Note that all images + * in a cursor have to be the same size. + */ + public void setHeight(int height) { + this.height = height; + } + + /** + * Sets the cursor's image(s) data. Each image data should be consecutively + * stored in the {@link IntBuffer} if more tha one image is contained in the + * cursor. + * @param imagesData the cursor's image(s) data. Each image data should be consecutively + * stored in the {@link IntBuffer} if more than one image is contained in the + * cursor. + */ + public void setImagesData(IntBuffer imagesData) { + this.imagesData = imagesData; + } + + /** + * Sets the cursor image delay for each frame of an animated cursor. If the + * cursor has no animation and consist of only 1 image, null is expected. + * @param imagesDelay + */ + public void setImagesDelay(IntBuffer imagesDelay) { + this.imagesDelay = imagesDelay; + } + + /** + * Sets the number of images in the cursor. + * @param numImages number of images in the cursor. + */ + public void setNumImages(int numImages) { + this.numImages = numImages; + } + + /** + * Sets the cursor's width. + * @param width The width of the cursor in pixels. Note that all images + * in a cursor have to be the same size. + */ + public void setWidth(int width) { + this.width = width; + } + + /** + * Sets the cursor's X coordinate for its hotspot. + * @param xHotSpot the cursor's X axis coordinate for its hotspot. Note that + * the coordinate system is the same as OpenGL. 0, 0 being lower left. + */ + public void setxHotSpot(int xHotSpot) { + this.xHotSpot = xHotSpot; + } + + /** + * Sets the cursor's Y axis coordinate for its hotspot. + * @param yHotSpot the cursor's Y axis coordinate for its hotspot. Note that + * the coordinate system is the same as OpenGL. 0, 0 being lower left. + */ + public void setyHotSpot(int yHotSpot) { + this.yHotSpot = yHotSpot; + } + + +} diff --git a/engine/src/core/com/jme3/asset/Desktop.cfg b/engine/src/core/com/jme3/asset/Desktop.cfg index 93dafb785..13b8a0634 100644 --- a/engine/src/core/com/jme3/asset/Desktop.cfg +++ b/engine/src/core/com/jme3/asset/Desktop.cfg @@ -3,6 +3,7 @@ LOCATOR / com.jme3.asset.plugins.ClasspathLocator LOADER com.jme3.texture.plugins.AWTLoader : jpg, bmp, gif, png, jpeg LOADER com.jme3.audio.plugins.WAVLoader : wav LOADER com.jme3.audio.plugins.OGGLoader : ogg +LOADER com.jme3.cursors.plugins.CursorLoader : ani, cur, ico LOADER com.jme3.material.plugins.J3MLoader : j3m LOADER com.jme3.material.plugins.J3MLoader : j3md LOADER com.jme3.font.plugins.BitmapFontLoader : fnt @@ -19,4 +20,4 @@ LOADER com.jme3.scene.plugins.ogre.SkeletonLoader : skeletonxml, skeleton.xml LOADER com.jme3.scene.plugins.ogre.MaterialLoader : material LOADER com.jme3.scene.plugins.ogre.SceneLoader : scene LOADER com.jme3.scene.plugins.blender.BlenderModelLoader : blend -LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, glsl, glsllib \ No newline at end of file +LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, glsl, glsllib diff --git a/engine/src/core/com/jme3/input/InputManager.java b/engine/src/core/com/jme3/input/InputManager.java index 73e2e8af7..86661fad3 100644 --- a/engine/src/core/com/jme3/input/InputManager.java +++ b/engine/src/core/com/jme3/input/InputManager.java @@ -32,6 +32,7 @@ package com.jme3.input; import com.jme3.app.Application; +import com.jme3.cursors.plugins.JmeCursor; import com.jme3.input.controls.*; import com.jme3.input.event.*; import com.jme3.math.FastMath; @@ -53,16 +54,16 @@ import java.util.logging.Logger; * or with input explicitly disabled. *

* The input manager has two concepts, a {@link Trigger} and a mapping. - * A trigger represents a specific input trigger, such as a key button, - * or a mouse axis. A mapping represents a link onto one or several triggers, - * when the appropriate trigger is activated (e.g. a key is pressed), the + * A trigger represents a specific input trigger, such as a key button, + * or a mouse axis. A mapping represents a link onto one or several triggers, + * when the appropriate trigger is activated (e.g. a key is pressed), the * mapping will be invoked. Any listeners registered to receive an event * from the mapping will have an event raised. *

* There are two types of events that {@link InputListener input listeners} * can receive, one is {@link ActionListener#onAction(java.lang.String, boolean, float) action} * events and another is {@link AnalogListener#onAnalog(java.lang.String, float, float) analog} - * events. + * events. *

* onAction events are raised when the specific input * activates or deactivates. For a digital input such as key press, the onAction() @@ -119,7 +120,7 @@ public class InputManager implements RawInputListener { /** * Initializes the InputManager. - * + * *

This should only be called internally in {@link Application}. * * @param mouse @@ -386,6 +387,10 @@ public class InputManager implements RawInputListener { } } + public void setMouseCursor(JmeCursor jmeCursor) { + mouse.setNativeCursor(jmeCursor); + } + /** * Callback from RawInputListener. Do not use. */ @@ -420,7 +425,7 @@ public class InputManager implements RawInputListener { if (evt.isRepeating()) { return; // repeat events not used for bindings } - + int hash = KeyTrigger.keyHash(evt.getKeyCode()); invokeActions(hash, evt.isPressed()); invokeTimedActions(hash, evt.getTime(), evt.isPressed()); @@ -439,12 +444,12 @@ public class InputManager implements RawInputListener { /** * Set the deadzone for joystick axes. - * + * *

{@link ActionListener#onAction(java.lang.String, boolean, float) } * events will only be raised if the joystick axis value is greater than * the deadZone. - * - * @param deadZone the deadzone for joystick axes. + * + * @param deadZone the deadzone for joystick axes. */ public void setAxisDeadZone(float deadZone) { this.axisDeadZone = deadZone; @@ -452,30 +457,30 @@ public class InputManager implements RawInputListener { /** * Returns the deadzone for joystick axes. - * + * * @return the deadzone for joystick axes. */ public float getAxisDeadZone() { return axisDeadZone; } - + /** * Adds a new listener to receive events on the given mappings. - * + * *

The given InputListener will be registered to receive events * on the specified mapping names. When a mapping raises an event, the * listener will have its appropriate method invoked, either * {@link ActionListener#onAction(java.lang.String, boolean, float) } * or {@link AnalogListener#onAnalog(java.lang.String, float, float) } - * depending on which interface the listener implements. + * depending on which interface the listener implements. * If the listener implements both interfaces, then it will receive the * appropriate event for each method. - * + * * @param listener The listener to register to receive input events. * @param mappingNames The mapping names which the listener will receive * events from. - * - * @see InputManager#removeListener(com.jme3.input.controls.InputListener) + * + * @see InputManager#removeListener(com.jme3.input.controls.InputListener) */ public void addListener(InputListener listener, String... mappingNames) { for (String mappingName : mappingNames) { @@ -492,14 +497,14 @@ public class InputManager implements RawInputListener { /** * Removes a listener from receiving events. - * + * *

This will unregister the listener from any mappings that it - * was previously registered with via + * was previously registered with via * {@link InputManager#addListener(com.jme3.input.controls.InputListener, java.lang.String[]) }. - * + * * @param listener The listener to unregister. - * - * @see InputManager#addListener(com.jme3.input.controls.InputListener, java.lang.String[]) + * + * @see InputManager#addListener(com.jme3.input.controls.InputListener, java.lang.String[]) */ public void removeListener(InputListener listener) { for (Mapping mapping : mappings.values()) { @@ -509,16 +514,16 @@ public class InputManager implements RawInputListener { /** * Create a new mapping to the given triggers. - * + * *

* The given mapping will be assigned to the given triggers, when * any of the triggers given raise an event, the listeners * registered to the mappings will receive appropriate events. - * + * * @param mappingName The mapping name to assign. * @param triggers The triggers to which the mapping is to be registered. - * - * @see InputManager#deleteMapping(java.lang.String) + * + * @see InputManager#deleteMapping(java.lang.String) */ public void addMapping(String mappingName, Trigger... triggers) { Mapping mapping = mappings.get(mappingName); @@ -549,23 +554,23 @@ public class InputManager implements RawInputListener { * * @param mappingName The mapping name to check. * - * @see InputManager#addMapping(java.lang.String, com.jme3.input.controls.Trigger[]) - * @see InputManager#deleteMapping(java.lang.String) - */ + * @see InputManager#addMapping(java.lang.String, com.jme3.input.controls.Trigger[]) + * @see InputManager#deleteMapping(java.lang.String) + */ public boolean hasMapping(String mappingName) { return mappings.containsKey(mappingName); } - + /** * Deletes a mapping from receiving trigger events. - * + * *

* The given mapping will no longer be assigned to receive trigger * events. - * + * * @param mappingName The mapping name to unregister. - * - * @see InputManager#addMapping(java.lang.String, com.jme3.input.controls.Trigger[]) + * + * @see InputManager#addMapping(java.lang.String, com.jme3.input.controls.Trigger[]) */ public void deleteMapping(String mappingName) { Mapping mapping = mappings.remove(mappingName); @@ -583,12 +588,12 @@ public class InputManager implements RawInputListener { /** * Deletes a specific trigger registered to a mapping. - * + * *

- * The given mapping will no longer receive events raised by the + * The given mapping will no longer receive events raised by the * trigger. - * - * @param mappingName The mapping name to cease receiving events from the + * + * @param mappingName The mapping name to cease receiving events from the * trigger. * @param trigger The trigger to no longer invoke events on the mapping. */ @@ -604,7 +609,7 @@ public class InputManager implements RawInputListener { } /** - * Clears all the input mappings from this InputManager. + * Clears all the input mappings from this InputManager. * Consequently, also clears all of the * InputListeners as well. */ @@ -625,12 +630,12 @@ public class InputManager implements RawInputListener { /** * Returns whether the mouse cursor is visible or not. - * + * *

By default the cursor is visible. - * + * * @return whether the mouse cursor is visible or not. - * - * @see InputManager#setCursorVisible(boolean) + * + * @see InputManager#setCursorVisible(boolean) */ public boolean isCursorVisible() { return mouseVisible; @@ -638,7 +643,7 @@ public class InputManager implements RawInputListener { /** * Set whether the mouse cursor should be visible or not. - * + * * @param visible whether the mouse cursor should be visible or not. */ public void setCursorVisible(boolean visible) { @@ -651,7 +656,7 @@ public class InputManager implements RawInputListener { /** * Returns the current cursor position. The position is relative to the * bottom-left of the screen and is in pixels. - * + * * @return the current cursor position */ public Vector2f getCursorPosition() { @@ -660,7 +665,7 @@ public class InputManager implements RawInputListener { /** * Returns an array of all joysticks installed on the system. - * + * * @return an array of all joysticks installed on the system. */ public Joystick[] getJoysticks() { @@ -669,22 +674,22 @@ public class InputManager implements RawInputListener { /** * Adds a {@link RawInputListener} to receive raw input events. - * + * *

* Any raw input listeners registered to this InputManager * will receive raw input events first, before they get handled - * by the InputManager itself. The listeners are + * by the InputManager itself. The listeners are * each processed in the order they were added, e.g. FIFO. *

* If a raw input listener has handled the event and does not wish * other listeners down the list to process the event, it may set the - * {@link InputEvent#setConsumed() consumed flag} to indicate the + * {@link InputEvent#setConsumed() consumed flag} to indicate the * event was consumed and shouldn't be processed any further. - * The listener may do this either at each of the event callbacks + * The listener may do this either at each of the event callbacks * or at the {@link RawInputListener#endInput() } method. - * + * * @param listener A listener to receive raw input events. - * + * * @see RawInputListener */ public void addRawInputListener(RawInputListener listener) { @@ -695,10 +700,10 @@ public class InputManager implements RawInputListener { /** * Removes a {@link RawInputListener} so that it no longer * receives raw input events. - * + * * @param listener The listener to cease receiving raw input events. - * - * @see InputManager#addRawInputListener(com.jme3.input.RawInputListener) + * + * @see InputManager#addRawInputListener(com.jme3.input.RawInputListener) */ public void removeRawInputListener(RawInputListener listener) { rawListeners.remove(listener); @@ -707,8 +712,8 @@ public class InputManager implements RawInputListener { /** * Clears all {@link RawInputListener}s. - * - * @see InputManager#addRawInputListener(com.jme3.input.RawInputListener) + * + * @see InputManager#addRawInputListener(com.jme3.input.RawInputListener) */ public void clearRawInputListeners() { rawListeners.clear(); @@ -716,14 +721,14 @@ public class InputManager implements RawInputListener { } private RawInputListener[] getRawListenerArray() { - if (rawListenerArray == null) + if (rawListenerArray == null) rawListenerArray = rawListeners.toArray(new RawInputListener[rawListeners.size()]); return rawListenerArray; } - + /** * Enable simulation of mouse events. Used for touchscreen input only. - * + * * @param value True to enable simulation of mouse events */ public void setSimulateMouse(boolean value) { @@ -736,16 +741,16 @@ public class InputManager implements RawInputListener { * */ public boolean getSimulateMouse() { - if (touch != null) { + if (touch != null) { return touch.getSimulateMouse(); } else { return false; } } - + /** * Enable simulation of keyboard events. Used for touchscreen input only. - * + * * @param value True to enable simulation of keyboard events */ public void setSimulateKeyboard(boolean value) { @@ -757,7 +762,7 @@ public class InputManager implements RawInputListener { private void processQueue() { int queueSize = inputQueue.size(); RawInputListener[] array = getRawListenerArray(); - + for (RawInputListener listener : array) { listener.beginInput(); @@ -819,7 +824,7 @@ public class InputManager implements RawInputListener { } /** - * Updates the InputManager. + * Updates the InputManager. * This will query current input devices and send * appropriate events to registered listeners. * @@ -827,11 +832,11 @@ public class InputManager implements RawInputListener { */ public void update(float tpf) { frameTPF = tpf; - + // Activate safemode if the TPF value is so small // that rounding errors are inevitable safeMode = tpf < 0.015f; - + long currentTime = keys.getInputTimeNanos(); frameDelta = currentTime - lastUpdateTime; @@ -859,7 +864,7 @@ public class InputManager implements RawInputListener { * Dispatches touch events to touch listeners * @param evt The touch event to be dispatched to all onTouch listeners */ - public void onTouchEventQueued(TouchEvent evt) { + public void onTouchEventQueued(TouchEvent evt) { ArrayList maps = bindings.get(TouchTrigger.touchHash(evt.getKeyCode())); if (maps == null) { return; @@ -873,12 +878,12 @@ public class InputManager implements RawInputListener { for (int j = listenerSize - 1; j >= 0; j--) { InputListener listener = listeners.get(j); if (listener instanceof TouchListener) { - ((TouchListener) listener).onTouch(mapping.name, evt, frameTPF); + ((TouchListener) listener).onTouch(mapping.name, evt, frameTPF); } } - } + } } - + /** * Callback from RawInputListener. Do not use. */ @@ -887,6 +892,6 @@ public class InputManager implements RawInputListener { if (!eventsPermitted) { throw new UnsupportedOperationException("TouchInput has raised an event at an illegal time."); } - inputQueue.add(evt); + inputQueue.add(evt); } } diff --git a/engine/src/core/com/jme3/input/MouseInput.java b/engine/src/core/com/jme3/input/MouseInput.java index f4a1f3694..99d4db5eb 100644 --- a/engine/src/core/com/jme3/input/MouseInput.java +++ b/engine/src/core/com/jme3/input/MouseInput.java @@ -32,6 +32,8 @@ package com.jme3.input; +import com.jme3.cursors.plugins.JmeCursor; + /** * A specific API for interfacing with the mouse. */ @@ -41,12 +43,12 @@ public interface MouseInput extends Input { * Mouse X axis. */ public static final int AXIS_X = 0; - + /** * Mouse Y axis. */ public static final int AXIS_Y = 1; - + /** * Mouse wheel axis. */ @@ -56,12 +58,12 @@ public interface MouseInput extends Input { * Left mouse button. */ public static final int BUTTON_LEFT = 0; - + /** * Right mouse button. */ public static final int BUTTON_RIGHT = 1; - + /** * Middle mouse button. */ @@ -69,15 +71,21 @@ public interface MouseInput extends Input { /** * Set whether the mouse cursor should be visible or not. - * + * * @param visible Whether the mouse cursor should be visible or not. */ public void setCursorVisible(boolean visible); /** * Returns the number of buttons the mouse has. Typically 3 for most mice. - * + * * @return the number of buttons the mouse has. */ public int getButtonCount(); + + /** + * Sets the cursor to use. + * @param cursor The cursor to use. + */ + public void setNativeCursor(JmeCursor cursor); } diff --git a/engine/src/core/com/jme3/input/dummy/DummyMouseInput.java b/engine/src/core/com/jme3/input/dummy/DummyMouseInput.java index b86206160..23b7a2a35 100644 --- a/engine/src/core/com/jme3/input/dummy/DummyMouseInput.java +++ b/engine/src/core/com/jme3/input/dummy/DummyMouseInput.java @@ -32,12 +32,13 @@ package com.jme3.input.dummy; +import com.jme3.cursors.plugins.JmeCursor; import com.jme3.input.MouseInput; /** * DummyMouseInput as an implementation of MouseInput that raises no * input events. - * + * * @author Kirill Vainer. */ public class DummyMouseInput extends DummyInput implements MouseInput { @@ -51,4 +52,7 @@ public class DummyMouseInput extends DummyInput implements MouseInput { return 0; } + public void setNativeCursor(JmeCursor cursor) { + } + } diff --git a/engine/src/desktop/com/jme3/input/awt/AwtMouseInput.java b/engine/src/desktop/com/jme3/input/awt/AwtMouseInput.java index 737863810..7fc3af551 100644 --- a/engine/src/desktop/com/jme3/input/awt/AwtMouseInput.java +++ b/engine/src/desktop/com/jme3/input/awt/AwtMouseInput.java @@ -32,6 +32,7 @@ package com.jme3.input.awt; +import com.jme3.cursors.plugins.JmeCursor; import com.jme3.input.MouseInput; import com.jme3.input.RawInputListener; import com.jme3.input.event.MouseButtonEvent; @@ -49,7 +50,7 @@ import javax.swing.SwingUtilities; * * @author Joshua Slack * @author MHenze (cylab) - * + * * @version $Revision$ */ public class AwtMouseInput implements MouseInput, MouseListener, MouseWheelListener, MouseMotionListener { @@ -66,7 +67,7 @@ public class AwtMouseInput implements MouseInput, MouseListener, MouseWheelListe private final ArrayList eventQueue = new ArrayList(); private final ArrayList eventQueueCopy = new ArrayList(); - + private int lastEventX; private int lastEventY; private int lastEventWheel; @@ -95,7 +96,7 @@ public class AwtMouseInput implements MouseInput, MouseListener, MouseWheelListe logger.log(Level.SEVERE, "Could not create a robot, so the mouse cannot be grabbed! ", e); } } - + public void setInputSource(Component comp) { if (component != null) { component.removeMouseListener(this); @@ -312,4 +313,7 @@ public class AwtMouseInput implements MouseInput, MouseListener, MouseWheelListe } return index; } + + public void setNativeCursor(JmeCursor cursor) { + } } diff --git a/engine/src/lwjgl/com/jme3/input/lwjgl/LwjglMouseInput.java b/engine/src/lwjgl/com/jme3/input/lwjgl/LwjglMouseInput.java index 2e45d9406..c8f2e31eb 100644 --- a/engine/src/lwjgl/com/jme3/input/lwjgl/LwjglMouseInput.java +++ b/engine/src/lwjgl/com/jme3/input/lwjgl/LwjglMouseInput.java @@ -32,6 +32,7 @@ package com.jme3.input.lwjgl; +import com.jme3.cursors.plugins.JmeCursor; import com.jme3.input.MouseInput; import com.jme3.input.RawInputListener; import com.jme3.input.event.MouseButtonEvent; @@ -70,7 +71,7 @@ public class LwjglMouseInput implements MouseInput { Mouse.create(); logger.info("Mouse created."); supportHardwareCursor = (Cursor.getCapabilities() & Cursor.CURSOR_ONE_BIT_TRANSPARENCY) != 0; - + // Recall state that was set before initialization Mouse.setGrabbed(!cursorVisible); } catch (LWJGLException ex) { @@ -138,7 +139,7 @@ public class LwjglMouseInput implements MouseInput { cursorVisible = visible; if (!context.isRenderable()) return; - + Mouse.setGrabbed(!visible); } @@ -150,4 +151,20 @@ public class LwjglMouseInput implements MouseInput { return Sys.getTime() * LwjglTimer.LWJGL_TIME_TO_NANOS; } + public void setNativeCursor(JmeCursor jmeCursor) { + try { + Cursor newCursor = new Cursor( + jmeCursor.getWidth(), + jmeCursor.getHeight(), + jmeCursor.getXHotSpot(), + jmeCursor.getYHotSpot(), + jmeCursor.getNumImages(), + jmeCursor.getImagesData(), + jmeCursor.getImagesDelay()); + Mouse.setNativeCursor(newCursor); + } catch (LWJGLException ex) { + Logger.getLogger(LwjglMouseInput.class.getName()).log(Level.SEVERE, null, ex); + } + } + } diff --git a/engine/src/niftygui/com/jme3/niftygui/InputSystemJme.java b/engine/src/niftygui/com/jme3/niftygui/InputSystemJme.java index 8227dc29b..265ef040f 100644 --- a/engine/src/niftygui/com/jme3/niftygui/InputSystemJme.java +++ b/engine/src/niftygui/com/jme3/niftygui/InputSystemJme.java @@ -98,7 +98,7 @@ public class InputSystemJme implements InputSystem, RawInputListener { x = (int) evt.getX(); y = (int) (height - evt.getY()); - if (!inputManager.getSimulateMouse()) { + if (!inputManager.getSimulateMouse()) { switch (evt.getType()) { case DOWN: consumed = nic.processMouseEvent(x, y, 0, 0, true); @@ -150,7 +150,7 @@ public class InputSystemJme implements InputSystem, RawInputListener { // Mouse button raised. End dragging if (wasPressed && !pressed) { if (!niftyOwnsDragging) { - forwardToNifty = false; + forwardToNifty = true; } isDragging = false; niftyOwnsDragging = false;