Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[fanfix.git] / src / jexer / TEditorWidget.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;
30
31 import java.io.IOException;
32
33 import jexer.bits.CellAttributes;
34 import jexer.event.TKeypressEvent;
35 import jexer.event.TMouseEvent;
36 import jexer.event.TResizeEvent;
37 import jexer.teditor.Document;
38 import jexer.teditor.Line;
39 import jexer.teditor.Word;
40 import 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 */
46 public class TEditorWidget extends TWidget {
47
48 // ------------------------------------------------------------------------
49 // Constants --------------------------------------------------------------
50 // ------------------------------------------------------------------------
51
52 /**
53 * The number of lines to scroll on mouse wheel up/down.
54 */
55 private static final int wheelScrollSize = 3;
56
57 // ------------------------------------------------------------------------
58 // Variables --------------------------------------------------------------
59 // ------------------------------------------------------------------------
60
61 /**
62 * The document being edited.
63 */
64 private Document document;
65
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
81 // ------------------------------------------------------------------------
82 // Constructors -----------------------------------------------------------
83 // ------------------------------------------------------------------------
84
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);
102
103 defaultColor = getTheme().getColor("teditor");
104 document = new Document(text, defaultColor);
105 }
106
107 // ------------------------------------------------------------------------
108 // TWidget ----------------------------------------------------------------
109 // ------------------------------------------------------------------------
110
111 /**
112 * Draw the text box.
113 */
114 @Override
115 public void draw() {
116 for (int i = 0; i < getHeight(); i++) {
117 // Background line
118 getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
119
120 // Now draw document's line
121 if (topLine + i < document.getLineCount()) {
122 Line line = document.getLine(topLine + i);
123 int x = 0;
124 for (Word word: line.getWords()) {
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(),
128 word.getColor());
129 x += word.getDisplayLength();
130 if (x - leftColumn > getWidth()) {
131 break;
132 }
133 }
134 }
135 }
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()) {
146 for (int i = 0; i < wheelScrollSize; i++) {
147 if (topLine > 0) {
148 topLine--;
149 alignDocument(false);
150 }
151 }
152 return;
153 }
154 if (mouse.isMouseWheelDown()) {
155 for (int i = 0; i < wheelScrollSize; i++) {
156 if (topLine < document.getLineCount() - 1) {
157 topLine++;
158 alignDocument(true);
159 }
160 }
161 return;
162 }
163
164 if (mouse.isMouse1()) {
165 // Set the row and column
166 int newLine = topLine + mouse.getY();
167 int newX = leftColumn + mouse.getX();
168 if (newLine > document.getLineCount() - 1) {
169 // Go to the end
170 document.setLineNumber(document.getLineCount() - 1);
171 document.end();
172 if (newLine > document.getLineCount() - 1) {
173 setCursorY(document.getLineCount() - 1 - topLine);
174 } else {
175 setCursorY(mouse.getY());
176 }
177 alignCursor();
178 return;
179 }
180
181 document.setLineNumber(newLine);
182 setCursorY(mouse.getY());
183 if (newX >= document.getCurrentLine().getDisplayLength()) {
184 document.end();
185 alignCursor();
186 } else {
187 document.setCursor(newX);
188 setCursorX(mouse.getX());
189 }
190 return;
191 }
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)) {
205 document.left();
206 alignTopLine(false);
207 } else if (keypress.equals(kbRight)) {
208 document.right();
209 alignTopLine(true);
210 } else if (keypress.equals(kbAltLeft)
211 || keypress.equals(kbCtrlLeft)
212 ) {
213 document.backwardsWord();
214 alignTopLine(false);
215 } else if (keypress.equals(kbAltRight)
216 || keypress.equals(kbCtrlRight)
217 ) {
218 document.forwardsWord();
219 alignTopLine(true);
220 } else if (keypress.equals(kbUp)) {
221 document.up();
222 alignTopLine(false);
223 } else if (keypress.equals(kbDown)) {
224 document.down();
225 alignTopLine(true);
226 } else if (keypress.equals(kbPgUp)) {
227 document.up(getHeight() - 1);
228 alignTopLine(false);
229 } else if (keypress.equals(kbPgDn)) {
230 document.down(getHeight() - 1);
231 alignTopLine(true);
232 } else if (keypress.equals(kbHome)) {
233 if (document.home()) {
234 leftColumn = 0;
235 if (leftColumn < 0) {
236 leftColumn = 0;
237 }
238 setCursorX(0);
239 }
240 } else if (keypress.equals(kbEnd)) {
241 if (document.end()) {
242 alignCursor();
243 }
244 } else if (keypress.equals(kbCtrlHome)) {
245 document.setLineNumber(0);
246 document.home();
247 topLine = 0;
248 leftColumn = 0;
249 setCursorX(0);
250 setCursorY(0);
251 } else if (keypress.equals(kbCtrlEnd)) {
252 document.setLineNumber(document.getLineCount() - 1);
253 document.end();
254 alignTopLine(false);
255 } else if (keypress.equals(kbIns)) {
256 document.setOverwrite(!document.getOverwrite());
257 } else if (keypress.equals(kbDel)) {
258 document.del();
259 alignCursor();
260 } else if (keypress.equals(kbBackspace)
261 || keypress.equals(kbBackspaceDel)
262 ) {
263 document.backspace();
264 alignTopLine(false);
265 } else if (keypress.equals(kbTab)) {
266 // TODO: tab character. For now just add spaces until we hit
267 // modulo 8.
268 for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
269 document.addChar(' ');
270 }
271 alignCursor();
272 } else if (keypress.equals(kbEnter)) {
273 document.enter();
274 alignTopLine(true);
275 } else if (!keypress.getKey().isFnKey()
276 && !keypress.getKey().isAlt()
277 && !keypress.getKey().isCtrl()
278 ) {
279 // Plain old keystroke, process it
280 document.addChar(keypress.getKey().getChar());
281 alignCursor();
282 } else {
283 // Pass other keys (tab etc.) on to TWidget
284 super.onKeypress(keypress);
285 }
286 }
287
288 /**
289 * Method that subclasses can override to handle window/screen resize
290 * events.
291 *
292 * @param resize resize event
293 */
294 @Override
295 public void onResize(final TResizeEvent resize) {
296 // Change my width/height, and pull the cursor in as needed.
297 if (resize.getType() == TResizeEvent.Type.WIDGET) {
298 setWidth(resize.getWidth());
299 setHeight(resize.getHeight());
300 // See if the cursor is now outside the window, and if so move
301 // things.
302 if (getCursorX() >= getWidth()) {
303 leftColumn += getCursorX() - (getWidth() - 1);
304 setCursorX(getWidth() - 1);
305 }
306 if (getCursorY() >= getHeight()) {
307 topLine += getCursorY() - (getHeight() - 1);
308 setCursorY(getHeight() - 1);
309 }
310 } else {
311 // Let superclass handle it
312 super.onResize(resize);
313 }
314 }
315
316 // ------------------------------------------------------------------------
317 // TEditorWidget ----------------------------------------------------------
318 // ------------------------------------------------------------------------
319
320 /**
321 * Align visible area with document current line.
322 *
323 * @param topLineIsTop if true, make the top visible line the document
324 * current line if it was off-screen. If false, make the bottom visible
325 * line the document current line.
326 */
327 private void alignTopLine(final boolean topLineIsTop) {
328 int line = document.getLineNumber();
329
330 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
331 // Need to move topLine to bring document back into view.
332 if (topLineIsTop) {
333 topLine = line - (getHeight() - 1);
334 if (topLine < 0) {
335 topLine = 0;
336 }
337 assert (topLine >= 0);
338 } else {
339 topLine = line;
340 assert (topLine >= 0);
341 }
342 }
343
344 /*
345 System.err.println("line " + line + " topLine " + topLine);
346 */
347
348 // Document is in view, let's set cursorY
349 assert (line >= topLine);
350 setCursorY(line - topLine);
351 alignCursor();
352 }
353
354 /**
355 * Align document current line with visible area.
356 *
357 * @param topLineIsTop if true, make the top visible line the document
358 * current line if it was off-screen. If false, make the bottom visible
359 * line the document current line.
360 */
361 private void alignDocument(final boolean topLineIsTop) {
362 int line = document.getLineNumber();
363 int cursor = document.getCursor();
364
365 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
366 // Need to move document to ensure it fits view.
367 if (topLineIsTop) {
368 document.setLineNumber(topLine);
369 } else {
370 document.setLineNumber(topLine + (getHeight() - 1));
371 }
372 if (cursor < document.getCurrentLine().getDisplayLength()) {
373 document.setCursor(cursor);
374 }
375 }
376
377 /*
378 System.err.println("getLineNumber() " + document.getLineNumber() +
379 " topLine " + topLine);
380 */
381
382 // Document is in view, let's set cursorY
383 setCursorY(document.getLineNumber() - topLine);
384 alignCursor();
385 }
386
387 /**
388 * Align visible cursor with document cursor.
389 */
390 private void alignCursor() {
391 int width = getWidth();
392
393 int desiredX = document.getCursor() - leftColumn;
394 if (desiredX < 0) {
395 // We need to push the screen to the left.
396 leftColumn = document.getCursor();
397 } else if (desiredX > width - 1) {
398 // We need to push the screen to the right.
399 leftColumn = document.getCursor() - (width - 1);
400 }
401
402 /*
403 System.err.println("document cursor " + document.getCursor() +
404 " leftColumn " + leftColumn);
405 */
406
407
408 setCursorX(document.getCursor() - leftColumn);
409 }
410
411 /**
412 * Get the number of lines in the underlying Document.
413 *
414 * @return the number of lines
415 */
416 public int getLineCount() {
417 return document.getLineCount();
418 }
419
420 /**
421 * Get the current visible top row number. 1-based.
422 *
423 * @return the visible top row number. Row 1 is the first row.
424 */
425 public int getVisibleRowNumber() {
426 return topLine + 1;
427 }
428
429 /**
430 * Set the current visible row number. 1-based.
431 *
432 * @param row the new visible row number. Row 1 is the first row.
433 */
434 public void setVisibleRowNumber(final int row) {
435 assert (row > 0);
436 if ((row > 0) && (row < document.getLineCount())) {
437 topLine = row - 1;
438 alignDocument(true);
439 }
440 }
441
442 /**
443 * Get the current editing row number. 1-based.
444 *
445 * @return the editing row number. Row 1 is the first row.
446 */
447 public int getEditingRowNumber() {
448 return document.getLineNumber() + 1;
449 }
450
451 /**
452 * Set the current editing row number. 1-based.
453 *
454 * @param row the new editing row number. Row 1 is the first row.
455 */
456 public void setEditingRowNumber(final int row) {
457 assert (row > 0);
458 if ((row > 0) && (row < document.getLineCount())) {
459 document.setLineNumber(row - 1);
460 alignTopLine(true);
461 }
462 }
463
464 /**
465 * Set the current visible column number. 1-based.
466 *
467 * @return the visible column number. Column 1 is the first column.
468 */
469 public int getVisibleColumnNumber() {
470 return leftColumn + 1;
471 }
472
473 /**
474 * Set the current visible column number. 1-based.
475 *
476 * @param column the new visible column number. Column 1 is the first
477 * column.
478 */
479 public void setVisibleColumnNumber(final int column) {
480 assert (column > 0);
481 if ((column > 0) && (column < document.getLineLengthMax())) {
482 leftColumn = column - 1;
483 alignDocument(true);
484 }
485 }
486
487 /**
488 * Get the current editing column number. 1-based.
489 *
490 * @return the editing column number. Column 1 is the first column.
491 */
492 public int getEditingColumnNumber() {
493 return document.getCursor() + 1;
494 }
495
496 /**
497 * Set the current editing column number. 1-based.
498 *
499 * @param column the new editing column number. Column 1 is the first
500 * column.
501 */
502 public void setEditingColumnNumber(final int column) {
503 if ((column > 0) && (column < document.getLineLength())) {
504 document.setCursor(column - 1);
505 alignCursor();
506 }
507 }
508
509 /**
510 * Get the maximum possible row number. 1-based.
511 *
512 * @return the maximum row number. Row 1 is the first row.
513 */
514 public int getMaximumRowNumber() {
515 return document.getLineCount() + 1;
516 }
517
518 /**
519 * Get the maximum possible column number. 1-based.
520 *
521 * @return the maximum column number. Column 1 is the first column.
522 */
523 public int getMaximumColumnNumber() {
524 return document.getLineLengthMax() + 1;
525 }
526
527 /**
528 * Get the dirty value.
529 *
530 * @return true if the buffer is dirty
531 */
532 public boolean isDirty() {
533 return document.isDirty();
534 }
535
536 /**
537 * Save contents to file.
538 *
539 * @param filename file to save to
540 * @throws IOException if a java.io operation throws
541 */
542 public void saveToFilename(final String filename) throws IOException {
543 document.saveToFilename(filename);
544 }
545
546 }