/* * 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: *

* * @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; /** Should the next click be blocked? */ private boolean blockClick = false; /** * 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); blockClick = false; } @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]); } blockClick = true; consumeEvent(); } @Override public void mouseClicked(int button, int x, int y, int clickCount) { if (!active) return; if (blockClick) { blockClick = false; 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; } }