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 }