reflowable text box
[fanfix.git] / src / jexer / TText.java
1 /**
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer;
32
33 import java.util.LinkedList;
34 import java.util.List;
35
36 import jexer.bits.CellAttributes;
37 import jexer.event.TKeypressEvent;
38 import jexer.event.TMouseEvent;
39 import static jexer.TKeypress.*;
40
41 /**
42 * TText implements a simple text windget.
43 */
44 public final class TText extends TWidget {
45
46 /**
47 * Text to display.
48 */
49 private String text;
50
51 /**
52 * Text converted to lines.
53 */
54 private List<String> lines;
55
56 /**
57 * Text color.
58 */
59 private String colorKey;
60
61 /**
62 * Vertical scrollbar.
63 */
64 private TVScroller vScroller;
65
66 /**
67 * Horizontal scrollbar.
68 */
69 private THScroller hScroller;
70
71 /**
72 * Maximum width of a single line.
73 */
74 private int maxLineWidth;
75
76 /**
77 * Number of lines between each paragraph.
78 */
79 private int lineSpacing = 1;
80
81 /**
82 * Convenience method used by TWindowLoggerOutput.
83 *
84 * @param line new line to add
85 */
86 public void addLine(final String line) {
87 if (text.length() == 0) {
88 text = line;
89 } else {
90 text += "\n\n";
91 text += line;
92 }
93 reflow();
94 }
95
96 /**
97 * Recompute the bounds for the scrollbars.
98 */
99 private void computeBounds() {
100 maxLineWidth = 0;
101 for (String line: lines) {
102 if (line.length() > maxLineWidth) {
103 maxLineWidth = line.length();
104 }
105 }
106
107 vScroller.setBottomValue(lines.size() - getHeight() + 1);
108 if (vScroller.getBottomValue() < 0) {
109 vScroller.setBottomValue(0);
110 }
111 if (vScroller.getValue() > vScroller.getBottomValue()) {
112 vScroller.setValue(vScroller.getBottomValue());
113 }
114
115 hScroller.setRightValue(maxLineWidth - getWidth() + 1);
116 if (hScroller.getRightValue() < 0) {
117 hScroller.setRightValue(0);
118 }
119 if (hScroller.getValue() > hScroller.getRightValue()) {
120 hScroller.setValue(hScroller.getRightValue());
121 }
122 }
123
124 /**
125 * Insert newlines into a string to wrap it to a maximum column.
126 * Terminate the final string with a newline. Note that interior
127 * newlines are converted to spaces.
128 *
129 * @param str the string
130 * @param n the maximum number of characters in a line
131 * @return the wrapped string
132 */
133 private String wrap(final String str, final int n) {
134 assert (n > 0);
135
136 StringBuilder sb = new StringBuilder();
137 StringBuilder word = new StringBuilder();
138 int col = 0;
139 for (int i = 0; i < str.length(); i++) {
140 char ch = str.charAt(i);
141 if (ch == '\n') {
142 ch = ' ';
143 }
144 if (ch == ' ') {
145 sb.append(word.toString());
146 sb.append(ch);
147 if (word.length() >= n - 1) {
148 sb.append('\n');
149 col = 0;
150 }
151 word = new StringBuilder();
152 } else {
153 word.append(ch);
154 }
155
156 col++;
157 if (col >= n - 1) {
158 sb.append('\n');
159 col = 0;
160 }
161 }
162 sb.append(word.toString());
163 sb.append('\n');
164 return sb.toString();
165 }
166
167
168 /**
169 * Resize text and scrollbars for a new width/height.
170 */
171 public void reflow() {
172 // Reset the lines
173 lines.clear();
174
175 // Break up text into paragraphs
176 String [] paragraphs = text.split("\n\n");
177 for (String p: paragraphs) {
178 String paragraph = wrap(p, getWidth() - 1);
179 for (String line: paragraph.split("\n")) {
180 lines.add(line);
181 }
182 for (int i = 0; i < lineSpacing; i++) {
183 lines.add("");
184 }
185 }
186
187 // Start at the top
188 if (vScroller == null) {
189 vScroller = new TVScroller(this, getWidth() - 1, 0,
190 getHeight() - 1);
191 vScroller.setTopValue(0);
192 vScroller.setValue(0);
193 } else {
194 vScroller.setX(getWidth() - 1);
195 vScroller.setHeight(getHeight() - 1);
196 }
197 vScroller.setBigChange(getHeight() - 1);
198
199 // Start at the left
200 if (hScroller == null) {
201 hScroller = new THScroller(this, 0, getHeight() - 1,
202 getWidth() - 1);
203 hScroller.setLeftValue(0);
204 hScroller.setValue(0);
205 } else {
206 hScroller.setY(getHeight() - 1);
207 hScroller.setWidth(getWidth() - 1);
208 }
209 hScroller.setBigChange(getWidth() - 1);
210
211 computeBounds();
212 }
213
214 /**
215 * Public constructor.
216 *
217 * @param parent parent widget
218 * @param text text on the screen
219 * @param x column relative to parent
220 * @param y row relative to parent
221 * @param width width of text area
222 * @param height height of text area
223 */
224 public TText(final TWidget parent, final String text, final int x,
225 final int y, final int width, final int height) {
226
227 this(parent, text, x, y, width, height, "ttext");
228 }
229
230 /**
231 * Public constructor.
232 *
233 * @param parent parent widget
234 * @param text text on the screen
235 * @param x column relative to parent
236 * @param y row relative to parent
237 * @param width width of text area
238 * @param height height of text area
239 * @param colorKey ColorTheme key color to use for foreground text.
240 * Default is "ttext"
241 */
242 public TText(final TWidget parent, final String text, final int x,
243 final int y, final int width, final int height, final String colorKey) {
244
245 // Set parent and window
246 super(parent);
247
248 setX(x);
249 setY(y);
250 setWidth(width);
251 setHeight(height);
252 this.text = text;
253 this.colorKey = colorKey;
254
255 lines = new LinkedList<String>();
256
257 reflow();
258 }
259
260 /**
261 * Draw the text box.
262 */
263 @Override
264 public void draw() {
265 // Setup my color
266 CellAttributes color = getTheme().getColor(colorKey);
267
268 int begin = vScroller.getValue();
269 int topY = 0;
270 for (int i = begin; i < lines.size(); i++) {
271 String line = lines.get(i);
272 if (hScroller.getValue() < line.length()) {
273 line = line.substring(hScroller.getValue());
274 } else {
275 line = "";
276 }
277 String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
278 getScreen().putStrXY(0, topY, String.format(formatString, line),
279 color);
280 topY++;
281
282 if (topY >= getHeight() - 1) {
283 break;
284 }
285 }
286
287 // Pad the rest with blank lines
288 for (int i = topY; i < getHeight() - 1; i++) {
289 getScreen().hLineXY(0, i, getWidth() - 1, ' ', color);
290 }
291
292 }
293
294 /**
295 * Handle mouse press events.
296 *
297 * @param mouse mouse button press event
298 */
299 @Override
300 public void onMouseDown(final TMouseEvent mouse) {
301 if (mouse.getMouseWheelUp()) {
302 vScroller.decrement();
303 return;
304 }
305 if (mouse.getMouseWheelDown()) {
306 vScroller.increment();
307 return;
308 }
309
310 // Pass to children
311 super.onMouseDown(mouse);
312 }
313
314 /**
315 * Handle keystrokes.
316 *
317 * @param keypress keystroke event
318 */
319 @Override
320 public void onKeypress(final TKeypressEvent keypress) {
321 if (keypress.equals(kbLeft)) {
322 hScroller.decrement();
323 } else if (keypress.equals(kbRight)) {
324 hScroller.increment();
325 } else if (keypress.equals(kbUp)) {
326 vScroller.decrement();
327 } else if (keypress.equals(kbDown)) {
328 vScroller.increment();
329 } else if (keypress.equals(kbPgUp)) {
330 vScroller.bigDecrement();
331 } else if (keypress.equals(kbPgDn)) {
332 vScroller.bigIncrement();
333 } else if (keypress.equals(kbHome)) {
334 vScroller.toTop();
335 } else if (keypress.equals(kbEnd)) {
336 vScroller.toBottom();
337 } else {
338 // Pass other keys (tab etc.) on
339 super.onKeypress(keypress);
340 }
341 }
342
343 }