1 /*
   2  * Copyright (c) 2010, 2014, 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.control;
  27 
  28 import java.util.ArrayList;
  29 import java.util.Collections;
  30 import java.util.List;
  31 import java.util.WeakHashMap;
  32 
  33 import javafx.beans.DefaultProperty;
  34 import javafx.beans.property.DoubleProperty;
  35 import javafx.beans.property.ObjectProperty;
  36 import javafx.beans.property.SimpleDoubleProperty;
  37 import javafx.beans.value.WritableValue;
  38 import javafx.collections.FXCollections;
  39 import javafx.collections.ListChangeListener;
  40 import javafx.collections.ObservableList;
  41 import javafx.geometry.Orientation;
  42 import javafx.scene.Node;
  43 
  44 import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection;
  45 
  46 import javafx.css.StyleableObjectProperty;
  47 import javafx.css.CssMetaData;
  48 import javafx.css.PseudoClass;
  49 
  50 import javafx.css.converter.EnumConverter;
  51 import javafx.scene.control.skin.SplitPaneSkin;
  52 
  53 import javafx.css.Styleable;
  54 import javafx.css.StyleableProperty;
  55 
  56 /**
  57  * <p>A control that has two or more sides, each separated by a divider, which can be
  58  * dragged by the user to give more space to one of the sides, resulting in
  59  * the other side shrinking by an equal amount.</p>
  60  *
  61  * <p>{@link Node Nodes} can be positioned horizontally next to each other, or stacked
  62  * vertically. This can be controlled by setting the {@link #orientationProperty()}.</p>
  63  *
  64  * <p> The dividers in a SplitPane have the following behavior
  65  * <ul>
  66  * <li>Dividers cannot overlap another divider</li>
  67  * <li>Dividers cannot overlap a node.</li>
  68  * <li>Dividers moving to the left/top will stop when the node's min size is reached.</li>
  69  * <li>Dividers moving to the right/bottom will stop when the node's max size is reached.</li>
  70  * </ul>
  71  *
  72  * <p>Nodes needs to be placed inside a layout container before they are added
  73  * into the SplitPane.  If the node is not inside a layout container
  74  * the maximum and minimum position of the divider will be the
  75  * maximum and minimum size of the content.
  76  * </p>
  77  *
  78  * <p>A divider's position ranges from 0 to 1.0(inclusive).  A position of 0 will place the
  79  * divider at the left/top most edge of the SplitPane plus the minimum size of the node.  A
  80  * position of 1.0 will place the divider at the right/bottom most edge of the SplitPane minus the
  81  * minimum size of the node.  A divider position of 0.5 will place the
  82  * the divider in the middle of the SplitPane.  Setting the divider position greater
  83  * than the node's maximum size position will result in the divider being set at the
  84  * node's maximum size position.  Setting the divider position less than the node's minimum size position
  85  * will result in the divider being set at the node's minimum size position. Therefore the value set in
  86  * {@link #setDividerPosition} and {@link #setDividerPositions} may not be the same as the value returned by
  87  * {@link #getDividerPositions}.
  88  * </p>
  89  *
  90  * <p>If there are more than two nodes in the SplitPane and the divider positions are set in such a
  91  * way that the dividers cannot fit the nodes the dividers will be automatically adjusted by the SplitPane.
  92  * <p>For example we have three nodes whose sizes and divider positions are
  93  * </p>
  94  * <pre>
  95  * Node 1: min 25 max 100
  96  * Node 2: min 100 max 200
  97  * Node 3: min 25 max 50
  98  * divider 1: 0.40
  99  * divider 2: 0.45
 100  * </pre>
 101  *
 102  * <p>The result will be Node 1 size will be its pref size and divider 1 will be positioned 0.40,
 103  * Node 2 size will be its min size and divider 2 position will be the min size of Node 2 plus
 104  * divider 1 position, and the remaining space will be given to Node 3.
 105  * </p>
 106  *
 107  * <p>
 108  * SplitPane sets focusTraversable to false.
 109  * </p>
 110  *
 111  * <p>Example:</p>
 112  * <pre><code>
 113  * SplitPane sp = new SplitPane();
 114  * final StackPane sp1 = new StackPane();
 115  * sp1.getItems().add(new Button("Button One"));
 116  * final StackPane sp2 = new StackPane();
 117  * sp2.getItems().add(new Button("Button Two"));
 118  * final StackPane sp3 = new StackPane();
 119  * sp3.getItems().add(new Button("Button Three"));
 120  * sp.getItems().addAll(sp1, sp2, sp3);
 121  * sp.setDividerPositions(0.3f, 0.6f, 0.9f);
 122  * </code></pre>
 123  *
 124  * @since JavaFX 2.0
 125  */
 126 @DefaultProperty("items")
 127 public class SplitPane extends Control {
 128 
 129     /********************************************************************
 130      *  static methods
 131      ********************************************************************/
 132     private static final String RESIZABLE_WITH_PARENT = "resizable-with-parent";
 133 
 134     /**
 135      * Sets a node in the SplitPane to be resizable or not when the SplitPane is
 136      * resized.  By default all node are resizable.  Setting value to false will
 137      * prevent the node from being resized.
 138      * @param node A node in the SplitPane.
 139      * @param value true if the node is resizable or false if not resizable.
 140      * @since JavaFX 2.1
 141      */
 142     public static void setResizableWithParent(Node node, Boolean value) {
 143         if (value == null) {
 144             node.getProperties().remove(RESIZABLE_WITH_PARENT);
 145         } else {
 146             node.getProperties().put(RESIZABLE_WITH_PARENT, value);
 147         }
 148     }
 149 
 150     /**
 151      * Return true if the node is resizable when the parent container is resized false otherwise.
 152      * @param node A node in the SplitPane.
 153      * @defaultValue true
 154      * @return true if the node is resizable false otherwise.
 155      * @since JavaFX 2.1
 156      */
 157     public static Boolean isResizableWithParent(Node node) {
 158         if (node.hasProperties()) {
 159             Object value = node.getProperties().get(RESIZABLE_WITH_PARENT);
 160             if (value != null) {
 161                 return (Boolean)value;
 162             }
 163         }
 164         return true;
 165     }
 166     
 167     /***************************************************************************
 168      *                                                                         *
 169      * Constructors                                                            *
 170      *                                                                         *
 171      **************************************************************************/
 172 
 173     /**
 174      * Creates a new SplitPane with no content.
 175      */
 176     public SplitPane() {
 177         this((Node[])null);
 178     }
 179 
 180     /**
 181      * Creates a new SplitPane with the given items set as the content to split
 182      * between one or more dividers.
 183      *
 184      * @param items The items to place inside the SplitPane.
 185      * @since JavaFX 8u40
 186      */
 187     public SplitPane(Node... items) {
 188         getStyleClass().setAll(DEFAULT_STYLE_CLASS);
 189         // focusTraversable is styleable through css. Calling setFocusTraversable
 190         // makes it look to css like the user set the value and css will not 
 191         // override. Initializing focusTraversable by calling applyStyle with a
 192         // null StyleOrigin ensures that css will be able to override the value.
 193         ((StyleableProperty<Boolean>)(WritableValue<Boolean>)focusTraversableProperty()).applyStyle(null, Boolean.FALSE); 
 194 
 195         getItems().addListener(new ListChangeListener<Node>() {
 196             @Override public void onChanged(Change<? extends Node> c) {
 197                 while (c.next()) {
 198                     int from = c.getFrom();
 199                     int index = from;
 200                     for (int i = 0; i < c.getRemovedSize(); i++) {
 201                         if (index < dividers.size()) {
 202                             dividerCache.put(index, Double.MAX_VALUE);
 203                         } else if (index == dividers.size()) {
 204                             if (!dividers.isEmpty()) {
 205                                 if (c.wasReplaced()) {
 206                                     dividerCache.put(index - 1, dividers.get(index - 1).getPosition());
 207                                 } else {
 208                                     dividerCache.put(index - 1, Double.MAX_VALUE);
 209                                 }
 210                             }
 211                         }
 212                         index++;
 213                     }
 214                     for (int i = 0; i < dividers.size(); i++) {
 215                         if (dividerCache.get(i) == null) {
 216                             dividerCache.put(i, dividers.get(i).getPosition());
 217                         }
 218                     }
 219                 }
 220                 dividers.clear();
 221                 for (int i = 0; i < getItems().size() - 1; i++) {
 222                     if (dividerCache.containsKey(i) && dividerCache.get(i) != Double.MAX_VALUE) {
 223                         Divider d = new Divider();
 224                         d.setPosition(dividerCache.get(i));
 225                         dividers.add(d);
 226                     } else {
 227                         dividers.add(new Divider());
 228                     }
 229                     dividerCache.remove(i);
 230                 }
 231             }
 232         });
 233 
 234         if (items != null) {
 235             getItems().addAll(items);
 236         }
 237         
 238         // initialize pseudo-class state
 239         pseudoClassStateChanged(HORIZONTAL_PSEUDOCLASS_STATE, true);
 240     }
 241 
 242     /***************************************************************************
 243      *                                                                         *
 244      * Properties                                                              *
 245      *                                                                         *
 246      **************************************************************************/
 247 
 248     // --- Vertical
 249     private ObjectProperty<Orientation> orientation;
 250 
 251     /**
 252      * <p>This property controls how the SplitPane should be displayed to the
 253      * user. {@link javafx.geometry.Orientation#HORIZONTAL} will result in
 254      * two (or more) nodes being placed next to each other horizontally, whilst
 255      * {@link javafx.geometry.Orientation#VERTICAL} will result in the nodes being
 256      * stacked vertically.</p>
 257      *
 258      */
 259     public final void setOrientation(Orientation value) {
 260         orientationProperty().set(value);
 261     };
 262 
 263     /**
 264      * The orientation for the SplitPane.
 265      * @return The orientation for the SplitPane.
 266      */
 267     public final Orientation getOrientation() {
 268         return orientation == null ? Orientation.HORIZONTAL : orientation.get();
 269     }
 270 
 271     /**
 272      * The orientation for the SplitPane.
 273      */
 274     public final ObjectProperty<Orientation> orientationProperty() {
 275         if (orientation == null) {
 276             orientation = new StyleableObjectProperty<Orientation>(Orientation.HORIZONTAL) {
 277                 @Override public void invalidated() {
 278                     final boolean isVertical = (get() == Orientation.VERTICAL);
 279                     pseudoClassStateChanged(VERTICAL_PSEUDOCLASS_STATE,    isVertical);
 280                     pseudoClassStateChanged(HORIZONTAL_PSEUDOCLASS_STATE, !isVertical);
 281                 }
 282                 
 283                 @Override public CssMetaData<SplitPane,Orientation> getCssMetaData() {
 284                     return StyleableProperties.ORIENTATION;
 285                 }
 286                 
 287                 @Override
 288                 public Object getBean() {
 289                     return SplitPane.this;
 290                 }
 291 
 292                 @Override
 293                 public String getName() {
 294                     return "orientation";
 295                 }
 296             };
 297         }
 298         return orientation;
 299     }
 300 
 301 
 302 
 303     /***************************************************************************
 304      *                                                                         *
 305      * Instance Variables                                                      *
 306      *                                                                         *
 307      **************************************************************************/
 308 
 309     private final ObservableList<Node> items = FXCollections.observableArrayList();
 310 
 311     private final ObservableList<Divider> dividers = FXCollections.observableArrayList();
 312     private final ObservableList<Divider> unmodifiableDividers = FXCollections.unmodifiableObservableList(dividers);
 313 
 314     // Cache the divider positions if the items have not been created.
 315     private final WeakHashMap<Integer, Double> dividerCache = new WeakHashMap<Integer, Double>();
 316 
 317     /***************************************************************************
 318      *                                                                         *
 319      * Public API                                                              *
 320      *                                                                         *
 321      **************************************************************************/
 322 
 323     /**
 324      * Returns an ObservableList which can be use to modify the contents of the SplitPane.
 325      * The order the nodes are placed into this list will be the same order in the SplitPane.
 326      *
 327      * @return the list of items in this SplitPane.
 328      */
 329     public ObservableList<Node> getItems() {
 330         return items;
 331     }
 332 
 333     /**
 334      * Returns an unmodifiable list of all the dividers in this SplitPane.
 335      *
 336      * @return the list of dividers.
 337      */
 338     @ReturnsUnmodifiableCollection public ObservableList<Divider> getDividers() {
 339         return unmodifiableDividers;
 340     }
 341 
 342     /**
 343      * Sets the position of the divider at the specified divider index.
 344      *
 345      * @param dividerIndex the index of the divider.
 346      * @param position the divider position, between 0.0 and 1.0 (inclusive).
 347      */
 348     public void setDividerPosition(int dividerIndex, double position) {
 349         if (getDividers().size() <= dividerIndex)  {
 350             dividerCache.put(dividerIndex, position);
 351             return;
 352         }
 353         if (dividerIndex >= 0) {
 354             getDividers().get(dividerIndex).setPosition(position);
 355         }
 356     }
 357 
 358     /**
 359      * Sets the position of the divider
 360      *
 361      * @param positions the divider position, between 0.0 and 1.0 (inclusive).
 362      */
 363     public void setDividerPositions(double... positions) {
 364         if (dividers.isEmpty()) {
 365             for (int i = 0; i < positions.length; i++) {
 366                 dividerCache.put(i, positions[i]);
 367             }
 368             return;
 369         }
 370         for (int i = 0; i < positions.length && i < dividers.size(); i++) {
 371             dividers.get(i).setPosition(positions[i]);
 372         }
 373     }
 374 
 375     /**
 376      * Returns an array of double containing the position of each divider.
 377      *
 378      * @return an array of double containing the position of each divider.
 379      */
 380     public double[] getDividerPositions() {
 381         double[] positions = new double[dividers.size()];
 382         for (int i = 0; i < dividers.size(); i++) {
 383             positions[i] = dividers.get(i).getPosition();
 384         }
 385         return positions;
 386     }
 387 
 388     /** {@inheritDoc} */
 389     @Override protected Skin<?> createDefaultSkin() {
 390         return new SplitPaneSkin(this);
 391     }
 392 
 393     /***************************************************************************
 394      *                                                                         *
 395      *                         Stylesheet Handling                             *
 396      *                                                                         *
 397      **************************************************************************/
 398 
 399     private static final String DEFAULT_STYLE_CLASS = "split-pane";
 400 
 401     /** @treatAsPrivate */
 402     private static class StyleableProperties {
 403         private static final CssMetaData<SplitPane,Orientation> ORIENTATION =
 404             new CssMetaData<SplitPane,Orientation>("-fx-orientation",
 405                 new EnumConverter<Orientation>(Orientation.class),
 406                 Orientation.HORIZONTAL) {
 407 
 408             @Override
 409             public Orientation getInitialValue(SplitPane node) {
 410                 // A vertical SplitPane should remain vertical 
 411                 return node.getOrientation();
 412             }
 413             
 414             @Override
 415             public boolean isSettable(SplitPane n) {
 416                 return n.orientation == null || !n.orientation.isBound();
 417             }
 418 
 419             @Override
 420             public StyleableProperty<Orientation> getStyleableProperty(SplitPane n) {
 421                 return (StyleableProperty<Orientation>)(WritableValue<Orientation>)n.orientationProperty();
 422             }
 423         };
 424 
 425         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 426         static {
 427             final List<CssMetaData<? extends Styleable, ?>> styleables =
 428                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
 429             styleables.add(ORIENTATION);
 430             STYLEABLES = Collections.unmodifiableList(styleables);
 431         }
 432     }
 433 
 434     /**
 435      * @return The CssMetaData associated with this class, which may include the
 436      * CssMetaData of its super classes.
 437      * @since JavaFX 8.0
 438      */
 439     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 440         return StyleableProperties.STYLEABLES;
 441     }
 442 
 443     /**
 444      * {@inheritDoc}
 445      * @since JavaFX 8.0
 446      */
 447     @Override
 448     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
 449         return getClassCssMetaData();
 450     }
 451 
 452     private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("vertical");
 453     private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("horizontal");
 454 
 455     /**
 456       * Most Controls return true for focusTraversable, so Control overrides
 457       * this method to return true, but SplitPane returns false for
 458       * focusTraversable's initial value; hence the override of the override. 
 459       * This method is called from CSS code to get the correct initial value.
 460       * @treatAsPrivate implementation detail
 461       */
 462     @Deprecated @Override
 463     protected /*do not make final*/ Boolean impl_cssGetFocusTraversableInitialValue() {
 464         return Boolean.FALSE;
 465     }
 466     
 467 
 468     /***************************************************************************
 469      *                                                                         *
 470      * Support Classes                                                         *
 471      *                                                                         *
 472      **************************************************************************/
 473 
 474     /**
 475      * Represents a single divider in the SplitPane.
 476      * @since JavaFX 2.0
 477      */
 478     public static class Divider {
 479 
 480         /**
 481          * Creates a default Divider instance.
 482          */
 483         public Divider() {
 484 
 485         }
 486 
 487         /**
 488          * <p>Represents the location where the divider should ideally be
 489          * positioned, between 0.0 and 1.0 (inclusive). 0.0 represents the
 490          * left- or top-most point, and 1.0 represents the right- or bottom-most
 491          * point (depending on the horizontal property). The SplitPane will attempt
 492          * to get the divider to the point requested, but it must take into account
 493          * the minimum width/height of the nodes contained within it.</p>
 494          *
 495          * <p>As the user drags the SplitPane divider around this property will
 496          * be updated to always represent its current location.</p>
 497          *
 498          * @defaultValue 0.5
 499          */
 500         private DoubleProperty position;
 501         public final void setPosition(double value) {
 502             positionProperty().set(value);
 503         }
 504 
 505         public final double getPosition() {
 506             return position == null ? 0.5F : position.get();
 507         }
 508 
 509         public final DoubleProperty positionProperty() {
 510             if (position == null) {
 511                 position = new SimpleDoubleProperty(this, "position", 0.5F);// {
 512 //                    @Override protected void invalidated() {
 513 //                        if (get() < 0) {
 514 //                            this.value = value;
 515 //                        } else if (get() > 1) {
 516 //                            this.value = value;
 517 //                        }
 518 //                    }
 519 //                };
 520             }
 521             return position;
 522         }
 523     }
 524 }