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