1 /* 2 * Copyright (c) 2010, 2016, 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.shape; 27 28 import com.sun.javafx.collections.TrackableObservableList; 29 import com.sun.javafx.geom.Path2D; 30 import com.sun.javafx.scene.DirtyBits; 31 import com.sun.javafx.scene.NodeHelper; 32 import com.sun.javafx.scene.shape.PathElementHelper; 33 import com.sun.javafx.scene.shape.PathHelper; 34 import com.sun.javafx.scene.shape.PathUtils; 35 import com.sun.javafx.scene.shape.ShapeHelper; 36 import com.sun.javafx.sg.prism.NGNode; 37 import com.sun.javafx.sg.prism.NGPath; 38 import javafx.beans.property.ObjectProperty; 39 import javafx.beans.property.ObjectPropertyBase; 40 import javafx.collections.ListChangeListener.Change; 41 import javafx.collections.ObservableList; 42 import javafx.css.StyleableProperty; 43 import javafx.geometry.BoundingBox; 44 import javafx.geometry.Bounds; 45 import javafx.scene.paint.Color; 46 import javafx.scene.paint.Paint; 47 48 import java.util.Collection; 49 import java.util.List; 50 import javafx.scene.Node; 51 52 /** 53 * The {@code Path} class represents a simple shape 54 * and provides facilities required for basic construction 55 * and management of a geometric path. Example: 56 * 57 <PRE> 58 import javafx.scene.shape.*; 59 60 Path path = new Path(); 61 62 MoveTo moveTo = new MoveTo(); 63 moveTo.setX(0.0f); 64 moveTo.setY(0.0f); 65 66 HLineTo hLineTo = new HLineTo(); 67 hLineTo.setX(70.0f); 68 69 QuadCurveTo quadCurveTo = new QuadCurveTo(); 70 quadCurveTo.setX(120.0f); 71 quadCurveTo.setY(60.0f); 72 quadCurveTo.setControlX(100.0f); 73 quadCurveTo.setControlY(0.0f); 74 75 LineTo lineTo = new LineTo(); 76 lineTo.setX(175.0f); 77 lineTo.setY(55.0f); 78 79 ArcTo arcTo = new ArcTo(); 80 arcTo.setX(50.0f); 81 arcTo.setY(50.0f); 82 arcTo.setRadiusX(50.0f); 83 arcTo.setRadiusY(50.0f); 84 85 path.getElements().add(moveTo); 86 path.getElements().add(hLineTo); 87 path.getElements().add(quadCurveTo); 88 path.getElements().add(lineTo); 89 path.getElements().add(arcTo); 90 91 </PRE> 92 * @since JavaFX 2.0 93 */ 94 public class Path extends Shape { 95 static { 96 PathHelper.setPathAccessor(new PathHelper.PathAccessor() { 97 @Override 98 public NGNode doCreatePeer(Node node) { 99 return ((Path) node).doCreatePeer(); 100 } 101 102 @Override 103 public void doUpdatePeer(Node node) { 104 ((Path) node).doUpdatePeer(); 105 } 106 107 @Override 108 public Paint doCssGetFillInitialValue(Shape shape) { 109 return ((Path) shape).doCssGetFillInitialValue(); 110 } 111 112 @Override 113 public Paint doCssGetStrokeInitialValue(Shape shape) { 114 return ((Path) shape).doCssGetStrokeInitialValue(); 115 } 116 117 @Override 118 public com.sun.javafx.geom.Shape doConfigShape(Shape shape) { 119 return ((Path) shape).doConfigShape(); 120 } 121 122 }); 123 } 124 125 private Path2D path2d = null; 126 127 { 128 // To initialize the class helper at the begining each constructor of this class 129 PathHelper.initHelper(this); 130 131 // overriding default values for fill and stroke 132 // Set through CSS property so that it appears to be a UA style rather 133 // that a USER style so that fill and stroke can still be set from CSS. 134 ((StyleableProperty)fillProperty()).applyStyle(null, null); 135 ((StyleableProperty)strokeProperty()).applyStyle(null, Color.BLACK); 136 } 137 138 /** 139 * Creates an empty instance of Path. 140 */ 141 public Path() { 142 } 143 144 /** 145 * Creates a new instance of Path 146 * @param elements Elements of the Path 147 * @since JavaFX 2.1 148 */ 149 public Path(PathElement... elements) { 150 if (elements != null) { 151 this.elements.addAll(elements); 152 } 153 } 154 155 /** 156 * Creates new instance of Path 157 * @param elements The collection of the elements of the Path 158 * @since JavaFX 2.2 159 */ 160 public Path(Collection<? extends PathElement> elements) { 161 if (elements != null) { 162 this.elements.addAll(elements); 163 } 164 } 165 166 void markPathDirty() { 167 path2d = null; 168 NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS); 169 impl_geomChanged(); 170 } 171 172 /** 173 * Defines the filling rule constant for determining the interior of the path. 174 * The value must be one of the following constants: 175 * {@code FillRile.EVEN_ODD} or {@code FillRule.NON_ZERO}. 176 * The default value is {@code FillRule.NON_ZERO}. 177 * 178 * @defaultValue FillRule.NON_ZERO 179 */ 180 private ObjectProperty<FillRule> fillRule; 181 182 public final void setFillRule(FillRule value) { 183 if (fillRule != null || value != FillRule.NON_ZERO) { 184 fillRuleProperty().set(value); 185 } 186 } 187 188 public final FillRule getFillRule() { 189 return fillRule == null ? FillRule.NON_ZERO : fillRule.get(); 190 } 191 192 public final ObjectProperty<FillRule> fillRuleProperty() { 193 if (fillRule == null) { 194 fillRule = new ObjectPropertyBase<FillRule>(FillRule.NON_ZERO) { 195 196 @Override 197 public void invalidated() { 198 NodeHelper.markDirty(Path.this, DirtyBits.NODE_CONTENTS); 199 impl_geomChanged(); 200 } 201 202 @Override 203 public Object getBean() { 204 return Path.this; 205 } 206 207 @Override 208 public String getName() { 209 return "fillRule"; 210 } 211 }; 212 } 213 return fillRule; 214 } 215 216 private boolean isPathValid; 217 /** 218 * Defines the array of path elements of this path. 219 * 220 * @defaultValue empty 221 */ 222 private final ObservableList<PathElement> elements = new TrackableObservableList<PathElement>() { 223 @Override 224 protected void onChanged(Change<PathElement> c) { 225 List<PathElement> list = c.getList(); 226 boolean firstElementChanged = false; 227 while (c.next()) { 228 List<PathElement> removed = c.getRemoved(); 229 for (int i = 0; i < c.getRemovedSize(); ++i) { 230 removed.get(i).removeNode(Path.this); 231 } 232 for (int i = c.getFrom(); i < c.getTo(); ++i) { 233 list.get(i).addNode(Path.this); 234 } 235 firstElementChanged |= c.getFrom() == 0; 236 } 237 238 //Note: as ArcTo may create a various number of PathElements, 239 // we cannot count the number of PathElements removed (fast enough). 240 // Thus we can optimize only if some elements were added to the end 241 if (path2d != null) { 242 c.reset(); 243 c.next(); 244 // we just have to check the first change, as more changes cannot come after such change 245 if (c.getFrom() == c.getList().size() && !c.wasRemoved() && c.wasAdded()) { 246 // some elements added 247 for (int i = c.getFrom(); i < c.getTo(); ++i) { 248 PathElementHelper.addTo(list.get(i), path2d); 249 } 250 } else { 251 path2d = null; 252 } 253 } 254 if (firstElementChanged) { 255 isPathValid = isFirstPathElementValid(); 256 } 257 258 NodeHelper.markDirty(Path.this, DirtyBits.NODE_CONTENTS); 259 impl_geomChanged(); 260 } 261 }; 262 263 /** 264 * Gets observable list of path elements of this path. 265 * @return Elements of this path 266 */ 267 public final ObservableList<PathElement> getElements() { return elements; } 268 269 /* 270 * Note: This method MUST only be called via its accessor method. 271 */ 272 private NGNode doCreatePeer() { 273 return new NGPath(); 274 } 275 276 /* 277 * Note: This method MUST only be called via its accessor method. 278 */ 279 private Path2D doConfigShape() { 280 if (isPathValid) { 281 if (path2d == null) { 282 path2d = PathUtils.configShape(getElements(), getFillRule() == FillRule.EVEN_ODD); 283 } else { 284 path2d.setWindingRule(getFillRule() == FillRule.NON_ZERO ? 285 Path2D.WIND_NON_ZERO : Path2D.WIND_EVEN_ODD); 286 } 287 return path2d; 288 } else { 289 return new Path2D(); 290 } 291 } 292 293 /** 294 * @treatAsPrivate implementation detail 295 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 296 */ 297 @Deprecated 298 @Override 299 protected Bounds impl_computeLayoutBounds() { 300 if (isPathValid) { 301 return super.impl_computeLayoutBounds(); 302 } 303 return new BoundingBox(0, 0, -1, -1); //create empty bounds 304 } 305 306 private boolean isFirstPathElementValid() { 307 ObservableList<PathElement> _elements = getElements(); 308 if (_elements != null && _elements.size() > 0) { 309 PathElement firstElement = _elements.get(0); 310 if (!firstElement.isAbsolute()) { 311 System.err.printf("First element of the path can not be relative. Path: %s\n", this); 312 return false; 313 } else if (firstElement instanceof MoveTo) { 314 return true; 315 } else { 316 System.err.printf("Missing initial moveto in path definition. Path: %s\n", this); 317 return false; 318 } 319 } 320 return true; 321 } 322 323 /* 324 * Note: This method MUST only be called via its accessor method. 325 */ 326 private void doUpdatePeer() { 327 if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) { 328 NGPath peer = NodeHelper.getPeer(this); 329 if (peer.acceptsPath2dOnUpdate()) { 330 peer.updateWithPath2d((Path2D) ShapeHelper.configShape(this)); 331 } else { 332 peer.reset(); 333 if (isPathValid) { 334 peer.setFillRule(getFillRule()); 335 for (final PathElement elt : getElements()) { 336 elt.addTo(peer); 337 } 338 peer.update(); 339 } 340 } 341 } 342 } 343 344 /*************************************************************************** 345 * * 346 * Stylesheet Handling * 347 * * 348 **************************************************************************/ 349 350 /* 351 * Some sub-class of Shape, such as {@link Line}, override the 352 * default value for the {@link Shape#fill} property. This allows 353 * CSS to get the correct initial value. 354 * 355 * Note: This method MUST only be called via its accessor method. 356 */ 357 private Paint doCssGetFillInitialValue() { 358 return null; 359 } 360 361 /* 362 * Some sub-class of Shape, such as {@link Line}, override the 363 * default value for the {@link Shape#stroke} property. This allows 364 * CSS to get the correct initial value. 365 * 366 * Note: This method MUST only be called via its accessor method. 367 */ 368 private Paint doCssGetStrokeInitialValue() { 369 return Color.BLACK; 370 } 371 372 /** 373 * Returns a string representation of this {@code Path} object. 374 * @return a string representation of this {@code Path} object. 375 */ 376 @Override 377 public String toString() { 378 final StringBuilder sb = new StringBuilder("Path["); 379 380 String id = getId(); 381 if (id != null) { 382 sb.append("id=").append(id).append(", "); 383 } 384 385 sb.append("elements=").append(getElements()); 386 387 sb.append(", fill=").append(getFill()); 388 sb.append(", fillRule=").append(getFillRule()); 389 390 Paint stroke = getStroke(); 391 if (stroke != null) { 392 sb.append(", stroke=").append(stroke); 393 sb.append(", strokeWidth=").append(getStrokeWidth()); 394 } 395 396 return sb.append("]").toString(); 397 } 398 }