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                 double max = 0;
 285                 double min = 0;
 286                 if (c != null) {
 287                     max = horizontal ? c.maxWidth(-1) : c.maxHeight(-1);
 288                     min = horizontal ? c.minWidth(-1) : c.minHeight(-1);
 289                 }
 290 
 291                 if (c.getArea() >= max) {
 292                     // Add the space that needs to be distributed to the others
 293                     extraSpace += (c.getArea() - max);
 294                     c.setArea(max);
 295                 }
 296                 c.setAvailable(c.getArea() - min);
 297                 if (c.getAvailable() < 0) {
 298                     spaceRequested += c.getAvailable();
 299                 }
 300             }
 301 
 302             spaceRequested = Math.abs(spaceRequested);
 303 
 304             // Add the panels where we can take space from
 305             List<Content> availableList = new ArrayList<Content>();
 306             List<Content> storageList = new ArrayList<Content>();
 307             List<Content> spaceRequestor = new ArrayList<Content>();
 308             double available = 0;
 309             for (Content c: contentRegions) {
 310                 if (c.getAvailable() >= 0) {
 311                     available += c.getAvailable();
 312                     availableList.add(c);
 313                 }
 314 
 315                 if (resize && !c.isResizableWithParent()) {
 316                     // We are making the SplitPane bigger and will need to
 317                     // distribute the extra space.
 318                     if (c.getArea() >= c.getResizableWithParentArea()) {
 319                         extraSpace += (c.getArea() - c.getResizableWithParentArea());
 320                     } else {
 321                         // We are making the SplitPane smaller and will need to
 322                         // distribute the space requested.
 323                         spaceRequested += (c.getResizableWithParentArea() - c.getArea());
 324                     }
 325                     c.setAvailable(0);
 326                 }
 327                 // Add the panels where we can add space to;
 328                 if (resize) {
 329                     if (c.isResizableWithParent()) {
 330                         storageList.add(c);
 331                     }
 332                 } else {
 333                     storageList.add(c);
 334                 }
 335                 // List of panels that need space.
 336                 if (c.getAvailable() < 0) {
 337                     spaceRequestor.add(c);
 338                 }
 339             }
 340 
 341             if (extraSpace > 0) {
 342                 extraSpace = distributeTo(storageList, extraSpace);
 343                 // After distributing add any panels that may still need space to the
 344                 // spaceRequestor list.
 345                 spaceRequested = 0;
 346                 spaceRequestor.clear();
 347                 available = 0;
 348                 availableList.clear();
 349                 for (Content c: contentRegions) {
 350                     if (c.getAvailable() < 0) {
 351                         spaceRequested += c.getAvailable();
 352                         spaceRequestor.add(c);
 353                     } else {
 354                         available += c.getAvailable();
 355                         availableList.add(c);
 356                     }
 357                 }
 358                 spaceRequested = Math.abs(spaceRequested);
 359             }
 360 
 361             if (available >= spaceRequested) {
 362                 for (Content requestor: spaceRequestor) {
 363                     double min = horizontal ? requestor.minWidth(-1) : requestor.minHeight(-1);
 364                     requestor.setArea(min);
 365                     requestor.setAvailable(0);
 366                 }
 367                 // After setting all the space requestors to their min we have to
 368                 // redistribute the space requested to any panel that still
 369                 // has available space.
 370                 if (spaceRequested > 0 && !spaceRequestor.isEmpty()) {
 371                     distributeFrom(spaceRequested, availableList);
 372                 }
 373 
 374                 // Only for resizing.  We should have all the panel areas
 375                 // available computed.  We can total them up and see
 376                 // how much space we have left or went over and redistribute.
 377                 if (resize) {
 378                     double total = 0;
 379                     for (Content c: contentRegions) {
 380                         if (c.isResizableWithParent()) {
 381                             total += c.getArea();
 382                         } else {
 383                             total += c.getResizableWithParentArea();
 384                         }
 385                     }
 386                     total += (dividerWidth * contentDividers.size());
 387                     if (total < (horizontal ? w : h)) {
 388                         extraSpace += ((horizontal ? w : h) - total);
 389                         distributeTo(storageList, extraSpace);
 390                     } else {
 391                         spaceRequested += (total - (horizontal ? w : h));
 392                         distributeFrom(spaceRequested, storageList);
 393                     }
 394                 }
 395             }
 396 
 397             setupContentAndDividerForLayout();
 398 
 399             // Check the bounds of every panel
 400             boolean passed = true;
 401             for (Content c: contentRegions) {
 402                 double max = horizontal ? c.maxWidth(-1) : c.maxHeight(-1);
 403                 double min = horizontal ? c.minWidth(-1) : c.minHeight(-1);
 404                 if (c.getArea() < min || c.getArea() > max) {
 405                     passed = false;
 406                     break;
 407                 }
 408             }
 409             if (passed) {
 410                 break;
 411             }
 412         }
 413 
 414         layoutDividersAndContent(w, h);
 415         resize = false;
 416     }
 417 
 418     /** {@inheritDoc} */
 419     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 420         double minWidth = 0;
 421         double maxMinWidth = 0;
 422         for (Content c: contentRegions) {
 423             minWidth += c.minWidth(-1);
 424             maxMinWidth = Math.max(maxMinWidth, c.minWidth(-1));
 425         }
 426         for (ContentDivider d: contentDividers) {
 427             minWidth += d.prefWidth(-1);
 428         }
 429         if (horizontal) {
 430             return minWidth + leftInset + rightInset;
 431         } else {
 432             return maxMinWidth + leftInset + rightInset;
 433         }
 434     }
 435 
 436     /** {@inheritDoc} */
 437     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 438         double minHeight = 0;
 439         double maxMinHeight = 0;
 440         for (Content c: contentRegions) {
 441             minHeight += c.minHeight(-1);
 442             maxMinHeight = Math.max(maxMinHeight, c.minHeight(-1));
 443         }
 444         for (ContentDivider d: contentDividers) {
 445             minHeight += d.prefWidth(-1);
 446         }
 447         if (horizontal) {
 448             return maxMinHeight + topInset + bottomInset;
 449         } else {
 450             return minHeight + topInset + bottomInset;
 451         }
 452     }
 453 
 454     /** {@inheritDoc} */
 455     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 456         double prefWidth = 0;
 457         double prefMaxWidth = 0;
 458         for (Content c: contentRegions) {
 459             prefWidth += c.prefWidth(-1);
 460             prefMaxWidth = Math.max(prefMaxWidth, c.prefWidth(-1));
 461         }
 462         for (ContentDivider d: contentDividers) {
 463             prefWidth += d.prefWidth(-1);
 464         }
 465         if (horizontal) {
 466             return prefWidth + leftInset + rightInset;
 467         } else {
 468             return prefMaxWidth + leftInset + rightInset;
 469         }
 470     }
 471 
 472     /** {@inheritDoc} */
 473     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 474         double prefHeight = 0;
 475         double maxPrefHeight = 0;
 476         for (Content c: contentRegions) {
 477             prefHeight += c.prefHeight(-1);
 478             maxPrefHeight = Math.max(maxPrefHeight, c.prefHeight(-1));
 479         }
 480         for (ContentDivider d: contentDividers) {
 481             prefHeight += d.prefWidth(-1);
 482         }
 483         if (horizontal) {
 484             return maxPrefHeight + topInset + bottomInset;
 485         } else {
 486             return prefHeight + topInset + bottomInset;
 487         }
 488     }
 489 
 490 
 491 
 492     /***************************************************************************
 493      *                                                                         *
 494      * Private implementation                                                  *
 495      *                                                                         *
 496      **************************************************************************/
 497 
 498     private void addContent(int index, Node n) {
 499         Content c = new Content(n);
 500         contentRegions.add(index, c);
 501         getChildren().add(index, c);
 502     }
 503 
 504     private void removeContent(Node n) {
 505         for (Content c: contentRegions) {
 506             if (c.getContent().equals(n)) {
 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 = snapSize(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 = snapSize(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         @Override protected double computeMaxWidth(double height) {
1194             return snapSize(content.maxWidth(height));
1195         }
1196 
1197         @Override protected double computeMaxHeight(double width) {
1198             return snapSize(content.maxHeight(width));
1199         }
1200     }
1201 }
1202