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