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