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 }