141 * region subclass.
142 * <p/>
143 * Region subclasses which layout their children will position nodes by setting
144 * {@link #setLayoutX(double) layoutX}/{@link #setLayoutY(double) layoutY} and do not alter
145 * {@link #setTranslateX(double) translateX}/{@link #setTranslateY(double) translateY}, which are reserved for
146 * adjustments and animation.
147 * @since JavaFX 2.0
148 */
149 public class Region extends Parent {
150 static {
151 RegionHelper.setRegionAccessor(new RegionHelper.RegionAccessor() {
152 @Override
153 public NGNode doCreatePeer(Node node) {
154 return ((Region) node).doCreatePeer();
155 }
156
157 @Override
158 public void doUpdatePeer(Node node) {
159 ((Region) node).doUpdatePeer();
160 }
161 });
162 }
163
164 /**
165 * Sentinel value which can be passed to a region's
166 * {@link #setMinWidth(double) setMinWidth},
167 * {@link #setMinHeight(double) setMinHeight},
168 * {@link #setMaxWidth(double) setMaxWidth} or
169 * {@link #setMaxHeight(double) setMaxHeight}
170 * methods to indicate that the preferred dimension should be used for that max and/or min constraint.
171 */
172 public static final double USE_PREF_SIZE = Double.NEGATIVE_INFINITY;
173
174 /**
175 * Sentinel value which can be passed to a region's
176 * {@link #setMinWidth(double) setMinWidth},
177 * {@link #setMinHeight(double) setMinHeight},
178 * {@link #setPrefWidth(double) setPrefWidth},
179 * {@link #setPrefHeight(double) setPrefHeight},
180 * {@link #setMaxWidth(double) setMaxWidth},
666 * The background of the Region, which is made up of zero or more BackgroundFills, and
667 * zero or more BackgroundImages. It is possible for a Background to be empty, where it
668 * has neither fills nor images, and is semantically equivalent to null.
669 * @since JavaFX 8.0
670 */
671 private final ObjectProperty<Background> background = new StyleableObjectProperty<Background>(null) {
672 private Background old = null;
673 @Override public Object getBean() { return Region.this; }
674 @Override public String getName() { return "background"; }
675 @Override public CssMetaData<Region, Background> getCssMetaData() {
676 return StyleableProperties.BACKGROUND;
677 }
678
679 @Override protected void invalidated() {
680 final Background b = get();
681 if(old != null ? !old.equals(b) : b != null) {
682 // They are different! Both cannot be null
683 if (old == null || b == null || !old.getOutsets().equals(b.getOutsets())) {
684 // We have determined that the outsets of these two different background
685 // objects is different, and therefore the bounds have changed.
686 impl_geomChanged();
687 insets.fireValueChanged();
688 }
689
690 // If the Background is made up of any BackgroundImage objects, then we must
691 // inspect the images of those BackgroundImage objects to see if they are still
692 // being loaded in the background or if they are animated. If so, then we need
693 // to attach a listener, so that when the image finishes loading or changes,
694 // we can repaint the region.
695 if (b != null) {
696 for (BackgroundImage i : b.getImages()) {
697 final Image image = i.image;
698 final Toolkit.ImageAccessor acc = Toolkit.getImageAccessor();
699 if (acc.isAnimation(image) || image.getProgress() < 1) {
700 addImageListener(image);
701 }
702 }
703 }
704
705 // And we must remove this listener from any old images
706 if (old != null) {
723 /**
724 * The border of the Region, which is made up of zero or more BorderStrokes, and
725 * zero or more BorderImages. It is possible for a Border to be empty, where it
726 * has neither strokes nor images, and is semantically equivalent to null.
727 * @since JavaFX 8.0
728 */
729 private final ObjectProperty<Border> border = new StyleableObjectProperty<Border>(null) {
730 private Border old = null;
731 @Override public Object getBean() { return Region.this; }
732 @Override public String getName() { return "border"; }
733 @Override public CssMetaData<Region, Border> getCssMetaData() {
734 return StyleableProperties.BORDER;
735 }
736 @Override protected void invalidated() {
737 final Border b = get();
738 if(old != null ? !old.equals(b) : b != null) {
739 // They are different! Both cannot be null
740 if (old == null || b == null || !old.getOutsets().equals(b.getOutsets())) {
741 // We have determined that the outsets of these two different border
742 // objects is different, and therefore the bounds have changed.
743 impl_geomChanged();
744 }
745 if (old == null || b == null || !old.getInsets().equals(b.getInsets())) {
746 insets.fireValueChanged();
747 }
748
749 // If the Border is made up of any BorderImage objects, then we must
750 // inspect the images of those BorderImage objects to see if they are still
751 // being loaded in the background or if they are animated. If so, then we need
752 // to attach a listener, so that when the image finishes loading or changes,
753 // we can repaint the region.
754 if (b != null) {
755 for (BorderImage i : b.getImages()) {
756 final Image image = i.image;
757 final Toolkit.ImageAccessor acc = Toolkit.getImageAccessor();
758 if (acc.isAnimation(image) || image.getProgress() < 1) {
759 addImageListener(image);
760 }
761 }
762 }
763
954 // access to a writable property for "width", but since there is now a protected
955 // set method, it is impossible for Region to ever bind this property.
956 protected void setWidth(double value) {
957 if(width == null) {
958 widthChanged(value);
959 } else {
960 width.set(value);
961 }
962 }
963
964 private void widthChanged(double value) {
965 // It is possible that somebody sets the width of the region to a value which
966 // it previously held. If this is the case, we want to avoid excessive layouts.
967 // Note that I have biased this for layout over binding, because the widthProperty
968 // is now going to recompute the width eagerly. The cost of excessive and
969 // unnecessary bounds changes, however, is relatively high.
970 if (value != _width) {
971 _width = value;
972 cornersValid = false;
973 boundingBox = null;
974 impl_layoutBoundsChanged();
975 impl_geomChanged();
976 NodeHelper.markDirty(this, DirtyBits.NODE_GEOMETRY);
977 setNeedsLayout(true);
978 requestParentLayout();
979 }
980 }
981
982 public final double getWidth() { return width == null ? _width : width.get(); }
983
984 public final ReadOnlyDoubleProperty widthProperty() {
985 if (width == null) {
986 width = new ReadOnlyDoubleWrapper(_width) {
987 @Override protected void invalidated() { widthChanged(get()); }
988 @Override public Object getBean() { return Region.this; }
989 @Override public String getName() { return "width"; }
990 };
991 }
992 return width.getReadOnlyProperty();
993 }
994
995 /**
1013 // access to a writable property for "height", but since there is now a protected
1014 // set method, it is impossible for Region to ever bind this property.
1015 protected void setHeight(double value) {
1016 if (height == null) {
1017 heightChanged(value);
1018 } else {
1019 height.set(value);
1020 }
1021 }
1022
1023 private void heightChanged(double value) {
1024 if (_height != value) {
1025 _height = value;
1026 cornersValid = false;
1027 // It is possible that somebody sets the height of the region to a value which
1028 // it previously held. If this is the case, we want to avoid excessive layouts.
1029 // Note that I have biased this for layout over binding, because the heightProperty
1030 // is now going to recompute the height eagerly. The cost of excessive and
1031 // unnecessary bounds changes, however, is relatively high.
1032 boundingBox = null;
1033 // Note: although impl_geomChanged will usually also invalidate the
1034 // layout bounds, that is not the case for Regions, and both must
1035 // be called separately.
1036 impl_geomChanged();
1037 impl_layoutBoundsChanged();
1038 // We use "NODE_GEOMETRY" to mean that the bounds have changed and
1039 // need to be sync'd with the render tree
1040 NodeHelper.markDirty(this, DirtyBits.NODE_GEOMETRY);
1041 // Change of the height (or width) won't change the preferred size.
1042 // So we don't need to flush the cache. We should however mark this node
1043 // as needs layout to be internally layouted.
1044 setNeedsLayout(true);
1045 // This call is only needed when this was not called from the parent during the layout.
1046 // Otherwise it would flush the cache of the parent, which is not necessary
1047 requestParentLayout();
1048 }
1049 }
1050
1051 public final double getHeight() { return height == null ? _height : height.get(); }
1052
1053 public final ReadOnlyDoubleProperty heightProperty() {
1054 if (height == null) {
1055 height = new ReadOnlyDoubleWrapper(_height) {
1056 @Override protected void invalidated() { heightChanged(get()); }
1057 @Override public Object getBean() { return Region.this; }
1332 @Override protected void invalidated() {
1333 final Shape value = get();
1334 if (_shape != value) {
1335 // The shape has changed. We need to add/remove listeners
1336 if (_shape != null) ShapeHelper.setShapeChangeListener(_shape, null);
1337 if (value != null) ShapeHelper.setShapeChangeListener(value, this);
1338 // Invalidate the bounds and such
1339 run();
1340 if (_shape == null || value == null) {
1341 // It either was null before, or is null now. In either case,
1342 // the result of the insets computation will have changed, and
1343 // we therefore need to fire that the insets value may have changed.
1344 insets.fireValueChanged();
1345 }
1346 // Update our reference to the old shape
1347 _shape = value;
1348 }
1349 }
1350
1351 @Override public void run() {
1352 impl_geomChanged();
1353 NodeHelper.markDirty(Region.this, DirtyBits.REGION_SHAPE);
1354 }
1355 };
1356
1357 /**
1358 * Specifies whether the shape, if defined, is scaled to match the size of the Region.
1359 * {@code true} means the shape is scaled to fit the size of the Region, {@code false}
1360 * means the shape is at its source size, its positioning depends on the value of
1361 * {@code centerShape}.
1362 *
1363 * @default true
1364 * @css shape-size true | false
1365 * @since JavaFX 8.0
1366 */
1367 private BooleanProperty scaleShape = null;
1368 public final void setScaleShape(boolean value) { scaleShapeProperty().set(value); }
1369 public final boolean isScaleShape() { return scaleShape == null ? true : scaleShape.get(); }
1370 public final BooleanProperty scaleShapeProperty() {
1371 if (scaleShape == null) {
1372 scaleShape = new StyleableBooleanProperty(true) {
1373 @Override public Object getBean() { return Region.this; }
1374 @Override public String getName() { return "scaleShape"; }
1375 @Override public CssMetaData<Region, Boolean> getCssMetaData() {
1376 return StyleableProperties.SCALE_SHAPE;
1377 }
1378 @Override public void invalidated() {
1379 impl_geomChanged();
1380 NodeHelper.markDirty(Region.this, DirtyBits.REGION_SHAPE);
1381 }
1382 };
1383 }
1384 return scaleShape;
1385 }
1386
1387 /**
1388 * Defines whether the shape is centered within the Region's width or height.
1389 * {@code true} means the shape centered within the Region's width and height,
1390 * {@code false} means the shape is positioned at its source position.
1391 *
1392 * @default true
1393 * @css position-shape true | false
1394 * @since JavaFX 8.0
1395 */
1396 private BooleanProperty centerShape = null;
1397 public final void setCenterShape(boolean value) { centerShapeProperty().set(value); }
1398 public final boolean isCenterShape() { return centerShape == null ? true : centerShape.get(); }
1399 public final BooleanProperty centerShapeProperty() {
1400 if (centerShape == null) {
1401 centerShape = new StyleableBooleanProperty(true) {
1402 @Override public Object getBean() { return Region.this; }
1403 @Override public String getName() { return "centerShape"; }
1404 @Override public CssMetaData<Region, Boolean> getCssMetaData() {
1405 return StyleableProperties.POSITION_SHAPE;
1406 }
1407 @Override public void invalidated() {
1408 impl_geomChanged();
1409 NodeHelper.markDirty(Region.this, DirtyBits.REGION_SHAPE);
1410 }
1411 };
1412 }
1413 return centerShape;
1414 }
1415
1416 /**
1417 * Defines a hint to the system indicating that the Shape used to define the region's
1418 * background is stable and would benefit from caching.
1419 *
1420 * @default true
1421 * @css -fx-cache-shape true | false
1422 * @since JavaFX 8.0
1423 */
1424 private BooleanProperty cacheShape = null;
1425 public final void setCacheShape(boolean value) { cacheShapeProperty().set(value); }
1426 public final boolean isCacheShape() { return cacheShape == null ? true : cacheShape.get(); }
1427 public final BooleanProperty cacheShapeProperty() {
1428 if (cacheShape == null) {
2740 // This is equivalent to:
2741 // translate(bounds.getMinX(), bounds.getMinY())
2742 // scale(scaleFactorX, scaleFactorY)
2743 // translate(-bounds.getMinX(), -bounds.getMinY())
2744 // translate(-leftOffset, -topOffset)
2745 //
2746 // which is an inversion of an transformation done to the shape
2747 // This gives us
2748 //
2749 //resX = resX * scaleFactorX - scaleFactorX * leftOffset - scaleFactorX * bounds.getMinX() + bounds.getMinX();
2750 //resY = resY * scaleFactorY - scaleFactorY * topOffset - scaleFactorY * bounds.getMinY() + bounds.getMinY();
2751 //
2752 // which can be further reduceD to
2753 resX = scaleFactorX * (resX - leftOffset - bounds.getMinX()) + bounds.getMinX();
2754 resY = scaleFactorY * (resY - topOffset - bounds.getMinY()) + bounds.getMinY();
2755
2756 }
2757 return shape.contains((float)resX, (float)resY);
2758 }
2759
2760 /**
2761 * @treatAsPrivate implementation detail
2762 * @deprecated This is an internal API that is not intended for use and will be removed in the next version
2763 */
2764 @Deprecated
2765 @Override protected boolean impl_computeContains(double localX, double localY) {
2766 // NOTE: This method only gets called if a quick check of bounds has already
2767 // occurred, so there is no need to test against bound again. We know that the
2768 // point (localX, localY) falls within the bounds of this node, now we need
2769 // to determine if it falls within the geometry of this node.
2770 // Also note that because Region defaults pickOnBounds to true, this code is
2771 // not usually executed. It will only be executed if pickOnBounds is set to false.
2772
2773 final double x2 = getWidth();
2774 final double y2 = getHeight();
2775
2776 final Background background = getBackground();
2777 // First check the shape. Shape could be impacted by scaleShape & positionShape properties.
2778 if (_shape != null) {
2779 if (background != null && !background.getFills().isEmpty()) {
2780 final List<BackgroundFill> fills = background.getFills();
2781 double topO = Double.MAX_VALUE;
2782 double leftO = Double.MAX_VALUE;
2783 double bottomO = Double.MAX_VALUE;
2784 double rightO = Double.MAX_VALUE;
2785 for (int i = 0, max = fills.size(); i < max; i++) {
3109 double scale = 1.0;
3110 if (tlhr + trhr > width) { scale = Math.min(scale, width / (tlhr + trhr)); }
3111 if (blhr + brhr > width) { scale = Math.min(scale, width / (blhr + brhr)); }
3112 if (tlvr + blvr > height) { scale = Math.min(scale, height / (tlvr + blvr)); }
3113 if (trvr + brvr > height) { scale = Math.min(scale, height / (trvr + brvr)); }
3114 if (scale < 1.0) {
3115 tlvr *= scale; tlhr *= scale;
3116 trvr *= scale; trhr *= scale;
3117 brvr *= scale; brhr *= scale;
3118 blvr *= scale; blhr *= scale;
3119 }
3120 if (radii.hasPercentBasedRadii || scale < 1.0) {
3121 return new CornerRadii(tlhr, tlvr, trvr, trhr, brhr, brvr, blvr, blhr,
3122 false, false, false, false, false, false, false, false);
3123 }
3124 return radii;
3125 }
3126
3127 /**
3128 * Some skins relying on this
3129 * @treatAsPrivate implementation detail
3130 * @deprecated This is an internal API that is not intended for use and will be removed in the next version
3131 */
3132 @Deprecated
3133 @Override protected void impl_pickNodeLocal(PickRay pickRay, PickResultChooser result) {
3134 double boundsDistance = impl_intersectsBounds(pickRay);
3135
3136 if (!Double.isNaN(boundsDistance) && ParentHelper.pickChildrenNode(this, pickRay, result)) {
3137 impl_intersects(pickRay, result);
3138 }
3139 }
3140
3141 private Bounds boundingBox;
3142
3143 /**
3144 * The layout bounds of this region: {@code 0, 0 width x height}
3145 *
3146 * @treatAsPrivate implementation detail
3147 * @deprecated This is an internal API that is not intended for use and will be removed in the next version
3148 */
3149 @Deprecated
3150 @Override protected final Bounds impl_computeLayoutBounds() {
3151 if (boundingBox == null) {
3152 // we reuse the bounding box if the width and height haven't changed.
3153 boundingBox = new BoundingBox(0, 0, 0, getWidth(), getHeight(), 0);
3154 }
3155 return boundingBox;
3156 }
3157
3158 /**
3159 * @treatAsPrivate implementation detail
3160 * @deprecated This is an internal API that is not intended for use and will be removed in the next version
3161 */
3162 @Deprecated
3163 @Override final protected void impl_notifyLayoutBoundsChanged() {
3164 // override Node's default behavior of having a geometric bounds change
3165 // trigger a change in layoutBounds. For Resizable nodes, layoutBounds
3166 // is unrelated to geometric bounds.
3167 }
3168
3169 private BaseBounds computeShapeBounds(BaseBounds bounds)
3170 {
3171 com.sun.javafx.geom.Shape s = ShapeHelper.configShape(_shape);
3172
3173 float[] bbox = {
3174 Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
3175 Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY,
3176 };
3177
3178 Background bg = getBackground();
3179 if (bg != null) {
3180 final RectBounds sBounds = s.getBounds();
3181 final Insets bgOutsets = bg.getOutsets();
3182 bbox[0] = sBounds.getMinX() - (float) bgOutsets.getLeft();
3183 bbox[1] = sBounds.getMinY() - (float) bgOutsets.getTop();
3202 double sw = Math.max(bs.getWidths().top, 0d);
3203 StrokeLineCap cap = bss.getLineCap();
3204 StrokeLineJoin join = bss.getLineJoin();
3205 float miterlimit = (float) Math.max(bss.getMiterLimit(), 1d);
3206 Toolkit.getToolkit().accumulateStrokeBounds(
3207 s,
3208 bbox, type, sw,
3209 cap, join, miterlimit, BaseTransform.IDENTITY_TRANSFORM);
3210
3211 }
3212 }
3213
3214 if (bbox[2] < bbox[0] || bbox[3] < bbox[1]) {
3215 return bounds.makeEmpty();
3216 }
3217
3218 return bounds.deriveWithNewBounds(bbox[0], bbox[1], 0.0f,
3219 bbox[2], bbox[3], 0.0f);
3220 }
3221
3222 /**
3223 * @treatAsPrivate implementation detail
3224 * @deprecated This is an internal API that is not intended for use and will be removed in the next version
3225 */
3226 @Deprecated
3227 @Override public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
3228 // Unlike Group, a Region has its own intrinsic geometric bounds, even if it has no children.
3229 // The bounds of the Region must take into account any backgrounds and borders and how
3230 // they are used to draw the Region. The geom bounds must always take into account
3231 // all pixels drawn (because the geom bounds forms the basis of the dirty regions).
3232 // Note that the layout bounds of a Region is not based on the geom bounds.
3233
3234 // Define some variables to hold the top-left and bottom-right corners of the bounds
3235 double bx1 = 0;
3236 double by1 = 0;
3237 double bx2 = getWidth();
3238 double by2 = getHeight();
3239
3240 // If the shape is defined, then the top-left and bottom-right corner positions
3241 // need to be redefined
3242 if (_shape != null && isScaleShape() == false) {
3243 // We will hijack the bounds here temporarily just to compute the shape bounds
3244 final BaseBounds shapeBounds = computeShapeBounds(bounds);
3245 final double shapeWidth = shapeBounds.getWidth();
3246 final double shapeHeight = shapeBounds.getHeight();
3247 if (isCenterShape()) {
3248 bx1 = (bx2 - shapeWidth) / 2;
3249 by1 = (by2 - shapeHeight) / 2;
3250 bx2 = bx1 + shapeWidth;
3251 by2 = by1 + shapeHeight;
3252 } else {
3253 bx1 = shapeBounds.getMinX();
3254 by1 = shapeBounds.getMinY();
3255 bx2 = shapeBounds.getMaxX();
3256 by2 = shapeBounds.getMaxY();
3257 }
3258 } else {
3259 // Expand the bounds to include the outsets from the background and border.
3260 // The outsets are the opposite of insets -- a measure of distance from the
3261 // edge of the Region outward. The outsets cannot, however, be negative.
3262 final Background background = getBackground();
3263 final Border border = getBorder();
3264 final Insets backgroundOutsets = background == null ? Insets.EMPTY : background.getOutsets();
3265 final Insets borderOutsets = border == null ? Insets.EMPTY : border.getOutsets();
3266 bx1 -= Math.max(backgroundOutsets.getLeft(), borderOutsets.getLeft());
3267 by1 -= Math.max(backgroundOutsets.getTop(), borderOutsets.getTop());
3268 bx2 += Math.max(backgroundOutsets.getRight(), borderOutsets.getRight());
3269 by2 += Math.max(backgroundOutsets.getBottom(), borderOutsets.getBottom());
3270 }
3271 // NOTE: Okay to call impl_computeGeomBounds with tx even in the 3D case
3272 // since Parent.impl_computeGeomBounds does handle 3D correctly.
3273 BaseBounds cb = super.impl_computeGeomBounds(bounds, tx);
3274 /*
3275 * This is a work around for RT-7680. Parent returns invalid bounds from
3276 * impl_computeGeomBounds when it has no children or if all its children
3277 * have invalid bounds. If RT-7680 were fixed, then we could omit this
3278 * first branch of the if and only use the else since the correct value
3279 * would be computed.
3280 */
3281 if (cb.isEmpty()) {
3282 // There are no children bounds, so
3283 bounds = bounds.deriveWithNewBounds(
3284 (float)bx1, (float)by1, 0.0f,
3285 (float)bx2, (float)by2, 0.0f);
3286 bounds = tx.transform(bounds, bounds);
3287 return bounds;
3288 } else {
3289 // Union with children's bounds
3290 BaseBounds tempBounds = TempState.getInstance().bounds;
3291 tempBounds = tempBounds.deriveWithNewBounds(
3292 (float)bx1, (float)by1, 0.0f,
3293 (float)bx2, (float)by2, 0.0f);
3294 BaseBounds bb = tx.transform(tempBounds, tempBounds);
3295 cb = cb.deriveWithUnion(bb);
3296 return cb;
|
141 * region subclass.
142 * <p/>
143 * Region subclasses which layout their children will position nodes by setting
144 * {@link #setLayoutX(double) layoutX}/{@link #setLayoutY(double) layoutY} and do not alter
145 * {@link #setTranslateX(double) translateX}/{@link #setTranslateY(double) translateY}, which are reserved for
146 * adjustments and animation.
147 * @since JavaFX 2.0
148 */
149 public class Region extends Parent {
150 static {
151 RegionHelper.setRegionAccessor(new RegionHelper.RegionAccessor() {
152 @Override
153 public NGNode doCreatePeer(Node node) {
154 return ((Region) node).doCreatePeer();
155 }
156
157 @Override
158 public void doUpdatePeer(Node node) {
159 ((Region) node).doUpdatePeer();
160 }
161
162 @Override
163 public Bounds doComputeLayoutBounds(Node node) {
164 return ((Region) node).doComputeLayoutBounds();
165 }
166
167 @Override
168 public BaseBounds doComputeGeomBounds(Node node,
169 BaseBounds bounds, BaseTransform tx) {
170 return ((Region) node).doComputeGeomBounds(bounds, tx);
171 }
172
173 @Override
174 public BaseBounds doComputeGeomBounds2(BaseBounds cb, Node node, BaseBounds bounds, BaseTransform tx) {
175 return ((Region) node).doComputeGeomBounds2(cb, bounds, tx);
176 }
177
178 @Override
179 public boolean doComputeContains(Node node, double localX, double localY) {
180 return ((Region) node).doComputeContains(localX, localY);
181 }
182
183 @Override
184 public void doNotifyLayoutBoundsChanged(Node node) {
185 ((Region) node).doNotifyLayoutBoundsChanged();
186 }
187
188 @Override
189 public void doPickNodeLocal(Node node, PickRay localPickRay,
190 PickResultChooser result) {
191 ((Region) node).doPickNodeLocal(localPickRay, result);
192 }
193 });
194 }
195
196 /**
197 * Sentinel value which can be passed to a region's
198 * {@link #setMinWidth(double) setMinWidth},
199 * {@link #setMinHeight(double) setMinHeight},
200 * {@link #setMaxWidth(double) setMaxWidth} or
201 * {@link #setMaxHeight(double) setMaxHeight}
202 * methods to indicate that the preferred dimension should be used for that max and/or min constraint.
203 */
204 public static final double USE_PREF_SIZE = Double.NEGATIVE_INFINITY;
205
206 /**
207 * Sentinel value which can be passed to a region's
208 * {@link #setMinWidth(double) setMinWidth},
209 * {@link #setMinHeight(double) setMinHeight},
210 * {@link #setPrefWidth(double) setPrefWidth},
211 * {@link #setPrefHeight(double) setPrefHeight},
212 * {@link #setMaxWidth(double) setMaxWidth},
698 * The background of the Region, which is made up of zero or more BackgroundFills, and
699 * zero or more BackgroundImages. It is possible for a Background to be empty, where it
700 * has neither fills nor images, and is semantically equivalent to null.
701 * @since JavaFX 8.0
702 */
703 private final ObjectProperty<Background> background = new StyleableObjectProperty<Background>(null) {
704 private Background old = null;
705 @Override public Object getBean() { return Region.this; }
706 @Override public String getName() { return "background"; }
707 @Override public CssMetaData<Region, Background> getCssMetaData() {
708 return StyleableProperties.BACKGROUND;
709 }
710
711 @Override protected void invalidated() {
712 final Background b = get();
713 if(old != null ? !old.equals(b) : b != null) {
714 // They are different! Both cannot be null
715 if (old == null || b == null || !old.getOutsets().equals(b.getOutsets())) {
716 // We have determined that the outsets of these two different background
717 // objects is different, and therefore the bounds have changed.
718 NodeHelper.geomChanged(Region.this);
719 insets.fireValueChanged();
720 }
721
722 // If the Background is made up of any BackgroundImage objects, then we must
723 // inspect the images of those BackgroundImage objects to see if they are still
724 // being loaded in the background or if they are animated. If so, then we need
725 // to attach a listener, so that when the image finishes loading or changes,
726 // we can repaint the region.
727 if (b != null) {
728 for (BackgroundImage i : b.getImages()) {
729 final Image image = i.image;
730 final Toolkit.ImageAccessor acc = Toolkit.getImageAccessor();
731 if (acc.isAnimation(image) || image.getProgress() < 1) {
732 addImageListener(image);
733 }
734 }
735 }
736
737 // And we must remove this listener from any old images
738 if (old != null) {
755 /**
756 * The border of the Region, which is made up of zero or more BorderStrokes, and
757 * zero or more BorderImages. It is possible for a Border to be empty, where it
758 * has neither strokes nor images, and is semantically equivalent to null.
759 * @since JavaFX 8.0
760 */
761 private final ObjectProperty<Border> border = new StyleableObjectProperty<Border>(null) {
762 private Border old = null;
763 @Override public Object getBean() { return Region.this; }
764 @Override public String getName() { return "border"; }
765 @Override public CssMetaData<Region, Border> getCssMetaData() {
766 return StyleableProperties.BORDER;
767 }
768 @Override protected void invalidated() {
769 final Border b = get();
770 if(old != null ? !old.equals(b) : b != null) {
771 // They are different! Both cannot be null
772 if (old == null || b == null || !old.getOutsets().equals(b.getOutsets())) {
773 // We have determined that the outsets of these two different border
774 // objects is different, and therefore the bounds have changed.
775 NodeHelper.geomChanged(Region.this);
776 }
777 if (old == null || b == null || !old.getInsets().equals(b.getInsets())) {
778 insets.fireValueChanged();
779 }
780
781 // If the Border is made up of any BorderImage objects, then we must
782 // inspect the images of those BorderImage objects to see if they are still
783 // being loaded in the background or if they are animated. If so, then we need
784 // to attach a listener, so that when the image finishes loading or changes,
785 // we can repaint the region.
786 if (b != null) {
787 for (BorderImage i : b.getImages()) {
788 final Image image = i.image;
789 final Toolkit.ImageAccessor acc = Toolkit.getImageAccessor();
790 if (acc.isAnimation(image) || image.getProgress() < 1) {
791 addImageListener(image);
792 }
793 }
794 }
795
986 // access to a writable property for "width", but since there is now a protected
987 // set method, it is impossible for Region to ever bind this property.
988 protected void setWidth(double value) {
989 if(width == null) {
990 widthChanged(value);
991 } else {
992 width.set(value);
993 }
994 }
995
996 private void widthChanged(double value) {
997 // It is possible that somebody sets the width of the region to a value which
998 // it previously held. If this is the case, we want to avoid excessive layouts.
999 // Note that I have biased this for layout over binding, because the widthProperty
1000 // is now going to recompute the width eagerly. The cost of excessive and
1001 // unnecessary bounds changes, however, is relatively high.
1002 if (value != _width) {
1003 _width = value;
1004 cornersValid = false;
1005 boundingBox = null;
1006 NodeHelper.layoutBoundsChanged(this);
1007 NodeHelper.geomChanged(this);
1008 NodeHelper.markDirty(this, DirtyBits.NODE_GEOMETRY);
1009 setNeedsLayout(true);
1010 requestParentLayout();
1011 }
1012 }
1013
1014 public final double getWidth() { return width == null ? _width : width.get(); }
1015
1016 public final ReadOnlyDoubleProperty widthProperty() {
1017 if (width == null) {
1018 width = new ReadOnlyDoubleWrapper(_width) {
1019 @Override protected void invalidated() { widthChanged(get()); }
1020 @Override public Object getBean() { return Region.this; }
1021 @Override public String getName() { return "width"; }
1022 };
1023 }
1024 return width.getReadOnlyProperty();
1025 }
1026
1027 /**
1045 // access to a writable property for "height", but since there is now a protected
1046 // set method, it is impossible for Region to ever bind this property.
1047 protected void setHeight(double value) {
1048 if (height == null) {
1049 heightChanged(value);
1050 } else {
1051 height.set(value);
1052 }
1053 }
1054
1055 private void heightChanged(double value) {
1056 if (_height != value) {
1057 _height = value;
1058 cornersValid = false;
1059 // It is possible that somebody sets the height of the region to a value which
1060 // it previously held. If this is the case, we want to avoid excessive layouts.
1061 // Note that I have biased this for layout over binding, because the heightProperty
1062 // is now going to recompute the height eagerly. The cost of excessive and
1063 // unnecessary bounds changes, however, is relatively high.
1064 boundingBox = null;
1065 // Note: although NodeHelper.geomChanged will usually also invalidate the
1066 // layout bounds, that is not the case for Regions, and both must
1067 // be called separately.
1068 NodeHelper.geomChanged(this);
1069 NodeHelper.layoutBoundsChanged(this);
1070 // We use "NODE_GEOMETRY" to mean that the bounds have changed and
1071 // need to be sync'd with the render tree
1072 NodeHelper.markDirty(this, DirtyBits.NODE_GEOMETRY);
1073 // Change of the height (or width) won't change the preferred size.
1074 // So we don't need to flush the cache. We should however mark this node
1075 // as needs layout to be internally layouted.
1076 setNeedsLayout(true);
1077 // This call is only needed when this was not called from the parent during the layout.
1078 // Otherwise it would flush the cache of the parent, which is not necessary
1079 requestParentLayout();
1080 }
1081 }
1082
1083 public final double getHeight() { return height == null ? _height : height.get(); }
1084
1085 public final ReadOnlyDoubleProperty heightProperty() {
1086 if (height == null) {
1087 height = new ReadOnlyDoubleWrapper(_height) {
1088 @Override protected void invalidated() { heightChanged(get()); }
1089 @Override public Object getBean() { return Region.this; }
1364 @Override protected void invalidated() {
1365 final Shape value = get();
1366 if (_shape != value) {
1367 // The shape has changed. We need to add/remove listeners
1368 if (_shape != null) ShapeHelper.setShapeChangeListener(_shape, null);
1369 if (value != null) ShapeHelper.setShapeChangeListener(value, this);
1370 // Invalidate the bounds and such
1371 run();
1372 if (_shape == null || value == null) {
1373 // It either was null before, or is null now. In either case,
1374 // the result of the insets computation will have changed, and
1375 // we therefore need to fire that the insets value may have changed.
1376 insets.fireValueChanged();
1377 }
1378 // Update our reference to the old shape
1379 _shape = value;
1380 }
1381 }
1382
1383 @Override public void run() {
1384 NodeHelper.geomChanged(Region.this);
1385 NodeHelper.markDirty(Region.this, DirtyBits.REGION_SHAPE);
1386 }
1387 };
1388
1389 /**
1390 * Specifies whether the shape, if defined, is scaled to match the size of the Region.
1391 * {@code true} means the shape is scaled to fit the size of the Region, {@code false}
1392 * means the shape is at its source size, its positioning depends on the value of
1393 * {@code centerShape}.
1394 *
1395 * @default true
1396 * @css shape-size true | false
1397 * @since JavaFX 8.0
1398 */
1399 private BooleanProperty scaleShape = null;
1400 public final void setScaleShape(boolean value) { scaleShapeProperty().set(value); }
1401 public final boolean isScaleShape() { return scaleShape == null ? true : scaleShape.get(); }
1402 public final BooleanProperty scaleShapeProperty() {
1403 if (scaleShape == null) {
1404 scaleShape = new StyleableBooleanProperty(true) {
1405 @Override public Object getBean() { return Region.this; }
1406 @Override public String getName() { return "scaleShape"; }
1407 @Override public CssMetaData<Region, Boolean> getCssMetaData() {
1408 return StyleableProperties.SCALE_SHAPE;
1409 }
1410 @Override public void invalidated() {
1411 NodeHelper.geomChanged(Region.this);
1412 NodeHelper.markDirty(Region.this, DirtyBits.REGION_SHAPE);
1413 }
1414 };
1415 }
1416 return scaleShape;
1417 }
1418
1419 /**
1420 * Defines whether the shape is centered within the Region's width or height.
1421 * {@code true} means the shape centered within the Region's width and height,
1422 * {@code false} means the shape is positioned at its source position.
1423 *
1424 * @default true
1425 * @css position-shape true | false
1426 * @since JavaFX 8.0
1427 */
1428 private BooleanProperty centerShape = null;
1429 public final void setCenterShape(boolean value) { centerShapeProperty().set(value); }
1430 public final boolean isCenterShape() { return centerShape == null ? true : centerShape.get(); }
1431 public final BooleanProperty centerShapeProperty() {
1432 if (centerShape == null) {
1433 centerShape = new StyleableBooleanProperty(true) {
1434 @Override public Object getBean() { return Region.this; }
1435 @Override public String getName() { return "centerShape"; }
1436 @Override public CssMetaData<Region, Boolean> getCssMetaData() {
1437 return StyleableProperties.POSITION_SHAPE;
1438 }
1439 @Override public void invalidated() {
1440 NodeHelper.geomChanged(Region.this);
1441 NodeHelper.markDirty(Region.this, DirtyBits.REGION_SHAPE);
1442 }
1443 };
1444 }
1445 return centerShape;
1446 }
1447
1448 /**
1449 * Defines a hint to the system indicating that the Shape used to define the region's
1450 * background is stable and would benefit from caching.
1451 *
1452 * @default true
1453 * @css -fx-cache-shape true | false
1454 * @since JavaFX 8.0
1455 */
1456 private BooleanProperty cacheShape = null;
1457 public final void setCacheShape(boolean value) { cacheShapeProperty().set(value); }
1458 public final boolean isCacheShape() { return cacheShape == null ? true : cacheShape.get(); }
1459 public final BooleanProperty cacheShapeProperty() {
1460 if (cacheShape == null) {
2772 // This is equivalent to:
2773 // translate(bounds.getMinX(), bounds.getMinY())
2774 // scale(scaleFactorX, scaleFactorY)
2775 // translate(-bounds.getMinX(), -bounds.getMinY())
2776 // translate(-leftOffset, -topOffset)
2777 //
2778 // which is an inversion of an transformation done to the shape
2779 // This gives us
2780 //
2781 //resX = resX * scaleFactorX - scaleFactorX * leftOffset - scaleFactorX * bounds.getMinX() + bounds.getMinX();
2782 //resY = resY * scaleFactorY - scaleFactorY * topOffset - scaleFactorY * bounds.getMinY() + bounds.getMinY();
2783 //
2784 // which can be further reduceD to
2785 resX = scaleFactorX * (resX - leftOffset - bounds.getMinX()) + bounds.getMinX();
2786 resY = scaleFactorY * (resY - topOffset - bounds.getMinY()) + bounds.getMinY();
2787
2788 }
2789 return shape.contains((float)resX, (float)resY);
2790 }
2791
2792 /*
2793 * Note: This method MUST only be called via its accessor method.
2794 */
2795 private boolean doComputeContains(double localX, double localY) {
2796 // NOTE: This method only gets called if a quick check of bounds has already
2797 // occurred, so there is no need to test against bound again. We know that the
2798 // point (localX, localY) falls within the bounds of this node, now we need
2799 // to determine if it falls within the geometry of this node.
2800 // Also note that because Region defaults pickOnBounds to true, this code is
2801 // not usually executed. It will only be executed if pickOnBounds is set to false.
2802
2803 final double x2 = getWidth();
2804 final double y2 = getHeight();
2805
2806 final Background background = getBackground();
2807 // First check the shape. Shape could be impacted by scaleShape & positionShape properties.
2808 if (_shape != null) {
2809 if (background != null && !background.getFills().isEmpty()) {
2810 final List<BackgroundFill> fills = background.getFills();
2811 double topO = Double.MAX_VALUE;
2812 double leftO = Double.MAX_VALUE;
2813 double bottomO = Double.MAX_VALUE;
2814 double rightO = Double.MAX_VALUE;
2815 for (int i = 0, max = fills.size(); i < max; i++) {
3139 double scale = 1.0;
3140 if (tlhr + trhr > width) { scale = Math.min(scale, width / (tlhr + trhr)); }
3141 if (blhr + brhr > width) { scale = Math.min(scale, width / (blhr + brhr)); }
3142 if (tlvr + blvr > height) { scale = Math.min(scale, height / (tlvr + blvr)); }
3143 if (trvr + brvr > height) { scale = Math.min(scale, height / (trvr + brvr)); }
3144 if (scale < 1.0) {
3145 tlvr *= scale; tlhr *= scale;
3146 trvr *= scale; trhr *= scale;
3147 brvr *= scale; brhr *= scale;
3148 blvr *= scale; blhr *= scale;
3149 }
3150 if (radii.hasPercentBasedRadii || scale < 1.0) {
3151 return new CornerRadii(tlhr, tlvr, trvr, trhr, brhr, brvr, blvr, blhr,
3152 false, false, false, false, false, false, false, false);
3153 }
3154 return radii;
3155 }
3156
3157 /**
3158 * Some skins relying on this
3159 *
3160 * Note: This method MUST only be called via its accessor method.
3161 */
3162 private void doPickNodeLocal(PickRay pickRay, PickResultChooser result) {
3163 double boundsDistance = NodeHelper.intersectsBounds(this, pickRay);
3164
3165 if (!Double.isNaN(boundsDistance) && ParentHelper.pickChildrenNode(this, pickRay, result)) {
3166 NodeHelper.intersects(this, pickRay, result);
3167 }
3168 }
3169
3170 private Bounds boundingBox;
3171
3172 /**
3173 * The layout bounds of this region: {@code 0, 0 width x height}
3174 */
3175 private final Bounds doComputeLayoutBounds() {
3176 if (boundingBox == null) {
3177 // we reuse the bounding box if the width and height haven't changed.
3178 boundingBox = new BoundingBox(0, 0, 0, getWidth(), getHeight(), 0);
3179 }
3180 return boundingBox;
3181 }
3182
3183 /*
3184 * Note: This method MUST only be called via its accessor method.
3185 */
3186 private void doNotifyLayoutBoundsChanged() {
3187 // override Node's default behavior of having a geometric bounds change
3188 // trigger a change in layoutBounds. For Resizable nodes, layoutBounds
3189 // is unrelated to geometric bounds.
3190 }
3191
3192 private BaseBounds computeShapeBounds(BaseBounds bounds)
3193 {
3194 com.sun.javafx.geom.Shape s = ShapeHelper.configShape(_shape);
3195
3196 float[] bbox = {
3197 Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
3198 Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY,
3199 };
3200
3201 Background bg = getBackground();
3202 if (bg != null) {
3203 final RectBounds sBounds = s.getBounds();
3204 final Insets bgOutsets = bg.getOutsets();
3205 bbox[0] = sBounds.getMinX() - (float) bgOutsets.getLeft();
3206 bbox[1] = sBounds.getMinY() - (float) bgOutsets.getTop();
3225 double sw = Math.max(bs.getWidths().top, 0d);
3226 StrokeLineCap cap = bss.getLineCap();
3227 StrokeLineJoin join = bss.getLineJoin();
3228 float miterlimit = (float) Math.max(bss.getMiterLimit(), 1d);
3229 Toolkit.getToolkit().accumulateStrokeBounds(
3230 s,
3231 bbox, type, sw,
3232 cap, join, miterlimit, BaseTransform.IDENTITY_TRANSFORM);
3233
3234 }
3235 }
3236
3237 if (bbox[2] < bbox[0] || bbox[3] < bbox[1]) {
3238 return bounds.makeEmpty();
3239 }
3240
3241 return bounds.deriveWithNewBounds(bbox[0], bbox[1], 0.0f,
3242 bbox[2], bbox[3], 0.0f);
3243 }
3244
3245 // Define some variables to hold the top-left and bottom-right corners of the bounds
3246 private double bx1, by1, bx2, by2;
3247
3248 /*
3249 * Note: This method MUST only be called via its accessor method.
3250 */
3251 private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
3252 // Unlike Group, a Region has its own intrinsic geometric bounds, even if it has no children.
3253 // The bounds of the Region must take into account any backgrounds and borders and how
3254 // they are used to draw the Region. The geom bounds must always take into account
3255 // all pixels drawn (because the geom bounds forms the basis of the dirty regions).
3256 // Note that the layout bounds of a Region is not based on the geom bounds.
3257
3258 // Define some variables to hold the top-left and bottom-right corners of the bounds
3259 bx1 = 0;
3260 by1 = 0;
3261 bx2 = getWidth();
3262 by2 = getHeight();
3263
3264 // If the shape is defined, then the top-left and bottom-right corner positions
3265 // need to be redefined
3266 if (_shape != null && isScaleShape() == false) {
3267 // We will hijack the bounds here temporarily just to compute the shape bounds
3268 final BaseBounds shapeBounds = computeShapeBounds(bounds);
3269 final double shapeWidth = shapeBounds.getWidth();
3270 final double shapeHeight = shapeBounds.getHeight();
3271 if (isCenterShape()) {
3272 bx1 = (bx2 - shapeWidth) / 2;
3273 by1 = (by2 - shapeHeight) / 2;
3274 bx2 = bx1 + shapeWidth;
3275 by2 = by1 + shapeHeight;
3276 } else {
3277 bx1 = shapeBounds.getMinX();
3278 by1 = shapeBounds.getMinY();
3279 bx2 = shapeBounds.getMaxX();
3280 by2 = shapeBounds.getMaxY();
3281 }
3282 } else {
3283 // Expand the bounds to include the outsets from the background and border.
3284 // The outsets are the opposite of insets -- a measure of distance from the
3285 // edge of the Region outward. The outsets cannot, however, be negative.
3286 final Background background = getBackground();
3287 final Border border = getBorder();
3288 final Insets backgroundOutsets = background == null ? Insets.EMPTY : background.getOutsets();
3289 final Insets borderOutsets = border == null ? Insets.EMPTY : border.getOutsets();
3290 bx1 -= Math.max(backgroundOutsets.getLeft(), borderOutsets.getLeft());
3291 by1 -= Math.max(backgroundOutsets.getTop(), borderOutsets.getTop());
3292 bx2 += Math.max(backgroundOutsets.getRight(), borderOutsets.getRight());
3293 by2 += Math.max(backgroundOutsets.getBottom(), borderOutsets.getBottom());
3294 }
3295
3296 return null;
3297 }
3298
3299 private BaseBounds doComputeGeomBounds2(BaseBounds cb, BaseBounds bounds,
3300 BaseTransform tx) {
3301 /*
3302 * This is a work around for RT-7680. Parent returns invalid bounds from
3303 * computeGeomBoundsImpl when it has no children or if all its children
3304 * have invalid bounds. If RT-7680 were fixed, then we could omit this
3305 * first branch of the if and only use the else since the correct value
3306 * would be computed.
3307 */
3308 if (cb.isEmpty()) {
3309 // There are no children bounds, so
3310 bounds = bounds.deriveWithNewBounds(
3311 (float)bx1, (float)by1, 0.0f,
3312 (float)bx2, (float)by2, 0.0f);
3313 bounds = tx.transform(bounds, bounds);
3314 return bounds;
3315 } else {
3316 // Union with children's bounds
3317 BaseBounds tempBounds = TempState.getInstance().bounds;
3318 tempBounds = tempBounds.deriveWithNewBounds(
3319 (float)bx1, (float)by1, 0.0f,
3320 (float)bx2, (float)by2, 0.0f);
3321 BaseBounds bb = tx.transform(tempBounds, tempBounds);
3322 cb = cb.deriveWithUnion(bb);
3323 return cb;
|