Commit | Line | Data |
---|---|---|
cd92b0ba KL |
1 | /* |
2 | * Jexer - Java Text User Interface | |
3 | * | |
4 | * The MIT License (MIT) | |
5 | * | |
a69ed767 | 6 | * Copyright (C) 2019 Kevin Lamonte |
cd92b0ba KL |
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 | ||
e8a11f98 | 34 | import jexer.bits.CellAttributes; |
2d3f60d8 | 35 | import jexer.bits.StringUtils; |
e8a11f98 | 36 | |
cd92b0ba | 37 | /** |
12b55d76 KL |
38 | * A Line represents a single line of text on the screen, as a collection of |
39 | * words. | |
cd92b0ba | 40 | */ |
12b55d76 | 41 | public class Line { |
cd92b0ba | 42 | |
615a0d99 KL |
43 | // ------------------------------------------------------------------------ |
44 | // Variables -------------------------------------------------------------- | |
45 | // ------------------------------------------------------------------------ | |
46 | ||
cd92b0ba | 47 | /** |
12b55d76 | 48 | * The list of words. |
cd92b0ba | 49 | */ |
12b55d76 | 50 | private ArrayList<Word> words = new ArrayList<Word>(); |
cd92b0ba | 51 | |
e8a11f98 KL |
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 | ||
cd92b0ba | 62 | /** |
39e86397 | 63 | * The current edition position on this line. |
cd92b0ba | 64 | */ |
39e86397 KL |
65 | private int position = 0; |
66 | ||
67 | /** | |
68 | * The current editing position screen column number. | |
69 | */ | |
70 | private int screenPosition = 0; | |
cd92b0ba KL |
71 | |
72 | /** | |
71a389c9 KL |
73 | * The raw text of this line, what is passed to Word to determine |
74 | * highlighting behavior. | |
cd92b0ba | 75 | */ |
71a389c9 | 76 | private StringBuilder rawText; |
cd92b0ba | 77 | |
615a0d99 KL |
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 | ||
cd92b0ba | 114 | /** |
71a389c9 KL |
115 | * Get a (shallow) copy of the words in this line. |
116 | * | |
117 | * @return a copy of the word list | |
cd92b0ba | 118 | */ |
71a389c9 KL |
119 | public List<Word> getWords() { |
120 | return new ArrayList<Word>(words); | |
121 | } | |
cd92b0ba | 122 | |
e8a11f98 | 123 | /** |
39e86397 KL |
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. | |
e8a11f98 KL |
134 | * |
135 | * @return the cursor position | |
136 | */ | |
137 | public int getCursor() { | |
39e86397 | 138 | return screenPosition; |
e8a11f98 KL |
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 | } | |
39e86397 KL |
154 | screenPosition = cursor; |
155 | position = screenToTextPosition(screenPosition); | |
e8a11f98 KL |
156 | } |
157 | ||
e23989a4 KL |
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 | ||
cd92b0ba | 170 | /** |
71a389c9 | 171 | * Get the on-screen display length. |
cd92b0ba | 172 | * |
71a389c9 | 173 | * @return the number of cells needed to display this line |
cd92b0ba | 174 | */ |
71a389c9 | 175 | public int getDisplayLength() { |
2d3f60d8 | 176 | int n = StringUtils.width(rawText.toString()); |
71a389c9 | 177 | |
71a389c9 KL |
178 | if (n > 0) { |
179 | // If we have any visible characters, add one to the display so | |
39e86397 | 180 | // that the position is immediately after the data. |
71a389c9 KL |
181 | return n + 1; |
182 | } | |
183 | return n; | |
cd92b0ba KL |
184 | } |
185 | ||
186 | /** | |
71a389c9 | 187 | * Get the raw string that matches this line. |
cd92b0ba | 188 | * |
71a389c9 | 189 | * @return the string |
cd92b0ba | 190 | */ |
71a389c9 KL |
191 | public String getRawString() { |
192 | return rawText.toString(); | |
193 | } | |
e8a11f98 | 194 | |
71a389c9 KL |
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); | |
2d3f60d8 KL |
202 | for (int i = 0; i < rawText.length();) { |
203 | int ch = rawText.codePointAt(i); | |
204 | i += Character.charCount(ch); | |
71a389c9 KL |
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(); | |
e8a11f98 | 213 | } |
cd92b0ba KL |
214 | } |
215 | ||
cd92b0ba | 216 | /** |
12b55d76 | 217 | * Decrement the cursor by one. If at the first column, do nothing. |
e8a11f98 KL |
218 | * |
219 | * @return true if the cursor position changed | |
cd92b0ba | 220 | */ |
e8a11f98 | 221 | public boolean left() { |
39e86397 | 222 | if (position == 0) { |
e8a11f98 | 223 | return false; |
cd92b0ba | 224 | } |
39e86397 KL |
225 | screenPosition -= StringUtils.width(rawText.codePointBefore(position)); |
226 | position -= Character.charCount(rawText.codePointBefore(position)); | |
e8a11f98 | 227 | return true; |
cd92b0ba KL |
228 | } |
229 | ||
230 | /** | |
12b55d76 | 231 | * Increment the cursor by one. If at the last column, do nothing. |
e8a11f98 KL |
232 | * |
233 | * @return true if the cursor position changed | |
cd92b0ba | 234 | */ |
e8a11f98 KL |
235 | public boolean right() { |
236 | if (getDisplayLength() == 0) { | |
237 | return false; | |
cd92b0ba | 238 | } |
39e86397 | 239 | if (position == getDisplayLength() - 1) { |
e8a11f98 KL |
240 | return false; |
241 | } | |
39e86397 KL |
242 | if (position < rawText.length()) { |
243 | screenPosition += StringUtils.width(rawText.codePointAt(position)); | |
244 | position += Character.charCount(rawText.codePointAt(position)); | |
2d3f60d8 | 245 | } |
39e86397 | 246 | assert (position <= rawText.length()); |
e8a11f98 | 247 | return true; |
cd92b0ba KL |
248 | } |
249 | ||
250 | /** | |
12b55d76 | 251 | * Go to the first column of this line. |
e8a11f98 KL |
252 | * |
253 | * @return true if the cursor position changed | |
cd92b0ba | 254 | */ |
e8a11f98 | 255 | public boolean home() { |
39e86397 KL |
256 | if (position > 0) { |
257 | position = 0; | |
258 | screenPosition = 0; | |
e8a11f98 KL |
259 | return true; |
260 | } | |
261 | return false; | |
cd92b0ba KL |
262 | } |
263 | ||
264 | /** | |
12b55d76 | 265 | * Go to the last column of this line. |
e8a11f98 KL |
266 | * |
267 | * @return true if the cursor position changed | |
cd92b0ba | 268 | */ |
e8a11f98 | 269 | public boolean end() { |
39e86397 KL |
270 | if (position != getDisplayLength() - 1) { |
271 | position = rawText.length(); | |
272 | screenPosition = StringUtils.width(rawText.toString()); | |
e8a11f98 KL |
273 | return true; |
274 | } | |
275 | return false; | |
cd92b0ba KL |
276 | } |
277 | ||
278 | /** | |
12b55d76 | 279 | * Delete the character under the cursor. |
cd92b0ba | 280 | */ |
12b55d76 | 281 | public void del() { |
71a389c9 KL |
282 | assert (words.size() > 0); |
283 | ||
39e86397 KL |
284 | if (position < getDisplayLength()) { |
285 | int n = Character.charCount(rawText.codePointAt(position)); | |
286 | for (int i = 0; i < n; i++) { | |
287 | rawText.deleteCharAt(position); | |
2d3f60d8 | 288 | } |
71a389c9 KL |
289 | } |
290 | ||
291 | // Re-scan the line to determine the new word boundaries. | |
292 | scanLine(); | |
cd92b0ba KL |
293 | } |
294 | ||
295 | /** | |
12b55d76 | 296 | * Delete the character immediately preceeding the cursor. |
cd92b0ba | 297 | */ |
12b55d76 | 298 | public void backspace() { |
71a389c9 KL |
299 | if (left()) { |
300 | del(); | |
301 | } | |
cd92b0ba KL |
302 | } |
303 | ||
304 | /** | |
e8a11f98 | 305 | * Insert a character at the cursor. |
cd92b0ba | 306 | * |
e8a11f98 | 307 | * @param ch the character to insert |
cd92b0ba | 308 | */ |
2d3f60d8 | 309 | public void addChar(final int ch) { |
39e86397 KL |
310 | if (position < getDisplayLength() - 1) { |
311 | rawText.insert(position, Character.toChars(ch)); | |
71a389c9 | 312 | } else { |
2d3f60d8 | 313 | rawText.append(Character.toChars(ch)); |
71a389c9 | 314 | } |
39e86397 KL |
315 | position += Character.charCount(ch); |
316 | screenPosition += StringUtils.width(ch); | |
71a389c9 | 317 | scanLine(); |
cd92b0ba KL |
318 | } |
319 | ||
e8a11f98 KL |
320 | /** |
321 | * Replace a character at the cursor. | |
322 | * | |
323 | * @param ch the character to replace | |
324 | */ | |
2d3f60d8 | 325 | public void replaceChar(final int ch) { |
39e86397 KL |
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); | |
71a389c9 | 334 | } else { |
2d3f60d8 | 335 | rawText.append(Character.toChars(ch)); |
39e86397 KL |
336 | position += Character.charCount(ch); |
337 | screenPosition += StringUtils.width(ch); | |
71a389c9 KL |
338 | } |
339 | scanLine(); | |
39e86397 KL |
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()); | |
e8a11f98 KL |
363 | } |
364 | ||
cd92b0ba | 365 | } |