/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion 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. */ package illarion.client.gui.controller.game; import de.lessvoid.nifty.Nifty; import de.lessvoid.nifty.NiftyEventSubscriber; import de.lessvoid.nifty.controls.ButtonClickedEvent; import de.lessvoid.nifty.controls.TextField; import de.lessvoid.nifty.controls.textfield.filter.input.TextFieldInputFilter; import de.lessvoid.nifty.controls.textfield.format.TextFieldDisplayFormat; import de.lessvoid.nifty.elements.Element; import de.lessvoid.nifty.input.NiftyStandardInputEvent; import de.lessvoid.nifty.screen.Screen; import de.lessvoid.nifty.screen.ScreenController; import illarion.client.world.World; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.stream.IntStream; /** * This class takes care for displaying and controlling the number select popup properly. * * @author Martin Karing <nitram@illarion.org> */ public final class NumberSelectPopupHandler implements ScreenController { /** * This is the callback interface for this class. Once the number select popup is closed for confirmed this one * is called. */ public interface Callback { /** * This function is called in case the popup is canceled. */ void popupCanceled(); /** * This function is called in case the user confirms the popup. * * @param value the confirmation value */ void popupConfirmed(int value); } /** * The logging instance for this class. */ @Nonnull private static final Logger LOGGER = LoggerFactory.getLogger(NumberSelectPopupHandler.class); /** * The parent instance of Nifty-GUI. */ private Nifty parentNifty; /** * The screen this popup is assigned to. */ private Screen parentScreen; /** * The currently active popup. */ @Nullable private Element activePopup; /** * The callback assigned to the popup. */ @Nullable private Callback activeCallback; /** * The largest number allowed. */ private int maxNumber; /** * The smallest number allowed. */ private int minNumber; @Override public void bind(@Nonnull Nifty nifty, @Nonnull Screen screen) { parentNifty = nifty; parentScreen = screen; } @Override public void onStartScreen() { parentNifty.subscribeAnnotations(this); } /** * Request a new popup. This will cancel any number input popup that was already opened. * * @param minValue the minimal value that is allowed to be selected by this number select popup * @param maxValue the maximal value that is allowed to be selected by this number select popup * @param callback the callback that is called in case the user interacts with the popup */ public void requestNewPopup( int minValue, int maxValue, @Nonnull Callback callback) { World.getUpdateTaskManager().addTask((container, delta) -> internalCreateNewPopup(minValue, maxValue, callback)); } /** * This function really creates the new topic. * * @param minValue the minimal value that is allowed to be selected by this number select popup * @param maxValue the maximal value that is allowed to be selected by this number select popup * @param callback the callback that is called in case the user interacts with the popup */ private void internalCreateNewPopup( int minValue, int maxValue, @Nonnull Callback callback) { cancelActivePopup(); activePopup = parentNifty.createPopup("numberSelect"); parentNifty.showPopup(parentScreen, activePopup.getId(), activePopup.findElementById("#numberInput")); activeCallback = callback; maxNumber = maxValue; minNumber = minValue; TextField textField = getTextField(); assert textField != null; textField.enableInputFilter(new InputFilter(textField, maxValue, minValue)); textField.setFormat(new TextFieldDisplayFormat() { @Nonnull @Override public CharSequence getDisplaySequence( @Nonnull CharSequence original, int start, int end) { if (original.length() == 0) { return Integer.toString(minValue); } return original.subSequence(start, end); } }); activePopup.addInputHandler(inputEvent -> { if (!(inputEvent instanceof NiftyStandardInputEvent)) { return false; } switch ((NiftyStandardInputEvent) inputEvent) { case Escape: cancelActivePopup(); return true; case SubmitText: confirmActivePopup(); return true; default: return false; } }); textField.setText(""); } /** * Event that arrives in case the right button (+1) is clicked. * * @param topic the topic of the event * @param event the button pressed event */ @NiftyEventSubscriber(pattern = ".+#numberSelectPopup#buttonRight") public void onButtonRightEvent(String topic, ButtonClickedEvent event) { if (activePopup == null) { return; } int currentValue = getCurrentValue(); if ((currentValue + 1) > maxNumber) { returnFocusToTextField(); return; } TextField textField = getTextField(); assert textField != null; textField.setText(Integer.toString(currentValue + 1)); returnFocusToTextField(); } /** * Event that arrives in case the left button (-1) is clicked. * * @param topic the topic of the event * @param event the button pressed event */ @NiftyEventSubscriber(pattern = ".+#numberSelectPopup#buttonLeft") public void onButtonLeftEvent(String topic, ButtonClickedEvent event) { if (activePopup == null) { return; } int currentValue = getCurrentValue(); if ((currentValue - 1) < minNumber) { returnFocusToTextField(); return; } TextField textField = getTextField(); assert textField != null; textField.setText(Integer.toString(currentValue - 1)); returnFocusToTextField(); } /** * Event that arrives in case the confirmation button is clicked. * * @param topic the topic of the event * @param event the button pressed event */ @NiftyEventSubscriber(pattern = ".+#numberSelectPopup#buttonOkay") public void onButtonOkayEvent(String topic, ButtonClickedEvent event) { if (activePopup == null) { return; } confirmActivePopup(); } /** * Event that arrives in case the cancel button is clicked. * * @param topic the topic of the event * @param event the button pressed event */ @NiftyEventSubscriber(pattern = ".+#numberSelectPopup#buttonCancel") public void onButtonCancelEvent(String topic, ButtonClickedEvent event) { if (activePopup == null) { return; } cancelActivePopup(); } @Override public void onEndScreen() { cancelActivePopup(); parentNifty.unsubscribeAnnotations(this); } /** * Get the current value that is displayed inside the text input. * * @return the displayed value or {@code 0} in case there is currently no popup */ private int getCurrentValue() { if (activePopup == null) { return 0; } TextField textField = getTextField(); assert textField != null; return Integer.parseInt(textField.getDisplayedText()); } /** * Set the focus to the text field and on the last written character. */ private void returnFocusToTextField() { TextField field = getTextField(); if (field != null) { field.getElement().setFocus(); field.setCursorPosition(field.getRealText().length()); } } /** * Get the text field control of the currently active popup. * * @return the text field of the popup */ @Nullable private TextField getTextField() { if (activePopup == null) { return null; } return activePopup.findNiftyControl("#numberInput", TextField.class); } /** * Cancel and destroy the currently active popup. This sends a cancel to the callback and removes the active popup. */ private void cancelActivePopup() { if (activePopup != null) { if (activeCallback == null) { LOGGER.error("Number select Callback gone missing!"); } else { activeCallback.popupCanceled(); } parentNifty.closePopup(activePopup.getId()); activePopup = null; activeCallback = null; } } /** * Confirm and destroy the currently active popup. This sends a confirmation to the callback and removes the * active popup. */ private void confirmActivePopup() { if (activePopup != null) { if (activeCallback == null) { LOGGER.error("Number select Callback gone missing!"); } else { activeCallback.popupConfirmed(getCurrentValue()); } parentNifty.closePopup(activePopup.getId()); activePopup = null; activeCallback = null; } } private static class InputFilter implements TextFieldInputFilter { @Nonnull private final TextField textField; private final int maxValue; private final int minValue; public InputFilter(@Nonnull TextField textField, int maxValue, int minValue) { this.textField = textField; this.maxValue = maxValue; this.minValue = minValue; } @Override public boolean acceptInput(int index, @Nonnull CharSequence newChars) { try (IntStream newCharStream = newChars.chars()) { if (!newCharStream.allMatch(Character::isDigit)) { return false; } } String currentText = textField.getRealText(); StringBuilder buffer = new StringBuilder(currentText); buffer.insert(index, newChars); return isValidNumber(buffer); } @Override public boolean acceptInput(int index, char newChar) { if (!Character.isDigit(newChar)) { return false; } String currentText = textField.getRealText(); StringBuilder buffer = new StringBuilder(currentText); buffer.insert(index, newChar); return isValidNumber(buffer); } private boolean isValidNumber(@Nonnull CharSequence sequence) { String text = sequence.toString(); int value = Integer.parseInt(text); return !((value > maxValue) || (value < minValue)); } } }