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}