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 selectionColumn1
= leftColumn
+ mouse
.getX();
169 selectionLine1
= topLine
+ mouse
.getY();
170 } else if (mouse
.isShift()) {
172 selectionColumn0
= leftColumn
+ mouse
.getX();
173 selectionLine0
= topLine
+ mouse
.getY();
174 selectionColumn1
= selectionColumn0
;
175 selectionLine1
= selectionLine0
;
178 // Set the row and column
179 int newLine
= topLine
+ mouse
.getY();
180 int newX
= leftColumn
+ mouse
.getX();
181 if (newLine
> document
.getLineCount() - 1) {
183 document
.setLineNumber(document
.getLineCount() - 1);
185 if (newLine
> document
.getLineCount() - 1) {
186 setCursorY(document
.getLineCount() - 1 - topLine
);
188 setCursorY(mouse
.getY());
192 selectionColumn1
= document
.getCursor();
193 selectionLine1
= document
.getLineNumber();
198 document
.setLineNumber(newLine
);
199 setCursorY(mouse
.getY());
200 if (newX
>= document
.getCurrentLine().getDisplayLength()) {
204 document
.setCursor(newX
);
205 setCursorX(mouse
.getX());
208 selectionColumn1
= document
.getCursor();
209 selectionLine1
= document
.getLineNumber();
217 super.onMouseDown(mouse
);
221 * Handle mouse motion events.
223 * @param mouse mouse motion event
226 public void onMouseMotion(final TMouseEvent mouse
) {
228 if (mouse
.isMouse1()) {
231 selectionColumn1
= leftColumn
+ mouse
.getX();
232 selectionLine1
= topLine
+ mouse
.getY();
233 } else if (mouse
.isShift()) {
235 selectionColumn0
= leftColumn
+ mouse
.getX();
236 selectionLine0
= topLine
+ mouse
.getY();
237 selectionColumn1
= selectionColumn0
;
238 selectionLine1
= selectionLine0
;
241 // Set the row and column
242 int newLine
= topLine
+ mouse
.getY();
243 int newX
= leftColumn
+ mouse
.getX();
244 if (newLine
> document
.getLineCount() - 1) {
246 document
.setLineNumber(document
.getLineCount() - 1);
248 if (newLine
> document
.getLineCount() - 1) {
249 setCursorY(document
.getLineCount() - 1 - topLine
);
251 setCursorY(mouse
.getY());
255 selectionColumn1
= document
.getCursor();
256 selectionLine1
= document
.getLineNumber();
261 document
.setLineNumber(newLine
);
262 setCursorY(mouse
.getY());
263 if (newX
>= document
.getCurrentLine().getDisplayLength()) {
267 document
.setCursor(newX
);
268 setCursorX(mouse
.getX());
271 selectionColumn1
= document
.getCursor();
272 selectionLine1
= document
.getLineNumber();
280 super.onMouseDown(mouse
);
284 * Handle mouse release events.
286 * @param mouse mouse button release event
289 public void onMouseUp(final TMouseEvent mouse
) {
293 super.onMouseDown(mouse
);
299 * @param keypress keystroke event
302 public void onKeypress(final TKeypressEvent keypress
) {
303 if (keypress
.getKey().isShift()) {
307 selectionColumn0
= document
.getCursor();
308 selectionLine0
= document
.getLineNumber();
309 selectionColumn1
= selectionColumn0
;
310 selectionLine1
= selectionLine0
;
313 if (keypress
.equals(kbLeft
)
314 || keypress
.equals(kbRight
)
315 || keypress
.equals(kbUp
)
316 || keypress
.equals(kbDown
)
317 || keypress
.equals(kbPgDn
)
318 || keypress
.equals(kbPgUp
)
319 || keypress
.equals(kbHome
)
320 || keypress
.equals(kbEnd
)
322 // Non-shifted navigation keys disable selection.
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
)
339 || keypress
.equals(kbAltShiftLeft
)
340 || keypress
.equals(kbCtrlShiftLeft
)
342 document
.backwardsWord();
344 } else if (keypress
.equals(kbAltRight
)
345 || keypress
.equals(kbCtrlRight
)
346 || keypress
.equals(kbAltShiftRight
)
347 || keypress
.equals(kbCtrlShiftRight
)
349 document
.forwardsWord();
351 } else if (keypress
.equals(kbUp
)
352 || keypress
.equals(kbShiftUp
)
356 } else if (keypress
.equals(kbDown
)
357 || keypress
.equals(kbShiftDown
)
361 } else if (keypress
.equals(kbPgUp
)
362 || keypress
.equals(kbShiftPgUp
)
364 document
.up(getHeight() - 1);
366 } else if (keypress
.equals(kbPgDn
)
367 || keypress
.equals(kbShiftPgDn
)
369 document
.down(getHeight() - 1);
371 } else if (keypress
.equals(kbHome
)
372 || keypress
.equals(kbShiftHome
)
374 if (document
.home()) {
376 if (leftColumn
< 0) {
381 } else if (keypress
.equals(kbEnd
)
382 || keypress
.equals(kbShiftEnd
)
384 if (document
.end()) {
387 } else if (keypress
.equals(kbCtrlHome
)
388 || keypress
.equals(kbCtrlShiftHome
)
390 document
.setLineNumber(0);
396 } else if (keypress
.equals(kbCtrlEnd
)
397 || keypress
.equals(kbCtrlShiftEnd
)
399 document
.setLineNumber(document
.getLineCount() - 1);
402 } else if (keypress
.equals(kbIns
)) {
403 document
.setOverwrite(!document
.getOverwrite());
404 } else if (keypress
.equals(kbDel
)) {
411 } else if (keypress
.equals(kbBackspace
)
412 || keypress
.equals(kbBackspaceDel
)
417 document
.backspace();
420 } else if (keypress
.equals(kbTab
)) {
422 // Add spaces until we hit modulo 8.
423 for (int i
= document
.getCursor(); (i
+ 1) % 8 != 0; i
++) {
424 document
.addChar(' ');
427 } else if (keypress
.equals(kbEnter
)) {
431 } else if (!keypress
.getKey().isFnKey()
432 && !keypress
.getKey().isAlt()
433 && !keypress
.getKey().isCtrl()
435 // Plain old keystroke, process it
437 document
.addChar(keypress
.getKey().getChar());
440 // Pass other keys (tab etc.) on to TWidget
441 super.onKeypress(keypress
);
445 selectionColumn1
= document
.getCursor();
446 selectionLine1
= document
.getLineNumber();
451 * Method that subclasses can override to handle window/screen resize
454 * @param resize resize event
457 public void onResize(final TResizeEvent resize
) {
458 // Change my width/height, and pull the cursor in as needed.
459 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
460 setWidth(resize
.getWidth());
461 setHeight(resize
.getHeight());
462 // See if the cursor is now outside the window, and if so move
464 if (getCursorX() >= getWidth()) {
465 leftColumn
+= getCursorX() - (getWidth() - 1);
466 setCursorX(getWidth() - 1);
468 if (getCursorY() >= getHeight()) {
469 topLine
+= getCursorY() - (getHeight() - 1);
470 setCursorY(getHeight() - 1);
473 // Let superclass handle it
474 super.onResize(resize
);
479 * Handle posted command events.
481 * @param command command event
484 public void onCommand(final TCommandEvent command
) {
485 if (command
.equals(cmCut
)) {
486 // Copy text to clipboard, and then remove it.
492 if (command
.equals(cmCopy
)) {
493 // Copy text to clipboard.
498 if (command
.equals(cmPaste
)) {
499 // Delete selected text, then paste text from clipboard.
502 String text
= getClipboard().pasteText();
504 for (int i
= 0; i
< text
.length(); ) {
505 int ch
= text
.codePointAt(i
);
506 onKeypress(new TKeypressEvent(false, 0, ch
, false, false,
508 i
+= Character
.charCount(ch
);
514 if (command
.equals(cmClear
)) {
522 // ------------------------------------------------------------------------
523 // TWidget ----------------------------------------------------------------
524 // ------------------------------------------------------------------------
531 CellAttributes selectedColor
= getTheme().getColor("teditor.selected");
533 int startCol
= selectionColumn0
;
534 int startRow
= selectionLine0
;
535 int endCol
= selectionColumn1
;
536 int endRow
= selectionLine1
;
538 if (((selectionColumn1
< selectionColumn0
)
539 && (selectionLine1
<= selectionLine0
))
540 || ((selectionColumn1
<= selectionColumn0
)
541 && (selectionLine1
< selectionLine0
))
543 // The user selected from bottom-right to top-left. Reverse the
544 // coordinates for the inverted section.
545 startCol
= selectionColumn1
;
546 startRow
= selectionLine1
;
547 endCol
= selectionColumn0
;
548 endRow
= selectionLine0
;
551 for (int i
= 0; i
< getHeight(); i
++) {
553 getScreen().hLineXY(0, i
, getWidth(), ' ', defaultColor
);
555 // Now draw document's line
556 if (topLine
+ i
< document
.getLineCount()) {
557 Line line
= document
.getLine(topLine
+ i
);
559 for (Word word
: line
.getWords()) {
560 // For now, we are cheating: draw outside the left region
561 // if needed and let screen do the clipping.
562 getScreen().putStringXY(x
- leftColumn
, i
, word
.getText(),
564 x
+= word
.getDisplayLength();
565 if (x
- leftColumn
> getWidth()) {
570 // Highlight selected region
572 if (startRow
== endRow
) {
573 if (topLine
+ i
== startRow
) {
574 for (x
= startCol
; x
<= endCol
; x
++) {
575 putAttrXY(x
- leftColumn
, i
, selectedColor
);
579 if (topLine
+ i
== startRow
) {
580 for (x
= startCol
; x
< line
.getDisplayLength(); x
++) {
581 putAttrXY(x
- leftColumn
, i
, selectedColor
);
583 } else if (topLine
+ i
== endRow
) {
584 for (x
= 0; x
<= endCol
; x
++) {
585 putAttrXY(x
- leftColumn
, i
, selectedColor
);
587 } else if ((topLine
+ i
>= startRow
)
588 && (topLine
+ i
<= endRow
)
590 for (x
= 0; x
< getWidth(); x
++) {
591 putAttrXY(x
, i
, selectedColor
);
601 // ------------------------------------------------------------------------
602 // TEditorWidget ----------------------------------------------------------
603 // ------------------------------------------------------------------------
606 * Align visible area with document current line.
608 * @param topLineIsTop if true, make the top visible line the document
609 * current line if it was off-screen. If false, make the bottom visible
610 * line the document current line.
612 private void alignTopLine(final boolean topLineIsTop
) {
613 int line
= document
.getLineNumber();
615 if ((line
< topLine
) || (line
> topLine
+ getHeight() - 1)) {
616 // Need to move topLine to bring document back into view.
618 topLine
= line
- (getHeight() - 1);
622 assert (topLine
>= 0);
625 assert (topLine
>= 0);
630 System.err.println("line " + line + " topLine " + topLine);
633 // Document is in view, let's set cursorY
634 assert (line
>= topLine
);
635 setCursorY(line
- topLine
);
640 * Align document current line with visible area.
642 * @param topLineIsTop if true, make the top visible line the document
643 * current line if it was off-screen. If false, make the bottom visible
644 * line the document current line.
646 private void alignDocument(final boolean topLineIsTop
) {
647 int line
= document
.getLineNumber();
648 int cursor
= document
.getCursor();
650 if ((line
< topLine
) || (line
> topLine
+ getHeight() - 1)) {
651 // Need to move document to ensure it fits view.
653 document
.setLineNumber(topLine
);
655 document
.setLineNumber(topLine
+ (getHeight() - 1));
657 if (cursor
< document
.getCurrentLine().getDisplayLength()) {
658 document
.setCursor(cursor
);
663 System.err.println("getLineNumber() " + document.getLineNumber() +
664 " topLine " + topLine);
667 // Document is in view, let's set cursorY
668 setCursorY(document
.getLineNumber() - topLine
);
673 * Align visible cursor with document cursor.
675 private void alignCursor() {
676 int width
= getWidth();
678 int desiredX
= document
.getCursor() - leftColumn
;
680 // We need to push the screen to the left.
681 leftColumn
= document
.getCursor();
682 } else if (desiredX
> width
- 1) {
683 // We need to push the screen to the right.
684 leftColumn
= document
.getCursor() - (width
- 1);
688 System.err.println("document cursor " + document.getCursor() +
689 " leftColumn " + leftColumn);
693 setCursorX(document
.getCursor() - leftColumn
);
697 * Get the number of lines in the underlying Document.
699 * @return the number of lines
701 public int getLineCount() {
702 return document
.getLineCount();
706 * Get the current visible top row number. 1-based.
708 * @return the visible top row number. Row 1 is the first row.
710 public int getVisibleRowNumber() {
715 * Set the current visible row number. 1-based.
717 * @param row the new visible row number. Row 1 is the first row.
719 public void setVisibleRowNumber(final int row
) {
721 if ((row
> 0) && (row
< document
.getLineCount())) {
728 * Get the current editing row number. 1-based.
730 * @return the editing row number. Row 1 is the first row.
732 public int getEditingRowNumber() {
733 return document
.getLineNumber() + 1;
737 * Set the current editing row number. 1-based.
739 * @param row the new editing row number. Row 1 is the first row.
741 public void setEditingRowNumber(final int row
) {
743 if ((row
> 0) && (row
< document
.getLineCount())) {
744 document
.setLineNumber(row
- 1);
750 * Set the current visible column number. 1-based.
752 * @return the visible column number. Column 1 is the first column.
754 public int getVisibleColumnNumber() {
755 return leftColumn
+ 1;
759 * Set the current visible column number. 1-based.
761 * @param column the new visible column number. Column 1 is the first
764 public void setVisibleColumnNumber(final int column
) {
766 if ((column
> 0) && (column
< document
.getLineLengthMax())) {
767 leftColumn
= column
- 1;
773 * Get the current editing column number. 1-based.
775 * @return the editing column number. Column 1 is the first column.
777 public int getEditingColumnNumber() {
778 return document
.getCursor() + 1;
782 * Set the current editing column number. 1-based.
784 * @param column the new editing column number. Column 1 is the first
787 public void setEditingColumnNumber(final int column
) {
788 if ((column
> 0) && (column
< document
.getLineLength())) {
789 document
.setCursor(column
- 1);
795 * Get the maximum possible row number. 1-based.
797 * @return the maximum row number. Row 1 is the first row.
799 public int getMaximumRowNumber() {
800 return document
.getLineCount() + 1;
804 * Get the maximum possible column number. 1-based.
806 * @return the maximum column number. Column 1 is the first column.
808 public int getMaximumColumnNumber() {
809 return document
.getLineLengthMax() + 1;
813 * Get the dirty value.
815 * @return true if the buffer is dirty
817 public boolean isDirty() {
818 return document
.isDirty();
822 * Save contents to file.
824 * @param filename file to save to
825 * @throws IOException if a java.io operation throws
827 public void saveToFilename(final String filename
) throws IOException
{
828 document
.saveToFilename(filename
);
832 * Delete text within the selection bounds.
834 private void deleteSelection() {
835 if (inSelection
== false) {
840 int startCol
= selectionColumn0
;
841 int startRow
= selectionLine0
;
842 int endCol
= selectionColumn1
;
843 int endRow
= selectionLine1
;
845 if (((selectionColumn1
< selectionColumn0
)
846 && (selectionLine1
<= selectionLine0
))
847 || ((selectionColumn1
<= selectionColumn0
)
848 && (selectionLine1
< selectionLine0
))
850 // The user selected from bottom-right to top-left. Reverse the
851 // coordinates for the inverted section.
852 startCol
= selectionColumn1
;
853 startRow
= selectionLine1
;
854 endCol
= selectionColumn0
;
855 endRow
= selectionLine0
;
858 // Place the cursor on the selection end, and "press backspace" until
859 // the cursor matches the selection start.
860 document
.setLineNumber(endRow
);
861 document
.setCursor(endCol
+ 1);
862 while (!((document
.getLineNumber() == startRow
)
863 && (document
.getCursor() == startCol
))
865 document
.backspace();
871 * Copy text within the selection bounds to clipboard.
873 private void copySelection() {
874 if (inSelection
== false) {
878 int startCol
= selectionColumn0
;
879 int startRow
= selectionLine0
;
880 int endCol
= selectionColumn1
;
881 int endRow
= selectionLine1
;
883 if (((selectionColumn1
< selectionColumn0
)
884 && (selectionLine1
<= selectionLine0
))
885 || ((selectionColumn1
<= selectionColumn0
)
886 && (selectionLine1
< selectionLine0
))
888 // The user selected from bottom-right to top-left. Reverse the
889 // coordinates for the inverted section.
890 startCol
= selectionColumn1
;
891 startRow
= selectionLine1
;
892 endCol
= selectionColumn0
;
893 endRow
= selectionLine0
;
896 StringBuilder sb
= new StringBuilder();
898 if (endRow
> startRow
) {
900 String line
= document
.getLine(startRow
).getRawString();
902 for (int i
= 0; i
< line
.length(); ) {
903 int ch
= line
.codePointAt(i
);
906 sb
.append(Character
.toChars(ch
));
908 x
+= StringUtils
.width(ch
);
909 i
+= Character
.charCount(ch
);
914 for (int y
= startRow
+ 1; y
< endRow
; y
++) {
915 sb
.append(document
.getLine(y
).getRawString());
920 line
= document
.getLine(endRow
).getRawString();
922 for (int i
= 0; i
< line
.length(); ) {
923 int ch
= line
.codePointAt(i
);
929 sb
.append(Character
.toChars(ch
));
930 x
+= StringUtils
.width(ch
);
931 i
+= Character
.charCount(ch
);
934 assert (startRow
== endRow
);
937 String line
= document
.getLine(startRow
).getRawString();
939 for (int i
= 0; i
< line
.length(); ) {
940 int ch
= line
.codePointAt(i
);
942 if ((x
>= startCol
) && (x
<= endCol
)) {
943 sb
.append(Character
.toChars(ch
));
946 x
+= StringUtils
.width(ch
);
947 i
+= Character
.charCount(ch
);
951 getClipboard().copyText(sb
.toString());
954 // ------------------------------------------------------------------------
955 // EditMenuUser -----------------------------------------------------------
956 // ------------------------------------------------------------------------
959 * Check if the cut menu item should be enabled.
961 * @return true if the cut menu item should be enabled
963 public boolean isEditMenuCut() {
968 * Check if the copy menu item should be enabled.
970 * @return true if the copy menu item should be enabled
972 public boolean isEditMenuCopy() {
977 * Check if the paste menu item should be enabled.
979 * @return true if the paste menu item should be enabled
981 public boolean isEditMenuPaste() {
986 * Check if the clear menu item should be enabled.
988 * @return true if the clear menu item should be enabled
990 public boolean isEditMenuClear() {