Java tutorial
/******************************************************************************* * Copyright 2013 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.mangecailloux.pebble.constant; import java.security.InvalidParameterException; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.graphics.g2d.NinePatch; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; import com.badlogic.gdx.scenes.scene2d.ui.Skin; import com.badlogic.gdx.scenes.scene2d.ui.Table.Debug; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle; import com.badlogic.gdx.scenes.scene2d.ui.TextField; import com.badlogic.gdx.scenes.scene2d.ui.TextField.TextFieldListener; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Disposable; import com.mangecailloux.pebble.directory.Directory; import com.mangecailloux.pebble.tools.TextFieldFilter; import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle; import com.badlogic.gdx.scenes.scene2d.ui.TextField.TextFieldStyle; import com.badlogic.gdx.scenes.scene2d.utils.Align; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.NinePatchDrawable; /** * <p>ConstantEditor displays an UI overlay to tweak your game constants. * Abbreviated CE in further comments.</p> * <p> CE uses or create a {@link Skin}, you need to have these style in you Skin file : * <ul> * <li>{@link TextButtonStyle} called "constantEditor", else it will take the default style.</li> * <li>{@link LabelStyle} called "constantEditor", else it will take the default style.</li> * <li>{@link TextFieldStyle} called "constantEditor", else it will take the default style.</li> * <ul> * You'll also need : * <ul> * <li>A {@link NinePatch} called "constantEditor-pane", else it will try to get the one called "default-pane-noborder", if not you won't have a background in the flick panel.</li> * </ul> * </p> * <p>Don't forget to add to your android manifest the permission to save your constants : android.permission.WRITE_EXTERNAL_STORAGE. * </p> */ public class ConstantEditor implements Disposable { /** padding for the mainTable when the CE is closed */ private final static int mainTablePadding = 5; /** minimal size for the buttons */ private int buttonMinimalSize = 60; /** linked constant manager */ private ConstantManager manager; /** true is the CE is open */ private boolean open; /** true is a change has occured in the constants, and a save is need to validate them*/ private boolean needSave; /** listener for the CE, can be null */ private ConstantEditorListener listener; /** true to debug the tables */ private boolean debug; /** <p>Align of the openButton when the CE is closed. Uses TableLayout Align constants. Top-Right corner by default.</p> * @see BaseTableLayout * */ private int openButtonAlign = Align.top | Align.right; // <UI variables> /** {@link Skin} used for the UI of the CE */ private final Skin skin; /** true if the skin is created by the CE. False if a user uses it's own skin, so we don't dispose it. */ private boolean disposeSkin; /** {@link Stage} for the CE's UI */ private final Stage stage; /** Custom {@link CLickListener} to handle buttons click*/ private final ButtonClickListener buttonClickListener = new ButtonClickListener(); /** Custom {@link TextFieldListener} to handle textField input*/ private final ConstantTextFieldListener textFieldListener = new ConstantTextFieldListener(); /** Main {@link Table} of the CE, contains all the other tables */ private final Table mainTable; /** {@link Table} containing utility buttons (Open, Back, Root, and Save). */ private final Table optionsTable; /** {@link FlickScrollPane} to scroll the constants and directories.*/ private final ScrollPane flickScrollpane; /** Widget of the FlickScrollPane. Contains all the {@link ConstantTable}. */ private final Table flickTable; /** Current Directory to display in the flickTable. */ private Directory<Constant> currentDirectory; /** Pool of {@link TextButton} to avoid runtime garbage when switching of Directory. */ private final Array<TextButton> buttonPool; /** Pool of {@link ConstantTable} to avoid runtime garbage when switching of Directory. */ private final Array<ConstantTable> constantTablePool; /** Open/Close {@link TextButton} for opening/closing the CE.*/ private final TextButton openButton; /** Back {@link TextButton} to navigate backward in the Directory. */ private final TextButton backButton; /** Root {@link TextButton} to return to the root in one go. */ private final TextButton rootButton; /** Save {@link TextButton} to save the constants */ private final TextButton saveButton; // </UI variables> /** * ConstantEditor constructor. * @param _manager {@link ConstantManager} to display. Cannot be null. * @param _skinPath String, path to the skin to use. Will create a new skin, disposed when the CE is disposed. * @param _createStage, To tell the CE to create and manage its own Stage. If not you can add it to your stage by getting the mainTable */ public ConstantEditor(ConstantManager _manager, String _skinPath, boolean _createStage) { this(_manager, new Skin(Gdx.files.internal(_skinPath + ".json")), _createStage); // As we have created a new Skin we dispose when dispose is called it to avoid memory leak. disposeSkin = true; } /** * ConstantEditor constructor. * @param _manager _manager {@link ConstantManager} to display. Cannot be null. * @param _skin {@link Skin} to use for the UI. * @param _width Initial width of the Stage. * @param _height Initial Height of the Stage. */ public ConstantEditor(ConstantManager _manager, Skin _skin, boolean _createStage) { if (_manager == null) throw new InvalidParameterException("ConstantEditor ctor : _manager must not be null"); manager = _manager; // initial directory is the root. currentDirectory = manager.getRoot(); open = false; needSave = false; debug = false; skin = _skin; disposeSkin = false; // Stage creation stage = (_createStage) ? new Stage() : null; // Pools creation buttonPool = new Array<TextButton>(false, 4); constantTablePool = new Array<ConstantTable>(false, 4); // get the button style TextButtonStyle style = null; if (skin.has("constantEditor", TextButtonStyle.class)) style = skin.get("constantEditor", TextButtonStyle.class); else style = skin.get(TextButtonStyle.class); openButton = new TextButton("Open", style); openButton.addListener(buttonClickListener); backButton = new TextButton("Back", style); backButton.addListener(buttonClickListener); backButton.setVisible(false); backButton.setTouchable(Touchable.disabled); rootButton = new TextButton("Root", style); rootButton.addListener(buttonClickListener); rootButton.setVisible(false); rootButton.setTouchable(Touchable.disabled); saveButton = new TextButton("Save", style); saveButton.addListener(buttonClickListener); saveButton.setVisible(false); saveButton.setTouchable(Touchable.disabled); mainTable = new Table(); if (stage != null) stage.addActor(mainTable); flickTable = new Table(); flickTable.top(); NinePatch patch = skin.getPatch("constantEditor-pane"); if (patch == null) patch = skin.getPatch("default-pane-noborder"); flickTable.setBackground(new NinePatchDrawable(patch)); flickScrollpane = new ScrollPane(flickTable); flickScrollpane.setupOverscroll(5, 10, 15); flickScrollpane.setScrollingDisabled(true, false); flickScrollpane.setFlingTime(0.25f); optionsTable = new Table(); refreshOptionTable(); mainTable.pad(mainTablePadding); mainTable.add(optionsTable).expand().fill(); } /**Disposes the Stage if owned and the Skin if it was created by the CE. */ @Override public void dispose() { if (stage != null) stage.dispose(); if (disposeSkin) skin.dispose(); } /** * Sets the Constant Manager to display. * @param _manager ConstantManager to display. */ public void setManager(ConstantManager _manager) { if (_manager == null) throw new InvalidParameterException("ConstantEditor ctor : _manager must not be null"); manager = _manager; currentDirectory = manager.getRoot(); if (open) refreshFlickTable(); } /** * Sets the minimal size for the buttons and ConstantTables. Useful to tweak the size the CE will take on screen. * @param _minimalSize _minimalSize for the UI components. */ public void setUIMinimalSizes(int _minimalSize) { buttonMinimalSize = _minimalSize; Cell<?> cell = optionsTable.getCell(openButton); if (cell != null) cell.minSize(buttonMinimalSize); cell = optionsTable.getCell(backButton); if (cell != null) cell.minSize(buttonMinimalSize); cell = optionsTable.getCell(rootButton); if (cell != null) cell.minSize(buttonMinimalSize); cell = optionsTable.getCell(saveButton); if (cell != null) cell.minSize(buttonMinimalSize); if (open) refreshFlickTable(); } /** * Align of the openButton when the CE is closed. Uses TableLayout Align constants. Top-Right corner by default. * @param _align can be any combination of TableLayout.TOP, TableLayout.RIGHT, TableLayout.LEFT, TableLayout.BOTTOM, TableLayout.CENTER. */ public void setOpenButtonAlign(int _align) { if (openButtonAlign != _align) { openButtonAlign = _align; if (!open) { Cell<?> cell = optionsTable.getCell(openButton); if (cell != null) cell.align(openButtonAlign); } } } /** * True to debug the tables. * @param _debug true to debug. */ public void debug(boolean _debug) { if (_debug != debug) { debug = _debug; if (debug) { mainTable.debug(Debug.all); flickTable.debug(Debug.all); optionsTable.debug(Debug.all); } else { mainTable.debug(Debug.none); flickTable.debug(Debug.none); optionsTable.debug(Debug.none); } } } /** * Must be called whenever your application is resizing. In the resize() method when using libGdx. * @param _width New width of your stage. * @param _height New height of your stage. * @param _stretch Stretch parameter for the Stage. See {@link Stage#setViewport}. */ public void resize(int _width, int _height) { if (stage != null) stage.getViewport().update(_width, _height, true); mainTable.setWidth(_width); mainTable.setHeight(_height); mainTable.invalidate(); } /** * Adds a listener to the CE to callback some events. * @param _listener {@link ConstantEditorListener} to listen to events. Can be null. */ public void setListener(ConstantEditorListener _listener) { listener = _listener; } /** * The CE InputProcessor should be added to the list of InputProcessor of the application. * @return the InputProcessor for the CE. */ public InputProcessor getInputProcessor() { return stage; } /** * If the CE has not created its own stage, you should add the main Table to your own. * @return the main table of the CE. */ public Table getRoot() { return mainTable; } /** * Render function must be called in the Render function of your application. At the end to have an overlay. * If the CE has not created it's own Stage, you don't have to call that function */ public void render(float _fDt) { if (stage != null) { stage.act(_fDt); stage.draw(); // TODO : replug debug infos // if(debug) // Table.drawDebug(stage); } } /** * <p>Update "touchability" and visibility on Root and Back buttons depending on the state of the CE.</p> * <p>They are visible only if the current displayed Directory is not the Root of the linked manager.</p> */ private void updateRootBackButtonsVisibility() { if (currentDirectory == manager.getRoot()) { backButton.setVisible(false); rootButton.setVisible(false); backButton.setTouchable(Touchable.disabled); rootButton.setTouchable(Touchable.disabled); } else { backButton.setVisible(true); rootButton.setVisible(true); backButton.setTouchable(Touchable.enabled); rootButton.setTouchable(Touchable.enabled); } } /** * <p>Update "touchability" and visibility on Save button depending on the state of the CE.</p> * <p>It's displayed only if the CE has made a change on one of the constant.</p> */ private void updateSaveButtonsVisibility() { if (needSave) { saveButton.setVisible(true); saveButton.setTouchable(Touchable.enabled); } else { saveButton.setVisible(false); saveButton.setTouchable(Touchable.disabled); } } /** * <p>Refresh the flickTable according to the state of the CE.</p> */ private void refreshFlickTable() { flickTable.clear(); // if the CE is not open we just clear the table. if (open) { updateRootBackButtonsVisibility(); int buttonPoolIndex = 0; int constantTablePoolIndex = 0; if (currentDirectory.children != null) { for (int i = 0; i < currentDirectory.children.size; i++) { TextButton button = null; // No more free button remaining in the pool, we need to add new button if (buttonPoolIndex == buttonPool.size) { TextButtonStyle style = null; if (skin.has("constantEditor", TextButtonStyle.class)) style = skin.get("constantEditor", TextButtonStyle.class); else style = skin.get(TextButtonStyle.class); button = new TextButton(currentDirectory.children.get(i).name, style); button.setName("directory" + i); button.addListener(buttonClickListener); buttonPool.add(button); } else // we get the button in the pool. { button = buttonPool.get(buttonPoolIndex); button.getLabel().setText(currentDirectory.children.get(i).name); } buttonPoolIndex++; // we add the button to the table. if (button != null) { flickTable.row(); flickTable.add(button).top().expandX().fill().minSize(buttonMinimalSize, buttonMinimalSize); } } } if (currentDirectory.elements != null) { for (int i = 0; i < currentDirectory.elements.size; i++) { ConstantTable constantTable = null; // No more free ConstantTable remaining in the pool, we need to add new one if (constantTablePoolIndex == constantTablePool.size) { constantTable = new ConstantTable(skin, constantTablePoolIndex, buttonMinimalSize / 2, textFieldListener); constantTablePool.add(constantTable); } else // we get the ConstantTable in the pool. { constantTable = constantTablePool.get(constantTablePoolIndex); } constantTablePoolIndex++; if (constantTable != null) { constantTable.initFromConstant(currentDirectory.elements.get(i)); // layout flickTable.row(); flickTable.add(constantTable).center().expandX().fillX().minSize(buttonMinimalSize, buttonMinimalSize); } } } } } /** * Update the optionsTable according the the state of the CE. */ private void refreshOptionTable() { optionsTable.clear(); if (open) { // If we are opened we add all the buttons optionsTable.add(openButton).minSize(buttonMinimalSize).top().right(); optionsTable.row(); optionsTable.add(backButton).minSize(buttonMinimalSize).top().right().expandX(); optionsTable.row(); optionsTable.add(rootButton).minSize(buttonMinimalSize).top().right().expandX(); optionsTable.row(); optionsTable.add(saveButton).minSize(buttonMinimalSize).top().right().expand(); openButton.getLabel().setText("Close"); updateRootBackButtonsVisibility(); updateSaveButtonsVisibility(); } else { // if we are closed we add only the Open Button. optionsTable.add(openButton).minSize(buttonMinimalSize).align(openButtonAlign).expand(); openButton.getLabel().setText("Open"); updateRootBackButtonsVisibility(); updateSaveButtonsVisibility(); } } //------------------------------------------------------------------ // <ButtonClickListener> //------------------------------------------------------------------ /** * Custom ClickListener to handle all buttons click in the CE. */ class ButtonClickListener extends ClickListener { //------------------------------------------------------------------ // <ActionCompletedListener> //------------------------------------------------------------------ class ActionCompletedListener implements Runnable { public static final int NONE = 0; public static final int OPEN = 1; public static final int CLOSE = 2; private int m_mode = NONE; Runnable setMode(int _mode) { m_mode = _mode; return this; } public void run() { if (m_mode == OPEN) { mainTable.setY(0.0f); mainTable.pad(0); mainTable.invalidate(); } else if (m_mode == CLOSE) { //When the anim is finished we effectively do the closing. open = false; refreshOptionTable(); refreshFlickTable(); // We add only the optionTable mainTable.clear(); mainTable.pad(mainTablePadding); mainTable.add(optionsTable).expand().fill(); if (listener != null) listener.onClosing(); mainTable.setX(0.0f); mainTable.setY(0.0f); } } } /** Listener to now if an actor action is completed */ private final ActionCompletedListener m_ActionListener = new ActionCompletedListener(); @Override public void clicked(InputEvent event, float x, float y) { Actor actor = event.getListenerActor(); if (actor == openButton) { if (!open) open(); else close(); } else if (actor == backButton) { back(); } else if (actor == rootButton) { backToRoot(); } else if (actor == saveButton) { save(); } else { handleDirectoryClick(actor); } } /** * Opens the CE. */ void open() { open = true; mainTable.clear(); refreshOptionTable(); refreshFlickTable(); if (listener != null) listener.onOpening(); // if we are open we add the two parts optionTable and flickPane mainTable.add(optionsTable).expand().fill(); mainTable.add(flickScrollpane).expand().fill(); // we launch the openAnimation on the Table float xOffset = mainTable.getWidth() * 0.65f; mainTable.setX(xOffset); mainTable.addAction( Actions.sequence(Actions.moveBy(-xOffset, mainTablePadding, 0.15f, Interpolation.pow3In), Actions.run(m_ActionListener.setMode(ActionCompletedListener.OPEN)))); } /** * Closes the CE. */ void close() { // We launch the close animation. float xOffset = mainTable.getWidth() - openButton.getX() - openButton.getWidth() - mainTablePadding; mainTable.addAction( Actions.sequence(Actions.moveBy(xOffset, -mainTablePadding, 0.15f, Interpolation.pow3In), Actions.run(m_ActionListener.setMode(ActionCompletedListener.CLOSE)))); } /** * Save the constants if needed. */ void save() { if (needSave) { if (manager.saveConstants()) { needSave = false; updateSaveButtonsVisibility(); } } } /** * Return to the root directory. */ void backToRoot() { if (currentDirectory.parent != null) { currentDirectory = manager.getRoot(); refreshFlickTable(); } } /** * Navigates one step backward in the Directory. */ void back() { if (currentDirectory.parent != null) { currentDirectory = currentDirectory.parent; refreshFlickTable(); } } /** * Navigates in the Directory when click on a child Directory Button. * @param actor Actor the click occured on. */ void handleDirectoryClick(Actor actor) { String name = actor.getName(); if (name != null) { String[] splitted = name.split("directory"); if (splitted.length == 2) { // gets the index of the directory. int index = Integer.parseInt(splitted[1]); if (index >= 0 && index < currentDirectory.children.size) { currentDirectory = currentDirectory.children.get(index); refreshFlickTable(); } } } } } //------------------------------------------------------------------ // </ButtonClickListener> //------------------------------------------------------------------ //------------------------------------------------------------------ // <ConstantTextFieldListener> //------------------------------------------------------------------ /** * Custom TextFieldListener to handle all textField input in the CE. */ class ConstantTextFieldListener implements TextFieldListener { @Override public void keyTyped(TextField textField, char key) { handleTextFieldClick(textField, key); } /** * <p>Handles input in the constant textfields.</p> * <p>Note : For now it is handled every time an key is inputed in a textField. We should only do it when a textField is unfocused.<p> * @param textField Inputed textField. * @param key Key entered. */ void handleTextFieldClick(TextField textField, char key) { String name = textField.getName(); String text = textField.getText(); if (name != null) { String[] splitted = name.split("textField"); if (splitted.length == 2) { // we get the index of the textfield. int index = Integer.parseInt(splitted[1]); if (index >= 0 && index < currentDirectory.elements.size) { Constant constant = currentDirectory.elements.get(index); if (text != null && text.length() != 0) { // We try to apply the change. boolean reverse = false; if (constant instanceof ConstantFloat) { ConstantFloat constantFloat = (ConstantFloat) constant; try { constantFloat.value = Float.parseFloat(text); } catch (NumberFormatException e) { reverse = true; } } else if (constant instanceof ConstantInt) { ConstantInt constantInt = (ConstantInt) constant; try { constantInt.value = Integer.parseInt(textField.getText()); } catch (NumberFormatException e) { reverse = true; } } // if the change cannot be applied, we restore the current value of the constant in the textField. if (reverse) { textField.setText(constant.toString()); } else { // We notify the saving and the change on the constant. needSave = true; updateSaveButtonsVisibility(); if (listener != null) listener.onConstantChanged(constant); } } } } } } } //------------------------------------------------------------------ // </ConstantTextFieldListener> //------------------------------------------------------------------ //------------------------------------------------------------------ // <ConstantEditorListener> //------------------------------------------------------------------ /** * Interface to listen to {@link ConstantEditor} events (opening, closing, etc.) */ public static interface ConstantEditorListener { /** Called when the constantEditor is opening.*/ void onOpening(); /** Called when the constantEditor is closing.*/ void onClosing(); /** Called when the value of a {@link Constant} has changed. * @param _constant Constant that have just changed.*/ void onConstantChanged(Constant _constant); } //------------------------------------------------------------------ // </ConstantEditorListener> //------------------------------------------------------------------ //------------------------------------------------------------------ // <ConstantTable> //------------------------------------------------------------------ /** * <p> * ConstantTable is a custom Table to display a {@link Constant}, it extends {@link Table}. * </p> * <p> * It contains : * a {@link Label} to display the constant's name and * a {@link TextField} to display and edit the constant's value. * </p> */ static class ConstantTable extends Table { /** {@link Label} to display the Constant name */ private final Label label; /** {@link Label} to display the Constant name */ private final TextField textField; /** @param _name Name of the {@link Table}, @see Actor * @param _skin {@link Skin} to use for the {@link Label} and {@link TextField}. * @param _id Unique ID, used to have unique names for the Table, Label and TextField. * @param _minimalHeight minimal height for the Label and TextField. * @param _listener {@link TextFieldListener} to know when a TextField is focused. * */ protected ConstantTable(Skin _skin, int _id, int _minimalHeight, TextFieldListener _listener) { super(); LabelStyle labelstyle = null; // Try to get the constantEditor style per default, else get the default one. if (_skin.has("constantEditor", LabelStyle.class)) labelstyle = _skin.get("constantEditor", LabelStyle.class); else labelstyle = _skin.get(LabelStyle.class); // Create new Label, set the right name with the _id label = new Label("", labelstyle); TextFieldStyle textFieldStyle = null; // Try to get the constantEditor style per default, else get the default one. if (_skin.has("constantEditor", TextFieldStyle.class)) textFieldStyle = _skin.get("constantEditor", TextFieldStyle.class); else textFieldStyle = _skin.get(TextFieldStyle.class); // Create new Label, set the right name with the _id textField = new TextField("", textFieldStyle); textField.setName("textField" + _id); // set the listener textField.setTextFieldListener(_listener); // Label is above the TextField, each have the same min Height to have a symmetrical table. add(label).expand().minHeight(_minimalHeight); row(); add(textField).expand().fill().minHeight(_minimalHeight); } /** Initializes the ConstantTable from the constant, sets the text and the textFieldListener. * @param _constant {@link Constant} used to initialize the ConstantTable. * */ protected void initFromConstant(Constant _constant) { // sets the texts label.setText(_constant.name); textField.setText(_constant.toString()); //sets the filter if (_constant instanceof ConstantFloat) textField.setTextFieldFilter(TextFieldFilter.floatFilter); else if (_constant instanceof ConstantInt) textField.setTextFieldFilter(TextFieldFilter.intFilter); } } //------------------------------------------------------------------ // </ConstantTable> //------------------------------------------------------------------ }