1 /*
   2  * Copyright (c) 2012, 2013, 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.layout;
  27 
  28 import java.util.Arrays;
  29 import java.util.Collections;
  30 import java.util.List;
  31 
  32 import javafx.beans.NamedArg;
  33 import javafx.geometry.Insets;
  34 import javafx.scene.Node;
  35 import javafx.scene.paint.Paint;
  36 import com.sun.javafx.UnmodifiableArrayList;
  37 import javafx.css.CssMetaData;
  38 import com.sun.javafx.css.SubCssMetaData;
  39 import com.sun.javafx.css.converters.InsetsConverter;
  40 import com.sun.javafx.css.converters.URLConverter;
  41 import com.sun.javafx.scene.layout.region.BorderImageSlices;
  42 import com.sun.javafx.scene.layout.region.BorderImageWidthConverter;
  43 import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
  44 import com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter;
  45 import com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter;
  46 import com.sun.javafx.scene.layout.region.Margins;
  47 import com.sun.javafx.scene.layout.region.RepeatStruct;
  48 import com.sun.javafx.scene.layout.region.RepeatStructConverter;
  49 import com.sun.javafx.scene.layout.region.SliceSequenceConverter;
  50 import javafx.css.Styleable;
  51 
  52 /**
  53  * The Border of a {@link Region}. A Border is an immutable object which
  54  * encapsulates the entire set of data required to render the border
  55  * of a Region. Because this class is immutable, you can freely reuse the same
  56  * Border on many different Regions. Please refer to
  57  * {@link ../doc-files/cssref.html JavaFX CSS Reference} for a complete description
  58  * of the CSS rules for styling the border of a Region.
  59  * <p/>
  60  * Every Border is comprised of {@link #getStrokes() strokes} and / or
  61  * {@link #getImages() images}. Neither list will ever be null, but either or
  62  * both may be empty. When rendering, if no images are specified or no
  63  * image succeeds in loading, then all strokes will be rendered in order.
  64  * If any image is specified and succeeds in loading, then no strokes will
  65  * be drawn, although they will still contribute to the {@link #getInsets() insets}
  66  * and {@link #getOutsets() outsets} of the Border.
  67  * <p/>
  68  * The Border's {@link #getOutsets() outsets} define any extension of the drawing area of a Region
  69  * which is necessary to account for all border drawing and positioning. These outsets are defined
  70  * by both the {@link BorderStroke}s and {@link BorderImage}s specified on this Border.
  71  * Outsets are strictly non-negative.
  72  * <p/>
  73  * {@link #getInsets()} are used to define the inner-most edge of all of the borders. It also is
  74  * always strictly non-negative. The Region uses the insets of the {@link Background} and Border
  75  * and the {@link javafx.scene.layout.Region#getPadding() Region's padding} to determine the
  76  * Region {@link javafx.scene.layout.Region#getInsets() insets}, which define the content area
  77  * for any children of the Region. The outsets of a Border together with the outsets of a Background
  78  * and the width and height of the Region define the geometric bounds of the Region (which in
  79  * turn contribute to the {@code layoutBounds}, {@code boundsInLocal}, and {@code boundsInParent}).
  80  * <p/>
  81  * A Border is most often used in cases where you want to skin the Region with an image,
  82  * often used in conjunction with 9-patch scaling techniques. In such cases, you may
  83  * also specify a stroked border which is only used when the image fails to load for some
  84  * reason.
  85  *
  86  * @since JavaFX 8.0
  87  */
  88 @SuppressWarnings("unchecked")
  89 public final class Border {
  90     static final CssMetaData<Node,Paint[]> BORDER_COLOR =
  91             new SubCssMetaData<Paint[]>("-fx-border-color",
  92                     LayeredBorderPaintConverter.getInstance());
  93 
  94     static final CssMetaData<Node,BorderStrokeStyle[][]> BORDER_STYLE =
  95             new SubCssMetaData<BorderStrokeStyle[][]>("-fx-border-style",
  96                     LayeredBorderStyleConverter.getInstance());
  97 
  98     static final CssMetaData<Node,Margins[]> BORDER_WIDTH =
  99             new SubCssMetaData<Margins[]> ("-fx-border-width",
 100                     Margins.SequenceConverter.getInstance());
 101 
 102     static final CssMetaData<Node,CornerRadii[]> BORDER_RADIUS =
 103             new SubCssMetaData<CornerRadii[]>("-fx-border-radius",
 104                     CornerRadiiConverter.getInstance());
 105 
 106     static final CssMetaData<Node,Insets[]> BORDER_INSETS =
 107             new SubCssMetaData<Insets[]>("-fx-border-insets",
 108                     InsetsConverter.SequenceConverter.getInstance());
 109 
 110     static final CssMetaData<Node,String[]> BORDER_IMAGE_SOURCE =
 111             new SubCssMetaData<String[]>("-fx-border-image-source",
 112                     URLConverter.SequenceConverter.getInstance());
 113 
 114     static final CssMetaData<Node,RepeatStruct[]> BORDER_IMAGE_REPEAT =
 115             new SubCssMetaData<RepeatStruct[]>("-fx-border-image-repeat",
 116                     RepeatStructConverter.getInstance(),
 117                     new RepeatStruct[] { new RepeatStruct(BackgroundRepeat.REPEAT, BackgroundRepeat.REPEAT) });
 118 
 119     static final CssMetaData<Node,BorderImageSlices[]> BORDER_IMAGE_SLICE =
 120             new SubCssMetaData<BorderImageSlices[]> ("-fx-border-image-slice",
 121                     SliceSequenceConverter.getInstance(),
 122                     new BorderImageSlices[] { BorderImageSlices.DEFAULT});
 123 
 124     static final CssMetaData<Node,BorderWidths[]> BORDER_IMAGE_WIDTH =
 125             new SubCssMetaData<BorderWidths[]>("-fx-border-image-width",
 126                     BorderImageWidthConverter.getInstance(),
 127                     new BorderWidths[] { BorderWidths.DEFAULT });
 128 
 129     static final CssMetaData<Node,Insets[]> BORDER_IMAGE_INSETS =
 130             new SubCssMetaData<Insets[]>("-fx-border-image-insets",
 131                     InsetsConverter.SequenceConverter.getInstance(),
 132                     new Insets[] {Insets.EMPTY});
 133 
 134     private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES =
 135             (List<CssMetaData<? extends Styleable, ?>>) (List) Collections.unmodifiableList(
 136                     // Unchecked!
 137                     Arrays.asList(BORDER_COLOR,
 138                             BORDER_STYLE,
 139                             BORDER_WIDTH,
 140                             BORDER_RADIUS,
 141                             BORDER_INSETS,
 142                             BORDER_IMAGE_SOURCE,
 143                             BORDER_IMAGE_REPEAT,
 144                             BORDER_IMAGE_SLICE,
 145                             BORDER_IMAGE_WIDTH,
 146                             BORDER_IMAGE_INSETS));
 147 
 148     /**
 149      * @return The CssMetaData associated with this class, which may include the
 150      * CssMetaData of its super classes.
 151      */
 152     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 153         return STYLEABLES;
 154     }
 155 
 156     /**
 157      * An empty Border, useful to use instead of null.
 158      */
 159     public static final Border EMPTY = new Border((BorderStroke[])null, null);
 160 
 161     /**
 162      * The list of BorderStrokes which together define the stroked portion
 163      * of this Border. This List is unmodifiable and immutable. It
 164      * will never be null. It will never contain any null elements.
 165      */
 166     public final List<BorderStroke> getStrokes() { return strokes; }
 167     final List<BorderStroke> strokes;
 168 
 169     /**
 170      * The list of BorderImages which together define the images to use
 171      * instead of stroke for this Border. If this list is specified and
 172      * at least one image within it succeeds in loading, then any specified
 173      * {@link #getStrokes strokes} are not drawn. If this list is null or no images
 174      * succeeded in loading, then any specified {@code strokes} are drawn.
 175      * <p>
 176      * This List is unmodifiable and immutable. It will never be null.
 177      * It will never contain any null elements.
 178      */
 179     public final List<BorderImage> getImages() { return images; }
 180     final List<BorderImage> images;
 181 
 182     /**
 183      * The outsets of the border define the outer-most edge of the border to be drawn.
 184      * The values in these outsets are strictly non-negative.
 185      */
 186     public final Insets getOutsets() { return outsets; }
 187     final Insets outsets;
 188 
 189     /**
 190      * The insets define the distance from the edge of the Region to the inner-most edge
 191      * of the border, if that distance is non-negative. The values in these outsets
 192      * are strictly non-negative.
 193      */
 194     public final Insets getInsets() { return insets; }
 195     final Insets insets;
 196 
 197     /**
 198      * Gets whether the Border is empty. It is empty if there are no strokes or images.
 199      * @return true if the Border is empty, false otherwise.
 200      */
 201     public final boolean isEmpty() {
 202         return strokes.isEmpty() && images.isEmpty();
 203     }
 204 
 205     /**
 206      * The cached hash code computation for the Border. One very big
 207      * reason for making Border immutable was to make it possible to
 208      * cache and reuse the same Border instance for multiple
 209      * Regions. To enable efficient caching, we cache the hash.
 210      */
 211     private final int hash;
 212 
 213     /**
 214      * Creates a new Border by supplying an array of BorderStrokes.
 215      * This array may be null, or may contain null values. Any null values
 216      * will be ignored and will not contribute to the {@link #getStrokes() strokes}
 217      * or {@link #getOutsets() outsets} or {@link #getInsets() insets}.
 218      *
 219      * @param strokes   The strokes. This may be null, and may contain nulls. Any
 220      *                  contained nulls are filtered out and not included in the
 221      *                  final List of strokes. A null array becomes an empty List.
 222      *                  If both strokes and images are specified, and if any one
 223      *                  of the images specified succeeds in loading, then no
 224      *                  strokes are shown. In this way, strokes can be defined as
 225      *                  a fallback in the case that an image failed to load.
 226      */
 227     public Border(@NamedArg("strokes") BorderStroke... strokes) {
 228         this(strokes, null);
 229     }
 230 
 231     /**
 232      * Creates a new Border by supplying an array of BorderImages.
 233      * This array may be null, or may contain null values. Any null values
 234      * will be ignored and will not contribute to the {@link #getImages() images}
 235      * or {@link #getOutsets() outsets} or {@link #getInsets() insets}.
 236      *
 237      * @param images    The images. This may be null, and may contain nulls. Any
 238      *                  contained nulls are filtered out and not included in the
 239      *                  final List of images. A null array becomes an empty List.
 240      */
 241     public Border(@NamedArg("images") BorderImage... images) {
 242         this(null, images);
 243     }
 244 
 245     /**
 246      * Creates a new Border by supplying a List of BorderStrokes and BorderImages.
 247      * These Lists may be null, or may contain null values. Any null values
 248      * will be ignored and will not contribute to the {@link #getStrokes() strokes}
 249      * or {@link #getImages() images}, {@link #getOutsets() outsets}, or
 250      * {@link #getInsets() insets}.
 251      *
 252      * @param strokes   The strokes. This may be null, and may contain nulls. Any
 253      *                  contained nulls are filtered out and not included in the
 254      *                  final List of strokes. A null array becomes an empty List.
 255      *                  If both strokes and images are specified, and if any one
 256      *                  of the images specified succeeds in loading, then no
 257      *                  strokes are shown. In this way, strokes can be defined as
 258      *                  a fallback in the case that an image failed to load.
 259      * @param images    The images. This may be null, and may contain nulls. Any
 260      *                  contained nulls are filtered out and not included in the
 261      *                  final List of images. A null array becomes an empty List.
 262      */
 263     public Border(@NamedArg("strokes") List<BorderStroke> strokes, @NamedArg("images") List<BorderImage> images) {
 264         // NOTE: This constructor had to be supplied in order to cause a Builder
 265         // to be auto-generated, because otherwise the types of the strokes and images
 266         // properties didn't match the types of the array based constructor parameters.
 267         // So a Builder will use this constructor, while the CSS engine uses the
 268         // array based constructor (for speed).
 269         this(strokes == null ? null : strokes.toArray(new BorderStroke[strokes.size()]),
 270              images == null ? null : images.toArray(new BorderImage[images.size()]));
 271     }
 272 
 273     /**
 274      * Creates a new Border by supplying an array of BorderStrokes and BorderImages.
 275      * These arrays may be null, or may contain null values. Any null values
 276      * will be ignored and will not contribute to the {@link #getStrokes() strokes}
 277      * or {@link #getImages() images}, {@link #getOutsets() outsets}, or
 278      * {@link #getInsets() insets}.
 279      *
 280      * @param strokes   The strokes. This may be null, and may contain nulls. Any
 281      *                  contained nulls are filtered out and not included in the
 282      *                  final List of strokes. A null array becomes an empty List.
 283      *                  If both strokes and images are specified, and if any one
 284      *                  of the images specified succeeds in loading, then no
 285      *                  strokes are shown. In this way, strokes can be defined as
 286      *                  a fallback in the case that an image failed to load.
 287      * @param images    The images. This may be null, and may contain nulls. Any
 288      *                  contained nulls are filtered out and not included in the
 289      *                  final List of images. A null array becomes an empty List.
 290      */
 291     public Border(@NamedArg("strokes") BorderStroke[] strokes, @NamedArg("images") BorderImage[] images) {
 292         double innerTop = 0, innerRight = 0, innerBottom = 0, innerLeft = 0;
 293         double outerTop = 0, outerRight = 0, outerBottom = 0, outerLeft = 0;
 294 
 295         if (strokes == null || strokes.length == 0) {
 296             this.strokes = Collections.emptyList();
 297         } else {
 298             final BorderStroke[] noNulls = new BorderStroke[strokes.length];
 299             int size = 0;
 300             for (int i=0; i<strokes.length; i++) {
 301                 final BorderStroke stroke = strokes[i];
 302                 if (stroke != null) {
 303                     noNulls[size++] = stroke;
 304 
 305                     // Calculate the insets and outsets. "insets" are the distance
 306                     // from the edge of the region to the inmost edge of the inmost border.
 307                     // Outsets are the distance from the edge of the region out towards the
 308                     // outer-most edge of the outer-most border.
 309                     final double strokeInnerTop = stroke.innerEdge.getTop();
 310                     final double strokeInnerRight = stroke.innerEdge.getRight();
 311                     final double strokeInnerBottom = stroke.innerEdge.getBottom();
 312                     final double strokeInnerLeft = stroke.innerEdge.getLeft();
 313 
 314                     innerTop = innerTop >= strokeInnerTop ? innerTop : strokeInnerTop;
 315                     innerRight = innerRight >= strokeInnerRight? innerRight : strokeInnerRight;
 316                     innerBottom = innerBottom >= strokeInnerBottom ? innerBottom : strokeInnerBottom;
 317                     innerLeft = innerLeft >= strokeInnerLeft ? innerLeft : strokeInnerLeft;
 318 
 319                     final double strokeOuterTop = stroke.outerEdge.getTop();
 320                     final double strokeOuterRight = stroke.outerEdge.getRight();
 321                     final double strokeOuterBottom = stroke.outerEdge.getBottom();
 322                     final double strokeOuterLeft = stroke.outerEdge.getLeft();
 323 
 324                     outerTop = outerTop >= strokeOuterTop ? outerTop : strokeOuterTop;
 325                     outerRight = outerRight >= strokeOuterRight? outerRight : strokeOuterRight;
 326                     outerBottom = outerBottom >= strokeOuterBottom ? outerBottom : strokeOuterBottom;
 327                     outerLeft = outerLeft >= strokeOuterLeft ? outerLeft : strokeOuterLeft;
 328                 }
 329             }
 330             this.strokes = new UnmodifiableArrayList<BorderStroke>(noNulls, size);
 331         }
 332 
 333         if (images == null || images.length == 0) {
 334             this.images = Collections.emptyList();
 335         } else {
 336             final BorderImage[] noNulls = new BorderImage[images.length];
 337             int size = 0;
 338             for (int i=0; i<images.length; i++) {
 339                 final BorderImage image = images[i];
 340                 if (image != null){
 341                     noNulls[size++] = image;
 342 
 343                     // The Image width + insets may contribute to the insets / outsets of
 344                     // this border.
 345                     final double imageInnerTop = image.innerEdge.getTop();
 346                     final double imageInnerRight = image.innerEdge.getRight();
 347                     final double imageInnerBottom = image.innerEdge.getBottom();
 348                     final double imageInnerLeft = image.innerEdge.getLeft();
 349 
 350                     innerTop = innerTop >= imageInnerTop ? innerTop : imageInnerTop;
 351                     innerRight = innerRight >= imageInnerRight? innerRight : imageInnerRight;
 352                     innerBottom = innerBottom >= imageInnerBottom ? innerBottom : imageInnerBottom;
 353                     innerLeft = innerLeft >= imageInnerLeft ? innerLeft : imageInnerLeft;
 354 
 355                     final double imageOuterTop = image.outerEdge.getTop();
 356                     final double imageOuterRight = image.outerEdge.getRight();
 357                     final double imageOuterBottom = image.outerEdge.getBottom();
 358                     final double imageOuterLeft = image.outerEdge.getLeft();
 359 
 360                     outerTop = outerTop >= imageOuterTop ? outerTop : imageOuterTop;
 361                     outerRight = outerRight >= imageOuterRight? outerRight : imageOuterRight;
 362                     outerBottom = outerBottom >= imageOuterBottom ? outerBottom : imageOuterBottom;
 363                     outerLeft = outerLeft >= imageOuterLeft ? outerLeft : imageOuterLeft;
 364                 }
 365             }
 366             this.images = new UnmodifiableArrayList<BorderImage>(noNulls, size);
 367         }
 368 
 369         // Both the BorderStroke and BorderImage class make sure to return the outsets
 370         // and insets in the right way, such that we don't have to worry about adjusting
 371         // the sign, etc, unlike in the Background implementation.
 372         outsets = new Insets(outerTop, outerRight, outerBottom, outerLeft);
 373         insets = new Insets(innerTop, innerRight, innerBottom, innerLeft);
 374 
 375         // Pre-compute the hash code. NOTE: all variables are prefixed with "this" so that we
 376         // do not accidentally compute the hash based on the constructor arguments rather than
 377         // based on the fields themselves!
 378         int result = this.strokes.hashCode();
 379         result = 31 * result + this.images.hashCode();
 380         hash = result;
 381     }
 382 
 383     /**
 384      * @inheritDoc
 385      */
 386     @Override public boolean equals(Object o) {
 387         if (this == o) return true;
 388         if (o == null || getClass() != o.getClass()) return false;
 389         Border border = (Border) o;
 390         if (this.hash != border.hash) return false;
 391 
 392         if (!images.equals(border.images)) return false;
 393         if (!strokes.equals(border.strokes)) return false;
 394 
 395         return true;
 396     }
 397 
 398     /**
 399      * @inheritDoc
 400      */
 401     @Override public int hashCode() {
 402         return hash;
 403     }
 404 }