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