1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene;
  27 
  28 import java.util.HashMap;
  29 import java.util.Map;
  30 
  31 import javafx.beans.InvalidationListener;
  32 import javafx.beans.Observable;
  33 import javafx.beans.property.ReadOnlyDoubleProperty;
  34 import javafx.beans.property.ReadOnlyDoublePropertyBase;
  35 import javafx.beans.property.ReadOnlyObjectProperty;
  36 import javafx.beans.property.ReadOnlyObjectPropertyBase;
  37 import javafx.geometry.Dimension2D;
  38 import javafx.scene.image.Image;
  39 
  40 import com.sun.javafx.cursor.CursorFrame;
  41 import com.sun.javafx.cursor.ImageCursorFrame;
  42 import com.sun.javafx.tk.Toolkit;
  43 import java.util.Arrays;
  44 import javafx.beans.NamedArg;
  45 
  46 
  47 /**
  48  * A custom image representation of the mouse cursor. On platforms that don't
  49  * support custom cursors, {@code Cursor.DEFAULT} will be used in place of the
  50  * specified ImageCursor.
  51  *
  52  * <p>Example:
  53  * <pre>
  54 import javafx.scene.*;
  55 import javafx.scene.image.*;
  56 
  57 Image image = new Image("mycursor.png");
  58 
  59 Scene scene = new Scene(400, 300);
  60 scene.setCursor(new ImageCursor(image,
  61                                 image.getWidth() / 2,
  62                                 image.getHeight() /2));
  63  * </pre>
  64  *
  65  * @since JavaFX 2.0
  66  */
  67 public class ImageCursor extends Cursor {
  68     /**
  69      * The image to display when the cursor is active. If the image is null,
  70      * {@code Cursor.DEFAULT} will be used.
  71      *
  72      * @defaultValue null
  73      */
  74     private ObjectPropertyImpl<Image> image;
  75 
  76     public final Image getImage() {
  77         return image == null ? null : image.get();
  78     }
  79 
  80     public final ReadOnlyObjectProperty<Image> imageProperty() {
  81         return imagePropertyImpl();
  82     }
  83 
  84     private ObjectPropertyImpl<Image> imagePropertyImpl() {
  85         if (image == null) {
  86             image = new ObjectPropertyImpl<Image>("image");
  87         }
  88 
  89         return image;
  90     }
  91 
  92     /**
  93      * The X coordinate of the cursor's hot spot. This hotspot represents the
  94      * location within the cursor image that will be displayed at the mouse
  95      * position. This must be in the range of [0,image.width-1]. A value
  96      * less than 0 will be set to 0. A value greater than
  97      * image.width-1 will be set to image.width-1.
  98      *
  99      * @defaultValue 0
 100      */
 101     private DoublePropertyImpl hotspotX;
 102 
 103     public final double getHotspotX() {
 104         return hotspotX == null ? 0.0 : hotspotX.get();
 105     }
 106 
 107     public final ReadOnlyDoubleProperty hotspotXProperty() {
 108         return hotspotXPropertyImpl();
 109     }
 110 
 111     private DoublePropertyImpl hotspotXPropertyImpl() {
 112         if (hotspotX == null) {
 113             hotspotX = new DoublePropertyImpl("hotspotX");
 114         }
 115 
 116         return hotspotX;
 117     }
 118 
 119     /**
 120      * The Y coordinate of the cursor's hot spot. This hotspot represents the
 121      * location within the cursor image that will be displayed at the mouse
 122      * position. This must be in the range of [0,image.height-1]. A value
 123      * less than 0 will be set to 0. A value greater than
 124      * image.height-1 will be set to image.height-1.
 125      *
 126      * @defaultValue 0
 127      */
 128     private DoublePropertyImpl hotspotY;
 129 
 130     public final double getHotspotY() {
 131         return hotspotY == null ? 0.0 : hotspotY.get();
 132     }
 133 
 134     public final ReadOnlyDoubleProperty hotspotYProperty() {
 135         return hotspotYPropertyImpl();
 136     }
 137 
 138     private DoublePropertyImpl hotspotYPropertyImpl() {
 139         if (hotspotY == null) {
 140             hotspotY = new DoublePropertyImpl("hotspotY");
 141         }
 142 
 143         return hotspotY;
 144     }
 145 
 146     private CursorFrame currentCursorFrame;
 147 
 148     /**
 149      * Stores the first cursor frame. For non-animated cursors there is only one
 150      * frame and so the {@code restCursorFrames} is {@code null}.
 151      */
 152     private ImageCursorFrame firstCursorFrame;
 153 
 154     /**
 155      * Maps platform images to cursor frames. It doesn't store the first cursor
 156      * frame and so it needs to be created only for animated cursors.
 157      */
 158     private Map<Object, ImageCursorFrame> otherCursorFrames;
 159 
 160     /**
 161      * Indicates whether the image cursor is currently in use. The active cursor
 162      * is bound to the image and invalidates its platform cursor when the image
 163      * changes.
 164      */
 165     private int activeCounter;
 166 
 167     /**
 168      * Constructs a new empty {@code ImageCursor} which will look as
 169      * {@code Cursor.DEFAULT}.
 170      */
 171     public ImageCursor() {
 172     }
 173 
 174     /**
 175      * Constructs an {@code ImageCursor} from the specified image. The cursor's
 176      * hot spot will default to the upper left corner.
 177      *
 178      * @param image the image
 179      */
 180     public ImageCursor(@NamedArg("image") final Image image) {
 181         this(image, 0f, 0f);
 182     }
 183 
 184     /**
 185      * Constructs an {@code ImageCursor} from the specified image and hotspot
 186      * coordinates.
 187      *
 188      * @param image the image
 189      * @param hotspotX the X coordinate of the cursor's hot spot
 190      * @param hotspotY the Y coordinate of the cursor's hot spot
 191      */
 192     public ImageCursor(@NamedArg("image") final Image image,
 193                        @NamedArg("hotspotX") double hotspotX,
 194                        @NamedArg("hotspotY") double hotspotY) {
 195         if ((image != null) && (image.getProgress() < 1)) {
 196             DelayedInitialization.applyTo(
 197                     this, image, hotspotX, hotspotY);
 198         } else {
 199             initialize(image, hotspotX, hotspotY);
 200         }
 201     }
 202 
 203     /**
 204      * Gets the supported cursor size that is closest to the specified preferred
 205      * size. A value of (0,0) is returned if the platform does not support
 206      * custom cursors.
 207      *
 208      * <p>
 209      * Note: if an image is used whose dimensions don't match a supported size
 210      * (as returned by this method), the implementation will resize the image to
 211      * a supported size. This may result in a loss of quality.
 212      *
 213      * <p>
 214      * Note: These values can vary between operating systems, graphics cards and
 215      * screen resolution, but at the time of this writing, a sample Windows
 216      * Vista machine returned 32x32 for all requested sizes, while sample Mac
 217      * and Linux machines returned the requested size up to a maximum of 64x64.
 218      * Applications should provide a 32x32 cursor, which will work well on all
 219      * platforms, and may optionally wish to provide a 64x64 cursor for those
 220      * platforms on which it is supported.
 221      *
 222      * @param preferredWidth the preferred width of the cursor
 223      * @param preferredHeight the preferred height of the cursor
 224      * @return the supported cursor size
 225      */
 226     public static Dimension2D getBestSize(double preferredWidth,
 227                                           double preferredHeight) {
 228         return Toolkit.getToolkit().getBestCursorSize((int) preferredWidth,
 229                                                       (int) preferredHeight);
 230     }
 231 
 232     /**
 233      * Returns the maximum number of colors supported in a custom image cursor
 234      * palette.
 235      *
 236      * <p>
 237      * Note: if an image is used which has more colors in its palette than the
 238      * supported maximum, the implementation will attempt to flatten the
 239      * palette to the maximum. This may result in a loss of quality.
 240      *
 241      * <p>
 242      * Note: These values can vary between operating systems, graphics cards and
 243      * screen resolution,  but at the time of this writing, a sample Windows
 244      * Vista machine returned 256, a sample Mac machine returned
 245      * Integer.MAX_VALUE, indicating support for full color cursors, and
 246      * a sample Linux machine returned 2. Applications may want to target these
 247      * three color depths for an optimal cursor on each platform.
 248      *
 249      * @return the maximum number of colors supported in a custom image cursor
 250      *      palette
 251      */
 252     public static int getMaximumColors() {
 253         return Toolkit.getToolkit().getMaximumCursorColors();
 254     }
 255 
 256     /**
 257      * Creates a custom image cursor from one of the specified images. This function
 258      * will choose the image whose size most closely matched the best cursor size.
 259      * The hotpotX of the returned ImageCursor is scaled by
 260      * chosenImage.width/images[0].width and the hotspotY is scaled by
 261      * chosenImage.height/images[0].height.
 262      * <p>
 263      * On platforms that don't support custom cursors, {@code Cursor.DEFAULT} will
 264      * be used in place of the returned ImageCursor.
 265      *
 266      * @param images a sequence of images from which to choose, in order of preference
 267      * @param hotspotX the X coordinate of the hotspot within the first image
 268      *        in the images sequence
 269      * @param hotspotY the Y coordinate of the hotspot within the first image
 270      *        in the images sequence
 271      * @return a cursor created from the best image
 272      */
 273     public static ImageCursor chooseBestCursor(
 274             final Image[] images, final double hotspotX, final double hotspotY) {
 275         final ImageCursor imageCursor = new ImageCursor();
 276 
 277         if (needsDelayedInitialization(images)) {
 278             DelayedInitialization.applyTo(
 279                     imageCursor, images, hotspotX, hotspotY);
 280         } else {
 281             imageCursor.initialize(images, hotspotX, hotspotY);
 282         }
 283 
 284         return imageCursor;
 285     }
 286 
 287     @Override CursorFrame getCurrentFrame() {
 288         if (currentCursorFrame != null) {
 289             return currentCursorFrame;
 290         }
 291 
 292         final Image cursorImage = getImage();
 293 
 294         if (cursorImage == null) {
 295             currentCursorFrame = Cursor.DEFAULT.getCurrentFrame();
 296             return currentCursorFrame;
 297         }
 298 
 299         final Object cursorPlatformImage = Toolkit.getImageAccessor().getPlatformImage(cursorImage);
 300         if (cursorPlatformImage == null) {
 301             currentCursorFrame = Cursor.DEFAULT.getCurrentFrame();
 302             return currentCursorFrame;
 303         }
 304 
 305         if (firstCursorFrame == null) {
 306             firstCursorFrame =
 307                     new ImageCursorFrame(cursorPlatformImage,
 308                                          cursorImage.getWidth(),
 309                                          cursorImage.getHeight(),
 310                                          getHotspotX(),
 311                                          getHotspotY());
 312             currentCursorFrame = firstCursorFrame;
 313         } else if (firstCursorFrame.getPlatformImage() == cursorPlatformImage) {
 314             currentCursorFrame = firstCursorFrame;
 315         } else {
 316             if (otherCursorFrames == null) {
 317                 otherCursorFrames = new HashMap<Object, ImageCursorFrame>();
 318             }
 319 
 320             currentCursorFrame = otherCursorFrames.get(cursorPlatformImage);
 321             if (currentCursorFrame == null) {
 322                 // cursor frame not created yet
 323                 final ImageCursorFrame newCursorFrame =
 324                         new ImageCursorFrame(cursorPlatformImage,
 325                                              cursorImage.getWidth(),
 326                                              cursorImage.getHeight(),
 327                                              getHotspotX(),
 328                                              getHotspotY());
 329 
 330                 otherCursorFrames.put(cursorPlatformImage, newCursorFrame);
 331                 currentCursorFrame = newCursorFrame;
 332             }
 333         }
 334 
 335         return currentCursorFrame;
 336      }
 337 
 338     private void invalidateCurrentFrame() {
 339         currentCursorFrame = null;
 340     }
 341 
 342     @Override
 343     void activate() {
 344         if (++activeCounter == 1) {
 345             bindImage(getImage());
 346             invalidateCurrentFrame();
 347         }
 348     }
 349 
 350     @Override
 351     void deactivate() {
 352         if (--activeCounter == 0) {
 353             unbindImage(getImage());
 354         }
 355     }
 356 
 357     private void initialize(final Image[] images,
 358                             final double hotspotX,
 359                             final double hotspotY) {
 360         final Dimension2D dim = getBestSize(1f, 1f);
 361 
 362         // If no valid image or if custom cursors are not supported, leave
 363         // the default image cursor
 364         if ((images.length == 0) || (dim.getWidth() == 0f)
 365                                  || (dim.getHeight() == 0f)) {
 366             return;
 367         }
 368 
 369         // If only a single image, use it to construct a custom cursor
 370         if (images.length == 1) {
 371             initialize(images[0], hotspotX, hotspotY);
 372             return;
 373         }
 374 
 375         final Image bestImage = findBestImage(images);
 376         final double scaleX = bestImage.getWidth() / images[0].getWidth();
 377         final double scaleY = bestImage.getHeight() / images[0].getHeight();
 378 
 379         initialize(bestImage, hotspotX * scaleX, hotspotY * scaleY);
 380     }
 381 
 382     private void initialize(Image newImage,
 383                             double newHotspotX,
 384                             double newHotspotY) {
 385         final Image oldImage = getImage();
 386         final double oldHotspotX = getHotspotX();
 387         final double oldHotspotY = getHotspotY();
 388 
 389         if ((newImage == null) || (newImage.getWidth() < 1f)
 390                                || (newImage.getHeight() < 1f)) {
 391             // If image is invalid set the hotspot to 0
 392             newHotspotX = 0f;
 393             newHotspotY = 0f;
 394         } else {
 395             if (newHotspotX < 0f) {
 396                 newHotspotX = 0f;
 397             }
 398             if (newHotspotX > (newImage.getWidth() - 1f)) {
 399                 newHotspotX = newImage.getWidth() - 1f;
 400             }
 401             if (newHotspotY < 0f) {
 402                 newHotspotY = 0f;
 403             }
 404             if (newHotspotY > (newImage.getHeight() - 1f)) {
 405                 newHotspotY = newImage.getHeight() - 1f;
 406             }
 407         }
 408 
 409         imagePropertyImpl().store(newImage);
 410         hotspotXPropertyImpl().store(newHotspotX);
 411         hotspotYPropertyImpl().store(newHotspotY);
 412 
 413         if (oldImage != newImage) {
 414             if (activeCounter > 0) {
 415                 unbindImage(oldImage);
 416                 bindImage(newImage);
 417             }
 418 
 419             invalidateCurrentFrame();
 420             image.fireValueChangedEvent();
 421         }
 422 
 423         if (oldHotspotX != newHotspotX) {
 424             hotspotX.fireValueChangedEvent();
 425         }
 426 
 427         if (oldHotspotY != newHotspotY) {
 428             hotspotY.fireValueChangedEvent();
 429         }
 430     }
 431 
 432     private InvalidationListener imageListener;
 433 
 434     private InvalidationListener getImageListener() {
 435         if (imageListener == null) {
 436             imageListener = valueModel -> invalidateCurrentFrame();
 437         }
 438 
 439         return imageListener;
 440     }
 441 
 442     private void bindImage(final Image toImage) {
 443         if (toImage == null) {
 444             return;
 445         }
 446 
 447         Toolkit.getImageAccessor().getImageProperty(toImage).addListener(getImageListener());
 448     }
 449 
 450     private void unbindImage(final Image fromImage) {
 451         if (fromImage == null) {
 452             return;
 453         }
 454 
 455         Toolkit.getImageAccessor().getImageProperty(fromImage).removeListener(getImageListener());
 456     }
 457 
 458     private static boolean needsDelayedInitialization(final Image[] images) {
 459         for (final Image image: images) {
 460             if (image.getProgress() < 1) {
 461                 return true;
 462             }
 463         }
 464 
 465         return false;
 466     }
 467 
 468     // Utility function to select the best image
 469     private static Image findBestImage(final Image[] images) {
 470         // Check for exact match and return the first such match
 471         for (final Image image: images) {
 472             final Dimension2D dim = getBestSize((int) image.getWidth(),
 473                                                 (int) image.getHeight());
 474             if ((dim.getWidth() == image.getWidth())
 475                     && (dim.getHeight() == image.getHeight())) {
 476                 return image;
 477             }
 478         }
 479 
 480         // No exact match, check for closest match without down-scaling
 481         // (i.e., smallest scale >= 1.0)
 482         Image bestImage = null;
 483         double bestRatio = Double.MAX_VALUE;
 484         for (final Image image: images) {
 485             if ((image.getWidth() > 0) && (image.getHeight() > 0)) {
 486                 final Dimension2D dim = getBestSize(image.getWidth(),
 487                                                     image.getHeight());
 488                 final double ratioX = dim.getWidth() / image.getWidth();
 489                 final double ratioY = dim.getHeight() / image.getHeight();
 490                 if ((ratioX >= 1) && (ratioY >= 1)) {
 491                     final double ratio = Math.max(ratioX, ratioY);
 492                     if (ratio < bestRatio) {
 493                         bestImage = image;
 494                         bestRatio = ratio;
 495                     }
 496                 }
 497             }
 498         }
 499         if (bestImage != null) {
 500             return bestImage;
 501         }
 502 
 503         // Still no match, check for closest match alowing for down-scaling
 504         // (i.e., smallest up-scale or down-scale >= 1.0)
 505         for (final Image image: images) {
 506             if ((image.getWidth() > 0) && (image.getHeight() > 0)) {
 507                 final Dimension2D dim = getBestSize(image.getWidth(),
 508                                                     image.getHeight());
 509                 if ((dim.getWidth() > 0) && (dim.getHeight() > 0)) {
 510                     double ratioX = dim.getWidth() / image.getWidth();
 511                     if (ratioX < 1) {
 512                         ratioX = 1 / ratioX;
 513                     }
 514                     double ratioY = dim.getHeight() / image.getHeight();
 515                     if (ratioY < 1) {
 516                         ratioY = 1 / ratioY;
 517                     }
 518                     final double ratio = Math.max(ratioX, ratioY);
 519                     if (ratio < bestRatio) {
 520                         bestImage = image;
 521                         bestRatio = ratio;
 522                     }
 523                 }
 524             }
 525         }
 526         if (bestImage != null) {
 527             return bestImage;
 528         }
 529 
 530         return images[0];
 531     }
 532 
 533     private final class DoublePropertyImpl extends ReadOnlyDoublePropertyBase {
 534         private final String name;
 535 
 536         private double value;
 537 
 538         public DoublePropertyImpl(final String name) {
 539             this.name = name;
 540         }
 541 
 542         public void store(final double value) {
 543             this.value = value;
 544         }
 545 
 546         @Override
 547         public void fireValueChangedEvent() {
 548             super.fireValueChangedEvent();
 549         }
 550 
 551         @Override
 552         public double get() {
 553             return value;
 554         }
 555 
 556         @Override
 557         public Object getBean() {
 558             return ImageCursor.this;
 559         }
 560 
 561         @Override
 562         public String getName() {
 563             return name;
 564         }
 565     }
 566 
 567     private final class ObjectPropertyImpl<T>
 568             extends ReadOnlyObjectPropertyBase<T> {
 569         private final String name;
 570 
 571         private T value;
 572 
 573         public ObjectPropertyImpl(final String name) {
 574             this.name = name;
 575         }
 576 
 577         public void store(final T value) {
 578             this.value = value;
 579         }
 580 
 581         @Override
 582         public void fireValueChangedEvent() {
 583             super.fireValueChangedEvent();
 584         }
 585 
 586         @Override
 587         public T get() {
 588             return value;
 589         }
 590 
 591         @Override
 592         public Object getBean() {
 593             return ImageCursor.this;
 594         }
 595 
 596         @Override
 597         public String getName() {
 598             return name;
 599         }
 600     }
 601 
 602     private static final class DelayedInitialization
 603             implements InvalidationListener {
 604         private final ImageCursor targetCursor;
 605 
 606         private final Image[] images;
 607         private final double hotspotX;
 608         private final double hotspotY;
 609 
 610         private final boolean initAsSingle;
 611 
 612         private int waitForImages;
 613 
 614         private DelayedInitialization(final ImageCursor targetCursor,
 615                                       final Image[] images,
 616                                       final double hotspotX,
 617                                       final double hotspotY,
 618                                       final boolean initAsSingle) {
 619             this.targetCursor = targetCursor;
 620             this.images = images;
 621             this.hotspotX = hotspotX;
 622             this.hotspotY = hotspotY;
 623             this.initAsSingle = initAsSingle;
 624         }
 625 
 626 
 627         public static void applyTo(final ImageCursor imageCursor,
 628                                    final Image[] images,
 629                                    final double hotspotX,
 630                                    final double hotspotY) {
 631             final DelayedInitialization delayedInitialization =
 632                     new DelayedInitialization(imageCursor,
 633                                               Arrays.copyOf(images, images.length),
 634                                               hotspotX,
 635                                               hotspotY,
 636                                               false);
 637             delayedInitialization.start();
 638         }
 639 
 640         public static void applyTo(final ImageCursor imageCursor,
 641                                    final Image image,
 642                                    final double hotspotX,
 643                                    final double hotspotY) {
 644             final DelayedInitialization delayedInitialization =
 645                     new DelayedInitialization(imageCursor,
 646                                               new Image[] { image },
 647                                               hotspotX,
 648                                               hotspotY,
 649                                               true);
 650             delayedInitialization.start();
 651         }
 652 
 653         private void start() {
 654             for (final Image image: images) {
 655                 if (image.getProgress() < 1) {
 656                     ++waitForImages;
 657                     image.progressProperty().addListener(this);
 658                 }
 659             }
 660         }
 661 
 662         private void cleanupAndFinishInitialization() {
 663             for (final Image image: images) {
 664                 image.progressProperty().removeListener(this);
 665             }
 666 
 667             if (initAsSingle) {
 668                 targetCursor.initialize(images[0], hotspotX, hotspotY);
 669             } else {
 670                 targetCursor.initialize(images, hotspotX, hotspotY);
 671             }
 672         }
 673 
 674         @Override
 675         public void invalidated(Observable valueModel) {
 676             if (((ReadOnlyDoubleProperty)valueModel).get() == 1) {
 677                 if (--waitForImages == 0) {
 678                     cleanupAndFinishInitialization();
 679                 }
 680             }
 681         }
 682     }
 683 }