All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.ImageCursor Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene;

import java.util.HashMap;
import java.util.Map;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoublePropertyBase;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectPropertyBase;
import javafx.geometry.Dimension2D;
import javafx.scene.image.Image;

import com.sun.javafx.cursor.CursorFrame;
import com.sun.javafx.cursor.ImageCursorFrame;
import com.sun.javafx.tk.Toolkit;
import java.util.Arrays;
import javafx.beans.NamedArg;


/**
 * A custom image representation of the mouse cursor. On platforms that don't
 * support custom cursors, {@code Cursor.DEFAULT} will be used in place of the
 * specified ImageCursor.
 *
 * 

Example: *

import javafx.scene.*;
import javafx.scene.image.*;

Image image = new Image("mycursor.png");

Scene scene = new Scene(400, 300);
scene.setCursor(new ImageCursor(image,
                                image.getWidth() / 2,
                                image.getHeight() /2));
 * 
* * @since JavaFX 2.0 */ public class ImageCursor extends Cursor { /** * The image to display when the cursor is active. If the image is null, * {@code Cursor.DEFAULT} will be used. * * @defaultValue null */ private ObjectPropertyImpl image; public final Image getImage() { return image == null ? null : image.get(); } public final ReadOnlyObjectProperty imageProperty() { return imagePropertyImpl(); } private ObjectPropertyImpl imagePropertyImpl() { if (image == null) { image = new ObjectPropertyImpl<>("image"); } return image; } /** * The X coordinate of the cursor's hot spot. This hotspot represents the * location within the cursor image that will be displayed at the mouse * position. This must be in the range of [0,image.width-1]. A value * less than 0 will be set to 0. A value greater than * image.width-1 will be set to image.width-1. * * @defaultValue 0 */ private DoublePropertyImpl hotspotX; public final double getHotspotX() { return hotspotX == null ? 0.0 : hotspotX.get(); } public final ReadOnlyDoubleProperty hotspotXProperty() { return hotspotXPropertyImpl(); } private DoublePropertyImpl hotspotXPropertyImpl() { if (hotspotX == null) { hotspotX = new DoublePropertyImpl("hotspotX"); } return hotspotX; } /** * The Y coordinate of the cursor's hot spot. This hotspot represents the * location within the cursor image that will be displayed at the mouse * position. This must be in the range of [0,image.height-1]. A value * less than 0 will be set to 0. A value greater than * image.height-1 will be set to image.height-1. * * @defaultValue 0 */ private DoublePropertyImpl hotspotY; public final double getHotspotY() { return hotspotY == null ? 0.0 : hotspotY.get(); } public final ReadOnlyDoubleProperty hotspotYProperty() { return hotspotYPropertyImpl(); } private DoublePropertyImpl hotspotYPropertyImpl() { if (hotspotY == null) { hotspotY = new DoublePropertyImpl("hotspotY"); } return hotspotY; } private CursorFrame currentCursorFrame; /** * Stores the first cursor frame. For non-animated cursors there is only one * frame and so the {@code restCursorFrames} is {@code null}. */ private ImageCursorFrame firstCursorFrame; /** * Maps platform images to cursor frames. It doesn't store the first cursor * frame and so it needs to be created only for animated cursors. */ private Map otherCursorFrames; /** * Indicates whether the image cursor is currently in use. The active cursor * is bound to the image and invalidates its platform cursor when the image * changes. */ private int activeCounter; /** * Constructs a new empty {@code ImageCursor} which will look as * {@code Cursor.DEFAULT}. */ public ImageCursor() { } /** * Constructs an {@code ImageCursor} from the specified image. The cursor's * hot spot will default to the upper left corner. * * @param image the image */ public ImageCursor(@NamedArg("image") final Image image) { this(image, 0f, 0f); } /** * Constructs an {@code ImageCursor} from the specified image and hotspot * coordinates. * * @param image the image * @param hotspotX the X coordinate of the cursor's hot spot * @param hotspotY the Y coordinate of the cursor's hot spot */ public ImageCursor(@NamedArg("image") final Image image, @NamedArg("hotspotX") double hotspotX, @NamedArg("hotspotY") double hotspotY) { if ((image != null) && (image.getProgress() < 1)) { DelayedInitialization.applyTo( this, image, hotspotX, hotspotY); } else { initialize(image, hotspotX, hotspotY); } } /** * Gets the supported cursor size that is closest to the specified preferred * size. A value of (0,0) is returned if the platform does not support * custom cursors. * *

* Note: if an image is used whose dimensions don't match a supported size * (as returned by this method), the implementation will resize the image to * a supported size. This may result in a loss of quality. * *

* Note: These values can vary between operating systems, graphics cards and * screen resolution, but at the time of this writing, a sample Windows * Vista machine returned 32x32 for all requested sizes, while sample Mac * and Linux machines returned the requested size up to a maximum of 64x64. * Applications should provide a 32x32 cursor, which will work well on all * platforms, and may optionally wish to provide a 64x64 cursor for those * platforms on which it is supported. * * @param preferredWidth the preferred width of the cursor * @param preferredHeight the preferred height of the cursor * @return the supported cursor size */ public static Dimension2D getBestSize(double preferredWidth, double preferredHeight) { return Toolkit.getToolkit().getBestCursorSize((int) preferredWidth, (int) preferredHeight); } /** * Returns the maximum number of colors supported in a custom image cursor * palette. * *

* Note: if an image is used which has more colors in its palette than the * supported maximum, the implementation will attempt to flatten the * palette to the maximum. This may result in a loss of quality. * *

* Note: These values can vary between operating systems, graphics cards and * screen resolution, but at the time of this writing, a sample Windows * Vista machine returned 256, a sample Mac machine returned * Integer.MAX_VALUE, indicating support for full color cursors, and * a sample Linux machine returned 2. Applications may want to target these * three color depths for an optimal cursor on each platform. * * @return the maximum number of colors supported in a custom image cursor * palette */ public static int getMaximumColors() { return Toolkit.getToolkit().getMaximumCursorColors(); } /** * Creates a custom image cursor from one of the specified images. This function * will choose the image whose size most closely matched the best cursor size. * The hotpotX of the returned ImageCursor is scaled by * chosenImage.width/images[0].width and the hotspotY is scaled by * chosenImage.height/images[0].height. *

* On platforms that don't support custom cursors, {@code Cursor.DEFAULT} will * be used in place of the returned ImageCursor. * * @param images a sequence of images from which to choose, in order of preference * @param hotspotX the X coordinate of the hotspot within the first image * in the images sequence * @param hotspotY the Y coordinate of the hotspot within the first image * in the images sequence * @return a cursor created from the best image */ public static ImageCursor chooseBestCursor( final Image[] images, final double hotspotX, final double hotspotY) { final ImageCursor imageCursor = new ImageCursor(); if (needsDelayedInitialization(images)) { DelayedInitialization.applyTo( imageCursor, images, hotspotX, hotspotY); } else { imageCursor.initialize(images, hotspotX, hotspotY); } return imageCursor; } @Override CursorFrame getCurrentFrame() { if (currentCursorFrame != null) { return currentCursorFrame; } final Image cursorImage = getImage(); if (cursorImage == null) { currentCursorFrame = Cursor.DEFAULT.getCurrentFrame(); return currentCursorFrame; } final Object cursorPlatformImage = Toolkit.getImageAccessor().getPlatformImage(cursorImage); if (cursorPlatformImage == null) { currentCursorFrame = Cursor.DEFAULT.getCurrentFrame(); return currentCursorFrame; } if (firstCursorFrame == null) { firstCursorFrame = new ImageCursorFrame(cursorPlatformImage, cursorImage.getWidth(), cursorImage.getHeight(), getHotspotX(), getHotspotY()); currentCursorFrame = firstCursorFrame; } else if (firstCursorFrame.getPlatformImage() == cursorPlatformImage) { currentCursorFrame = firstCursorFrame; } else { if (otherCursorFrames == null) { otherCursorFrames = new HashMap<>(); } currentCursorFrame = otherCursorFrames.get(cursorPlatformImage); if (currentCursorFrame == null) { // cursor frame not created yet final ImageCursorFrame newCursorFrame = new ImageCursorFrame(cursorPlatformImage, cursorImage.getWidth(), cursorImage.getHeight(), getHotspotX(), getHotspotY()); otherCursorFrames.put(cursorPlatformImage, newCursorFrame); currentCursorFrame = newCursorFrame; } } return currentCursorFrame; } private void invalidateCurrentFrame() { currentCursorFrame = null; } @Override void activate() { if (++activeCounter == 1) { bindImage(getImage()); invalidateCurrentFrame(); } } @Override void deactivate() { if (--activeCounter == 0) { unbindImage(getImage()); } } private void initialize(final Image[] images, final double hotspotX, final double hotspotY) { final Dimension2D dim = getBestSize(1f, 1f); // If no valid image or if custom cursors are not supported, leave // the default image cursor if ((images.length == 0) || (dim.getWidth() == 0f) || (dim.getHeight() == 0f)) { return; } // If only a single image, use it to construct a custom cursor if (images.length == 1) { initialize(images[0], hotspotX, hotspotY); return; } final Image bestImage = findBestImage(images); final double scaleX = bestImage.getWidth() / images[0].getWidth(); final double scaleY = bestImage.getHeight() / images[0].getHeight(); initialize(bestImage, hotspotX * scaleX, hotspotY * scaleY); } private void initialize(Image newImage, double newHotspotX, double newHotspotY) { final Image oldImage = getImage(); final double oldHotspotX = getHotspotX(); final double oldHotspotY = getHotspotY(); if ((newImage == null) || (newImage.getWidth() < 1f) || (newImage.getHeight() < 1f)) { // If image is invalid set the hotspot to 0 newHotspotX = 0f; newHotspotY = 0f; } else { if (newHotspotX < 0f) { newHotspotX = 0f; } if (newHotspotX > (newImage.getWidth() - 1f)) { newHotspotX = newImage.getWidth() - 1f; } if (newHotspotY < 0f) { newHotspotY = 0f; } if (newHotspotY > (newImage.getHeight() - 1f)) { newHotspotY = newImage.getHeight() - 1f; } } imagePropertyImpl().store(newImage); hotspotXPropertyImpl().store(newHotspotX); hotspotYPropertyImpl().store(newHotspotY); if (oldImage != newImage) { if (activeCounter > 0) { unbindImage(oldImage); bindImage(newImage); } invalidateCurrentFrame(); image.fireValueChangedEvent(); } if (oldHotspotX != newHotspotX) { hotspotX.fireValueChangedEvent(); } if (oldHotspotY != newHotspotY) { hotspotY.fireValueChangedEvent(); } } private InvalidationListener imageListener; private InvalidationListener getImageListener() { if (imageListener == null) { imageListener = valueModel -> invalidateCurrentFrame(); } return imageListener; } private void bindImage(final Image toImage) { if (toImage == null) { return; } Toolkit.getImageAccessor().getImageProperty(toImage).addListener(getImageListener()); } private void unbindImage(final Image fromImage) { if (fromImage == null) { return; } Toolkit.getImageAccessor().getImageProperty(fromImage).removeListener(getImageListener()); } private static boolean needsDelayedInitialization(final Image[] images) { for (final Image image: images) { if (image.getProgress() < 1) { return true; } } return false; } // Utility function to select the best image private static Image findBestImage(final Image[] images) { // Check for exact match and return the first such match for (final Image image: images) { final Dimension2D dim = getBestSize((int) image.getWidth(), (int) image.getHeight()); if ((dim.getWidth() == image.getWidth()) && (dim.getHeight() == image.getHeight())) { return image; } } // No exact match, check for closest match without down-scaling // (i.e., smallest scale >= 1.0) Image bestImage = null; double bestRatio = Double.MAX_VALUE; for (final Image image: images) { if ((image.getWidth() > 0) && (image.getHeight() > 0)) { final Dimension2D dim = getBestSize(image.getWidth(), image.getHeight()); final double ratioX = dim.getWidth() / image.getWidth(); final double ratioY = dim.getHeight() / image.getHeight(); if ((ratioX >= 1) && (ratioY >= 1)) { final double ratio = Math.max(ratioX, ratioY); if (ratio < bestRatio) { bestImage = image; bestRatio = ratio; } } } } if (bestImage != null) { return bestImage; } // Still no match, check for closest match alowing for down-scaling // (i.e., smallest up-scale or down-scale >= 1.0) for (final Image image: images) { if ((image.getWidth() > 0) && (image.getHeight() > 0)) { final Dimension2D dim = getBestSize(image.getWidth(), image.getHeight()); if ((dim.getWidth() > 0) && (dim.getHeight() > 0)) { double ratioX = dim.getWidth() / image.getWidth(); if (ratioX < 1) { ratioX = 1 / ratioX; } double ratioY = dim.getHeight() / image.getHeight(); if (ratioY < 1) { ratioY = 1 / ratioY; } final double ratio = Math.max(ratioX, ratioY); if (ratio < bestRatio) { bestImage = image; bestRatio = ratio; } } } } if (bestImage != null) { return bestImage; } return images[0]; } private final class DoublePropertyImpl extends ReadOnlyDoublePropertyBase { private final String name; private double value; public DoublePropertyImpl(final String name) { this.name = name; } public void store(final double value) { this.value = value; } @Override public void fireValueChangedEvent() { super.fireValueChangedEvent(); } @Override public double get() { return value; } @Override public Object getBean() { return ImageCursor.this; } @Override public String getName() { return name; } } private final class ObjectPropertyImpl extends ReadOnlyObjectPropertyBase { private final String name; private T value; public ObjectPropertyImpl(final String name) { this.name = name; } public void store(final T value) { this.value = value; } @Override public void fireValueChangedEvent() { super.fireValueChangedEvent(); } @Override public T get() { return value; } @Override public Object getBean() { return ImageCursor.this; } @Override public String getName() { return name; } } private static final class DelayedInitialization implements InvalidationListener { private final ImageCursor targetCursor; private final Image[] images; private final double hotspotX; private final double hotspotY; private final boolean initAsSingle; private int waitForImages; private DelayedInitialization(final ImageCursor targetCursor, final Image[] images, final double hotspotX, final double hotspotY, final boolean initAsSingle) { this.targetCursor = targetCursor; this.images = images; this.hotspotX = hotspotX; this.hotspotY = hotspotY; this.initAsSingle = initAsSingle; } public static void applyTo(final ImageCursor imageCursor, final Image[] images, final double hotspotX, final double hotspotY) { final DelayedInitialization delayedInitialization = new DelayedInitialization(imageCursor, Arrays.copyOf(images, images.length), hotspotX, hotspotY, false); delayedInitialization.start(); } public static void applyTo(final ImageCursor imageCursor, final Image image, final double hotspotX, final double hotspotY) { final DelayedInitialization delayedInitialization = new DelayedInitialization(imageCursor, new Image[] { image }, hotspotX, hotspotY, true); delayedInitialization.start(); } private void start() { for (final Image image: images) { if (image.getProgress() < 1) { ++waitForImages; image.progressProperty().addListener(this); } } } private void cleanupAndFinishInitialization() { for (final Image image: images) { image.progressProperty().removeListener(this); } if (initAsSingle) { targetCursor.initialize(images[0], hotspotX, hotspotY); } else { targetCursor.initialize(images, hotspotX, hotspotY); } } @Override public void invalidated(Observable valueModel) { if (((ReadOnlyDoubleProperty)valueModel).get() == 1) { if (--waitForImages == 0) { cleanupAndFinishInitialization(); } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy