Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene; 027 028import java.util.HashMap; 029import java.util.Map; 030 031import javafx.beans.InvalidationListener; 032import javafx.beans.Observable; 033import javafx.beans.property.ReadOnlyDoubleProperty; 034import javafx.beans.property.ReadOnlyDoublePropertyBase; 035import javafx.beans.property.ReadOnlyObjectProperty; 036import javafx.beans.property.ReadOnlyObjectPropertyBase; 037import javafx.geometry.Dimension2D; 038import javafx.scene.image.Image; 039 040import com.sun.javafx.cursor.CursorFrame; 041import com.sun.javafx.cursor.ImageCursorFrame; 042import com.sun.javafx.tk.Toolkit; 043import java.util.Arrays; 044 045 046/** 047 * A custom image representation of the mouse cursor. On platforms that don't 048 * support custom cursors, {@code Cursor.DEFAULT} will be used in place of the 049 * specified ImageCursor. 050 * 051 * <p>Example: 052 * <pre> 053import javafx.scene.*; 054import javafx.scene.image.*; 055 056Image image = new Image("mycursor.png"); 057 058Scene scene = new Scene(400, 300); 059scene.setCursor(new ImageCursor(image, 060 image.getWidth() / 2, 061 image.getHeight() /2)); 062 * </pre> 063 * 064 * @since JavaFX 1.3 065 */ 066public class ImageCursor extends Cursor { 067 /** 068 * The image to display when the cursor is active. If the image is null, 069 * {@code Cursor.DEFAULT} will be used. 070 * 071 * @defaultValue null 072 */ 073 private ObjectPropertyImpl<Image> image; 074 075 public final Image getImage() { 076 return image == null ? null : image.get(); 077 } 078 079 public final ReadOnlyObjectProperty<Image> imageProperty() { 080 return imagePropertyImpl(); 081 } 082 083 private ObjectPropertyImpl<Image> imagePropertyImpl() { 084 if (image == null) { 085 image = new ObjectPropertyImpl<Image>("image"); 086 } 087 088 return image; 089 } 090 091 /** 092 * The X coordinate of the cursor's hot spot. This hotspot represents the 093 * location within the cursor image that will be displayed at the mouse 094 * position. This must be in the range of [0,image.width-1]. A value 095 * less than 0 will be set to 0. A value greater than 096 * image.width-1 will be set to image.width-1. 097 * 098 * @defaultValue 0 099 */ 100 private DoublePropertyImpl hotspotX; 101 102 public final double getHotspotX() { 103 return hotspotX == null ? 0.0 : hotspotX.get(); 104 } 105 106 public final ReadOnlyDoubleProperty hotspotXProperty() { 107 return hotspotXPropertyImpl(); 108 } 109 110 private DoublePropertyImpl hotspotXPropertyImpl() { 111 if (hotspotX == null) { 112 hotspotX = new DoublePropertyImpl("hotspotX"); 113 } 114 115 return hotspotX; 116 } 117 118 /** 119 * The Y coordinate of the cursor's hot spot. This hotspot represents the 120 * location within the cursor image that will be displayed at the mouse 121 * position. This must be in the range of [0,image.height-1]. A value 122 * less than 0 will be set to 0. A value greater than 123 * image.height-1 will be set to image.height-1. 124 * 125 * @defaultValue 0 126 */ 127 private DoublePropertyImpl hotspotY; 128 129 public final double getHotspotY() { 130 return hotspotY == null ? 0.0 : hotspotY.get(); 131 } 132 133 public final ReadOnlyDoubleProperty hotspotYProperty() { 134 return hotspotYPropertyImpl(); 135 } 136 137 private DoublePropertyImpl hotspotYPropertyImpl() { 138 if (hotspotY == null) { 139 hotspotY = new DoublePropertyImpl("hotspotY"); 140 } 141 142 return hotspotY; 143 } 144 145 private CursorFrame currentCursorFrame; 146 147 /** 148 * Stores the first cursor frame. For non-animated cursors there is only one 149 * frame and so the {@code restCursorFrames} is {@code null}. 150 */ 151 private ImageCursorFrame firstCursorFrame; 152 153 /** 154 * Maps platform images to cursor frames. It doesn't store the first cursor 155 * frame and so it needs to be created only for animated cursors. 156 */ 157 private Map<Object, ImageCursorFrame> otherCursorFrames; 158 159 /** 160 * Indicates whether the image cursor is currently in use. The active cursor 161 * is bound to the image and invalidates its platform cursor when the image 162 * changes. 163 */ 164 private int activeCounter; 165 166 /** 167 * Constructs a new empty {@code ImageCursor} which will look as 168 * {@code Cursor.DEFAULT}. 169 */ 170 public ImageCursor() { 171 } 172 173 /** 174 * Constructs an {@code ImageCursor} from the specified image. The cursor's 175 * hot spot will default to the upper left corner. 176 * 177 * @param image the image 178 */ 179 public ImageCursor(final Image image) { 180 this(image, 0f, 0f); 181 } 182 183 /** 184 * Constructs an {@code ImageCursor} from the specified image and hotspot 185 * coordinates. 186 * 187 * @param image the image 188 * @param hotspotX the X coordinate of the cursor's hot spot 189 * @param hotspotY the Y coordinate of the cursor's hot spot 190 */ 191 public ImageCursor(final Image image, 192 double hotspotX, 193 double hotspotY) { 194 if ((image != null) && (image.getProgress() < 1)) { 195 DelayedInitialization.applyTo( 196 this, image, hotspotX, hotspotY); 197 } else { 198 initialize(image, hotspotX, hotspotY); 199 } 200 } 201 202 /** 203 * Gets the supported cursor size that is closest to the specified preferred 204 * size. A value of (0,0) is returned if the platform does not support 205 * custom cursors. 206 * 207 * <p> 208 * Note: if an image is used whose dimensions don't match a supported size 209 * (as returned by this method), the implementation will resize the image to 210 * a supported size. This may result in a loss of quality. 211 * 212 * <p> 213 * Note: These values can vary between operating systems, graphics cards and 214 * screen resolution, but at the time of this writing, a sample Windows 215 * Vista machine returned 32x32 for all requested sizes, while sample Mac 216 * and Linux machines returned the requested size up to a maximum of 64x64. 217 * Applications should provide a 32x32 cursor, which will work well on all 218 * platforms, and may optionally wish to provide a 64x64 cursor for those 219 * platforms on which it is supported. 220 * 221 * @param preferredWidth the preferred width of the cursor 222 * @param preferredHeight the preferred height of the cursor 223 * @return the supported cursor size 224 * @since JavaFX 1.3 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 * @since JavaFX 1.3 252 */ 253 public static int getMaximumColors() { 254 return Toolkit.getToolkit().getMaximumCursorColors(); 255 } 256 257 /** 258 * Creates a custom image cursor from one of the specified images. This function 259 * will choose the image whose size most closely matched the best cursor size. 260 * The hotpotX of the returned ImageCursor is scaled by 261 * chosenImage.width/images[0].width and the hotspotY is scaled by 262 * chosenImage.height/images[0].height. 263 * <p> 264 * On platforms that don't support custom cursors, {@code Cursor.DEFAULT} will 265 * be used in place of the returned ImageCursor. 266 * 267 * @param images a sequence of images from which to choose, in order of preference 268 * @param hotspotX the X coordinate of the hotspot within the first image 269 * in the images sequence 270 * @param hotspotY the Y coordinate of the hotspot within the first image 271 * in the images sequence 272 * @return a cursor created from the best image 273 * @since JavaFX 1.3 274 */ 275 public static ImageCursor chooseBestCursor( 276 final Image[] images, final double hotspotX, final double hotspotY) { 277 final ImageCursor imageCursor = new ImageCursor(); 278 279 if (needsDelayedInitialization(images)) { 280 DelayedInitialization.applyTo( 281 imageCursor, images, hotspotX, hotspotY); 282 } else { 283 imageCursor.initialize(images, hotspotX, hotspotY); 284 } 285 286 return imageCursor; 287 } 288 289 @Override CursorFrame getCurrentFrame() { 290 if (currentCursorFrame != null) { 291 return currentCursorFrame; 292 } 293 294 final Image cursorImage = getImage(); 295 296 if (cursorImage == null) { 297 currentCursorFrame = Cursor.DEFAULT.getCurrentFrame(); 298 return currentCursorFrame; 299 } 300 301 final Object cursorPlatformImage = cursorImage.impl_getPlatformImage(); 302 if (cursorPlatformImage == null) { 303 currentCursorFrame = Cursor.DEFAULT.getCurrentFrame(); 304 return currentCursorFrame; 305 } 306 307 if (firstCursorFrame == null) { 308 firstCursorFrame = 309 new ImageCursorFrame(cursorPlatformImage, 310 cursorImage.getWidth(), 311 cursorImage.getHeight(), 312 getHotspotX(), 313 getHotspotY()); 314 currentCursorFrame = firstCursorFrame; 315 } else if (firstCursorFrame.getPlatformImage() == cursorPlatformImage) { 316 currentCursorFrame = firstCursorFrame; 317 } else { 318 if (otherCursorFrames == null) { 319 otherCursorFrames = new HashMap<Object, ImageCursorFrame>(); 320 } 321 322 currentCursorFrame = otherCursorFrames.get(cursorPlatformImage); 323 if (currentCursorFrame == null) { 324 // cursor frame not created yet 325 final ImageCursorFrame newCursorFrame = 326 new ImageCursorFrame(cursorPlatformImage, 327 cursorImage.getWidth(), 328 cursorImage.getHeight(), 329 getHotspotX(), 330 getHotspotY()); 331 332 otherCursorFrames.put(cursorPlatformImage, newCursorFrame); 333 currentCursorFrame = newCursorFrame; 334 } 335 } 336 337 return currentCursorFrame; 338 } 339 340 private void invalidateCurrentFrame() { 341 currentCursorFrame = null; 342 } 343 344 @Override 345 void activate() { 346 if (++activeCounter == 1) { 347 bindImage(getImage()); 348 invalidateCurrentFrame(); 349 } 350 } 351 352 @Override 353 void deactivate() { 354 if (--activeCounter == 0) { 355 unbindImage(getImage()); 356 } 357 } 358 359 private void initialize(final Image[] images, 360 final double hotspotX, 361 final double hotspotY) { 362 final Dimension2D dim = getBestSize(1f, 1f); 363 364 // If no valid image or if custom cursors are not supported, leave 365 // the default image cursor 366 if ((images.length == 0) || (dim.getWidth() == 0f) 367 || (dim.getHeight() == 0f)) { 368 return; 369 } 370 371 // If only a single image, use it to construct a custom cursor 372 if (images.length == 1) { 373 initialize(images[0], hotspotX, hotspotY); 374 return; 375 } 376 377 final Image bestImage = findBestImage(images); 378 final double scaleX = bestImage.getWidth() / images[0].getWidth(); 379 final double scaleY = bestImage.getHeight() / images[0].getHeight(); 380 381 initialize(bestImage, hotspotX * scaleX, hotspotY * scaleY); 382 } 383 384 private void initialize(Image newImage, 385 double newHotspotX, 386 double newHotspotY) { 387 final Image oldImage = getImage(); 388 final double oldHotspotX = getHotspotX(); 389 final double oldHotspotY = getHotspotY(); 390 391 if ((newImage == null) || (newImage.getWidth() < 1f) 392 || (newImage.getHeight() < 1f)) { 393 // If image is invalid set the hotspot to 0 394 newHotspotX = 0f; 395 newHotspotY = 0f; 396 } else { 397 if (newHotspotX < 0f) { 398 newHotspotX = 0f; 399 } 400 if (newHotspotX > (newImage.getWidth() - 1f)) { 401 newHotspotX = newImage.getWidth() - 1f; 402 } 403 if (newHotspotY < 0f) { 404 newHotspotY = 0f; 405 } 406 if (newHotspotY > (newImage.getHeight() - 1f)) { 407 newHotspotY = newImage.getHeight() - 1f; 408 } 409 } 410 411 imagePropertyImpl().store(newImage); 412 hotspotXPropertyImpl().store(newHotspotX); 413 hotspotYPropertyImpl().store(newHotspotY); 414 415 if (oldImage != newImage) { 416 if (activeCounter > 0) { 417 unbindImage(oldImage); 418 bindImage(newImage); 419 } 420 421 invalidateCurrentFrame(); 422 image.fireValueChangedEvent(); 423 } 424 425 if (oldHotspotX != newHotspotX) { 426 hotspotX.fireValueChangedEvent(); 427 } 428 429 if (oldHotspotY != newHotspotY) { 430 hotspotY.fireValueChangedEvent(); 431 } 432 } 433 434 private InvalidationListener imageListener; 435 436 private InvalidationListener getImageListener() { 437 if (imageListener == null) { 438 imageListener = new InvalidationListener() { 439 @Override 440 public void invalidated(Observable valueModel) { 441 invalidateCurrentFrame(); 442 } 443 }; 444 } 445 446 return imageListener; 447 } 448 449 private void bindImage(final Image toImage) { 450 if (toImage == null) { 451 return; 452 } 453 454 Toolkit.getImageAccessor().getImageProperty(toImage).addListener(getImageListener()); 455 } 456 457 private void unbindImage(final Image fromImage) { 458 if (fromImage == null) { 459 return; 460 } 461 462 Toolkit.getImageAccessor().getImageProperty(fromImage).removeListener(getImageListener()); 463 } 464 465 private static boolean needsDelayedInitialization(final Image[] images) { 466 for (final Image image: images) { 467 if (image.getProgress() < 1) { 468 return true; 469 } 470 } 471 472 return false; 473 } 474 475 // Utility function to select the best image 476 private static Image findBestImage(final Image[] images) { 477 // Check for exact match and return the first such match 478 for (final Image image: images) { 479 final Dimension2D dim = getBestSize((int) image.getWidth(), 480 (int) image.getHeight()); 481 if ((dim.getWidth() == image.getWidth()) 482 && (dim.getHeight() == image.getHeight())) { 483 return image; 484 } 485 } 486 487 // No exact match, check for closest match without down-scaling 488 // (i.e., smallest scale >= 1.0) 489 Image bestImage = null; 490 double bestRatio = Double.MAX_VALUE; 491 for (final Image image: images) { 492 if ((image.getWidth() > 0) && (image.getHeight() > 0)) { 493 final Dimension2D dim = getBestSize(image.getWidth(), 494 image.getHeight()); 495 final double ratioX = dim.getWidth() / image.getWidth(); 496 final double ratioY = dim.getHeight() / image.getHeight(); 497 if ((ratioX >= 1) && (ratioY >= 1)) { 498 final double ratio = Math.max(ratioX, ratioY); 499 if (ratio < bestRatio) { 500 bestImage = image; 501 bestRatio = ratio; 502 } 503 } 504 } 505 } 506 if (bestImage != null) { 507 return bestImage; 508 } 509 510 // Still no match, check for closest match alowing for down-scaling 511 // (i.e., smallest up-scale or down-scale >= 1.0) 512 for (final Image image: images) { 513 if ((image.getWidth() > 0) && (image.getHeight() > 0)) { 514 final Dimension2D dim = getBestSize(image.getWidth(), 515 image.getHeight()); 516 if ((dim.getWidth() > 0) && (dim.getHeight() > 0)) { 517 double ratioX = dim.getWidth() / image.getWidth(); 518 if (ratioX < 1) { 519 ratioX = 1 / ratioX; 520 } 521 double ratioY = dim.getHeight() / image.getHeight(); 522 if (ratioY < 1) { 523 ratioY = 1 / ratioY; 524 } 525 final double ratio = Math.max(ratioX, ratioY); 526 if (ratio < bestRatio) { 527 bestImage = image; 528 bestRatio = ratio; 529 } 530 } 531 } 532 } 533 if (bestImage != null) { 534 return bestImage; 535 } 536 537 return images[0]; 538 } 539 540 private final class DoublePropertyImpl extends ReadOnlyDoublePropertyBase { 541 private final String name; 542 543 private double value; 544 545 public DoublePropertyImpl(final String name) { 546 this.name = name; 547 } 548 549 public void store(final double value) { 550 this.value = value; 551 } 552 553 @Override 554 public void fireValueChangedEvent() { 555 super.fireValueChangedEvent(); 556 } 557 558 @Override 559 public double get() { 560 return value; 561 } 562 563 @Override 564 public Object getBean() { 565 return ImageCursor.this; 566 } 567 568 @Override 569 public String getName() { 570 return name; 571 } 572 } 573 574 private final class ObjectPropertyImpl<T> 575 extends ReadOnlyObjectPropertyBase<T> { 576 private final String name; 577 578 private T value; 579 580 public ObjectPropertyImpl(final String name) { 581 this.name = name; 582 } 583 584 public void store(final T value) { 585 this.value = value; 586 } 587 588 @Override 589 public void fireValueChangedEvent() { 590 super.fireValueChangedEvent(); 591 } 592 593 @Override 594 public T get() { 595 return value; 596 } 597 598 @Override 599 public Object getBean() { 600 return ImageCursor.this; 601 } 602 603 @Override 604 public String getName() { 605 return name; 606 } 607 } 608 609 private static final class DelayedInitialization 610 implements InvalidationListener { 611 private final ImageCursor targetCursor; 612 613 private final Image[] images; 614 private final double hotspotX; 615 private final double hotspotY; 616 617 private final boolean initAsSingle; 618 619 private int waitForImages; 620 621 private DelayedInitialization(final ImageCursor targetCursor, 622 final Image[] images, 623 final double hotspotX, 624 final double hotspotY, 625 final boolean initAsSingle) { 626 this.targetCursor = targetCursor; 627 this.images = images; 628 this.hotspotX = hotspotX; 629 this.hotspotY = hotspotY; 630 this.initAsSingle = initAsSingle; 631 } 632 633 634 public static void applyTo(final ImageCursor imageCursor, 635 final Image[] images, 636 final double hotspotX, 637 final double hotspotY) { 638 final DelayedInitialization delayedInitialization = 639 new DelayedInitialization(imageCursor, 640 Arrays.copyOf(images, images.length), 641 hotspotX, 642 hotspotY, 643 false); 644 delayedInitialization.start(); 645 } 646 647 public static void applyTo(final ImageCursor imageCursor, 648 final Image image, 649 final double hotspotX, 650 final double hotspotY) { 651 final DelayedInitialization delayedInitialization = 652 new DelayedInitialization(imageCursor, 653 new Image[] { image }, 654 hotspotX, 655 hotspotY, 656 true); 657 delayedInitialization.start(); 658 } 659 660 private void start() { 661 for (final Image image: images) { 662 if (image.getProgress() < 1) { 663 ++waitForImages; 664 image.progressProperty().addListener(this); 665 } 666 } 667 } 668 669 private void cleanupAndFinishInitialization() { 670 for (final Image image: images) { 671 image.progressProperty().removeListener(this); 672 } 673 674 if (initAsSingle) { 675 targetCursor.initialize(images[0], hotspotX, hotspotY); 676 } else { 677 targetCursor.initialize(images, hotspotX, hotspotY); 678 } 679 } 680 681 @Override 682 public void invalidated(Observable valueModel) { 683 if (((ReadOnlyDoubleProperty)valueModel).get() == 1) { 684 if (--waitForImages == 0) { 685 cleanupAndFinishInitialization(); 686 } 687 } 688 } 689 } 690}