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