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