/*******************************************************************************
* 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.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
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.Stage;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.utils.Align;
/** A table that can be dragged and act as a modal window. The top padding is used as the window's title height.
* <p>
* The preferred size of a window is the preferred size of the title text and the children as laid out by the table. After adding
* children to the window, it can be convenient to call {@link #pack()} to size the window to the size of the children.
* @author Nathan Sweet */
public class Window extends Table {
static private final Vector2 tmpPosition = new Vector2();
static private final Vector2 tmpSize = new Vector2();
static private final int MOVE = 1 << 5;
private WindowStyle style;
boolean isMovable = true, isModal, isResizable;
int resizeBorder = 8;
boolean keepWithinStage = true;
Label titleLabel;
Table titleTable;
boolean drawTitleTable;
protected int edge;
protected boolean dragging;
public Window (String title, Skin skin) {
this(title, skin.get(WindowStyle.class));
setSkin(skin);
}
public Window (String title, Skin skin, String styleName) {
this(title, skin.get(styleName, WindowStyle.class));
setSkin(skin);
}
public Window (String title, WindowStyle style) {
if (title == null) throw new IllegalArgumentException("title cannot be null.");
setTouchable(Touchable.enabled);
setClip(true);
titleLabel = new Label(title, new LabelStyle(style.titleFont, style.titleFontColor));
titleLabel.setEllipsis(true);
titleTable = new Table() {
public void draw (Batch batch, float parentAlpha) {
if (drawTitleTable) super.draw(batch, parentAlpha);
}
};
titleTable.add(titleLabel).expandX().fillX().minWidth(0);
addActor(titleTable);
setStyle(style);
setWidth(150);
setHeight(150);
addCaptureListener(new InputListener() {
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
toFront();
return false;
}
});
addListener(new InputListener() {
float startX, startY, lastX, lastY;
private void updateEdge (float x, float y) {
float border = resizeBorder / 2f;
float width = getWidth(), height = getHeight();
float padTop = getPadTop(), padLeft = getPadLeft(), padBottom = getPadBottom(), padRight = getPadRight();
float left = padLeft, right = width - padRight, bottom = padBottom;
edge = 0;
if (isResizable && x >= left - border && x <= right + border && y >= bottom - border) {
if (x < left + border) edge |= Align.left;
if (x > right - border) edge |= Align.right;
if (y < bottom + border) edge |= Align.bottom;
if (edge != 0) border += 25;
if (x < left + border) edge |= Align.left;
if (x > right - border) edge |= Align.right;
if (y < bottom + border) edge |= Align.bottom;
}
if (isMovable && edge == 0 && y <= height && y >= height - padTop && x >= left && x <= right) edge = MOVE;
}
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
if (button == 0) {
updateEdge(x, y);
dragging = edge != 0;
startX = x;
startY = y;
lastX = x - getWidth();
lastY = y - getHeight();
}
return edge != 0 || isModal;
}
public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
dragging = false;
}
public void touchDragged (InputEvent event, float x, float y, int pointer) {
if (!dragging) return;
float width = getWidth(), height = getHeight();
float windowX = getX(), windowY = getY();
float minWidth = getMinWidth(), maxWidth = getMaxWidth();
float minHeight = getMinHeight(), maxHeight = getMaxHeight();
Stage stage = getStage();
boolean clampPosition = keepWithinStage && getParent() == stage.getRoot();
if ((edge & MOVE) != 0) {
float amountX = x - startX, amountY = y - startY;
windowX += amountX;
windowY += amountY;
}
if ((edge & Align.left) != 0) {
float amountX = x - startX;
if (width - amountX < minWidth) amountX = -(minWidth - width);
if (clampPosition && windowX + amountX < 0) amountX = -windowX;
width -= amountX;
windowX += amountX;
}
if ((edge & Align.bottom) != 0) {
float amountY = y - startY;
if (height - amountY < minHeight) amountY = -(minHeight - height);
if (clampPosition && windowY + amountY < 0) amountY = -windowY;
height -= amountY;
windowY += amountY;
}
if ((edge & Align.right) != 0) {
float amountX = x - lastX - width;
if (width + amountX < minWidth) amountX = minWidth - width;
if (clampPosition && windowX + width + amountX > stage.getWidth()) amountX = stage.getWidth() - windowX - width;
width += amountX;
}
if ((edge & Align.top) != 0) {
float amountY = y - lastY - height;
if (height + amountY < minHeight) amountY = minHeight - height;
if (clampPosition && windowY + height + amountY > stage.getHeight())
amountY = stage.getHeight() - windowY - height;
height += amountY;
}
setBounds(Math.round(windowX), Math.round(windowY), Math.round(width), Math.round(height));
}
public boolean mouseMoved (InputEvent event, float x, float y) {
updateEdge(x, y);
return isModal;
}
public boolean scrolled (InputEvent event, float x, float y, int amount) {
return isModal;
}
public boolean keyDown (InputEvent event, int keycode) {
return isModal;
}
public boolean keyUp (InputEvent event, int keycode) {
return isModal;
}
public boolean keyTyped (InputEvent event, char character) {
return isModal;
}
});
}
public void setStyle (WindowStyle style) {
if (style == null) throw new IllegalArgumentException("style cannot be null.");
this.style = style;
setBackground(style.background);
titleLabel.setStyle(new LabelStyle(style.titleFont, style.titleFontColor));
invalidateHierarchy();
}
/** Returns the window's style. Modifying the returned style may not have an effect until {@link #setStyle(WindowStyle)} is
* called. */
public WindowStyle getStyle () {
return style;
}
void keepWithinStage () {
if (!keepWithinStage) return;
Stage stage = getStage();
Camera camera = stage.getCamera();
if (camera instanceof OrthographicCamera) {
OrthographicCamera orthographicCamera = (OrthographicCamera)camera;
float parentWidth = stage.getWidth();
float parentHeight = stage.getHeight();
if (getX(Align.right) - camera.position.x > parentWidth / 2 / orthographicCamera.zoom)
setPosition(camera.position.x + parentWidth / 2 / orthographicCamera.zoom, getY(Align.right), Align.right);
if (getX(Align.left) - camera.position.x < -parentWidth / 2 / orthographicCamera.zoom)
setPosition(camera.position.x - parentWidth / 2 / orthographicCamera.zoom, getY(Align.left), Align.left);
if (getY(Align.top) - camera.position.y > parentHeight / 2 / orthographicCamera.zoom)
setPosition(getX(Align.top), camera.position.y + parentHeight / 2 / orthographicCamera.zoom, Align.top);
if (getY(Align.bottom) - camera.position.y < -parentHeight / 2 / orthographicCamera.zoom)
setPosition(getX(Align.bottom), camera.position.y - parentHeight / 2 / orthographicCamera.zoom, Align.bottom);
} else if (getParent() == stage.getRoot()) {
float parentWidth = stage.getWidth();
float parentHeight = stage.getHeight();
if (getX() < 0) setX(0);
if (getRight() > parentWidth) setX(parentWidth - getWidth());
if (getY() < 0) setY(0);
if (getTop() > parentHeight) setY(parentHeight - getHeight());
}
}
public void draw (Batch batch, float parentAlpha) {
Stage stage = getStage();
if (stage.getKeyboardFocus() == null) stage.setKeyboardFocus(this);
keepWithinStage();
if (style.stageBackground != null) {
stageToLocalCoordinates(tmpPosition.set(0, 0));
stageToLocalCoordinates(tmpSize.set(stage.getWidth(), stage.getHeight()));
drawStageBackground(batch, parentAlpha, getX() + tmpPosition.x, getY() + tmpPosition.y, getX() + tmpSize.x,
getY() + tmpSize.y);
}
super.draw(batch, parentAlpha);
}
protected void drawStageBackground (Batch batch, float parentAlpha, float x, float y, float width, float height) {
Color color = getColor();
batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
style.stageBackground.draw(batch, x, y, width, height);
}
protected void drawBackground (Batch batch, float parentAlpha, float x, float y) {
super.drawBackground(batch, parentAlpha, x, y);
// Manually draw the title table before clipping is done.
titleTable.getColor().a = getColor().a;
float padTop = getPadTop(), padLeft = getPadLeft();
titleTable.setSize(getWidth() - padLeft - getPadRight(), padTop);
titleTable.setPosition(padLeft, getHeight() - padTop);
drawTitleTable = true;
titleTable.draw(batch, parentAlpha);
drawTitleTable = false; // Avoid drawing the title table again in drawChildren.
}
public Actor hit (float x, float y, boolean touchable) {
Actor hit = super.hit(x, y, touchable);
if (hit == null && isModal && (!touchable || getTouchable() == Touchable.enabled)) return this;
float height = getHeight();
if (hit == null || hit == this) return hit;
if (y <= height && y >= height - getPadTop() && x >= 0 && x <= getWidth()) {
// Hit the title bar, don't use the hit child if it is in the Window's table.
Actor current = hit;
while (current.getParent() != this)
current = current.getParent();
if (getCell(current) != null) return this;
}
return hit;
}
public boolean isMovable () {
return isMovable;
}
public void setMovable (boolean isMovable) {
this.isMovable = isMovable;
}
public boolean isModal () {
return isModal;
}
public void setModal (boolean isModal) {
this.isModal = isModal;
}
public void setKeepWithinStage (boolean keepWithinStage) {
this.keepWithinStage = keepWithinStage;
}
public boolean isResizable () {
return isResizable;
}
public void setResizable (boolean isResizable) {
this.isResizable = isResizable;
}
public void setResizeBorder (int resizeBorder) {
this.resizeBorder = resizeBorder;
}
public boolean isDragging () {
return dragging;
}
public float getPrefWidth () {
return Math.max(super.getPrefWidth(), titleTable.getPrefWidth() + getPadLeft() + getPadRight());
}
public Table getTitleTable () {
return titleTable;
}
public Label getTitleLabel () {
return titleLabel;
}
/** The style for a window, see {@link Window}.
* @author Nathan Sweet */
static public class WindowStyle {
/** Optional. */
public Drawable background;
public BitmapFont titleFont;
/** Optional. */
public Color titleFontColor = new Color(1, 1, 1, 1);
/** Optional. */
public Drawable stageBackground;
public WindowStyle () {
}
public WindowStyle (BitmapFont titleFont, Color titleFontColor, Drawable background) {
this.background = background;
this.titleFont = titleFont;
this.titleFontColor.set(titleFontColor);
}
public WindowStyle (WindowStyle style) {
this.background = style.background;
this.titleFont = style.titleFont;
this.titleFontColor = new Color(style.titleFontColor);
}
}
}