/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
******************************************************************************/
package com.badlogic.gdx.scenes.scene2d.ui;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.scenes.scene2d.utils.Layout;
import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack;
import com.badlogic.gdx.utils.GdxRuntimeException;
/**
* A container that contains two widgets and is divided either horizontally or vertically. The user may resize the
* widgets. The child widgets are always sized to fill their half of the splitpane.
* <p>
* The preferred size of a splitpane is that of the child widgets and the size of the {@link SplitPaneStyle#handle}. The
* widgets are sized depending on the splitpane's size and the {@link #setSplitAmount(float) split position}.
*
* @author mzechner
* @author Nathan Sweet
*/
public class SplitPane extends WidgetGroup {
SplitPaneStyle style;
private Actor firstWidget, secondWidget;
boolean vertical;
float splitAmount = 0.5f, minAmount, maxAmount = 1;
private float oldSplitAmount;
private Rectangle firstWidgetBounds = new Rectangle();
private Rectangle secondWidgetBounds = new Rectangle();
Rectangle handleBounds = new Rectangle();
private Rectangle firstScissors = new Rectangle();
private Rectangle secondScissors = new Rectangle();
Vector2 lastPoint = new Vector2();
Vector2 handlePosition = new Vector2();
/**
* @param firstWidget
* May be null.
* @param secondWidget
* May be null.
*/
public SplitPane(Actor firstWidget, Actor secondWidget, boolean vertical, Skin skin) {
this(firstWidget, secondWidget, vertical, skin, "default-" + (vertical ? "vertical" : "horizontal"));
}
/**
* @param firstWidget
* May be null.
* @param secondWidget
* May be null.
*/
public SplitPane(Actor firstWidget, Actor secondWidget, boolean vertical, Skin skin, String styleName) {
this(firstWidget, secondWidget, vertical, skin.get(styleName, SplitPaneStyle.class));
}
/**
* @param firstWidget
* May be null.
* @param secondWidget
* May be null.
*/
public SplitPane(Actor firstWidget, Actor secondWidget, boolean vertical, SplitPaneStyle style) {
this.firstWidget = firstWidget;
this.secondWidget = secondWidget;
this.vertical = vertical;
setStyle(style);
setFirstWidget(firstWidget);
setSecondWidget(secondWidget);
setWidth(getPrefWidth());
setHeight(getPrefHeight());
initialize();
}
private void initialize() {
addListener(new InputListener() {
int draggingPointer = -1;
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
if (draggingPointer != -1)
return false;
if (pointer == 0 && button != 0)
return false;
if (handleBounds.contains(x, y)) {
draggingPointer = pointer;
lastPoint.set(x, y);
handlePosition.set(handleBounds.x, handleBounds.y);
return true;
}
return false;
}
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
if (pointer == draggingPointer)
draggingPointer = -1;
}
public void touchDragged(InputEvent event, float x, float y, int pointer) {
if (pointer != draggingPointer)
return;
Drawable handle = style.handle;
if (!vertical) {
float delta = x - lastPoint.x;
float availWidth = getWidth() - handle.getMinWidth();
float dragX = handlePosition.x + delta;
handlePosition.x = dragX;
dragX = Math.max(0, dragX);
dragX = Math.min(availWidth, dragX);
splitAmount = dragX / availWidth;
if (splitAmount < minAmount)
splitAmount = minAmount;
if (splitAmount > maxAmount)
splitAmount = maxAmount;
lastPoint.set(x, y);
} else {
float delta = y - lastPoint.y;
float availHeight = getHeight() - handle.getMinHeight();
float dragY = handlePosition.y + delta;
handlePosition.y = dragY;
dragY = Math.max(0, dragY);
dragY = Math.min(availHeight, dragY);
splitAmount = 1 - (dragY / availHeight);
if (splitAmount < minAmount)
splitAmount = minAmount;
if (splitAmount > maxAmount)
splitAmount = maxAmount;
lastPoint.set(x, y);
}
invalidate();
}
});
}
public void setStyle(SplitPaneStyle style) {
this.style = style;
invalidateHierarchy();
}
/**
* Returns the split pane's style. Modifying the returned style may not have an effect until
* {@link #setStyle(SplitPaneStyle)} is called.
*/
public SplitPaneStyle getStyle() {
return style;
}
@Override
public void layout() {
if (!vertical)
calculateHorizBoundsAndPositions();
else
calculateVertBoundsAndPositions();
Actor firstWidget = this.firstWidget;
Rectangle firstWidgetBounds = this.firstWidgetBounds;
if (firstWidget != null) {
firstWidget.setX(firstWidgetBounds.x);
firstWidget.setY(firstWidgetBounds.y);
if (firstWidget.getWidth() != firstWidgetBounds.width
|| firstWidget.getHeight() != firstWidgetBounds.height) {
firstWidget.setWidth(firstWidgetBounds.width);
firstWidget.setHeight(firstWidgetBounds.height);
if (firstWidget instanceof Layout) {
Layout layout = (Layout) firstWidget;
layout.invalidate();
layout.validate();
}
} else {
if (firstWidget instanceof Layout)
((Layout) firstWidget).validate();
}
}
Actor secondWidget = this.secondWidget;
Rectangle secondWidgetBounds = this.secondWidgetBounds;
if (secondWidget != null) {
secondWidget.setX(secondWidgetBounds.x);
secondWidget.setY(secondWidgetBounds.y);
if (secondWidget.getWidth() != secondWidgetBounds.width
|| secondWidget.getHeight() != secondWidgetBounds.height) {
secondWidget.setWidth(secondWidgetBounds.width);
secondWidget.setHeight(secondWidgetBounds.height);
if (secondWidget instanceof Layout) {
Layout layout = (Layout) secondWidget;
layout.invalidate();
layout.validate();
}
} else {
if (secondWidget instanceof Layout)
((Layout) secondWidget).validate();
}
}
}
@Override
public float getPrefWidth() {
float width = firstWidget instanceof Layout ? ((Layout) firstWidget).getPrefWidth() : firstWidget.getWidth();
width += secondWidget instanceof Layout ? ((Layout) secondWidget).getPrefWidth() : secondWidget.getWidth();
if (!vertical)
width += style.handle.getMinWidth();
return width;
}
@Override
public float getPrefHeight() {
float height = firstWidget instanceof Layout ? ((Layout) firstWidget).getPrefHeight() : firstWidget.getHeight();
height += secondWidget instanceof Layout ? ((Layout) secondWidget).getPrefHeight() : secondWidget.getHeight();
if (vertical)
height += style.handle.getMinHeight();
return height;
}
public float getMinWidth() {
return 0;
}
public float getMinHeight() {
return 0;
}
public void setVertical(boolean vertical) {
this.vertical = vertical;
}
private void calculateHorizBoundsAndPositions() {
Drawable handle = style.handle;
float height = getHeight();
float availWidth = getWidth() - handle.getMinWidth();
float leftAreaWidth = (int) (availWidth * splitAmount);
float rightAreaWidth = availWidth - leftAreaWidth;
float handleWidth = handle.getMinWidth();
firstWidgetBounds.set(0, 0, leftAreaWidth, height);
secondWidgetBounds.set(leftAreaWidth + handleWidth, 0, rightAreaWidth, height);
handleBounds.set(leftAreaWidth, 0, handleWidth, height);
}
private void calculateVertBoundsAndPositions() {
Drawable handle = style.handle;
float width = getWidth();
float height = getHeight();
float availHeight = height - handle.getMinHeight();
float topAreaHeight = (int) (availHeight * splitAmount);
float bottomAreaHeight = availHeight - topAreaHeight;
float handleHeight = handle.getMinHeight();
firstWidgetBounds.set(0, height - topAreaHeight, width, topAreaHeight);
secondWidgetBounds.set(0, 0, width, bottomAreaHeight);
handleBounds.set(0, bottomAreaHeight, width, handleHeight);
}
@Override
public void draw(SpriteBatch batch, float parentAlpha) {
validate();
Color color = getColor();
Drawable handle = style.handle;
applyTransform(batch, computeTransform());
Matrix4 transform = batch.getTransformMatrix();
if (firstWidget != null) {
ScissorStack.calculateScissors(getStage().getCamera(), transform, firstWidgetBounds, firstScissors);
if (ScissorStack.pushScissors(firstScissors)) {
if (firstWidget.isVisible())
firstWidget.draw(batch, parentAlpha * color.a);
batch.flush();
ScissorStack.popScissors();
}
}
if (secondWidget != null) {
ScissorStack.calculateScissors(getStage().getCamera(), transform, secondWidgetBounds, secondScissors);
if (ScissorStack.pushScissors(secondScissors)) {
if (secondWidget.isVisible())
secondWidget.draw(batch, parentAlpha * color.a);
batch.flush();
ScissorStack.popScissors();
}
}
batch.setColor(color.r, color.g, color.b, color.a);
handle.draw(batch, handleBounds.x, handleBounds.y, handleBounds.width, handleBounds.height);
resetTransform(batch);
}
/**
* @param split
* The split amount between the min and max amount.
*/
public void setSplitAmount(float split) {
this.splitAmount = Math.max(Math.min(maxAmount, split), minAmount);
invalidate();
}
public float getSplit() {
return splitAmount;
}
public void setMinSplitAmount(float minAmount) {
if (minAmount < 0)
throw new GdxRuntimeException("minAmount has to be >= 0");
if (minAmount >= maxAmount)
throw new GdxRuntimeException("minAmount has to be < maxAmount");
this.minAmount = minAmount;
}
public void setMaxSplitAmount(float maxAmount) {
if (maxAmount > 1)
throw new GdxRuntimeException("maxAmount has to be >= 0");
if (maxAmount <= minAmount)
throw new GdxRuntimeException("maxAmount has to be > minAmount");
this.maxAmount = maxAmount;
}
/**
* @param widget
* May be null.
*/
public void setFirstWidget(Actor widget) {
if (firstWidget != null)
super.removeActor(firstWidget);
firstWidget = widget;
if (widget != null)
super.addActor(widget);
invalidate();
}
/**
* @param widget
* May be null.
*/
public void setSecondWidget(Actor widget) {
if (secondWidget != null)
super.removeActor(secondWidget);
secondWidget = widget;
if (widget != null)
super.addActor(widget);
invalidate();
}
public void addActor(Actor actor) {
throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
}
public void addActorAt(int index, Actor actor) {
throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
}
public void addActorBefore(Actor actorBefore, Actor actor) {
throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
}
public boolean removeActor(Actor actor) {
throw new UnsupportedOperationException("Use ScrollPane#setWidget(null).");
}
/**
* The style for a splitpane, see {@link SplitPane}.
*
* @author mzechner
* @author Nathan Sweet
*/
static public class SplitPaneStyle {
public Drawable handle;
public SplitPaneStyle() {
}
public SplitPaneStyle(Drawable handle) {
this.handle = handle;
}
public SplitPaneStyle(SplitPaneStyle style) {
this.handle = style.handle;
}
}
}