#50 additional mouse pointer options
[fanfix.git] / src / jexer / teditor / Line.java
CommitLineData
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 */
29package jexer.teditor;
30
31import java.util.ArrayList;
32import java.util.List;
33
e8a11f98 34import jexer.bits.CellAttributes;
2d3f60d8 35import 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 41public 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
cd92b0ba 158 /**
71a389c9 159 * Get the on-screen display length.
cd92b0ba 160 *
71a389c9 161 * @return the number of cells needed to display this line
cd92b0ba 162 */
71a389c9 163 public int getDisplayLength() {
2d3f60d8 164 int n = StringUtils.width(rawText.toString());
71a389c9 165
71a389c9
KL
166 if (n > 0) {
167 // If we have any visible characters, add one to the display so
39e86397 168 // that the position is immediately after the data.
71a389c9
KL
169 return n + 1;
170 }
171 return n;
cd92b0ba
KL
172 }
173
174 /**
71a389c9 175 * Get the raw string that matches this line.
cd92b0ba 176 *
71a389c9 177 * @return the string
cd92b0ba 178 */
71a389c9
KL
179 public String getRawString() {
180 return rawText.toString();
181 }
e8a11f98 182
71a389c9
KL
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);
2d3f60d8
KL
190 for (int i = 0; i < rawText.length();) {
191 int ch = rawText.codePointAt(i);
192 i += Character.charCount(ch);
71a389c9
KL
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();
e8a11f98 201 }
cd92b0ba
KL
202 }
203
cd92b0ba 204 /**
12b55d76 205 * Decrement the cursor by one. If at the first column, do nothing.
e8a11f98
KL
206 *
207 * @return true if the cursor position changed
cd92b0ba 208 */
e8a11f98 209 public boolean left() {
39e86397 210 if (position == 0) {
e8a11f98 211 return false;
cd92b0ba 212 }
39e86397
KL
213 screenPosition -= StringUtils.width(rawText.codePointBefore(position));
214 position -= Character.charCount(rawText.codePointBefore(position));
e8a11f98 215 return true;
cd92b0ba
KL
216 }
217
218 /**
12b55d76 219 * Increment the cursor by one. If at the last column, do nothing.
e8a11f98
KL
220 *
221 * @return true if the cursor position changed
cd92b0ba 222 */
e8a11f98
KL
223 public boolean right() {
224 if (getDisplayLength() == 0) {
225 return false;
cd92b0ba 226 }
39e86397 227 if (position == getDisplayLength() - 1) {
e8a11f98
KL
228 return false;
229 }
39e86397
KL
230 if (position < rawText.length()) {
231 screenPosition += StringUtils.width(rawText.codePointAt(position));
232 position += Character.charCount(rawText.codePointAt(position));
2d3f60d8 233 }
39e86397 234 assert (position <= rawText.length());
e8a11f98 235 return true;
cd92b0ba
KL
236 }
237
238 /**
12b55d76 239 * Go to the first column of this line.
e8a11f98
KL
240 *
241 * @return true if the cursor position changed
cd92b0ba 242 */
e8a11f98 243 public boolean home() {
39e86397
KL
244 if (position > 0) {
245 position = 0;
246 screenPosition = 0;
e8a11f98
KL
247 return true;
248 }
249 return false;
cd92b0ba
KL
250 }
251
252 /**
12b55d76 253 * Go to the last column of this line.
e8a11f98
KL
254 *
255 * @return true if the cursor position changed
cd92b0ba 256 */
e8a11f98 257 public boolean end() {
39e86397
KL
258 if (position != getDisplayLength() - 1) {
259 position = rawText.length();
260 screenPosition = StringUtils.width(rawText.toString());
e8a11f98
KL
261 return true;
262 }
263 return false;
cd92b0ba
KL
264 }
265
266 /**
12b55d76 267 * Delete the character under the cursor.
cd92b0ba 268 */
12b55d76 269 public void del() {
71a389c9
KL
270 assert (words.size() > 0);
271
39e86397
KL
272 if (position < getDisplayLength()) {
273 int n = Character.charCount(rawText.codePointAt(position));
274 for (int i = 0; i < n; i++) {
275 rawText.deleteCharAt(position);
2d3f60d8 276 }
71a389c9
KL
277 }
278
279 // Re-scan the line to determine the new word boundaries.
280 scanLine();
cd92b0ba
KL
281 }
282
283 /**
12b55d76 284 * Delete the character immediately preceeding the cursor.
cd92b0ba 285 */
12b55d76 286 public void backspace() {
71a389c9
KL
287 if (left()) {
288 del();
289 }
cd92b0ba
KL
290 }
291
292 /**
e8a11f98 293 * Insert a character at the cursor.
cd92b0ba 294 *
e8a11f98 295 * @param ch the character to insert
cd92b0ba 296 */
2d3f60d8 297 public void addChar(final int ch) {
39e86397
KL
298 if (position < getDisplayLength() - 1) {
299 rawText.insert(position, Character.toChars(ch));
71a389c9 300 } else {
2d3f60d8 301 rawText.append(Character.toChars(ch));
71a389c9 302 }
39e86397
KL
303 position += Character.charCount(ch);
304 screenPosition += StringUtils.width(ch);
71a389c9 305 scanLine();
cd92b0ba
KL
306 }
307
e8a11f98
KL
308 /**
309 * Replace a character at the cursor.
310 *
311 * @param ch the character to replace
312 */
2d3f60d8 313 public void replaceChar(final int ch) {
39e86397
KL
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);
71a389c9 322 } else {
2d3f60d8 323 rawText.append(Character.toChars(ch));
39e86397
KL
324 position += Character.charCount(ch);
325 screenPosition += StringUtils.width(ch);
71a389c9
KL
326 }
327 scanLine();
39e86397
KL
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());
e8a11f98
KL
351 }
352
cd92b0ba 353}