1 /*
   2  * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.layout;
  27 
  28 import java.util.ArrayList;
  29 import java.util.Collections;
  30 import java.util.List;
  31 import javafx.beans.property.DoubleProperty;
  32 import javafx.beans.property.DoublePropertyBase;
  33 import javafx.beans.property.ObjectProperty;
  34 import javafx.css.CssMetaData;
  35 import javafx.css.StyleableDoubleProperty;
  36 import javafx.css.StyleableObjectProperty;
  37 import javafx.css.StyleableProperty;
  38 import javafx.geometry.HPos;
  39 import javafx.geometry.Insets;
  40 import javafx.geometry.Orientation;
  41 import javafx.geometry.Pos;
  42 import javafx.geometry.VPos;
  43 import javafx.scene.Node;
  44 import javafx.css.converter.EnumConverter;
  45 import javafx.css.converter.SizeConverter;
  46 import javafx.css.Styleable;
  47 
  48 import static javafx.geometry.Orientation.*;
  49 import javafx.util.Callback;
  50 
  51 /**
  52  * FlowPane lays out its children in a flow that wraps at the flowpane's boundary.
  53  * <p>
  54  * A horizontal flowpane (the default) will layout nodes in rows, wrapping at the
  55  * flowpane's width.  A vertical flowpane lays out nodes in columns,
  56  * wrapping at the flowpane's height.  If the flowpane has a border and/or padding set,
  57  * the content will be flowed within those insets.
  58  * <p>
  59  * FlowPane's prefWrapLength property establishes it's preferred width
  60  * (for horizontal) or preferred height (for vertical). Applications should set
  61  * prefWrapLength if the default value (400) doesn't suffice.  Note that prefWrapLength
  62  * is used only for calculating the preferred size and may not reflect the actual
  63  * wrapping dimension, which tracks the actual size of the flowpane.
  64  * <p>
  65  * The alignment property controls how the rows and columns are aligned
  66  * within the bounds of the flowpane and defaults to Pos.TOP_LEFT.  It is also possible
  67  * to control the alignment of nodes within the rows and columns by setting
  68  * rowValignment for horizontal or columnHalignment for vertical.
  69  * <p>
  70  * Example of a horizontal flowpane:
  71  * <pre>{@code
  72  *     Image images[] = { ... };
  73  *     FlowPane flow = new FlowPane();
  74  *     flow.setVgap(8);
  75  *     flow.setHgap(4);
  76  *     flow.setPrefWrapLength(300); // preferred width = 300
  77  *     for (int i = 0; i < images.length; i++) {
  78  *         flow.getChildren().add(new ImageView(image[i]);
  79  *     }
  80  * }</pre>
  81  *
  82  *<p>
  83  * Example of a vertical flowpane:
  84  * <pre>{@code
  85  *     FlowPane flow = new FlowPane(Orientation.VERTICAL);
  86  *     flow.setColumnHalignment(HPos.LEFT); // align labels on left
  87  *     flow.setPrefWrapLength(200); // preferred height = 200
  88  *     for (int i = 0; i < titles.size(); i++) {
  89  *         flow.getChildren().add(new Label(titles[i]);
  90  *     }
  91  * }</pre>
  92  *
  93  * <p>
  94  * FlowPane lays out each managed child regardless of the child's visible property value;
  95  * unmanaged children are ignored for all layout calculations.</p>
  96  *
  97  * <p>
  98  * FlowPane may be styled with backgrounds and borders using CSS.  See
  99  * {@link javafx.scene.layout.Region Region} superclass for details.</p>
 100  *
 101  * <h3>Resizable Range</h3>
 102  *
 103  * <p>
 104  * A flowpane's parent will resize the flowpane within the flowpane's resizable range
 105  * during layout.   By default the flowpane computes this range based on its content
 106  * as outlined in the tables below.
 107  * </p>
 108  * <table border="1">
 109  * <caption>Horizontal</caption>
 110  * <tr><td></td><th scope="col">width</th><th scope="col">height</th></tr>
 111  * <tr><th scope="row">minimum</th>
 112  * <td>left/right insets plus largest of children's pref widths</td>
 113  * <td>top/bottom insets plus height required to display all children at their preferred heights when wrapped at a specified width</td></tr>
 114  * <tr><th scope="row">preferred</th>
 115  * <td>left/right insets plus prefWrapLength</td>
 116  * <td>top/bottom insets plus height required to display all children at their pref heights when wrapped at a specified width</td></tr>
 117  * <tr><th scope="row">maximum</th>
 118  * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr>
 119  * </table>
 120  * <br>
 121  * <table border="1">
 122  * <caption>Vertical</caption>
 123  * <tr><td></td><th scope="col">width</th><th scope="col">height</th></tr>
 124  * <tr><th scope="row">minimum</th>
 125  * <td>left/right insets plus width required to display all children at their preferred widths when wrapped at a specified height</td>
 126  * <td>top/bottom insets plus largest of children's pref heights</td></tr>
 127  * <tr><th scope="row">preferred</th>
 128  * <td>left/right insets plus width required to display all children at their pref widths when wrapped at the specified height</td>
 129  * <td>top/bottom insets plus prefWrapLength</td></tr>
 130  * <tr><th scope="row">maximum</th>
 131  * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr>
 132  * </table>
 133  * <p>
 134  * A flowpane's unbounded maximum width and height are an indication to the parent that
 135  * it may be resized beyond its preferred size to fill whatever space is assigned to it.
 136  * <p>
 137  * FlowPane provides properties for setting the size range directly.  These
 138  * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the
 139  * application may set them to other values as needed:
 140  * <pre>{@code
 141  *     <b>flowpane.setMaxWidth(500);</b>
 142  * }</pre>
 143  * Applications may restore the computed values by setting these properties back
 144  * to Region.USE_COMPUTED_SIZE.
 145  * <p>
 146  * FlowPane does not clip its content by default, so it is possible that childrens'
 147  * bounds may extend outside its own bounds if a child's pref size is larger than
 148  * the space flowpane has to allocate for it.</p>
 149  *
 150  * @since JavaFX 2.0
 151  */
 152 public class FlowPane extends Pane {
 153 
 154     /********************************************************************
 155      *  BEGIN static methods
 156      ********************************************************************/
 157     private static final String MARGIN_CONSTRAINT = "flowpane-margin";
 158 
 159     /**
 160      * Sets the margin for the child when contained by a flowpane.
 161      * If set, the flowpane will layout it out with the margin space around it.
 162      * Setting the value to null will remove the constraint.
 163      * @param child the child node of a flowpane
 164      * @param value the margin of space around the child
 165      */
 166     public static void setMargin(Node child, Insets value) {
 167         setConstraint(child, MARGIN_CONSTRAINT, value);
 168     }
 169 
 170     /**
 171      * Returns the child's margin constraint if set.
 172      * @param child the child node of a flowpane
 173      * @return the margin for the child or null if no margin was set
 174      */
 175     public static Insets getMargin(Node child) {
 176         return (Insets)getConstraint(child, MARGIN_CONSTRAINT);
 177     }
 178 
 179     private static final Callback<Node, Insets> marginAccessor = n -> getMargin(n);
 180 
 181     /**
 182      * Removes all flowpane constraints from the child node.
 183      * @param child the child node
 184      */
 185     public static void clearConstraints(Node child) {
 186         setMargin(child, null);
 187     }
 188 
 189     /********************************************************************
 190      *  END static methods
 191      ********************************************************************/
 192 
 193     /**
 194      * Creates a horizontal FlowPane layout with hgap/vgap = 0.
 195      */
 196     public FlowPane() {
 197         super();
 198     }
 199 
 200     /**
 201      * Creates a FlowPane layout with the specified orientation and hgap/vgap = 0.
 202      * @param orientation the direction the tiles should flow &amp; wrap
 203      */
 204     public FlowPane(Orientation orientation) {
 205         this();
 206         setOrientation(orientation);
 207     }
 208 
 209     /**
 210      * Creates a horizontal FlowPane layout with the specified hgap/vgap.
 211      * @param hgap the amount of horizontal space between each tile
 212      * @param vgap the amount of vertical space between each tile
 213      */
 214     public FlowPane(double hgap, double vgap) {
 215         this();
 216         setHgap(hgap);
 217         setVgap(vgap);
 218     }
 219 
 220     /**
 221      * Creates a FlowPane layout with the specified orientation and hgap/vgap.
 222      * @param orientation the direction the tiles should flow &amp; wrap
 223      * @param hgap the amount of horizontal space between each tile
 224      * @param vgap the amount of vertical space between each tile
 225      */
 226     public FlowPane(Orientation orientation, double hgap, double vgap) {
 227         this();
 228         setOrientation(orientation);
 229         setHgap(hgap);
 230         setVgap(vgap);
 231     }
 232 
 233     /**
 234      * Creates a horizontal FlowPane layout with hgap/vgap = 0.
 235      * @param children The initial set of children for this pane.
 236      * @since JavaFX 8.0
 237      */
 238     public FlowPane(Node... children) {
 239         super();
 240         getChildren().addAll(children);
 241     }
 242 
 243     /**
 244      * Creates a FlowPane layout with the specified orientation and hgap/vgap = 0.
 245      * @param orientation the direction the tiles should flow &amp; wrap
 246      * @param children The initial set of children for this pane.
 247      * @since JavaFX 8.0
 248      */
 249     public FlowPane(Orientation orientation, Node... children) {
 250         this();
 251         setOrientation(orientation);
 252         getChildren().addAll(children);
 253     }
 254 
 255     /**
 256      * Creates a horizontal FlowPane layout with the specified hgap/vgap.
 257      * @param hgap the amount of horizontal space between each tile
 258      * @param vgap the amount of vertical space between each tile
 259      * @param children The initial set of children for this pane.
 260      * @since JavaFX 8.0
 261      */
 262     public FlowPane(double hgap, double vgap, Node... children) {
 263         this();
 264         setHgap(hgap);
 265         setVgap(vgap);
 266         getChildren().addAll(children);
 267     }
 268 
 269     /**
 270      * Creates a FlowPane layout with the specified orientation and hgap/vgap.
 271      * @param orientation the direction the tiles should flow &amp; wrap
 272      * @param hgap the amount of horizontal space between each tile
 273      * @param vgap the amount of vertical space between each tile
 274      * @param children The initial set of children for this pane.
 275      * @since JavaFX 8.0
 276      */
 277     public FlowPane(Orientation orientation, double hgap, double vgap, Node... children) {
 278         this();
 279         setOrientation(orientation);
 280         setHgap(hgap);
 281         setVgap(vgap);
 282         getChildren().addAll(children);
 283     }
 284 
 285     /**
 286      * The orientation of this flowpane.
 287      * A horizontal flowpane lays out children left to right, wrapping at the
 288      * flowpane's width boundary.   A vertical flowpane lays out children top to
 289      * bottom, wrapping at the flowpane's height.
 290      * The default is horizontal.
 291      * @return the orientation of this flowpane
 292      */
 293     public final ObjectProperty<Orientation> orientationProperty() {
 294         if (orientation == null) {
 295             orientation = new StyleableObjectProperty(HORIZONTAL) {
 296                 @Override
 297                 public void invalidated() {
 298                     requestLayout();
 299                 }
 300 
 301                 @Override
 302                 public CssMetaData<FlowPane, Orientation> getCssMetaData() {
 303                     return StyleableProperties.ORIENTATION;
 304                 }
 305 
 306                 @Override
 307                 public Object getBean() {
 308                     return FlowPane.this;
 309                 }
 310 
 311                 @Override
 312                 public String getName() {
 313                     return "orientation";
 314                 }
 315             };
 316         }
 317         return orientation;
 318     }
 319 
 320     private ObjectProperty<Orientation> orientation;
 321     public final void setOrientation(Orientation value) { orientationProperty().set(value); }
 322     public final Orientation getOrientation() { return orientation == null ? HORIZONTAL : orientation.get();  }
 323 
 324     /**
 325      * The amount of horizontal space between each node in a horizontal flowpane
 326      * or the space between columns in a vertical flowpane.
 327      * @return the amount of horizontal space between each node in a horizontal
 328      * flowpane or the space between columns in a vertical flowpane
 329      */
 330     public final DoubleProperty hgapProperty() {
 331         if (hgap == null) {
 332             hgap = new StyleableDoubleProperty() {
 333 
 334                 @Override
 335                 public void invalidated() {
 336                     requestLayout();
 337                 }
 338 
 339                 @Override
 340                 public CssMetaData<FlowPane, Number> getCssMetaData() {
 341                     return StyleableProperties.HGAP;
 342                 }
 343 
 344                 @Override
 345                 public Object getBean() {
 346                     return FlowPane.this;
 347                 }
 348 
 349                 @Override
 350                 public String getName() {
 351                     return "hgap";
 352                 }
 353             };
 354         }
 355         return hgap;
 356     }
 357 
 358     private DoubleProperty hgap;
 359     public final void setHgap(double value) { hgapProperty().set(value); }
 360     public final double getHgap() { return hgap == null ? 0 : hgap.get(); }
 361 
 362     /**
 363      * The amount of vertical space between each node in a vertical flowpane
 364      * or the space between rows in a horizontal flowpane.
 365      * @return the amount of vertical space between each node in a vertical
 366      * flowpane or the space between rows in a horizontal flowpane
 367      */
 368     public final DoubleProperty vgapProperty() {
 369         if (vgap == null) {
 370             vgap = new StyleableDoubleProperty() {
 371                 @Override
 372                 public void invalidated() {
 373                     requestLayout();
 374                 }
 375 
 376                 @Override
 377                 public CssMetaData<FlowPane, Number> getCssMetaData() {
 378                     return StyleableProperties.VGAP;
 379                 }
 380 
 381                 @Override
 382                 public Object getBean() {
 383                     return FlowPane.this;
 384                 }
 385 
 386                 @Override
 387                 public String getName() {
 388                     return "vgap";
 389                 }
 390             };
 391         }
 392         return vgap;
 393     }
 394 
 395     private DoubleProperty vgap;
 396     public final void setVgap(double value) { vgapProperty().set(value); }
 397     public final double getVgap() { return vgap == null ? 0 : vgap.get(); }
 398 
 399 
 400     /**
 401      * The preferred width where content should wrap in a horizontal flowpane or
 402      * the preferred height where content should wrap in a vertical flowpane.
 403      * <p>
 404      * This value is used only to compute the preferred size of the flowpane and may
 405      * not reflect the actual width or height, which may change if the flowpane is
 406      * resized to something other than its preferred size.
 407      * <p>
 408      * Applications should initialize this value to define a reasonable span
 409      * for wrapping the content.
 410      *
 411      * @return the preferred width where content should wrap in a horizontal
 412      * flowpane or the preferred height where content should wrap in a vertical
 413      * flowpane
 414      */
 415     public final DoubleProperty prefWrapLengthProperty() {
 416         if (prefWrapLength == null) {
 417             prefWrapLength = new DoublePropertyBase(400) {
 418                 @Override
 419                 protected void invalidated() {
 420                     requestLayout();
 421                 }
 422 
 423                 @Override
 424                 public Object getBean() {
 425                     return FlowPane.this;
 426                 }
 427 
 428                 @Override
 429                 public String getName() {
 430                     return "prefWrapLength";
 431                 }
 432             };
 433         }
 434         return prefWrapLength;
 435     }
 436     private DoubleProperty prefWrapLength;
 437     public final void setPrefWrapLength(double value) { prefWrapLengthProperty().set(value); }
 438     public final double getPrefWrapLength() { return prefWrapLength == null ? 400 : prefWrapLength.get(); }
 439 
 440 
 441     /**
 442      * The overall alignment of the flowpane's content within its width and height.
 443      * <p>For a horizontal flowpane, each row will be aligned within the flowpane's width
 444      * using the alignment's hpos value, and the rows will be aligned within the
 445      * flowpane's height using the alignment's vpos value.
 446      * <p>For a vertical flowpane, each column will be aligned within the flowpane's height
 447      * using the alignment's vpos value, and the columns will be aligned within the
 448      * flowpane's width using the alignment's hpos value.
 449      * @return the overall alignment of the flowpane's content within its width
 450      * and height
 451      */
 452     public final ObjectProperty<Pos> alignmentProperty() {
 453         if (alignment == null) {
 454             alignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) {
 455 
 456                 @Override
 457                 public void invalidated() {
 458                     requestLayout();
 459                 }
 460 
 461                 @Override
 462                 public CssMetaData<FlowPane, Pos> getCssMetaData() {
 463                     return StyleableProperties.ALIGNMENT;
 464                 }
 465 
 466                 @Override
 467                 public Object getBean() {
 468                     return FlowPane.this;
 469                 }
 470 
 471                 @Override
 472                 public String getName() {
 473                     return "alignment";
 474                 }
 475             };
 476         }
 477         return alignment;
 478     }
 479 
 480     private ObjectProperty<Pos> alignment;
 481     public final void setAlignment(Pos value) { alignmentProperty().set(value); }
 482     public final Pos getAlignment() { return alignment == null ? Pos.TOP_LEFT : alignment.get(); }
 483     private Pos getAlignmentInternal() {
 484         Pos localPos = getAlignment();
 485         return localPos == null ? Pos.TOP_LEFT : localPos;
 486     }
 487 
 488     /**
 489      * The horizontal alignment of nodes within each column of a vertical flowpane.
 490      * The property is ignored for horizontal flowpanes.
 491      * @return the horizontal alignment of nodes within each column of a
 492      * vertical flowpane
 493      */
 494     public final ObjectProperty<HPos> columnHalignmentProperty() {
 495         if (columnHalignment == null) {
 496             columnHalignment = new StyleableObjectProperty<HPos>(HPos.LEFT) {
 497 
 498                 @Override
 499                 public void invalidated() {
 500                     requestLayout();
 501                 }
 502 
 503                 @Override
 504                 public CssMetaData<FlowPane, HPos> getCssMetaData() {
 505                     return StyleableProperties.COLUMN_HALIGNMENT;
 506                 }
 507 
 508                 @Override
 509                 public Object getBean() {
 510                     return FlowPane.this;
 511                 }
 512 
 513                 @Override
 514                 public String getName() {
 515                     return "columnHalignment";
 516                 }
 517             };
 518         }
 519         return columnHalignment;
 520     }
 521 
 522     private ObjectProperty<HPos> columnHalignment;
 523     public final void setColumnHalignment(HPos value) { columnHalignmentProperty().set(value); }
 524     public final HPos getColumnHalignment() { return columnHalignment == null ? HPos.LEFT : columnHalignment.get(); }
 525     private HPos getColumnHalignmentInternal() {
 526         HPos localPos = getColumnHalignment();
 527         return localPos == null ? HPos.LEFT : localPos;
 528     }
 529 
 530     /**
 531      * The vertical alignment of nodes within each row of a horizontal flowpane.
 532      * If this property is set to VPos.BASELINE, then the flowpane will always
 533      * resize children to their preferred heights, rather than expanding heights
 534      * to fill the row height.
 535      * The property is ignored for vertical flowpanes.
 536      * @return the vertical alignment of nodes within each row of a horizontal
 537      * flowpane
 538      */
 539     public final ObjectProperty<VPos> rowValignmentProperty() {
 540         if (rowValignment == null) {
 541             rowValignment = new StyleableObjectProperty<VPos>(VPos.CENTER) {
 542                 @Override
 543                 public void invalidated() {
 544                     requestLayout();
 545                 }
 546 
 547                 @Override
 548                 public CssMetaData<FlowPane, VPos> getCssMetaData() {
 549                     return StyleableProperties.ROW_VALIGNMENT;
 550                 }
 551 
 552                 @Override
 553                 public Object getBean() {
 554                     return FlowPane.this;
 555                 }
 556 
 557                 @Override
 558                 public String getName() {
 559                     return "rowValignment";
 560                 }
 561             };
 562         }
 563         return rowValignment;
 564     }
 565 
 566     private ObjectProperty<VPos> rowValignment;
 567     public final void setRowValignment(VPos value) { rowValignmentProperty().set(value); }
 568     public final VPos getRowValignment() { return rowValignment == null ? VPos.CENTER : rowValignment.get(); }
 569     private VPos getRowValignmentInternal() {
 570         VPos localPos =  getRowValignment();
 571         return localPos == null ? VPos.CENTER : localPos;
 572     }
 573 
 574     @Override public Orientation getContentBias() {
 575         return getOrientation();
 576     }
 577 
 578     @Override protected double computeMinWidth(double height) {
 579         if (getContentBias() == HORIZONTAL) {
 580             double maxPref = 0;
 581             final List<Node> children = getChildren();
 582             for (int i=0, size=children.size(); i<size; i++) {
 583                 Node child = children.get(i);
 584                 if (child.isManaged()) {
 585                     maxPref = Math.max(maxPref, child.prefWidth(-1));
 586                 }
 587             }
 588             final Insets insets = getInsets();
 589             return insets.getLeft() + snapSizeX(maxPref) + insets.getRight();
 590         }
 591         return computePrefWidth(height);
 592     }
 593 
 594     @Override protected double computeMinHeight(double width) {
 595         if (getContentBias() == VERTICAL) {
 596             double maxPref = 0;
 597             final List<Node> children = getChildren();
 598             for (int i=0, size=children.size(); i<size; i++) {
 599                 Node child = children.get(i);
 600                 if (child.isManaged()) {
 601                     maxPref = Math.max(maxPref, child.prefHeight(-1));
 602                 }
 603             }
 604             final Insets insets = getInsets();
 605             return insets.getTop() + snapSizeY(maxPref) + insets.getBottom();
 606         }
 607         return computePrefHeight(width);
 608     }
 609 
 610     @Override protected double computePrefWidth(double forHeight) {
 611         final Insets insets = getInsets();
 612         if (getOrientation() == HORIZONTAL) {
 613             // horizontal
 614             double maxRunWidth = getPrefWrapLength();
 615             List<Run> hruns = getRuns(maxRunWidth);
 616             double w = computeContentWidth(hruns);
 617             w = getPrefWrapLength() > w ? getPrefWrapLength() : w;
 618             return insets.getLeft() + snapSizeX(w) + insets.getRight();
 619         } else {
 620             // vertical
 621             double maxRunHeight = forHeight != -1?
 622                 forHeight - insets.getTop() - insets.getBottom() : getPrefWrapLength();
 623             List<Run> vruns = getRuns(maxRunHeight);
 624             return insets.getLeft() + computeContentWidth(vruns) + insets.getRight();
 625         }
 626     }
 627 
 628     @Override protected double computePrefHeight(double forWidth) {
 629         final Insets insets = getInsets();
 630         if (getOrientation() == HORIZONTAL) {
 631             // horizontal
 632             double maxRunWidth = forWidth != -1?
 633                 forWidth - insets.getLeft() - insets.getRight() : getPrefWrapLength();
 634             List<Run> hruns = getRuns(maxRunWidth);
 635             return insets.getTop() + computeContentHeight(hruns) + insets.getBottom();
 636         } else {
 637             // vertical
 638             double maxRunHeight = getPrefWrapLength();
 639             List<Run> vruns = getRuns(maxRunHeight);
 640             double h = computeContentHeight(vruns);
 641             h = getPrefWrapLength() > h ? getPrefWrapLength() : h;
 642             return insets.getTop() + snapSizeY(h) + insets.getBottom();
 643         }
 644     }
 645 
 646     @Override public void requestLayout() {
 647         if (!computingRuns) {
 648             runs = null;
 649         }
 650         super.requestLayout();
 651     }
 652 
 653     private List<Run> runs = null;
 654     private double lastMaxRunLength = -1;
 655     boolean computingRuns = false;
 656 
 657     private List<Run> getRuns(double maxRunLength) {
 658         if (runs == null || maxRunLength != lastMaxRunLength) {
 659             computingRuns = true;
 660             lastMaxRunLength = maxRunLength;
 661             runs = new ArrayList();
 662             double runLength = 0;
 663             double runOffset = 0;
 664             Run run = new Run();
 665             double vgap = snapSpaceY(this.getVgap());
 666             double hgap = snapSpaceX(this.getHgap());
 667 
 668             final List<Node> children = getChildren();
 669             for (int i=0, size=children.size(); i<size; i++) {
 670                 Node child = children.get(i);
 671                 if (child.isManaged()) {
 672                     LayoutRect nodeRect = new LayoutRect();
 673                     nodeRect.node = child;
 674                     Insets margin = getMargin(child);
 675                     nodeRect.width = computeChildPrefAreaWidth(child, margin);
 676                     nodeRect.height = computeChildPrefAreaHeight(child, margin);
 677                     double nodeLength = getOrientation() == HORIZONTAL ? nodeRect.width : nodeRect.height;
 678                     if (runLength + nodeLength > maxRunLength && runLength > 0) {
 679                         // wrap to next run *unless* its the only node in the run
 680                         normalizeRun(run, runOffset);
 681                         if (getOrientation() == HORIZONTAL) {
 682                             // horizontal
 683                             runOffset += run.height + vgap;
 684                         } else {
 685                             // vertical
 686                             runOffset += run.width + hgap;
 687                         }
 688                         runs.add(run);
 689                         runLength = 0;
 690                         run = new Run();
 691                     }
 692                     if (getOrientation() == HORIZONTAL) {
 693                         // horizontal
 694                         nodeRect.x = runLength;
 695                         runLength += nodeRect.width + hgap;
 696                     } else {
 697                         // vertical
 698                         nodeRect.y = runLength;
 699                         runLength += nodeRect.height + vgap;
 700                     }
 701                     run.rects.add(nodeRect);
 702                 }
 703 
 704             }
 705             // insert last run
 706             normalizeRun(run, runOffset);
 707             runs.add(run);
 708             computingRuns = false;
 709         }
 710         return runs;
 711     }
 712 
 713     private void normalizeRun(final Run run, double runOffset) {
 714         if (getOrientation() == HORIZONTAL) {
 715             // horizontal
 716             ArrayList<Node> rownodes = new ArrayList();
 717             run.width = (run.rects.size()-1)*snapSpaceX(getHgap());
 718             for (int i=0, max=run.rects.size(); i<max; i++) {
 719                 LayoutRect lrect = run.rects.get(i);
 720                 rownodes.add(lrect.node);
 721                 run.width += lrect.width;
 722                 lrect.y = runOffset;
 723             }
 724             run.height = computeMaxPrefAreaHeight(rownodes, marginAccessor, getRowValignment());
 725             run.baselineOffset = getRowValignment() == VPos.BASELINE?
 726                     getAreaBaselineOffset(rownodes, marginAccessor, i -> run.rects.get(i).width, run.height, true) : 0;
 727 
 728         } else {
 729             // vertical
 730             run.height = (run.rects.size()-1)*snapSpaceY(getVgap());
 731             double maxw = 0;
 732             for (int i=0, max=run.rects.size(); i<max; i++) {
 733                 LayoutRect lrect = run.rects.get(i);
 734                 run.height += lrect.height;
 735                 lrect.x = runOffset;
 736                 maxw = Math.max(maxw, lrect.width);
 737             }
 738 
 739             run.width = maxw;
 740             run.baselineOffset = run.height;
 741         }
 742     }
 743 
 744     private double computeContentWidth(List<Run> runs) {
 745         double cwidth = getOrientation() == HORIZONTAL ? 0 : (runs.size()-1)*snapSpaceX(getHgap());
 746         for (int i=0, max=runs.size(); i<max; i++) {
 747             Run run = runs.get(i);
 748             if (getOrientation() == HORIZONTAL) {
 749                 cwidth = Math.max(cwidth, run.width);
 750             } else {
 751                 // vertical
 752                 cwidth += run.width;
 753             }
 754         }
 755         return cwidth;
 756     }
 757 
 758     private double computeContentHeight(List<Run> runs) {
 759         double cheight = getOrientation() == VERTICAL ? 0 : (runs.size()-1)*snapSpaceY(getVgap());
 760         for (int i=0, max=runs.size(); i<max; i++) {
 761             Run run = runs.get(i);
 762             if (getOrientation() == VERTICAL) {
 763                 cheight = Math.max(cheight, run.height);
 764             } else {
 765                 // horizontal
 766                 cheight += run.height;
 767             }
 768         }
 769         return cheight;
 770     }
 771 
 772     @Override protected void layoutChildren() {
 773         final Insets insets = getInsets();
 774         final double width = getWidth();
 775         final double height = getHeight();
 776         final double top = insets.getTop();
 777         final double left = insets.getLeft();
 778         final double bottom = insets.getBottom();
 779         final double right = insets.getRight();
 780         final double insideWidth = width - left - right;
 781         final double insideHeight = height - top - bottom;
 782 
 783         //REMIND(aim): need to figure out how to cache the runs to avoid over-calculation
 784         final List<Run> runs = getRuns(getOrientation() == HORIZONTAL ? insideWidth : insideHeight);
 785 
 786         // Now that the nodes are broken into runs, figure out alignments
 787         for (int i=0, max=runs.size(); i<max; i++) {
 788             final Run run = runs.get(i);
 789             final double xoffset = left + computeXOffset(insideWidth,
 790                                      getOrientation() == HORIZONTAL ? run.width : computeContentWidth(runs),
 791                                      getAlignmentInternal().getHpos());
 792             final double yoffset = top + computeYOffset(insideHeight,
 793                                     getOrientation() == VERTICAL ? run.height : computeContentHeight(runs),
 794                                     getAlignmentInternal().getVpos());
 795             for (int j = 0; j < run.rects.size(); j++) {
 796                 final LayoutRect lrect = run.rects.get(j);
 797 //              System.out.println("flowpane.layout: run="+i+" "+run.width+"x"+run.height+" xoffset="+xoffset+" yoffset="+yoffset+" lrect="+lrect);
 798                 final double x = xoffset + lrect.x;
 799                 final double y = yoffset + lrect.y;
 800                 layoutInArea(lrect.node, x, y,
 801                            getOrientation() == HORIZONTAL? lrect.width : run.width,
 802                            getOrientation() == VERTICAL? lrect.height : run.height,
 803                            run.baselineOffset, getMargin(lrect.node),
 804                            getColumnHalignmentInternal(), getRowValignmentInternal());
 805             }
 806         }
 807     }
 808 
 809     /***************************************************************************
 810      *                                                                         *
 811      *                         Stylesheet Handling                             *
 812      *                                                                         *
 813      **************************************************************************/
 814 
 815 
 816      /*
 817       * Super-lazy instantiation pattern from Bill Pugh.
 818       */
 819      private static class StyleableProperties {
 820 
 821          private static final CssMetaData<FlowPane,Pos> ALIGNMENT =
 822              new CssMetaData<FlowPane,Pos>("-fx-alignment",
 823                  new EnumConverter<Pos>(Pos.class), Pos.TOP_LEFT) {
 824 
 825             @Override
 826             public boolean isSettable(FlowPane node) {
 827                 return node.alignment == null || !node.alignment.isBound();
 828             }
 829 
 830             @Override
 831             public StyleableProperty<Pos> getStyleableProperty(FlowPane node) {
 832                 return (StyleableProperty<Pos>)node.alignmentProperty();
 833             }
 834 
 835          };
 836 
 837          private static final CssMetaData<FlowPane,HPos> COLUMN_HALIGNMENT =
 838              new CssMetaData<FlowPane,HPos>("-fx-column-halignment",
 839                  new EnumConverter<HPos>(HPos.class), HPos.LEFT) {
 840 
 841             @Override
 842             public boolean isSettable(FlowPane node) {
 843                 return node.columnHalignment == null || !node.columnHalignment.isBound();
 844             }
 845 
 846             @Override
 847             public StyleableProperty<HPos> getStyleableProperty(FlowPane node) {
 848                 return (StyleableProperty<HPos>)node.columnHalignmentProperty();
 849             }
 850 
 851          };
 852 
 853          private static final CssMetaData<FlowPane,Number> HGAP =
 854              new CssMetaData<FlowPane,Number>("-fx-hgap",
 855                  SizeConverter.getInstance(), 0.0){
 856 
 857             @Override
 858             public boolean isSettable(FlowPane node) {
 859                 return node.hgap == null || !node.hgap.isBound();
 860             }
 861 
 862             @Override
 863             public StyleableProperty<Number> getStyleableProperty(FlowPane node) {
 864                 return (StyleableProperty<Number>)node.hgapProperty();
 865             }
 866 
 867          };
 868 
 869          private static final CssMetaData<FlowPane,VPos> ROW_VALIGNMENT =
 870              new CssMetaData<FlowPane,VPos>("-fx-row-valignment",
 871                  new EnumConverter<VPos>(VPos.class), VPos.CENTER) {
 872 
 873             @Override
 874             public boolean isSettable(FlowPane node) {
 875                 return node.rowValignment == null || !node.rowValignment.isBound();
 876             }
 877 
 878             @Override
 879             public StyleableProperty<VPos> getStyleableProperty(FlowPane node) {
 880                 return (StyleableProperty<VPos>)node.rowValignmentProperty();
 881             }
 882 
 883          };
 884 
 885          private static final CssMetaData<FlowPane,Orientation> ORIENTATION =
 886              new CssMetaData<FlowPane,Orientation>("-fx-orientation",
 887                  new EnumConverter<Orientation>(Orientation.class),
 888                  Orientation.HORIZONTAL) {
 889 
 890             @Override
 891             public Orientation getInitialValue(FlowPane node) {
 892                 // A vertical flow pane should remain vertical
 893                 return node.getOrientation();
 894             }
 895 
 896             @Override
 897             public boolean isSettable(FlowPane node) {
 898                 return node.orientation == null || !node.orientation.isBound();
 899             }
 900 
 901             @Override
 902             public StyleableProperty<Orientation> getStyleableProperty(FlowPane node) {
 903                 return (StyleableProperty<Orientation>)node.orientationProperty();
 904             }
 905 
 906          };
 907 
 908          private static final CssMetaData<FlowPane,Number> VGAP =
 909              new CssMetaData<FlowPane,Number>("-fx-vgap",
 910                  SizeConverter.getInstance(), 0.0){
 911 
 912             @Override
 913             public boolean isSettable(FlowPane node) {
 914                 return node.vgap == null || !node.vgap.isBound();
 915             }
 916 
 917             @Override
 918             public StyleableProperty<Number> getStyleableProperty(FlowPane node) {
 919                 return (StyleableProperty<Number>)node.vgapProperty();
 920             }
 921 
 922          };
 923 
 924          private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 925          static {
 926 
 927             final List<CssMetaData<? extends Styleable, ?>> styleables =
 928                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
 929             styleables.add(ALIGNMENT);
 930             styleables.add(COLUMN_HALIGNMENT);
 931             styleables.add(HGAP);
 932             styleables.add(ROW_VALIGNMENT);
 933             styleables.add(ORIENTATION);
 934             styleables.add(VGAP);
 935 
 936             STYLEABLES = Collections.unmodifiableList(styleables);
 937          }
 938     }
 939 
 940 
 941     /**
 942      * @return The CssMetaData associated with this class, which may include the
 943      * CssMetaData of its superclasses.
 944      * @since JavaFX 8.0
 945      */
 946     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 947         return StyleableProperties.STYLEABLES;
 948     }
 949 
 950     /**
 951      * {@inheritDoc}
 952      *
 953      * @since JavaFX 8.0
 954      */
 955 
 956 
 957     @Override
 958     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 959         return getClassCssMetaData();
 960     }
 961 
 962     //REMIND(aim); replace when we get mutable rects
 963     private static class LayoutRect {
 964         public Node node;
 965         double x;
 966         double y;
 967         double width;
 968         double height;
 969 
 970         @Override public String toString() {
 971             return "LayoutRect node id="+node.getId()+" "+x+","+y+" "+width+"x"+height;
 972         }
 973     }
 974 
 975     private static class Run {
 976         ArrayList<LayoutRect> rects = new ArrayList();
 977         double width;
 978         double height;
 979         double baselineOffset;
 980     }
 981 }