1 /*
   2  * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import javafx.beans.value.ChangeListener;
  29 import javafx.beans.value.ObservableValue;
  30 import javafx.collections.FXCollections;
  31 import javafx.collections.ListChangeListener;
  32 import javafx.collections.ObservableList;
  33 import javafx.geometry.HPos;
  34 import javafx.geometry.NodeOrientation;
  35 import javafx.geometry.Orientation;
  36 import javafx.geometry.VPos;
  37 import javafx.scene.Cursor;
  38 import javafx.scene.Node;
  39 import javafx.scene.control.Accordion;
  40 import javafx.scene.control.Control;
  41 import javafx.scene.control.SkinBase;
  42 import javafx.scene.control.SplitPane;
  43 import javafx.scene.input.MouseEvent;
  44 import javafx.scene.layout.StackPane;
  45 import javafx.scene.shape.Rectangle;
  46 import java.util.ArrayList;
  47 import java.util.Iterator;
  48 import java.util.List;
  49 import java.util.ListIterator;
  50 
  51 /**
  52  * Default skin implementation for the {@link SplitPane} control.
  53  *
  54  * @see SplitPane
  55  * @since 9
  56  */
  57 public class SplitPaneSkin extends SkinBase<SplitPane> {
  58 
  59     /***************************************************************************
  60      *                                                                         *
  61      * Private fields                                                          *
  62      *                                                                         *
  63      **************************************************************************/
  64 
  65     private ObservableList<Content> contentRegions;
  66     private ObservableList<ContentDivider> contentDividers;
  67     private boolean horizontal;
  68 
  69 
  70 
  71     /***************************************************************************
  72      *                                                                         *
  73      * Constructors                                                            *
  74      *                                                                         *
  75      **************************************************************************/
  76 
  77     /**
  78      * Creates a new SplitPaneSkin instance, installing the necessary child
  79      * nodes into the Control {@link Control#getChildren() children} list, as
  80      * well as the necessary input mappings for handling key, mouse, etc events.
  81      *
  82      * @param control The control that this skin should be installed onto.
  83      */
  84     public SplitPaneSkin(final SplitPane control) {
  85         super(control);
  86 //        control.setManaged(false);
  87         horizontal = getSkinnable().getOrientation() == Orientation.HORIZONTAL;
  88 
  89         contentRegions = FXCollections.<Content>observableArrayList();
  90         contentDividers = FXCollections.<ContentDivider>observableArrayList();
  91 
  92         int index = 0;
  93         for (Node n: getSkinnable().getItems()) {
  94             addContent(index++, n);
  95         }
  96         initializeContentListener();
  97 
  98         for (SplitPane.Divider d: getSkinnable().getDividers()) {
  99             addDivider(d);
 100         }
 101 
 102         registerChangeListener(control.orientationProperty(), e -> {
 103             this.horizontal = getSkinnable().getOrientation() == Orientation.HORIZONTAL;
 104             this.previousSize = -1;
 105             for (ContentDivider c: contentDividers) {
 106                 c.setGrabberStyle(horizontal);
 107             }
 108             getSkinnable().requestLayout();
 109         });
 110         registerChangeListener(control.widthProperty(), e -> getSkinnable().requestLayout());
 111         registerChangeListener(control.heightProperty(), e -> getSkinnable().requestLayout());
 112     }
 113 
 114 
 115 
 116     /***************************************************************************
 117      *                                                                         *
 118      * Public API                                                              *
 119      *                                                                         *
 120      **************************************************************************/
 121 
 122     /** {@inheritDoc} */
 123     @Override protected void layoutChildren(final double x, final double y,
 124                                             final double w, final double h) {
 125         final SplitPane s = getSkinnable();
 126         final double sw = s.getWidth();
 127         final double sh = s.getHeight();
 128 
 129         if (!s.isVisible() ||
 130                 (horizontal ? sw == 0 : sh == 0) ||
 131                 contentRegions.isEmpty()) {
 132             return;
 133         }
 134 
 135         double dividerWidth = contentDividers.isEmpty() ? 0 : contentDividers.get(0).prefWidth(-1);
 136 
 137         if (contentDividers.size() > 0 && previousSize != -1 && previousSize != (horizontal ? sw  : sh)) {
 138             //This algorithm adds/subtracts a little to each panel on every resize
 139             List<Content> resizeList = new ArrayList<Content>();
 140             for (Content c: contentRegions) {
 141                 if (c.isResizableWithParent()) {
 142                     resizeList.add(c);
 143                 }
 144             }
 145 
 146             double delta = (horizontal ? s.getWidth() : s.getHeight()) - previousSize;
 147             boolean growing = delta > 0;
 148 
 149             delta = Math.abs(delta);
 150 
 151             if (delta != 0 && !resizeList.isEmpty()) {
 152                 int portion = (int)(delta)/resizeList.size();
 153                 int remainder = (int)delta%resizeList.size();
 154                 int size = 0;
 155                 if (portion == 0) {
 156                     portion = remainder;
 157                     size = remainder;
 158                     remainder = 0;
 159                 } else {
 160                     size = portion * resizeList.size();
 161                 }
 162 
 163                 while (size > 0 && !resizeList.isEmpty()) {
 164                     if (growing) {
 165                         lastDividerUpdate++;
 166                     } else {
 167                         lastDividerUpdate--;
 168                         if (lastDividerUpdate < 0) {
 169                             lastDividerUpdate = contentRegions.size() - 1;
 170                         }
 171                     }
 172                     int id = lastDividerUpdate%contentRegions.size();
 173                     Content content = contentRegions.get(id);
 174                     if (content.isResizableWithParent() && resizeList.contains(content)) {
 175                         double area = content.getArea();
 176                         if (growing) {
 177                             double max = horizontal ? content.maxWidth(-1) : content.maxHeight(-1);
 178                             if ((area + portion) <= max) {
 179                                 area += portion;
 180                             } else {
 181                                 resizeList.remove(content);
 182                                 continue;
 183                             }
 184                         } else {
 185                             double min = horizontal ? content.minWidth(-1) : content.minHeight(-1);
 186                             if ((area - portion) >= min) {
 187                                 area -= portion;
 188                             } else {
 189                                 resizeList.remove(content);
 190                                 continue;
 191                             }
 192                         }
 193                         content.setArea(area);
 194                         size -= portion;
 195                         if (size == 0 && remainder != 0) {
 196                             portion = remainder;
 197                             size = remainder;
 198                             remainder = 0;
 199                         } else if (size == 0) {
 200                             break;
 201                         }
 202                     }
 203                 }
 204 
 205                 // If we are resizing the window save the current area into
 206                 // resizableWithParentArea.  We use this value during layout.
 207                 {
 208                     for (Content c: contentRegions) {
 209                         c.setResizableWithParentArea(c.getArea());
 210                         c.setAvailable(0);
 211                     }
 212                 }
 213                 resize = true;
 214             }
 215 
 216             previousSize = horizontal ? sw : sh;
 217         } else {
 218             previousSize = horizontal ? sw : sh;
 219         }
 220 
 221         // If the window is less than the min size we want to resize
 222         // proportionally
 223         double minSize = totalMinSize();
 224         if (minSize > (horizontal ? w : h)) {
 225             double percentage = 0;
 226             for (int i = 0; i < contentRegions.size(); i++) {
 227                 Content c = contentRegions.get(i);
 228                 double min = horizontal ? c.minWidth(-1) : c.minHeight(-1);
 229                 percentage = min/minSize;
 230                 if (horizontal) {
 231                     c.setArea(snapSpaceX(percentage * w));
 232                 } else {
 233                     c.setArea(snapSpaceY(percentage * h));
 234                 }
 235                 c.setAvailable(0);
 236             }
 237             setupContentAndDividerForLayout();
 238             layoutDividersAndContent(w, h);
 239             resize = false;
 240             return;
 241         }
 242 
 243         for(int trys = 0; trys < 10; trys++) {
 244             // Compute the area in between each divider.
 245             ContentDivider previousDivider = null;
 246             ContentDivider divider = null;
 247             for (int i = 0; i < contentRegions.size(); i++) {
 248                 double space = 0;
 249                 if (i < contentDividers.size()) {
 250                     divider = contentDividers.get(i);
 251                     if (divider.posExplicit) {
 252                         checkDividerPosition(divider, posToDividerPos(divider, divider.d.getPosition()),
 253                                 divider.getDividerPos());
 254                     }
 255                     if (i == 0) {
 256                         // First panel
 257                         space = getAbsoluteDividerPos(divider);
 258                     } else {
 259                         double newPos = getAbsoluteDividerPos(previousDivider) + dividerWidth;
 260                         // Middle panels
 261                         if (getAbsoluteDividerPos(divider) <= getAbsoluteDividerPos(previousDivider)) {
 262                             // The current divider and the previous divider share the same position
 263                             // or the current divider position is less than the previous position.
 264                             // We will set the divider next to the previous divider.
 265                             setAndCheckAbsoluteDividerPos(divider, newPos);
 266                         }
 267                         space = getAbsoluteDividerPos(divider) - newPos;
 268                     }
 269                 } else if (i == contentDividers.size()) {
 270                     // Last panel
 271                     space = (horizontal ? w : h) - (previousDivider != null ? getAbsoluteDividerPos(previousDivider) + dividerWidth : 0);
 272                 }
 273                 if (!resize || divider.posExplicit) {
 274                     contentRegions.get(i).setArea(space);
 275                 }
 276                 previousDivider = divider;
 277             }
 278 
 279             // Compute the amount of space we have available.
 280             // Available is amount of space we can take from a panel before we reach its min.
 281             // If available is negative we don't have enough space and we will
 282             // proportionally take the space from the other availables.  If we have extra space
 283             // we will porportionally give it to the others
 284             double spaceRequested = 0;
 285             double extraSpace = 0;
 286             for (Content c: contentRegions) {
 287                 if (c == null) continue;
 288 
 289                 double max = horizontal ? c.maxWidth(-1) : c.maxHeight(-1);
 290                 double min = horizontal ? c.minWidth(-1) : c.minHeight(-1);
 291 
 292                 if (c.getArea() >= max) {
 293                     // Add the space that needs to be distributed to the others
 294                     extraSpace += (c.getArea() - max);
 295                     c.setArea(max);
 296                 }
 297                 c.setAvailable(c.getArea() - min);
 298                 if (c.getAvailable() < 0) {
 299                     spaceRequested += c.getAvailable();
 300                 }
 301             }
 302 
 303             spaceRequested = Math.abs(spaceRequested);
 304 
 305             // Add the panels where we can take space from
 306             List<Content> availableList = new ArrayList<Content>();
 307             List<Content> storageList = new ArrayList<Content>();
 308             List<Content> spaceRequestor = new ArrayList<Content>();
 309             double available = 0;
 310             for (Content c: contentRegions) {
 311                 if (c.getAvailable() >= 0) {
 312                     available += c.getAvailable();
 313                     availableList.add(c);
 314                 }
 315 
 316                 if (resize && !c.isResizableWithParent()) {
 317                     // We are making the SplitPane bigger and will need to
 318                     // distribute the extra space.
 319                     if (c.getArea() >= c.getResizableWithParentArea()) {
 320                         extraSpace += (c.getArea() - c.getResizableWithParentArea());
 321                     } else {
 322                         // We are making the SplitPane smaller and will need to
 323                         // distribute the space requested.
 324                         spaceRequested += (c.getResizableWithParentArea() - c.getArea());
 325                     }
 326                     c.setAvailable(0);
 327                 }
 328                 // Add the panels where we can add space to;
 329                 if (resize) {
 330                     if (c.isResizableWithParent()) {
 331                         storageList.add(c);
 332                     }
 333                 } else {
 334                     storageList.add(c);
 335                 }
 336                 // List of panels that need space.
 337                 if (c.getAvailable() < 0) {
 338                     spaceRequestor.add(c);
 339                 }
 340             }
 341 
 342             if (extraSpace > 0) {
 343                 extraSpace = distributeTo(storageList, extraSpace);
 344                 // After distributing add any panels that may still need space to the
 345                 // spaceRequestor list.
 346                 spaceRequested = 0;
 347                 spaceRequestor.clear();
 348                 available = 0;
 349                 availableList.clear();
 350                 for (Content c: contentRegions) {
 351                     if (c.getAvailable() < 0) {
 352                         spaceRequested += c.getAvailable();
 353                         spaceRequestor.add(c);
 354                     } else {
 355                         available += c.getAvailable();
 356                         availableList.add(c);
 357                     }
 358                 }
 359                 spaceRequested = Math.abs(spaceRequested);
 360             }
 361 
 362             if (available >= spaceRequested) {
 363                 for (Content requestor: spaceRequestor) {
 364                     double min = horizontal ? requestor.minWidth(-1) : requestor.minHeight(-1);
 365                     requestor.setArea(min);
 366                     requestor.setAvailable(0);
 367                 }
 368                 // After setting all the space requestors to their min we have to
 369                 // redistribute the space requested to any panel that still
 370                 // has available space.
 371                 if (spaceRequested > 0 && !spaceRequestor.isEmpty()) {
 372                     distributeFrom(spaceRequested, availableList);
 373                 }
 374 
 375                 // Only for resizing.  We should have all the panel areas
 376                 // available computed.  We can total them up and see
 377                 // how much space we have left or went over and redistribute.
 378                 if (resize) {
 379                     double total = 0;
 380                     for (Content c: contentRegions) {
 381                         if (c.isResizableWithParent()) {
 382                             total += c.getArea();
 383                         } else {
 384                             total += c.getResizableWithParentArea();
 385                         }
 386                     }
 387                     total += (dividerWidth * contentDividers.size());
 388                     if (total < (horizontal ? w : h)) {
 389                         extraSpace += ((horizontal ? w : h) - total);
 390                         distributeTo(storageList, extraSpace);
 391                     } else {
 392                         spaceRequested += (total - (horizontal ? w : h));
 393                         distributeFrom(spaceRequested, storageList);
 394                     }
 395                 }
 396             }
 397 
 398             setupContentAndDividerForLayout();
 399 
 400             // Check the bounds of every panel
 401             boolean passed = true;
 402             for (Content c: contentRegions) {
 403                 double max = horizontal ? c.maxWidth(-1) : c.maxHeight(-1);
 404                 double min = horizontal ? c.minWidth(-1) : c.minHeight(-1);
 405                 if (c.getArea() < min || c.getArea() > max) {
 406                     passed = false;
 407                     break;
 408                 }
 409             }
 410             if (passed) {
 411                 break;
 412             }
 413         }
 414 
 415         layoutDividersAndContent(w, h);
 416         resize = false;
 417     }
 418 
 419     /** {@inheritDoc} */
 420     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 421         double minWidth = 0;
 422         double maxMinWidth = 0;
 423         for (Content c: contentRegions) {
 424             minWidth += c.minWidth(-1);
 425             maxMinWidth = Math.max(maxMinWidth, c.minWidth(-1));
 426         }
 427         for (ContentDivider d: contentDividers) {
 428             minWidth += d.prefWidth(-1);
 429         }
 430         if (horizontal) {
 431             return minWidth + leftInset + rightInset;
 432         } else {
 433             return maxMinWidth + leftInset + rightInset;
 434         }
 435     }
 436 
 437     /** {@inheritDoc} */
 438     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 439         double minHeight = 0;
 440         double maxMinHeight = 0;
 441         for (Content c: contentRegions) {
 442             minHeight += c.minHeight(-1);
 443             maxMinHeight = Math.max(maxMinHeight, c.minHeight(-1));
 444         }
 445         for (ContentDivider d: contentDividers) {
 446             minHeight += d.prefWidth(-1);
 447         }
 448         if (horizontal) {
 449             return maxMinHeight + topInset + bottomInset;
 450         } else {
 451             return minHeight + topInset + bottomInset;
 452         }
 453     }
 454 
 455     /** {@inheritDoc} */
 456     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 457         double prefWidth = 0;
 458         double prefMaxWidth = 0;
 459         for (Content c: contentRegions) {
 460             prefWidth += c.prefWidth(-1);
 461             prefMaxWidth = Math.max(prefMaxWidth, c.prefWidth(-1));
 462         }
 463         for (ContentDivider d: contentDividers) {
 464             prefWidth += d.prefWidth(-1);
 465         }
 466         if (horizontal) {
 467             return prefWidth + leftInset + rightInset;
 468         } else {
 469             return prefMaxWidth + leftInset + rightInset;
 470         }
 471     }
 472 
 473     /** {@inheritDoc} */
 474     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 475         double prefHeight = 0;
 476         double maxPrefHeight = 0;
 477         for (Content c: contentRegions) {
 478             prefHeight += c.prefHeight(-1);
 479             maxPrefHeight = Math.max(maxPrefHeight, c.prefHeight(-1));
 480         }
 481         for (ContentDivider d: contentDividers) {
 482             prefHeight += d.prefWidth(-1);
 483         }
 484         if (horizontal) {
 485             return maxPrefHeight + topInset + bottomInset;
 486         } else {
 487             return prefHeight + topInset + bottomInset;
 488         }
 489     }
 490 
 491 
 492 
 493     /***************************************************************************
 494      *                                                                         *
 495      * Private implementation                                                  *
 496      *                                                                         *
 497      **************************************************************************/
 498 
 499     private void addContent(int index, Node n) {
 500         Content c = new Content(n);
 501         contentRegions.add(index, c);
 502         getChildren().add(index, c);
 503     }
 504 
 505     private void removeContent(Node n) {
 506         for (Content c: contentRegions) {
 507             if (c.getContent().equals(n)) {
 508                 c.dispose();
 509                 getChildren().remove(c);
 510                 contentRegions.remove(c);
 511                 break;
 512             }
 513         }
 514     }
 515 
 516     private void initializeContentListener() {
 517         getSkinnable().getItems().addListener((ListChangeListener<Node>) c -> {
 518             while (c.next()) {
 519                 if (c.wasPermutated() || c.wasUpdated()) {
 520                     /**
 521                      * the contents were either moved, or updated.
 522                      * rebuild the contents to re-sync
 523                      */
 524                     getChildren().clear();
 525                     contentRegions.clear();
 526                     int index = 0;
 527                     for (Node n : c.getList()) {
 528                         addContent(index++, n);
 529                     }
 530 
 531                 } else {
 532                     for (Node n : c.getRemoved()) {
 533                         removeContent(n);
 534                     }
 535 
 536                     int index = c.getFrom();
 537                     for (Node n : c.getAddedSubList()) {
 538                         addContent(index++, n);
 539                     }
 540                 }
 541             }
 542             // TODO there may be a more efficient way than rebuilding all the dividers
 543             // everytime the list changes.
 544             removeAllDividers();
 545             for (SplitPane.Divider d: getSkinnable().getDividers()) {
 546                 addDivider(d);
 547             }
 548         });
 549     }
 550 
 551     private void checkDividerPosition(ContentDivider divider, double newPos, double oldPos) {
 552         double dividerWidth = divider.prefWidth(-1);
 553         Content left = getLeft(divider);
 554         Content right = getRight(divider);
 555         double minLeft = left == null ? 0 : (horizontal) ? left.minWidth(-1) : left.minHeight(-1);
 556         double minRight = right == null ? 0 : (horizontal) ? right.minWidth(-1) : right.minHeight(-1);
 557         double maxLeft = left == null ? 0 :
 558             left.getContent() != null ? (horizontal) ? left.getContent().maxWidth(-1) : left.getContent().maxHeight(-1) : 0;
 559         double maxRight = right == null ? 0 :
 560             right.getContent() != null ? (horizontal) ? right.getContent().maxWidth(-1) : right.getContent().maxHeight(-1) : 0;
 561 
 562         double previousDividerPos = 0;
 563         double nextDividerPos = getSize();
 564         int index = contentDividers.indexOf(divider);
 565 
 566         if (index - 1 >= 0) {
 567             previousDividerPos = contentDividers.get(index - 1).getDividerPos();
 568             if (previousDividerPos == -1) {
 569                 // Get the divider position if it hasn't been initialized.
 570                 previousDividerPos = getAbsoluteDividerPos(contentDividers.get(index - 1));
 571             }
 572         }
 573         if (index + 1 < contentDividers.size()) {
 574             nextDividerPos = contentDividers.get(index + 1).getDividerPos();
 575             if (nextDividerPos == -1) {
 576                 // Get the divider position if it hasn't been initialized.
 577                 nextDividerPos = getAbsoluteDividerPos(contentDividers.get(index + 1));
 578             }
 579         }
 580 
 581         // Set the divider into the correct position by looking at the max and min content sizes.
 582         checkDividerPos = false;
 583         if (newPos > oldPos) {
 584             double max = previousDividerPos == 0 ? maxLeft : previousDividerPos + dividerWidth + maxLeft;
 585             double min = nextDividerPos - minRight - dividerWidth;
 586             double stopPos = Math.min(max, min);
 587             if (newPos >= stopPos) {
 588                 setAbsoluteDividerPos(divider, stopPos);
 589             } else {
 590                 double rightMax = nextDividerPos - maxRight - dividerWidth;
 591                 if (newPos <= rightMax) {
 592                     setAbsoluteDividerPos(divider, rightMax);
 593                 } else {
 594                     setAbsoluteDividerPos(divider, newPos);
 595                 }
 596             }
 597         } else {
 598             double max = nextDividerPos - maxRight - dividerWidth;
 599             double min = previousDividerPos == 0 ? minLeft : previousDividerPos + minLeft + dividerWidth;
 600             double stopPos = Math.max(max, min);
 601             if (newPos <= stopPos) {
 602                 setAbsoluteDividerPos(divider, stopPos);
 603             } else {
 604                 double leftMax = previousDividerPos + maxLeft + dividerWidth;
 605                 if (newPos >= leftMax) {
 606                     setAbsoluteDividerPos(divider, leftMax);
 607                 } else {
 608                     setAbsoluteDividerPos(divider, newPos);
 609                 }
 610             }
 611         }
 612         checkDividerPos = true;
 613     }
 614 
 615     private void addDivider(SplitPane.Divider d) {
 616         ContentDivider c = new ContentDivider(d);
 617         c.setInitialPos(d.getPosition());
 618         c.setDividerPos(-1);
 619         ChangeListener<Number> posPropertyListener = new PosPropertyListener(c);
 620         c.setPosPropertyListener(posPropertyListener);
 621         d.positionProperty().addListener(posPropertyListener);
 622         initializeDivderEventHandlers(c);
 623         contentDividers.add(c);
 624         getChildren().add(c);
 625     }
 626 
 627     private void removeAllDividers() {
 628         ListIterator<ContentDivider> dividers = contentDividers.listIterator();
 629         while (dividers.hasNext()) {
 630             ContentDivider c = dividers.next();
 631             getChildren().remove(c);
 632             c.getDivider().positionProperty().removeListener(c.getPosPropertyListener());
 633             dividers.remove();
 634         }
 635         lastDividerUpdate = 0;
 636     }
 637 
 638     private void initializeDivderEventHandlers(final ContentDivider divider) {
 639         // TODO: do we need to consume all mouse events?
 640         // they only bubble to the skin which consumes them by default
 641         divider.addEventHandler(MouseEvent.ANY, event -> {
 642             event.consume();
 643         });
 644 
 645         divider.setOnMousePressed(e -> {
 646             if (horizontal) {
 647                 divider.setInitialPos(divider.getDividerPos());
 648                 divider.setPressPos(e.getSceneX());
 649                 divider.setPressPos(getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT
 650                         ? getSkinnable().getWidth() - e.getSceneX() : e.getSceneX());
 651             } else {
 652                 divider.setInitialPos(divider.getDividerPos());
 653                 divider.setPressPos(e.getSceneY());
 654             }
 655             e.consume();
 656         });
 657 
 658         divider.setOnMouseDragged(e -> {
 659             double delta = 0;
 660             if (horizontal) {
 661                 delta = getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT
 662                         ? getSkinnable().getWidth() - e.getSceneX() : e.getSceneX();
 663             } else {
 664                 delta = e.getSceneY();
 665             }
 666             delta -= divider.getPressPos();
 667             setAndCheckAbsoluteDividerPos(divider, Math.ceil(divider.getInitialPos() + delta));
 668             e.consume();
 669         });
 670     }
 671 
 672     private Content getLeft(ContentDivider d) {
 673         int index = contentDividers.indexOf(d);
 674         if (index != -1) {
 675             return contentRegions.get(index);
 676         }
 677         return null;
 678     }
 679 
 680     private Content getRight(ContentDivider d) {
 681         int index = contentDividers.indexOf(d);
 682         if (index != -1) {
 683             return contentRegions.get(index + 1);
 684         }
 685         return null;
 686     }
 687 
 688     // Value is the left edge of the divider
 689     private void setAbsoluteDividerPos(ContentDivider divider, double value) {
 690         if (getSkinnable().getWidth() > 0 && getSkinnable().getHeight() > 0 && divider != null) {
 691             SplitPane.Divider paneDivider = divider.getDivider();
 692             divider.setDividerPos(value);
 693             double size = getSize();
 694             if (size != 0) {
 695                 // Adjust the position to the center of the
 696                 // divider and convert its position to a percentage.
 697                 double pos = value + divider.prefWidth(-1)/2;
 698                 paneDivider.setPosition(pos / size);
 699             } else {
 700                 paneDivider.setPosition(0);
 701             }
 702         }
 703     }
 704 
 705     // Updates the divider with the SplitPane.Divider's position
 706     // The value updated to SplitPane.Divider will be the center of the divider.
 707     // The returned position will be the left edge of the divider
 708     private double getAbsoluteDividerPos(ContentDivider divider) {
 709         if (getSkinnable().getWidth() > 0 && getSkinnable().getHeight() > 0 && divider != null) {
 710             SplitPane.Divider paneDivider = divider.getDivider();
 711             double newPos = posToDividerPos(divider, paneDivider.getPosition());
 712             divider.setDividerPos(newPos);
 713             return newPos;
 714         }
 715         return 0;
 716     }
 717 
 718     // Returns the left edge of the divider at pos
 719     // Pos is the percentage location from SplitPane.Divider.
 720     private double posToDividerPos(ContentDivider divider, double pos) {
 721         double newPos = getSize() * pos;
 722         if (pos == 1) {
 723             newPos -= divider.prefWidth(-1);
 724         } else {
 725             newPos -= divider.prefWidth(-1)/2;
 726         }
 727         return Math.round(newPos);
 728     }
 729 
 730     private double totalMinSize() {
 731         double dividerWidth = !contentDividers.isEmpty() ? contentDividers.size() * contentDividers.get(0).prefWidth(-1) : 0;
 732         double minSize = 0;
 733         for (Content c: contentRegions) {
 734             if (horizontal) {
 735                 minSize += c.minWidth(-1);
 736             } else {
 737                 minSize += c.minHeight(-1);
 738             }
 739         }
 740         return minSize + dividerWidth;
 741     }
 742 
 743     private double getSize() {
 744         final SplitPane s = getSkinnable();
 745         double size = totalMinSize();
 746         if (horizontal) {
 747             if (s.getWidth() > size) {
 748                 size = s.getWidth() - snappedLeftInset() - snappedRightInset();
 749             }
 750         } else {
 751             if (s.getHeight() > size) {
 752                 size = s.getHeight() - snappedTopInset() - snappedBottomInset();
 753             }
 754         }
 755         return size;
 756     }
 757 
 758     // Evenly distribute the size to the available list.
 759     // size is the amount to distribute.
 760     private double distributeTo(List<Content> available, double size) {
 761         if (available.isEmpty()) {
 762             return size;
 763         }
 764 
 765         size = horizontal ? snapSizeX(size) : snapSizeY(size);
 766         int portion = (int)(size)/available.size();
 767         int remainder;
 768 
 769         while (size > 0 && !available.isEmpty()) {
 770             Iterator<Content> i = available.iterator();
 771             while (i.hasNext()) {
 772                 Content c = i.next();
 773                 double max = Math.min((horizontal ? c.maxWidth(-1) : c.maxHeight(-1)), Double.MAX_VALUE);
 774                 double min = horizontal ? c.minWidth(-1) : c.minHeight(-1);
 775 
 776                 // We have too much space
 777                 if (c.getArea() >= max) {
 778                     c.setAvailable(c.getArea() - min);
 779                     i.remove();
 780                     continue;
 781                 }
 782                 // Not enough space
 783                 if (portion >= (max - c.getArea())) {
 784                     size -= (max - c.getArea());
 785                     c.setArea(max);
 786                     c.setAvailable(max - min);
 787                     i.remove();
 788                 } else {
 789                     // Enough space
 790                     c.setArea(c.getArea() + portion);
 791                     c.setAvailable(c.getArea() - min);
 792                     size -= portion;
 793                 }
 794                 if ((int)size == 0) {
 795                     return size;
 796                 }
 797             }
 798             if (available.isEmpty()) {
 799                 // We reached the max size for everything just return
 800                 return size;
 801             }
 802             portion = (int)(size)/available.size();
 803             remainder = (int)(size)%available.size();
 804             if (portion == 0 && remainder != 0) {
 805                 portion = remainder;
 806                 remainder = 0;
 807             }
 808         }
 809         return size;
 810     }
 811 
 812     // Evenly distribute the size from the available list.
 813     // size is the amount to distribute.
 814     private double distributeFrom(double size, List<Content> available) {
 815         if (available.isEmpty()) {
 816             return size;
 817         }
 818 
 819         size = horizontal ? snapSizeX(size) : snapSizeY(size);
 820         int portion = (int)(size)/available.size();
 821         int remainder;
 822 
 823         while (size > 0 && !available.isEmpty()) {
 824             Iterator<Content> i = available.iterator();
 825             while (i.hasNext()) {
 826                 Content c = i.next();
 827                 //not enough space taking available and setting min
 828                 if (portion >= c.getAvailable()) {
 829                     c.setArea(c.getArea() - c.getAvailable()); // Min size
 830                     size -= c.getAvailable();
 831                     c.setAvailable(0);
 832                     i.remove();
 833                 } else {
 834                     //enough space
 835                     c.setArea(c.getArea() - portion);
 836                     c.setAvailable(c.getAvailable() - portion);
 837                     size -= portion;
 838                 }
 839                 if ((int)size == 0) {
 840                     return size;
 841                 }
 842             }
 843             if (available.isEmpty()) {
 844                 // We reached the min size for everything just return
 845                 return size;
 846             }
 847             portion = (int)(size)/available.size();
 848             remainder = (int)(size)%available.size();
 849             if (portion == 0 && remainder != 0) {
 850                 portion = remainder;
 851                 remainder = 0;
 852             }
 853         }
 854         return size;
 855     }
 856 
 857     private void setupContentAndDividerForLayout() {
 858         // Set all the value to prepare for layout
 859         double dividerWidth = contentDividers.isEmpty() ? 0 : contentDividers.get(0).prefWidth(-1);
 860         double startX = 0;
 861         double startY = 0;
 862         for (Content c: contentRegions) {
 863             if (resize && !c.isResizableWithParent()) {
 864                 c.setArea(c.getResizableWithParentArea());
 865             }
 866 
 867             c.setX(startX);
 868             c.setY(startY);
 869             if (horizontal) {
 870                 startX += (c.getArea() + dividerWidth);
 871             } else {
 872                 startY += (c.getArea() + dividerWidth);
 873             }
 874         }
 875 
 876         startX = 0;
 877         startY = 0;
 878         // The dividers are already in the correct positions.  Disable
 879         // checking the divider positions.
 880         checkDividerPos = false;
 881         for (int i = 0; i < contentDividers.size(); i++) {
 882             ContentDivider d = contentDividers.get(i);
 883             if (horizontal) {
 884                 startX += getLeft(d).getArea() + (i == 0 ? 0 : dividerWidth);
 885             } else {
 886                 startY += getLeft(d).getArea() + (i == 0 ? 0 : dividerWidth);
 887             }
 888             d.setX(startX);
 889             d.setY(startY);
 890             setAbsoluteDividerPos(d, (horizontal ? d.getX() : d.getY()));
 891             d.posExplicit = false;
 892         }
 893         checkDividerPos = true;
 894     }
 895 
 896     private void layoutDividersAndContent(double width, double height) {
 897         final double paddingX = snappedLeftInset();
 898         final double paddingY = snappedTopInset();
 899         final double dividerWidth = contentDividers.isEmpty() ? 0 : contentDividers.get(0).prefWidth(-1);
 900 
 901         for (Content c: contentRegions) {
 902 //            System.out.println("LAYOUT " + c.getId() + " PANELS X " + c.getX() + " Y " + c.getY() + " W " + (horizontal ? c.getArea() : width) + " H " + (horizontal ? height : c.getArea()));
 903             if (horizontal) {
 904                 c.setClipSize(c.getArea(), height);
 905                 layoutInArea(c, c.getX() + paddingX, c.getY() + paddingY, c.getArea(), height,
 906                     0/*baseline*/,HPos.CENTER, VPos.CENTER);
 907             } else {
 908                 c.setClipSize(width, c.getArea());
 909                 layoutInArea(c, c.getX() + paddingX, c.getY() + paddingY, width, c.getArea(),
 910                     0/*baseline*/,HPos.CENTER, VPos.CENTER);
 911             }
 912         }
 913         for (ContentDivider c: contentDividers) {
 914 //            System.out.println("LAYOUT DIVIDERS X " + c.getX() + " Y " + c.getY() + " W " + (horizontal ? dividerWidth : width) + " H " + (horizontal ? height : dividerWidth));
 915             if (horizontal) {
 916                 c.resize(dividerWidth, height);
 917                 positionInArea(c, c.getX() + paddingX, c.getY() + paddingY, dividerWidth, height,
 918                     /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
 919             } else {
 920                 c.resize(width, dividerWidth);
 921                 positionInArea(c, c.getX() + paddingX, c.getY() + paddingY, width, dividerWidth,
 922                     /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
 923             }
 924         }
 925     }
 926 
 927     private double previousSize = -1;
 928     private int lastDividerUpdate = 0;
 929     private boolean resize = false;
 930     private boolean checkDividerPos = true;
 931 
 932     private void setAndCheckAbsoluteDividerPos(ContentDivider divider, double value) {
 933         double oldPos = divider.getDividerPos();
 934         setAbsoluteDividerPos(divider, value);
 935         checkDividerPosition(divider, value, oldPos);
 936     }
 937 
 938 
 939 
 940     /***************************************************************************
 941      *                                                                         *
 942      * Support classes                                                         *
 943      *                                                                         *
 944      **************************************************************************/
 945 
 946     // This listener is to be removed from 'removed' dividers and added to 'added' dividers
 947     class PosPropertyListener implements ChangeListener<Number> {
 948         ContentDivider divider;
 949 
 950         public PosPropertyListener(ContentDivider divider) {
 951             this.divider = divider;
 952         }
 953 
 954         @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
 955             if (checkDividerPos) {
 956                 // When checking is enforced, we know that the position was set explicitly
 957                 divider.posExplicit = true;
 958             }
 959             getSkinnable().requestLayout();
 960         }
 961     }
 962 
 963 
 964     class ContentDivider extends StackPane {
 965         private double initialPos;
 966         private double dividerPos;
 967         private double pressPos;
 968         private SplitPane.Divider d;
 969         private StackPane grabber;
 970         private double x;
 971         private double y;
 972         private boolean posExplicit;
 973         private ChangeListener<Number> listener;
 974 
 975         public ContentDivider(SplitPane.Divider d) {
 976             getStyleClass().setAll("split-pane-divider");
 977 
 978             this.d = d;
 979             this.initialPos = 0;
 980             this.dividerPos = 0;
 981             this.pressPos = 0;
 982 
 983             grabber = new StackPane() {
 984                 @Override protected double computeMinWidth(double height) {
 985                     return 0;
 986                 }
 987 
 988                 @Override protected double computeMinHeight(double width) {
 989                     return 0;
 990                 }
 991 
 992                 @Override protected double computePrefWidth(double height) {
 993                     return snappedLeftInset() + snappedRightInset();
 994                 }
 995 
 996                 @Override protected double computePrefHeight(double width) {
 997                     return snappedTopInset() + snappedBottomInset();
 998                 }
 999 
1000                 @Override protected double computeMaxWidth(double height) {
1001                     return computePrefWidth(-1);
1002                 }
1003 
1004                 @Override protected double computeMaxHeight(double width) {
1005                     return computePrefHeight(-1);
1006                 }
1007             };
1008             setGrabberStyle(horizontal);
1009             getChildren().add(grabber);
1010 
1011             // TODO register a listener for SplitPane.Divider position
1012         }
1013 
1014         public SplitPane.Divider getDivider() {
1015             return this.d;
1016         }
1017 
1018         public final void setGrabberStyle(boolean horizontal) {
1019             grabber.getStyleClass().clear();
1020             grabber.getStyleClass().setAll("vertical-grabber");
1021             setCursor(Cursor.V_RESIZE);
1022             if (horizontal) {
1023                 grabber.getStyleClass().setAll("horizontal-grabber");
1024                 setCursor(Cursor.H_RESIZE);
1025             }
1026         }
1027 
1028         public double getInitialPos() {
1029             return initialPos;
1030         }
1031 
1032         public void setInitialPos(double initialPos) {
1033             this.initialPos = initialPos;
1034         }
1035 
1036         public double getDividerPos() {
1037             return dividerPos;
1038         }
1039 
1040         public void setDividerPos(double dividerPos) {
1041             this.dividerPos = dividerPos;
1042         }
1043 
1044         public double getPressPos() {
1045             return pressPos;
1046         }
1047 
1048         public void setPressPos(double pressPos) {
1049             this.pressPos = pressPos;
1050         }
1051 
1052         // TODO remove x and y and replace with dividerpos.
1053         public double getX() {
1054             return x;
1055         }
1056 
1057         public void setX(double x) {
1058             this.x = x;
1059         }
1060 
1061         public double getY() {
1062             return y;
1063         }
1064 
1065         public void setY(double y) {
1066             this.y = y;
1067         }
1068 
1069         public ChangeListener<Number> getPosPropertyListener() {
1070             return listener;
1071         }
1072 
1073         public void setPosPropertyListener(ChangeListener<Number> listener) {
1074             this.listener = listener;
1075         }
1076 
1077         @Override protected double computeMinWidth(double height) {
1078             return computePrefWidth(height);
1079         }
1080 
1081         @Override protected double computeMinHeight(double width) {
1082             return computePrefHeight(width);
1083         }
1084 
1085         @Override protected double computePrefWidth(double height) {
1086             return snappedLeftInset() + snappedRightInset();
1087         }
1088 
1089         @Override protected double computePrefHeight(double width) {
1090             return snappedTopInset() + snappedBottomInset();
1091         }
1092 
1093         @Override protected double computeMaxWidth(double height) {
1094             return computePrefWidth(height);
1095         }
1096 
1097         @Override protected double computeMaxHeight(double width) {
1098             return computePrefHeight(width);
1099         }
1100 
1101         @Override protected void layoutChildren() {
1102             double grabberWidth = grabber.prefWidth(-1);
1103             double grabberHeight = grabber.prefHeight(-1);
1104             double grabberX = (getWidth() - grabberWidth)/2;
1105             double grabberY = (getHeight() - grabberHeight)/2;
1106             grabber.resize(grabberWidth, grabberHeight);
1107             positionInArea(grabber, grabberX, grabberY, grabberWidth, grabberHeight,
1108                     /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1109         }
1110     }
1111 
1112     static class Content extends StackPane {
1113         private Node content;
1114         private Rectangle clipRect;
1115         private double x;
1116         private double y;
1117         private double area;
1118         private double resizableWithParentArea;
1119         private double available;
1120 
1121         public Content(Node n) {
1122             this.clipRect = new Rectangle();
1123             setClip(clipRect);
1124             this.content = n;
1125             if (n != null) {
1126                 getChildren().add(n);
1127             }
1128             this.x = 0;
1129             this.y = 0;
1130         }
1131 
1132         public Node getContent() {
1133             return content;
1134         }
1135 
1136         public double getX() {
1137             return x;
1138         }
1139 
1140         public void setX(double x) {
1141             this.x = x;
1142         }
1143 
1144         public double getY() {
1145             return y;
1146         }
1147 
1148         public void setY(double y) {
1149             this.y = y;
1150         }
1151 
1152         // This is the area of the panel.  This will be used as the
1153         // width/height during layout.
1154         public double getArea() {
1155             return area;
1156         }
1157 
1158         public void setArea(double area) {
1159             this.area = area;
1160         }
1161 
1162         // This is the minimum available area for other panels to use
1163         // if they need more space.
1164         public double getAvailable() {
1165             return available;
1166         }
1167 
1168         public void setAvailable(double available) {
1169             this.available = available;
1170         }
1171 
1172         public boolean isResizableWithParent() {
1173             return SplitPane.isResizableWithParent(content);
1174         }
1175 
1176         public double getResizableWithParentArea() {
1177             return resizableWithParentArea;
1178         }
1179 
1180         // This is used to save the current area during resizing when
1181         // isResizeableWithParent equals false.
1182         public void setResizableWithParentArea(double resizableWithParentArea) {
1183             if (!isResizableWithParent()) {
1184                 this.resizableWithParentArea = resizableWithParentArea;
1185             } else {
1186                 this.resizableWithParentArea = 0;
1187             }
1188         }
1189 
1190         protected void setClipSize(double w, double h) {
1191             clipRect.setWidth(w);
1192             clipRect.setHeight(h);
1193         }
1194 
1195         private void dispose() {
1196             getChildren().remove(content);
1197         }
1198 
1199         @Override protected double computeMaxWidth(double height) {
1200             return snapSizeX(content.maxWidth(height));
1201         }
1202 
1203         @Override protected double computeMaxHeight(double width) {
1204             return snapSizeY(content.maxHeight(width));
1205         }
1206     }
1207 }
1208