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