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}