/* * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.layout; import javafx.beans.NamedArg; import javafx.css.CssMetaData; import javafx.css.Styleable; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import java.util.Arrays; import java.util.Collections; import java.util.List; import com.sun.javafx.UnmodifiableArrayList; import com.sun.javafx.css.SubCssMetaData; import javafx.css.converter.InsetsConverter; import javafx.css.converter.PaintConverter; import javafx.css.converter.URLConverter; import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter; import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter; import com.sun.javafx.scene.layout.region.CornerRadiiConverter; import com.sun.javafx.scene.layout.region.RepeatStruct; import com.sun.javafx.scene.layout.region.RepeatStructConverter; import com.sun.javafx.tk.Toolkit; /** * The Background of a {@link Region}. A Background is an immutable object which * encapsulates the entire set of data required to render the background * of a Region. Because this class is immutable, you can freely reuse the same * Background on many different Regions. Please refer to * {@link ../doc-files/cssref.html JavaFX CSS Reference} for a complete description * of the CSS rules for styling the background of a Region. *

* Every Background is comprised of {@link #getFills() fills} and / or * {@link #getImages() images}. Neither list will ever be null, but either or * both may be empty. Each defined {@link BackgroundFill} is rendered in order, * followed by each defined {@link BackgroundImage}. *

* The Background's {@link #getOutsets() outsets} define any extension of the drawing area of a Region * which is necessary to account for all background drawing. These outsets are strictly * defined by the BackgroundFills that are specified on this Background, if any, because * all BackgroundImages are clipped to the drawing area, and do not define it. The * outsets values are strictly non-negative. * * @since JavaFX 8.0 */ @SuppressWarnings("unchecked") public final class Background { static final CssMetaData BACKGROUND_COLOR = new SubCssMetaData<>("-fx-background-color", PaintConverter.SequenceConverter.getInstance(), new Paint[] {Color.TRANSPARENT}); static final CssMetaData BACKGROUND_RADIUS = new SubCssMetaData<>("-fx-background-radius", CornerRadiiConverter.getInstance(), new CornerRadii[] {CornerRadii.EMPTY}); static final CssMetaData BACKGROUND_INSETS = new SubCssMetaData<>("-fx-background-insets", InsetsConverter.SequenceConverter.getInstance(), new Insets[] {Insets.EMPTY}); static final CssMetaData BACKGROUND_IMAGE = new SubCssMetaData<>("-fx-background-image", URLConverter.SequenceConverter.getInstance()); static final CssMetaData BACKGROUND_REPEAT = new SubCssMetaData<>("-fx-background-repeat", RepeatStructConverter.getInstance(), new RepeatStruct[] {new RepeatStruct(BackgroundRepeat.REPEAT, BackgroundRepeat.REPEAT) }); static final CssMetaData BACKGROUND_POSITION = new SubCssMetaData<>("-fx-background-position", LayeredBackgroundPositionConverter.getInstance(), new BackgroundPosition[] { BackgroundPosition.DEFAULT }); static final CssMetaData BACKGROUND_SIZE = new SubCssMetaData<>("-fx-background-size", LayeredBackgroundSizeConverter.getInstance(), new BackgroundSize[] { BackgroundSize.DEFAULT } ); private static final List> STYLEABLES = (List>) (List) Collections.unmodifiableList( // Unchecked! Arrays.asList(BACKGROUND_COLOR, BACKGROUND_INSETS, BACKGROUND_RADIUS, BACKGROUND_IMAGE, BACKGROUND_REPEAT, BACKGROUND_POSITION, BACKGROUND_SIZE)); /** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its super classes. */ public static List> getClassCssMetaData() { return STYLEABLES; } /** * An empty Background, useful to use instead of null. */ public static final Background EMPTY = new Background((BackgroundFill[])null, null); /** * The list of BackgroundFills which together define the filled portion * of this Background. This List is unmodifiable and immutable. It * will never be null. The elements of this list will also never be null. */ public final List getFills() { return fills; } final List fills; /** * The list of BackgroundImages which together define the image portion * of this Background. This List is unmodifiable and immutable. It * will never be null. The elements of this list will also never be null. */ public final List getImages() { return images; } final List images; /** * The outsets of this Background. This represents the largest * bounding rectangle within which all drawing for the Background * will take place. The outsets will never be negative, and represent * the distance from the edge of the Region outward. Any BackgroundImages * which would extend beyond the outsets will be clipped. Only the * BackgroundFills contribute to the outsets. */ public final Insets getOutsets() { return outsets; } final Insets outsets; /** * Gets whether the background is empty. It is empty if there are no fills or images. * @return true if the Background is empty, false otherwise. */ public final boolean isEmpty() { return fills.isEmpty() && images.isEmpty(); } /** * Specifies whether the Background has at least one opaque fill. */ private final boolean hasOpaqueFill; /** * Package-private immutable fields referring to the opaque insets * of this Background. */ private final double opaqueFillTop, opaqueFillRight, opaqueFillBottom, opaqueFillLeft; final boolean hasPercentageBasedOpaqueFills; /** * True if there are any fills that are in some way based on the size of the region. * For example, if a CornerRadii on the fill is percentage based in either or both * dimensions. */ final boolean hasPercentageBasedFills; /** * The cached hash code computation for the Background. One very big * reason for making Background immutable was to make it possible to * cache and reuse the same Background instance for multiple * Regions (for example, every un-hovered Button should have the same * Background instance). To enable efficient caching, we cache the hash. */ private final int hash; /** * Create a new Background by supplying an array of BackgroundFills. * This array may be null, or may contain null values. Any null values * will be ignored and will not contribute to the {@link #getFills() fills} * or {@link #getOutsets() outsets}. * * @param fills The fills. This may be null, and may contain nulls. Any * contained nulls are filtered out and not included in the * final List of fills. A null array becomes an empty List. */ public Background(final @NamedArg("fills") BackgroundFill... fills) { this(fills, null); } /** * Create a new Background by supplying an array of BackgroundImages. * This array may be null, or may contain null values. Any null values will * be ignored and will not contribute to the {@link #getImages() images}. * * @param images The images. This may be null, and may contain nulls. Any * contained nulls are filtered out and not included in the * final List of images. A null array becomes an empty List. */ public Background(final @NamedArg("images") BackgroundImage... images) { this(null, images); } /** * Create a new Background supply two Lists, one for background fills and * one for background images. Either list may be null, and may contain nulls. * Any null values in these lists will be ignored and will not * contribute to the {@link #getFills() fills}, {@link #getImages() images}, or * {@link #getOutsets() outsets}. * * @param fills The fills. This may be null, and may contain nulls. Any * contained nulls are filtered out and not included in the * final List of fills. A null List becomes an empty List. * @param images The images. This may be null, and may contain nulls. Any * contained nulls are filtered out and not included in the * final List of images. A null List becomes an empty List. */ public Background(final @NamedArg("fills") List fills, final @NamedArg("images") List images) { // NOTE: This constructor had to be supplied in order to cause a Builder // to be auto-generated, because otherwise the types of the fills and images // properties didn't match the types of the array based constructor parameters. // So a Builder will use this constructor, while the CSS engine uses the // array based constructor (for speed). this(fills == null ? null : fills.toArray(new BackgroundFill[fills.size()]), images == null ? null : images.toArray(new BackgroundImage[images.size()])); } /** * Create a new Background by supplying two arrays, one for background fills, * and one for background images. Either array may be null, and may contain null * values. Any null values in these arrays will be ignored and will not * contribute to the {@link #getFills() fills}, {@link #getImages() images}, or * {@link #getOutsets() outsets}. * * @param fills The fills. This may be null, and may contain nulls. Any * contained nulls are filtered out and not included in the * final List of fills. A null array becomes an empty List. * @param images The images. This may be null, and may contain nulls. Any * contained nulls are filtered out and not included in the * final List of images. A null array becomes an empty List. */ public Background(final @NamedArg("fills") BackgroundFill[] fills, final @NamedArg("images") BackgroundImage[] images) { // The cumulative insets double outerTop = 0, outerRight = 0, outerBottom = 0, outerLeft = 0; boolean hasPercentOpaqueInsets = false; boolean hasPercentFillRadii = false; boolean opaqueFill = false; // If the fills is empty or null then we know we can just use the shared // immutable empty list from Collections. if (fills == null || fills.length == 0) { this.fills = Collections.emptyList(); } else { // We need to iterate over all of the supplied elements in the fills array. // Each null element is ignored. Each non-null element is inspected to // see if it contributes to the outsets. final BackgroundFill[] noNulls = new BackgroundFill[fills.length]; int size = 0; for (int i=0; i(noNulls, size); } hasPercentageBasedFills = hasPercentFillRadii; // This ensures that we either have outsets of 0, if all the insets were positive, // or a value greater than zero if they were negative. outsets = new Insets( Math.max(0, -outerTop), Math.max(0, -outerRight), Math.max(0, -outerBottom), Math.max(0, -outerLeft)); // An null or empty images array results in an empty list if (images == null || images.length == 0) { this.images = Collections.emptyList(); } else { // Filter out any null values and create an immutable array list final BackgroundImage[] noNulls = new BackgroundImage[images.length]; int size = 0; for (int i=0; i(noNulls, size); } hasOpaqueFill = opaqueFill; if (hasPercentOpaqueInsets) { opaqueFillTop = Double.NaN; opaqueFillRight = Double.NaN; opaqueFillBottom = Double.NaN; opaqueFillLeft = Double.NaN; } else { double[] trbl = new double[4]; computeOpaqueInsets(1, 1, true, trbl); opaqueFillTop = trbl[0]; opaqueFillRight = trbl[1]; opaqueFillBottom = trbl[2]; opaqueFillLeft = trbl[3]; } hasPercentageBasedOpaqueFills = hasPercentOpaqueInsets; // Pre-compute the hash code. NOTE: all variables are prefixed with "this" so that we // do not accidentally compute the hash based on the constructor arguments rather than // based on the fields themselves! int result = this.fills.hashCode(); result = 31 * result + this.images.hashCode(); hash = result; } /** * Gets whether the fill of this Background is based on percentages (that is, relative to the * size of the region being styled). Specifically, this returns true if any of the CornerRadii * on any of the fills on this Background has a radius that is based on percentages. * * @return True if any CornerRadii of any BackgroundFill on this background would return true, false otherwise. * @since JavaFX 8.0 */ public boolean isFillPercentageBased() { return hasPercentageBasedFills; } /** * Computes the opaque insets for a region with the specified width and height. This call * must be made whenever the width or height of the region change, because the opaque insets * are based on background fills, and the corner radii of a background fill can be percentage * based. Thus, we need to potentially recompute the opaque insets whenever the width or * height of the region change. On the other hand, if there are no percentage based corner * radii, then we can simply return the pre-computed and cached answers. * * @param width The width of the region * @param height The height of the region * @param trbl A four-element array of doubles in order: top, right, bottom, left. */ void computeOpaqueInsets(double width, double height, double[] trbl) { computeOpaqueInsets(width, height, false, trbl); } /** * Computes the opaque insets. The first time this is called from the constructor * we want to take the long route through and compute everything, whether there are * percentage based insets or not (the constructor ensures not to call it in the case * that it has percentage based insets!). All other times, this is called by the other * computeOpaqueInsets method with "firstTime" set to false, such that if we have * percentage based insets, then we will bail early. * * This method takes into account both fills and images. Because images can be * lazy loaded, we cannot pre-compute a bunch of things in the constructor for images * the way we can with fills. Instead, each time the method is called, we have to * inspect the images. However, we do have fast paths for cases where fills are used * and not images. * * @param width The width of the region * @param height The height of the region * @param firstTime Whether this is being called from the constructor * @param trbl A four-element array of doubles in order: top, right, bottom, left. */ private void computeOpaqueInsets(double width, double height, boolean firstTime, double[] trbl) { double opaqueRegionTop = Double.NaN, opaqueRegionRight = Double.NaN, opaqueRegionBottom = Double.NaN, opaqueRegionLeft = Double.NaN; // If during object construction we determined that there is an opaque fill, then we need // to visit the fills and figure out which ones contribute to the opaque insets if (hasOpaqueFill) { // If during construction time we determined that none of the fills had a percentage based // opaque inset, then we can just use the pre-computed values. This is worth doing since // at this time all CSS based radii for BackgroundFills are literal values! if (!firstTime && !hasPercentageBasedOpaqueFills) { opaqueRegionTop = opaqueFillTop; opaqueRegionRight = opaqueFillRight; opaqueRegionBottom = opaqueFillBottom; opaqueRegionLeft = opaqueFillLeft; } else { // NOTE: We know at this point that there is an opaque fill, and that at least one // of them uses a percentage for at least one corner radius. Iterate over each // BackgroundFill. If the fill is opaque, then we will compute the largest rectangle // which will fit within its opaque area, taking the corner radii into account. // Initialize them to the "I Don't Know" answer. for (int i=0, max=fills.size(); i= opaqueRegionTop; final boolean largerRight = r >= opaqueRegionRight; final boolean largerBottom = b >= opaqueRegionBottom; final boolean largerLeft = l >= opaqueRegionLeft; if (largerTop && largerRight && largerBottom && largerLeft) { // The new fill is completely contained within the existing rect, so no change continue; } else if (!largerTop && !largerRight && !largerBottom && !largerLeft) { // The new fill completely contains the existing rect, so use these // new values for our opaque region opaqueRegionTop = fillTop; opaqueRegionRight = fillRight; opaqueRegionBottom = fillBottom; opaqueRegionLeft = fillLeft; } else if (l == opaqueRegionLeft && r == opaqueRegionRight) { // The left and right insets are the same between the two rects, so just pick // the smallest top and bottom opaqueRegionTop = Math.min(t, opaqueRegionTop); opaqueRegionBottom = Math.min(b, opaqueRegionBottom); } else if (t == opaqueRegionTop && b == opaqueRegionBottom) { // The top and bottom are the same between the two rects so just pick // the smallest left and right opaqueRegionLeft = Math.min(l, opaqueRegionLeft); opaqueRegionRight = Math.min(r, opaqueRegionRight); } else { // They are disjoint or overlap in some other manner. So we will just // ignore this region. continue; } } } } } } // Check the background images. Since the image of a BackgroundImage might load asynchronously // and since we must inspect the image to check for opacity, we just have to visit all the // images each time this method is called rather than pre-computing results. With some work // we could end up caching the result eventually. final Toolkit.ImageAccessor acc = Toolkit.getImageAccessor(); for (BackgroundImage bi : images) { if (bi.opaque == null) { // If the image is not yet loaded, just skip it // Note: Unit test wants this to be com.sun.javafx.tk.PlatformImage, not com.sun.prism.Image final com.sun.javafx.tk.PlatformImage platformImage = acc.getImageProperty(bi.image).get(); if (platformImage == null) continue; // The image has been loaded, so update the opaque flag if (platformImage instanceof com.sun.prism.Image) { bi.opaque = ((com.sun.prism.Image)platformImage).isOpaque(); } else { continue; } } // At this point we know that we're processing an image which has already been resolved // and we know whether it is opaque or not. Of course, we only care about processing // opaque images. if (bi.opaque) { if (bi.size.cover || (bi.size.height == BackgroundSize.AUTO && bi.size.width == BackgroundSize.AUTO && bi.size.widthAsPercentage && bi.size.heightAsPercentage)) { // If the size mode is "cover" or AUTO, AUTO, and percentage based, then we're done -- we can simply // accumulate insets of "0" opaqueRegionTop = Double.isNaN(opaqueRegionTop) ? 0 : Math.min(0, opaqueRegionTop); opaqueRegionRight = Double.isNaN(opaqueRegionRight) ? 0 : Math.min(0, opaqueRegionRight); opaqueRegionBottom = Double.isNaN(opaqueRegionBottom) ? 0 : Math.min(0, opaqueRegionBottom); opaqueRegionLeft = Double.isNaN(opaqueRegionLeft) ? 0 : Math.min(0, opaqueRegionLeft); break; } else { // Here we are taking into account all potential tiling cases including "contain". Basically, // as long as the repeat is *not* SPACE, we know that we'll be touching every pixel, and we // don't really care how big the tiles end up being. The only case where we care about the // actual tile size is in the NO_REPEAT modes. // If the repeatX or repeatY includes "SPACE" Then we bail, because we can't be happy about // spaces strewn about within the region. if (bi.repeatX == BackgroundRepeat.SPACE || bi.repeatY == BackgroundRepeat.SPACE) { bi.opaque = false; // We'll treat it as false in the future continue; } // If the repeatX and repeatY are "REPEAT" and/or "ROUND" (any combination thereof) then // we know all pixels within the region width / height are being touched, so we can just // set the opaqueRegion variables and we're done. final boolean filledX = bi.repeatX == BackgroundRepeat.REPEAT || bi.repeatX == BackgroundRepeat.ROUND; final boolean filledY = bi.repeatY == BackgroundRepeat.REPEAT || bi.repeatY == BackgroundRepeat.ROUND; if (filledX && filledY) { opaqueRegionTop = Double.isNaN(opaqueRegionTop) ? 0 : Math.min(0, opaqueRegionTop); opaqueRegionRight = Double.isNaN(opaqueRegionRight) ? 0 : Math.min(0, opaqueRegionRight); opaqueRegionBottom = Double.isNaN(opaqueRegionBottom) ? 0 : Math.min(0, opaqueRegionBottom); opaqueRegionLeft = Double.isNaN(opaqueRegionLeft) ? 0 : Math.min(0, opaqueRegionLeft); break; } // We know that one or the other dimension is not filled, so we have to compute the right // width / height. This is basically a big copy/paste from NGRegion! Blah! final double w = bi.size.widthAsPercentage ? bi.size.width * width : bi.size.width; final double h = bi.size.heightAsPercentage ? bi.size.height * height : bi.size.height; final double imgUnscaledWidth = bi.image.getWidth(); final double imgUnscaledHeight = bi.image.getHeight(); // Now figure out the width and height of each tile to be drawn. The actual image // dimensions may be one thing, but we need to figure out what the size of the image // in the destination is going to be. final double tileWidth, tileHeight; if (bi.size.contain) { // In the case of "contain", we compute the destination size based on the largest // possible scale such that the aspect ratio is maintained, yet one side of the // region is completely filled. final double scaleX = width / imgUnscaledWidth; final double scaleY = height / imgUnscaledHeight; final double scale = Math.min(scaleX, scaleY); tileWidth = Math.ceil(scale * imgUnscaledWidth); tileHeight = Math.ceil(scale * imgUnscaledHeight); } else if (bi.size.width >= 0 && bi.size.height >= 0) { // The width and height have been expressly defined. Note that AUTO is -1, // and all other negative values are disallowed, so by checking >= 0, we // are essentially saying "if neither is AUTO" tileWidth = w; tileHeight = h; } else if (w >= 0) { // In this case, the width is specified, but the height is AUTO tileWidth = w; final double scale = tileWidth / imgUnscaledWidth; tileHeight = imgUnscaledHeight * scale; } else if (h >= 0) { // Here the height is specified and the width is AUTO tileHeight = h; final double scale = tileHeight / imgUnscaledHeight; tileWidth = imgUnscaledWidth * scale; } else { // Both are auto. tileWidth = imgUnscaledWidth; tileHeight = imgUnscaledHeight; } opaqueRegionTop = Double.isNaN(opaqueRegionTop) ? 0 : Math.min(0, opaqueRegionTop); opaqueRegionRight = Double.isNaN(opaqueRegionRight) ? (width - tileWidth) : Math.min(width - tileWidth, opaqueRegionRight); opaqueRegionBottom = Double.isNaN(opaqueRegionBottom) ? (height - tileHeight) : Math.min(height - tileHeight, opaqueRegionBottom); opaqueRegionLeft = Double.isNaN(opaqueRegionLeft) ? 0 : Math.min(0, opaqueRegionLeft); } } } trbl[0] = opaqueRegionTop; trbl[1] = opaqueRegionRight; trbl[2] = opaqueRegionBottom; trbl[3] = opaqueRegionLeft; } /** * @inheritDoc */ @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Background that = (Background) o; // Because the hash is cached, this can be a very fast check if (hash != that.hash) return false; if (!fills.equals(that.fills)) return false; if (!images.equals(that.images)) return false; return true; } /** * @inheritDoc */ @Override public int hashCode() { return hash; } }