/*
* opsu! - an open-source osu! client
* Copyright (C) 2014-2017 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.user;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.KineticScrolling;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.ArrayList;
import java.util.List;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.gui.AbstractComponent;
import org.newdawn.slick.gui.GUIContext;
import org.newdawn.slick.gui.TextField;
/**
* User selection overlay.
*/
public class UserSelectOverlay extends AbstractComponent {
/** Listener for events. */
public interface UserSelectOverlayListener {
/**
* Notification that the overlay was closed.
* @param userChanged true if the user was changed
*/
void close(boolean userChanged);
}
/** Whether this component is active. */
private boolean active;
/** Users. */
private List<UserButton> userButtons = new ArrayList<UserButton>();
/** The event listener. */
private final UserSelectOverlayListener listener;
/** The top-left coordinates. */
private float x, y;
/** Dimensions. */
private int width, height;
/** The relative offsets of the title. */
private final int titleY;
/** The relative offsets of the start of the users section. */
private final int usersStartX, usersStartY;
/** Padding between user buttons. */
private final int usersPaddingY;
/** Kinetic scrolling. */
private final KineticScrolling scrolling;
/** The maximum scroll offset. */
private int maxScrollOffset;
/** The current selected button (between a mouse press and release). */
private UserButton selectedButton;
/**
* The y coordinate of a mouse press, recorded in {@link #mousePressed(int, int, int)}.
* If this is -1 directly after a mouse press, then it was not within the overlay.
*/
private int mousePressY = -1;
/** Should all unprocessed events be consumed, and the overlay closed? */
private boolean consumeAndClose = false;
/** Global alpha. */
private float globalAlpha = 1f;
/** Textfield used for entering new user names. */
private TextField textField;
/** New user. */
private User newUser;
/** New user button. */
private UserButton newUserButton;
/** New user icons. */
private MenuButton[] newUserIcons;
/** States. */
private enum State { USER_SELECT, CREATE_USER }
/** Current state. */
private State state = State.USER_SELECT;
/** State change progress. */
private AnimatedValue stateChangeProgress = new AnimatedValue(500, 0f, 1f, AnimationEquation.LINEAR);
/** Colors. */
private static final Color
COLOR_BG = new Color(Color.black),
COLOR_WHITE = new Color(Color.white),
COLOR_GRAY = new Color(Color.lightGray),
COLOR_RED = new Color(Color.red);
// game-related variables
private Input input;
private int containerWidth, containerHeight;
/**
* Creates the user selection overlay.
* @param container the game container
* @param listener the event listener
*/
public UserSelectOverlay(GameContainer container, UserSelectOverlayListener listener) {
super(container);
this.listener = listener;
this.input = container.getInput();
this.containerWidth = container.getWidth();
this.containerHeight = container.getHeight();
// overlay positions
this.x = containerWidth / 3;
this.y = 0;
this.width = containerWidth / 3;
this.height = containerHeight;
// user positions
this.titleY = Fonts.LARGE.getLineHeight() * 2;
this.usersStartX = (width - UserButton.getWidth()) / 2;
this.usersStartY = (int) (titleY + Fonts.XLARGE.getLineHeight() * 1.5f);
this.usersPaddingY = UserButton.getHeight() / 10;
// new user
this.newUser = new User("", UserList.DEFAULT_ICON);
this.newUserButton = new UserButton(
(int) (this.x + usersStartX),
(int) (this.y + usersStartY + Fonts.MEDIUMBOLD.getLineHeight()),
Color.white
);
newUserButton.setUser(newUser);
newUserButton.setHoverAnimationDuration(400);
newUserButton.setHoverAnimationEquation(AnimationEquation.LINEAR);
// new user text field
this.textField = new TextField(container, null, 0, 0, 0, 0);
textField.setMaxLength(UserList.MAX_USER_NAME_LENGTH);
// new user icons
this.newUserIcons = new MenuButton[UserButton.getIconCount()];
for (int i = 0; i < newUserIcons.length; i++) {
newUserIcons[i] = new MenuButton(UserButton.getIconImage(i), 0, 0);
newUserIcons[i].setHoverFade(0.5f);
}
// kinetic scrolling
this.scrolling = new KineticScrolling();
scrolling.setAllowOverScroll(true);
}
@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 height; }
/** Sets the alpha level of the overlay. */
public void setAlpha(float alpha) {
COLOR_BG.a = 0.7f * alpha;
globalAlpha = alpha;
}
/**
* Returns true if the coordinates are within the overlay 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 + height));
}
/** Activates the component. */
public void activate() {
this.active = true;
mousePressY = -1;
globalAlpha = 1f;
// set initial state
state = State.USER_SELECT;
stateChangeProgress.setTime(stateChangeProgress.getDuration());
prepareUserSelect();
}
/** Deactivates the component. */
public void deactivate() { this.active = false; }
/**
* Whether to consume all unprocessed events, and close the overlay.
* @param flag {@code true} to consume all events (default is {@code false})
*/
public void setConsumeAndClose(boolean flag) { this.consumeAndClose = flag; }
@Override
public void render(GUIContext container, Graphics g) throws SlickException {
g.setClip((int) x, (int) y, width, height);
// background
g.setColor(COLOR_BG);
g.fillRect(x, y, width, height);
// render states
if (!stateChangeProgress.isFinished()) {
// blend states
float t = stateChangeProgress.getValue();
if (state == State.CREATE_USER)
t = 1f - t;
renderUserSelect(g, t);
renderUserCreate(g, 1f - t);
} else if (state == State.USER_SELECT)
renderUserSelect(g, globalAlpha);
else if (state == State.CREATE_USER)
renderUserCreate(g, globalAlpha);
g.clearClip();
}
/** Renders the user selection menu. */
private void renderUserSelect(Graphics g, float alpha) {
COLOR_WHITE.a = alpha;
// title
String title = "User Select";
Fonts.XLARGE.drawString(
x + (width - Fonts.XLARGE.getWidth(title)) / 2,
(int) (y + titleY - scrolling.getPosition()),
title, COLOR_WHITE
);
// users
int cx = (int) (x + usersStartX);
int cy = (int) (y + -scrolling.getPosition() + usersStartY);
for (UserButton button : userButtons) {
button.setPosition(cx, cy);
if (cy < height)
button.draw(g, alpha);
cy += UserButton.getHeight() + usersPaddingY;
}
// scrollbar
int scrollbarWidth = 10, scrollbarHeight = 45;
float scrollbarX = x + width - scrollbarWidth;
float scrollbarY = y + (scrolling.getPosition() / maxScrollOffset) * (height - scrollbarHeight);
g.setColor(COLOR_WHITE);
g.fillRect(scrollbarX, scrollbarY, scrollbarWidth, scrollbarHeight);
}
/** Renders the user creation menu. */
private void renderUserCreate(Graphics g, float alpha) {
COLOR_WHITE.a = COLOR_RED.a = alpha;
COLOR_GRAY.a = alpha * 0.8f;
// title
String title = "Add User";
Fonts.XLARGE.drawString(
x + (width - Fonts.XLARGE.getWidth(title)) / 2,
(int) (y + titleY),
title, COLOR_WHITE
);
// user button
int cy = (int) (y + usersStartY);
String caption = "Click the profile below to create it.";
Fonts.MEDIUM.drawString(x + (width - Fonts.MEDIUM.getWidth(caption)) / 2, cy, caption, COLOR_WHITE);
cy += Fonts.MEDIUM.getLineHeight();
newUserButton.draw(g, alpha);
cy += UserButton.getHeight() + Fonts.MEDIUMBOLD.getLineHeight();
// user name
String nameHeader = "Name";
Fonts.MEDIUMBOLD.drawString(x + (width - Fonts.MEDIUMBOLD.getWidth(nameHeader)) / 2, cy, nameHeader, COLOR_WHITE);
cy += Fonts.MEDIUMBOLD.getLineHeight();
Color textColor = COLOR_WHITE;
String name = newUser.getName();
if (name.isEmpty()) {
name = "Type a name...";
textColor = COLOR_GRAY;
} else if (!UserList.get().isValidUserName(name))
textColor = COLOR_RED;
int textWidth = Fonts.LARGE.getWidth(name);
int searchTextX = (int) (x + (width - textWidth) / 2);
Fonts.LARGE.drawString(searchTextX, cy, name, textColor);
cy += Fonts.LARGE.getLineHeight();
g.setColor(textColor);
g.setLineWidth(2f);
g.drawLine(searchTextX, cy, searchTextX + textWidth, cy);
cy += Fonts.MEDIUMBOLD.getLineHeight();
// user icons
String iconHeader = "Icon";
Fonts.MEDIUMBOLD.drawString(x + (width - Fonts.MEDIUMBOLD.getWidth(iconHeader)) / 2, cy, iconHeader, COLOR_WHITE);
cy += Fonts.MEDIUMBOLD.getLineHeight() + usersPaddingY;
int iconSize = UserButton.getIconSize();
int paddingX = iconSize / 4;
int maxPerLine = UserButton.getWidth() / (iconSize + paddingX);
// start scroll area here
g.setClip((int) x, cy, width, height - (int) (cy - y));
int scrollOffset = ((newUserIcons.length - 1) / maxPerLine + 1) * (iconSize + usersPaddingY);
scrollOffset -= height - cy;
scrollOffset = Math.max(scrollOffset, 0);
scrolling.setMinMax(0, scrollOffset);
cy += -scrolling.getPosition();
for (int i = 0; i < newUserIcons.length; i += maxPerLine) {
// draw line-by-line
int n = Math.min(maxPerLine, newUserIcons.length - i);
int cx = (int) (x + usersStartX + (UserButton.getWidth() - iconSize * n - paddingX * (n - 1)) / 2);
for (int j = 0; j < n; j++) {
MenuButton button = newUserIcons[i + j];
button.setX(cx + iconSize / 2);
button.setY(cy + iconSize / 2);
if (cy < height) {
button.getImage().setAlpha((newUser.getIconId() == i + j) ?
alpha : alpha * button.getHoverAlpha() * 0.9f
);
button.getImage().draw(cx, cy);
}
cx += iconSize + paddingX;
}
cy += iconSize + usersPaddingY;
}
}
/**
* Updates the overlay.
* @param delta the delta interval since the last call
*/
public void update(int delta) {
if (!active)
return;
scrolling.update(delta);
stateChangeProgress.update(delta);
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
// user button hover updates
if (state == State.USER_SELECT || !stateChangeProgress.isFinished()) {
UserButton hover = getButtonAtPosition(mouseX, mouseY);
for (UserButton button : userButtons)
button.hoverUpdate(delta, button == hover);
}
if (state == State.CREATE_USER || !stateChangeProgress.isFinished()) {
newUserButton.hoverUpdate(delta, UserList.get().isValidUserName(newUser.getName()));
for (int i = 0; i < newUserIcons.length; i++)
newUserIcons[i].hoverUpdate(delta, mouseX, mouseY);
}
}
@Override
public void mousePressed(int button, int x, int y) {
if (!active)
return;
if (!contains(x, y)) {
if (consumeAndClose) {
consumeEvent();
listener.close(false);
}
return;
}
consumeEvent();
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
scrolling.pressed();
mousePressY = y;
if (state == State.USER_SELECT)
selectedButton = getButtonAtPosition(x, y);
}
@Override
public void mouseReleased(int button, int x, int y) {
if (!active)
return;
consumeEvent();
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
// check if clicked, not dragged
boolean mouseDragged = (Math.abs(y - mousePressY) >= 5);
mousePressY = -1;
scrolling.released();
if (mouseDragged)
return;
if (state == State.USER_SELECT) {
if (selectedButton != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
if (selectedButton.getUser() == null) {
// new user
state = State.CREATE_USER;
stateChangeProgress.setTime(0);
prepareUserCreate();
} else {
// select user
String name = selectedButton.getUser().getName();
if (!name.equals(UserList.get().getCurrentUser().getName())) {
UserList.get().changeUser(name);
listener.close(true);
} else
listener.close(false);
}
}
} else if (state == State.CREATE_USER) {
// add new user
if (newUserButton.contains(x, y))
createNewUser();
else {
// change user icons
for (int i = 0; i < newUserIcons.length; i++) {
if (newUserIcons[i].contains(x, y)) {
SoundController.playSound(SoundEffect.MENUCLICK);
newUser.setIconId(i);
break;
}
}
}
}
selectedButton = null;
}
@Override
public void mouseDragged(int oldx, int oldy, int newx, int newy) {
if (!active)
return;
consumeEvent();
int diff = newy - oldy;
if (diff != 0)
scrolling.dragged(-diff);
}
@Override
public void mouseWheelMoved(int delta) {
if (UI.globalMouseWheelMoved(delta, true)) {
consumeEvent();
return;
}
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
if (!active)
return;
if (!contains(mouseX, mouseY)) {
if (consumeAndClose) {
consumeEvent();
listener.close(false);
}
return;
}
consumeEvent();
scrolling.scrollOffset(-delta);
}
@Override
public void keyPressed(int key, char c) {
if (!active)
return;
consumeEvent();
// esc: close overlay or clear text
if (key == Input.KEY_ESCAPE) {
if (state == State.CREATE_USER && !textField.getText().isEmpty()) {
textField.setText("");
newUser.setName("");
} else
listener.close(false);
return;
}
if (UI.globalKeyPressed(key))
return;
// key entry
if (state == State.CREATE_USER) {
// enter: create user
if (key == Input.KEY_ENTER) {
createNewUser();
return;
}
textField.setFocus(true);
textField.keyPressed(key, c);
textField.setFocus(false);
newUser.setName(textField.getText());
if (c > 255 && Character.isLetterOrDigit(c)) {
Fonts.loadGlyphs(Fonts.LARGE, c);
Fonts.loadGlyphs(Fonts.MEDIUM, c);
}
}
}
/**
* Returns the button at the given position.
* @param cx the x coordinate
* @param cy the y coordinate
* @return the button, or {@code null} if none
*/
private UserButton getButtonAtPosition(int cx, int cy) {
if (cy < y || cx < x + usersStartX || cx > x + usersStartX + UserButton.getWidth())
return null; // out of bounds
for (UserButton button : userButtons) {
if (button.contains(cx, cy))
return button;
}
return null;
}
/** Creates a new user and switches to it. */
private void createNewUser() {
SoundController.playSound(SoundEffect.MENUCLICK);
String name = newUser.getName();
int icon = newUser.getIconId();
if (!UserList.get().isValidUserName(name)) {
String error = name.isEmpty() ? "Enter a name for the user." : "You can't use that name.";
UI.getNotificationManager().sendBarNotification(error);
newUserButton.flash();
} else {
if (UserList.get().createNewUser(name, icon) == null)
UI.getNotificationManager().sendBarNotification("Something wrong happened.");
else {
// change user
UserList.get().changeUser(name);
UI.getNotificationManager().sendNotification("New user created.\nEnjoy the game! :)", Colors.GREEN);
listener.close(true);
}
}
}
/** Prepares the user selection state. */
private void prepareUserSelect() {
selectedButton = null;
// initialize user buttons
userButtons.clear();
UserButton defaultUser = null;
for (User user : UserList.get().getUsers()) {
UserButton button = new UserButton(0, 0, Color.white);
button.setUser(user);
if (user.getName().equals(UserList.DEFAULT_USER_NAME))
defaultUser = button;
else
userButtons.add(button);
}
if (defaultUser != null)
userButtons.add(defaultUser); // add default user at the end
userButtons.add(new UserButton(0, 0, Color.white)); // create new user
maxScrollOffset = Math.max(0,
(UserButton.getHeight() + usersPaddingY) * userButtons.size() -
(int) ((height - usersStartY) * 0.9f));
scrolling.setPosition(0f);
scrolling.setAllowOverScroll(true);
scrolling.setMinMax(0, maxScrollOffset);
}
/** Prepares the user creation state. */
private void prepareUserCreate() {
newUser.setName("");
newUser.setIconId(UserList.DEFAULT_ICON);
textField.setText("");
newUserButton.resetHover();
for (int i = 0; i < newUserIcons.length; i++)
newUserIcons[i].resetHover();
scrolling.setPosition(0f);
scrolling.setAllowOverScroll(false);
scrolling.setMinMax(0, 0); // real max is set in renderUserCreate()
}
@Override
public void setFocus(boolean focus) { /* does not currently use the "focus" concept */ }
}