com.badlogic.gdx.scenes.scene2d.ui.SelectBox Maven / Gradle / Ivy
Show all versions of gdx Show documentation
/*******************************************************************************
* 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 static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.math.Interpolation;
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.List.ListStyle;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane.ScrollPaneStyle;
import com.badlogic.gdx.scenes.scene2d.utils.ArraySelection;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.Disableable;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import com.badlogic.gdx.utils.ObjectSet;
import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.Pools;
/** A select box (aka a drop-down list) allows a user to choose one of a number of values from a list. When inactive, the selected
* value is displayed. When activated, it shows the list of values that may be selected.
*
* {@link ChangeEvent} is fired when the selectbox selection changes.
*
* The preferred size of the select box is determined by the maximum text bounds of the items and the size of the
* {@link SelectBoxStyle#background}.
* @author mzechner
* @author Nathan Sweet */
public class SelectBox extends Widget implements Disableable {
static final Vector2 temp = new Vector2();
SelectBoxStyle style;
final Array items = new Array();
SelectBoxScrollPane scrollPane;
private float prefWidth, prefHeight;
private ClickListener clickListener;
boolean disabled;
private int alignment = Align.left;
boolean selectedPrefWidth;
final ArraySelection selection = new ArraySelection(items) {
public boolean fireChangeEvent () {
if (selectedPrefWidth) invalidateHierarchy();
return super.fireChangeEvent();
}
};
public SelectBox (Skin skin) {
this(skin.get(SelectBoxStyle.class));
}
public SelectBox (Skin skin, String styleName) {
this(skin.get(styleName, SelectBoxStyle.class));
}
public SelectBox (SelectBoxStyle style) {
setStyle(style);
setSize(getPrefWidth(), getPrefHeight());
selection.setActor(this);
selection.setRequired(true);
scrollPane = new SelectBoxScrollPane(this);
addListener(clickListener = new ClickListener() {
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
if (pointer == 0 && button != 0) return false;
if (isDisabled()) return false;
if (scrollPane.hasParent())
hideScrollPane();
else
showScrollPane();
return true;
}
});
}
/** Allows a subclass to customize the scroll pane shown when the select box is open. */
protected SelectBoxScrollPane newScrollPane () {
return new SelectBoxScrollPane(this);
}
/** Set the max number of items to display when the select box is opened. Set to 0 (the default) to display as many as fit in
* the stage height. */
public void setMaxListCount (int maxListCount) {
scrollPane.maxListCount = maxListCount;
}
/** @return Max number of items to display when the box is opened, or <= 0 to display them all. */
public int getMaxListCount () {
return scrollPane.maxListCount;
}
protected void setStage (Stage stage) {
if (stage == null) scrollPane.hide();
super.setStage(stage);
}
public void setStyle (SelectBoxStyle style) {
if (style == null) throw new IllegalArgumentException("style cannot be null.");
this.style = style;
if (scrollPane != null) {
scrollPane.setStyle(style.scrollStyle);
scrollPane.list.setStyle(style.listStyle);
}
invalidateHierarchy();
}
/** Returns the select box's style. Modifying the returned style may not have an effect until {@link #setStyle(SelectBoxStyle)}
* is called. */
public SelectBoxStyle getStyle () {
return style;
}
/** Set the backing Array that makes up the choices available in the SelectBox */
public void setItems (T... newItems) {
if (newItems == null) throw new IllegalArgumentException("newItems cannot be null.");
float oldPrefWidth = getPrefWidth();
items.clear();
items.addAll(newItems);
selection.validate();
scrollPane.list.setItems(items);
invalidate();
if (oldPrefWidth != getPrefWidth()) invalidateHierarchy();
}
/** Sets the items visible in the select box. */
public void setItems (Array newItems) {
if (newItems == null) throw new IllegalArgumentException("newItems cannot be null.");
float oldPrefWidth = getPrefWidth();
if (newItems != items) {
items.clear();
items.addAll(newItems);
}
selection.validate();
scrollPane.list.setItems(items);
invalidate();
if (oldPrefWidth != getPrefWidth()) invalidateHierarchy();
}
public void clearItems () {
if (items.size == 0) return;
items.clear();
selection.clear();
scrollPane.list.clearItems();
invalidateHierarchy();
}
/** Returns the internal items array. If modified, {@link #setItems(Array)} must be called to reflect the changes. */
public Array getItems () {
return items;
}
public void layout () {
Drawable bg = style.background;
BitmapFont font = style.font;
if (bg != null) {
prefHeight = Math.max(bg.getTopHeight() + bg.getBottomHeight() + font.getCapHeight() - font.getDescent() * 2,
bg.getMinHeight());
} else
prefHeight = font.getCapHeight() - font.getDescent() * 2;
Pool layoutPool = Pools.get(GlyphLayout.class);
GlyphLayout layout = layoutPool.obtain();
if (selectedPrefWidth) {
prefWidth = 0;
if (bg != null) prefWidth = bg.getLeftWidth() + bg.getRightWidth();
T selected = getSelected();
if (selected != null) {
layout.setText(font, toString(selected));
prefWidth += layout.width;
}
} else {
float maxItemWidth = 0;
for (int i = 0; i < items.size; i++) {
layout.setText(font, toString(items.get(i)));
maxItemWidth = Math.max(layout.width, maxItemWidth);
}
prefWidth = maxItemWidth;
if (bg != null) prefWidth = Math.max(prefWidth + bg.getLeftWidth() + bg.getRightWidth(), bg.getMinWidth());
ListStyle listStyle = style.listStyle;
ScrollPaneStyle scrollStyle = style.scrollStyle;
float scrollWidth = maxItemWidth + listStyle.selection.getLeftWidth() + listStyle.selection.getRightWidth();
bg = scrollStyle.background;
if (bg != null) scrollWidth = Math.max(scrollWidth + bg.getLeftWidth() + bg.getRightWidth(), bg.getMinWidth());
if (scrollPane == null || !scrollPane.disableY) {
scrollWidth += Math.max(style.scrollStyle.vScroll != null ? style.scrollStyle.vScroll.getMinWidth() : 0,
style.scrollStyle.vScrollKnob != null ? style.scrollStyle.vScrollKnob.getMinWidth() : 0);
}
prefWidth = Math.max(prefWidth, scrollWidth);
}
layoutPool.free(layout);
}
/** Returns appropriate background drawable from the style based on the current select box state. */
protected @Null Drawable getBackgroundDrawable () {
if (isDisabled() && style.backgroundDisabled != null) return style.backgroundDisabled;
if (scrollPane.hasParent() && style.backgroundOpen != null) return style.backgroundOpen;
if (isOver() && style.backgroundOver != null) return style.backgroundOver;
return style.background;
}
/** Returns the appropriate label font color from the style based on the current button state. */
protected Color getFontColor () {
if (isDisabled() && style.disabledFontColor != null) return style.disabledFontColor;
if (style.overFontColor != null && (isOver() || scrollPane.hasParent())) return style.overFontColor;
return style.fontColor;
}
public void draw (Batch batch, float parentAlpha) {
validate();
Drawable background = getBackgroundDrawable();
Color fontColor = getFontColor();
BitmapFont font = style.font;
Color color = getColor();
float x = getX(), y = getY();
float width = getWidth(), height = getHeight();
batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
if (background != null) background.draw(batch, x, y, width, height);
T selected = selection.first();
if (selected != null) {
if (background != null) {
width -= background.getLeftWidth() + background.getRightWidth();
height -= background.getBottomHeight() + background.getTopHeight();
x += background.getLeftWidth();
y += (int)(height / 2 + background.getBottomHeight() + font.getData().capHeight / 2);
} else {
y += (int)(height / 2 + font.getData().capHeight / 2);
}
font.setColor(fontColor.r, fontColor.g, fontColor.b, fontColor.a * parentAlpha);
drawItem(batch, font, selected, x, y, width);
}
}
protected GlyphLayout drawItem (Batch batch, BitmapFont font, T item, float x, float y, float width) {
String string = toString(item);
return font.draw(batch, string, x, y, 0, string.length(), width, alignment, false, "...");
}
/** Sets the alignment of the selected item in the select box. See {@link #getList()} and {@link List#setAlignment(int)} to set
* the alignment in the list shown when the select box is open.
* @param alignment See {@link Align}. */
public void setAlignment (int alignment) {
this.alignment = alignment;
}
/** Get the set of selected items, useful when multiple items are selected
* @return a Selection object containing the selected elements */
public ArraySelection getSelection () {
return selection;
}
/** Returns the first selected item, or null. For multiple selections use {@link SelectBox#getSelection()}. */
public @Null T getSelected () {
return selection.first();
}
/** Sets the selection to only the passed item, if it is a possible choice, else selects the first item. */
public void setSelected (@Null T item) {
if (items.contains(item, false))
selection.set(item);
else if (items.size > 0)
selection.set(items.first());
else
selection.clear();
}
/** @return The index of the first selected item. The top item has an index of 0. Nothing selected has an index of -1. */
public int getSelectedIndex () {
ObjectSet selected = selection.items();
return selected.size == 0 ? -1 : items.indexOf(selected.first(), false);
}
/** Sets the selection to only the selected index. */
public void setSelectedIndex (int index) {
selection.set(items.get(index));
}
/** When true the pref width is based on the selected item. */
public void setSelectedPrefWidth (boolean selectedPrefWidth) {
this.selectedPrefWidth = selectedPrefWidth;
}
public boolean getSelectedPrefWidth () {
return selectedPrefWidth;
}
/** Returns the pref width of the select box if the widest item was selected, for use when
* {@link #setSelectedPrefWidth(boolean)} is true. */
public float getMaxSelectedPrefWidth () {
Pool layoutPool = Pools.get(GlyphLayout.class);
GlyphLayout layout = layoutPool.obtain();
float width = 0;
for (int i = 0; i < items.size; i++) {
layout.setText(style.font, toString(items.get(i)));
width = Math.max(layout.width, width);
}
Drawable bg = style.background;
if (bg != null) width = Math.max(width + bg.getLeftWidth() + bg.getRightWidth(), bg.getMinWidth());
return width;
}
public void setDisabled (boolean disabled) {
if (disabled && !this.disabled) hideScrollPane();
this.disabled = disabled;
}
public boolean isDisabled () {
return disabled;
}
public float getPrefWidth () {
validate();
return prefWidth;
}
public float getPrefHeight () {
validate();
return prefHeight;
}
protected String toString (T item) {
return item.toString();
}
/** @deprecated Use {@link #showScrollPane()}. */
@Deprecated
public void showList () {
showScrollPane();
}
public void showScrollPane () {
if (items.size == 0) return;
if (getStage() != null) scrollPane.show(getStage());
}
/** @deprecated Use {@link #hideScrollPane()}. */
@Deprecated
public void hideList () {
hideScrollPane();
}
public void hideScrollPane () {
scrollPane.hide();
}
/** Returns the list shown when the select box is open. */
public List getList () {
return scrollPane.list;
}
/** Disables scrolling of the list shown when the select box is open. */
public void setScrollingDisabled (boolean y) {
scrollPane.setScrollingDisabled(true, y);
invalidateHierarchy();
}
/** Returns the scroll pane containing the list that is shown when the select box is open. */
public SelectBoxScrollPane getScrollPane () {
return scrollPane;
}
public boolean isOver () {
return clickListener.isOver();
}
public ClickListener getClickListener () {
return clickListener;
}
protected void onShow (Actor scrollPane, boolean below) {
scrollPane.getColor().a = 0;
scrollPane.addAction(fadeIn(0.3f, Interpolation.fade));
}
protected void onHide (Actor scrollPane) {
scrollPane.getColor().a = 1;
scrollPane.addAction(sequence(fadeOut(0.15f, Interpolation.fade), removeActor()));
}
/** The scroll pane shown when a select box is open.
* @author Nathan Sweet */
static public class SelectBoxScrollPane extends ScrollPane {
final SelectBox selectBox;
int maxListCount;
private final Vector2 stagePosition = new Vector2();
final List list;
private InputListener hideListener;
private Actor previousScrollFocus;
public SelectBoxScrollPane (final SelectBox selectBox) {
super(null, selectBox.style.scrollStyle);
this.selectBox = selectBox;
setOverscroll(false, false);
setFadeScrollBars(false);
setScrollingDisabled(true, false);
list = newList();
list.setTouchable(Touchable.disabled);
list.setTypeToSelect(true);
setActor(list);
list.addListener(new ClickListener() {
public void clicked (InputEvent event, float x, float y) {
T selected = list.getSelected();
// Force clicking the already selected item to trigger a change event.
if (selected != null) selectBox.selection.items().clear(51);
selectBox.selection.choose(selected);
hide();
}
public boolean mouseMoved (InputEvent event, float x, float y) {
int index = list.getItemIndexAt(y);
if (index != -1) list.setSelectedIndex(index);
return true;
}
});
addListener(new InputListener() {
public void exit (InputEvent event, float x, float y, int pointer, @Null Actor toActor) {
if (toActor == null || !isAscendantOf(toActor)) list.selection.set(selectBox.getSelected());
}
});
hideListener = new InputListener() {
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
Actor target = event.getTarget();
if (isAscendantOf(target)) return false;
list.selection.set(selectBox.getSelected());
hide();
return false;
}
public boolean keyDown (InputEvent event, int keycode) {
switch (keycode) {
case Keys.NUMPAD_ENTER:
case Keys.ENTER:
selectBox.selection.choose(list.getSelected());
// Fall thru.
case Keys.ESCAPE:
hide();
event.stop();
return true;
}
return false;
}
};
}
/** Allows a subclass to customize the select box list. The default implementation returns a list that delegates
* {@link List#toString(Object)} to {@link SelectBox#toString(Object)}. */
protected List newList () {
return new List(selectBox.style.listStyle) {
public String toString (T obj) {
return selectBox.toString(obj);
}
};
}
public void show (Stage stage) {
if (list.isTouchable()) return;
stage.addActor(this);
stage.addCaptureListener(hideListener);
stage.addListener(list.getKeyListener());
selectBox.localToStageCoordinates(stagePosition.set(0, 0));
// Show the list above or below the select box, limited to a number of items and the available height in the stage.
float itemHeight = list.getItemHeight();
float height = itemHeight * (maxListCount <= 0 ? selectBox.items.size : Math.min(maxListCount, selectBox.items.size));
Drawable scrollPaneBackground = getStyle().background;
if (scrollPaneBackground != null) height += scrollPaneBackground.getTopHeight() + scrollPaneBackground.getBottomHeight();
Drawable listBackground = list.getStyle().background;
if (listBackground != null) height += listBackground.getTopHeight() + listBackground.getBottomHeight();
float heightBelow = stagePosition.y;
float heightAbove = stage.getHeight() - heightBelow - selectBox.getHeight();
boolean below = true;
if (height > heightBelow) {
if (heightAbove > heightBelow) {
below = false;
height = Math.min(height, heightAbove);
} else
height = heightBelow;
}
if (below)
setY(stagePosition.y - height);
else
setY(stagePosition.y + selectBox.getHeight());
setX(stagePosition.x);
setHeight(height);
validate();
float width = Math.max(getPrefWidth(), selectBox.getWidth());
setWidth(width);
validate();
scrollTo(0, list.getHeight() - selectBox.getSelectedIndex() * itemHeight - itemHeight / 2, 0, 0, true, true);
updateVisualScroll();
previousScrollFocus = null;
Actor actor = stage.getScrollFocus();
if (actor != null && !actor.isDescendantOf(this)) previousScrollFocus = actor;
stage.setScrollFocus(this);
list.selection.set(selectBox.getSelected());
list.setTouchable(Touchable.enabled);
clearActions();
selectBox.onShow(this, below);
}
public void hide () {
if (!list.isTouchable() || !hasParent()) return;
list.setTouchable(Touchable.disabled);
Stage stage = getStage();
if (stage != null) {
stage.removeCaptureListener(hideListener);
stage.removeListener(list.getKeyListener());
if (previousScrollFocus != null && previousScrollFocus.getStage() == null) previousScrollFocus = null;
Actor actor = stage.getScrollFocus();
if (actor == null || isAscendantOf(actor)) stage.setScrollFocus(previousScrollFocus);
}
clearActions();
selectBox.onHide(this);
}
public void draw (Batch batch, float parentAlpha) {
selectBox.localToStageCoordinates(temp.set(0, 0));
if (!temp.equals(stagePosition)) hide();
super.draw(batch, parentAlpha);
}
public void act (float delta) {
super.act(delta);
toFront();
}
protected void setStage (Stage stage) {
Stage oldStage = getStage();
if (oldStage != null) {
oldStage.removeCaptureListener(hideListener);
oldStage.removeListener(list.getKeyListener());
}
super.setStage(stage);
}
public List getList () {
return list;
}
public SelectBox getSelectBox () {
return selectBox;
}
}
/** The style for a select box, see {@link SelectBox}.
* @author mzechner
* @author Nathan Sweet */
static public class SelectBoxStyle {
public BitmapFont font;
public Color fontColor = new Color(1, 1, 1, 1);
public @Null Color overFontColor, disabledFontColor;
public @Null Drawable background;
public ScrollPaneStyle scrollStyle;
public ListStyle listStyle;
public @Null Drawable backgroundOver, backgroundOpen, backgroundDisabled;
public SelectBoxStyle () {
}
public SelectBoxStyle (BitmapFont font, Color fontColor, @Null Drawable background, ScrollPaneStyle scrollStyle,
ListStyle listStyle) {
this.font = font;
this.fontColor.set(fontColor);
this.background = background;
this.scrollStyle = scrollStyle;
this.listStyle = listStyle;
}
public SelectBoxStyle (SelectBoxStyle style) {
font = style.font;
fontColor.set(style.fontColor);
if (style.overFontColor != null) overFontColor = new Color(style.overFontColor);
if (style.disabledFontColor != null) disabledFontColor = new Color(style.disabledFontColor);
background = style.background;
scrollStyle = new ScrollPaneStyle(style.scrollStyle);
listStyle = new ListStyle(style.listStyle);
backgroundOver = style.backgroundOver;
backgroundOpen = style.backgroundOpen;
backgroundDisabled = style.backgroundDisabled;
}
}
}