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