2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
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.
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.
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/>.
17 * Copyright (C) 2010-2015 Martin
19 package com
.googlecode
.lanterna
.gui2
;
21 import com
.googlecode
.lanterna
.TerminalTextUtils
;
22 import com
.googlecode
.lanterna
.TerminalPosition
;
23 import com
.googlecode
.lanterna
.TerminalSize
;
24 import com
.googlecode
.lanterna
.input
.KeyStroke
;
26 import java
.util
.ArrayList
;
27 import java
.util
.List
;
28 import java
.util
.regex
.Pattern
;
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.
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.
38 public class TextBox
extends AbstractInteractableComponent
<TextBox
> {
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.
46 * The {@code TextBox} contains a single line of text and is typically drawn on one row
50 * The {@code TextBox} contains a none, one or many lines of text and is normally drawn over multiple lines
56 private final List
<String
> lines
;
57 private final Style style
;
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
;
71 * Default constructor, this creates a single-line {@code TextBox} of size 10 which is initially empty
74 this(new TerminalSize(10, 1), "", Style
.SINGLE_LINE
);
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}
82 public TextBox(String initialContent
) {
83 this(null, initialContent
, initialContent
.contains("\n") ? Style
.MULTI_LINE
: Style
.SINGLE_LINE
);
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.
90 * @param initialContent Initial content of the {@code TextBox}
91 * @param style Forced style instead of auto-detecting
93 public TextBox(String initialContent
, Style style
) {
94 this(null, initialContent
, style
);
98 * Creates a new empty {@code TextBox} with a specific size
99 * @param preferredSize Size of the {@code TextBox}
101 public TextBox(TerminalSize preferredSize
) {
102 this(preferredSize
, (preferredSize
!= null && preferredSize
.getRows() > 1) ? Style
.MULTI_LINE
: Style
.SINGLE_LINE
);
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
110 public TextBox(TerminalSize preferredSize
, Style style
) {
111 this(preferredSize
, "", style
);
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}
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
);
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
129 public TextBox(TerminalSize preferredSize
, String initialContent
, Style style
) {
130 this.lines
= new ArrayList
<String
>();
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
= ' ';
141 this.validationPattern
= null;
142 setText(initialContent
);
143 if (preferredSize
== null) {
144 preferredSize
= new TerminalSize(Math
.max(10, longestRow
), lines
.size());
146 setPreferredSize(preferredSize
);
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
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");
166 this.validationPattern
= validationPattern
;
171 * Updates the text content of the {@code TextBox} to the supplied string.
172 * @param text New text to assign to the {@code TextBox}
175 public synchronized TextBox
setText(String text
) {
176 String
[] split
= text
.split("\n");
179 for(String line
: split
) {
182 if(caretPosition
.getRow() > lines
.size() - 1) {
183 caretPosition
= caretPosition
.withRow(lines
.size() - 1);
185 if(caretPosition
.getColumn() > lines
.get(caretPosition
.getRow()).length()) {
186 caretPosition
= caretPosition
.withColumn(lines
.get(caretPosition
.getRow()).length());
193 public TextBoxRenderer
getRenderer() {
194 return (TextBoxRenderer
)super.getRenderer();
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}
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
);
210 if(longestRow
< lineWidth
+ 1) {
211 longestRow
= lineWidth
+ 1;
213 addLine(line
.substring(i
+ 1));
216 else if(Character
.isISOControl(c
)) {
222 String string
= bob
.toString();
223 if(!validated(string
)) {
224 throw new IllegalStateException("TextBox validation pattern " + validationPattern
+ " does not match the supplied text");
226 int lineWidth
= TerminalTextUtils
.getColumnWidth(string
);
228 if(longestRow
< lineWidth
+ 1) {
229 longestRow
= lineWidth
+ 1;
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
243 public TextBox
setCaretWarp(boolean caretWarp
) {
244 this.caretWarp
= caretWarp
;
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
252 public boolean isCaretWarp() {
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
261 public TerminalPosition
getCaretPosition() {
262 return caretPosition
;
266 * Returns the text in this {@code TextBox}, for multi-line mode all lines will be concatenated together with \n as
268 * @return The text inside this {@code TextBox}
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
));
275 return bob
.toString();
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
284 public String
getTextOrDefault(String defaultValueIfEmpty
) {
285 String text
= getText();
287 return defaultValueIfEmpty
;
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
297 public Character
getMask() {
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
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");
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
321 public boolean isReadOnly() {
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
331 public TextBox
setReadOnly(boolean readOnly
) {
332 this.readOnly
= readOnly
;
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
344 public boolean isVerticalFocusSwitching() {
345 return verticalFocusSwitching
;
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
356 public TextBox
setVerticalFocusSwitching(boolean verticalFocusSwitching
) {
357 this.verticalFocusSwitching
= verticalFocusSwitching
;
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
367 public boolean isHorizontalFocusSwitching() {
368 return horizontalFocusSwitching
;
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
378 public TextBox
setHorizontalFocusSwitching(boolean horizontalFocusSwitching
) {
379 this.horizontalFocusSwitching
= horizontalFocusSwitching
;
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.
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
391 public synchronized String
getLine(int index
) {
392 return lines
.get(index
);
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
399 public synchronized int getLineCount() {
404 protected TextBoxRenderer
createDefaultRenderer() {
405 return new DefaultTextBoxRenderer();
409 public synchronized Result
handleKeyStroke(KeyStroke keyStroke
) {
411 return handleKeyStrokeReadOnly(keyStroke
);
413 String line
= lines
.get(caretPosition
.getRow());
414 switch(keyStroke
.getKeyType()) {
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);
423 return Result
.HANDLED
;
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);
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
);
441 return Result
.HANDLED
;
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
);
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);
456 return Result
.HANDLED
;
458 if(caretPosition
.getColumn() > 0) {
459 caretPosition
= caretPosition
.withRelativeColumn(-1);
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());
465 else if(horizontalFocusSwitching
) {
466 return Result
.MOVE_FOCUS_LEFT
;
468 return Result
.HANDLED
;
470 if(caretPosition
.getColumn() < lines
.get(caretPosition
.getRow()).length()) {
471 caretPosition
= caretPosition
.withRelativeColumn(1);
473 else if(style
== Style
.MULTI_LINE
&& caretWarp
&& caretPosition
.getRow() < lines
.size() - 1) {
474 caretPosition
= caretPosition
.withRelativeRow(1);
475 caretPosition
= caretPosition
.withColumn(0);
477 else if(horizontalFocusSwitching
) {
478 return Result
.MOVE_FOCUS_RIGHT
;
480 return Result
.HANDLED
;
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());
490 caretPosition
= caretPosition
.withColumn(TerminalTextUtils
.getStringCharacterIndex(line
, trueColumnPosition
));
493 else if(verticalFocusSwitching
) {
494 return Result
.MOVE_FOCUS_UP
;
496 return Result
.HANDLED
;
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());
506 caretPosition
= caretPosition
.withColumn(TerminalTextUtils
.getStringCharacterIndex(line
, trueColumnPosition
));
509 else if(verticalFocusSwitching
) {
510 return Result
.MOVE_FOCUS_DOWN
;
512 return Result
.HANDLED
;
514 caretPosition
= caretPosition
.withColumn(line
.length());
515 return Result
.HANDLED
;
517 if(style
== Style
.SINGLE_LINE
) {
518 return Result
.MOVE_FOCUS_NEXT
;
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);
527 return Result
.HANDLED
;
529 caretPosition
= caretPosition
.withColumn(0);
530 return Result
.HANDLED
;
532 caretPosition
= caretPosition
.withRelativeRow(getSize().getRows());
533 if(caretPosition
.getRow() > lines
.size() - 1) {
534 caretPosition
= caretPosition
.withRow(lines
.size() - 1);
536 if(lines
.get(caretPosition
.getRow()).length() < caretPosition
.getColumn()) {
537 caretPosition
= caretPosition
.withColumn(lines
.get(caretPosition
.getRow()).length());
539 return Result
.HANDLED
;
541 caretPosition
= caretPosition
.withRelativeRow(-getSize().getRows());
542 if(caretPosition
.getRow() < 0) {
543 caretPosition
= caretPosition
.withRow(0);
545 if(lines
.get(caretPosition
.getRow()).length() < caretPosition
.getColumn()) {
546 caretPosition
= caretPosition
.withColumn(lines
.get(caretPosition
.getRow()).length());
548 return Result
.HANDLED
;
551 return super.handleKeyStroke(keyStroke
);
554 private boolean validated(String line
) {
555 return validationPattern
== null || line
.isEmpty() || validationPattern
.matcher(line
).matches();
558 private Result
handleKeyStrokeReadOnly(KeyStroke keyStroke
) {
559 switch (keyStroke
.getKeyType()) {
561 if(getRenderer().getViewTopLeft().getColumn() == 0 && horizontalFocusSwitching
) {
562 return Result
.MOVE_FOCUS_LEFT
;
564 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeColumn(-1));
565 return Result
.HANDLED
;
567 if(getRenderer().getViewTopLeft().getColumn() + getSize().getColumns() == longestRow
&& horizontalFocusSwitching
) {
568 return Result
.MOVE_FOCUS_RIGHT
;
570 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeColumn(1));
571 return Result
.HANDLED
;
573 if(getRenderer().getViewTopLeft().getRow() == 0 && verticalFocusSwitching
) {
574 return Result
.MOVE_FOCUS_UP
;
576 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(-1));
577 return Result
.HANDLED
;
579 if(getRenderer().getViewTopLeft().getRow() + getSize().getRows() == lines
.size() && verticalFocusSwitching
) {
580 return Result
.MOVE_FOCUS_DOWN
;
582 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(1));
583 return Result
.HANDLED
;
585 getRenderer().setViewTopLeft(TerminalPosition
.TOP_LEFT_CORNER
);
586 return Result
.HANDLED
;
588 getRenderer().setViewTopLeft(TerminalPosition
.TOP_LEFT_CORNER
.withRow(getLineCount() - getSize().getRows()));
589 return Result
.HANDLED
;
591 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(getSize().getRows()));
592 return Result
.HANDLED
;
594 getRenderer().setViewTopLeft(getRenderer().getViewTopLeft().withRelativeRow(-getSize().getRows()));
595 return Result
.HANDLED
;
598 return super.handleKeyStroke(keyStroke
);
602 * Helper interface that doesn't add any new methods but makes coding new text box renderers a little bit more clear
604 public interface TextBoxRenderer
extends InteractableRenderer
<TextBox
> {
605 TerminalPosition
getViewTopLeft();
606 void setViewTopLeft(TerminalPosition position
);
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.
614 public static class DefaultTextBoxRenderer
implements TextBoxRenderer
{
615 private TerminalPosition viewTopLeft
;
616 private ScrollBar verticalScrollBar
;
617 private ScrollBar horizontalScrollBar
;
618 private boolean hideScrollBars
;
621 * Default constructor
623 public DefaultTextBoxRenderer() {
624 viewTopLeft
= TerminalPosition
.TOP_LEFT_CORNER
;
625 verticalScrollBar
= new ScrollBar(Direction
.VERTICAL
);
626 horizontalScrollBar
= new ScrollBar(Direction
.HORIZONTAL
);
627 hideScrollBars
= false;
631 public TerminalPosition
getViewTopLeft() {
636 public void setViewTopLeft(TerminalPosition position
) {
637 if(position
.getColumn() < 0) {
638 position
= position
.withColumn(0);
640 if(position
.getRow() < 0) {
641 position
= position
.withRow(0);
643 viewTopLeft
= position
;
647 public TerminalPosition
getCursorLocation(TextBox component
) {
648 if(component
.isReadOnly()) {
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()));
658 .withColumn(TerminalTextUtils
.getColumnIndex(line
, caretPosition
.getColumn()))
659 .withRelativeColumn(-viewTopLeft
.getColumn())
660 .withRelativeRow(-viewTopLeft
.getRow());
664 public TerminalSize
getPreferredSize(TextBox component
) {
665 return new TerminalSize(component
.longestRow
, component
.lines
.size());
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
674 public void setHideScrollBars(boolean hideScrollBars
) {
675 this.hideScrollBars
= hideScrollBars
;
679 public void drawComponent(TextGUIGraphics graphics
, TextBox component
) {
680 TerminalSize realTextArea
= graphics
.getSize();
681 if(realTextArea
.getRows() == 0 || realTextArea
.getColumns() == 0) {
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;
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;
700 drawTextArea(graphics
.newTextGraphics(TerminalPosition
.TOP_LEFT_CORNER
, realTextArea
), component
);
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)));
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)));
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);
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);
735 if (component
.isFocused()) {
736 graphics
.applyThemeStyle(graphics
.getThemeDefinition(TextBox
.class).getActive());
739 graphics
.applyThemeStyle(graphics
.getThemeDefinition(TextBox
.class).getNormal());
741 graphics
.fill(component
.unusedSpaceCharacter
);
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()));
749 //Adjust the view if necessary
750 int trueColumnPosition
= TerminalTextUtils
.getColumnIndex(caretLine
, caretPosition
.getColumn());
751 if (trueColumnPosition
< viewTopLeft
.getColumn()) {
752 viewTopLeft
= viewTopLeft
.withColumn(trueColumnPosition
);
754 else if (trueColumnPosition
>= textAreaSize
.getColumns() + viewTopLeft
.getColumn()) {
755 viewTopLeft
= viewTopLeft
.withColumn(trueColumnPosition
- textAreaSize
.getColumns() + 1);
757 if (caretPosition
.getRow() < viewTopLeft
.getRow()) {
758 viewTopLeft
= viewTopLeft
.withRow(caretPosition
.getRow());
760 else if (caretPosition
.getRow() >= textAreaSize
.getRows() + viewTopLeft
.getRow()) {
761 viewTopLeft
= viewTopLeft
.withRow(caretPosition
.getRow() - textAreaSize
.getRows() + 1);
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);
773 for (int row
= 0; row
< textAreaSize
.getRows(); row
++) {
774 int rowIndex
= row
+ viewTopLeft
.getRow();
775 if(rowIndex
>= component
.lines
.size()) {
778 String line
= component
.lines
.get(rowIndex
);
779 graphics
.putString(0, row
, TerminalTextUtils
.fitString(line
, viewTopLeft
.getColumn(), textAreaSize
.getColumns()));