Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[fanfix.git] / src / jexer / teditor / Line.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
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:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
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.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.teditor;
30
31 import java.util.ArrayList;
32 import java.util.List;
33
34 import jexer.bits.CellAttributes;
35 import jexer.bits.StringUtils;
36
37 /**
38 * A Line represents a single line of text on the screen, as a collection of
39 * words.
40 */
41 public class Line {
42
43 // ------------------------------------------------------------------------
44 // Variables --------------------------------------------------------------
45 // ------------------------------------------------------------------------
46
47 /**
48 * The list of words.
49 */
50 private ArrayList<Word> words = new ArrayList<Word>();
51
52 /**
53 * The default color for the TEditor class.
54 */
55 private CellAttributes defaultColor = null;
56
57 /**
58 * The text highlighter to use.
59 */
60 private Highlighter highlighter = null;
61
62 /**
63 * The current edition position on this line.
64 */
65 private int position = 0;
66
67 /**
68 * The current editing position screen column number.
69 */
70 private int screenPosition = 0;
71
72 /**
73 * The raw text of this line, what is passed to Word to determine
74 * highlighting behavior.
75 */
76 private StringBuilder rawText;
77
78 // ------------------------------------------------------------------------
79 // Constructors -----------------------------------------------------------
80 // ------------------------------------------------------------------------
81
82 /**
83 * Construct a new Line from an existing text string, and highlight
84 * certain strings.
85 *
86 * @param str the text string
87 * @param defaultColor the color for unhighlighted text
88 * @param highlighter the highlighter to use
89 */
90 public Line(final String str, final CellAttributes defaultColor,
91 final Highlighter highlighter) {
92
93 this.defaultColor = defaultColor;
94 this.highlighter = highlighter;
95 this.rawText = new StringBuilder(str);
96
97 scanLine();
98 }
99
100 /**
101 * Construct a new Line from an existing text string.
102 *
103 * @param str the text string
104 * @param defaultColor the color for unhighlighted text
105 */
106 public Line(final String str, final CellAttributes defaultColor) {
107 this(str, defaultColor, null);
108 }
109
110 // ------------------------------------------------------------------------
111 // Line -------------------------------------------------------------------
112 // ------------------------------------------------------------------------
113
114 /**
115 * Get a (shallow) copy of the words in this line.
116 *
117 * @return a copy of the word list
118 */
119 public List<Word> getWords() {
120 return new ArrayList<Word>(words);
121 }
122
123 /**
124 * Get the current cursor position in the text.
125 *
126 * @return the cursor position
127 */
128 public int getRawCursor() {
129 return position;
130 }
131
132 /**
133 * Get the current cursor position on screen.
134 *
135 * @return the cursor position
136 */
137 public int getCursor() {
138 return screenPosition;
139 }
140
141 /**
142 * Set the current cursor position.
143 *
144 * @param cursor the new cursor position
145 */
146 public void setCursor(final int cursor) {
147 if ((cursor < 0)
148 || ((cursor >= getDisplayLength())
149 && (getDisplayLength() > 0))
150 ) {
151 throw new IndexOutOfBoundsException("Max length is " +
152 getDisplayLength() + ", requested position " + cursor);
153 }
154 screenPosition = cursor;
155 position = screenToTextPosition(screenPosition);
156 }
157
158 /**
159 * Get the character at the current cursor position in the text.
160 *
161 * @return the character, or -1 if the cursor is at the end of the line
162 */
163 public int getChar() {
164 if (position == rawText.length()) {
165 return -1;
166 }
167 return rawText.codePointAt(position);
168 }
169
170 /**
171 * Get the on-screen display length.
172 *
173 * @return the number of cells needed to display this line
174 */
175 public int getDisplayLength() {
176 int n = StringUtils.width(rawText.toString());
177
178 if (n > 0) {
179 // If we have any visible characters, add one to the display so
180 // that the position is immediately after the data.
181 return n + 1;
182 }
183 return n;
184 }
185
186 /**
187 * Get the raw string that matches this line.
188 *
189 * @return the string
190 */
191 public String getRawString() {
192 return rawText.toString();
193 }
194
195 /**
196 * Scan rawText and make words out of it.
197 */
198 private void scanLine() {
199 words.clear();
200 Word word = new Word(this.defaultColor, this.highlighter);
201 words.add(word);
202 for (int i = 0; i < rawText.length();) {
203 int ch = rawText.codePointAt(i);
204 i += Character.charCount(ch);
205 Word newWord = word.addChar(ch);
206 if (newWord != word) {
207 words.add(newWord);
208 word = newWord;
209 }
210 }
211 for (Word w: words) {
212 w.applyHighlight();
213 }
214 }
215
216 /**
217 * Decrement the cursor by one. If at the first column, do nothing.
218 *
219 * @return true if the cursor position changed
220 */
221 public boolean left() {
222 if (position == 0) {
223 return false;
224 }
225 screenPosition -= StringUtils.width(rawText.codePointBefore(position));
226 position -= Character.charCount(rawText.codePointBefore(position));
227 return true;
228 }
229
230 /**
231 * Increment the cursor by one. If at the last column, do nothing.
232 *
233 * @return true if the cursor position changed
234 */
235 public boolean right() {
236 if (getDisplayLength() == 0) {
237 return false;
238 }
239 if (position == getDisplayLength() - 1) {
240 return false;
241 }
242 if (position < rawText.length()) {
243 screenPosition += StringUtils.width(rawText.codePointAt(position));
244 position += Character.charCount(rawText.codePointAt(position));
245 }
246 assert (position <= rawText.length());
247 return true;
248 }
249
250 /**
251 * Go to the first column of this line.
252 *
253 * @return true if the cursor position changed
254 */
255 public boolean home() {
256 if (position > 0) {
257 position = 0;
258 screenPosition = 0;
259 return true;
260 }
261 return false;
262 }
263
264 /**
265 * Go to the last column of this line.
266 *
267 * @return true if the cursor position changed
268 */
269 public boolean end() {
270 if (position != getDisplayLength() - 1) {
271 position = rawText.length();
272 screenPosition = StringUtils.width(rawText.toString());
273 return true;
274 }
275 return false;
276 }
277
278 /**
279 * Delete the character under the cursor.
280 */
281 public void del() {
282 assert (words.size() > 0);
283
284 if (position < getDisplayLength()) {
285 int n = Character.charCount(rawText.codePointAt(position));
286 for (int i = 0; i < n; i++) {
287 rawText.deleteCharAt(position);
288 }
289 }
290
291 // Re-scan the line to determine the new word boundaries.
292 scanLine();
293 }
294
295 /**
296 * Delete the character immediately preceeding the cursor.
297 */
298 public void backspace() {
299 if (left()) {
300 del();
301 }
302 }
303
304 /**
305 * Insert a character at the cursor.
306 *
307 * @param ch the character to insert
308 */
309 public void addChar(final int ch) {
310 if (position < getDisplayLength() - 1) {
311 rawText.insert(position, Character.toChars(ch));
312 } else {
313 rawText.append(Character.toChars(ch));
314 }
315 position += Character.charCount(ch);
316 screenPosition += StringUtils.width(ch);
317 scanLine();
318 }
319
320 /**
321 * Replace a character at the cursor.
322 *
323 * @param ch the character to replace
324 */
325 public void replaceChar(final int ch) {
326 if (position < getDisplayLength() - 1) {
327 // Replace character
328 String oldText = rawText.toString();
329 rawText = new StringBuilder(oldText.substring(0, position));
330 rawText.append(Character.toChars(ch));
331 rawText.append(oldText.substring(position + 1));
332 screenPosition += StringUtils.width(rawText.codePointAt(position));
333 position += Character.charCount(ch);
334 } else {
335 rawText.append(Character.toChars(ch));
336 position += Character.charCount(ch);
337 screenPosition += StringUtils.width(ch);
338 }
339 scanLine();
340 }
341
342 /**
343 * Determine string position from screen position.
344 *
345 * @param screenPosition the position on screen
346 * @return the equivalent position in text
347 */
348 protected int screenToTextPosition(final int screenPosition) {
349 if (screenPosition == 0) {
350 return 0;
351 }
352
353 int n = 0;
354 for (int i = 0; i < rawText.length(); i++) {
355 n += StringUtils.width(rawText.codePointAt(i));
356 if (n >= screenPosition) {
357 return i + 1;
358 }
359 }
360 // screenPosition exceeds the available text length.
361 throw new IndexOutOfBoundsException("screenPosition " + screenPosition +
362 " exceeds available text length " + rawText.length());
363 }
364
365 }