1 /* 2 * Copyright (c) 2008, 2017, 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.image; 27 28 import com.sun.javafx.beans.event.AbstractNotifyListener; 29 import com.sun.javafx.css.StyleManager; 30 import javafx.css.converter.URLConverter; 31 import com.sun.javafx.geom.BaseBounds; 32 import com.sun.javafx.geom.transform.BaseTransform; 33 import com.sun.javafx.jmx.MXNodeAlgorithm; 34 import com.sun.javafx.jmx.MXNodeAlgorithmContext; 35 import com.sun.javafx.scene.DirtyBits; 36 import com.sun.javafx.scene.ImageViewHelper; 37 import com.sun.javafx.scene.NodeHelper; 38 import com.sun.javafx.sg.prism.NGImageView; 39 import com.sun.javafx.sg.prism.NGNode; 40 import com.sun.javafx.tk.Toolkit; 41 import javafx.beans.DefaultProperty; 42 import javafx.beans.Observable; 43 import javafx.beans.property.*; 44 import javafx.css.CssMetaData; 45 import javafx.css.Styleable; 46 import javafx.css.StyleableProperty; 47 import javafx.css.StyleableStringProperty; 48 import javafx.geometry.NodeOrientation; 49 import javafx.geometry.Rectangle2D; 50 import javafx.scene.AccessibleRole; 51 import javafx.scene.Node; 52 import java.util.ArrayList; 53 import java.util.Collections; 54 import java.util.List; 55 56 /** 57 * The {@code ImageView} is a {@code Node} used for painting images loaded with 58 * {@link Image} class. 59 * 60 * <p> 61 * This class allows resizing the displayed image (with or without preserving 62 * the original aspect ratio) and specifying a viewport into the source image 63 * for restricting the pixels displayed by this {@code ImageView}. 64 * </p> 65 * 66 * 67 * <p> 68 * Example code for displaying images 69 * </p> 70 * 71 * <pre><code> 72 * import javafx.application.Application; 73 * import javafx.geometry.Rectangle2D; 74 * import javafx.scene.Group; 75 * import javafx.scene.Scene; 76 * import javafx.scene.image.Image; 77 * import javafx.scene.image.ImageView; 78 * import javafx.scene.layout.HBox; 79 * import javafx.scene.paint.Color; 80 * import javafx.stage.Stage; 81 * 82 * public class HelloImageView extends Application { 83 * 84 * {@literal @Override} public void start(Stage stage) { 85 * // load the image 86 * Image image = new Image("flower.png"); 87 * 88 * // simple displays ImageView the image as is 89 * ImageView iv1 = new ImageView(); 90 * iv1.setImage(image); 91 * 92 * // resizes the image to have width of 100 while preserving the ratio and using 93 * // higher quality filtering method; this ImageView is also cached to 94 * // improve performance 95 * ImageView iv2 = new ImageView(); 96 * iv2.setImage(image); 97 * iv2.setFitWidth(100); 98 * iv2.setPreserveRatio(true); 99 * iv2.setSmooth(true); 100 * iv2.setCache(true); 101 * 102 * // defines a viewport into the source image (achieving a "zoom" effect) and 103 * // displays it rotated 104 * ImageView iv3 = new ImageView(); 105 * iv3.setImage(image); 106 * Rectangle2D viewportRect = new Rectangle2D(40, 35, 110, 110); 107 * iv3.setViewport(viewportRect); 108 * iv3.setRotate(90); 109 * 110 * Group root = new Group(); 111 * Scene scene = new Scene(root); 112 * scene.setFill(Color.BLACK); 113 * HBox box = new HBox(); 114 * box.getChildren().add(iv1); 115 * box.getChildren().add(iv2); 116 * box.getChildren().add(iv3); 117 * root.getChildren().add(box); 118 * 119 * stage.setTitle("ImageView"); 120 * stage.setWidth(415); 121 * stage.setHeight(200); 122 * stage.setScene(scene); 123 * stage.sizeToScene(); 124 * stage.show(); 125 * } 126 * 127 * public static void main(String[] args) { 128 * Application.launch(args); 129 * } 130 * } 131 * </code></pre> 132 * <p> 133 * The code above produces the following: 134 * </p> 135 * <p> 136 * <img src="doc-files/imageview.png" alt="A visual rendering of the ImageView example"> 137 * </p> 138 * @since JavaFX 2.0 139 */ 140 @DefaultProperty("image") 141 public class ImageView extends Node { 142 static { 143 // This is used by classes in different packages to get access to 144 // private and package private methods. 145 ImageViewHelper.setImageViewAccessor(new ImageViewHelper.ImageViewAccessor() { 146 @Override 147 public NGNode doCreatePeer(Node node) { 148 return ((ImageView) node).doCreatePeer(); 149 } 150 151 @Override 152 public void doUpdatePeer(Node node) { 153 ((ImageView) node).doUpdatePeer(); 154 } 155 156 @Override 157 public BaseBounds doComputeGeomBounds(Node node, 158 BaseBounds bounds, BaseTransform tx) { 159 return ((ImageView) node).doComputeGeomBounds(bounds, tx); 160 } 161 162 @Override 163 public boolean doComputeContains(Node node, double localX, double localY) { 164 return ((ImageView) node).doComputeContains(localX, localY); 165 } 166 167 @Override 168 public Object doProcessMXNode(Node node, MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) { 169 return ((ImageView) node).doProcessMXNode(alg, ctx); 170 } 171 }); 172 } 173 174 { 175 // To initialize the class helper at the begining each constructor of this class 176 ImageViewHelper.initHelper(this); 177 } 178 /** 179 * Allocates a new ImageView object. 180 */ 181 public ImageView() { 182 getStyleClass().add(DEFAULT_STYLE_CLASS); 183 setAccessibleRole(AccessibleRole.IMAGE_VIEW); 184 setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); 185 } 186 187 /** 188 * Allocates a new ImageView object with image loaded from the specified 189 * URL. 190 * <p> 191 * The {@code new ImageView(url)} has the same effect as 192 * {@code new ImageView(new Image(url))}. 193 * </p> 194 * 195 * @param url the string representing the URL from which to load the image 196 * @throws NullPointerException if URL is null 197 * @throws IllegalArgumentException if URL is invalid or unsupported 198 * @since JavaFX 2.1 199 */ 200 public ImageView(String url) { 201 this(new Image(url)); 202 } 203 204 /** 205 * Allocates a new ImageView object using the given image. 206 * 207 * @param image Image that this ImageView uses 208 */ 209 public ImageView(Image image) { 210 getStyleClass().add(DEFAULT_STYLE_CLASS); 211 setAccessibleRole(AccessibleRole.IMAGE_VIEW); 212 setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); 213 setImage(image); 214 } 215 216 /** 217 * The {@link Image} to be painted by this {@code ImageView}. 218 * 219 * @defaultValue null 220 */ 221 private ObjectProperty<Image> image; 222 223 public final void setImage(Image value) { 224 imageProperty().set(value); 225 } 226 public final Image getImage() { 227 return image == null ? null : image.get(); 228 } 229 230 private Image oldImage; 231 public final ObjectProperty<Image> imageProperty() { 232 if (image == null) { 233 image = new ObjectPropertyBase<Image>() { 234 235 private boolean needsListeners = false; 236 237 @Override 238 public void invalidated() { 239 Image _image = get(); 240 boolean dimensionChanged = _image == null || oldImage == null || 241 (oldImage.getWidth() != _image.getWidth() || 242 oldImage.getHeight() != _image.getHeight()); 243 244 if (needsListeners) { 245 Toolkit.getImageAccessor().getImageProperty(oldImage). 246 removeListener(platformImageChangeListener.getWeakListener()); 247 } 248 249 needsListeners = _image != null && (_image.isAnimation() || _image.getProgress() < 1); 250 oldImage = _image; 251 252 if (needsListeners) { 253 Toolkit.getImageAccessor().getImageProperty(_image). 254 addListener(platformImageChangeListener.getWeakListener()); 255 } 256 if (dimensionChanged) { 257 invalidateWidthHeight(); 258 NodeHelper.geomChanged(ImageView.this); 259 } 260 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_CONTENTS); 261 } 262 263 @Override 264 public Object getBean() { 265 return ImageView.this; 266 } 267 268 @Override 269 public String getName() { 270 return "image"; 271 } 272 }; 273 } 274 return image; 275 } 276 277 private StringProperty imageUrl = null; 278 /** 279 * The imageUrl property is set from CSS and then the image property is 280 * set from the invalidated method. This ensures that the same image isn't 281 * reloaded. 282 */ 283 private StringProperty imageUrlProperty() { 284 if (imageUrl == null) { 285 imageUrl = new StyleableStringProperty() { 286 287 @Override 288 protected void invalidated() { 289 290 final String imageUrl = get(); 291 if (imageUrl != null) { 292 setImage(StyleManager.getInstance().getCachedImage(imageUrl)); 293 } else { 294 setImage(null); 295 } 296 } 297 298 @Override 299 public Object getBean() { 300 return ImageView.this; 301 } 302 303 @Override 304 public String getName() { 305 return "imageUrl"; 306 } 307 308 @Override 309 public CssMetaData<ImageView,String> getCssMetaData() { 310 return StyleableProperties.IMAGE; 311 } 312 313 }; 314 } 315 return imageUrl; 316 } 317 318 private final AbstractNotifyListener platformImageChangeListener = 319 new AbstractNotifyListener() { 320 @Override 321 public void invalidated(Observable valueModel) { 322 invalidateWidthHeight(); 323 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_CONTENTS); 324 NodeHelper.geomChanged(ImageView.this); 325 } 326 }; 327 /** 328 * The current x coordinate of the {@code ImageView} origin. 329 * 330 * @defaultValue 0 331 */ 332 private DoubleProperty x; 333 334 335 public final void setX(double value) { 336 xProperty().set(value); 337 } 338 339 public final double getX() { 340 return x == null ? 0.0 : x.get(); 341 } 342 343 public final DoubleProperty xProperty() { 344 if (x == null) { 345 x = new DoublePropertyBase() { 346 347 @Override 348 protected void invalidated() { 349 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_GEOMETRY); 350 NodeHelper.geomChanged(ImageView.this); 351 } 352 353 @Override 354 public Object getBean() { 355 return ImageView.this; 356 } 357 358 @Override 359 public String getName() { 360 return "x"; 361 } 362 }; 363 } 364 return x; 365 } 366 367 /** 368 * The current y coordinate of the {@code ImageView} origin. 369 * 370 * @defaultValue 0 371 */ 372 private DoubleProperty y; 373 374 375 public final void setY(double value) { 376 yProperty().set(value); 377 } 378 379 public final double getY() { 380 return y == null ? 0.0 : y.get(); 381 } 382 383 public final DoubleProperty yProperty() { 384 if (y == null) { 385 y = new DoublePropertyBase() { 386 387 @Override 388 protected void invalidated() { 389 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_GEOMETRY); 390 NodeHelper.geomChanged(ImageView.this); 391 } 392 393 @Override 394 public Object getBean() { 395 return ImageView.this; 396 } 397 398 @Override 399 public String getName() { 400 return "y"; 401 } 402 }; 403 } 404 return y; 405 } 406 407 /** 408 * The width of the bounding box within which the source image is resized as 409 * necessary to fit. If set to a value <= 0, then the intrinsic width of the 410 * image will be used as the {@code fitWidth}. 411 * <p> 412 * See {@link #preserveRatio} for information on interaction between image 413 * view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio} 414 * attributes. 415 * 416 * @defaultValue 0 417 */ 418 private DoubleProperty fitWidth; 419 420 421 public final void setFitWidth(double value) { 422 fitWidthProperty().set(value); 423 } 424 425 public final double getFitWidth() { 426 return fitWidth == null ? 0.0 : fitWidth.get(); 427 } 428 429 public final DoubleProperty fitWidthProperty() { 430 if (fitWidth == null) { 431 fitWidth = new DoublePropertyBase() { 432 433 @Override 434 protected void invalidated() { 435 invalidateWidthHeight(); 436 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT); 437 NodeHelper.geomChanged(ImageView.this); 438 } 439 440 @Override 441 public Object getBean() { 442 return ImageView.this; 443 } 444 445 @Override 446 public String getName() { 447 return "fitWidth"; 448 } 449 }; 450 } 451 return fitWidth; 452 } 453 454 /** 455 * The height of the bounding box within which the source image is resized 456 * as necessary to fit. If set to a value <= 0, then the intrinsic height of 457 * the image will be used as the {@code fitHeight}. 458 * <p> 459 * See {@link #preserveRatio} for information on interaction between image 460 * view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio} 461 * attributes. 462 * </p> 463 * 464 * @defaultValue 0 465 */ 466 private DoubleProperty fitHeight; 467 468 469 public final void setFitHeight(double value) { 470 fitHeightProperty().set(value); 471 } 472 473 public final double getFitHeight() { 474 return fitHeight == null ? 0.0 : fitHeight.get(); 475 } 476 477 public final DoubleProperty fitHeightProperty() { 478 if (fitHeight == null) { 479 fitHeight = new DoublePropertyBase() { 480 481 @Override 482 protected void invalidated() { 483 invalidateWidthHeight(); 484 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT); 485 NodeHelper.geomChanged(ImageView.this); 486 } 487 488 @Override 489 public Object getBean() { 490 return ImageView.this; 491 } 492 493 @Override 494 public String getName() { 495 return "fitHeight"; 496 } 497 }; 498 } 499 return fitHeight; 500 } 501 502 /** 503 * Indicates whether to preserve the aspect ratio of the source image when 504 * scaling to fit the image within the fitting bounding box. 505 * <p> 506 * If set to {@code true}, it affects the dimensions of this 507 * {@code ImageView} in the following way * 508 * <ul> 509 * <li>If only {@code fitWidth} is set, height is scaled to preserve ratio 510 * <li>If only {@code fitHeight} is set, width is scaled to preserve ratio 511 * <li>If both are set, they both may be scaled to get the best fit in a 512 * width by height rectangle while preserving the original aspect ratio 513 * </ul> 514 * 515 * If unset or set to {@code false}, it affects the dimensions of this 516 * {@code ImageView} in the following way * 517 * <ul> 518 * <li>If only {@code fitWidth} is set, image's view width is scaled to 519 * match and height is unchanged; 520 * <li>If only {@code fitHeight} is set, image's view height is scaled to 521 * match and height is unchanged; 522 * <li>If both are set, the image view is scaled to match both. 523 * </ul> 524 * 525 * Note that the dimensions of this node as reported by the node's bounds 526 * will be equal to the size of the scaled image and is guaranteed to be 527 * contained within {@code fitWidth x fitHeight} bonding box. 528 * 529 * @defaultValue false 530 */ 531 private BooleanProperty preserveRatio; 532 533 534 public final void setPreserveRatio(boolean value) { 535 preserveRatioProperty().set(value); 536 } 537 538 public final boolean isPreserveRatio() { 539 return preserveRatio == null ? false : preserveRatio.get(); 540 } 541 542 public final BooleanProperty preserveRatioProperty() { 543 if (preserveRatio == null) { 544 preserveRatio = new BooleanPropertyBase() { 545 546 @Override 547 protected void invalidated() { 548 invalidateWidthHeight(); 549 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT); 550 NodeHelper.geomChanged(ImageView.this); 551 } 552 553 @Override 554 public Object getBean() { 555 return ImageView.this; 556 } 557 558 @Override 559 public String getName() { 560 return "preserveRatio"; 561 } 562 }; 563 } 564 return preserveRatio; 565 } 566 567 /** 568 * Indicates whether to use a better quality filtering algorithm or a faster 569 * one when transforming or scaling the source image to fit within the 570 * bounding box provided by {@code fitWidth} and {@code fitHeight}. 571 * 572 * <p> 573 * If set to {@code true} a better quality filtering will be used, if set to 574 * {@code false} a faster but lesser quality filtering will be used. 575 * </p> 576 * 577 * <p> 578 * The default value depends on platform configuration. 579 * </p> 580 * 581 * @defaultValue platform-dependent 582 */ 583 private BooleanProperty smooth; 584 585 586 public final void setSmooth(boolean value) { 587 smoothProperty().set(value); 588 } 589 590 public final boolean isSmooth() { 591 return smooth == null ? SMOOTH_DEFAULT : smooth.get(); 592 } 593 594 public final BooleanProperty smoothProperty() { 595 if (smooth == null) { 596 smooth = new BooleanPropertyBase(SMOOTH_DEFAULT) { 597 598 @Override 599 protected void invalidated() { 600 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_SMOOTH); 601 } 602 603 @Override 604 public Object getBean() { 605 return ImageView.this; 606 } 607 608 @Override 609 public String getName() { 610 return "smooth"; 611 } 612 }; 613 } 614 return smooth; 615 } 616 617 /** 618 * Platform-dependent default value of the {@link #smoothProperty smooth} property. 619 */ 620 public static final boolean SMOOTH_DEFAULT = Toolkit.getToolkit() 621 .getDefaultImageSmooth(); 622 /** 623 * The rectangular viewport into the image. The viewport is specified in the 624 * coordinates of the image, prior to scaling or any other transformations. 625 * 626 * <p> 627 * If {@code viewport} is {@code null}, the entire image is displayed. If 628 * {@code viewport} is non-{@code null}, only the portion of the image which 629 * falls within the viewport will be displayed. If the image does not fully 630 * cover the viewport then any remaining area of the viewport will be empty. 631 * </p> 632 * 633 * @defaultValue null 634 */ 635 private ObjectProperty<Rectangle2D> viewport; 636 637 638 public final void setViewport(Rectangle2D value) { 639 viewportProperty().set(value); 640 } 641 642 public final Rectangle2D getViewport() { 643 return viewport == null ? null : viewport.get(); 644 } 645 646 public final ObjectProperty<Rectangle2D> viewportProperty() { 647 if (viewport == null) { 648 viewport = new ObjectPropertyBase<Rectangle2D>() { 649 650 @Override 651 protected void invalidated() { 652 invalidateWidthHeight(); 653 NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT); 654 NodeHelper.geomChanged(ImageView.this); 655 } 656 657 @Override 658 public Object getBean() { 659 return ImageView.this; 660 } 661 662 @Override 663 public String getName() { 664 return "viewport"; 665 } 666 }; 667 } 668 return viewport; 669 } 670 671 // Need to track changes to image width and image height and recompute 672 // bounds when changed. 673 // imageWidth = bind image.width on replace { 674 // NodeHelper.geomChanged(ImageView.this); 675 // } 676 // 677 // imageHeight = bind image.height on replace { 678 // NodeHelper.geomChanged(ImageView.this); 679 // } 680 681 private double destWidth, destHeight; 682 683 /* 684 * Note: This method MUST only be called via its accessor method. 685 */ 686 private NGNode doCreatePeer() { 687 return new NGImageView(); 688 } 689 690 /* 691 * Note: This method MUST only be called via its accessor method. 692 */ 693 private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) { 694 recomputeWidthHeight(); 695 696 bounds = bounds.deriveWithNewBounds((float)getX(), (float)getY(), 0.0f, 697 (float)(getX() + destWidth), (float)(getY() + destHeight), 0.0f); 698 bounds = tx.transform(bounds, bounds); 699 return bounds; 700 } 701 702 private boolean validWH; 703 704 private void invalidateWidthHeight() { 705 validWH = false; 706 } 707 708 private void recomputeWidthHeight() { 709 if (validWH) { 710 return; 711 } 712 Image localImage = getImage(); 713 Rectangle2D localViewport = getViewport(); 714 715 double w = 0; 716 double h = 0; 717 if (localViewport != null && localViewport.getWidth() > 0 && localViewport.getHeight() > 0) { 718 w = localViewport.getWidth(); 719 h = localViewport.getHeight(); 720 } else if (localImage != null) { 721 w = localImage.getWidth(); 722 h = localImage.getHeight(); 723 } 724 725 double localFitWidth = getFitWidth(); 726 double localFitHeight = getFitHeight(); 727 728 if (isPreserveRatio() && w > 0 && h > 0 && (localFitWidth > 0 || localFitHeight > 0)) { 729 if (localFitWidth <= 0 || (localFitHeight > 0 && localFitWidth * h > localFitHeight * w)) { 730 w = w * localFitHeight / h; 731 h = localFitHeight; 732 } else { 733 h = h * localFitWidth / w; 734 w = localFitWidth; 735 } 736 } else { 737 if (localFitWidth > 0f) { 738 w = localFitWidth; 739 } 740 if (localFitHeight > 0f) { 741 h = localFitHeight; 742 } 743 } 744 745 // Store these values for use later in doComputeContains() to support 746 // Node.contains(). 747 destWidth = w; 748 destHeight = h; 749 750 validWH = true; 751 } 752 753 /* 754 * Note: This method MUST only be called via its accessor method. 755 */ 756 private boolean doComputeContains(double localX, double localY) { 757 if (getImage() == null) { 758 return false; 759 } 760 761 recomputeWidthHeight(); 762 // Local Note bounds contain test is already done by the caller. 763 // (Node.contains()). 764 765 double dx = localX - getX(); 766 double dy = localY - getY(); 767 768 Image localImage = getImage(); 769 double srcWidth = localImage.getWidth(); 770 double srcHeight = localImage.getHeight(); 771 double viewWidth = srcWidth; 772 double viewHeight = srcHeight; 773 double vw = 0; 774 double vh = 0; 775 double vminx = 0; 776 double vminy = 0; 777 Rectangle2D localViewport = getViewport(); 778 if (localViewport != null) { 779 vw = localViewport.getWidth(); 780 vh = localViewport.getHeight(); 781 vminx = localViewport.getMinX(); 782 vminy = localViewport.getMinY(); 783 } 784 785 if (vw > 0 && vh > 0) { 786 viewWidth = vw; 787 viewHeight = vh; 788 } 789 790 // desWidth Note and destHeight are computed by NodeHelper.computeGeomBounds() 791 // via a call from Node.contains() before calling 792 // doComputeContains(). 793 // Transform into image's coordinate system. 794 dx = vminx + dx * viewWidth / destWidth; 795 dy = vminy + dy * viewHeight / destHeight; 796 // test whether it's inside the original image AND inside of viewport 797 // (viewport may stick out from the image bounds) 798 if (dx < 0.0 || dy < 0.0 || dx >= srcWidth || dy >= srcHeight || 799 dx < vminx || dy < vminy || 800 dx >= vminx + viewWidth || dy >= vminy + viewHeight) { 801 return false; 802 } 803 // Do alpha test on the picked pixel. 804 return Toolkit.getToolkit().imageContains( 805 Toolkit.getImageAccessor().getPlatformImage(localImage), (float)dx, (float)dy); 806 } 807 808 /*************************************************************************** 809 * * Stylesheet Handling * * 810 **************************************************************************/ 811 812 private static final String DEFAULT_STYLE_CLASS = "image-view"; 813 814 /* 815 * Super-lazy instantiation pattern from Bill Pugh. 816 */ 817 private static class StyleableProperties { 818 // TODO 819 // "preserve-ratio","smooth","viewport","fit-width","fit-height" 820 private static final CssMetaData<ImageView, String> IMAGE = 821 new CssMetaData<ImageView,String>("-fx-image", 822 URLConverter.getInstance()) { 823 824 @Override 825 public boolean isSettable(ImageView n) { 826 // Note that we care about the image, not imageUrl 827 return n.image == null || !n.image.isBound(); 828 } 829 830 @Override 831 public StyleableProperty<String> getStyleableProperty(ImageView n) { 832 return (StyleableProperty<String>)n.imageUrlProperty(); 833 } 834 }; 835 836 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 837 static { 838 final List<CssMetaData<? extends Styleable, ?>> styleables = 839 new ArrayList<CssMetaData<? extends Styleable, ?>>(Node.getClassCssMetaData()); 840 styleables.add(IMAGE); 841 STYLEABLES = Collections.unmodifiableList(styleables); 842 } 843 } 844 845 /** 846 * @return The CssMetaData associated with this class, which may include the 847 * CssMetaData of its superclasses. 848 * @since JavaFX 8.0 849 */ 850 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 851 return StyleableProperties.STYLEABLES; 852 } 853 854 /** 855 * {@inheritDoc} 856 * @return the CssMetaData associated with this class, which may include the 857 * CssMetaData of its super classes. 858 * @since JavaFX 8.0 859 */ 860 @Override 861 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 862 return getClassCssMetaData(); 863 } 864 865 void updateViewport() { 866 recomputeWidthHeight(); 867 if (getImage() == null || Toolkit.getImageAccessor().getPlatformImage(getImage()) == null) { 868 return; 869 } 870 871 Rectangle2D localViewport = getViewport(); 872 final NGImageView peer = NodeHelper.getPeer(this); 873 if (localViewport != null) { 874 peer.setViewport((float)localViewport.getMinX(), (float)localViewport.getMinY(), 875 (float)localViewport.getWidth(), (float)localViewport.getHeight(), 876 (float)destWidth, (float)destHeight); 877 } else { 878 peer.setViewport(0, 0, 0, 0, (float)destWidth, (float)destHeight); 879 } 880 } 881 882 /* 883 * Note: This method MUST only be called via its accessor method. 884 */ 885 private void doUpdatePeer() { 886 final NGImageView peer = NodeHelper.getPeer(this); 887 if (NodeHelper.isDirty(this, DirtyBits.NODE_GEOMETRY)) { 888 peer.setX((float)getX()); 889 peer.setY((float)getY()); 890 } 891 if (NodeHelper.isDirty(this, DirtyBits.NODE_SMOOTH)) { 892 peer.setSmooth(isSmooth()); 893 } 894 if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) { 895 peer.setImage(getImage() != null 896 ? Toolkit.getImageAccessor().getPlatformImage(getImage()) : null); 897 } 898 // The NG part expects this to be called when image changes 899 if (NodeHelper.isDirty(this, DirtyBits.NODE_VIEWPORT) || NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) { 900 updateViewport(); 901 } 902 } 903 904 /* 905 * Note: This method MUST only be called via its accessor method. 906 */ 907 private Object doProcessMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) { 908 return alg.processLeafNode(this, ctx); 909 } 910 }