Change build scripts
[jvcard.git] / src / com / googlecode / lanterna / gui2 / TextBox.java
CommitLineData
a3b510ab
NR
1/*
2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
3 *
4 * lanterna is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Copyright (C) 2010-2015 Martin
18 */
19package com.googlecode.lanterna.gui2;
20
21import com.googlecode.lanterna.TerminalTextUtils;
22import com.googlecode.lanterna.TerminalPosition;
23import com.googlecode.lanterna.TerminalSize;
24import com.googlecode.lanterna.input.KeyStroke;
25
26import java.util.ArrayList;
27import java.util.List;
28import java.util.regex.Pattern;
29
30/**
31 * This component keeps a text content that is editable by the user. A TextBox can be single line or multiline and lets
32 * the user navigate the cursor in the text area by using the arrow keys, page up, page down, home and end. For
33 * multi-line {@code TextBox}:es, scrollbars will be automatically displayed if needed.
34 * <p>
35 * Size-wise, a {@code TextBox} should be hard-coded to a particular size, it's not good at guessing how large it should
36 * be. You can do this through the constructor.
37 */
38public class TextBox extends AbstractInteractableComponent<TextBox> {
39
40 /**
41 * Enum value to force a {@code TextBox} to be either single line or multi line. This is usually auto-detected if
42 * the text box has some initial content by scanning that content for \n characters.
43 */
44 public enum Style {
45 /**
46 * The {@code TextBox} contains a single line of text and is typically drawn on one row
47 */
48 SINGLE_LINE,
49 /**
50 * The {@code TextBox} contains a none, one or many lines of text and is normally drawn over multiple lines
51 */
52 MULTI_LINE,
53 ;
54 }
55
56 private final List<String> lines;
57 private final Style style;
58
59 private TerminalPosition caretPosition;
60 private boolean caretWarp;
61 private boolean readOnly;
62 private boolean horizontalFocusSwitching;
63 private boolean verticalFocusSwitching;
64 private int maxLineLength;
65 private int longestRow;
66 private char unusedSpaceCharacter;
67 private Character mask;
68 private Pattern validationPattern;
69
70 /**
71 * Default constructor, this creates a single-line {@code TextBox} of size 10 which is initially empty
72 */
73 public TextBox() {
74 this(new TerminalSize(10, 1), "", Style.SINGLE_LINE);
75 }
76
77 /**
78 * Constructor that creates a {@code TextBox} with an initial content and attempting to be big enough to display
79 * the whole text at once without scrollbars
80 * @param initialContent Initial content of the {@code TextBox}
81 */
82 public TextBox(String initialContent) {
83 this(null, initialContent, initialContent.contains("\n") ? Style.MULTI_LINE : Style.SINGLE_LINE);
84 }
85
86 /**
87 * Creates a {@code TextBox} that has an initial content and attempting to be big enough to display the whole text
88 * at once without scrollbars.
89 *
90 * @param initialContent Initial content of the {@code TextBox}
91 * @param style Forced style instead of auto-detecting
92 */
93 public TextBox(String initialContent, Style style) {
94 this(null, initialContent, style);
95 }
96
97 /**
98 * Creates a new empty {@code TextBox} with a specific size
99 * @param preferredSize Size of the {@code TextBox}
100 */
101 public TextBox(TerminalSize preferredSize) {
102 this(preferredSize, (preferredSize != null && preferredSize.getRows() > 1) ? Style.MULTI_LINE : Style.SINGLE_LINE);
103 }
104
105 /**
106 * Creates a new empty {@code TextBox} with a specific size and style
107 * @param preferredSize Size of the {@code TextBox}
108 * @param style Style to use
109 */
110 public TextBox(TerminalSize preferredSize, Style style) {
111 this(preferredSize, "", style);
112 }
113
114 /**
115 * Creates a new empty {@code TextBox} with a specific size and initial content
116 * @param preferredSize Size of the {@code TextBox}
117 * @param initialContent Initial content of the {@code TextBox}
118 */
119 public TextBox(TerminalSize preferredSize, String initialContent) {
120 this(preferredSize, initialContent, (preferredSize != null && preferredSize.getRows() > 1) || initialContent.contains("\n") ? Style.MULTI_LINE : Style.SINGLE_LINE);
121 }
122
123 /**
124 * Main constructor of the {@code TextBox} which decides size, initial content and style
125 * @param preferredSize Size of the {@code TextBox}
126 * @param initialContent Initial content of the {@code TextBox}
127 * @param style Style to use for this {@code TextBox}, instead of auto-detecting
128 */
129 public TextBox(TerminalSize preferredSize, String initialContent, Style style) {
130 this.lines = new ArrayList<String>();
131 this.style = style;
132 this.readOnly = false;
133 this.caretWarp = false;
134 this.verticalFocusSwitching = true;
135 this.horizontalFocusSwitching = (style == Style.SINGLE_LINE);
136 this.caretPosition = TerminalPosition.TOP_LEFT_CORNER;
137 this.maxLineLength = -1;
138 this.longestRow = 1; //To fit the cursor
139 this.unusedSpaceCharacter = ' ';
140 this.mask = null;
141 this.validationPattern = null;
142 setText(initialContent);
143 if (preferredSize == null) {
144 preferredSize = new TerminalSize(Math.max(10, longestRow), lines.size());
145 }
146 setPreferredSize(preferredSize);
147 }
148
149 /**
150 * Sets a pattern on which the content of the text box is to be validated. For multi-line TextBox:s, the pattern is
151 * checked against each line individually, not the content as a whole. Partial matchings will not be allowed, the
152 * whole pattern must match, however, empty lines will always be allowed. When the user tried to modify the content
153 * of the TextBox in a way that does not match the pattern, the operation will be silently ignored. If you set this
154 * pattern to {@code null}, all validation is turned off.
155 * @param validationPattern Pattern to validate the lines in this TextBox against, or {@code null} to disable
156 * @return itself
157 */
158 public synchronized TextBox setValidationPattern(Pattern validationPattern) {
159 if(validationPattern != null) {
160 for(String line: lines) {
161 if(!validated(line)) {
162 throw new IllegalStateException("TextBox validation pattern " + validationPattern + " does not match existing content");
163 }
164 }
165 }
166 this.validationPattern = validationPattern;
167 return this;
168 }
169
170 /**
171 * Updates the text content of the {@code TextBox} to the supplied string.
172 * @param text New text to assign to the {@code TextBox}
173 * @return Itself
174 */
175 public synchronized TextBox setText(String text) {
176 String[] split = text.split("\n");
177 lines.clear();
178 longestRow = 1;
179 for(String line : split) {
180 addLine(line);
181 }
182 if(caretPosition.getRow() > lines.size() - 1) {
183 caretPosition = caretPosition.withRow(lines.size() - 1);
184 }
185 if(caretPosition.getColumn() > lines.get(caretPosition.getRow()).length()) {
186 caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
187 }
188 invalidate();
189 return this;
190 }
191
192 @Override
193 public TextBoxRenderer getRenderer() {
194 return (TextBoxRenderer)super.getRenderer();
195 }
196
197 /**
198 * Adds a single line to the {@code TextBox} at the end, this only works when in multi-line mode
199 * @param line Line to add at the end of the content in this {@code TextBox}
200 * @return Itself
201 */
202 public synchronized TextBox addLine(String line) {
203 StringBuilder bob = new StringBuilder();
204 for(int i = 0; i < line.length(); i++) {
205 char c = line.charAt(i);
206 if(c == '\n' && style == Style.MULTI_LINE) {
207 String string = bob.toString();
208 int lineWidth = TerminalTextUtils.getColumnWidth(string);
209 lines.add(string);
210 if(longestRow < lineWidth + 1) {
211 longestRow = lineWidth + 1;
212 }
213 addLine(line.substring(i + 1));
214 return this;
215 }
216 else if(Character.isISOControl(c)) {
217 continue;
218 }
219
220 bob.append(c);
221 }
222 String string = bob.toString();
223 if(!validated(string)) {
224 throw new IllegalStateException("TextBox validation pattern " + validationPattern + " does not match the supplied text");
225 }
226 int lineWidth = TerminalTextUtils.getColumnWidth(string);
227 lines.add(string);
228 if(longestRow < lineWidth + 1) {
229 longestRow = lineWidth + 1;
230 }
231 invalidate();
232 return this;
233 }
234
235 /**
236 * Sets if the caret should jump to the beginning of the next line if right arrow is pressed while at the end of a
237 * line. Similarly, pressing left arrow at the beginning of a line will make the caret jump to the end of the
238 * previous line. This only makes sense for multi-line TextBox:es; for single-line ones it has no effect. By default
239 * this is {@code false}.
240 * @param caretWarp Whether the caret will warp at the beginning/end of lines
241 * @return Itself
242 */
243 public TextBox setCaretWarp(boolean caretWarp) {
244 this.caretWarp = caretWarp;
245 return this;
246 }
247
248 /**
249 * Checks whether caret warp mode is enabled or not. See {@code setCaretWarp} for more details.
250 * @return {@code true} if caret warp mode is enabled
251 */
252 public boolean isCaretWarp() {
253 return caretWarp;
254 }
255
256 /**
257 * Returns the position of the caret, as a {@code TerminalPosition} where the row and columns equals the coordinates
258 * in a multi-line {@code TextBox} and for single-line {@code TextBox} you can ignore the {@code row} component.
259 * @return Position of the text input caret
260 */
261 public TerminalPosition getCaretPosition() {
262 return caretPosition;
263 }
264
265 /**
266 * Returns the text in this {@code TextBox}, for multi-line mode all lines will be concatenated together with \n as
267 * separator.
268 * @return The text inside this {@code TextBox}
269 */
270 public synchronized String getText() {
271 StringBuilder bob = new StringBuilder(lines.get(0));
272 for(int i = 1; i < lines.size(); i++) {
273 bob.append("\n").append(lines.get(i));
274 }
275 return bob.toString();
276 }
277
278 /**
279 * Helper method, it will return the content of the {@code TextBox} unless it's empty in which case it will return
280 * the supplied default value
281 * @param defaultValueIfEmpty Value to return if the {@code TextBox} is empty
282 * @return Text in the {@code TextBox} or {@code defaultValueIfEmpty} is the {@code TextBox} is empty
283 */
284 public String getTextOrDefault(String defaultValueIfEmpty) {
285 String text = getText();
286 if(text.isEmpty()) {
287 return defaultValueIfEmpty;
288 }
289 return text;
290 }
291
292 /**
293 * Returns the current text mask, meaning the substitute to draw instead of the text inside the {@code TextBox}.
294 * This is normally used for password input fields so the password isn't shown
295 * @return Current text mask or {@code null} if there is no mask
296 */
297 public Character getMask() {
298 return mask;
299 }
300
301 /**
302 * Sets the current text mask, meaning the substitute to draw instead of the text inside the {@code TextBox}.
303 * This is normally used for password input fields so the password isn't shown
304 * @param mask New text mask or {@code null} if there is no mask
305 * @return Itself
306 */
307 public TextBox setMask(Character mask) {
308 if(mask != null && TerminalTextUtils.isCharCJK(mask)) {
309 throw new IllegalArgumentException("Cannot use a CJK character as a mask");
310 }
311 this.mask = mask;
312 invalidate();
313 return this;
314 }
315
316 /**
317 * Returns {@code true} if this {@code TextBox} is in read-only mode, meaning text input from the user through the
318 * keyboard is prevented
319 * @return {@code true} if this {@code TextBox} is in read-only mode
320 */
321 public boolean isReadOnly() {
322 return readOnly;
323 }
324
325 /**
326 * Sets the read-only mode of the {@code TextBox}, meaning text input from the user through the keyboard is
327 * prevented. The user can still focus and scroll through the text in this mode.
328 * @param readOnly If {@code true} then the {@code TextBox} will switch to read-only mode
329 * @return Itself
330 */
331 public TextBox setReadOnly(boolean readOnly) {
332 this.readOnly = readOnly;
333 invalidate();
334 return this;
335 }
336
337 /**
338 * If {@code true}, the component will switch to the next available component above if the cursor is at the top of
339 * the TextBox and the user presses the 'up' array key, or switch to the next available component below if the
340 * cursor is at the bottom of the TextBox and the user presses the 'down' array key. The means that for single-line
341 * TextBox:es, pressing up and down will always switch focus.
342 * @return {@code true} if vertical focus switching is enabled
343 */
344 public boolean isVerticalFocusSwitching() {
345 return verticalFocusSwitching;
346 }
347
348 /**
349 * If set to {@code true}, the component will switch to the next available component above if the cursor is at the
350 * top of the TextBox and the user presses the 'up' array key, or switch to the next available component below if
351 * the cursor is at the bottom of the TextBox and the user presses the 'down' array key. The means that for
352 * single-line TextBox:es, pressing up and down will always switch focus with this mode enabled.
353 * @param verticalFocusSwitching If called with true, vertical focus switching will be enabled
354 * @return Itself
355 */
356 public TextBox setVerticalFocusSwitching(boolean verticalFocusSwitching) {
357 this.verticalFocusSwitching = verticalFocusSwitching;
358 return this;
359 }
360
361 /**
362 * If {@code true}, the TextBox will switch focus to the next available component to the left if the cursor in the
363 * TextBox is at the left-most position (index 0) on the row and the user pressed the 'left' arrow key, or vice
364 * versa for pressing the 'right' arrow key when the cursor in at the right-most position of the current row.
365 * @return {@code true} if horizontal focus switching is enabled
366 */
367 public boolean isHorizontalFocusSwitching() {
368 return horizontalFocusSwitching;
369 }
370
371 /**
372 * If set to {@code true}, the TextBox will switch focus to the next available component to the left if the cursor
373 * in the TextBox is at the left-most position (index 0) on the row and the user pressed the 'left' arrow key, or
374 * vice versa for pressing the 'right' arrow key when the cursor in at the right-most position of the current row.
375 * @param horizontalFocusSwitching If called with true, horizontal focus switching will be enabled
376 * @return Itself
377 */
378 public TextBox setHorizontalFocusSwitching(boolean horizontalFocusSwitching) {
379 this.horizontalFocusSwitching = horizontalFocusSwitching;
380 return this;
381 }
382
383 /**
384 * Returns the line on the specific row. For non-multiline TextBox:es, calling this with index set to 0 will return
385 * the same as calling {@code getText()}. If the row index is invalid (less than zero or equals or larger than the
386 * number of rows), this method will throw IndexOutOfBoundsException.
387 * @param index
388 * @return The line at the specified index, as a String
389 * @throws IndexOutOfBoundsException if the row index is less than zero or too large
390 */
391 public synchronized String getLine(int index) {
392 return lines.get(index);
393 }
394
395 /**
396 * Returns the number of lines currently in this TextBox. For single-line TextBox:es, this will always return 1.
397 * @return Number of lines of text currently in this TextBox
398 */
399 public synchronized int getLineCount() {
400 return lines.size();
401 }
402
403 @Override
404 protected TextBoxRenderer createDefaultRenderer() {
405 return new DefaultTextBoxRenderer();
406 }
407
408 @Override
409 public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
410 if(readOnly) {
411 return handleKeyStrokeReadOnly(keyStroke);
412 }
413 String line = lines.get(caretPosition.getRow());
414 switch(keyStroke.getKeyType()) {
415 case Character:
416 if(maxLineLength == -1 || maxLineLength > line.length() + 1) {
417 line = line.substring(0, caretPosition.getColumn()) + keyStroke.getCharacter() + line.substring(caretPosition.getColumn());
418 if(validated(line)) {
419 lines.set(caretPosition.getRow(), line);
420 caretPosition = caretPosition.withRelativeColumn(1);
421 }
422 }
423 return Result.HANDLED;
424 case Backspace:
425 if(caretPosition.getColumn() > 0) {
426 line = line.substring(0, caretPosition.getColumn() - 1) + line.substring(caretPosition.getColumn());
427 if(validated(line)) {
428 lines.set(caretPosition.getRow(), line);
429 caretPosition = caretPosition.withRelativeColumn(-1);
430 }
431 }
432 else if(style == Style.MULTI_LINE && caretPosition.getRow() > 0) {
433 String concatenatedLines = lines.get(caretPosition.getRow() - 1) + line;
434 if(validated(concatenatedLines)) {
435 lines.remove(caretPosition.getRow());
436 caretPosition = caretPosition.withRelativeRow(-1);
437 caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
438 lines.set(caretPosition.getRow(), concatenatedLines);
439 }
440 }
441 return Result.HANDLED;
442 case Delete:
443 if(caretPosition.getColumn() < line.length()) {
444 line = line.substring(0, caretPosition.getColumn()) + line.substring(caretPosition.getColumn() + 1);
445 if(validated(line)) {
446 lines.set(caretPosition.getRow(), line);
447 }
448 }
449 else if(style == Style.MULTI_LINE && caretPosition.getRow() < lines.size() - 1) {
450 String concatenatedLines = line + lines.get(caretPosition.getRow() + 1);
451 if(validated(concatenatedLines)) {
452 lines.set(caretPosition.getRow(), concatenatedLines);
453 lines.remove(caretPosition.getRow() + 1);
454 }
455 }
456 return Result.HANDLED;
457 case ArrowLeft:
458 if(caretPosition.getColumn() > 0) {
459 caretPosition = caretPosition.withRelativeColumn(-1);
460 }
461 else if(style == Style.MULTI_LINE && caretWarp && caretPosition.getRow() > 0) {
462 caretPosition = caretPosition.withRelativeRow(-1);
463 caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
464 }
465 else if(horizontalFocusSwitching) {
466 return Result.MOVE_FOCUS_LEFT;
467 }
468 return Result.HANDLED;
469 case ArrowRight:
470 if(caretPosition.getColumn() < lines.get(caretPosition.getRow()).length()) {
471 caretPosition = caretPosition.withRelativeColumn(1);
472 }
473 else if(style == Style.MULTI_LINE && caretWarp && caretPosition.getRow() < lines.size() - 1) {
474 caretPosition = caretPosition.withRelativeRow(1);
475 caretPosition = caretPosition.withColumn(0);
476 }
477 else if(horizontalFocusSwitching) {
478 return Result.MOVE_FOCUS_RIGHT;
479 }
480 return Result.HANDLED;
481 case ArrowUp:
482 if(caretPosition.getRow() > 0) {
483 int trueColumnPosition = TerminalTextUtils.getColumnIndex(lines.get(caretPosition.getRow()), caretPosition.getColumn());
484 caretPosition = caretPosition.withRelativeRow(-1);
485 line = lines.get(caretPosition.getRow());
486 if(trueColumnPosition > TerminalTextUtils.getColumnWidth(line)) {
487 caretPosition = caretPosition.withColumn(line.length());
488 }
489 else {
490 caretPosition = caretPosition.withColumn(TerminalTextUtils.getStringCharacterIndex(line, trueColumnPosition));
491 }
492 }
493 else if(verticalFocusSwitching) {
494 return Result.MOVE_FOCUS_UP;
495 }
496 return Result.HANDLED;
497 case ArrowDown:
498 if(caretPosition.getRow() < lines.size() - 1) {
499 int trueColumnPosition = TerminalTextUtils.getColumnIndex(lines.get(caretPosition.getRow()), caretPosition.getColumn());
500 caretPosition = caretPosition.withRelativeRow(1);
501 line = lines.get(caretPosition.getRow());
502 if(trueColumnPosition > TerminalTextUtils.getColumnWidth(line)) {
503 caretPosition = caretPosition.withColumn(line.length());
504 }
505 else {
506 caretPosition = caretPosition.withColumn(TerminalTextUtils.getStringCharacterIndex(line, trueColumnPosition));
507 }
508 }
509 else if(verticalFocusSwitching) {
510 return Result.MOVE_FOCUS_DOWN;
511 }
512 return Result.HANDLED;
513 case End:
514 caretPosition = caretPosition.withColumn(line.length());
515 return Result.HANDLED;
516 case Enter:
517 if(style == Style.SINGLE_LINE) {
518 return Result.MOVE_FOCUS_NEXT;
519 }
520 String newLine = line.substring(caretPosition.getColumn());
521 String oldLine = line.substring(0, caretPosition.getColumn());
522 if(validated(newLine) && validated(oldLine)) {
523 lines.set(caretPosition.getRow(), oldLine);
524 lines.add(caretPosition.getRow() + 1, newLine);
525 caretPosition = caretPosition.withColumn(0).withRelativeRow(1);
526 }
527 return Result.HANDLED;
528 case Home:
529 caretPosition = caretPosition.withColumn(0);
530 return Result.HANDLED;
531 case PageDown:
532 caretPosition = caretPosition.withRelativeRow(getSize().getRows());
533 if(caretPosition.getRow() > lines.size() - 1) {
534 caretPosition = caretPosition.withRow(lines.size() - 1);
535 }
536 if(lines.get(caretPosition.getRow()).length() < caretPosition.getColumn()) {
537 caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
538 }
539 return Result.HANDLED;
540 case PageUp:
541 caretPosition = caretPosition.withRelativeRow(-getSize().getRows());
542 if(caretPosition.getRow() < 0) {
543 caretPosition = caretPosition.withRow(0);
544 }
545 if(lines.get(caretPosition.getRow()).length() < caretPosition.getColumn()) {
546 caretPosition = caretPosition.withColumn(lines.get(caretPosition.getRow()).length());
547 }
548 return Result.HANDLED;
549 default:
550 }
551 return super.handleKeyStroke(keyStroke);
552 }
553
554 private boolean validated(String line) {
555 return validationPattern == null || line.isEmpty() || validationPattern.matcher(line).matches();
556 }
557
558 private Result handleKeyStrokeReadOnly(KeyStroke keyStroke) {
559 switch (keyStroke.getKeyType()) {
560 case ArrowLeft:
561 if(getRenderer().getViewTopLeft().getColumn() == 0 && horizontalFocusSwitching) {
562 return Result.MOVE_FOCUS_LEFT;
563 }
564 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeColumn(-1));
565 return Result.HANDLED;
566 case ArrowRight:
567 if(getRenderer().getViewTopLeft().getColumn() + getSize().getColumns() == longestRow && horizontalFocusSwitching) {
568 return Result.MOVE_FOCUS_RIGHT;
569 }
570 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeColumn(1));
571 return Result.HANDLED;
572 case ArrowUp:
573 if(getRenderer().getViewTopLeft().getRow() == 0 && verticalFocusSwitching) {
574 return Result.MOVE_FOCUS_UP;
575 }
576 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(-1));
577 return Result.HANDLED;
578 case ArrowDown:
579 if(getRenderer().getViewTopLeft().getRow() + getSize().getRows() == lines.size() && verticalFocusSwitching) {
580 return Result.MOVE_FOCUS_DOWN;
581 }
582 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(1));
583 return Result.HANDLED;
584 case Home:
585 getRenderer().setViewTopLeft(TerminalPosition.TOP_LEFT_CORNER);
586 return Result.HANDLED;
587 case End:
588 getRenderer().setViewTopLeft(TerminalPosition.TOP_LEFT_CORNER.withRow(getLineCount() - getSize().getRows()));
589 return Result.HANDLED;
590 case PageDown:
591 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(getSize().getRows()));
592 return Result.HANDLED;
593 case PageUp:
594 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(-getSize().getRows()));
595 return Result.HANDLED;
596 default:
597 }
598 return super.handleKeyStroke(keyStroke);
599 }
600
601 /**
602 * Helper interface that doesn't add any new methods but makes coding new text box renderers a little bit more clear
603 */
604 public interface TextBoxRenderer extends InteractableRenderer<TextBox> {
605 TerminalPosition getViewTopLeft();
606 void setViewTopLeft(TerminalPosition position);
607 }
608
609 /**
610 * This is the default text box renderer that is used if you don't override anything. With this renderer, the text
611 * box is filled with a solid background color and the text is drawn on top of it. Scrollbars are added for
612 * multi-line text whenever the text inside the {@code TextBox} does not fit in the available area.
613 */
614 public static class DefaultTextBoxRenderer implements TextBoxRenderer {
615 private TerminalPosition viewTopLeft;
616 private ScrollBar verticalScrollBar;
617 private ScrollBar horizontalScrollBar;
618 private boolean hideScrollBars;
619
620 /**
621 * Default constructor
622 */
623 public DefaultTextBoxRenderer() {
624 viewTopLeft = TerminalPosition.TOP_LEFT_CORNER;
625 verticalScrollBar = new ScrollBar(Direction.VERTICAL);
626 horizontalScrollBar = new ScrollBar(Direction.HORIZONTAL);
627 hideScrollBars = false;
628 }
629
630 @Override
631 public TerminalPosition getViewTopLeft() {
632 return viewTopLeft;
633 }
634
635 @Override
636 public void setViewTopLeft(TerminalPosition position) {
637 if(position.getColumn() < 0) {
638 position = position.withColumn(0);
639 }
640 if(position.getRow() < 0) {
641 position = position.withRow(0);
642 }
643 viewTopLeft = position;
644 }
645
646 @Override
647 public TerminalPosition getCursorLocation(TextBox component) {
648 if(component.isReadOnly()) {
649 return null;
650 }
651
652 //Adjust caret position if necessary
653 TerminalPosition caretPosition = component.getCaretPosition();
654 String line = component.getLine(caretPosition.getRow());
655 caretPosition = caretPosition.withColumn(Math.min(caretPosition.getColumn(), line.length()));
656
657 return caretPosition
658 .withColumn(TerminalTextUtils.getColumnIndex(line, caretPosition.getColumn()))
659 .withRelativeColumn(-viewTopLeft.getColumn())
660 .withRelativeRow(-viewTopLeft.getRow());
661 }
662
663 @Override
664 public TerminalSize getPreferredSize(TextBox component) {
665 return new TerminalSize(component.longestRow, component.lines.size());
666 }
667
668 /**
669 * Controls whether scrollbars should be visible or not when a multi-line {@code TextBox} has more content than
670 * it can draw in the area it was assigned (default: false)
671 * @param hideScrollBars If {@code true}, don't show scrollbars if the multi-line content is bigger than the
672 * area
673 */
674 public void setHideScrollBars(boolean hideScrollBars) {
675 this.hideScrollBars = hideScrollBars;
676 }
677
678 @Override
679 public void drawComponent(TextGUIGraphics graphics, TextBox component) {
680 TerminalSize realTextArea = graphics.getSize();
681 if(realTextArea.getRows() == 0 || realTextArea.getColumns() == 0) {
682 return;
683 }
684 boolean drawVerticalScrollBar = false;
685 boolean drawHorizontalScrollBar = false;
686 int textBoxLineCount = component.getLineCount();
687 if(!hideScrollBars && textBoxLineCount > realTextArea.getRows() && realTextArea.getColumns() > 1) {
688 realTextArea = realTextArea.withRelativeColumns(-1);
689 drawVerticalScrollBar = true;
690 }
691 if(!hideScrollBars && component.longestRow > realTextArea.getColumns() && realTextArea.getRows() > 1) {
692 realTextArea = realTextArea.withRelativeRows(-1);
693 drawHorizontalScrollBar = true;
694 if(textBoxLineCount > realTextArea.getRows() && realTextArea.getRows() == graphics.getSize().getRows()) {
695 realTextArea = realTextArea.withRelativeColumns(-1);
696 drawVerticalScrollBar = true;
697 }
698 }
699
700 drawTextArea(graphics.newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, realTextArea), component);
701
702 //Draw scrollbars, if any
703 if(drawVerticalScrollBar) {
704 verticalScrollBar.setViewSize(realTextArea.getRows());
705 verticalScrollBar.setScrollMaximum(textBoxLineCount);
706 verticalScrollBar.setScrollPosition(viewTopLeft.getRow());
707 verticalScrollBar.draw(graphics.newTextGraphics(
708 new TerminalPosition(graphics.getSize().getColumns() - 1, 0),
709 new TerminalSize(1, graphics.getSize().getRows() - 1)));
710 }
711 if(drawHorizontalScrollBar) {
712 horizontalScrollBar.setViewSize(realTextArea.getColumns());
713 horizontalScrollBar.setScrollMaximum(component.longestRow - 1);
714 horizontalScrollBar.setScrollPosition(viewTopLeft.getColumn());
715 horizontalScrollBar.draw(graphics.newTextGraphics(
716 new TerminalPosition(0, graphics.getSize().getRows() - 1),
717 new TerminalSize(graphics.getSize().getColumns() - 1, 1)));
718 }
719 }
720
721 private void drawTextArea(TextGUIGraphics graphics, TextBox component) {
722 TerminalSize textAreaSize = graphics.getSize();
723 if(viewTopLeft.getColumn() + textAreaSize.getColumns() > component.longestRow) {
724 viewTopLeft = viewTopLeft.withColumn(component.longestRow - textAreaSize.getColumns());
725 if(viewTopLeft.getColumn() < 0) {
726 viewTopLeft = viewTopLeft.withColumn(0);
727 }
728 }
729 if(viewTopLeft.getRow() + textAreaSize.getRows() > component.getLineCount()) {
730 viewTopLeft = viewTopLeft.withRow(component.getLineCount() - textAreaSize.getRows());
731 if(viewTopLeft.getRow() < 0) {
732 viewTopLeft = viewTopLeft.withRow(0);
733 }
734 }
735 if (component.isFocused()) {
736 graphics.applyThemeStyle(graphics.getThemeDefinition(TextBox.class).getActive());
737 }
738 else {
739 graphics.applyThemeStyle(graphics.getThemeDefinition(TextBox.class).getNormal());
740 }
741 graphics.fill(component.unusedSpaceCharacter);
742
743 if(!component.isReadOnly()) {
744 //Adjust caret position if necessary
745 TerminalPosition caretPosition = component.getCaretPosition();
746 String caretLine = component.getLine(caretPosition.getRow());
747 caretPosition = caretPosition.withColumn(Math.min(caretPosition.getColumn(), caretLine.length()));
748
749 //Adjust the view if necessary
750 int trueColumnPosition = TerminalTextUtils.getColumnIndex(caretLine, caretPosition.getColumn());
751 if (trueColumnPosition < viewTopLeft.getColumn()) {
752 viewTopLeft = viewTopLeft.withColumn(trueColumnPosition);
753 }
754 else if (trueColumnPosition >= textAreaSize.getColumns() + viewTopLeft.getColumn()) {
755 viewTopLeft = viewTopLeft.withColumn(trueColumnPosition - textAreaSize.getColumns() + 1);
756 }
757 if (caretPosition.getRow() < viewTopLeft.getRow()) {
758 viewTopLeft = viewTopLeft.withRow(caretPosition.getRow());
759 }
760 else if (caretPosition.getRow() >= textAreaSize.getRows() + viewTopLeft.getRow()) {
761 viewTopLeft = viewTopLeft.withRow(caretPosition.getRow() - textAreaSize.getRows() + 1);
762 }
763
764 //Additional corner-case for CJK characters
765 if(trueColumnPosition - viewTopLeft.getColumn() == graphics.getSize().getColumns() - 1) {
766 if(caretLine.length() > caretPosition.getColumn() &&
767 TerminalTextUtils.isCharCJK(caretLine.charAt(caretPosition.getColumn()))) {
768 viewTopLeft = viewTopLeft.withRelativeColumn(1);
769 }
770 }
771 }
772
773 for (int row = 0; row < textAreaSize.getRows(); row++) {
774 int rowIndex = row + viewTopLeft.getRow();
775 if(rowIndex >= component.lines.size()) {
776 continue;
777 }
778 String line = component.lines.get(rowIndex);
779 graphics.putString(0, row, TerminalTextUtils.fitString(line, viewTopLeft.getColumn(), textAreaSize.getColumns()));
780 }
781 }
782 }
783}