/*
* Copyright (c) 2008, 2014, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javafx.scene.image;
import com.sun.javafx.beans.event.
AbstractNotifyListener;
import com.sun.javafx.css.
StyleManager;
import com.sun.javafx.css.converters.
URLConverter;
import com.sun.javafx.geom.
BaseBounds;
import com.sun.javafx.geom.transform.
BaseTransform;
import com.sun.javafx.jmx.
MXNodeAlgorithm;
import com.sun.javafx.jmx.
MXNodeAlgorithmContext;
import com.sun.javafx.scene.
DirtyBits;
import com.sun.javafx.sg.prism.
NGImageView;
import com.sun.javafx.sg.prism.
NGNode;
import com.sun.javafx.tk.
Toolkit;
import javafx.beans.
DefaultProperty;
import javafx.beans.
Observable;
import javafx.beans.property.*;
import javafx.css.
CssMetaData;
import javafx.css.
Styleable;
import javafx.css.
StyleableProperty;
import javafx.css.
StyleableStringProperty;
import javafx.geometry.
NodeOrientation;
import javafx.geometry.
Rectangle2D;
import javafx.scene.
AccessibleRole;
import javafx.scene.
Node;
import java.util.
ArrayList;
import java.util.
Collections;
import java.util.
List;
/**
* The {@code ImageView} is a {@code Node} used for painting images loaded with
* {@link Image} class.
*
* <p>
* This class allows resizing the displayed image (with or without preserving
* the original aspect ratio) and specifying a viewport into the source image
* for restricting the pixels displayed by this {@code ImageView}.
* </p>
*
*
* <p>
* Example code for displaying images
* </p>
*
* <pre>
* <code>
* import javafx.application.Application;
* import javafx.geometry.Rectangle2D;
* import javafx.scene.Group;
* import javafx.scene.Scene;
* import javafx.scene.image.Image;
* import javafx.scene.image.ImageView;
* import javafx.scene.layout.HBox;
* import javafx.scene.paint.Color;
* import javafx.stage.Stage;
*
* public class HelloMenu extends Application {
*
* @Override public void start(Stage stage) {
* // load the image
* Image image = new Image("flower.png");
*
* // simple displays ImageView the image as is
* ImageView iv1 = new ImageView();
* iv1.setImage(image);
*
* // resizes the image to have width of 100 while preserving the ratio and using
* // higher quality filtering method; this ImageView is also cached to
* // improve performance
* ImageView iv2 = new ImageView();
* iv2.setImage(image);
* iv2.setFitWidth(100);
* iv2.setPreserveRatio(true);
* iv2.setSmooth(true);
* iv2.setCache(true);
*
* // defines a viewport into the source image (achieving a "zoom" effect) and
* // displays it rotated
* ImageView iv3 = new ImageView();
* iv3.setImage(image);
* Rectangle2D viewportRect = new Rectangle2D(40, 35, 110, 110);
* iv3.setViewport(viewportRect);
* iv3.setRotate(90);
*
* Group root = new Group();
* Scene scene = new Scene(root);
* scene.setFill(Color.BLACK);
* HBox box = new HBox();
* box.getChildren().add(iv1);
* box.getChildren().add(iv2);
* box.getChildren().add(iv3);
* root.getChildren().add(box);
*
* stage.setTitle("ImageView");
* stage.setWidth(415);
* stage.setHeight(200);
* stage.setScene(scene);
* stage.sizeToScene();
* stage.show();
* }
*
* public static void main(String[] args) {
* Application.launch(args);
* }
* }
* </code>
* </pre>
* <p>
* The code above produces the following:
* </p>
* <p>
* <img src="doc-files/imageview.png"/>
* </p>
* @since JavaFX 2.0
*/
@
DefaultProperty("image")
public class
ImageView extends
Node {
/**
* Allocates a new ImageView object.
*/
public
ImageView() {
getStyleClass().
add(
DEFAULT_STYLE_CLASS);
setAccessibleRole(
AccessibleRole.
IMAGE_VIEW);
setNodeOrientation(
NodeOrientation.
LEFT_TO_RIGHT);
}
/**
* Allocates a new ImageView object with image loaded from the specified
* URL.
* <p>
* The {@code new ImageView(url)} has the same effect as
* {@code new ImageView(new Image(url))}.
* </p>
*
* @param url the string representing the URL from which to load the image
* @throws NullPointerException if URL is null
* @throws IllegalArgumentException if URL is invalid or unsupported
* @since JavaFX 2.1
*/
public
ImageView(
String url) {
this(new
Image(
url));
}
/**
* Allocates a new ImageView object using the given image.
*
* @param image Image that this ImageView uses
*/
public
ImageView(
Image image) {
getStyleClass().
add(
DEFAULT_STYLE_CLASS);
setAccessibleRole(
AccessibleRole.
IMAGE_VIEW);
setNodeOrientation(
NodeOrientation.
LEFT_TO_RIGHT);
setImage(
image);
}
/**
* The {@link Image} to be painted by this {@code ImageView}.
*
* @defaultValue null
*/
private
ObjectProperty<
Image>
image;
public final void
setImage(
Image value) {
imageProperty().
set(
value);
}
public final
Image getImage() {
return
image == null ? null :
image.
get();
}
private
Image oldImage;
public final
ObjectProperty<
Image>
imageProperty() {
if (
image == null) {
image = new
ObjectPropertyBase<
Image>() {
private boolean
needsListeners = false;
@
Override
public void
invalidated() {
Image _image =
get();
boolean
dimensionChanged =
_image == null ||
oldImage == null ||
(
oldImage.
getWidth() !=
_image.
getWidth() ||
oldImage.
getHeight() !=
_image.
getHeight());
if (
needsListeners) {
Toolkit.
getImageAccessor().
getImageProperty(
oldImage).
removeListener(
platformImageChangeListener.
getWeakListener());
}
needsListeners =
_image != null && (
_image.
isAnimation() ||
_image.
getProgress() < 1);
oldImage =
_image;
if (
needsListeners) {
Toolkit.
getImageAccessor().
getImageProperty(
_image).
addListener(
platformImageChangeListener.
getWeakListener());
}
if (
dimensionChanged) {
invalidateWidthHeight();
impl_geomChanged();
}
impl_markDirty(
DirtyBits.
NODE_CONTENTS);
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "image";
}
};
}
return
image;
}
private
StringProperty imageUrl = null;
/**
* The imageUrl property is set from CSS and then the image property is
* set from the invalidated method. This ensures that the same image isn't
* reloaded.
*/
private
StringProperty imageUrlProperty() {
if (
imageUrl == null) {
imageUrl = new
StyleableStringProperty() {
@
Override
protected void
invalidated() {
final
String imageUrl =
get();
if (
imageUrl != null) {
setImage(
StyleManager.
getInstance().
getCachedImage(
imageUrl));
} else {
setImage(null);
}
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "imageUrl";
}
@
Override
public
CssMetaData<
ImageView,
String>
getCssMetaData() {
return
StyleableProperties.
IMAGE;
}
};
}
return
imageUrl;
}
private final
AbstractNotifyListener platformImageChangeListener =
new
AbstractNotifyListener() {
@
Override
public void
invalidated(
Observable valueModel) {
invalidateWidthHeight();
impl_markDirty(
DirtyBits.
NODE_CONTENTS);
impl_geomChanged();
}
};
/**
* The current x coordinate of the {@code ImageView} origin.
*
* @defaultValue 0
*/
private
DoubleProperty x;
public final void
setX(double
value) {
xProperty().
set(
value);
}
public final double
getX() {
return
x == null ? 0.0 :
x.
get();
}
public final
DoubleProperty xProperty() {
if (
x == null) {
x = new
DoublePropertyBase() {
@
Override
protected void
invalidated() {
impl_markDirty(
DirtyBits.
NODE_GEOMETRY);
impl_geomChanged();
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "x";
}
};
}
return
x;
}
/**
* The current y coordinate of the {@code ImageView} origin.
*
* @defaultValue 0
*/
private
DoubleProperty y;
public final void
setY(double
value) {
yProperty().
set(
value);
}
public final double
getY() {
return
y == null ? 0.0 :
y.
get();
}
public final
DoubleProperty yProperty() {
if (
y == null) {
y = new
DoublePropertyBase() {
@
Override
protected void
invalidated() {
impl_markDirty(
DirtyBits.
NODE_GEOMETRY);
impl_geomChanged();
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "y";
}
};
}
return
y;
}
/**
* The width of the bounding box within which the source image is resized as
* necessary to fit. If set to a value <= 0, then the intrinsic width of the
* image will be used as the {@code fitWidth}.
* <p/>
* See {@link #preserveRatio} for information on interaction between image
* view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio}
* attributes.
*
* @defaultValue 0
*/
private
DoubleProperty fitWidth;
public final void
setFitWidth(double
value) {
fitWidthProperty().
set(
value);
}
public final double
getFitWidth() {
return
fitWidth == null ? 0.0 :
fitWidth.
get();
}
public final
DoubleProperty fitWidthProperty() {
if (
fitWidth == null) {
fitWidth = new
DoublePropertyBase() {
@
Override
protected void
invalidated() {
invalidateWidthHeight();
impl_markDirty(
DirtyBits.
NODE_VIEWPORT);
impl_geomChanged();
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "fitWidth";
}
};
}
return
fitWidth;
}
/**
* The height of the bounding box within which the source image is resized
* as necessary to fit. If set to a value <= 0, then the intrinsic height of
* the image will be used as the {@code fitHeight}.
* <p>
* See {@link #preserveRatio} for information on interaction between image
* view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio}
* attributes.
* </p>
*
* @defaultValue 0
*/
private
DoubleProperty fitHeight;
public final void
setFitHeight(double
value) {
fitHeightProperty().
set(
value);
}
public final double
getFitHeight() {
return
fitHeight == null ? 0.0 :
fitHeight.
get();
}
public final
DoubleProperty fitHeightProperty() {
if (
fitHeight == null) {
fitHeight = new
DoublePropertyBase() {
@
Override
protected void
invalidated() {
invalidateWidthHeight();
impl_markDirty(
DirtyBits.
NODE_VIEWPORT);
impl_geomChanged();
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "fitHeight";
}
};
}
return
fitHeight;
}
/**
* Indicates whether to preserve the aspect ratio of the source image when
* scaling to fit the image within the fitting bounding box.
* <p/>
* If set to {@code true}, it affects the dimensions of this
* {@code ImageView} in the following way *
* <ul>
* <li>If only {@code fitWidth} is set, height is scaled to preserve ratio
* <li>If only {@code fitHeight} is set, width is scaled to preserve ratio
* <li>If both are set, they both may be scaled to get the best fit in a
* width by height rectangle while preserving the original aspect ratio
* </ul>
*
* If unset or set to {@code false}, it affects the dimensions of this
* {@code ImageView} in the following way *
* <ul>
* <li>If only {@code fitWidth} is set, image's view width is scaled to
* match and height is unchanged;
* <li>If only {@code fitHeight} is set, image's view height is scaled to
* match and height is unchanged;
* <li>If both are set, the image view is scaled to match both.
* </ul>
* </p>
* Note that the dimensions of this node as reported by the node's bounds
* will be equal to the size of the scaled image and is guaranteed to be
* contained within {@code fitWidth x fitHeight} bonding box.
*
* @defaultValue false
*/
private
BooleanProperty preserveRatio;
public final void
setPreserveRatio(boolean
value) {
preserveRatioProperty().
set(
value);
}
public final boolean
isPreserveRatio() {
return
preserveRatio == null ? false :
preserveRatio.
get();
}
public final
BooleanProperty preserveRatioProperty() {
if (
preserveRatio == null) {
preserveRatio = new
BooleanPropertyBase() {
@
Override
protected void
invalidated() {
invalidateWidthHeight();
impl_markDirty(
DirtyBits.
NODE_VIEWPORT);
impl_geomChanged();
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "preserveRatio";
}
};
}
return
preserveRatio;
}
/**
* Indicates whether to use a better quality filtering algorithm or a faster
* one when transforming or scaling the source image to fit within the
* bounding box provided by {@code fitWidth} and {@code fitHeight}.
*
* <p>
* If set to {@code true} a better quality filtering will be used, if set to
* {@code false} a faster but lesser quality filtering will be used.
* </p>
*
* <p>
* The default value depends on platform configuration.
* </p>
*
* @defaultValue platform-dependent
*/
private
BooleanProperty smooth;
public final void
setSmooth(boolean
value) {
smoothProperty().
set(
value);
}
public final boolean
isSmooth() {
return
smooth == null ?
SMOOTH_DEFAULT :
smooth.
get();
}
public final
BooleanProperty smoothProperty() {
if (
smooth == null) {
smooth = new
BooleanPropertyBase(
SMOOTH_DEFAULT) {
@
Override
protected void
invalidated() {
impl_markDirty(
DirtyBits.
NODE_SMOOTH);
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "smooth";
}
};
}
return
smooth;
}
/**
* Platform-dependent default value of the {@link #smoothProperty smooth} property.
*/
public static final boolean
SMOOTH_DEFAULT =
Toolkit.
getToolkit()
.
getDefaultImageSmooth();
/**
* The rectangular viewport into the image. The viewport is specified in the
* coordinates of the image, prior to scaling or any other transformations.
*
* <p>
* If {@code viewport} is {@code null}, the entire image is displayed. If
* {@code viewport} is non-{@code null}, only the portion of the image which
* falls within the viewport will be displayed. If the image does not fully
* cover the viewport then any remaining area of the viewport will be empty.
* </p>
*
* @defaultValue null
*/
private
ObjectProperty<
Rectangle2D>
viewport;
public final void
setViewport(
Rectangle2D value) {
viewportProperty().
set(
value);
}
public final
Rectangle2D getViewport() {
return
viewport == null ? null :
viewport.
get();
}
public final
ObjectProperty<
Rectangle2D>
viewportProperty() {
if (
viewport == null) {
viewport = new
ObjectPropertyBase<
Rectangle2D>() {
@
Override
protected void
invalidated() {
invalidateWidthHeight();
impl_markDirty(
DirtyBits.
NODE_VIEWPORT);
impl_geomChanged();
}
@
Override
public
Object getBean() {
return
ImageView.this;
}
@
Override
public
String getName() {
return "viewport";
}
};
}
return
viewport;
}
// Need to track changes to image width and image height and recompute
// bounds when changed.
// imageWidth = bind image.width on replace {
// impl_geomChanged();
// }
//
// imageHeight = bind image.height on replace {
// impl_geomChanged();
// }
private double
destWidth,
destHeight;
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@
Deprecated
@
Override protected
NGNode impl_createPeer() {
return new
NGImageView();
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@
Deprecated
@
Override public
BaseBounds impl_computeGeomBounds(
BaseBounds bounds,
BaseTransform tx) {
recomputeWidthHeight();
bounds =
bounds.
deriveWithNewBounds((float)
getX(), (float)
getY(), 0.0f,
(float)(
getX() +
destWidth), (float)(
getY() +
destHeight), 0.0f);
bounds =
tx.
transform(
bounds,
bounds);
return
bounds;
}
private boolean
validWH;
private void
invalidateWidthHeight() {
validWH = false;
}
private void
recomputeWidthHeight() {
if (
validWH) {
return;
}
Image localImage =
getImage();
Rectangle2D localViewport =
getViewport();
double
w = 0;
double
h = 0;
if (
localViewport != null &&
localViewport.
getWidth() > 0 &&
localViewport.
getHeight() > 0) {
w =
localViewport.
getWidth();
h =
localViewport.
getHeight();
} else if (
localImage != null) {
w =
localImage.
getWidth();
h =
localImage.
getHeight();
}
double
localFitWidth =
getFitWidth();
double
localFitHeight =
getFitHeight();
if (
isPreserveRatio() &&
w > 0 &&
h > 0 && (
localFitWidth > 0 ||
localFitHeight > 0)) {
if (
localFitWidth <= 0 || (
localFitHeight > 0 &&
localFitWidth *
h >
localFitHeight *
w)) {
w =
w *
localFitHeight /
h;
h =
localFitHeight;
} else {
h =
h *
localFitWidth /
w;
w =
localFitWidth;
}
} else {
if (
localFitWidth > 0f) {
w =
localFitWidth;
}
if (
localFitHeight > 0f) {
h =
localFitHeight;
}
}
// Store these values for use later in impl_computeContains() to support
// Node.contains().
destWidth =
w;
destHeight =
h;
validWH = true;
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@
Deprecated
@
Override protected boolean
impl_computeContains(double
localX, double
localY) {
if (
getImage() == null) {
return false;
}
recomputeWidthHeight();
// Local Note bounds contain test is already done by the caller.
// (Node.contains()).
double
dx =
localX -
getX();
double
dy =
localY -
getY();
Image localImage =
getImage();
double
srcWidth =
localImage.
getWidth();
double
srcHeight =
localImage.
getHeight();
double
viewWidth =
srcWidth;
double
viewHeight =
srcHeight;
double
vw = 0;
double
vh = 0;
double
vminx = 0;
double
vminy = 0;
Rectangle2D localViewport =
getViewport();
if (
localViewport != null) {
vw =
localViewport.
getWidth();
vh =
localViewport.
getHeight();
vminx =
localViewport.
getMinX();
vminy =
localViewport.
getMinY();
}
if (
vw > 0 &&
vh > 0) {
viewWidth =
vw;
viewHeight =
vh;
}
// desWidth Note and destHeight are computed by impl_computeGeomBounds()
// via a call from Node.contains() before calling
// impl_computeContains().
// Transform into image's coordinate system.
dx =
vminx +
dx *
viewWidth /
destWidth;
dy =
vminy +
dy *
viewHeight /
destHeight;
// test whether it's inside the original image AND inside of viewport
// (viewport may stick out from the image bounds)
if (
dx < 0.0 ||
dy < 0.0 ||
dx >=
srcWidth ||
dy >=
srcHeight ||
dx <
vminx ||
dy <
vminy ||
dx >=
vminx +
viewWidth ||
dy >=
vminy +
viewHeight) {
return false;
}
// Do alpha test on the picked pixel.
return
Toolkit.
getToolkit().
imageContains(
localImage.
impl_getPlatformImage(), (float)
dx, (float)
dy);
}
/***************************************************************************
* * Stylesheet Handling * *
**************************************************************************/
private static final
String DEFAULT_STYLE_CLASS = "image-view";
/**
* Super-lazy instantiation pattern from Bill Pugh.
* @treatAsPrivate implementation detail
*/
private static class
StyleableProperties {
// TODO
// "preserve-ratio","smooth","viewport","fit-width","fit-height"
private static final
CssMetaData<
ImageView,
String>
IMAGE =
new
CssMetaData<
ImageView,
String>("-fx-image",
URLConverter.
getInstance()) {
@
Override
public boolean
isSettable(
ImageView n) {
// Note that we care about the image, not imageUrl
return
n.
image == null || !
n.
image.
isBound();
}
@
Override
public
StyleableProperty<
String>
getStyleableProperty(
ImageView n) {
return (
StyleableProperty<
String>)
n.
imageUrlProperty();
}
};
private static final
List<
CssMetaData<? extends
Styleable, ?>>
STYLEABLES;
static {
final
List<
CssMetaData<? extends
Styleable, ?>>
styleables =
new
ArrayList<
CssMetaData<? extends
Styleable, ?>>(
Node.
getClassCssMetaData());
styleables.
add(
IMAGE);
STYLEABLES =
Collections.
unmodifiableList(
styleables);
}
}
/**
* @return The CssMetaData associated with this class, which may include the
* CssMetaData of its super classes.
* @since JavaFX 8.0
*/
public static
List<
CssMetaData<? extends
Styleable, ?>>
getClassCssMetaData() {
return
StyleableProperties.
STYLEABLES;
}
/**
* {@inheritDoc}
*
* @since JavaFX 8.0
*/
@
Override
public
List<
CssMetaData<? extends
Styleable, ?>>
getCssMetaData() {
return
getClassCssMetaData();
}
void
updateViewport() {
recomputeWidthHeight();
if (
getImage() == null ||
getImage().
impl_getPlatformImage() == null) {
return;
}
Rectangle2D localViewport =
getViewport();
final
NGImageView peer =
impl_getPeer();
if (
localViewport != null) {
peer.
setViewport((float)
localViewport.
getMinX(), (float)
localViewport.
getMinY(),
(float)
localViewport.
getWidth(), (float)
localViewport.
getHeight(),
(float)
destWidth, (float)
destHeight);
} else {
peer.
setViewport(0, 0, 0, 0, (float)
destWidth, (float)
destHeight);
}
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@
Deprecated
@
Override public void
impl_updatePeer() {
super.impl_updatePeer();
final
NGImageView peer =
impl_getPeer();
if (
impl_isDirty(
DirtyBits.
NODE_GEOMETRY)) {
peer.
setX((float)
getX());
peer.
setY((float)
getY());
}
if (
impl_isDirty(
DirtyBits.
NODE_SMOOTH)) {
peer.
setSmooth(
isSmooth());
}
if (
impl_isDirty(
DirtyBits.
NODE_CONTENTS)) {
peer.
setImage(
getImage()!= null?
getImage().
impl_getPlatformImage():null);
}
// The NG part expects this to be called when image changes
if (
impl_isDirty(
DirtyBits.
NODE_VIEWPORT) ||
impl_isDirty(
DirtyBits.
NODE_CONTENTS)) {
updateViewport();
}
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@
Deprecated
@
Override public
Object impl_processMXNode(
MXNodeAlgorithm alg,
MXNodeAlgorithmContext ctx) {
return
alg.
processLeafNode(this,
ctx);
}
}