001package squidpony.squidgrid.gui.gdx;
002
003import com.badlogic.gdx.Gdx;
004import com.badlogic.gdx.graphics.Color;
005import com.badlogic.gdx.graphics.g2d.Batch;
006import com.badlogic.gdx.scenes.scene2d.InputEvent;
007import com.badlogic.gdx.scenes.scene2d.InputListener;
008import com.badlogic.gdx.scenes.scene2d.ui.Label;
009import squidpony.IColorCenter;
010import squidpony.panel.IColoredString;
011
012import java.util.ArrayList;
013import java.util.List;
014
015/**
016 * A specialized SquidPanel that is meant for displaying messages in a scrolling pane. You primarily use this class by
017 * calling appendMessage() or appendWrappingMessage(), but the full SquidPanel API is available as well, though it isn't
018 * the best idea to use that set of methods with this class in many cases. Messages can be Strings or IColoredStrings.
019 * Height must be at least 3 cells, because clicking/tapping the top or bottom borders (which are part of the grid's
020 * height, which leaves 1 row in the middle for a message) will scroll up or down.
021 * Created by Tommy Ettinger on 12/10/2015.
022 * 
023 * @see LinesPanel An alternative, which is also designed to write messages (not
024 *      in a scrolling pane though), but which is backed up by {@link com.badlogic.gdx.scenes.scene2d.Actor}
025 *      instead of {@link SquidPanel} (hence better supports tight serif fonts)
026 */
027public class SquidMessageBox extends SquidPanel {
028    protected ArrayList<IColoredString<Color>> messages = new ArrayList<>(256);
029    protected ArrayList<Label> labels = new ArrayList<>(256);
030    protected int messageIndex = 0;
031    //private static Pattern lineWrapper;
032    protected GDXMarkup markup = new GDXMarkup();
033    private char[][] basicBorders;
034    /**
035     * Creates a bare-bones panel with all default values for text rendering.
036     *
037     * @param gridWidth  the number of cells horizontally
038     * @param gridHeight the number of cells vertically, must be at least 3
039     */
040    public SquidMessageBox(int gridWidth, int gridHeight) {
041        super(gridWidth, gridHeight);
042        if(gridHeight < 3)
043            throw new IllegalArgumentException("gridHeight must be at least 3, was given: " + gridHeight);
044        basicBorders = assembleBorders();
045        appendMessage("");
046        //lineWrapper = Pattern.compile(".{1," + (gridWidth - 2) + "}(\\s|-|$)+");
047    }
048
049    /**
050     * Creates a panel with the given grid and cell size. Uses a default square font.
051     *
052     * @param gridWidth  the number of cells horizontally
053     * @param gridHeight the number of cells vertically
054     * @param cellWidth  the number of horizontal pixels in each cell
055     * @param cellHeight the number of vertical pixels in each cell
056     */
057    public SquidMessageBox(int gridWidth, int gridHeight, int cellWidth, int cellHeight) {
058        super(gridWidth, gridHeight, cellWidth, cellHeight);
059        if(gridHeight < 3)
060            throw new IllegalArgumentException("gridHeight must be at least 3, was given: " + gridHeight);
061        basicBorders = assembleBorders();
062        appendMessage("");
063        //lineWrapper = Pattern.compile(".{1," + (gridWidth - 2) + "}(\\s|-|$)+");
064    }
065
066    /**
067     * Builds a panel with the given grid size and all other parameters determined by the factory. Even if sprite images
068     * are being used, a TextCellFactory is still needed to perform sizing and other utility functions.
069     * <p/>
070     * If the TextCellFactory has not yet been initialized, then it will be sized at 12x12 px per cell. If it is null
071     * then a default one will be created and initialized.
072     *
073     * @param gridWidth  the number of cells horizontally
074     * @param gridHeight the number of cells vertically
075     * @param factory    the factory to use for cell rendering
076     */
077    public SquidMessageBox(int gridWidth, int gridHeight, TextCellFactory factory) {
078        super(gridWidth, gridHeight, factory);
079        if(gridHeight < 3)
080            throw new IllegalArgumentException("gridHeight must be at least 3, was given: " + gridHeight);
081        basicBorders = assembleBorders();
082        appendMessage("");
083        //lineWrapper = Pattern.compile(".{1," + (gridWidth - 2) + "}(\\s|-|$)+");
084    }
085
086    /**
087     * Builds a panel with the given grid size and all other parameters determined by the factory. Even if sprite images
088     * are being used, a TextCellFactory is still needed to perform sizing and other utility functions.
089     * <p/>
090     * If the TextCellFactory has not yet been initialized, then it will be sized at 12x12 px per cell. If it is null
091     * then a default one will be created and initialized.
092     *
093     * @param gridWidth  the number of cells horizontally
094     * @param gridHeight the number of cells vertically
095     * @param factory    the factory to use for cell rendering
096     * @param center     The color center to use. Can be {@code null}, but then must be set later on with
097     *                   {@link #setColorCenter(IColorCenter)}.
098     */
099    public SquidMessageBox(int gridWidth, int gridHeight, final TextCellFactory factory, IColorCenter<Color> center) {
100        super(gridWidth, gridHeight, factory, center);
101        if(gridHeight < 3)
102            throw new IllegalArgumentException("gridHeight must be at least 3, was given: " + gridHeight);
103        basicBorders = assembleBorders();
104        appendMessage("");
105        //lineWrapper = Pattern.compile(".{1," + (gridWidth - 2) + "}(\\s|-|$)+");
106
107    }
108    private void makeBordersClickable()
109    {
110        final float cellH = getHeight() / gridHeight;
111        clearListeners();
112        addListener(new InputListener(){
113            @Override
114                        public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
115                if(x >= 0 && x < getWidth())
116                {
117                    if(y < cellH)
118                    {
119                        nudgeDown();
120                        return true;
121                    }
122                    else if(y >= getHeight() - cellH * 2)
123                    {
124                        nudgeUp();
125                        return true;
126                    }
127                }
128                return false;
129            }
130        });
131    }
132
133    /**
134     * The primary way of using this class. Appends a new line to the message listing and scrolls to the bottom.
135     * @param message a String that should be no longer than gridWidth - 2; will be truncated otherwise.
136     */
137    public void appendMessage(String message)
138    {
139        IColoredString.Impl<Color> truncated = new IColoredString.Impl<>(message, defaultForeground);
140        truncated.setLength(gridWidth - 2);
141        messages.add(truncated);
142        messageIndex = messages.size() - 1;
143    }
144    /**
145     * Appends a new line to the message listing and scrolls to the bottom. If the message cannot fit on one line,
146     * it will be word-wrapped and one or more messages will be appended after it.
147     * @param message a String; this method has no specific length restrictions
148     */
149    public void appendWrappingMessage(String message)
150    {
151        if(message.length() <= gridWidth - 2)
152        {
153            appendMessage(message);
154            return;
155        }
156        List<IColoredString<Color>> truncated = new IColoredString.Impl<>(message, defaultForeground).wrap(gridWidth - 2);
157        for (IColoredString<Color> t : truncated)
158        {
159            appendMessage(t.present());
160        }
161        messageIndex = messages.size() - 1;
162    }
163
164    /**
165     * A common way of using this class. Appends a new line as an IColoredString to the message listing and scrolls to
166     * the bottom.
167     * @param message an IColoredString that should be no longer than gridWidth - 2; will be truncated otherwise.
168     */
169    public void appendMessage(IColoredString<Color> message)
170    {
171        IColoredString.Impl<Color> truncated = new IColoredString.Impl<>();
172        truncated.append(message);
173        truncated.setLength(gridWidth - 2);
174        messageIndex = messages.size() - 1;
175    }
176
177    /**
178     * Appends a new line as an IColoredString to the message listing and scrolls to the bottom. If the message cannot
179     * fit on one line, it will be word-wrapped and one or more messages will be appended after it.
180     * @param message an IColoredString with type parameter Color; this method has no specific length restrictions
181     */
182    public void appendWrappingMessage(IColoredString<Color> message)
183    {
184        if(message.length() <= gridWidth - 2)
185        {
186            appendMessage(message);
187            return;
188        }
189        List<IColoredString<Color>> truncated = message.wrap(gridWidth - 2);
190        for (IColoredString<Color> t : truncated)
191        {
192            appendMessage(t);
193        }
194        messages.addAll(truncated);
195        messageIndex = messages.size() - 1;
196    }
197
198    /**
199     * Used internally to scroll up by one line, but can also be triggered by your code.
200     */
201    public void nudgeUp()
202    {
203        messageIndex = Math.max(0, messageIndex - 1);
204    }
205
206    /**
207     * Used internally to scroll down by one line, but can also be triggered by your code.
208     */
209    public void nudgeDown()
210    {
211        messageIndex = Math.min(messages.size() - 1, messageIndex + 1);
212    }
213    private char[][] assembleBorders() {
214        char[][] result = new char[gridWidth][gridHeight];
215        result[0][0] = '┌';
216        result[gridWidth - 1][0] = '┐';
217        result[0][gridHeight - 1] = '└';
218        result[gridWidth - 1][gridHeight - 1] = '┘';
219        for (int i = 1; i < gridWidth - 1; i++) {
220            result[i][0] = '─';
221            result[i][gridHeight - 1] = '─';
222        }
223        for (int y = 1; y < gridHeight - 1; y++) {
224            result[0][y] = '│';
225            result[gridWidth - 1][y] = '│';
226        }
227        for (int y = 1; y < gridHeight - 1; y++) {
228            for (int x = 1; x < gridWidth - 1; x++) {
229                result[x][y] = ' ';
230                result[x][y] = ' ';
231            }
232        }
233        return result;
234    }
235
236    @Override
237    public void draw(Batch batch, float parentAlpha) {
238        super.draw(batch, parentAlpha);
239        put(basicBorders);
240        for (int i = 1; i < gridHeight - 1 && i <= messageIndex; i++) {
241            put(1, gridHeight - 1 - i, messages.get(messageIndex + 1 - i));
242        }
243        act(Gdx.graphics.getDeltaTime());
244    }
245
246    /**
247     * Set the x, y position of the lower left corner, plus set the width and height.
248     * ACTUALLY NEEDED to make the borders clickable. It can't know
249     * the boundaries of the clickable area until it knows its own position and bounds.
250     *
251     * @param x x position in pixels or other units that libGDX is set to use
252     * @param x y position in pixels or other units that libGDX is set to use
253     * @param width the width in pixels (usually) of the message box; changes on resize
254     * @param height the height in pixels (usually) of the message box; changes on resize
255     */
256    @Override
257    public void setBounds(float x, float y, float width, float height) {
258        super.setBounds(x, y, width, height);
259        makeBordersClickable();
260    }
261}