#25 fix OOB on mouse down
[nikiroo-utils.git] / src / jexer / teditor / Document.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 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.io.FileOutputStream;
32 import java.io.IOException;
33 import java.io.OutputStreamWriter;
34 import java.util.ArrayList;
35 import java.util.List;
36
37 import jexer.bits.CellAttributes;
38
39 /**
40 * A Document represents a text file, as a collection of lines.
41 */
42 public class Document {
43
44 /**
45 * The list of lines.
46 */
47 private ArrayList<Line> lines = new ArrayList<Line>();
48
49 /**
50 * The current line number being edited. Note that this is 0-based, the
51 * first line is line number 0.
52 */
53 private int lineNumber = 0;
54
55 /**
56 * The overwrite flag. When true, characters overwrite data.
57 */
58 private boolean overwrite = false;
59
60 /**
61 * If true, the document has been edited.
62 */
63 private boolean dirty = false;
64
65 /**
66 * The default color for the TEditor class.
67 */
68 private CellAttributes defaultColor = null;
69
70 /**
71 * The text highlighter to use.
72 */
73 private Highlighter highlighter = new Highlighter();
74
75 /**
76 * Get the overwrite flag.
77 *
78 * @return true if addChar() overwrites data, false if it inserts
79 */
80 public boolean getOverwrite() {
81 return overwrite;
82 }
83
84 /**
85 * Get the dirty value.
86 *
87 * @return true if the buffer is dirty
88 */
89 public boolean isDirty() {
90 return dirty;
91 }
92
93 /**
94 * Save contents to file.
95 *
96 * @param filename file to save to
97 * @throws IOException if a java.io operation throws
98 */
99 public void saveToFilename(final String filename) throws IOException {
100 OutputStreamWriter output = null;
101 try {
102 output = new OutputStreamWriter(new FileOutputStream(filename),
103 "UTF-8");
104
105 for (Line line: lines) {
106 output.write(line.getRawString());
107 output.write("\n");
108 }
109
110 dirty = false;
111 }
112 finally {
113 if (output != null) {
114 output.close();
115 }
116 }
117 }
118
119 /**
120 * Set the overwrite flag.
121 *
122 * @param overwrite true if addChar() should overwrite data, false if it
123 * should insert
124 */
125 public void setOverwrite(final boolean overwrite) {
126 this.overwrite = overwrite;
127 }
128
129 /**
130 * Get the current line number being edited.
131 *
132 * @return the line number. Note that this is 0-based: 0 is the first
133 * line.
134 */
135 public int getLineNumber() {
136 return lineNumber;
137 }
138
139 /**
140 * Get the current editing line.
141 *
142 * @return the line
143 */
144 public Line getCurrentLine() {
145 return lines.get(lineNumber);
146 }
147
148 /**
149 * Get a specific line by number.
150 *
151 * @param lineNumber the line number. Note that this is 0-based: 0 is
152 * the first line.
153 * @return the line
154 */
155 public Line getLine(final int lineNumber) {
156 return lines.get(lineNumber);
157 }
158
159 /**
160 * Set the current line number being edited.
161 *
162 * @param n the line number. Note that this is 0-based: 0 is the first
163 * line.
164 */
165 public void setLineNumber(final int n) {
166 if ((n < 0) || (n > lines.size())) {
167 throw new IndexOutOfBoundsException("Lines array size is " +
168 lines.size() + ", requested index " + n);
169 }
170 lineNumber = n;
171 }
172
173 /**
174 * Get the current cursor position of the editing line.
175 *
176 * @return the cursor position
177 */
178 public int getCursor() {
179 return lines.get(lineNumber).getCursor();
180 }
181
182 /**
183 * Set the current cursor position of the editing line. 0-based.
184 *
185 * @param cursor the new cursor position
186 */
187 public void setCursor(final int cursor) {
188 if (cursor >= lines.get(lineNumber).getDisplayLength()) {
189 lines.get(lineNumber).end();
190 } else {
191 lines.get(lineNumber).setCursor(cursor);
192 }
193 }
194
195 /**
196 * Construct a new Document from an existing text string.
197 *
198 * @param str the text string
199 * @param defaultColor the color for unhighlighted text
200 */
201 public Document(final String str, final CellAttributes defaultColor) {
202 this.defaultColor = defaultColor;
203
204 // TODO: set different colors based on file extension
205 highlighter.setJavaColors();
206
207 String [] rawLines = str.split("\n");
208 for (int i = 0; i < rawLines.length; i++) {
209 lines.add(new Line(rawLines[i], this.defaultColor, highlighter));
210 }
211 }
212
213 /**
214 * Increment the line number by one. If at the last line, do nothing.
215 *
216 * @return true if the editing line changed
217 */
218 public boolean down() {
219 if (lineNumber < lines.size() - 1) {
220 int x = lines.get(lineNumber).getCursor();
221 lineNumber++;
222 if (x >= lines.get(lineNumber).getDisplayLength()) {
223 lines.get(lineNumber).end();
224 } else {
225 lines.get(lineNumber).setCursor(x);
226 }
227 return true;
228 }
229 return false;
230 }
231
232 /**
233 * Increment the line number by n. If n would go past the last line,
234 * increment only to the last line.
235 *
236 * @param n the number of lines to increment by
237 * @return true if the editing line changed
238 */
239 public boolean down(final int n) {
240 if (lineNumber < lines.size() - 1) {
241 int x = lines.get(lineNumber).getCursor();
242 lineNumber += n;
243 if (lineNumber > lines.size() - 1) {
244 lineNumber = lines.size() - 1;
245 }
246 if (x >= lines.get(lineNumber).getDisplayLength()) {
247 lines.get(lineNumber).end();
248 } else {
249 lines.get(lineNumber).setCursor(x);
250 }
251 return true;
252 }
253 return false;
254 }
255
256 /**
257 * Decrement the line number by one. If at the first line, do nothing.
258 *
259 * @return true if the editing line changed
260 */
261 public boolean up() {
262 if (lineNumber > 0) {
263 int x = lines.get(lineNumber).getCursor();
264 lineNumber--;
265 if (x >= lines.get(lineNumber).getDisplayLength()) {
266 lines.get(lineNumber).end();
267 } else {
268 lines.get(lineNumber).setCursor(x);
269 }
270 return true;
271 }
272 return false;
273 }
274
275 /**
276 * Decrement the line number by n. If n would go past the first line,
277 * decrement only to the first line.
278 *
279 * @param n the number of lines to decrement by
280 * @return true if the editing line changed
281 */
282 public boolean up(final int n) {
283 if (lineNumber > 0) {
284 int x = lines.get(lineNumber).getCursor();
285 lineNumber -= n;
286 if (lineNumber < 0) {
287 lineNumber = 0;
288 }
289 if (x >= lines.get(lineNumber).getDisplayLength()) {
290 lines.get(lineNumber).end();
291 } else {
292 lines.get(lineNumber).setCursor(x);
293 }
294 return true;
295 }
296 return false;
297 }
298
299 /**
300 * Decrement the cursor by one. If at the first column, do nothing.
301 *
302 * @return true if the cursor position changed
303 */
304 public boolean left() {
305 if (!lines.get(lineNumber).left()) {
306 // We are on the leftmost column, wrap
307 if (up()) {
308 end();
309 } else {
310 return false;
311 }
312 }
313 return true;
314 }
315
316 /**
317 * Increment the cursor by one. If at the last column, do nothing.
318 *
319 * @return true if the cursor position changed
320 */
321 public boolean right() {
322 if (!lines.get(lineNumber).right()) {
323 // We are on the rightmost column, wrap
324 if (down()) {
325 home();
326 } else {
327 return false;
328 }
329 }
330 return true;
331 }
332
333 /**
334 * Go to the first column of this line.
335 *
336 * @return true if the cursor position changed
337 */
338 public boolean home() {
339 return lines.get(lineNumber).home();
340 }
341
342 /**
343 * Go to the last column of this line.
344 *
345 * @return true if the cursor position changed
346 */
347 public boolean end() {
348 return lines.get(lineNumber).end();
349 }
350
351 /**
352 * Delete the character under the cursor.
353 */
354 public void del() {
355 dirty = true;
356 int cursor = lines.get(lineNumber).getCursor();
357 if (cursor < lines.get(lineNumber).getDisplayLength() - 1) {
358 lines.get(lineNumber).del();
359 } else if (lineNumber < lines.size() - 2) {
360 // Join two lines
361 StringBuilder newLine = new StringBuilder(lines.
362 get(lineNumber).getRawString());
363 newLine.append(lines.get(lineNumber + 1).getRawString());
364 lines.set(lineNumber, new Line(newLine.toString(),
365 defaultColor, highlighter));
366 lines.get(lineNumber).setCursor(cursor);
367 lines.remove(lineNumber + 1);
368 }
369 }
370
371 /**
372 * Delete the character immediately preceeding the cursor.
373 */
374 public void backspace() {
375 dirty = true;
376 int cursor = lines.get(lineNumber).getCursor();
377 if (cursor > 0) {
378 lines.get(lineNumber).backspace();
379 } else if (lineNumber > 0) {
380 // Join two lines
381 lineNumber--;
382 String firstLine = lines.get(lineNumber).getRawString();
383 if (firstLine.length() > 0) {
384 // Backspacing combining two lines
385 StringBuilder newLine = new StringBuilder(firstLine);
386 newLine.append(lines.get(lineNumber + 1).getRawString());
387 lines.set(lineNumber, new Line(newLine.toString(),
388 defaultColor, highlighter));
389 lines.get(lineNumber).setCursor(firstLine.length());
390 lines.remove(lineNumber + 1);
391 } else {
392 // Backspacing an empty line
393 lines.remove(lineNumber);
394 lines.get(lineNumber).setCursor(0);
395 }
396 }
397 }
398
399 /**
400 * Split the current line into two, like pressing the enter key.
401 */
402 public void enter() {
403 dirty = true;
404 int cursor = lines.get(lineNumber).getCursor();
405 String original = lines.get(lineNumber).getRawString();
406 String firstLine = original.substring(0, cursor);
407 String secondLine = original.substring(cursor);
408 lines.add(lineNumber + 1, new Line(secondLine, defaultColor,
409 highlighter));
410 lines.set(lineNumber, new Line(firstLine, defaultColor, highlighter));
411 lineNumber++;
412 lines.get(lineNumber).home();
413 }
414
415 /**
416 * Replace or insert a character at the cursor, depending on overwrite
417 * flag.
418 *
419 * @param ch the character to replace or insert
420 */
421 public void addChar(final char ch) {
422 dirty = true;
423 if (overwrite) {
424 lines.get(lineNumber).replaceChar(ch);
425 } else {
426 lines.get(lineNumber).addChar(ch);
427 }
428 }
429
430 /**
431 * Get a (shallow) copy of the list of lines.
432 *
433 * @return the list of lines
434 */
435 public List<Line> getLines() {
436 return new ArrayList<Line>(lines);
437 }
438
439 /**
440 * Get the number of lines.
441 *
442 * @return the number of lines
443 */
444 public int getLineCount() {
445 return lines.size();
446 }
447
448 /**
449 * Compute the maximum line length for this document.
450 *
451 * @return the number of cells needed to display the longest line
452 */
453 public int getLineLengthMax() {
454 int n = 0;
455 for (Line line : lines) {
456 if (line.getDisplayLength() > n) {
457 n = line.getDisplayLength();
458 }
459 }
460 return n;
461 }
462
463 /**
464 * Get the current line length.
465 *
466 * @return the number of cells needed to display the current line
467 */
468 public int getLineLength() {
469 return lines.get(lineNumber).getDisplayLength();
470 }
471
472 }