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