425 lines
13 KiB
Java
425 lines
13 KiB
Java
/*
|
|
* opsu! - an open-source osu! client
|
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
|
*
|
|
* opsu! is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* opsu! is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package itdelatrisu.opsu.ui;
|
|
|
|
import itdelatrisu.opsu.GameImage;
|
|
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
|
|
|
import org.newdawn.slick.Color;
|
|
import org.newdawn.slick.Font;
|
|
import org.newdawn.slick.Graphics;
|
|
import org.newdawn.slick.Image;
|
|
import org.newdawn.slick.Input;
|
|
import org.newdawn.slick.SlickException;
|
|
import org.newdawn.slick.UnicodeFont;
|
|
import org.newdawn.slick.gui.AbstractComponent;
|
|
import org.newdawn.slick.gui.GUIContext;
|
|
|
|
/**
|
|
* Simple dropdown menu.
|
|
* <p>
|
|
* Basic usage:
|
|
* <ul>
|
|
* <li>Override {@link #menuClicked(int)} to perform actions when the menu is clicked
|
|
* (e.g. play a sound effect, block input under certain conditions).
|
|
* <li>Override {@link #itemSelected(int, Object)} to perform actions when a new item is selected.
|
|
* <li>Call {@link #activate()}/{@link #deactivate()} whenever the component is needed
|
|
* (e.g. in a state's {@code enter} and {@code leave} events.
|
|
* </ul>
|
|
*
|
|
* @param <E> the type of the elements in the menu
|
|
*/
|
|
public class DropdownMenu<E> extends AbstractComponent {
|
|
/** Padding ratios for drawing. */
|
|
private static final float PADDING_Y = 0.1f, CHEVRON_X = 0.03f;
|
|
|
|
/** Whether this component is active. */
|
|
private boolean active;
|
|
|
|
/** The menu items. */
|
|
private E[] items;
|
|
|
|
/** The menu item names. */
|
|
private String[] itemNames;
|
|
|
|
/** The index of the selected item. */
|
|
private int itemIndex = 0;
|
|
|
|
/** Whether the menu is expanded. */
|
|
private boolean expanded = false;
|
|
|
|
/** The expanding animation progress. */
|
|
private AnimatedValue expandProgress = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR);
|
|
|
|
/** The last update time, in milliseconds. */
|
|
private long lastUpdateTime;
|
|
|
|
/** The top-left coordinates. */
|
|
private float x, y;
|
|
|
|
/** The width and height of the dropdown menu. */
|
|
private int width, height;
|
|
|
|
/** The height of the base item. */
|
|
private int baseHeight;
|
|
|
|
/** The vertical offset between items. */
|
|
private float offsetY;
|
|
|
|
/** The colors to use. */
|
|
private Color
|
|
textColor = Color.white, backgroundColor = Color.black,
|
|
highlightColor = Colors.BLUE_DIVIDER, borderColor = Colors.BLUE_DIVIDER,
|
|
chevronDownColor = textColor, chevronRightColor = backgroundColor;
|
|
|
|
/** The fonts to use. */
|
|
private UnicodeFont fontNormal = Fonts.MEDIUM, fontSelected = Fonts.MEDIUMBOLD;
|
|
|
|
/** The chevron images. */
|
|
private Image chevronDown, chevronRight;
|
|
|
|
/**
|
|
* Creates a new dropdown menu.
|
|
* @param container the container rendering this menu
|
|
* @param items the list of items (with names given as their {@code toString()} methods)
|
|
* @param x the top-left x coordinate
|
|
* @param y the top-left y coordinate
|
|
*/
|
|
public DropdownMenu(GUIContext container, E[] items, float x, float y) {
|
|
this(container, items, x, y, 0);
|
|
}
|
|
|
|
/**
|
|
* Creates a new dropdown menu with the given fonts.
|
|
* @param container the container rendering this menu
|
|
* @param items the list of items (with names given as their {@code toString()} methods)
|
|
* @param x the top-left x coordinate
|
|
* @param y the top-left y coordinate
|
|
* @param normal the normal font
|
|
* @param selected the font for the selected item
|
|
*/
|
|
public DropdownMenu(GUIContext container, E[] items, float x, float y, UnicodeFont normal, UnicodeFont selected) {
|
|
this(container, items, x, y, 0, normal, selected);
|
|
}
|
|
|
|
/**
|
|
* Creates a new dropdown menu with the given width.
|
|
* @param container the container rendering this menu
|
|
* @param items the list of items (with names given as their {@code toString()} methods)
|
|
* @param x the top-left x coordinate
|
|
* @param y the top-left y coordinate
|
|
* @param width the menu width
|
|
*/
|
|
public DropdownMenu(GUIContext container, E[] items, float x, float y, int width) {
|
|
super(container);
|
|
init(items, x, y, width);
|
|
}
|
|
|
|
/**
|
|
* Creates a new dropdown menu with the given width and fonts.
|
|
* @param container the container rendering this menu
|
|
* @param items the list of items (with names given as their {@code toString()} methods)
|
|
* @param x the top-left x coordinate
|
|
* @param y the top-left y coordinate
|
|
* @param width the menu width
|
|
* @param normal the normal font
|
|
* @param selected the font for the selected item
|
|
*/
|
|
public DropdownMenu(GUIContext container, E[] items, float x, float y, int width, UnicodeFont normal, UnicodeFont selected) {
|
|
super(container);
|
|
this.fontNormal = normal;
|
|
this.fontSelected = selected;
|
|
init(items, x, y, width);
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum item width from the list.
|
|
*/
|
|
private int getMaxItemWidth() {
|
|
int maxWidth = 0;
|
|
for (int i = 0; i < itemNames.length; i++) {
|
|
int w = fontSelected.getWidth(itemNames[i]);
|
|
if (w > maxWidth)
|
|
maxWidth = w;
|
|
}
|
|
return maxWidth;
|
|
}
|
|
|
|
/**
|
|
* Initializes the component.
|
|
*/
|
|
private void init(E[] items, float x, float y, int width) {
|
|
this.items = items;
|
|
this.itemNames = new String[items.length];
|
|
for (int i = 0; i < itemNames.length; i++)
|
|
itemNames[i] = items[i].toString();
|
|
this.x = x;
|
|
this.y = y;
|
|
this.baseHeight = fontNormal.getLineHeight();
|
|
this.offsetY = baseHeight + baseHeight * PADDING_Y;
|
|
this.height = (int) (offsetY * (items.length + 1));
|
|
int chevronDownSize = baseHeight * 4 / 5;
|
|
this.chevronDown = GameImage.CHEVRON_DOWN.getImage().getScaledCopy(chevronDownSize, chevronDownSize);
|
|
int chevronRightSize = baseHeight * 2 / 3;
|
|
this.chevronRight = GameImage.CHEVRON_RIGHT.getImage().getScaledCopy(chevronRightSize, chevronRightSize);
|
|
int maxItemWidth = getMaxItemWidth();
|
|
int minWidth = maxItemWidth + chevronRight.getWidth() * 2;
|
|
this.width = Math.max(width, minWidth);
|
|
}
|
|
|
|
@Override
|
|
public void setLocation(int x, int y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
|
|
@Override
|
|
public int getX() { return (int) x; }
|
|
|
|
@Override
|
|
public int getY() { return (int) y; }
|
|
|
|
@Override
|
|
public int getWidth() { return width; }
|
|
|
|
@Override
|
|
public int getHeight() { return (expanded) ? height : baseHeight; }
|
|
|
|
/** Activates the component. */
|
|
public void activate() { this.active = true; }
|
|
|
|
/** Deactivates the component. */
|
|
public void deactivate() { this.active = false; }
|
|
|
|
/**
|
|
* Returns whether the dropdown menu is currently open.
|
|
* @return true if open, false otherwise
|
|
*/
|
|
public boolean isOpen() { return expanded; }
|
|
|
|
/**
|
|
* Opens or closes the dropdown menu.
|
|
* @param flag true to open, false to close
|
|
*/
|
|
public void open(boolean flag) { this.expanded = flag; }
|
|
|
|
/**
|
|
* Returns true if the coordinates are within the menu bounds.
|
|
* @param cx the x coordinate
|
|
* @param cy the y coordinate
|
|
*/
|
|
public boolean contains(float cx, float cy) {
|
|
return (cx > x && cx < x + width && (
|
|
(cy > y && cy < y + baseHeight) ||
|
|
(expanded && cy > y + offsetY && cy < y + height)));
|
|
}
|
|
|
|
/**
|
|
* Returns true if the coordinates are within the base item bounds.
|
|
* @param cx the x coordinate
|
|
* @param cy the y coordinate
|
|
*/
|
|
public boolean baseContains(float cx, float cy) {
|
|
return (cx > x && cx < x + width && cy > y && cy < y + baseHeight);
|
|
}
|
|
|
|
@Override
|
|
public void render(GUIContext container, Graphics g) throws SlickException {
|
|
// update animation
|
|
long time = container.getTime();
|
|
if (lastUpdateTime > 0) {
|
|
int delta = (int) (time - lastUpdateTime);
|
|
expandProgress.update((expanded) ? delta : -delta * 2);
|
|
}
|
|
this.lastUpdateTime = time;
|
|
|
|
// get parameters
|
|
Input input = container.getInput();
|
|
int idx = getIndexAt(input.getMouseX(), input.getMouseY());
|
|
float t = expandProgress.getValue();
|
|
if (expanded)
|
|
t = AnimationEquation.OUT_CUBIC.calc(t);
|
|
|
|
// background and border
|
|
Color oldGColor = g.getColor();
|
|
float oldLineWidth = g.getLineWidth();
|
|
final int cornerRadius = 6;
|
|
g.setLineWidth(1f);
|
|
g.setColor((idx == -1) ? highlightColor : backgroundColor);
|
|
g.fillRoundRect(x, y, width, baseHeight, cornerRadius);
|
|
g.setColor(borderColor);
|
|
g.drawRoundRect(x, y, width, baseHeight, cornerRadius);
|
|
if (expanded || t >= 0.0001) {
|
|
float oldBackgroundAlpha = backgroundColor.a;
|
|
backgroundColor.a *= t;
|
|
g.setColor(backgroundColor);
|
|
g.fillRoundRect(x, y + offsetY, width, (height - offsetY) * t, cornerRadius);
|
|
backgroundColor.a = oldBackgroundAlpha;
|
|
}
|
|
if (idx >= 0 && t >= 0.9999) {
|
|
g.setColor(highlightColor);
|
|
float yPos = y + offsetY + (offsetY * idx);
|
|
int yOff = 0, hOff = 0;
|
|
if (idx == 0 || idx == items.length - 1) {
|
|
g.fillRoundRect(x, yPos, width, offsetY, cornerRadius);
|
|
if (idx == 0)
|
|
yOff = cornerRadius;
|
|
hOff = cornerRadius;
|
|
}
|
|
g.fillRect(x, yPos + yOff, width, offsetY - hOff);
|
|
}
|
|
g.setColor(oldGColor);
|
|
g.setLineWidth(oldLineWidth);
|
|
|
|
// text
|
|
chevronDown.draw(x + width - chevronDown.getWidth() - width * CHEVRON_X, y + (baseHeight - chevronDown.getHeight()) / 2f, chevronDownColor);
|
|
fontNormal.drawString(x + (width * 0.03f), y + (fontNormal.getPaddingTop() + fontNormal.getPaddingBottom()) / 2f, itemNames[itemIndex], textColor);
|
|
float oldTextAlpha = textColor.a;
|
|
textColor.a *= t;
|
|
if (expanded || t >= 0.0001) {
|
|
for (int i = 0; i < itemNames.length; i++) {
|
|
Font f = (i == itemIndex) ? fontSelected : fontNormal;
|
|
if (i == idx && t >= 0.999)
|
|
chevronRight.draw(x, y + offsetY + (offsetY * i) + (offsetY - chevronRight.getHeight()) / 2f, chevronRightColor);
|
|
f.drawString(x + chevronRight.getWidth(), y + offsetY + (offsetY * i * t), itemNames[i], textColor);
|
|
}
|
|
}
|
|
textColor.a = oldTextAlpha;
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the item at the given location, -1 for the base item,
|
|
* and -2 if there is no item at the location.
|
|
* @param cx the x coordinate
|
|
* @param cy the y coordinate
|
|
*/
|
|
private int getIndexAt(float cx, float cy) {
|
|
if (!contains(cx, cy))
|
|
return -2;
|
|
if (cy <= y + baseHeight)
|
|
return -1;
|
|
if (!expanded)
|
|
return -2;
|
|
return (int) ((cy - (y + offsetY)) / offsetY);
|
|
}
|
|
|
|
/**
|
|
* Resets the menu state.
|
|
*/
|
|
public void reset() {
|
|
this.expanded = false;
|
|
this.lastUpdateTime = 0;
|
|
expandProgress.setTime(0);
|
|
}
|
|
|
|
@Override
|
|
public void mousePressed(int button, int x, int y) {
|
|
if (!active)
|
|
return;
|
|
|
|
if (button == Input.MOUSE_MIDDLE_BUTTON)
|
|
return;
|
|
|
|
int idx = getIndexAt(x, y);
|
|
if (idx == -2) {
|
|
this.expanded = false;
|
|
return;
|
|
}
|
|
if (!menuClicked(idx))
|
|
return;
|
|
this.expanded = (idx == -1) ? !expanded : false;
|
|
if (idx >= 0 && itemIndex != idx) {
|
|
this.itemIndex = idx;
|
|
itemSelected(idx, items[idx]);
|
|
}
|
|
consumeEvent();
|
|
}
|
|
|
|
/**
|
|
* Notification that a new item was selected (via override).
|
|
* @param index the index of the item selected
|
|
* @param item the item selected
|
|
*/
|
|
public void itemSelected(int index, E item) {}
|
|
|
|
/**
|
|
* Notification that the menu was clicked (via override).
|
|
* @param index the index of the item clicked, or -1 for the base item
|
|
* @return true to process the click, or false to block/intercept it
|
|
*/
|
|
public boolean menuClicked(int index) { return true; }
|
|
|
|
@Override
|
|
public void setFocus(boolean focus) { /* does not currently use the "focus" concept */ }
|
|
|
|
@Override
|
|
public void mouseReleased(int button, int x, int y) { /* does not currently use the "focus" concept */ }
|
|
|
|
/**
|
|
* Selects the item at the given index.
|
|
* @param index the list item index
|
|
* @throws IllegalArgumentException if {@code index} is negative or greater than or equal to size
|
|
*/
|
|
public void setSelectedIndex(int index) {
|
|
if (index < 0 || index >= items.length)
|
|
throw new IllegalArgumentException();
|
|
this.itemIndex = index;
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the selected item.
|
|
*/
|
|
public int getSelectedIndex() { return itemIndex; }
|
|
|
|
/**
|
|
* Returns the selected item.
|
|
*/
|
|
public E getSelectedItem() { return items[itemIndex]; }
|
|
|
|
/**
|
|
* Returns the item at the given index.
|
|
* @param index the list item index
|
|
*/
|
|
public E getItemAt(int index) { return items[index]; }
|
|
|
|
/**
|
|
* Returns the number of items in the list.
|
|
*/
|
|
public int getItemCount() { return items.length; }
|
|
|
|
/** Sets the text color. */
|
|
public void setTextColor(Color c) { this.textColor = c; }
|
|
|
|
/** Sets the background color. */
|
|
public void setBackgroundColor(Color c) { this.backgroundColor = c; }
|
|
|
|
/** Sets the highlight color. */
|
|
public void setHighlightColor(Color c) { this.highlightColor = c; }
|
|
|
|
/** Sets the border color. */
|
|
public void setBorderColor(Color c) { this.borderColor = c; }
|
|
|
|
/** Sets the down chevron color. */
|
|
public void setChevronDownColor(Color c) { this.chevronDownColor = c; }
|
|
|
|
/** Sets the right chevron color. */
|
|
public void setChevronRightColor(Color c) { this.chevronRightColor = c; }
|
|
}
|