blob: fbe8318d9efa794d27c576804eaeb80236344505 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 BestSolution.at and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
*******************************************************************************/
package org.eclipse.fx.ui.panes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.SimpleStyleableIntegerProperty;
import javafx.css.StyleConverter;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
/**
* <p>
* A layout container who lays out its children in a row or column arrangement
* (as specified by the orientation) and places a Sash between each child.
* </p>
* <p>
* One child may be maximized to occupy the entire size of the {@link SashPane}
* </p>
* <p>
* The relative sizes of the children may be specified using weights
* </p>
*
* @since 2.6.0
*/
public class SashPane extends Region {
private static final String USER_AGENT_STYLESHEET = SashPane.class.getResource("sash-pane.css").toExternalForm(); //$NON-NLS-1$
private static final String LAYOUT_KEY = "sashData"; //$NON-NLS-1$
static final int DRAG_MINIMUM = 20;
private Sash[] sashes = new Sash[0];
private BooleanProperty horizontal = new SimpleBooleanProperty(this, "horizontal", true) {//$NON-NLS-1$
@Override
protected void invalidated() {
super.invalidated();
clearCache();
}
};
private ObjectProperty<Node> maximizedControl = new SimpleObjectProperty<Node>(this, "maximizedControl") {//$NON-NLS-1$
@Override
protected void invalidated() {
super.invalidated();
clearCache();
}
};
private static final CssMetaData<SashPane, Number> SASH_WIDTH = new CssMetaData<SashPane, Number>("-fx-sash-width", //$NON-NLS-1$
StyleConverter.getSizeConverter(), Integer.valueOf(8)) {
@Override
public boolean isSettable(SashPane node) {
return !node.sashWidthProperty().isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty<Number> getStyleableProperty(SashPane node) {
return (StyleableProperty<Number>) node.sashWidthProperty();
}
};
@NonNull
private final IntegerProperty sashWidth = new SimpleStyleableIntegerProperty(SASH_WIDTH, this, "sashWidth", //$NON-NLS-1$
Integer.valueOf(8));
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Region.getClassCssMetaData());
styleables.add(SASH_WIDTH);
STYLEABLES = Collections.unmodifiableList(styleables);
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return STYLEABLES;
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
private ObservableList<Node> items = FXCollections.observableArrayList();
private Sash draggedSash;
private Node c1;
private Node c2;
private double resize_1;
private double resize_2;
private double start;
private Bounds clientArea;
private double resize_total;
private static PseudoClass HORIZONTAL = PseudoClass.getPseudoClass("horizontal"); //$NON-NLS-1$
private static PseudoClass VERTICAL = PseudoClass.getPseudoClass("vertical"); //$NON-NLS-1$
/**
* Create a new sash pane
*/
public SashPane() {
getStyleClass().add("sash-pane"); //$NON-NLS-1$
this.items.addListener((Change<? extends Node> c) -> {
while (c.next()) {
if (c.wasRemoved()) {
getChildren().removeIf(n -> n instanceof SashChild
&& c.getRemoved().contains(((SashChild) n).getChildren().get(0)));
}
if (c.wasAdded()) {
getChildren().addAll(c.getFrom(),
c.getAddedSubList().stream().map(SashChild::new).collect(Collectors.toList()));
}
}
clearCache();
});
this.sashWidth.addListener( o -> {
clearCache();
requestLayout();
} );
pseudoClassStateChanged(HORIZONTAL, true);
pseudoClassStateChanged(VERTICAL, false);
this.horizontal.addListener((o, ol, ne) -> {
pseudoClassStateChanged(HORIZONTAL, ne.booleanValue());
pseudoClassStateChanged(VERTICAL, !ne.booleanValue());
});
}
@Override
public String getUserAgentStylesheet() {
return USER_AGENT_STYLESHEET;
}
/**
* @return children of the {@link SashPane}
*/
public ObservableList<Node> getItems() {
return this.items;
}
void clearCache() {
this.clientArea = null;
}
@Override
protected void layoutChildren() {
int x = (int) Math.ceil(getPadding().getLeft());
int y = (int) Math.ceil(getPadding().getTop());
int w = (int) Math.floor(getWidth()) - (int) Math.ceil(getPadding().getLeft())
- (int) Math.ceil(getPadding().getRight());
int h = (int) Math.floor(getHeight()) - (int) Math.ceil(getPadding().getTop())
- (int) Math.ceil(getPadding().getBottom());
Bounds b = new BoundingBox(x, y, w, h);
if (this.clientArea != null && this.clientArea.equals(b)) {
return;
}
if (w <= 1 || h <= 1) {
return;
}
List<Node> children = getManagedChildren().stream().filter(Node::isVisible).collect(Collectors.toList());
if (children.isEmpty()) {
return;
}
if (this.maximizedControl.get() != null && children.contains(this.maximizedControl.get())) {
for (int i = 0; i < children.size(); i++) {
if (children.get(i) != this.maximizedControl.get()) {
children.get(i).resizeRelocate(0, 0, 0, 0);
} else {
children.get(i).resizeRelocate(x, y, h, w);
}
}
return;
}
// TODO Adjust sash numbers
long[] ratios = new long[children.size()];
long total = 0;
for (int i = 0; i < children.size(); i++) {
Object data = children.get(i).getProperties().get(LAYOUT_KEY);
if (data != null && data instanceof SashFormData) {
ratios[i] = ((SashFormData) data).weight;
} else {
data = new SashFormData();
children.get(i).getProperties().put(LAYOUT_KEY, data);
((SashFormData) data).weight = ratios[i] = ((200 << 16) + 999) / 1000;
}
total += ratios[i];
}
if (this.sashes.length < children.size() - 1) {
Sash[] newSashes = new Sash[children.size() - 1];
System.arraycopy(this.sashes, 0, newSashes, 0, this.sashes.length);
for (int i = this.sashes.length; i < newSashes.length; i++) {
newSashes[i] = createSash();
}
this.sashes = newSashes;
}
if (this.sashes.length > children.size() - 1) {
if (children.size() == 0) {
for (int i = 0; i < this.sashes.length; i++) {
getChildren().remove(this.sashes[i]);
}
this.sashes = new Sash[0];
} else {
Sash[] newSashes = new Sash[children.size() - 1];
System.arraycopy(this.sashes, 0, newSashes, 0, newSashes.length);
for (int i = children.size() - 1; i < this.sashes.length; i++) {
getChildren().remove(this.sashes[i]);
}
this.sashes = newSashes;
}
}
// int sashwidth = 10; //TODO sashes.length > 0 ? sashForm.SASH_WIDTH +
// sashes [0].getBorderWidth() * 2 : sashForm.SASH_WIDTH;
if (this.horizontal.get()) {
int width = (int) (ratios[0] * (w - this.sashes.length * getSashWidth()) / total);
children.get(0).resizeRelocate(x, y, width, h);
x += width;
for (int i = 1; i < children.size() - 1; i++) {
this.sashes[i - 1].resizeRelocate(x, y, getSashWidth(), h);
x += getSashWidth();
width = (int) (ratios[i] * (w - this.sashes.length * getSashWidth()) / total);
children.get(i).resizeRelocate(x, y, width, h);
x += width;
}
if (children.size() > 1) {
this.sashes[this.sashes.length - 1].resizeRelocate(x, y, getSashWidth(), h);
x += getSashWidth();
width = w - x;
children.get(children.size() - 1).resizeRelocate(x, y, width, h);
}
} else {
int height = (int) (ratios[0] * (h - this.sashes.length * getSashWidth()) / total);
children.get(0).resizeRelocate(x, y, w, height);
y += height;
for (int i = 1; i < children.size() - 1; i++) {
this.sashes[i - 1].resizeRelocate(x, y, w, getSashWidth());
y += getSashWidth();
height = (int) (ratios[i] * (h - this.sashes.length * getSashWidth()) / total);
children.get(i).resizeRelocate(x, y, w, height);
y += height;
}
if (children.size() > 1) {
this.sashes[this.sashes.length - 1].resizeRelocate(x, y, w, getSashWidth());
y += getSashWidth();
height = h - y;
children.get(children.size() - 1).resizeRelocate(x, y, w, height);
}
}
this.clientArea = b;
}
private void handleDragSash(MouseEvent e) {
Rectangle rectangle = new Rectangle(getWidth(), getHeight(), getInsets());
if (this.horizontal.get()) {
double delta = e.getScreenX() - this.start;
long newSize_1 = (long) (this.resize_1 + delta);
long newSize_2 = (long) (this.resize_2 - delta);
if (newSize_1 < DRAG_MINIMUM) {
newSize_2 = (long) this.resize_total - DRAG_MINIMUM;
newSize_1 = DRAG_MINIMUM;
}
if (newSize_2 < DRAG_MINIMUM) {
newSize_1 = (long) this.resize_total - DRAG_MINIMUM;
newSize_2 = DRAG_MINIMUM;
}
Object data1 = this.c1.getProperties().get(LAYOUT_KEY);
if (data1 == null || !(data1 instanceof SashFormData)) {
data1 = new SashFormData();
this.c1.getProperties().put(LAYOUT_KEY, data1);
}
Object data2 = this.c2.getProperties().get(LAYOUT_KEY);
if (data2 == null || !(data2 instanceof SashFormData)) {
data2 = new SashFormData();
this.c2.getProperties().put(LAYOUT_KEY, data2);
}
((SashFormData) data1).weight = (((long) newSize_1 << 16) + rectangle.width - 1) / rectangle.width;
((SashFormData) data2).weight = (((long) newSize_2 << 16) + rectangle.width - 1) / rectangle.width;
this.c1.resize(newSize_1, rectangle.height);
this.c2.resizeRelocate(this.c1.getLayoutX() + newSize_1 + getSashWidth(), this.c2.getLayoutY(), newSize_2,
rectangle.height);
this.draggedSash.relocate(this.c1.getLayoutX() + newSize_1, this.draggedSash.getLayoutY());
} else {
double delta = e.getScreenY() - this.start;
long newSize_1 = (long) (this.resize_1 + delta);
long newSize_2 = (long) (this.resize_2 - delta);
Object data1 = this.c1.getProperties().get(LAYOUT_KEY);
if (data1 == null || !(data1 instanceof SashFormData)) {
data1 = new SashFormData();
this.c1.getProperties().put(LAYOUT_KEY, data1);
}
Object data2 = this.c2.getProperties().get(LAYOUT_KEY);
if (data2 == null || !(data2 instanceof SashFormData)) {
data2 = new SashFormData();
this.c2.getProperties().put(LAYOUT_KEY, data2);
}
((SashFormData) data1).weight = (((long) newSize_1 << 16) + rectangle.height - 1) / rectangle.height;
((SashFormData) data2).weight = (((long) newSize_2 << 16) + rectangle.height - 1) / rectangle.height;
this.c1.resize(rectangle.width, newSize_1);
this.c2.resizeRelocate(this.c2.getLayoutX(), this.c1.getLayoutY() + newSize_1 + getSashWidth(),
rectangle.width, newSize_2);
this.draggedSash.relocate(this.draggedSash.getLayoutX(), this.c1.getLayoutY() + newSize_1);
}
requestLayout();
}
private void handlePressedSash(MouseEvent e) {
Sash sash = (Sash) e.getSource();
int sashIndex = -1;
for (int i = 0; i < this.sashes.length; i++) {
if (this.sashes[i] == sash) {
sashIndex = i;
break;
}
}
if (sashIndex == -1)
return;
this.start = this.horizontal.get() ? e.getScreenX() : e.getScreenY();
this.draggedSash = sash;
this.c1 = getManagedChildren().get(sashIndex);
Bounds b = this.c1.getLayoutBounds();
this.resize_1 = this.horizontal.get() ? b.getWidth() : b.getHeight();
this.c2 = getManagedChildren().get(sashIndex + 1);
b = this.c2.getLayoutBounds();
this.resize_2 = this.horizontal.get() ? b.getWidth() : b.getHeight();
this.resize_total = this.resize_1 + this.resize_2;
}
private Sash createSash() {
Sash sash = new Sash();
sash.setOnMousePressed(this::handlePressedSash);
sash.setOnMouseDragged(this::handleDragSash);
getChildren().add(sash);
return sash;
}
/**
* <p>
* Specify the relative weight of each child in the {@link SashPane}. This
* will determine what percent of the total width (if {@link SashPane} has
* Horizontal orientation) or total height (if {@link SashPane} has Vertical
* orientation) each control will occupy.
* </p>
* <p>
* The weights must be positive values and there must be an entry for each
* non-sash child of the SashForm.
* </p>
*
* @param weights
*/
public void setWeights(int[] weights) {
List<Node> children = getManagedChildren();
if (weights == null || weights.length != children.size()) {
throw new IllegalArgumentException();
}
int total = 0;
for (int i = 0; i < weights.length; i++) {
if (weights[i] < 0) {
throw new IllegalArgumentException();
}
total += weights[i];
}
if (total == 0) {
throw new IllegalArgumentException();
}
for (int i = 0; i < children.size(); i++) {
Object data = children.get(i).getProperties().get(LAYOUT_KEY);
if (data == null || !(data instanceof SashFormData)) {
data = new SashFormData();
children.get(i).getProperties().put(LAYOUT_KEY, data);
}
((SashFormData) data).weight = (((long) weights[i] << 16) + total - 1) / total;
}
clearCache();
requestLayout();
}
/**
* <p>
* Answer the relative weight of each child in the SashForm.
* </p>
* <p>
* The weight represents the percent of the total width (if SashForm has
* Horizontal orientation) or total height (if SashForm has Vertical
* orientation) each control occupies.
* </p>
* <p>
* The weights are returned in order of the creation of the widgets
* (weight[0] corresponds to the weight of the first child created).
* </p>
*
* @return the relative weight of each child
*/
public int[] getWeights() {
List<Node> children = getManagedChildren();
int[] ratios = new int[children.size()];
for (int i = 0; i < children.size(); i++) {
Object data = children.get(i).getProperties().get(LAYOUT_KEY);
if (data != null && data instanceof SashFormData) {
ratios[i] = (int) (((SashFormData) data).weight * 1000 >> 16);
} else {
ratios[i] = 200;
}
}
return ratios;
}
/**
* @return Property defining if children are layed out in columns
* (<code>true</code>) or rows (<code>false</code>)
*/
public final BooleanProperty horizontalProperty() {
return this.horizontal;
}
/**
* @return <code>true</code> if children layed out in columns
*/
public final boolean isHorizontal() {
return this.horizontalProperty().get();
}
/**
* Change the layout of children to columns (<code>true</code>) or rows
* (<code>false</code>)
*
* @param horizontal
*/
public final void setHorizontal(final boolean horizontal) {
this.horizontalProperty().set(horizontal);
}
// public final ObjectProperty<Node> maximizedControlProperty() {
// return this.maximizedControl;
// }
//
// public final Node getMaximizedControl() {
// return this.maximizedControlProperty().get();
// }
//
// public final void setMaximizedControl(final Node maximizedControl) {
// this.maximizedControlProperty().set(maximizedControl);
// }
/**
* @return width of the sash shown between items
*/
public final IntegerProperty sashWidthProperty() {
return this.sashWidth;
}
/**
* @return the width of a sash
*/
public final int getSashWidth() {
return this.sashWidthProperty().get();
}
/**
* Set the width of a sash
*
* @param sashWidth
* the width
*/
public final void setSashWidth(final int sashWidth) {
this.sashWidthProperty().set(sashWidth);
}
static class SashFormData {
public long weight;
}
static class Sash extends Region {
public Sash() {
getStyleClass().add("sash"); //$NON-NLS-1$
// setStyle("-fx-background-color: black;");
setManaged(false);
setMouseTransparent(false);
}
}
static class SashChild extends StackPane {
private javafx.scene.shape.Rectangle r = new javafx.scene.shape.Rectangle();
public SashChild(Node c) {
getChildren().add(c);
setClip(this.r);
}
@Override
public void resize(double width, double height) {
super.resize(width, height);
this.r.setWidth(width);
this.r.setHeight(height);
}
}
static class Rectangle {
int x;
int y;
int width;
int height;
public Rectangle(double width, double height, Insets insets) {
this.x = (int) Math.ceil(insets.getLeft());
this.y = (int) Math.ceil(insets.getTop());
this.width = (int) Math.floor(width) - (int) Math.ceil(insets.getLeft())
- (int) Math.ceil(insets.getRight());
this.height = (int) Math.floor(height) - (int) Math.ceil(insets.getTop())
- (int) Math.ceil(insets.getBottom());
}
public Rectangle(Bounds b) {
this.x = (int) Math.ceil(b.getMinX());
this.y = (int) Math.ceil(b.getMinY());
this.width = (int) Math.floor(b.getWidth());
this.height = (int) Math.floor(b.getHeight());
}
}
}