• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  * Copyright 2011 See AUTHORS file.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  ******************************************************************************/
16 
17 package com.badlogic.gdx.scenes.scene2d.ui;
18 
19 import static com.badlogic.gdx.scenes.scene2d.actions.Actions.fadeIn;
20 import static com.badlogic.gdx.scenes.scene2d.actions.Actions.fadeOut;
21 import static com.badlogic.gdx.scenes.scene2d.actions.Actions.removeActor;
22 import static com.badlogic.gdx.scenes.scene2d.actions.Actions.sequence;
23 
24 import com.badlogic.gdx.Input.Keys;
25 import com.badlogic.gdx.graphics.Color;
26 import com.badlogic.gdx.graphics.g2d.Batch;
27 import com.badlogic.gdx.graphics.g2d.BitmapFont;
28 import com.badlogic.gdx.graphics.g2d.GlyphLayout;
29 import com.badlogic.gdx.math.Interpolation;
30 import com.badlogic.gdx.math.Vector2;
31 import com.badlogic.gdx.scenes.scene2d.Actor;
32 import com.badlogic.gdx.scenes.scene2d.InputEvent;
33 import com.badlogic.gdx.scenes.scene2d.InputListener;
34 import com.badlogic.gdx.scenes.scene2d.Stage;
35 import com.badlogic.gdx.scenes.scene2d.Touchable;
36 import com.badlogic.gdx.scenes.scene2d.ui.List.ListStyle;
37 import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane.ScrollPaneStyle;
38 import com.badlogic.gdx.scenes.scene2d.utils.ArraySelection;
39 import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent;
40 import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
41 import com.badlogic.gdx.scenes.scene2d.utils.Disableable;
42 import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
43 import com.badlogic.gdx.utils.Align;
44 import com.badlogic.gdx.utils.Array;
45 import com.badlogic.gdx.utils.ObjectSet;
46 import com.badlogic.gdx.utils.Pool;
47 import com.badlogic.gdx.utils.Pools;
48 
49 /** 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
50  * value is displayed. When activated, it shows the list of values that may be selected.
51  * <p>
52  * {@link ChangeEvent} is fired when the selectbox selection changes.
53  * <p>
54  * The preferred size of the select box is determined by the maximum text bounds of the items and the size of the
55  * {@link SelectBoxStyle#background}.
56  * @author mzechner
57  * @author Nathan Sweet */
58 public class SelectBox<T> extends Widget implements Disableable {
59 	static final Vector2 temp = new Vector2();
60 
61 	SelectBoxStyle style;
62 	final Array<T> items = new Array();
63 	final ArraySelection<T> selection = new ArraySelection(items);
64 	SelectBoxList<T> selectBoxList;
65 	private float prefWidth, prefHeight;
66 	private ClickListener clickListener;
67 	boolean disabled;
68 	private GlyphLayout layout = new GlyphLayout();
69 
SelectBox(Skin skin)70 	public SelectBox (Skin skin) {
71 		this(skin.get(SelectBoxStyle.class));
72 	}
73 
SelectBox(Skin skin, String styleName)74 	public SelectBox (Skin skin, String styleName) {
75 		this(skin.get(styleName, SelectBoxStyle.class));
76 	}
77 
SelectBox(SelectBoxStyle style)78 	public SelectBox (SelectBoxStyle style) {
79 		setStyle(style);
80 		setSize(getPrefWidth(), getPrefHeight());
81 
82 		selection.setActor(this);
83 		selection.setRequired(true);
84 
85 		selectBoxList = new SelectBoxList(this);
86 
87 		addListener(clickListener = new ClickListener() {
88 			public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
89 				if (pointer == 0 && button != 0) return false;
90 				if (disabled) return false;
91 				if (selectBoxList.hasParent())
92 					hideList();
93 				else
94 					showList();
95 				return true;
96 			}
97 		});
98 	}
99 
100 	/** 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
101 	 * the stage height. */
setMaxListCount(int maxListCount)102 	public void setMaxListCount (int maxListCount) {
103 		selectBoxList.maxListCount = maxListCount;
104 	}
105 
106 	/** @return Max number of items to display when the box is opened, or <= 0 to display them all. */
getMaxListCount()107 	public int getMaxListCount () {
108 		return selectBoxList.maxListCount;
109 	}
110 
setStage(Stage stage)111 	protected void setStage (Stage stage) {
112 		if (stage == null) selectBoxList.hide();
113 		super.setStage(stage);
114 	}
115 
setStyle(SelectBoxStyle style)116 	public void setStyle (SelectBoxStyle style) {
117 		if (style == null) throw new IllegalArgumentException("style cannot be null.");
118 		this.style = style;
119 		invalidateHierarchy();
120 	}
121 
122 	/** Returns the select box's style. Modifying the returned style may not have an effect until {@link #setStyle(SelectBoxStyle)}
123 	 * is called. */
getStyle()124 	public SelectBoxStyle getStyle () {
125 		return style;
126 	}
127 
128 	/** Set the backing Array that makes up the choices available in the SelectBox */
setItems(T... newItems)129 	public void setItems (T... newItems) {
130 		if (newItems == null) throw new IllegalArgumentException("newItems cannot be null.");
131 		float oldPrefWidth = getPrefWidth();
132 
133 		items.clear();
134 		items.addAll(newItems);
135 		selection.validate();
136 		selectBoxList.list.setItems(items);
137 
138 		invalidate();
139 		if (oldPrefWidth != getPrefWidth()) invalidateHierarchy();
140 	}
141 
142 	/** Sets the items visible in the select box. */
setItems(Array<T> newItems)143 	public void setItems (Array<T> newItems) {
144 		if (newItems == null) throw new IllegalArgumentException("newItems cannot be null.");
145 		float oldPrefWidth = getPrefWidth();
146 
147 		items.clear();
148 		items.addAll(newItems);
149 		selection.validate();
150 		selectBoxList.list.setItems(items);
151 
152 		invalidate();
153 		if (oldPrefWidth != getPrefWidth()) invalidateHierarchy();
154 	}
155 
clearItems()156 	public void clearItems () {
157 		if (items.size == 0) return;
158 		items.clear();
159 		selection.clear();
160 		invalidateHierarchy();
161 	}
162 
163 	/** Returns the internal items array. If modified, {@link #setItems(Array)} must be called to reflect the changes. */
getItems()164 	public Array<T> getItems () {
165 		return items;
166 	}
167 
168 	@Override
layout()169 	public void layout () {
170 		Drawable bg = style.background;
171 		BitmapFont font = style.font;
172 
173 		if (bg != null) {
174 			prefHeight = Math.max(bg.getTopHeight() + bg.getBottomHeight() + font.getCapHeight() - font.getDescent() * 2,
175 				bg.getMinHeight());
176 		} else
177 			prefHeight = font.getCapHeight() - font.getDescent() * 2;
178 
179 		float maxItemWidth = 0;
180 		Pool<GlyphLayout> layoutPool = Pools.get(GlyphLayout.class);
181 		GlyphLayout layout = layoutPool.obtain();
182 		for (int i = 0; i < items.size; i++) {
183 			layout.setText(font, toString(items.get(i)));
184 			maxItemWidth = Math.max(layout.width, maxItemWidth);
185 		}
186 		layoutPool.free(layout);
187 
188 		prefWidth = maxItemWidth;
189 		if (bg != null) prefWidth += bg.getLeftWidth() + bg.getRightWidth();
190 
191 		ListStyle listStyle = style.listStyle;
192 		ScrollPaneStyle scrollStyle = style.scrollStyle;
193 		prefWidth = Math.max(
194 			prefWidth,
195 			maxItemWidth
196 				+ (scrollStyle.background == null ? 0 : scrollStyle.background.getLeftWidth()
197 					+ scrollStyle.background.getRightWidth())
198 				+ listStyle.selection.getLeftWidth()
199 				+ listStyle.selection.getRightWidth()
200 				+ Math.max(style.scrollStyle.vScroll != null ? style.scrollStyle.vScroll.getMinWidth() : 0,
201 					style.scrollStyle.vScrollKnob != null ? style.scrollStyle.vScrollKnob.getMinWidth() : 0));
202 	}
203 
204 	@Override
draw(Batch batch, float parentAlpha)205 	public void draw (Batch batch, float parentAlpha) {
206 		validate();
207 
208 		Drawable background;
209 		if (disabled && style.backgroundDisabled != null)
210 			background = style.backgroundDisabled;
211 		else if (selectBoxList.hasParent() && style.backgroundOpen != null)
212 			background = style.backgroundOpen;
213 		else if (clickListener.isOver() && style.backgroundOver != null)
214 			background = style.backgroundOver;
215 		else if (style.background != null)
216 			background = style.background;
217 		else
218 			background = null;
219 		final BitmapFont font = style.font;
220 		final Color fontColor = (disabled && style.disabledFontColor != null) ? style.disabledFontColor : style.fontColor;
221 
222 		Color color = getColor();
223 		float x = getX();
224 		float y = getY();
225 		float width = getWidth();
226 		float height = getHeight();
227 
228 		batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
229 		if (background != null) background.draw(batch, x, y, width, height);
230 
231 		T selected = selection.first();
232 		if (selected != null) {
233 			String string = toString(selected);
234 			if (background != null) {
235 				width -= background.getLeftWidth() + background.getRightWidth();
236 				height -= background.getBottomHeight() + background.getTopHeight();
237 				x += background.getLeftWidth();
238 				y += (int)(height / 2 + background.getBottomHeight() + font.getData().capHeight / 2);
239 			} else {
240 				y += (int)(height / 2 + font.getData().capHeight / 2);
241 			}
242 			font.setColor(fontColor.r, fontColor.g, fontColor.b, fontColor.a * parentAlpha);
243 			layout.setText(font, string, 0, string.length(), font.getColor(), width, Align.left, false, "...");
244 			font.draw(batch, layout, x, y);
245 		}
246 	}
247 
248 	/** Get the set of selected items, useful when multiple items are selected
249 	 * @return a Selection object containing the selected elements */
getSelection()250 	public ArraySelection<T> getSelection () {
251 		return selection;
252 	}
253 
254 	/** Returns the first selected item, or null. For multiple selections use {@link SelectBox#getSelection()}. */
getSelected()255 	public T getSelected () {
256 		return selection.first();
257 	}
258 
259 	/** Sets the selection to only the passed item, if it is a possible choice, else selects the first item. */
setSelected(T item)260 	public void setSelected (T item) {
261 		if (items.contains(item, false))
262 			selection.set(item);
263 		else if (items.size > 0)
264 			selection.set(items.first());
265 		else
266 			selection.clear();
267 	}
268 
269 	/** @return The index of the first selected item. The top item has an index of 0. Nothing selected has an index of -1. */
getSelectedIndex()270 	public int getSelectedIndex () {
271 		ObjectSet<T> selected = selection.items();
272 		return selected.size == 0 ? -1 : items.indexOf(selected.first(), false);
273 	}
274 
275 	/** Sets the selection to only the selected index. */
setSelectedIndex(int index)276 	public void setSelectedIndex (int index) {
277 		selection.set(items.get(index));
278 	}
279 
setDisabled(boolean disabled)280 	public void setDisabled (boolean disabled) {
281 		if (disabled && !this.disabled) hideList();
282 		this.disabled = disabled;
283 	}
284 
isDisabled()285 	public boolean isDisabled () {
286 		return disabled;
287 	}
288 
getPrefWidth()289 	public float getPrefWidth () {
290 		validate();
291 		return prefWidth;
292 	}
293 
getPrefHeight()294 	public float getPrefHeight () {
295 		validate();
296 		return prefHeight;
297 	}
298 
toString(T obj)299 	protected String toString (T obj) {
300 		return obj.toString();
301 	}
302 
showList()303 	public void showList () {
304 		if (items.size == 0) return;
305 		selectBoxList.show(getStage());
306 	}
307 
hideList()308 	public void hideList () {
309 		selectBoxList.hide();
310 	}
311 
312 	/** Returns the list shown when the select box is open. */
getList()313 	public List<T> getList () {
314 		return selectBoxList.list;
315 	}
316 
317 	/** Returns the scroll pane containing the list that is shown when the select box is open. */
getScrollPane()318 	public ScrollPane getScrollPane () {
319 		return selectBoxList;
320 	}
321 
onShow(Actor selectBoxList, boolean below)322 	protected void onShow (Actor selectBoxList, boolean below) {
323 		selectBoxList.getColor().a = 0;
324 		selectBoxList.addAction(fadeIn(0.3f, Interpolation.fade));
325 	}
326 
onHide(Actor selectBoxList)327 	protected void onHide (Actor selectBoxList) {
328 		selectBoxList.getColor().a = 1;
329 		selectBoxList.addAction(sequence(fadeOut(0.15f, Interpolation.fade), removeActor()));
330 	}
331 
332 	/** @author Nathan Sweet */
333 	static class SelectBoxList<T> extends ScrollPane {
334 		private final SelectBox<T> selectBox;
335 		int maxListCount;
336 		private final Vector2 screenPosition = new Vector2();
337 		final List<T> list;
338 		private InputListener hideListener;
339 		private Actor previousScrollFocus;
340 
SelectBoxList(final SelectBox<T> selectBox)341 		public SelectBoxList (final SelectBox<T> selectBox) {
342 			super(null, selectBox.style.scrollStyle);
343 			this.selectBox = selectBox;
344 
345 			setOverscroll(false, false);
346 			setFadeScrollBars(false);
347 			setScrollingDisabled(true, false);
348 
349 			list = new List<T>(selectBox.style.listStyle) {
350 				@Override
351 				protected String toString (T obj) {
352 					return selectBox.toString(obj);
353 				}
354 			};
355 			list.setTouchable(Touchable.disabled);
356 			setWidget(list);
357 
358 			list.addListener(new ClickListener() {
359 				public void clicked (InputEvent event, float x, float y) {
360 					selectBox.selection.choose(list.getSelected());
361 					hide();
362 				}
363 
364 				public boolean mouseMoved (InputEvent event, float x, float y) {
365 					list.setSelectedIndex(Math.min(selectBox.items.size - 1, (int)((list.getHeight() - y) / list.getItemHeight())));
366 					return true;
367 				}
368 			});
369 
370 			addListener(new InputListener() {
371 				public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) {
372 					if (toActor == null || !isAscendantOf(toActor)) list.selection.set(selectBox.getSelected());
373 				}
374 			});
375 
376 			hideListener = new InputListener() {
377 				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
378 					Actor target = event.getTarget();
379 					if (isAscendantOf(target)) return false;
380 					list.selection.set(selectBox.getSelected());
381 					hide();
382 					return false;
383 				}
384 
385 				public boolean keyDown (InputEvent event, int keycode) {
386 					if (keycode == Keys.ESCAPE) hide();
387 					return false;
388 				}
389 			};
390 		}
391 
show(Stage stage)392 		public void show (Stage stage) {
393 			if (list.isTouchable()) return;
394 
395 			stage.removeCaptureListener(hideListener);
396 			stage.addCaptureListener(hideListener);
397 			stage.addActor(this);
398 
399 			selectBox.localToStageCoordinates(screenPosition.set(0, 0));
400 
401 			// Show the list above or below the select box, limited to a number of items and the available height in the stage.
402 			float itemHeight = list.getItemHeight();
403 			float height = itemHeight * (maxListCount <= 0 ? selectBox.items.size : Math.min(maxListCount, selectBox.items.size));
404 			Drawable scrollPaneBackground = getStyle().background;
405 			if (scrollPaneBackground != null)
406 				height += scrollPaneBackground.getTopHeight() + scrollPaneBackground.getBottomHeight();
407 			Drawable listBackground = list.getStyle().background;
408 			if (listBackground != null) height += listBackground.getTopHeight() + listBackground.getBottomHeight();
409 
410 			float heightBelow = screenPosition.y;
411 			float heightAbove = stage.getCamera().viewportHeight - screenPosition.y - selectBox.getHeight();
412 			boolean below = true;
413 			if (height > heightBelow) {
414 				if (heightAbove > heightBelow) {
415 					below = false;
416 					height = Math.min(height, heightAbove);
417 				} else
418 					height = heightBelow;
419 			}
420 
421 			if (below)
422 				setY(screenPosition.y - height);
423 			else
424 				setY(screenPosition.y + selectBox.getHeight());
425 			setX(screenPosition.x);
426 			setHeight(height);
427 			validate();
428 			float width = Math.max(getPrefWidth(), selectBox.getWidth());
429 			if (getPrefHeight() > height) width += getScrollBarWidth();
430 			if (scrollPaneBackground != null) {
431 				// Assume left and right padding are the same, so right padding can include a shadow.
432 				width += Math.max(0, scrollPaneBackground.getRightWidth() - scrollPaneBackground.getLeftWidth());
433 			}
434 			setWidth(width);
435 
436 			validate();
437 			scrollTo(0, list.getHeight() - selectBox.getSelectedIndex() * itemHeight - itemHeight / 2, 0, 0, true, true);
438 			updateVisualScroll();
439 
440 			previousScrollFocus = null;
441 			Actor actor = stage.getScrollFocus();
442 			if (actor != null && !actor.isDescendantOf(this)) previousScrollFocus = actor;
443 			stage.setScrollFocus(this);
444 
445 			list.selection.set(selectBox.getSelected());
446 			list.setTouchable(Touchable.enabled);
447 			clearActions();
448 			selectBox.onShow(this, below);
449 		}
450 
hide()451 		public void hide () {
452 			if (!list.isTouchable() || !hasParent()) return;
453 			list.setTouchable(Touchable.disabled);
454 
455 			Stage stage = getStage();
456 			if (stage != null) {
457 				stage.removeCaptureListener(hideListener);
458 				if (previousScrollFocus != null && previousScrollFocus.getStage() == null) previousScrollFocus = null;
459 				Actor actor = stage.getScrollFocus();
460 				if (actor == null || isAscendantOf(actor)) stage.setScrollFocus(previousScrollFocus);
461 			}
462 
463 			clearActions();
464 			selectBox.onHide(this);
465 		}
466 
draw(Batch batch, float parentAlpha)467 		public void draw (Batch batch, float parentAlpha) {
468 			selectBox.localToStageCoordinates(temp.set(0, 0));
469 			if (!temp.equals(screenPosition)) hide();
470 			super.draw(batch, parentAlpha);
471 		}
472 
act(float delta)473 		public void act (float delta) {
474 			super.act(delta);
475 			toFront();
476 		}
477 	}
478 
479 	/** The style for a select box, see {@link SelectBox}.
480 	 * @author mzechner
481 	 * @author Nathan Sweet */
482 	static public class SelectBoxStyle {
483 		public BitmapFont font;
484 		public Color fontColor = new Color(1, 1, 1, 1);
485 		/** Optional. */
486 		public Color disabledFontColor;
487 		/** Optional. */
488 		public Drawable background;
489 		public ScrollPaneStyle scrollStyle;
490 		public ListStyle listStyle;
491 		/** Optional. */
492 		public Drawable backgroundOver, backgroundOpen, backgroundDisabled;
493 
SelectBoxStyle()494 		public SelectBoxStyle () {
495 		}
496 
SelectBoxStyle(BitmapFont font, Color fontColor, Drawable background, ScrollPaneStyle scrollStyle, ListStyle listStyle)497 		public SelectBoxStyle (BitmapFont font, Color fontColor, Drawable background, ScrollPaneStyle scrollStyle,
498 			ListStyle listStyle) {
499 			this.font = font;
500 			this.fontColor.set(fontColor);
501 			this.background = background;
502 			this.scrollStyle = scrollStyle;
503 			this.listStyle = listStyle;
504 		}
505 
SelectBoxStyle(SelectBoxStyle style)506 		public SelectBoxStyle (SelectBoxStyle style) {
507 			this.font = style.font;
508 			this.fontColor.set(style.fontColor);
509 			if (style.disabledFontColor != null) this.disabledFontColor = new Color(style.disabledFontColor);
510 			this.background = style.background;
511 			this.backgroundOver = style.backgroundOver;
512 			this.backgroundOpen = style.backgroundOpen;
513 			this.backgroundDisabled = style.backgroundDisabled;
514 			this.scrollStyle = new ScrollPaneStyle(style.scrollStyle);
515 			this.listStyle = new ListStyle(style.listStyle);
516 		}
517 	}
518 }
519