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 &lt;= 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 &lt;= 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 }