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