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()) {
226 selectionColumn1
= leftColumn
+ mouse
.getX();
227 selectionLine1
= topLine
+ mouse
.getY();
230 selectionColumn0
= leftColumn
+ mouse
.getX();
231 selectionLine0
= topLine
+ mouse
.getY();
232 selectionColumn1
= selectionColumn0
;
233 selectionLine1
= selectionLine0
;
236 // Set the row and column
237 int newLine
= topLine
+ mouse
.getY();
238 int newX
= leftColumn
+ mouse
.getX();
239 if (newLine
> document
.getLineCount() - 1) {
241 document
.setLineNumber(document
.getLineCount() - 1);
243 if (newLine
> document
.getLineCount() - 1) {
244 setCursorY(document
.getLineCount() - 1 - topLine
);
246 setCursorY(mouse
.getY());
250 selectionColumn1
= document
.getCursor();
251 selectionLine1
= document
.getLineNumber();
256 document
.setLineNumber(newLine
);
257 setCursorY(mouse
.getY());
258 if (newX
>= document
.getCurrentLine().getDisplayLength()) {
262 document
.setCursor(newX
);
263 setCursorX(mouse
.getX());
266 selectionColumn1
= document
.getCursor();
267 selectionLine1
= document
.getLineNumber();
273 super.onMouseDown(mouse
);
279 * @param keypress keystroke event
282 public void onKeypress(final TKeypressEvent keypress
) {
283 if (keypress
.getKey().isShift()) {
287 selectionColumn0
= document
.getCursor();
288 selectionLine0
= document
.getLineNumber();
289 selectionColumn1
= selectionColumn0
;
290 selectionLine1
= selectionLine0
;
293 if (keypress
.equals(kbLeft
)
294 || keypress
.equals(kbRight
)
295 || keypress
.equals(kbUp
)
296 || keypress
.equals(kbDown
)
297 || keypress
.equals(kbPgDn
)
298 || keypress
.equals(kbPgUp
)
299 || keypress
.equals(kbHome
)
300 || keypress
.equals(kbEnd
)
302 // Non-shifted navigation keys disable selection.
307 if (keypress
.equals(kbLeft
)
308 || keypress
.equals(kbShiftLeft
)
312 } else if (keypress
.equals(kbRight
)
313 || keypress
.equals(kbShiftRight
)
317 } else if (keypress
.equals(kbAltLeft
)
318 || keypress
.equals(kbCtrlLeft
)
319 || keypress
.equals(kbAltShiftLeft
)
320 || keypress
.equals(kbCtrlShiftLeft
)
322 document
.backwardsWord();
324 } else if (keypress
.equals(kbAltRight
)
325 || keypress
.equals(kbCtrlRight
)
326 || keypress
.equals(kbAltShiftRight
)
327 || keypress
.equals(kbCtrlShiftRight
)
329 document
.forwardsWord();
331 } else if (keypress
.equals(kbUp
)
332 || keypress
.equals(kbShiftUp
)
336 } else if (keypress
.equals(kbDown
)
337 || keypress
.equals(kbShiftDown
)
341 } else if (keypress
.equals(kbPgUp
)
342 || keypress
.equals(kbShiftPgUp
)
344 document
.up(getHeight() - 1);
346 } else if (keypress
.equals(kbPgDn
)
347 || keypress
.equals(kbShiftPgDn
)
349 document
.down(getHeight() - 1);
351 } else if (keypress
.equals(kbHome
)
352 || keypress
.equals(kbShiftHome
)
354 if (document
.home()) {
356 if (leftColumn
< 0) {
361 } else if (keypress
.equals(kbEnd
)
362 || keypress
.equals(kbShiftEnd
)
364 if (document
.end()) {
367 } else if (keypress
.equals(kbCtrlHome
)
368 || keypress
.equals(kbCtrlShiftHome
)
370 document
.setLineNumber(0);
376 } else if (keypress
.equals(kbCtrlEnd
)
377 || keypress
.equals(kbCtrlShiftEnd
)
379 document
.setLineNumber(document
.getLineCount() - 1);
382 } else if (keypress
.equals(kbIns
)) {
383 document
.setOverwrite(!document
.getOverwrite());
384 } else if (keypress
.equals(kbDel
)) {
391 } else if (keypress
.equals(kbBackspace
)
392 || keypress
.equals(kbBackspaceDel
)
397 document
.backspace();
400 } else if (keypress
.equals(kbTab
)) {
402 // Add spaces until we hit modulo 8.
403 for (int i
= document
.getCursor(); (i
+ 1) % 8 != 0; i
++) {
404 document
.addChar(' ');
407 } else if (keypress
.equals(kbEnter
)) {
411 } else if (!keypress
.getKey().isFnKey()
412 && !keypress
.getKey().isAlt()
413 && !keypress
.getKey().isCtrl()
415 // Plain old keystroke, process it
417 document
.addChar(keypress
.getKey().getChar());
420 // Pass other keys (tab etc.) on to TWidget
421 super.onKeypress(keypress
);
425 selectionColumn1
= document
.getCursor();
426 selectionLine1
= document
.getLineNumber();
431 * Method that subclasses can override to handle window/screen resize
434 * @param resize resize event
437 public void onResize(final TResizeEvent resize
) {
438 // Change my width/height, and pull the cursor in as needed.
439 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
440 setWidth(resize
.getWidth());
441 setHeight(resize
.getHeight());
442 // See if the cursor is now outside the window, and if so move
444 if (getCursorX() >= getWidth()) {
445 leftColumn
+= getCursorX() - (getWidth() - 1);
446 setCursorX(getWidth() - 1);
448 if (getCursorY() >= getHeight()) {
449 topLine
+= getCursorY() - (getHeight() - 1);
450 setCursorY(getHeight() - 1);
453 // Let superclass handle it
454 super.onResize(resize
);
459 * Handle posted command events.
461 * @param command command event
464 public void onCommand(final TCommandEvent command
) {
465 if (command
.equals(cmCut
)) {
466 // Copy text to clipboard, and then remove it.
472 if (command
.equals(cmCopy
)) {
473 // Copy text to clipboard.
478 if (command
.equals(cmPaste
)) {
479 // Delete selected text, then paste text from clipboard.
482 String text
= getClipboard().pasteText();
484 for (int i
= 0; i
< text
.length(); ) {
485 int ch
= text
.codePointAt(i
);
486 onKeypress(new TKeypressEvent(false, 0, ch
, false, false,
488 i
+= Character
.charCount(ch
);
494 if (command
.equals(cmClear
)) {
502 // ------------------------------------------------------------------------
503 // TWidget ----------------------------------------------------------------
504 // ------------------------------------------------------------------------
511 CellAttributes selectedColor
= getTheme().getColor("teditor.selected");
513 int startCol
= selectionColumn0
;
514 int startRow
= selectionLine0
;
515 int endCol
= selectionColumn1
;
516 int endRow
= selectionLine1
;
518 if (((selectionColumn1
< selectionColumn0
)
519 && (selectionLine1
<= selectionLine0
))
520 || ((selectionColumn1
<= selectionColumn0
)
521 && (selectionLine1
< selectionLine0
))
523 // The user selected from bottom-right to top-left. Reverse the
524 // coordinates for the inverted section.
525 startCol
= selectionColumn1
;
526 startRow
= selectionLine1
;
527 endCol
= selectionColumn0
;
528 endRow
= selectionLine0
;
531 for (int i
= 0; i
< getHeight(); i
++) {
533 getScreen().hLineXY(0, i
, getWidth(), ' ', defaultColor
);
535 // Now draw document's line
536 if (topLine
+ i
< document
.getLineCount()) {
537 Line line
= document
.getLine(topLine
+ i
);
539 for (Word word
: line
.getWords()) {
540 // For now, we are cheating: draw outside the left region
541 // if needed and let screen do the clipping.
542 getScreen().putStringXY(x
- leftColumn
, i
, word
.getText(),
544 x
+= word
.getDisplayLength();
545 if (x
- leftColumn
> getWidth()) {
550 // Highlight selected region
552 if (startRow
== endRow
) {
553 if (topLine
+ i
== startRow
) {
554 for (x
= startCol
; x
<= endCol
; x
++) {
555 putAttrXY(x
- leftColumn
, i
, selectedColor
);
559 if (topLine
+ i
== startRow
) {
560 for (x
= startCol
; x
< line
.getDisplayLength(); x
++) {
561 putAttrXY(x
- leftColumn
, i
, selectedColor
);
563 } else if (topLine
+ i
== endRow
) {
564 for (x
= 0; x
<= endCol
; x
++) {
565 putAttrXY(x
- leftColumn
, i
, selectedColor
);
567 } else if ((topLine
+ i
>= startRow
)
568 && (topLine
+ i
<= endRow
)
570 for (x
= 0; x
< getWidth(); x
++) {
571 putAttrXY(x
, i
, selectedColor
);
581 // ------------------------------------------------------------------------
582 // TEditorWidget ----------------------------------------------------------
583 // ------------------------------------------------------------------------
586 * Align visible area with document current line.
588 * @param topLineIsTop if true, make the top visible line the document
589 * current line if it was off-screen. If false, make the bottom visible
590 * line the document current line.
592 private void alignTopLine(final boolean topLineIsTop
) {
593 int line
= document
.getLineNumber();
595 if ((line
< topLine
) || (line
> topLine
+ getHeight() - 1)) {
596 // Need to move topLine to bring document back into view.
598 topLine
= line
- (getHeight() - 1);
602 assert (topLine
>= 0);
605 assert (topLine
>= 0);
610 System.err.println("line " + line + " topLine " + topLine);
613 // Document is in view, let's set cursorY
614 assert (line
>= topLine
);
615 setCursorY(line
- topLine
);
620 * Align document current line with visible area.
622 * @param topLineIsTop if true, make the top visible line the document
623 * current line if it was off-screen. If false, make the bottom visible
624 * line the document current line.
626 private void alignDocument(final boolean topLineIsTop
) {
627 int line
= document
.getLineNumber();
628 int cursor
= document
.getCursor();
630 if ((line
< topLine
) || (line
> topLine
+ getHeight() - 1)) {
631 // Need to move document to ensure it fits view.
633 document
.setLineNumber(topLine
);
635 document
.setLineNumber(topLine
+ (getHeight() - 1));
637 if (cursor
< document
.getCurrentLine().getDisplayLength()) {
638 document
.setCursor(cursor
);
643 System.err.println("getLineNumber() " + document.getLineNumber() +
644 " topLine " + topLine);
647 // Document is in view, let's set cursorY
648 setCursorY(document
.getLineNumber() - topLine
);
653 * Align visible cursor with document cursor.
655 private void alignCursor() {
656 int width
= getWidth();
658 int desiredX
= document
.getCursor() - leftColumn
;
660 // We need to push the screen to the left.
661 leftColumn
= document
.getCursor();
662 } else if (desiredX
> width
- 1) {
663 // We need to push the screen to the right.
664 leftColumn
= document
.getCursor() - (width
- 1);
668 System.err.println("document cursor " + document.getCursor() +
669 " leftColumn " + leftColumn);
673 setCursorX(document
.getCursor() - leftColumn
);
677 * Get the number of lines in the underlying Document.
679 * @return the number of lines
681 public int getLineCount() {
682 return document
.getLineCount();
686 * Get the current visible top row number. 1-based.
688 * @return the visible top row number. Row 1 is the first row.
690 public int getVisibleRowNumber() {
695 * Set the current visible row number. 1-based.
697 * @param row the new visible row number. Row 1 is the first row.
699 public void setVisibleRowNumber(final int row
) {
701 if ((row
> 0) && (row
< document
.getLineCount())) {
708 * Get the current editing row number. 1-based.
710 * @return the editing row number. Row 1 is the first row.
712 public int getEditingRowNumber() {
713 return document
.getLineNumber() + 1;
717 * Set the current editing row number. 1-based.
719 * @param row the new editing row number. Row 1 is the first row.
721 public void setEditingRowNumber(final int row
) {
723 if ((row
> 0) && (row
< document
.getLineCount())) {
724 document
.setLineNumber(row
- 1);
730 * Set the current visible column number. 1-based.
732 * @return the visible column number. Column 1 is the first column.
734 public int getVisibleColumnNumber() {
735 return leftColumn
+ 1;
739 * Set the current visible column number. 1-based.
741 * @param column the new visible column number. Column 1 is the first
744 public void setVisibleColumnNumber(final int column
) {
746 if ((column
> 0) && (column
< document
.getLineLengthMax())) {
747 leftColumn
= column
- 1;
753 * Get the current editing column number. 1-based.
755 * @return the editing column number. Column 1 is the first column.
757 public int getEditingColumnNumber() {
758 return document
.getCursor() + 1;
762 * Set the current editing column number. 1-based.
764 * @param column the new editing column number. Column 1 is the first
767 public void setEditingColumnNumber(final int column
) {
768 if ((column
> 0) && (column
< document
.getLineLength())) {
769 document
.setCursor(column
- 1);
775 * Get the maximum possible row number. 1-based.
777 * @return the maximum row number. Row 1 is the first row.
779 public int getMaximumRowNumber() {
780 return document
.getLineCount() + 1;
784 * Get the maximum possible column number. 1-based.
786 * @return the maximum column number. Column 1 is the first column.
788 public int getMaximumColumnNumber() {
789 return document
.getLineLengthMax() + 1;
793 * Get the dirty value.
795 * @return true if the buffer is dirty
797 public boolean isDirty() {
798 return document
.isDirty();
802 * Save contents to file.
804 * @param filename file to save to
805 * @throws IOException if a java.io operation throws
807 public void saveToFilename(final String filename
) throws IOException
{
808 document
.saveToFilename(filename
);
812 * Delete text within the selection bounds.
814 private void deleteSelection() {
815 if (inSelection
== false) {
820 int startCol
= selectionColumn0
;
821 int startRow
= selectionLine0
;
822 int endCol
= selectionColumn1
;
823 int endRow
= selectionLine1
;
825 if (((selectionColumn1
< selectionColumn0
)
826 && (selectionLine1
<= selectionLine0
))
827 || ((selectionColumn1
<= selectionColumn0
)
828 && (selectionLine1
< selectionLine0
))
830 // The user selected from bottom-right to top-left. Reverse the
831 // coordinates for the inverted section.
832 startCol
= selectionColumn1
;
833 startRow
= selectionLine1
;
834 endCol
= selectionColumn0
;
835 endRow
= selectionLine0
;
838 // Place the cursor on the selection end, and "press backspace" until
839 // the cursor matches the selection start.
840 document
.setLineNumber(endRow
);
841 document
.setCursor(endCol
+ 1);
842 while (!((document
.getLineNumber() == startRow
)
843 && (document
.getCursor() == startCol
))
845 document
.backspace();
851 * Copy text within the selection bounds to clipboard.
853 private void copySelection() {
854 if (inSelection
== false) {
858 int startCol
= selectionColumn0
;
859 int startRow
= selectionLine0
;
860 int endCol
= selectionColumn1
;
861 int endRow
= selectionLine1
;
863 if (((selectionColumn1
< selectionColumn0
)
864 && (selectionLine1
<= selectionLine0
))
865 || ((selectionColumn1
<= selectionColumn0
)
866 && (selectionLine1
< selectionLine0
))
868 // The user selected from bottom-right to top-left. Reverse the
869 // coordinates for the inverted section.
870 startCol
= selectionColumn1
;
871 startRow
= selectionLine1
;
872 endCol
= selectionColumn0
;
873 endRow
= selectionLine0
;
876 StringBuilder sb
= new StringBuilder();
878 if (endRow
> startRow
) {
880 String line
= document
.getLine(startRow
).getRawString();
882 for (int i
= 0; i
< line
.length(); ) {
883 int ch
= line
.codePointAt(i
);
886 sb
.append(Character
.toChars(ch
));
888 x
+= StringUtils
.width(ch
);
889 i
+= Character
.charCount(ch
);
894 for (int y
= startRow
+ 1; y
< endRow
; y
++) {
895 sb
.append(document
.getLine(y
).getRawString());
900 line
= document
.getLine(endRow
).getRawString();
902 for (int i
= 0; i
< line
.length(); ) {
903 int ch
= line
.codePointAt(i
);
909 sb
.append(Character
.toChars(ch
));
910 x
+= StringUtils
.width(ch
);
911 i
+= Character
.charCount(ch
);
914 assert (startRow
== endRow
);
917 String line
= document
.getLine(startRow
).getRawString();
919 for (int i
= 0; i
< line
.length(); ) {
920 int ch
= line
.codePointAt(i
);
922 if ((x
>= startCol
) && (x
<= endCol
)) {
923 sb
.append(Character
.toChars(ch
));
926 x
+= StringUtils
.width(ch
);
927 i
+= Character
.charCount(ch
);
931 getClipboard().copyText(sb
.toString());
934 // ------------------------------------------------------------------------
935 // EditMenuUser -----------------------------------------------------------
936 // ------------------------------------------------------------------------
939 * Check if the cut menu item should be enabled.
941 * @return true if the cut menu item should be enabled
943 public boolean isEditMenuCut() {
948 * Check if the copy menu item should be enabled.
950 * @return true if the copy menu item should be enabled
952 public boolean isEditMenuCopy() {
957 * Check if the paste menu item should be enabled.
959 * @return true if the paste menu item should be enabled
961 public boolean isEditMenuPaste() {
966 * Check if the clear menu item should be enabled.
968 * @return true if the clear menu item should be enabled
970 public boolean isEditMenuClear() {