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
.bits
.StringUtils
;
35 import jexer
.event
.TCommandEvent
;
36 import jexer
.event
.TKeypressEvent
;
37 import jexer
.event
.TMouseEvent
;
38 import jexer
.event
.TResizeEvent
;
39 import jexer
.teditor
.Document
;
40 import jexer
.teditor
.Line
;
41 import jexer
.teditor
.Word
;
42 import static jexer
.TCommand
.*;
43 import static jexer
.TKeypress
.*;
46 * TEditorWidget displays an editable text document. It is unaware of
47 * scrolling behavior, but can respond to mouse and keyboard events.
49 public class TEditorWidget
extends TWidget
implements EditMenuUser
{
51 // ------------------------------------------------------------------------
52 // Constants --------------------------------------------------------------
53 // ------------------------------------------------------------------------
56 * The number of lines to scroll on mouse wheel up/down.
58 private static final int wheelScrollSize
= 3;
60 // ------------------------------------------------------------------------
61 // Variables --------------------------------------------------------------
62 // ------------------------------------------------------------------------
65 * The document being edited.
67 private Document document
;
70 * The default color for the TEditor class.
72 private CellAttributes defaultColor
= null;
75 * The topmost line number in the visible area. 0-based.
77 private int topLine
= 0;
80 * The leftmost column number in the visible area. 0-based.
82 private int leftColumn
= 0;
85 * If true, the mouse is dragging a selection.
87 private boolean inSelection
= false;
90 * Selection starting column.
92 private int selectionColumn0
;
95 * Selection starting line.
97 private int selectionLine0
;
100 * Selection ending column.
102 private int selectionColumn1
;
105 * Selection ending line.
107 private int selectionLine1
;
109 // ------------------------------------------------------------------------
110 // Constructors -----------------------------------------------------------
111 // ------------------------------------------------------------------------
114 * Public constructor.
116 * @param parent parent widget
117 * @param text text on the screen
118 * @param x column relative to parent
119 * @param y row relative to parent
120 * @param width width of text area
121 * @param height height of text area
123 public TEditorWidget(final TWidget parent
, final String text
, final int x
,
124 final int y
, final int width
, final int height
) {
126 // Set parent and window
127 super(parent
, x
, y
, width
, height
);
129 setCursorVisible(true);
131 defaultColor
= getTheme().getColor("teditor");
132 document
= new Document(text
, defaultColor
);
135 // ------------------------------------------------------------------------
136 // Event handlers ---------------------------------------------------------
137 // ------------------------------------------------------------------------
140 * Handle mouse press events.
142 * @param mouse mouse button press event
145 public void onMouseDown(final TMouseEvent mouse
) {
146 if (mouse
.isMouseWheelUp()) {
147 for (int i
= 0; i
< wheelScrollSize
; i
++) {
150 alignDocument(false);
155 if (mouse
.isMouseWheelDown()) {
156 for (int i
= 0; i
< wheelScrollSize
; i
++) {
157 if (topLine
< document
.getLineCount() - 1) {
165 if (mouse
.isMouse1()) {
168 selectionColumn0
= leftColumn
+ mouse
.getX();
169 selectionLine0
= topLine
+ mouse
.getY();
170 selectionColumn1
= selectionColumn0
;
171 selectionLine1
= selectionLine0
;
173 // Set the row and column
174 int newLine
= topLine
+ mouse
.getY();
175 int newX
= leftColumn
+ mouse
.getX();
176 if (newLine
> document
.getLineCount() - 1) {
178 document
.setLineNumber(document
.getLineCount() - 1);
180 if (newLine
> document
.getLineCount() - 1) {
181 setCursorY(document
.getLineCount() - 1 - topLine
);
183 setCursorY(mouse
.getY());
187 selectionColumn1
= document
.getCursor();
188 selectionLine1
= document
.getLineNumber();
193 document
.setLineNumber(newLine
);
194 setCursorY(mouse
.getY());
195 if (newX
>= document
.getCurrentLine().getDisplayLength()) {
199 document
.setCursor(newX
);
200 setCursorX(mouse
.getX());
203 selectionColumn1
= document
.getCursor();
204 selectionLine1
= document
.getLineNumber();
212 super.onMouseDown(mouse
);
216 * Handle mouse motion events.
218 * @param mouse mouse motion event
221 public void onMouseMotion(final TMouseEvent mouse
) {
223 if (mouse
.isMouse1()) {
224 // Set the row and column
225 int newLine
= topLine
+ mouse
.getY();
226 int newX
= leftColumn
+ mouse
.getX();
227 if ((newLine
< 0) || (newX
< 0)) {
233 selectionColumn1
= newX
;
234 selectionLine1
= newLine
;
237 selectionColumn0
= newX
;
238 selectionLine0
= newLine
;
239 selectionColumn1
= selectionColumn0
;
240 selectionLine1
= selectionLine0
;
243 if (newLine
> document
.getLineCount() - 1) {
245 document
.setLineNumber(document
.getLineCount() - 1);
247 if (newLine
> document
.getLineCount() - 1) {
248 setCursorY(document
.getLineCount() - 1 - topLine
);
250 setCursorY(mouse
.getY());
254 selectionColumn1
= document
.getCursor();
255 selectionLine1
= document
.getLineNumber();
259 document
.setLineNumber(newLine
);
260 setCursorY(mouse
.getY());
261 if (newX
>= document
.getCurrentLine().getDisplayLength()) {
265 document
.setCursor(newX
);
266 setCursorX(mouse
.getX());
269 selectionColumn1
= document
.getCursor();
270 selectionLine1
= document
.getLineNumber();
276 super.onMouseDown(mouse
);
282 * @param keypress keystroke event
285 public void onKeypress(final TKeypressEvent keypress
) {
286 if (keypress
.getKey().isShift()) {
290 selectionColumn0
= document
.getCursor();
291 selectionLine0
= document
.getLineNumber();
292 selectionColumn1
= selectionColumn0
;
293 selectionLine1
= selectionLine0
;
296 if (keypress
.equals(kbLeft
)
297 || keypress
.equals(kbRight
)
298 || keypress
.equals(kbUp
)
299 || keypress
.equals(kbDown
)
300 || keypress
.equals(kbPgDn
)
301 || keypress
.equals(kbPgUp
)
302 || keypress
.equals(kbHome
)
303 || keypress
.equals(kbEnd
)
305 // Non-shifted navigation keys disable selection.
310 if (keypress
.equals(kbLeft
)
311 || keypress
.equals(kbShiftLeft
)
315 } else if (keypress
.equals(kbRight
)
316 || keypress
.equals(kbShiftRight
)
320 } else if (keypress
.equals(kbAltLeft
)
321 || keypress
.equals(kbCtrlLeft
)
322 || keypress
.equals(kbAltShiftLeft
)
323 || keypress
.equals(kbCtrlShiftLeft
)
325 document
.backwardsWord();
327 } else if (keypress
.equals(kbAltRight
)
328 || keypress
.equals(kbCtrlRight
)
329 || keypress
.equals(kbAltShiftRight
)
330 || keypress
.equals(kbCtrlShiftRight
)
332 document
.forwardsWord();
334 } else if (keypress
.equals(kbUp
)
335 || keypress
.equals(kbShiftUp
)
339 } else if (keypress
.equals(kbDown
)
340 || keypress
.equals(kbShiftDown
)
344 } else if (keypress
.equals(kbPgUp
)
345 || keypress
.equals(kbShiftPgUp
)
347 document
.up(getHeight() - 1);
349 } else if (keypress
.equals(kbPgDn
)
350 || keypress
.equals(kbShiftPgDn
)
352 document
.down(getHeight() - 1);
354 } else if (keypress
.equals(kbHome
)
355 || keypress
.equals(kbShiftHome
)
357 if (document
.home()) {
359 if (leftColumn
< 0) {
364 } else if (keypress
.equals(kbEnd
)
365 || keypress
.equals(kbShiftEnd
)
367 if (document
.end()) {
370 } else if (keypress
.equals(kbCtrlHome
)
371 || keypress
.equals(kbCtrlShiftHome
)
373 document
.setLineNumber(0);
379 } else if (keypress
.equals(kbCtrlEnd
)
380 || keypress
.equals(kbCtrlShiftEnd
)
382 document
.setLineNumber(document
.getLineCount() - 1);
385 } else if (keypress
.equals(kbIns
)) {
386 document
.setOverwrite(!document
.getOverwrite());
387 } else if (keypress
.equals(kbDel
)) {
394 } else if (keypress
.equals(kbBackspace
)
395 || keypress
.equals(kbBackspaceDel
)
400 document
.backspace();
403 } else if (keypress
.equals(kbTab
)) {
405 // Add spaces until we hit modulo 8.
406 for (int i
= document
.getCursor(); (i
+ 1) % 8 != 0; i
++) {
407 document
.addChar(' ');
410 } else if (keypress
.equals(kbEnter
)) {
414 } else if (!keypress
.getKey().isFnKey()
415 && !keypress
.getKey().isAlt()
416 && !keypress
.getKey().isCtrl()
418 // Plain old keystroke, process it
420 document
.addChar(keypress
.getKey().getChar());
423 // Pass other keys (tab etc.) on to TWidget
424 super.onKeypress(keypress
);
428 selectionColumn1
= document
.getCursor();
429 selectionLine1
= document
.getLineNumber();
434 * Method that subclasses can override to handle window/screen resize
437 * @param resize resize event
440 public void onResize(final TResizeEvent resize
) {
441 // Change my width/height, and pull the cursor in as needed.
442 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
443 setWidth(resize
.getWidth());
444 setHeight(resize
.getHeight());
445 // See if the cursor is now outside the window, and if so move
447 if (getCursorX() >= getWidth()) {
448 leftColumn
+= getCursorX() - (getWidth() - 1);
449 setCursorX(getWidth() - 1);
451 if (getCursorY() >= getHeight()) {
452 topLine
+= getCursorY() - (getHeight() - 1);
453 setCursorY(getHeight() - 1);
456 // Let superclass handle it
457 super.onResize(resize
);
462 * Handle posted command events.
464 * @param command command event
467 public void onCommand(final TCommandEvent command
) {
468 if (command
.equals(cmCut
)) {
469 // Copy text to clipboard, and then remove it.
475 if (command
.equals(cmCopy
)) {
476 // Copy text to clipboard.
481 if (command
.equals(cmPaste
)) {
482 // Delete selected text, then paste text from clipboard.
485 String text
= getClipboard().pasteText();
487 for (int i
= 0; i
< text
.length(); ) {
488 int ch
= text
.codePointAt(i
);
489 onKeypress(new TKeypressEvent(false, 0, ch
, false, false,
491 i
+= Character
.charCount(ch
);
497 if (command
.equals(cmClear
)) {
505 // ------------------------------------------------------------------------
506 // TWidget ----------------------------------------------------------------
507 // ------------------------------------------------------------------------
514 CellAttributes selectedColor
= getTheme().getColor("teditor.selected");
516 int startCol
= selectionColumn0
;
517 int startRow
= selectionLine0
;
518 int endCol
= selectionColumn1
;
519 int endRow
= selectionLine1
;
521 if (((selectionColumn1
< selectionColumn0
)
522 && (selectionLine1
== selectionLine0
))
523 || (selectionLine1
< selectionLine0
)
525 // The user selected from bottom-to-top and/or right-to-left.
526 // Reverse the coordinates for the inverted section.
527 startCol
= selectionColumn1
;
528 startRow
= selectionLine1
;
529 endCol
= selectionColumn0
;
530 endRow
= selectionLine0
;
533 for (int i
= 0; i
< getHeight(); i
++) {
535 getScreen().hLineXY(0, i
, getWidth(), ' ', defaultColor
);
537 // Now draw document's line
538 if (topLine
+ i
< document
.getLineCount()) {
539 Line line
= document
.getLine(topLine
+ i
);
541 for (Word word
: line
.getWords()) {
542 // For now, we are cheating: draw outside the left region
543 // if needed and let screen do the clipping.
544 getScreen().putStringXY(x
- leftColumn
, i
, word
.getText(),
546 x
+= word
.getDisplayLength();
547 if (x
- leftColumn
> getWidth()) {
552 // Highlight selected region
554 if (startRow
== endRow
) {
555 if (topLine
+ i
== startRow
) {
556 for (x
= startCol
; x
<= endCol
; x
++) {
557 putAttrXY(x
- leftColumn
, i
, selectedColor
);
561 if (topLine
+ i
== startRow
) {
562 for (x
= startCol
; x
< line
.getDisplayLength(); x
++) {
563 putAttrXY(x
- leftColumn
, i
, selectedColor
);
565 } else if (topLine
+ i
== endRow
) {
566 for (x
= 0; x
<= endCol
; x
++) {
567 putAttrXY(x
- leftColumn
, i
, selectedColor
);
569 } else if ((topLine
+ i
>= startRow
)
570 && (topLine
+ i
<= endRow
)
572 for (x
= 0; x
< getWidth(); x
++) {
573 putAttrXY(x
, i
, selectedColor
);
583 // ------------------------------------------------------------------------
584 // TEditorWidget ----------------------------------------------------------
585 // ------------------------------------------------------------------------
588 * Align visible area with document current line.
590 * @param topLineIsTop if true, make the top visible line the document
591 * current line if it was off-screen. If false, make the bottom visible
592 * line the document current line.
594 private void alignTopLine(final boolean topLineIsTop
) {
595 int line
= document
.getLineNumber();
597 if ((line
< topLine
) || (line
> topLine
+ getHeight() - 1)) {
598 // Need to move topLine to bring document back into view.
600 topLine
= line
- (getHeight() - 1);
604 assert (topLine
>= 0);
607 assert (topLine
>= 0);
612 System.err.println("line " + line + " topLine " + topLine);
615 // Document is in view, let's set cursorY
616 assert (line
>= topLine
);
617 setCursorY(line
- topLine
);
622 * Align document current line with visible area.
624 * @param topLineIsTop if true, make the top visible line the document
625 * current line if it was off-screen. If false, make the bottom visible
626 * line the document current line.
628 private void alignDocument(final boolean topLineIsTop
) {
629 int line
= document
.getLineNumber();
630 int cursor
= document
.getCursor();
632 if ((line
< topLine
) || (line
> topLine
+ getHeight() - 1)) {
633 // Need to move document to ensure it fits view.
635 document
.setLineNumber(topLine
);
637 document
.setLineNumber(topLine
+ (getHeight() - 1));
639 if (cursor
< document
.getCurrentLine().getDisplayLength()) {
640 document
.setCursor(cursor
);
645 System.err.println("getLineNumber() " + document.getLineNumber() +
646 " topLine " + topLine);
649 // Document is in view, let's set cursorY
650 setCursorY(document
.getLineNumber() - topLine
);
655 * Align visible cursor with document cursor.
657 private void alignCursor() {
658 int width
= getWidth();
660 int desiredX
= document
.getCursor() - leftColumn
;
662 // We need to push the screen to the left.
663 leftColumn
= document
.getCursor();
664 } else if (desiredX
> width
- 1) {
665 // We need to push the screen to the right.
666 leftColumn
= document
.getCursor() - (width
- 1);
670 System.err.println("document cursor " + document.getCursor() +
671 " leftColumn " + leftColumn);
675 setCursorX(document
.getCursor() - leftColumn
);
679 * Get the number of lines in the underlying Document.
681 * @return the number of lines
683 public int getLineCount() {
684 return document
.getLineCount();
688 * Get the current visible top row number. 1-based.
690 * @return the visible top row number. Row 1 is the first row.
692 public int getVisibleRowNumber() {
697 * Set the current visible row number. 1-based.
699 * @param row the new visible row number. Row 1 is the first row.
701 public void setVisibleRowNumber(final int row
) {
703 if ((row
> 0) && (row
< document
.getLineCount())) {
710 * Get the current editing row number. 1-based.
712 * @return the editing row number. Row 1 is the first row.
714 public int getEditingRowNumber() {
715 return document
.getLineNumber() + 1;
719 * Set the current editing row number. 1-based.
721 * @param row the new editing row number. Row 1 is the first row.
723 public void setEditingRowNumber(final int row
) {
725 if ((row
> 0) && (row
< document
.getLineCount())) {
726 document
.setLineNumber(row
- 1);
732 * Set the current visible column number. 1-based.
734 * @return the visible column number. Column 1 is the first column.
736 public int getVisibleColumnNumber() {
737 return leftColumn
+ 1;
741 * Set the current visible column number. 1-based.
743 * @param column the new visible column number. Column 1 is the first
746 public void setVisibleColumnNumber(final int column
) {
748 if ((column
> 0) && (column
< document
.getLineLengthMax())) {
749 leftColumn
= column
- 1;
755 * Get the current editing column number. 1-based.
757 * @return the editing column number. Column 1 is the first column.
759 public int getEditingColumnNumber() {
760 return document
.getCursor() + 1;
764 * Set the current editing column number. 1-based.
766 * @param column the new editing column number. Column 1 is the first
769 public void setEditingColumnNumber(final int column
) {
770 if ((column
> 0) && (column
< document
.getLineLength())) {
771 document
.setCursor(column
- 1);
777 * Get the maximum possible row number. 1-based.
779 * @return the maximum row number. Row 1 is the first row.
781 public int getMaximumRowNumber() {
782 return document
.getLineCount() + 1;
786 * Get the maximum possible column number. 1-based.
788 * @return the maximum column number. Column 1 is the first column.
790 public int getMaximumColumnNumber() {
791 return document
.getLineLengthMax() + 1;
795 * Get the dirty value.
797 * @return true if the buffer is dirty
799 public boolean isDirty() {
800 return document
.isDirty();
804 * Save contents to file.
806 * @param filename file to save to
807 * @throws IOException if a java.io operation throws
809 public void saveToFilename(final String filename
) throws IOException
{
810 document
.saveToFilename(filename
);
814 * Delete text within the selection bounds.
816 private void deleteSelection() {
817 if (inSelection
== false) {
822 int startCol
= selectionColumn0
;
823 int startRow
= selectionLine0
;
824 int endCol
= selectionColumn1
;
825 int endRow
= selectionLine1
;
828 System.err.println("INITIAL: " + startRow + " " + startCol + " " +
829 endRow + " " + endCol + " " +
830 document.getLineNumber() + " " + document.getCursor());
833 if (((selectionColumn1
< selectionColumn0
)
834 && (selectionLine1
== selectionLine0
))
835 || (selectionLine1
< selectionLine0
)
837 // The user selected from bottom-to-top and/or right-to-left.
838 // Reverse the coordinates for the inverted section.
839 startCol
= selectionColumn1
;
840 startRow
= selectionLine1
;
841 endCol
= selectionColumn0
;
842 endRow
= selectionLine0
;
844 if (endRow
>= document
.getLineCount()) {
845 // The selection started beyond EOF, trim it to EOF.
846 endRow
= document
.getLineCount() - 1;
847 endCol
= document
.getLine(endRow
).getDisplayLength();
848 } else if (endRow
== document
.getLineCount() - 1) {
849 // The selection started beyond EOF, trim it to EOF.
850 if (endCol
>= document
.getLine(endRow
).getDisplayLength()) {
851 endCol
= document
.getLine(endRow
).getDisplayLength() - 1;
856 System.err.println("FLIP: " + startRow + " " + startCol + " " +
857 endRow + " " + endCol + " " +
858 document.getLineNumber() + " " + document.getCursor());
859 System.err.println(" --END: " + endRow + " " + document.getLineCount() +
860 " " + document.getLine(endRow).getDisplayLength());
863 assert (endRow
< document
.getLineCount());
864 if (endCol
>= document
.getLine(endRow
).getDisplayLength()) {
865 endCol
= document
.getLine(endRow
).getDisplayLength() - 1;
870 if (startCol
>= document
.getLine(startRow
).getDisplayLength()) {
871 startCol
= document
.getLine(startRow
).getDisplayLength() - 1;
877 // Place the cursor on the selection end, and "press backspace" until
878 // the cursor matches the selection start.
880 System.err.println("BEFORE: " + startRow + " " + startCol + " " +
881 endRow + " " + endCol + " " +
882 document.getLineNumber() + " " + document.getCursor());
884 document
.setLineNumber(endRow
);
885 document
.setCursor(endCol
+ 1);
886 while (!((document
.getLineNumber() == startRow
)
887 && (document
.getCursor() == startCol
))
890 System.err.println("DURING: " + startRow + " " + startCol + " " +
891 endRow + " " + endCol + " " +
892 document.getLineNumber() + " " + document.getCursor());
895 document
.backspace();
901 * Copy text within the selection bounds to clipboard.
903 private void copySelection() {
904 if (inSelection
== false) {
908 int startCol
= selectionColumn0
;
909 int startRow
= selectionLine0
;
910 int endCol
= selectionColumn1
;
911 int endRow
= selectionLine1
;
913 if (((selectionColumn1
< selectionColumn0
)
914 && (selectionLine1
== selectionLine0
))
915 || (selectionLine1
< selectionLine0
)
917 // The user selected from bottom-to-top and/or right-to-left.
918 // Reverse the coordinates for the inverted section.
919 startCol
= selectionColumn1
;
920 startRow
= selectionLine1
;
921 endCol
= selectionColumn0
;
922 endRow
= selectionLine0
;
925 StringBuilder sb
= new StringBuilder();
927 if (endRow
> startRow
) {
929 String line
= document
.getLine(startRow
).getRawString();
931 for (int i
= 0; i
< line
.length(); ) {
932 int ch
= line
.codePointAt(i
);
935 sb
.append(Character
.toChars(ch
));
937 x
+= StringUtils
.width(ch
);
938 i
+= Character
.charCount(ch
);
943 for (int y
= startRow
+ 1; y
< endRow
; y
++) {
944 sb
.append(document
.getLine(y
).getRawString());
949 line
= document
.getLine(endRow
).getRawString();
951 for (int i
= 0; i
< line
.length(); ) {
952 int ch
= line
.codePointAt(i
);
958 sb
.append(Character
.toChars(ch
));
959 x
+= StringUtils
.width(ch
);
960 i
+= Character
.charCount(ch
);
963 assert (startRow
== endRow
);
966 String line
= document
.getLine(startRow
).getRawString();
968 for (int i
= 0; i
< line
.length(); ) {
969 int ch
= line
.codePointAt(i
);
971 if ((x
>= startCol
) && (x
<= endCol
)) {
972 sb
.append(Character
.toChars(ch
));
975 x
+= StringUtils
.width(ch
);
976 i
+= Character
.charCount(ch
);
980 getClipboard().copyText(sb
.toString());
983 // ------------------------------------------------------------------------
984 // EditMenuUser -----------------------------------------------------------
985 // ------------------------------------------------------------------------
988 * Check if the cut menu item should be enabled.
990 * @return true if the cut menu item should be enabled
992 public boolean isEditMenuCut() {
997 * Check if the copy menu item should be enabled.
999 * @return true if the copy menu item should be enabled
1001 public boolean isEditMenuCopy() {
1006 * Check if the paste menu item should be enabled.
1008 * @return true if the paste menu item should be enabled
1010 public boolean isEditMenuPaste() {
1015 * Check if the clear menu item should be enabled.
1017 * @return true if the clear menu item should be enabled
1019 public boolean isEditMenuClear() {