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