/*
* 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 .
*/
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.
*
* Basic usage:
*
* - Override {@link #menuClicked(int)} to perform actions when the menu is clicked
* (e.g. play a sound effect, block input under certain conditions).
*
- Override {@link #itemSelected(int, Object)} to perform actions when a new item is selected.
*
- Call {@link #activate()}/{@link #deactivate()} whenever the component is needed
* (e.g. in a state's {@code enter} and {@code leave} events.
*
*
* @param the type of the elements in the menu
*/
public class DropdownMenu 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; }
}