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