2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
31 import java
.io
.IOException
;
33 import jexer
.bits
.CellAttributes
;
34 import jexer
.event
.TCommandEvent
;
35 import jexer
.event
.TKeypressEvent
;
36 import jexer
.event
.TMouseEvent
;
37 import jexer
.event
.TResizeEvent
;
38 import jexer
.teditor
.Document
;
39 import jexer
.teditor
.Line
;
40 import jexer
.teditor
.Word
;
41 import static jexer
.TCommand
.*;
42 import static jexer
.TKeypress
.*;
45 * TEditorWidget displays an editable text document. It is unaware of
46 * scrolling behavior, but can respond to mouse and keyboard events.
48 public class TEditorWidget
extends TWidget
implements EditMenuUser
{
50 // ------------------------------------------------------------------------
51 // Constants --------------------------------------------------------------
52 // ------------------------------------------------------------------------
55 * The number of lines to scroll on mouse wheel up/down.
57 private static final int wheelScrollSize
= 3;
59 // ------------------------------------------------------------------------
60 // Variables --------------------------------------------------------------
61 // ------------------------------------------------------------------------
64 * The document being edited.
66 private Document document
;
69 * The default color for the TEditor class.
71 private CellAttributes defaultColor
= null;
74 * The topmost line number in the visible area. 0-based.
76 private int topLine
= 0;
79 * The leftmost column number in the visible area. 0-based.
81 private int leftColumn
= 0;
84 * If true, selection is a rectangle.
86 private boolean selectionRectangle
= false;
89 * If true, the mouse is dragging a selection.
91 private boolean inSelection
= false;
94 * Selection starting column.
96 private int selectionColumn0
;
99 * Selection starting line.
101 private int selectionLine0
;
104 * Selection ending column.
106 private int selectionColumn1
;
109 * Selection ending line.
111 private int selectionLine1
;
113 // ------------------------------------------------------------------------
114 // Constructors -----------------------------------------------------------
115 // ------------------------------------------------------------------------
118 * Public constructor.
120 * @param parent parent widget
121 * @param text text on the screen
122 * @param x column relative to parent
123 * @param y row relative to parent
124 * @param width width of text area
125 * @param height height of text area
127 public TEditorWidget(final TWidget parent
, final String text
, final int x
,
128 final int y
, final int width
, final int height
) {
130 // Set parent and window
131 super(parent
, x
, y
, width
, height
);
133 setCursorVisible(true);
135 defaultColor
= getTheme().getColor("teditor");
136 document
= new Document(text
, defaultColor
);
139 // ------------------------------------------------------------------------
140 // Event handlers ---------------------------------------------------------
141 // ------------------------------------------------------------------------
144 * Handle mouse press events.
146 * @param mouse mouse button press event
149 public void onMouseDown(final TMouseEvent mouse
) {
150 if (mouse
.isMouseWheelUp()) {
151 for (int i
= 0; i
< wheelScrollSize
; i
++) {
154 alignDocument(false);
159 if (mouse
.isMouseWheelDown()) {
160 for (int i
= 0; i
< wheelScrollSize
; i
++) {
161 if (topLine
< document
.getLineCount() - 1) {
169 if (mouse
.isMouse1()) {
172 selectionColumn1
= leftColumn
+ mouse
.getX();
173 selectionLine1
= topLine
+ mouse
.getY();
174 } else if (mouse
.isShift() || mouse
.isCtrl()) {
176 selectionColumn0
= leftColumn
+ mouse
.getX();
177 selectionLine0
= topLine
+ mouse
.getY();
178 selectionColumn1
= selectionColumn0
;
179 selectionLine1
= selectionLine0
;
180 selectionRectangle
= mouse
.isAlt() | mouse
.isCtrl();
183 // Set the row and column
184 int newLine
= topLine
+ mouse
.getY();
185 int newX
= leftColumn
+ mouse
.getX();
186 if (newLine
> document
.getLineCount() - 1) {
188 document
.setLineNumber(document
.getLineCount() - 1);
190 if (newLine
> document
.getLineCount() - 1) {
191 setCursorY(document
.getLineCount() - 1 - topLine
);
193 setCursorY(mouse
.getY());
197 selectionColumn1
= document
.getCursor();
198 selectionLine1
= document
.getLineNumber();
199 selectionRectangle
= mouse
.isCtrl();
204 document
.setLineNumber(newLine
);
205 setCursorY(mouse
.getY());
206 if (newX
>= document
.getCurrentLine().getDisplayLength()) {
210 document
.setCursor(newX
);
211 setCursorX(mouse
.getX());
214 selectionColumn1
= document
.getCursor();
215 selectionLine1
= document
.getLineNumber();
216 selectionRectangle
= mouse
.isCtrl();
224 super.onMouseDown(mouse
);
228 * Handle mouse motion events.
230 * @param mouse mouse motion event
233 public void onMouseMotion(final TMouseEvent mouse
) {
235 if (mouse
.isMouse1()) {
238 selectionColumn1
= leftColumn
+ mouse
.getX();
239 selectionLine1
= topLine
+ mouse
.getY();
240 } else if (mouse
.isShift() || mouse
.isCtrl()) {
242 selectionColumn0
= leftColumn
+ mouse
.getX();
243 selectionLine0
= topLine
+ mouse
.getY();
244 selectionColumn1
= selectionColumn0
;
245 selectionLine1
= selectionLine0
;
246 selectionRectangle
= mouse
.isAlt() | mouse
.isCtrl();
249 // Set the row and column
250 int newLine
= topLine
+ mouse
.getY();
251 int newX
= leftColumn
+ mouse
.getX();
252 if (newLine
> document
.getLineCount() - 1) {
254 document
.setLineNumber(document
.getLineCount() - 1);
256 if (newLine
> document
.getLineCount() - 1) {
257 setCursorY(document
.getLineCount() - 1 - topLine
);
259 setCursorY(mouse
.getY());
263 selectionColumn1
= document
.getCursor();
264 selectionLine1
= document
.getLineNumber();
265 selectionRectangle
= mouse
.isCtrl();
270 document
.setLineNumber(newLine
);
271 setCursorY(mouse
.getY());
272 if (newX
>= document
.getCurrentLine().getDisplayLength()) {
276 document
.setCursor(newX
);
277 setCursorX(mouse
.getX());
280 selectionColumn1
= document
.getCursor();
281 selectionLine1
= document
.getLineNumber();
282 selectionRectangle
= mouse
.isCtrl();
290 super.onMouseDown(mouse
);
294 * Handle mouse release events.
296 * @param mouse mouse button release event
299 public void onMouseUp(final TMouseEvent mouse
) {
303 super.onMouseDown(mouse
);
309 * @param keypress keystroke event
312 public void onKeypress(final TKeypressEvent keypress
) {
313 if (keypress
.getKey().isShift() || keypress
.getKey().isCtrl()) {
317 selectionColumn0
= document
.getCursor();
318 selectionLine0
= document
.getLineNumber();
319 selectionColumn1
= selectionColumn0
;
320 selectionLine1
= selectionLine0
;
321 selectionRectangle
= keypress
.getKey().isCtrl();
327 if (keypress
.equals(kbLeft
)
328 || keypress
.equals(kbShiftLeft
)
332 } else if (keypress
.equals(kbRight
)
333 || keypress
.equals(kbShiftRight
)
337 } else if (keypress
.equals(kbAltLeft
)
338 || keypress
.equals(kbCtrlLeft
)
340 document
.backwardsWord();
342 } else if (keypress
.equals(kbAltRight
)
343 || keypress
.equals(kbCtrlRight
)
345 document
.forwardsWord();
347 } else if (keypress
.equals(kbUp
)
348 || keypress
.equals(kbShiftUp
)
352 } else if (keypress
.equals(kbDown
)
353 || keypress
.equals(kbShiftDown
)
357 } else if (keypress
.equals(kbPgUp
)) {
358 document
.up(getHeight() - 1);
360 } else if (keypress
.equals(kbPgDn
)) {
361 document
.down(getHeight() - 1);
363 } else if (keypress
.equals(kbHome
)) {
364 if (document
.home()) {
366 if (leftColumn
< 0) {
371 } else if (keypress
.equals(kbEnd
)) {
372 if (document
.end()) {
375 } else if (keypress
.equals(kbCtrlHome
)) {
376 document
.setLineNumber(0);
382 } else if (keypress
.equals(kbCtrlEnd
)) {
383 document
.setLineNumber(document
.getLineCount() - 1);
386 } else if (keypress
.equals(kbIns
)) {
387 document
.setOverwrite(!document
.getOverwrite());
388 } else if (keypress
.equals(kbDel
)) {
391 } else if (keypress
.equals(kbBackspace
)
392 || keypress
.equals(kbBackspaceDel
)
394 document
.backspace();
396 } else if (keypress
.equals(kbTab
)) {
397 // Add spaces until we hit modulo 8.
398 for (int i
= document
.getCursor(); (i
+ 1) % 8 != 0; i
++) {
399 document
.addChar(' ');
402 } else if (keypress
.equals(kbEnter
)) {
405 } else if (!keypress
.getKey().isFnKey()
406 && !keypress
.getKey().isAlt()
407 && !keypress
.getKey().isCtrl()
409 // Plain old keystroke, process it
410 document
.addChar(keypress
.getKey().getChar());
413 // Pass other keys (tab etc.) on to TWidget
414 super.onKeypress(keypress
);
418 selectionColumn1
= document
.getCursor();
419 selectionLine1
= document
.getLineNumber();
420 selectionRectangle
= keypress
.getKey().isCtrl();
425 * Method that subclasses can override to handle window/screen resize
428 * @param resize resize event
431 public void onResize(final TResizeEvent resize
) {
432 // Change my width/height, and pull the cursor in as needed.
433 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
434 setWidth(resize
.getWidth());
435 setHeight(resize
.getHeight());
436 // See if the cursor is now outside the window, and if so move
438 if (getCursorX() >= getWidth()) {
439 leftColumn
+= getCursorX() - (getWidth() - 1);
440 setCursorX(getWidth() - 1);
442 if (getCursorY() >= getHeight()) {
443 topLine
+= getCursorY() - (getHeight() - 1);
444 setCursorY(getHeight() - 1);
447 // Let superclass handle it
448 super.onResize(resize
);
453 * Handle posted command events.
455 * @param command command event
458 public void onCommand(final TCommandEvent command
) {
459 if (command
.equals(cmCut
)) {
460 // Copy text to clipboard, and then remove it.
468 if (command
.equals(cmCopy
)) {
469 // Copy text to clipboard.
476 if (command
.equals(cmPaste
)) {
477 // Delete selected text, then paste text from clipboard.
480 String text
= getClipboard().pasteText();
482 for (int i
= 0; i
< text
.length(); ) {
483 int ch
= text
.codePointAt(i
);
484 onKeypress(new TKeypressEvent(false, 0, ch
, false, false,
486 i
+= Character
.charCount(ch
);
492 if (command
.equals(cmClear
)) {
500 // ------------------------------------------------------------------------
501 // TWidget ----------------------------------------------------------------
502 // ------------------------------------------------------------------------
509 for (int i
= 0; i
< getHeight(); i
++) {
511 getScreen().hLineXY(0, i
, getWidth(), ' ', defaultColor
);
513 // Now draw document's line
514 if (topLine
+ i
< document
.getLineCount()) {
515 Line line
= document
.getLine(topLine
+ i
);
517 for (Word word
: line
.getWords()) {
518 // For now, we are cheating: draw outside the left region
519 // if needed and let screen do the clipping.
520 getScreen().putStringXY(x
- leftColumn
, i
, word
.getText(),
522 x
+= word
.getDisplayLength();
523 if (x
- leftColumn
> getWidth()) {
529 // TODO: highlight selected region
533 // ------------------------------------------------------------------------
534 // TEditorWidget ----------------------------------------------------------
535 // ------------------------------------------------------------------------
538 * Align visible area with document current line.
540 * @param topLineIsTop if true, make the top visible line the document
541 * current line if it was off-screen. If false, make the bottom visible
542 * line the document current line.
544 private void alignTopLine(final boolean topLineIsTop
) {
545 int line
= document
.getLineNumber();
547 if ((line
< topLine
) || (line
> topLine
+ getHeight() - 1)) {
548 // Need to move topLine to bring document back into view.
550 topLine
= line
- (getHeight() - 1);
554 assert (topLine
>= 0);
557 assert (topLine
>= 0);
562 System.err.println("line " + line + " topLine " + topLine);
565 // Document is in view, let's set cursorY
566 assert (line
>= topLine
);
567 setCursorY(line
- topLine
);
572 * Align document current line with visible area.
574 * @param topLineIsTop if true, make the top visible line the document
575 * current line if it was off-screen. If false, make the bottom visible
576 * line the document current line.
578 private void alignDocument(final boolean topLineIsTop
) {
579 int line
= document
.getLineNumber();
580 int cursor
= document
.getCursor();
582 if ((line
< topLine
) || (line
> topLine
+ getHeight() - 1)) {
583 // Need to move document to ensure it fits view.
585 document
.setLineNumber(topLine
);
587 document
.setLineNumber(topLine
+ (getHeight() - 1));
589 if (cursor
< document
.getCurrentLine().getDisplayLength()) {
590 document
.setCursor(cursor
);
595 System.err.println("getLineNumber() " + document.getLineNumber() +
596 " topLine " + topLine);
599 // Document is in view, let's set cursorY
600 setCursorY(document
.getLineNumber() - topLine
);
605 * Align visible cursor with document cursor.
607 private void alignCursor() {
608 int width
= getWidth();
610 int desiredX
= document
.getCursor() - leftColumn
;
612 // We need to push the screen to the left.
613 leftColumn
= document
.getCursor();
614 } else if (desiredX
> width
- 1) {
615 // We need to push the screen to the right.
616 leftColumn
= document
.getCursor() - (width
- 1);
620 System.err.println("document cursor " + document.getCursor() +
621 " leftColumn " + leftColumn);
625 setCursorX(document
.getCursor() - leftColumn
);
629 * Get the number of lines in the underlying Document.
631 * @return the number of lines
633 public int getLineCount() {
634 return document
.getLineCount();
638 * Get the current visible top row number. 1-based.
640 * @return the visible top row number. Row 1 is the first row.
642 public int getVisibleRowNumber() {
647 * Set the current visible row number. 1-based.
649 * @param row the new visible row number. Row 1 is the first row.
651 public void setVisibleRowNumber(final int row
) {
653 if ((row
> 0) && (row
< document
.getLineCount())) {
660 * Get the current editing row number. 1-based.
662 * @return the editing row number. Row 1 is the first row.
664 public int getEditingRowNumber() {
665 return document
.getLineNumber() + 1;
669 * Set the current editing row number. 1-based.
671 * @param row the new editing row number. Row 1 is the first row.
673 public void setEditingRowNumber(final int row
) {
675 if ((row
> 0) && (row
< document
.getLineCount())) {
676 document
.setLineNumber(row
- 1);
682 * Set the current visible column number. 1-based.
684 * @return the visible column number. Column 1 is the first column.
686 public int getVisibleColumnNumber() {
687 return leftColumn
+ 1;
691 * Set the current visible column number. 1-based.
693 * @param column the new visible column number. Column 1 is the first
696 public void setVisibleColumnNumber(final int column
) {
698 if ((column
> 0) && (column
< document
.getLineLengthMax())) {
699 leftColumn
= column
- 1;
705 * Get the current editing column number. 1-based.
707 * @return the editing column number. Column 1 is the first column.
709 public int getEditingColumnNumber() {
710 return document
.getCursor() + 1;
714 * Set the current editing column number. 1-based.
716 * @param column the new editing column number. Column 1 is the first
719 public void setEditingColumnNumber(final int column
) {
720 if ((column
> 0) && (column
< document
.getLineLength())) {
721 document
.setCursor(column
- 1);
727 * Get the maximum possible row number. 1-based.
729 * @return the maximum row number. Row 1 is the first row.
731 public int getMaximumRowNumber() {
732 return document
.getLineCount() + 1;
736 * Get the maximum possible column number. 1-based.
738 * @return the maximum column number. Column 1 is the first column.
740 public int getMaximumColumnNumber() {
741 return document
.getLineLengthMax() + 1;
745 * Get the dirty value.
747 * @return true if the buffer is dirty
749 public boolean isDirty() {
750 return document
.isDirty();
754 * Save contents to file.
756 * @param filename file to save to
757 * @throws IOException if a java.io operation throws
759 public void saveToFilename(final String filename
) throws IOException
{
760 document
.saveToFilename(filename
);
764 * Delete text within the selection bounds.
766 private void deleteSelection() {
767 if (inSelection
== false) {
774 // ------------------------------------------------------------------------
775 // EditMenuUser -----------------------------------------------------------
776 // ------------------------------------------------------------------------
779 * Check if the cut menu item should be enabled.
781 * @return true if the cut menu item should be enabled
783 public boolean isEditMenuCut() {
788 * Check if the copy menu item should be enabled.
790 * @return true if the copy menu item should be enabled
792 public boolean isEditMenuCopy() {
797 * Check if the paste menu item should be enabled.
799 * @return true if the paste menu item should be enabled
801 public boolean isEditMenuPaste() {
806 * Check if the clear menu item should be enabled.
808 * @return true if the clear menu item should be enabled
810 public boolean isEditMenuClear() {