/******************************************************************************* * 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.Batch; 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 side of the SplitPane. * <p> * Minimum and maximum split amounts can be set to limit the motion of the resizing handle. The handle position is also prevented * from shrinking the children below their minimum sizes. If these limits over-constrain the handle, it will be locked and placed * at an averaged location, resulting in cropped children. The minimum child size can be ignored (allowing dynamic cropping) by * wrapping the child in a {@linkplain Container} with a minimum size of 0 and {@linkplain Container#fill() fill()} set, or by * overriding {@link #clampSplitAmount()}. * <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 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 Rectangle firstWidgetBounds = new Rectangle(); private Rectangle secondWidgetBounds = new Rectangle(); Rectangle handleBounds = new Rectangle(); private Rectangle tempScissors = 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.vertical = vertical; setStyle(style); setFirstWidget(firstWidget); setSecondWidget(secondWidget); setSize(getPrefWidth(), 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; 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); 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 () { clampSplitAmount(); if (!vertical) calculateHorizBoundsAndPositions(); else calculateVertBoundsAndPositions(); Actor firstWidget = this.firstWidget; if (firstWidget != null) { Rectangle firstWidgetBounds = this.firstWidgetBounds; firstWidget.setBounds(firstWidgetBounds.x, firstWidgetBounds.y, firstWidgetBounds.width, firstWidgetBounds.height); if (firstWidget instanceof Layout) ((Layout)firstWidget).validate(); } Actor secondWidget = this.secondWidget; if (secondWidget != null) { Rectangle secondWidgetBounds = this.secondWidgetBounds; secondWidget.setBounds(secondWidgetBounds.x, secondWidgetBounds.y, secondWidgetBounds.width, secondWidgetBounds.height); if (secondWidget instanceof Layout) ((Layout)secondWidget).validate(); } } @Override public float getPrefWidth () { float first = firstWidget == null ? 0 : (firstWidget instanceof Layout ? ((Layout)firstWidget).getPrefWidth() : firstWidget.getWidth()); float second = secondWidget == null ? 0 : (secondWidget instanceof Layout ? ((Layout)secondWidget).getPrefWidth() : secondWidget.getWidth()); if (vertical) return Math.max(first, second); return first + style.handle.getMinWidth() + second; } @Override public float getPrefHeight () { float first = firstWidget == null ? 0 : (firstWidget instanceof Layout ? ((Layout)firstWidget).getPrefHeight() : firstWidget.getHeight()); float second = secondWidget == null ? 0 : (secondWidget instanceof Layout ? ((Layout)secondWidget).getPrefHeight() : secondWidget.getHeight()); if (!vertical) return Math.max(first, second); return first + style.handle.getMinHeight() + second; } public float getMinWidth () { float first = firstWidget instanceof Layout ? ((Layout)firstWidget).getMinWidth() : 0; float second = secondWidget instanceof Layout ? ((Layout)secondWidget).getMinWidth() : 0; if (vertical) return Math.max(first, second); return first + style.handle.getMinWidth() + second; } public float getMinHeight () { float first = firstWidget instanceof Layout ? ((Layout)firstWidget).getMinHeight() : 0; float second = secondWidget instanceof Layout ? ((Layout)secondWidget).getMinHeight() : 0; if (!vertical) return Math.max(first, second); return first + style.handle.getMinHeight() + second; } public void setVertical (boolean vertical) { if (this.vertical == vertical) return; this.vertical = vertical; invalidateHierarchy(); } public boolean isVertical () { return 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 (Batch batch, float parentAlpha) { validate(); Color color = getColor(); float alpha = color.a * parentAlpha; applyTransform(batch, computeTransform()); if (firstWidget != null && firstWidget.isVisible()) { batch.flush(); getStage().calculateScissors(firstWidgetBounds, tempScissors); if (ScissorStack.pushScissors(tempScissors)) { firstWidget.draw(batch, alpha); batch.flush(); ScissorStack.popScissors(); } } if (secondWidget != null && secondWidget.isVisible()) { batch.flush(); getStage().calculateScissors(secondWidgetBounds, tempScissors); if (ScissorStack.pushScissors(tempScissors)) { secondWidget.draw(batch, alpha); batch.flush(); ScissorStack.popScissors(); } } batch.setColor(color.r, color.g, color.b, alpha); style.handle.draw(batch, handleBounds.x, handleBounds.y, handleBounds.width, handleBounds.height); resetTransform(batch); } /** @param splitAmount The split amount between the min and max amount. This parameter is clamped during * layout. See {@link #clampSplitAmount()}.*/ public void setSplitAmount (float splitAmount) { this.splitAmount = splitAmount; // will be clamped during layout invalidate(); } public float getSplitAmount () { return splitAmount; } /** Called during layout to clamp the {@link #splitAmount} within the set limits. By default it imposes the limits of the * {@linkplain #getMinSplitAmount() min amount}, {@linkplain #getMaxSplitAmount() max amount}, and min sizes of the children. This * method is internally called in response to layout, so it should not call {@link #invalidate()}. */ protected void clampSplitAmount () { float effectiveMinAmount = minAmount, effectiveMaxAmount = maxAmount; if (vertical){ float availableHeight = getHeight() - style.handle.getMinHeight(); if (firstWidget instanceof Layout) effectiveMinAmount = Math.max(effectiveMinAmount, Math.min(((Layout)firstWidget).getMinHeight() / availableHeight, 1)); if (secondWidget instanceof Layout) effectiveMaxAmount = Math.min(effectiveMaxAmount, 1 - Math.min(((Layout)secondWidget).getMinHeight() / availableHeight, 1)); } else { float availableWidth = getWidth() - style.handle.getMinWidth(); if (firstWidget instanceof Layout) effectiveMinAmount = Math.max(effectiveMinAmount, Math.min(((Layout)firstWidget).getMinWidth() / availableWidth, 1)); if (secondWidget instanceof Layout) effectiveMaxAmount = Math.min(effectiveMaxAmount, 1 - Math.min(((Layout)secondWidget).getMinWidth() / availableWidth, 1)); } if (effectiveMinAmount > effectiveMaxAmount) // Locked handle. Average the position. splitAmount = 0.5f * (effectiveMinAmount + effectiveMaxAmount); else splitAmount = Math.max(Math.min(splitAmount, effectiveMaxAmount), effectiveMinAmount); } public float getMinSplitAmount () { return minAmount; } public void setMinSplitAmount (float minAmount) { if (minAmount < 0 || minAmount > 1) throw new GdxRuntimeException("minAmount has to be >= 0 and <= 1"); this.minAmount = minAmount; } public float getMaxSplitAmount () { return maxAmount; } public void setMaxSplitAmount (float maxAmount) { if (maxAmount < 0 || maxAmount > 1) throw new GdxRuntimeException("maxAmount has to be >= 0 and <= 1"); 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 SplitPane#setWidget."); } public void addActorAt (int index, Actor actor) { throw new UnsupportedOperationException("Use SplitPane#setWidget."); } public void addActorBefore (Actor actorBefore, Actor actor) { throw new UnsupportedOperationException("Use SplitPane#setWidget."); } public boolean removeActor (Actor actor) { if (actor == null) throw new IllegalArgumentException("actor cannot be null."); if (actor == firstWidget) { setFirstWidget(null); return true; } if (actor == secondWidget) { setSecondWidget(null); return true; } return true; } public boolean removeActor (Actor actor, boolean unfocus) { if (actor == null) throw new IllegalArgumentException("actor cannot be null."); if (actor == firstWidget) { super.removeActor(actor, unfocus); firstWidget = null; invalidate(); return true; } if (actor == secondWidget) { super.removeActor(actor, unfocus); secondWidget = null; invalidate(); return true; } return false; } /** 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; } } }