Update main screenshot
[fanfix.git] / src / jexer / TEditorWidget.java
CommitLineData
12b55d76
KL
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
a69ed767 6 * Copyright (C) 2019 Kevin Lamonte
12b55d76
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;
30
71a389c9
KL
31import java.io.IOException;
32
12b55d76
KL
33import jexer.bits.CellAttributes;
34import jexer.event.TKeypressEvent;
35import jexer.event.TMouseEvent;
e8a11f98 36import jexer.event.TResizeEvent;
12b55d76
KL
37import jexer.teditor.Document;
38import jexer.teditor.Line;
39import jexer.teditor.Word;
40import static jexer.TKeypress.*;
41
42/**
43 * TEditorWidget displays an editable text document. It is unaware of
44 * scrolling behavior, but can respond to mouse and keyboard events.
45 */
051e2913 46public class TEditorWidget extends TWidget {
12b55d76 47
615a0d99
KL
48 // ------------------------------------------------------------------------
49 // Constants --------------------------------------------------------------
50 // ------------------------------------------------------------------------
51
b1a8acec
KL
52 /**
53 * The number of lines to scroll on mouse wheel up/down.
54 */
55 private static final int wheelScrollSize = 3;
56
615a0d99
KL
57 // ------------------------------------------------------------------------
58 // Variables --------------------------------------------------------------
59 // ------------------------------------------------------------------------
60
12b55d76
KL
61 /**
62 * The document being edited.
63 */
64 private Document document;
65
e8a11f98
KL
66 /**
67 * The default color for the TEditor class.
68 */
69 private CellAttributes defaultColor = null;
70
71 /**
72 * The topmost line number in the visible area. 0-based.
73 */
74 private int topLine = 0;
75
76 /**
77 * The leftmost column number in the visible area. 0-based.
78 */
79 private int leftColumn = 0;
80
615a0d99
KL
81 // ------------------------------------------------------------------------
82 // Constructors -----------------------------------------------------------
83 // ------------------------------------------------------------------------
84
12b55d76
KL
85 /**
86 * Public constructor.
87 *
88 * @param parent parent widget
89 * @param text text on the screen
90 * @param x column relative to parent
91 * @param y row relative to parent
92 * @param width width of text area
93 * @param height height of text area
94 */
95 public TEditorWidget(final TWidget parent, final String text, final int x,
96 final int y, final int width, final int height) {
97
98 // Set parent and window
99 super(parent, x, y, width, height);
100
101 setCursorVisible(true);
e8a11f98
KL
102
103 defaultColor = getTheme().getColor("teditor");
104 document = new Document(text, defaultColor);
12b55d76
KL
105 }
106
615a0d99
KL
107 // ------------------------------------------------------------------------
108 // TWidget ----------------------------------------------------------------
109 // ------------------------------------------------------------------------
110
12b55d76
KL
111 /**
112 * Draw the text box.
113 */
114 @Override
115 public void draw() {
12b55d76
KL
116 for (int i = 0; i < getHeight(); i++) {
117 // Background line
e8a11f98 118 getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
12b55d76
KL
119
120 // Now draw document's line
e8a11f98
KL
121 if (topLine + i < document.getLineCount()) {
122 Line line = document.getLine(topLine + i);
12b55d76
KL
123 int x = 0;
124 for (Word word: line.getWords()) {
e8a11f98
KL
125 // For now, we are cheating: draw outside the left region
126 // if needed and let screen do the clipping.
127 getScreen().putStringXY(x - leftColumn, i, word.getText(),
12b55d76
KL
128 word.getColor());
129 x += word.getDisplayLength();
e8a11f98 130 if (x - leftColumn > getWidth()) {
12b55d76
KL
131 break;
132 }
133 }
134 }
135 }
12b55d76
KL
136 }
137
138 /**
139 * Handle mouse press events.
140 *
141 * @param mouse mouse button press event
142 */
143 @Override
144 public void onMouseDown(final TMouseEvent mouse) {
145 if (mouse.isMouseWheelUp()) {
b1a8acec
KL
146 for (int i = 0; i < wheelScrollSize; i++) {
147 if (topLine > 0) {
148 topLine--;
149 alignDocument(false);
150 }
e8a11f98 151 }
12b55d76
KL
152 return;
153 }
154 if (mouse.isMouseWheelDown()) {
b1a8acec
KL
155 for (int i = 0; i < wheelScrollSize; i++) {
156 if (topLine < document.getLineCount() - 1) {
157 topLine++;
158 alignDocument(true);
159 }
e8a11f98 160 }
12b55d76
KL
161 return;
162 }
163
e8a11f98
KL
164 if (mouse.isMouse1()) {
165 // Set the row and column
166 int newLine = topLine + mouse.getY();
167 int newX = leftColumn + mouse.getX();
df602ccf 168 if (newLine > document.getLineCount() - 1) {
e8a11f98
KL
169 // Go to the end
170 document.setLineNumber(document.getLineCount() - 1);
171 document.end();
df602ccf
KL
172 if (newLine > document.getLineCount() - 1) {
173 setCursorY(document.getLineCount() - 1 - topLine);
e8a11f98 174 } else {
df602ccf 175 setCursorY(mouse.getY());
e8a11f98
KL
176 }
177 alignCursor();
178 return;
179 }
180
181 document.setLineNumber(newLine);
182 setCursorY(mouse.getY());
4297b49b 183 if (newX >= document.getCurrentLine().getDisplayLength()) {
e8a11f98
KL
184 document.end();
185 alignCursor();
186 } else {
df602ccf 187 document.setCursor(newX);
e8a11f98
KL
188 setCursorX(mouse.getX());
189 }
190 return;
191 }
12b55d76
KL
192
193 // Pass to children
194 super.onMouseDown(mouse);
195 }
196
197 /**
198 * Handle keystrokes.
199 *
200 * @param keypress keystroke event
201 */
202 @Override
203 public void onKeypress(final TKeypressEvent keypress) {
204 if (keypress.equals(kbLeft)) {
df602ccf
KL
205 document.left();
206 alignTopLine(false);
12b55d76 207 } else if (keypress.equals(kbRight)) {
df602ccf
KL
208 document.right();
209 alignTopLine(true);
12b55d76 210 } else if (keypress.equals(kbUp)) {
71a389c9
KL
211 document.up();
212 alignTopLine(false);
12b55d76 213 } else if (keypress.equals(kbDown)) {
71a389c9
KL
214 document.down();
215 alignTopLine(true);
12b55d76 216 } else if (keypress.equals(kbPgUp)) {
71a389c9
KL
217 document.up(getHeight() - 1);
218 alignTopLine(false);
12b55d76 219 } else if (keypress.equals(kbPgDn)) {
71a389c9
KL
220 document.down(getHeight() - 1);
221 alignTopLine(true);
12b55d76 222 } else if (keypress.equals(kbHome)) {
e8a11f98
KL
223 if (document.home()) {
224 leftColumn = 0;
225 if (leftColumn < 0) {
226 leftColumn = 0;
227 }
228 setCursorX(0);
229 }
12b55d76 230 } else if (keypress.equals(kbEnd)) {
e8a11f98
KL
231 if (document.end()) {
232 alignCursor();
233 }
12b55d76
KL
234 } else if (keypress.equals(kbCtrlHome)) {
235 document.setLineNumber(0);
236 document.home();
e8a11f98
KL
237 topLine = 0;
238 leftColumn = 0;
239 setCursorX(0);
240 setCursorY(0);
12b55d76
KL
241 } else if (keypress.equals(kbCtrlEnd)) {
242 document.setLineNumber(document.getLineCount() - 1);
243 document.end();
71a389c9 244 alignTopLine(false);
12b55d76
KL
245 } else if (keypress.equals(kbIns)) {
246 document.setOverwrite(!document.getOverwrite());
247 } else if (keypress.equals(kbDel)) {
248 document.del();
71a389c9 249 alignCursor();
12b55d76
KL
250 } else if (keypress.equals(kbBackspace)) {
251 document.backspace();
df602ccf
KL
252 alignTopLine(false);
253 } else if (keypress.equals(kbTab)) {
254 // TODO: tab character. For now just add spaces until we hit
255 // modulo 8.
256 for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
257 document.addChar(' ');
258 }
e8a11f98 259 alignCursor();
71a389c9 260 } else if (keypress.equals(kbEnter)) {
df602ccf
KL
261 document.enter();
262 alignTopLine(true);
12b55d76
KL
263 } else if (!keypress.getKey().isFnKey()
264 && !keypress.getKey().isAlt()
265 && !keypress.getKey().isCtrl()
266 ) {
267 // Plain old keystroke, process it
afdec5e9
KL
268 // TODO: fix document to use ints, not chars
269 document.addChar((char) keypress.getKey().getChar());
71a389c9 270 alignCursor();
12b55d76
KL
271 } else {
272 // Pass other keys (tab etc.) on to TWidget
273 super.onKeypress(keypress);
274 }
275 }
276
e8a11f98
KL
277 /**
278 * Method that subclasses can override to handle window/screen resize
279 * events.
280 *
281 * @param resize resize event
282 */
283 @Override
284 public void onResize(final TResizeEvent resize) {
285 // Change my width/height, and pull the cursor in as needed.
286 if (resize.getType() == TResizeEvent.Type.WIDGET) {
287 setWidth(resize.getWidth());
288 setHeight(resize.getHeight());
289 // See if the cursor is now outside the window, and if so move
290 // things.
291 if (getCursorX() >= getWidth()) {
292 leftColumn += getCursorX() - (getWidth() - 1);
293 setCursorX(getWidth() - 1);
294 }
295 if (getCursorY() >= getHeight()) {
296 topLine += getCursorY() - (getHeight() - 1);
297 setCursorY(getHeight() - 1);
298 }
299 } else {
300 // Let superclass handle it
301 super.onResize(resize);
302 }
303 }
304
615a0d99
KL
305 // ------------------------------------------------------------------------
306 // TEditorWidget ----------------------------------------------------------
307 // ------------------------------------------------------------------------
308
309 /**
310 * Align visible area with document current line.
311 *
312 * @param topLineIsTop if true, make the top visible line the document
313 * current line if it was off-screen. If false, make the bottom visible
314 * line the document current line.
315 */
316 private void alignTopLine(final boolean topLineIsTop) {
317 int line = document.getLineNumber();
318
319 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
320 // Need to move topLine to bring document back into view.
321 if (topLineIsTop) {
322 topLine = line - (getHeight() - 1);
323 if (topLine < 0) {
324 topLine = 0;
325 }
326 assert (topLine >= 0);
327 } else {
328 topLine = line;
329 assert (topLine >= 0);
330 }
331 }
332
333 /*
334 System.err.println("line " + line + " topLine " + topLine);
335 */
336
337 // Document is in view, let's set cursorY
338 assert (line >= topLine);
339 setCursorY(line - topLine);
340 alignCursor();
341 }
342
343 /**
344 * Align document current line with visible area.
345 *
346 * @param topLineIsTop if true, make the top visible line the document
347 * current line if it was off-screen. If false, make the bottom visible
348 * line the document current line.
349 */
350 private void alignDocument(final boolean topLineIsTop) {
351 int line = document.getLineNumber();
352 int cursor = document.getCursor();
353
354 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
355 // Need to move document to ensure it fits view.
356 if (topLineIsTop) {
357 document.setLineNumber(topLine);
358 } else {
359 document.setLineNumber(topLine + (getHeight() - 1));
360 }
361 if (cursor < document.getCurrentLine().getDisplayLength()) {
362 document.setCursor(cursor);
363 }
364 }
365
366 /*
367 System.err.println("getLineNumber() " + document.getLineNumber() +
368 " topLine " + topLine);
369 */
370
371 // Document is in view, let's set cursorY
372 setCursorY(document.getLineNumber() - topLine);
373 alignCursor();
374 }
375
376 /**
377 * Align visible cursor with document cursor.
378 */
379 private void alignCursor() {
380 int width = getWidth();
381
382 int desiredX = document.getCursor() - leftColumn;
383 if (desiredX < 0) {
384 // We need to push the screen to the left.
385 leftColumn = document.getCursor();
386 } else if (desiredX > width - 1) {
387 // We need to push the screen to the right.
388 leftColumn = document.getCursor() - (width - 1);
389 }
390
391 /*
392 System.err.println("document cursor " + document.getCursor() +
393 " leftColumn " + leftColumn);
394 */
395
396
397 setCursorX(document.getCursor() - leftColumn);
398 }
399
71a389c9
KL
400 /**
401 * Get the number of lines in the underlying Document.
402 *
403 * @return the number of lines
404 */
405 public int getLineCount() {
406 return document.getLineCount();
407 }
408
b6faeac0
KL
409 /**
410 * Get the current visible top row number. 1-based.
411 *
412 * @return the visible top row number. Row 1 is the first row.
413 */
414 public int getVisibleRowNumber() {
415 return topLine + 1;
416 }
417
418 /**
419 * Set the current visible row number. 1-based.
420 *
421 * @param row the new visible row number. Row 1 is the first row.
422 */
423 public void setVisibleRowNumber(final int row) {
424 assert (row > 0);
425 if ((row > 0) && (row < document.getLineCount())) {
426 topLine = row - 1;
427 alignDocument(true);
428 }
429 }
430
71a389c9
KL
431 /**
432 * Get the current editing row number. 1-based.
433 *
434 * @return the editing row number. Row 1 is the first row.
435 */
436 public int getEditingRowNumber() {
437 return document.getLineNumber() + 1;
438 }
439
440 /**
441 * Set the current editing row number. 1-based.
442 *
443 * @param row the new editing row number. Row 1 is the first row.
444 */
445 public void setEditingRowNumber(final int row) {
fe0770f9
KL
446 assert (row > 0);
447 if ((row > 0) && (row < document.getLineCount())) {
448 document.setLineNumber(row - 1);
449 alignTopLine(true);
450 }
71a389c9
KL
451 }
452
453 /**
454 * Get the current editing column number. 1-based.
455 *
456 * @return the editing column number. Column 1 is the first column.
457 */
458 public int getEditingColumnNumber() {
459 return document.getCursor() + 1;
460 }
461
462 /**
463 * Set the current editing column number. 1-based.
464 *
465 * @param column the new editing column number. Column 1 is the first
466 * column.
467 */
468 public void setEditingColumnNumber(final int column) {
fe0770f9
KL
469 if ((column > 0) && (column < document.getLineLength())) {
470 document.setCursor(column - 1);
471 alignCursor();
472 }
71a389c9
KL
473 }
474
475 /**
476 * Get the maximum possible row number. 1-based.
477 *
478 * @return the maximum row number. Row 1 is the first row.
479 */
480 public int getMaximumRowNumber() {
481 return document.getLineCount() + 1;
482 }
483
484 /**
485 * Get the maximum possible column number. 1-based.
486 *
487 * @return the maximum column number. Column 1 is the first column.
488 */
489 public int getMaximumColumnNumber() {
490 return document.getLineLengthMax() + 1;
491 }
492
493 /**
494 * Get the dirty value.
495 *
496 * @return true if the buffer is dirty
497 */
498 public boolean isDirty() {
499 return document.isDirty();
500 }
501
502 /**
503 * Save contents to file.
504 *
505 * @param filename file to save to
506 * @throws IOException if a java.io operation throws
507 */
508 public void saveToFilename(final String filename) throws IOException {
509 document.saveToFilename(filename);
510 }
511
12b55d76 512}