#35 CJK in editor working
[nikiroo-utils.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 on-screen display length.
160 *
161 * @return the number of cells needed to display this line
162 */
163 public int getDisplayLength() {
164 int n = StringUtils.width(rawText.toString());
165
166 if (n > 0) {
167 // If we have any visible characters, add one to the display so
168 // that the position is immediately after the data.
169 return n + 1;
170 }
171 return n;
172 }
173
174 /**
175 * Get the raw string that matches this line.
176 *
177 * @return the string
178 */
179 public String getRawString() {
180 return rawText.toString();
181 }
182
183 /**
184 * Scan rawText and make words out of it.
185 */
186 private void scanLine() {
187 words.clear();
188 Word word = new Word(this.defaultColor, this.highlighter);
189 words.add(word);
190 for (int i = 0; i < rawText.length();) {
191 int ch = rawText.codePointAt(i);
192 i += Character.charCount(ch);
193 Word newWord = word.addChar(ch);
194 if (newWord != word) {
195 words.add(newWord);
196 word = newWord;
197 }
198 }
199 for (Word w: words) {
200 w.applyHighlight();
201 }
202 }
203
204 /**
205 * Decrement the cursor by one. If at the first column, do nothing.
206 *
207 * @return true if the cursor position changed
208 */
209 public boolean left() {
210 if (position == 0) {
211 return false;
212 }
213 screenPosition -= StringUtils.width(rawText.codePointBefore(position));
214 position -= Character.charCount(rawText.codePointBefore(position));
215 return true;
216 }
217
218 /**
219 * Increment the cursor by one. If at the last column, do nothing.
220 *
221 * @return true if the cursor position changed
222 */
223 public boolean right() {
224 if (getDisplayLength() == 0) {
225 return false;
226 }
227 if (position == getDisplayLength() - 1) {
228 return false;
229 }
230 if (position < rawText.length()) {
231 screenPosition += StringUtils.width(rawText.codePointAt(position));
232 position += Character.charCount(rawText.codePointAt(position));
233 }
234 assert (position <= rawText.length());
235 return true;
236 }
237
238 /**
239 * Go to the first column of this line.
240 *
241 * @return true if the cursor position changed
242 */
243 public boolean home() {
244 if (position > 0) {
245 position = 0;
246 screenPosition = 0;
247 return true;
248 }
249 return false;
250 }
251
252 /**
253 * Go to the last column of this line.
254 *
255 * @return true if the cursor position changed
256 */
257 public boolean end() {
258 if (position != getDisplayLength() - 1) {
259 position = rawText.length();
260 screenPosition = StringUtils.width(rawText.toString());
261 return true;
262 }
263 return false;
264 }
265
266 /**
267 * Delete the character under the cursor.
268 */
269 public void del() {
270 assert (words.size() > 0);
271
272 if (position < getDisplayLength()) {
273 int n = Character.charCount(rawText.codePointAt(position));
274 for (int i = 0; i < n; i++) {
275 rawText.deleteCharAt(position);
276 }
277 }
278
279 // Re-scan the line to determine the new word boundaries.
280 scanLine();
281 }
282
283 /**
284 * Delete the character immediately preceeding the cursor.
285 */
286 public void backspace() {
287 if (left()) {
288 del();
289 }
290 }
291
292 /**
293 * Insert a character at the cursor.
294 *
295 * @param ch the character to insert
296 */
297 public void addChar(final int ch) {
298 if (position < getDisplayLength() - 1) {
299 rawText.insert(position, Character.toChars(ch));
300 } else {
301 rawText.append(Character.toChars(ch));
302 }
303 position += Character.charCount(ch);
304 screenPosition += StringUtils.width(ch);
305 scanLine();
306 }
307
308 /**
309 * Replace a character at the cursor.
310 *
311 * @param ch the character to replace
312 */
313 public void replaceChar(final int ch) {
314 if (position < getDisplayLength() - 1) {
315 // Replace character
316 String oldText = rawText.toString();
317 rawText = new StringBuilder(oldText.substring(0, position));
318 rawText.append(Character.toChars(ch));
319 rawText.append(oldText.substring(position + 1));
320 screenPosition += StringUtils.width(rawText.codePointAt(position));
321 position += Character.charCount(ch);
322 } else {
323 rawText.append(Character.toChars(ch));
324 position += Character.charCount(ch);
325 screenPosition += StringUtils.width(ch);
326 }
327 scanLine();
328 }
329
330 /**
331 * Determine string position from screen position.
332 *
333 * @param screenPosition the position on screen
334 * @return the equivalent position in text
335 */
336 protected int screenToTextPosition(final int screenPosition) {
337 if (screenPosition == 0) {
338 return 0;
339 }
340
341 int n = 0;
342 for (int i = 0; i < rawText.length(); i++) {
343 n += StringUtils.width(rawText.codePointAt(i));
344 if (n >= screenPosition) {
345 return i + 1;
346 }
347 }
348 // screenPosition exceeds the available text length.
349 throw new IndexOutOfBoundsException("screenPosition " + screenPosition +
350 " exceeds available text length " + rawText.length());
351 }
352
353 }