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