2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.teditor
;
31 import java
.util
.ArrayList
;
32 import java
.util
.List
;
34 import jexer
.bits
.CellAttributes
;
35 import jexer
.bits
.GraphicsChars
;
36 import jexer
.bits
.StringUtils
;
39 * A Line represents a single line of text on the screen, as a collection of
44 // ------------------------------------------------------------------------
45 // Variables --------------------------------------------------------------
46 // ------------------------------------------------------------------------
51 private ArrayList
<Word
> words
= new ArrayList
<Word
>();
54 * The default color for the TEditor class.
56 private CellAttributes defaultColor
= null;
59 * The text highlighter to use.
61 private Highlighter highlighter
= null;
64 * The current edition position on this line.
66 private int position
= 0;
69 * The current editing position screen column number.
71 private int screenPosition
= 0;
74 * The raw text of this line, what is passed to Word to determine
75 * highlighting behavior.
77 private StringBuilder rawText
;
79 // ------------------------------------------------------------------------
80 // Constructors -----------------------------------------------------------
81 // ------------------------------------------------------------------------
84 * Construct a new Line from an existing text string, and highlight
87 * @param str the text string
88 * @param defaultColor the color for unhighlighted text
89 * @param highlighter the highlighter to use
91 public Line(final String str
, final CellAttributes defaultColor
,
92 final Highlighter highlighter
) {
94 this.defaultColor
= defaultColor
;
95 this.highlighter
= highlighter
;
97 this.rawText
= new StringBuilder();
99 for (int i
= 0; i
< str
.length(); i
++) {
100 char ch
= str
.charAt(i
);
108 } while ((j
% 8) != 0);
111 if ((ch
<= 0x20) || (ch
== 0x7F)) {
112 // Replace all other C0 bytes with CP437 glyphs.
113 rawText
.append(GraphicsChars
.CP437
[(int) ch
]);
126 * Construct a new Line from an existing text string.
128 * @param str the text string
129 * @param defaultColor the color for unhighlighted text
131 public Line(final String str
, final CellAttributes defaultColor
) {
132 this(str
, defaultColor
, null);
136 * Private constructor used by dup().
142 // ------------------------------------------------------------------------
143 // Line -------------------------------------------------------------------
144 // ------------------------------------------------------------------------
147 * Create a duplicate instance.
149 * @return duplicate intance
152 Line other
= new Line();
153 other
.defaultColor
= defaultColor
;
154 other
.highlighter
= highlighter
;
155 other
.position
= position
;
156 other
.screenPosition
= screenPosition
;
157 other
.rawText
= new StringBuilder(rawText
);
163 * Get a (shallow) copy of the words in this line.
165 * @return a copy of the word list
167 public List
<Word
> getWords() {
168 return new ArrayList
<Word
>(words
);
172 * Get the current cursor position in the text.
174 * @return the cursor position
176 public int getRawCursor() {
181 * Get the current cursor position on screen.
183 * @return the cursor position
185 public int getCursor() {
186 return screenPosition
;
190 * Set the current cursor position.
192 * @param cursor the new cursor position
194 public void setCursor(final int cursor
) {
196 || ((cursor
>= getDisplayLength())
197 && (getDisplayLength() > 0))
199 throw new IndexOutOfBoundsException("Max length is " +
200 getDisplayLength() + ", requested position " + cursor
);
202 screenPosition
= cursor
;
203 position
= screenToTextPosition(screenPosition
);
207 * Get the character at the current cursor position in the text.
209 * @return the character, or -1 if the cursor is at the end of the line
211 public int getChar() {
212 if (position
== rawText
.length()) {
215 return rawText
.codePointAt(position
);
219 * Get the on-screen display length.
221 * @return the number of cells needed to display this line
223 public int getDisplayLength() {
224 int n
= StringUtils
.width(rawText
.toString());
227 // If we have any visible characters, add one to the display so
228 // that the position is immediately after the data.
235 * Get the raw string that matches this line.
239 public String
getRawString() {
240 return rawText
.toString();
244 * Get the raw length of this line.
246 * @return the length of this line in characters, which may be different
247 * from the number of cells needed to display it
249 public int length() {
250 return rawText
.length();
254 * Scan rawText and make words out of it. Note package private access.
258 Word word
= new Word(this.defaultColor
, this.highlighter
);
260 for (int i
= 0; i
< rawText
.length();) {
261 int ch
= rawText
.codePointAt(i
);
262 i
+= Character
.charCount(ch
);
263 Word newWord
= word
.addChar(ch
);
264 if (newWord
!= word
) {
269 for (Word w
: words
) {
275 * Decrement the cursor by one. If at the first column, do nothing.
277 * @return true if the cursor position changed
279 public boolean left() {
283 screenPosition
-= StringUtils
.width(rawText
.codePointBefore(position
));
284 position
-= Character
.charCount(rawText
.codePointBefore(position
));
289 * Increment the cursor by one. If at the last column, do nothing.
291 * @return true if the cursor position changed
293 public boolean right() {
294 if (getDisplayLength() == 0) {
297 if (screenPosition
== getDisplayLength() - 1) {
300 if (position
< rawText
.length()) {
301 screenPosition
+= StringUtils
.width(rawText
.codePointAt(position
));
302 position
+= Character
.charCount(rawText
.codePointAt(position
));
304 assert (position
<= rawText
.length());
309 * Go to the first column of this line.
311 * @return true if the cursor position changed
313 public boolean home() {
323 * Go to the last column of this line.
325 * @return true if the cursor position changed
327 public boolean end() {
328 if (screenPosition
!= getDisplayLength() - 1) {
329 position
= rawText
.length();
330 screenPosition
= StringUtils
.width(rawText
.toString());
337 * Delete the character under the cursor.
340 assert (words
.size() > 0);
342 if (screenPosition
< getDisplayLength()) {
343 int n
= Character
.charCount(rawText
.codePointAt(position
));
344 for (int i
= 0; i
< n
; i
++) {
345 rawText
.deleteCharAt(position
);
349 // Re-scan the line to determine the new word boundaries.
354 * Delete the character immediately preceeding the cursor.
356 * @param tabSize the tab stop size
357 * @param backspaceUnindents If true, backspace at an indent level goes
358 * back a full indent level. If false, backspace always goes back one
361 public void backspace(final int tabSize
, final boolean backspaceUnindents
) {
362 if ((backspaceUnindents
== true)
364 && (screenPosition
> 0)
365 && (rawText
.charAt(position
- 1) == ' ')
366 && ((screenPosition
% tabSize
) == 0)
368 boolean doBackTab
= true;
369 for (int i
= 0; i
< position
; i
++) {
370 if (rawText
.charAt(i
) != ' ') {
387 * Insert a character at the cursor.
389 * @param ch the character to insert
391 public void addChar(final int ch
) {
392 if (screenPosition
< getDisplayLength() - 1) {
393 rawText
.insert(position
, Character
.toChars(ch
));
395 rawText
.append(Character
.toChars(ch
));
397 position
+= Character
.charCount(ch
);
398 screenPosition
+= StringUtils
.width(ch
);
403 * Replace a character at the cursor.
405 * @param ch the character to replace
407 public void replaceChar(final int ch
) {
408 if (screenPosition
< getDisplayLength() - 1) {
410 String oldText
= rawText
.toString();
411 rawText
= new StringBuilder(oldText
.substring(0, position
));
412 rawText
.append(Character
.toChars(ch
));
413 rawText
.append(oldText
.substring(position
+ 1));
414 screenPosition
+= StringUtils
.width(rawText
.codePointAt(position
));
415 position
+= Character
.charCount(ch
);
417 rawText
.append(Character
.toChars(ch
));
418 position
+= Character
.charCount(ch
);
419 screenPosition
+= StringUtils
.width(ch
);
425 * Determine string position from screen position.
427 * @param screenPosition the position on screen
428 * @return the equivalent position in text
430 private int screenToTextPosition(final int screenPosition
) {
431 if (screenPosition
== 0) {
436 for (int i
= 0; i
< rawText
.length(); i
++) {
437 n
+= StringUtils
.width(rawText
.codePointAt(i
));
438 if (n
>= screenPosition
) {
442 // screenPosition exceeds the available text length.
443 throw new IndexOutOfBoundsException("screenPosition " + screenPosition
+
444 " exceeds available text length " + rawText
.length());
448 * Trim trailing whitespace from line, repositioning cursor if needed.
450 public void trimRight() {
451 if (rawText
.length() == 0) {
454 if (!Character
.isWhitespace(rawText
.charAt(rawText
.length() - 1))) {
457 while ((rawText
.length() > 0)
458 && Character
.isWhitespace(rawText
.charAt(rawText
.length() - 1))
460 rawText
.deleteCharAt(rawText
.length() - 1);
462 if (position
>= rawText
.length()) {
469 * Handle the tab character.
471 * @param tabSize the tab stop size
473 public void tab(final int tabSize
) {
477 } while ((screenPosition
% tabSize
) != 0);
482 * Handle the backtab (shift-tab) character.
484 * @param tabSize the tab stop size
486 public void backTab(final int tabSize
) {
487 if ((tabSize
> 0) && (screenPosition
> 0)
488 && (rawText
.charAt(position
- 1) == ' ')
491 backspace(tabSize
, false);
492 } while (((screenPosition
% tabSize
) != 0)
493 && (screenPosition
> 0)
494 && (rawText
.charAt(position
- 1) == ' '));