/*
* 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.options;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.OpsuConstants;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.options.Options.GameOption;
import itdelatrisu.opsu.options.Options.GameOption.OptionType;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.DropdownMenu;
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.AnimationEquation;
import java.util.IdentityHashMap;
import java.util.Map;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
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;
/**
* Options overlay.
*
* @author yugecin (https://github.com/yugecin) (base, heavily modified)
*/
public class OptionsOverlay extends AbstractComponent {
/** Listener for events. */
public interface OptionsOverlayListener {
/** Notification that the overlay was closed. */
void close();
}
/** Whether this component is active. */
private boolean active;
/** The option groups to show. */
private final OptionGroup[] groups;
/** The event listener. */
private final OptionsOverlayListener listener;
/** Control images. */
private final Image sliderBallImg, checkOnImg, checkOffImg;
/** Control image size. */
private final int iconSize;
/** Search image. */
private Image searchImg;
/** Target duration, in ms, of the move animation for the indicator. */
private static final int INDICATOR_MOVE_ANIMATION_TIME = 166;
/** Selected option indicator virtual position. */
private int indicatorPos;
/** Selected option indicator render position. */
private int indicatorRenderPos;
/** Selected option indicator offset to next position. */
private int indicatorOffsetToNextPos;
/** Selected option indicator move to next position animation time past. */
private int indicatorMoveAnimationTime;
/** Target duration, in ms, of the fadeout animation for the indicator. */
private static final int INDICATOR_HIDE_ANIMATION_TIME = 500;
/** Selected option indicator hide animation time past. */
private int indicatorHideAnimationTime;
/** Buttons. */
private final MenuButton backButton, restartButton;
/** The top-left coordinates. */
private float x, y;
/** The width and height of the overlay. */
private final int targetWidth, height;
/** The real width of the overlay, altered by the hide/show animation */
private int width;
/** The current hovered option. */
private GameOption hoverOption;
/** The current selected option (between a mouse press and release). */
private GameOption selectedOption;
/** The relative offsets of the start of the options section. */
private final int optionStartX, optionStartY;
/** The dimensions of an option. */
private int optionWidth, optionHeight;
/** Y offset from the option position to the option text position. */
private int optionTextOffsetY;
/** The size of the control images (sliderball, checkbox). */
private int controlImageSize;
/** The vertical padding for the control images to vertical align them. */
private int controlImagePadding;
/** The width of the grey line next to groups. */
private static final int LINE_WIDTH = 3;
/** Right padding. */
private int paddingRight;
/** Left padding to the grey line. */
private int paddingLeft;
/** Left padding to the option text. */
private int paddingTextLeft;
/** Y position of the options text. */
private int textOptionsY;
/** Y position of the change text. */
private int textChangeY;
/** Y position of the search block. */
private int searchY;
/** Y offset from the search block to the search text. */
private int textSearchYOffset;
/** The padding for an option group title. */
private final int optionGroupPadding;
/** Whether or not a slider is currently being adjusted. */
private boolean isAdjustingSlider;
/** The current absolute x-coordinate of the selected slider. */
private int sliderOptionStartX;
/** The current width of the selected slider. */
private int sliderOptionWidth;
/** HashMap which contains dropdown menus corresponding to options. */
private Map<GameOption, DropdownMenu<Object>> dropdownMenus;
/** The vertical padding to use when rendering a dropdown menu. */
private int dropdownMenuPaddingY;
/** The dropdown menu that is currently open. */
private DropdownMenu<Object> openDropdownMenu;
/**
* The virtual Y position of the open dropdown menu.
* Used to calculate the maximum scrolling offset.
*/
private int openDropdownVirtualY;
/** Kinetic scrolling. */
private final KineticScrolling scrolling;
/** The maximum scroll offset. */
private int maxScrollOffset;
/**
* 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;
/** Last mouse position recorded in {@link #update(int)}. */
private int prevMouseX = -1, prevMouseY = -1;
/** The delay before the next slider movement sound effect, in ms. */
private int sliderSoundDelay;
/** The interval between slider movement sound effects, in ms. */
private static final int SLIDER_SOUND_INTERVAL = 90;
/** Key entry states. */
private boolean keyEntryLeft = false, keyEntryRight = false;
/** Should all unprocessed events be consumed, and the overlay closed? */
private boolean consumeAndClose = false;
/** Whether to show the restart button. */
private boolean showRestartButton = false;
/** Textfield used for searching options. */
private TextField searchField;
/** Last search text. */
private String lastSearchText;
/** Desired alpha values for specific colors. */
private static final float
BG_ALPHA = 0.7f,
LINEALPHA = 0.8f,
INDICATOR_ALPHA = 0.8f;
/** Colors. */
private static final Color
COLOR_BG = new Color(Color.black),
COLOR_WHITE = new Color(Color.white),
COLOR_PINK = new Color(Colors.PINK_OPTION),
COLOR_CYAN = new Color(88, 218, 254),
COLOR_GREY = new Color(55, 55, 57),
COLOR_BLUE = new Color(Colors.BLUE_BACKGROUND),
COLOR_COMBOBOX_HOVER = new Color(185, 19, 121),
COLOR_INDICATOR = new Color(Color.black);
// game-related variables
private GameContainer container;
private Input input;
private int containerWidth, containerHeight;
/**
* Creates the options overlay.
* @param container the game container
* @param groups the option groups
* @param listener the event listener
*/
public OptionsOverlay(GameContainer container, OptionGroup[] groups, OptionsOverlayListener listener) {
super(container);
this.container = container;
this.groups = groups;
this.listener = listener;
this.input = container.getInput();
this.containerWidth = container.getWidth();
this.containerHeight = container.getHeight();
// control images
this.iconSize = (int) (18 * GameImage.getUIscale());
this.sliderBallImg = GameImage.CONTROL_SLIDER_BALL.getImage().getScaledCopy(iconSize, iconSize);
this.checkOnImg = GameImage.CONTROL_CHECK_ON.getImage().getScaledCopy(iconSize, iconSize);
this.checkOffImg = GameImage.CONTROL_CHECK_OFF.getImage().getScaledCopy(iconSize, iconSize);
int searchImgSize = (int) (Fonts.LARGE.getLineHeight() * 0.75f);
this.searchImg = GameImage.SEARCH.getImage().getScaledCopy(searchImgSize, searchImgSize);
// overlay positions
this.x = 0;
this.y = 0;
this.targetWidth = (int) (containerWidth * 0.42f);
this.height = containerHeight;
// option positions
this.paddingRight = (int) (containerWidth * 0.009375f); // not so accurate
this.paddingLeft = (int) (containerWidth * 0.0180f); // not so accurate
this.paddingTextLeft = paddingLeft + LINE_WIDTH + (int) (containerWidth * 0.00625f); // not so accurate
this.optionStartX = paddingTextLeft;
this.textOptionsY = Fonts.LARGE.getLineHeight() * 2;
this.textChangeY = textOptionsY + Fonts.LARGE.getLineHeight();
this.searchY = textChangeY + Fonts.MEDIUM.getLineHeight() * 2;
this.textSearchYOffset = Fonts.MEDIUM.getLineHeight() / 2;
this.optionStartY = searchY + Fonts.MEDIUM.getLineHeight() + Fonts.LARGE.getLineHeight();
int paddingX = 24;
this.optionWidth = targetWidth - paddingX * 2;
this.optionHeight = (int) (Fonts.MEDIUM.getLineHeight() * 1.3f);
this.optionGroupPadding = (int) (Fonts.LARGE.getLineHeight() * 1.5f);
this.optionTextOffsetY = (int) ((optionHeight - Fonts.MEDIUM.getLineHeight()) / 2f);
this.controlImageSize = (int) (Fonts.MEDIUM.getLineHeight() * 0.7f);
this.controlImagePadding = (optionHeight - controlImageSize) / 2;
// back button
int backSize = Fonts.XLARGE.getLineHeight() * 2 / 3;
Image backArrow = GameImage.CHEVRON_LEFT.getImage().getScaledCopy(backSize, backSize);
this.backButton = new MenuButton(backArrow, 0, 0);
backButton.setHoverExpand(1.5f);
// restart button
Image restartImg = GameImage.UPDATE.getImage().getScaledCopy(backSize, backSize);
this.restartButton = new MenuButton(restartImg, 0, 0);
restartButton.setHoverAnimationDuration(2000);
restartButton.setHoverRotate(360);
// search field
this.searchField = new TextField(container, null, 0, 0, 0, 0);
searchField.setMaxLength(20);
// 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; }
public void setWidth(int width) {
this.width = width;
this.optionWidth = width - optionStartX - paddingRight;
}
/** Returns the target width. */
public int getTargetWidth() { return targetWidth; }
/** Sets the alpha level of the overlay. */
public void setAlpha(float alpha) {
COLOR_BG.a = BG_ALPHA * alpha;
COLOR_WHITE.a = alpha;
COLOR_PINK.a = alpha;
COLOR_CYAN.a = alpha;
COLOR_GREY.a = alpha * LINEALPHA;
COLOR_BLUE.a = alpha;
COLOR_COMBOBOX_HOVER.a = alpha;
COLOR_INDICATOR.a = alpha * (1f - (float) indicatorHideAnimationTime / INDICATOR_HIDE_ANIMATION_TIME) * INDICATOR_ALPHA;
}
@Override
public int getHeight() { return height; }
/**
* 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() {
if (active)
return;
this.active = true;
scrolling.setPosition(0f);
if (dropdownMenus == null)
createDropdownMenus();
resetSearch();
for (Map.Entry<GameOption, DropdownMenu<Object>> entry : dropdownMenus.entrySet()) {
GameOption option = entry.getKey();
DropdownMenu<Object> menu = entry.getValue();
// activate component
menu.activate();
// find the current value (maybe find a better way to do this...)
String selectedValue = option.getValueString();
for (int i = 0; i < menu.getItemCount(); i++) {
if (menu.getItemAt(i).toString().equals(selectedValue)) {
menu.setSelectedIndex(i);
break;
}
}
}
}
/** Deactivates the component. */
public void deactivate() {
if (!active)
return;
this.active = false;
searchField.setFocus(false);
for (DropdownMenu<Object> menu : dropdownMenus.values())
menu.deactivate();
resetOpenDropdownMenu();
}
/**
* 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);
// title
String title = "Options";
String subtitle = String.format("Change the way %s behaves", OpsuConstants.PROJECT_NAME);
Fonts.LARGE.drawString(
x + (width - Fonts.LARGE.getWidth(title)) / 2,
(int) (y + textOptionsY - scrolling.getPosition()),
title, COLOR_WHITE
);
Fonts.MEDIUM.drawString(
x + (width - Fonts.MEDIUM.getWidth(subtitle)) / 2,
(int) (y + textChangeY - scrolling.getPosition()),
subtitle, COLOR_PINK
);
// selected option indicator
g.setColor(COLOR_INDICATOR);
g.fillRect(x, indicatorRenderPos - scrolling.getPosition(), width, optionHeight);
// options
renderOptions(g);
if (openDropdownMenu != null) {
openDropdownMenu.render(container, g);
if (!openDropdownMenu.isOpen())
openDropdownMenu = null;
}
// search
int ypos = (int) (y + searchY + textSearchYOffset - scrolling.getPosition());
if (scrolling.getPosition() > searchY) {
ypos = (int) (y + textSearchYOffset);
g.setColor(COLOR_BG);
g.fillRect(x, y, width, textSearchYOffset * 2 + Fonts.LARGE.getLineHeight());
}
String searchText = "Type to search!";
if (lastSearchText.length() > 0)
searchText = lastSearchText;
int searchTextX = (int) (x + (width - Fonts.LARGE.getWidth(searchText) - searchImg.getWidth() - 10) / 2);
Fonts.LARGE.drawString(searchTextX + searchImg.getWidth() + 10, ypos, searchText, COLOR_WHITE);
searchImg.draw(
searchTextX,
ypos + Fonts.LARGE.getLineHeight() * 0.25f,
COLOR_WHITE
);
// back arrow
backButton.setX(x + backButton.getImage().getWidth());
backButton.setY(y + textSearchYOffset + backButton.getImage().getHeight() / 2);
backButton.draw(COLOR_WHITE);
// restart button
if (showRestartButton) {
restartButton.setX(x + width - restartButton.getImage().getWidth() * 1.5f);
restartButton.setY(y + textSearchYOffset + restartButton.getImage().getHeight() / 2);
restartButton.draw(COLOR_WHITE);
}
// 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);
g.clearClip();
// key entry state
if (keyEntryLeft || keyEntryRight) {
g.setColor(COLOR_BG);
g.fillRect(0, 0, containerWidth, containerHeight);
g.setColor(COLOR_WHITE);
String prompt = keyEntryLeft ?
"Press the new left-click key." : "Press the new right-click key.";
String subtext = "Click anywhere or hit ESC to cancel.";
float promptY = (containerHeight - Fonts.XLARGE.getLineHeight() - Fonts.DEFAULT.getLineHeight()) / 2 - 2;
float subtextY = promptY + Fonts.XLARGE.getLineHeight() + 2;
Fonts.XLARGE.drawString((containerWidth - Fonts.XLARGE.getWidth(prompt)) / 2, promptY, prompt);
Fonts.DEFAULT.drawString((containerWidth - Fonts.DEFAULT.getWidth(subtext)) / 2, subtextY, subtext);
}
}
/**
* Renders all options.
* @param g the graphics context
*/
private void renderOptions(Graphics g) throws SlickException {
// render all headers and options
int cy = (int) (y + -scrolling.getPosition() + optionStartY);
int virtualY = 0;
for (OptionGroup group : groups) {
if (!group.isVisible())
continue;
// header
int lineStartY = (int) (cy + Fonts.LARGE.getLineHeight() * 0.6f);
if (group.getOptions() == null) {
// section header
Fonts.XLARGE.drawString(
x + width - Fonts.XLARGE.getWidth(group.getName()) - paddingRight,
(int) (cy + Fonts.XLARGE.getLineHeight() * 0.3f),
group.getName(), COLOR_CYAN
);
} else {
// subsection header
Fonts.MEDIUMBOLD.drawString(x + paddingTextLeft, lineStartY, group.getName(), COLOR_WHITE);
}
cy += optionGroupPadding;
virtualY += optionGroupPadding;
if (group.getOptions() == null)
continue; // header only
// options
int lineHeight = (int) (Fonts.LARGE.getLineHeight() * 0.9f);
boolean finished = false;
for (GameOption option : group.getOptions()) {
if (!option.isVisible())
continue;
// render the option if it fits, or is the open list option
boolean isOpenOption = (openDropdownMenu != null && openDropdownMenu.equals(dropdownMenus.get(option)));
if (cy > -optionHeight || isOpenOption) {
renderOption(g, option, cy);
if (isOpenOption)
openDropdownVirtualY = virtualY;
}
cy += optionHeight;
virtualY += optionHeight;
lineHeight += optionHeight;
if (cy > height) {
finished = true;
break;
}
}
// container rectangle
g.setColor(COLOR_GREY);
g.fillRect(x + paddingLeft, lineStartY, LINE_WIDTH, lineHeight);
if (finished)
break;
}
// recompute max scroll offset
int scrollOffset = 0;
for (OptionGroup group : groups) {
if (!group.isVisible())
continue;
scrollOffset += optionGroupPadding;
if (group.getOptions() == null)
continue;
for (GameOption option : group.getOptions()) {
if (!option.isVisible())
continue;
scrollOffset += optionHeight;
}
}
if (openDropdownMenu != null)
scrollOffset = Math.max(scrollOffset, openDropdownVirtualY + openDropdownMenu.getHeight());
scrollOffset -= (int) ((height - optionStartY) * 0.9f);
scrollOffset = Math.max(scrollOffset, 0);
maxScrollOffset = scrollOffset;
scrolling.setMinMax(0, maxScrollOffset);
}
/**
* Renders the given option.
* @param g the graphics context
* @param option the game option
* @param cy the y coordinate
*/
private void renderOption(Graphics g, GameOption option, int cy) throws SlickException {
OptionType type = option.getType();
Object[] items = option.getItemList();
if (items != null)
renderListOption(g, option, cy);
else if (type == OptionType.BOOLEAN)
renderCheckOption(option, cy);
else if (type == OptionType.NUMERIC)
renderSliderOption(g, option, cy);
else
renderGenericOption(option, cy);
}
/**
* Renders a list option.
* @param g the graphics context
* @param option the game option
* @param cy the y coordinate
*/
private void renderListOption(Graphics g, GameOption option, int cy) throws SlickException {
DropdownMenu<Object> dropdown = dropdownMenus.get(option);
if (dropdown == null)
return; // no options?
// draw option name
int nameWidth = Fonts.MEDIUM.getWidth(option.getName());
Fonts.MEDIUM.drawString(x + optionStartX, cy + optionTextOffsetY, option.getName(), COLOR_WHITE);
nameWidth += 15;
int comboboxStartX = (int) (x + optionStartX + nameWidth);
int comboboxWidth = optionWidth - nameWidth;
if (comboboxWidth < controlImageSize)
return;
// draw dropdown menu
dropdown.setWidth(comboboxWidth);
dropdown.setLocation(comboboxStartX, cy + dropdownMenuPaddingY);
if (dropdown.isOpen()) {
openDropdownMenu = dropdown;
return;
}
if (openDropdownMenu == null)
dropdown.activate();
else
dropdown.deactivate();
dropdown.render(container, g);
}
/**
* Renders a boolean option.
* @param option the game option
* @param cy the y coordinate
*/
private void renderCheckOption(GameOption option, int cy) {
// draw checkbox
if (option.getBooleanValue())
checkOnImg.draw(x + optionStartX, cy + controlImagePadding, COLOR_PINK);
else
checkOffImg.draw(x + optionStartX, cy + controlImagePadding, COLOR_PINK);
// draw option name
Fonts.MEDIUM.drawString(x + optionStartX + 30, cy + optionTextOffsetY, option.getName(), COLOR_WHITE);
}
/**
* Renders a slider option.
* @param g the graphics context
* @param option the game option
* @param cy the y coordinate
*/
private void renderSliderOption(Graphics g, GameOption option, int cy) {
// draw option name and value
final int padding = 10;
String value = option.getValueString();
int nameWidth = Fonts.MEDIUM.getWidth(option.getName());
int valueWidth = Fonts.MEDIUM.getWidth(value);
Fonts.MEDIUM.drawString(x + optionStartX, cy + optionTextOffsetY, option.getName(), COLOR_WHITE);
Fonts.MEDIUM.drawString(x + optionStartX + optionWidth - valueWidth, cy + optionTextOffsetY, value, COLOR_BLUE);
// calculate slider positions
int sliderWidth = optionWidth - nameWidth - padding - padding - valueWidth;
if (sliderWidth <= 1)
return; // menu hasn't slid in far enough to need to draw the slider
int sliderStartX = (int) (x + optionStartX + nameWidth + padding);
if (hoverOption == option) {
sliderOptionStartX = sliderStartX;
if (!isAdjustingSlider)
sliderOptionWidth = sliderWidth;
else
sliderWidth = sliderOptionWidth;
}
int sliderEndX = sliderStartX + sliderWidth;
// draw slider
float sliderValue = (float) (option.getIntegerValue() - option.getMinValue()) / (option.getMaxValue() - option.getMinValue());
float sliderBallPos = sliderStartX + (int) ((sliderWidth - controlImageSize) * sliderValue);
g.setLineWidth(3f);
g.setColor(COLOR_PINK);
if (sliderValue > 0.0001f)
g.drawLine(sliderStartX, cy + optionHeight / 2, sliderBallPos, cy + optionHeight / 2);
sliderBallImg.draw(sliderBallPos, cy + controlImagePadding, COLOR_PINK);
if (sliderValue < 0.999f) {
float oldAlpha = COLOR_PINK.a;
COLOR_PINK.a *= 0.45f;
g.setColor(COLOR_PINK);
g.drawLine(sliderBallPos + controlImageSize + 1, cy + optionHeight / 2, sliderEndX, cy + optionHeight / 2);
COLOR_PINK.a = oldAlpha;
}
}
/**
* Renders a generic option.
* @param option the game option
* @param cy the y coordinate
*/
private void renderGenericOption(GameOption option, int cy) {
// draw option name and value
String value = option.getValueString();
int valueWidth = Fonts.MEDIUM.getWidth(value);
Fonts.MEDIUM.drawString(x + optionStartX, cy + optionTextOffsetY, option.getName(), COLOR_WHITE);
Fonts.MEDIUM.drawString(x + optionStartX + optionWidth - valueWidth, cy + optionTextOffsetY, value, COLOR_BLUE);
}
/**
* Updates the overlay.
* @param delta the delta interval since the last call
*/
public void update(int delta) {
if (!active)
return;
// check if mouse moved
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
boolean mouseMoved;
if (mouseX == prevMouseX && mouseY == prevMouseY)
mouseMoved = false;
else {
mouseMoved = true;
updateHoverOption(mouseX, mouseY);
prevMouseX = mouseX;
prevMouseY = mouseY;
}
// delta updates
if (hoverOption != null && getOptionAtPosition(mouseX, mouseY) == hoverOption && !keyEntryLeft && !keyEntryRight)
UI.updateTooltip(delta, hoverOption.getDescription(), true);
else if (showRestartButton && restartButton.contains(mouseX, mouseY) && !keyEntryLeft && !keyEntryRight)
UI.updateTooltip(delta, "Click to restart the game.", false);
backButton.hoverUpdate(delta, backButton.contains(mouseX, mouseY) && !keyEntryLeft && !keyEntryRight);
if (showRestartButton)
restartButton.autoHoverUpdate(delta, false);
sliderSoundDelay = Math.max(sliderSoundDelay - delta, 0);
scrolling.update(delta);
updateIndicatorAlpha(delta);
// selected option indicator position
indicatorRenderPos = indicatorPos;
if (indicatorMoveAnimationTime > 0) {
indicatorMoveAnimationTime += delta;
if (indicatorMoveAnimationTime > INDICATOR_MOVE_ANIMATION_TIME) {
indicatorMoveAnimationTime = 0;
indicatorRenderPos += indicatorOffsetToNextPos;
indicatorOffsetToNextPos = 0;
indicatorPos = indicatorRenderPos;
} else {
float progress = (float) indicatorMoveAnimationTime / INDICATOR_MOVE_ANIMATION_TIME;
indicatorRenderPos += AnimationEquation.OUT_BACK.calc(progress) * indicatorOffsetToNextPos;
}
}
if (!mouseMoved)
return;
// update slider option
if (isAdjustingSlider)
adjustSlider(mouseX, mouseY);
}
/**
* Updates the alpha value of the selected option indicator.
*/
private void updateIndicatorAlpha(int delta) {
if (hoverOption == null) {
if (indicatorHideAnimationTime < INDICATOR_HIDE_ANIMATION_TIME) {
indicatorHideAnimationTime += delta;
if (indicatorHideAnimationTime > INDICATOR_HIDE_ANIMATION_TIME)
indicatorHideAnimationTime = INDICATOR_HIDE_ANIMATION_TIME;
float progress = AnimationEquation.IN_CUBIC.calc((float) indicatorHideAnimationTime / INDICATOR_HIDE_ANIMATION_TIME);
COLOR_INDICATOR.a = (1f - progress) * INDICATOR_ALPHA;
}
} else if (indicatorHideAnimationTime > 0) {
indicatorHideAnimationTime -= delta * 3;
if (indicatorHideAnimationTime < 0)
indicatorHideAnimationTime = 0;
COLOR_INDICATOR.a = (1f - (float) indicatorHideAnimationTime / INDICATOR_HIDE_ANIMATION_TIME) * INDICATOR_ALPHA;
}
}
/**
* Updates a slider option based on the current mouse coordinates.
* @param mouseX the mouse x coordinate
* @param mouseY the mouse y coordinate
*/
private void updateSliderOption(int mouseX, int mouseY) {
int min = hoverOption.getMinValue();
int max = hoverOption.getMaxValue();
int value = min + Math.round((float) (max - min) * (mouseX - sliderOptionStartX) / sliderOptionWidth);
hoverOption.setValue(Utils.clamp(value, min, max));
}
/**
* Updates the "hovered" option based on the current mouse coordinates.
* @param mouseX the mouse x coordinate
* @param mouseY the mouse y coordinate
*/
private void updateHoverOption(int mouseX, int mouseY) {
if (openDropdownMenu != null || keyEntryLeft || keyEntryRight)
return;
if (selectedOption != null) {
hoverOption = selectedOption;
return;
}
if (mouseX > width) {
hoverOption = null;
return;
}
hoverOption = getOptionAtPosition(mouseX, mouseY);
}
/** Resets the open dropdown menu, if any. */
private void resetOpenDropdownMenu() {
if (openDropdownMenu != null) {
openDropdownMenu.reset();
openDropdownMenu = null;
}
}
/** Shows or hides an option. */
private void toggleOption(GameOption option, boolean visible) {
option.setVisible(visible);
DropdownMenu<Object> menu = dropdownMenus.get(option);
if (menu != null) {
if (visible)
menu.activate();
else
menu.deactivate();
}
}
/**
* Resets the search.
*/
private void resetSearch() {
for (OptionGroup group : groups) {
group.setVisible(true);
if (group.getOptions() == null)
continue;
for (GameOption option : group.getOptions())
toggleOption(option, true);
}
searchField.setText("");
lastSearchText = "";
resetOpenDropdownMenu();
}
/**
* Update the visible options to conform to the search string.
*/
private void updateSearch() {
// matching a header name will match all sub-items
OptionGroup lastHeader = null;
boolean lastHeaderMatches = false;
for (OptionGroup group : groups) {
boolean groupMatches = group.getName().toLowerCase().contains(lastSearchText);
if (group.getOptions() == null) {
lastHeaderMatches = groupMatches;
lastHeader = group;
group.setVisible(false);
continue;
}
boolean allOptionsHidden = true;
for (GameOption option : group.getOptions()) {
if (lastHeaderMatches || groupMatches) {
allOptionsHidden = false;
toggleOption(option, true);
continue;
}
if (option.matches(lastSearchText)) {
allOptionsHidden = false;
toggleOption(option, true);
} else
toggleOption(option, false);
}
if (allOptionsHidden)
group.setVisible(false);
else {
if (lastHeader != null)
lastHeader.setVisible(true);
group.setVisible(true);
}
}
resetOpenDropdownMenu();
updateHoverOption(prevMouseX, prevMouseY);
}
@Override
public void mousePressed(int button, int x, int y) {
// key entry state
if (keyEntryLeft || keyEntryRight) {
keyEntryLeft = keyEntryRight = false;
consumeEvent();
return;
}
if (!active)
return;
if (!contains(x, y)) {
if (consumeAndClose) {
consumeEvent();
listener.close();
}
return;
}
consumeEvent();
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
// back button: close overlay
if (backButton.contains(x, y)) {
SoundController.playSound(SoundEffect.MENUCLICK);
listener.close();
return;
}
// restart button: restart game
if (showRestartButton && restartButton.contains(x, y)) {
container.setForceExit(false);
container.exit();
return;
}
scrolling.pressed();
// clicked an option?
hoverOption = selectedOption = getOptionAtPosition(x, y);
mousePressY = y;
if (hoverOption != null) {
if (hoverOption.getType() == OptionType.NUMERIC) {
isAdjustingSlider = sliderOptionStartX <= x && x < sliderOptionStartX + sliderOptionWidth;
if (isAdjustingSlider) {
SoundController.playSound(SoundEffect.MENUCLICK);
updateSliderOption(x, y);
}
}
}
}
@Override
public void mouseReleased(int button, int x, int y) {
if (!active)
return;
// check if associated mouse press was in the overlay
if (mousePressY == -1)
return;
consumeEvent();
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
// finish adjusting slider
if (isAdjustingSlider) {
isAdjustingSlider = false;
adjustSlider(x, y);
}
// check if clicked, not dragged
boolean mouseDragged = (Math.abs(y - mousePressY) >= 5);
mousePressY = -1;
selectedOption = null;
sliderOptionWidth = 0;
scrolling.released();
if (mouseDragged)
return;
// update based on option type
if (hoverOption != null) {
if (hoverOption.getType() == OptionType.BOOLEAN) {
SoundController.playSound(SoundEffect.MENUCLICK);
boolean oldValue = hoverOption.getBooleanValue();
hoverOption.toggle(container);
// show restart button?
if (oldValue != hoverOption.getBooleanValue() && hoverOption.isRestartRequired()) {
showRestartButton = true;
UI.getNotificationManager().sendBarNotification("Restart to apply changes.");
}
} else if (hoverOption.getItemList() != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
} else if (hoverOption == GameOption.KEY_LEFT) {
SoundController.playSound(SoundEffect.MENUCLICK);
keyEntryLeft = true;
} else if (hoverOption == GameOption.KEY_RIGHT) {
SoundController.playSound(SoundEffect.MENUCLICK);
keyEntryRight = true;
}
}
}
@Override
public void mouseDragged(int oldx, int oldy, int newx, int newy) {
if (!active)
return;
if (!isAdjustingSlider) {
int diff = newy - oldy;
if (diff != 0)
scrolling.dragged(-diff);
}
consumeEvent();
}
@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();
}
return;
}
if (!isAdjustingSlider)
scrolling.scrollOffset(-delta);
updateHoverOption(prevMouseX, prevMouseY);
consumeEvent();
}
@Override
public void keyPressed(int key, char c) {
if (!active)
return;
consumeEvent();
// key entry state
if (keyEntryRight) {
Options.setGameKeyRight(key);
keyEntryRight = false;
return;
} else if (keyEntryLeft) {
Options.setGameKeyLeft(key);
keyEntryLeft = false;
return;
}
// esc: close open list option, otherwise close overlay
if (key == Input.KEY_ESCAPE) {
if (lastSearchText.length() > 0)
resetSearch();
else
listener.close();
return;
}
if (UI.globalKeyPressed(key))
return;
searchField.setFocus(true);
searchField.keyPressed(key, c);
searchField.setFocus(false);
if (!searchField.getText().equals(lastSearchText)) {
lastSearchText = searchField.getText().toLowerCase();
updateSearch();
}
}
/**
* Handles a slider adjustment to the given mouse coordinates.
* @param mouseX the mouse x coordinate
* @param mouseY the mouse y coordinate
*/
private void adjustSlider(int mouseX, int mouseY) {
int oldSliderValue = hoverOption.getIntegerValue();
// set new value
updateSliderOption(mouseX, mouseY);
// play sound effect
if (hoverOption.getIntegerValue() != oldSliderValue && sliderSoundDelay == 0) {
sliderSoundDelay = SLIDER_SOUND_INTERVAL;
SoundController.playSound(SoundEffect.MENUCLICK);
}
}
/**
* Returns the option at the given position, using the current scroll offset.
* @param cx the x coordinate
* @param cy the y coordinate
* @return the option, or {@code null} if none
*/
private GameOption getOptionAtPosition(int cx, int cy) {
if (cy < y || cy < textSearchYOffset * 2 + Fonts.LARGE.getLineHeight() || cx < x + optionStartX || cx > x + optionStartX + optionWidth)
return null; // out of bounds
int mouseVirtualY = (int) (scrolling.getPosition() + cy - y - optionStartY);
for (OptionGroup group : groups) {
if (!group.isVisible())
continue;
mouseVirtualY -= optionGroupPadding;
if (group.getOptions() == null)
continue;
for (GameOption option : group.getOptions()) {
if (!option.isVisible())
continue;
if (mouseVirtualY <= optionHeight) {
if (mouseVirtualY >= 0) {
int indicatorPos = (int) (scrolling.getPosition() + cy - mouseVirtualY);
if (indicatorPos != this.indicatorPos + indicatorOffsetToNextPos) {
this.indicatorPos += indicatorOffsetToNextPos; // finish the current moving animation
indicatorOffsetToNextPos = indicatorPos - this.indicatorPos;
indicatorMoveAnimationTime = 1; // starts animation
}
return option;
}
return null;
}
mouseVirtualY -= optionHeight;
}
}
return null;
}
@Override
public void setFocus(boolean focus) { /* does not currently use the "focus" concept */ }
/** Creates the dropdown menus. */
private void createDropdownMenus() {
this.dropdownMenus = new IdentityHashMap<GameOption, DropdownMenu<Object>>();
for (OptionGroup group : groups) {
if (group.getOptions() == null)
continue;
for (final GameOption option : group.getOptions()) {
Object[] items = option.getItemList();
if (items == null)
continue;
// build dropdown menu
DropdownMenu<Object> menu = new DropdownMenu<Object>(container, items, 0, 0) {
@Override
public void itemSelected(int index, Object item) {
option.selectItem(index, OptionsOverlay.this.container);
// show restart button?
if (option.isRestartRequired()) {
showRestartButton = true;
UI.getNotificationManager().sendBarNotification("Restart to apply changes.");
}
}
@Override
public boolean menuClicked(int index) {
SoundController.playSound(SoundEffect.MENUCLICK);
openDropdownMenu = null;
return true;
}
};
menu.setBackgroundColor(COLOR_BG);
menu.setBorderColor(Color.transparent);
menu.setChevronDownColor(COLOR_WHITE);
menu.setChevronRightColor(COLOR_BG);
menu.setHighlightColor(COLOR_COMBOBOX_HOVER);
menu.setTextColor(COLOR_WHITE);
dropdownMenuPaddingY = (optionHeight - menu.getHeight()) / 2;
dropdownMenus.put(option, menu);
}
}
}
/**
* Resets all state.
*/
public void reset() {
hoverOption = selectedOption = null;
isAdjustingSlider = false;
resetOpenDropdownMenu();
sliderOptionStartX = sliderOptionWidth = 0;
keyEntryLeft = keyEntryRight = false;
mousePressY = -1;
prevMouseX = prevMouseY = -1;
sliderSoundDelay = 0;
backButton.resetHover();
restartButton.resetHover();
}
}