package squidpony.squidgrid.gui.gdx; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.Label; import squidpony.IColorCenter; import squidpony.panel.IColoredString; import java.util.ArrayList; import java.util.List; /** * A specialized SquidPanel that is meant for displaying messages in a scrolling pane. You primarily use this class by * calling appendMessage() or appendWrappingMessage(), but the full SquidPanel API is available as well, though it isn't * the best idea to use that set of methods with this class in many cases. Messages can be Strings or IColoredStrings. * Height must be at least 3 cells, because clicking/tapping the top or bottom borders (which are part of the grid's * height, which leaves 1 row in the middle for a message) will scroll up or down. * Created by Tommy Ettinger on 12/10/2015. * * @see LinesPanel An alternative, which is also designed to write messages (not * in a scrolling pane though), but which is backed up by {@link com.badlogic.gdx.scenes.scene2d.Actor} * instead of {@link SquidPanel} (hence better supports tight variable-width fonts) */ public class SquidMessageBox extends SquidPanel { protected ArrayList<IColoredString<Color>> messages = new ArrayList<>(256); protected ArrayList<Label> labels = new ArrayList<>(256); protected int messageIndex = 0; //private static Pattern lineWrapper; private char[][] basicBorders; /** * Creates a bare-bones panel with all default values for text rendering. * * @param gridWidth the number of cells horizontally * @param gridHeight the number of cells vertically, must be at least 3 */ public SquidMessageBox(int gridWidth, int gridHeight) { super(gridWidth, gridHeight); if(gridHeight < 3) throw new IllegalArgumentException("gridHeight must be at least 3, was given: " + gridHeight); basicBorders = assembleBorders(); appendMessage(""); //lineWrapper = Pattern.compile(".{1," + (gridWidth - 2) + "}(\\s|-|$)+"); } /** * Creates a panel with the given grid and cell size. Uses a default square font. * * @param gridWidth the number of cells horizontally * @param gridHeight the number of cells vertically * @param cellWidth the number of horizontal pixels in each cell * @param cellHeight the number of vertical pixels in each cell */ public SquidMessageBox(int gridWidth, int gridHeight, int cellWidth, int cellHeight) { super(gridWidth, gridHeight, cellWidth, cellHeight); if(gridHeight < 3) throw new IllegalArgumentException("gridHeight must be at least 3, was given: " + gridHeight); basicBorders = assembleBorders(); appendMessage(""); //lineWrapper = Pattern.compile(".{1," + (gridWidth - 2) + "}(\\s|-|$)+"); } /** * Builds a panel with the given grid size and all other parameters determined by the factory. Even if sprite images * are being used, a TextCellFactory is still needed to perform sizing and other utility functions. * <p/> * If the TextCellFactory has not yet been initialized, then it will be sized at 12x12 px per cell. If it is null * then a default one will be created and initialized. * * @param gridWidth the number of cells horizontally * @param gridHeight the number of cells vertically * @param factory the factory to use for cell rendering */ public SquidMessageBox(int gridWidth, int gridHeight, TextCellFactory factory) { super(gridWidth, gridHeight, factory); if(gridHeight < 3) throw new IllegalArgumentException("gridHeight must be at least 3, was given: " + gridHeight); basicBorders = assembleBorders(); appendMessage(""); //lineWrapper = Pattern.compile(".{1," + (gridWidth - 2) + "}(\\s|-|$)+"); } /** * Builds a panel with the given grid size and all other parameters determined by the factory. Even if sprite images * are being used, a TextCellFactory is still needed to perform sizing and other utility functions. * <p/> * If the TextCellFactory has not yet been initialized, then it will be sized at 12x12 px per cell. If it is null * then a default one will be created and initialized. * * @param gridWidth the number of cells horizontally * @param gridHeight the number of cells vertically * @param factory the factory to use for cell rendering * @param center The color center to use. Can be {@code null}, but then must be set later on with * {@link #setColorCenter(IColorCenter)}. */ public SquidMessageBox(int gridWidth, int gridHeight, final TextCellFactory factory, IColorCenter<Color> center) { super(gridWidth, gridHeight, factory, center); if(gridHeight < 3) throw new IllegalArgumentException("gridHeight must be at least 3, was given: " + gridHeight); basicBorders = assembleBorders(); appendMessage(""); //lineWrapper = Pattern.compile(".{1," + (gridWidth - 2) + "}(\\s|-|$)+"); } private void makeBordersClickable() { final float cellH = getHeight() / gridHeight; clearListeners(); addListener(new InputListener(){ @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if(x >= 0 && x < getWidth()) { if(y < cellH) { nudgeDown(); return true; } else if(y >= getHeight() - cellH * 2) { nudgeUp(); return true; } } return false; } }); } /** * The primary way of using this class. Appends a new line to the message listing and scrolls to the bottom. * @param message a String that should be no longer than gridWidth - 2; will be truncated otherwise. */ public void appendMessage(String message) { IColoredString.Impl<Color> truncated = new IColoredString.Impl<>(message, defaultForeground); truncated.setLength(gridWidth - 2); messages.add(truncated); messageIndex = messages.size() - 1; } /** * Appends a new line to the message listing and scrolls to the bottom. If the message cannot fit on one line, * it will be word-wrapped and one or more messages will be appended after it. * @param message a String; this method has no specific length restrictions */ public void appendWrappingMessage(String message) { if(message.length() <= gridWidth - 2) { appendMessage(message); return; } List<IColoredString<Color>> truncated = new IColoredString.Impl<>(message, defaultForeground).wrap(gridWidth - 2); for (IColoredString<Color> t : truncated) { appendMessage(t.present()); } messageIndex = messages.size() - 1; } /** * A common way of using this class. Appends a new line as an IColoredString to the message listing and scrolls to * the bottom. * @param message an IColoredString that should be no longer than gridWidth - 2; will be truncated otherwise. */ public void appendMessage(IColoredString<Color> message) { IColoredString.Impl<Color> truncated = new IColoredString.Impl<>(); truncated.append(message); truncated.setLength(gridWidth - 2); messageIndex = messages.size() - 1; } /** * Appends a new line as an IColoredString to the message listing and scrolls to the bottom. If the message cannot * fit on one line, it will be word-wrapped and one or more messages will be appended after it. * @param message an IColoredString with type parameter Color; this method has no specific length restrictions */ public void appendWrappingMessage(IColoredString<Color> message) { if(message.length() <= gridWidth - 2) { appendMessage(message); return; } List<IColoredString<Color>> truncated = message.wrap(gridWidth - 2); for (IColoredString<Color> t : truncated) { appendMessage(t); } messages.addAll(truncated); messageIndex = messages.size() - 1; } /** * Used internally to scroll up by one line, but can also be triggered by your code. */ public void nudgeUp() { messageIndex = Math.max(0, messageIndex - 1); } /** * Used internally to scroll down by one line, but can also be triggered by your code. */ public void nudgeDown() { messageIndex = Math.min(messages.size() - 1, messageIndex + 1); } private char[][] assembleBorders() { char[][] result = new char[gridWidth][gridHeight]; result[0][0] = '┌'; result[gridWidth - 1][0] = '┐'; result[0][gridHeight - 1] = '└'; result[gridWidth - 1][gridHeight - 1] = '┘'; for (int i = 1; i < gridWidth - 1; i++) { result[i][0] = '─'; result[i][gridHeight - 1] = '─'; } for (int y = 1; y < gridHeight - 1; y++) { result[0][y] = '│'; result[gridWidth - 1][y] = '│'; } for (int y = 1; y < gridHeight - 1; y++) { for (int x = 1; x < gridWidth - 1; x++) { result[x][y] = ' '; result[x][y] = ' '; } } return result; } @Override public void draw(Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); put(basicBorders); for (int i = 1; i < gridHeight - 1 && i <= messageIndex; i++) { put(1, gridHeight - 1 - i, messages.get(messageIndex + 1 - i)); } act(Gdx.graphics.getDeltaTime()); } /** * Set the x, y position of the lower left corner, plus set the width and height. * ACTUALLY NEEDED to make the borders clickable. It can't know * the boundaries of the clickable area until it knows its own position and bounds. * * @param x x position in pixels or other units that libGDX is set to use * @param y y position in pixels or other units that libGDX is set to use * @param width the width in pixels (usually) of the message box; changes on resize * @param height the height in pixels (usually) of the message box; changes on resize */ @Override public void setBounds(float x, float y, float width, float height) { super.setBounds(x, y, width, height); makeBordersClickable(); } }