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