Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.text; 027 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.List; 031 032import javafx.beans.property.DoubleProperty; 033import javafx.beans.property.ObjectProperty; 034import javafx.geometry.HPos; 035import javafx.geometry.Insets; 036import javafx.geometry.Orientation; 037import javafx.geometry.VPos; 038import javafx.scene.Node; 039import javafx.scene.layout.Pane; 040 041import javafx.css.StyleableDoubleProperty; 042import javafx.css.StyleableObjectProperty; 043import javafx.css.CssMetaData; 044import com.sun.javafx.css.converters.EnumConverter; 045import com.sun.javafx.css.converters.SizeConverter; 046import com.sun.javafx.geom.BaseBounds; 047import com.sun.javafx.geom.Point2D; 048import com.sun.javafx.geom.RectBounds; 049import com.sun.javafx.scene.text.GlyphList; 050import com.sun.javafx.scene.text.TextLayout; 051import com.sun.javafx.scene.text.TextLayoutFactory; 052import com.sun.javafx.scene.text.TextSpan; 053import com.sun.javafx.tk.Toolkit; 054import javafx.css.Styleable; 055import javafx.css.StyleableProperty; 056 057/** 058 * TextFlow is special layout designed to lay out rich text. 059 * It can be used to layout several {@link Text} nodes in a single text flow. 060 * The TextFlow uses the text and the font of each {@link Text} node inside of it 061 * plus it own width and text alignment to determine the location for each child. 062 * A single {@link Text} node can span over several lines due to wrapping and 063 * the visual location of {@link Text} node can differ from the logical location 064 * due to bidi reordering. 065 * 066 * <p> 067 * Any other Node, rather than Text, will be treated as embedded object in the 068 * text layout. It will be inserted in the content using its preferred width, 069 * height, and baseline offset. 070 * 071 * <p> 072 * When a {@link Text} node is inside of a TextFlow some its properties are ignored. 073 * For example, the x and y properties of the {@link Text} node are ignored since 074 * the location of the node is determined by the parent. Likewise, the wrapping 075 * width in the {@link Text} node is ignored since the width used for wrapping 076 * is the TextFlow's width. 077 * 078 * <p> 079 * The wrapping width of the layout is determined by the region's current width. 080 * It can be specified by the application by setting the textflow's preferred 081 * width. If no wrapping is desired, the application can either set the preferred 082 * with to Double.MAX_VALUE or Region.USE_COMPUTED_SIZE. 083 * 084 * <p> 085 * Paragraphs are separated by {@code '\n'} present in any Text child. 086 * 087 * <p> 088 * Example of a TextFlow: 089 * <pre><code> 090 * Text text1 = new Text("Big italic red text"); 091 * text1.setFill(Color.RED); 092 * text1.setFont(Font.font("Helvetica", FontPosture.ITALIC, 40)); 093 * Text text2 = new Text(" little bold blue text"); 094 * text2.setFill(Color.BLUE); 095 * text2.setFont(Font.font("Helvetica", FontWeight.BOLD, 10)); 096 * TextFlow textFlow = new TextFlow(text1, text2); 097 * </code></pre> 098 * 099 * <p> 100 * TextFlow lays out each managed child regardless of the child's visible property value; 101 * unmanaged children are ignored for all layout calculations.</p> 102 * 103 * <p> 104 * TextFlow may be styled with backgrounds and borders using CSS. See 105 * {@link javafx.scene.layout.Region Region} superclass for details.</p> 106 * 107 * <h4>Resizable Range</h4> 108 * 109 * A textflow's parent will resize the textflow within the textflow's range 110 * during layout. By default the textflow computes this range based on its content 111 * as outlined in the tables below. 112 * <p> 113 * <table border="1"> 114 * <tr><td></td><th>width</th><th>height</th></tr> 115 * <tr><th>minimum</th> 116 * <td>left/right insets</td> 117 * <td>top/bottom insets plus the height of the text content</td></tr> 118 * <tr><th>preferred</th> 119 * <td>left/right insets plus the width of the text content</td> 120 * <td>top/bottom insets plus the height of the text content</td></tr> 121 * <tr><th>maximum</th> 122 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr> 123 * </table> 124 * <p> 125 * A textflow's unbounded maximum width and height are an indication to the parent that 126 * it may be resized beyond its preferred size to fill whatever space is assigned to it. 127 * <p> 128 * TextFlow provides properties for setting the size range directly. These 129 * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the 130 * application may set them to other values as needed: 131 * <pre><code> 132 * <b>textflow.setMaxWidth(500);</b> 133 * </code></pre> 134 * Applications may restore the computed values by setting these properties back 135 * to Region.USE_COMPUTED_SIZE. 136 * <p> 137 * TextFlow does not clip its content by default, so it is possible that childrens' 138 * bounds may extend outside its own bounds if a child's pref size is larger than 139 * the space textflow has to allocate for it.</p> 140 * 141 * @since 8.0 142 */ 143public class TextFlow extends Pane { 144 145 private TextLayout layout; 146 private boolean needsContent; 147 private boolean inLayout; 148 149 /** 150 * Creates an empty TextFlow layout. 151 */ 152 public TextFlow() { 153 super(); 154 } 155 156 /** 157 * Creates a TextFlow layout with the given children. 158 * 159 * @param children children. 160 */ 161 public TextFlow(Node... children) { 162 this(); 163 getChildren().addAll(children); 164 } 165 166 @Override protected void setWidth(double value) { 167 if (value != getWidth()) { 168 TextLayout layout = getTextLayout(); 169 Insets insets = getInsets(); 170 double left = snapSpace(insets.getLeft()); 171 double right = snapSpace(insets.getRight()); 172 double width = Math.max(1, value - left - right); 173 layout.setWrapWidth((float)width); 174 super.setWidth(value); 175 } 176 } 177 178 @Override protected double computePrefWidth(double height) { 179 TextLayout layout = getTextLayout(); 180 layout.setWrapWidth(0); 181 double width = layout.getBounds().getWidth(); 182 Insets insets = getInsets(); 183 double left = snapSpace(insets.getLeft()); 184 double right = snapSpace(insets.getRight()); 185 double wrappingWidth = Math.max(1, getWidth() - left - right); 186 layout.setWrapWidth((float)wrappingWidth); 187 return left + width + right; 188 } 189 190 @Override protected double computePrefHeight(double width) { 191 TextLayout layout = getTextLayout(); 192 Insets insets = getInsets(); 193 double left = snapSpace(insets.getLeft()); 194 double right = snapSpace(insets.getRight()); 195 if (width == USE_COMPUTED_SIZE) { 196 layout.setWrapWidth(0); 197 } else { 198 double wrappingWidth = Math.max(1, width - left - right); 199 layout.setWrapWidth((float)wrappingWidth); 200 } 201 double height = layout.getBounds().getHeight(); 202 double wrappingWidth = Math.max(1, getWidth() - left - right); 203 layout.setWrapWidth((float)wrappingWidth); 204 double top = snapSpace(insets.getTop()); 205 double bottom = snapSpace(insets.getBottom()); 206 return top + height + bottom; 207 } 208 209 @Override protected double computeMinHeight(double width) { 210 return computePrefHeight(width); 211 } 212 213 @Override public void requestLayout() { 214 /* The geometry of text nodes can be changed during layout children. 215 * For that reason it has to call impl_geomChanged() causing 216 * requestLayout() to happen during layoutChildren(). 217 * The inLayout flag prevents this call to cause any extra work. 218 */ 219 if (inLayout) return; 220 221 /* 222 * There is no need to reset the text layout's content every time 223 * requestLayout() is called. For example, the content needs 224 * to be set when: 225 * children add or removed 226 * children managed state changes 227 * children geomChanged (width/height of embedded node) 228 * children content changes (text/font of text node) 229 * The content does not need to set when: 230 * the width/height changes in the region 231 * the insets changes in the region 232 * 233 * Unfortunately, it is not possible to know what change invoked request 234 * layout. The solution is to always reset the content in the text 235 * layout and rely on it to preserve itself if the new content equals to 236 * the old one. The cost to generate the new content is not avoid. 237 */ 238 needsContent = true; 239 super.requestLayout(); 240 } 241 242 @Override public Orientation getContentBias() { 243 return Orientation.HORIZONTAL; 244 } 245 246 @Override protected void layoutChildren() { 247 inLayout = true; 248 Insets insets = getInsets(); 249 double top = snapSpace(insets.getTop()); 250 double left = snapSpace(insets.getLeft()); 251 252 GlyphList[] runs = getTextLayout().getRuns(); 253 for (int j = 0; j < runs.length; j++) { 254 GlyphList run = runs[j]; 255 TextSpan span = run.getTextSpan(); 256 if (span instanceof EmbeddedSpan) { 257 Node child = ((EmbeddedSpan)span).getNode(); 258 Point2D location = run.getLocation(); 259 double baselineOffset = -run.getLineBounds().getMinY(); 260 261 layoutInArea(child, left + location.x, top + location.y, 262 run.getWidth(), run.getHeight(), 263 baselineOffset, null, true, true, 264 HPos.CENTER, VPos.BASELINE); 265 } 266 } 267 268 List<Node> managed = getManagedChildren(); 269 for (Node node: managed) { 270 if (node instanceof Text) { 271 Text text = (Text)node; 272 text.layoutSpan(runs); 273 BaseBounds spanBounds = text.getSpanBounds(); 274 text.relocate(left + spanBounds.getMinX(), 275 top + spanBounds.getMinY()); 276 } 277 } 278 inLayout = false; 279 } 280 281 private static class EmbeddedSpan implements TextSpan { 282 RectBounds bounds; 283 Node node; 284 public EmbeddedSpan(Node node, double baseline, double width, double height) { 285 this.node = node; 286 bounds = new RectBounds(0, (float)-baseline, 287 (float)width, (float)(height - baseline)); 288 } 289 290 @Override public String getText() { 291 return "\uFFFC"; 292 } 293 294 @Override public Object getFont() { 295 return null; 296 } 297 298 @Override public RectBounds getBounds() { 299 return bounds; 300 } 301 302 public Node getNode() { 303 return node; 304 } 305 } 306 307 TextLayout getTextLayout() { 308 if (layout == null) { 309 TextLayoutFactory factory = Toolkit.getToolkit().getTextLayoutFactory(); 310 layout = factory.createLayout(); 311 needsContent = true; 312 } 313 if (needsContent) { 314 List<Node> children = getManagedChildren(); 315 TextSpan[] spans = new TextSpan[children.size()]; 316 for (int i = 0; i < spans.length; i++) { 317 Node node = children.get(i); 318 if (node instanceof Text) { 319 spans[i] = ((Text)node).getTextSpan(); 320 } else { 321 /* Creating a text span every time forces text layout 322 * to run a full text analysis in the new content. 323 */ 324 double baseline = node.getBaselineOffset(); 325 double width = computeChildPrefAreaWidth(node, null); 326 double height = computeChildPrefAreaHeight(node, null); 327 spans[i] = new EmbeddedSpan(node, baseline, width, height); 328 } 329 } 330 layout.setContent(spans); 331 needsContent = false; 332 } 333 return layout; 334 } 335 336 /** 337 * Defines horizontal text alignment. 338 * 339 * @defaultValue TextAlignment.LEFT 340 */ 341 private ObjectProperty<TextAlignment> textAlignment; 342 343 public final void setTextAlignment(TextAlignment value) { 344 textAlignmentProperty().set(value); 345 } 346 347 public final TextAlignment getTextAlignment() { 348 return textAlignment == null ? TextAlignment.LEFT : textAlignment.get(); 349 } 350 351 public final ObjectProperty<TextAlignment> textAlignmentProperty() { 352 if (textAlignment == null) { 353 textAlignment = 354 new StyleableObjectProperty<TextAlignment>(TextAlignment.LEFT) { 355 @Override public Object getBean() { return TextFlow.this; } 356 @Override public String getName() { return "textAlignment"; } 357 @Override public CssMetaData<TextFlow, TextAlignment> getCssMetaData() { 358 return StyleableProperties.TEXT_ALIGNMENT; 359 } 360 @Override public void invalidated() { 361 TextAlignment align = get(); 362 if (align == null) align = TextAlignment.LEFT; 363 TextLayout layout = getTextLayout(); 364 layout.setAlignment(align.ordinal()); 365 requestLayout(); 366 } 367 }; 368 } 369 return textAlignment; 370 } 371 372 /** 373 * Defines the vertical space in pixel between lines. 374 * 375 * @defaultValue 0 376 * 377 * @since 8.0 378 */ 379 private DoubleProperty lineSpacing; 380 381 public final void setLineSpacing(double spacing) { 382 lineSpacingProperty().set(spacing); 383 } 384 385 public final double getLineSpacing() { 386 return lineSpacing == null ? 0 : lineSpacing.get(); 387 } 388 389 public final DoubleProperty lineSpacingProperty() { 390 if (lineSpacing == null) { 391 lineSpacing = 392 new StyleableDoubleProperty(0) { 393 @Override public Object getBean() { return TextFlow.this; } 394 @Override public String getName() { return "lineSpacing"; } 395 @Override public CssMetaData<TextFlow, Number> getCssMetaData() { 396 return StyleableProperties.LINE_SPACING; 397 } 398 @Override public void invalidated() { 399 TextLayout layout = getTextLayout(); 400 if (layout.setLineSpacing((float)get())) { 401 requestLayout(); 402 } 403 } 404 }; 405 } 406 return lineSpacing; 407 } 408 409 @Override public final double getBaselineOffset() { 410 Insets insets = getInsets(); 411 double top = snapSpace(insets.getTop()); 412 return top - getTextLayout().getBounds().getMinY(); 413 } 414 415 /*************************************************************************** 416 * * 417 * Stylesheet Handling * 418 * * 419 **************************************************************************/ 420 421 /** 422 * Super-lazy instantiation pattern from Bill Pugh. 423 * @treatAsPrivate implementation detail 424 */ 425 private static class StyleableProperties { 426 427 private static final 428 CssMetaData<TextFlow, TextAlignment> TEXT_ALIGNMENT = 429 new CssMetaData<TextFlow,TextAlignment>("-fx-text-alignment", 430 new EnumConverter<TextAlignment>(TextAlignment.class), 431 TextAlignment.LEFT) { 432 433 @Override public boolean isSettable(TextFlow node) { 434 return node.textAlignment == null || !node.textAlignment.isBound(); 435 } 436 437 @Override public StyleableProperty<TextAlignment> getStyleableProperty(TextFlow node) { 438 return (StyleableProperty<TextAlignment>)node.textAlignmentProperty(); 439 } 440 }; 441 442 private static final 443 CssMetaData<TextFlow,Number> LINE_SPACING = 444 new CssMetaData<TextFlow,Number>("-fx-line-spacing", 445 SizeConverter.getInstance(), 0) { 446 447 @Override public boolean isSettable(TextFlow node) { 448 return node.lineSpacing == null || !node.lineSpacing.isBound(); 449 } 450 451 @Override public StyleableProperty<Number> getStyleableProperty(TextFlow node) { 452 return (StyleableProperty<Number>)node.lineSpacingProperty(); 453 } 454 }; 455 456 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 457 static { 458 final List<CssMetaData<? extends Styleable, ?>> styleables = 459 new ArrayList<CssMetaData<? extends Styleable, ?>>(Pane.getClassCssMetaData()); 460 styleables.add(TEXT_ALIGNMENT); 461 styleables.add(LINE_SPACING); 462 STYLEABLES = Collections.unmodifiableList(styleables); 463 } 464 } 465 466 /** 467 * @return The CssMetaData associated with this class, which may include the 468 * CssMetaData of its super classes. 469 */ 470 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 471 return StyleableProperties.STYLEABLES; 472 } 473 474 /** 475 * {@inheritDoc} 476 * 477 */ 478 479 480 @Override 481 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 482 return getClassCssMetaData(); 483 } 484 485 /* The methods in this section are copied from Region due to package visibility restriction */ 486 private static double snapSpace(double value, boolean snapToPixel) { 487 return snapToPixel ? Math.round(value) : value; 488 } 489 490 static double boundedSize(double min, double pref, double max) { 491 double a = pref >= min ? pref : min; 492 double b = min >= max ? min : max; 493 return a <= b ? a : b; 494 } 495 496 double computeChildPrefAreaWidth(Node child, Insets margin) { 497 return computeChildPrefAreaWidth(child, margin, -1); 498 } 499 500 double computeChildPrefAreaWidth(Node child, Insets margin, double height) { 501 final boolean snap = isSnapToPixel(); 502 double top = margin != null? snapSpace(margin.getTop(), snap) : 0; 503 double bottom = margin != null? snapSpace(margin.getBottom(), snap) : 0; 504 double left = margin != null? snapSpace(margin.getLeft(), snap) : 0; 505 double right = margin != null? snapSpace(margin.getRight(), snap) : 0; 506 double alt = -1; 507 if (child.getContentBias() == Orientation.VERTICAL) { // width depends on height 508 alt = snapSize(boundedSize( 509 child.minHeight(-1), height != -1? height - top - bottom : 510 child.prefHeight(-1), child.maxHeight(-1))); 511 } 512 return left + snapSize(boundedSize(child.minWidth(alt), child.prefWidth(alt), child.maxWidth(alt))) + right; 513 } 514 515 double computeChildPrefAreaHeight(Node child, Insets margin) { 516 return computeChildPrefAreaHeight(child, margin, -1); 517 } 518 519 double computeChildPrefAreaHeight(Node child, Insets margin, double width) { 520 final boolean snap = isSnapToPixel(); 521 double top = margin != null? snapSpace(margin.getTop(), snap) : 0; 522 double bottom = margin != null? snapSpace(margin.getBottom(), snap) : 0; 523 double left = margin != null? snapSpace(margin.getLeft(), snap) : 0; 524 double right = margin != null? snapSpace(margin.getRight(), snap) : 0; 525 double alt = -1; 526 if (child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width 527 alt = snapSize(boundedSize( 528 child.minWidth(-1), width != -1? width - left - right : 529 child.prefWidth(-1), child.maxWidth(-1))); 530 } 531 return top + snapSize(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom; 532 } 533 /* end of copied code */ 534 535}