Merge commit '8b2627ce767579eb616e262b3f45f810a88ec200'
[fanfix.git] / src / jexer / TEditorWidget.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 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 */
29 package jexer;
30
31 import java.io.IOException;
32 import java.util.ArrayList;
33 import java.util.List;
34
35 import jexer.bits.CellAttributes;
36 import jexer.bits.StringUtils;
37 import jexer.event.TCommandEvent;
38 import jexer.event.TKeypressEvent;
39 import jexer.event.TMouseEvent;
40 import jexer.event.TResizeEvent;
41 import jexer.teditor.Document;
42 import jexer.teditor.Line;
43 import jexer.teditor.Word;
44 import static jexer.TCommand.*;
45 import static jexer.TKeypress.*;
46
47 /**
48 * TEditorWidget displays an editable text document. It is unaware of
49 * scrolling behavior, but can respond to mouse and keyboard events.
50 */
51 public class TEditorWidget extends TWidget implements EditMenuUser {
52
53 // ------------------------------------------------------------------------
54 // Constants --------------------------------------------------------------
55 // ------------------------------------------------------------------------
56
57 /**
58 * The number of lines to scroll on mouse wheel up/down.
59 */
60 private static final int wheelScrollSize = 3;
61
62 // ------------------------------------------------------------------------
63 // Variables --------------------------------------------------------------
64 // ------------------------------------------------------------------------
65
66 /**
67 * The document being edited.
68 */
69 protected Document document;
70
71 /**
72 * The default color for the editable text.
73 */
74 private CellAttributes defaultColor = null;
75
76 /**
77 * The topmost line number in the visible area. 0-based.
78 */
79 private int topLine = 0;
80
81 /**
82 * The leftmost column number in the visible area. 0-based.
83 */
84 private int leftColumn = 0;
85
86 /**
87 * If true, the mouse is dragging a selection.
88 */
89 private boolean inSelection = false;
90
91 /**
92 * Selection starting column.
93 */
94 private int selectionColumn0;
95
96 /**
97 * Selection starting line.
98 */
99 private int selectionLine0;
100
101 /**
102 * Selection ending column.
103 */
104 private int selectionColumn1;
105
106 /**
107 * Selection ending line.
108 */
109 private int selectionLine1;
110
111 /**
112 * The list of undo/redo states.
113 */
114 private List<SavedState> undoList = new ArrayList<SavedState>();
115
116 /**
117 * The position in undoList for undo/redo.
118 */
119 private int undoListI = 0;
120
121 /**
122 * The maximum size of the undo list.
123 */
124 private int undoLevel = 50;
125
126 /**
127 * The saved state for an undo/redo operation.
128 */
129 private class SavedState {
130 /**
131 * The Document state.
132 */
133 public Document document;
134
135 /**
136 * The topmost line number in the visible area. 0-based.
137 */
138 public int topLine = 0;
139
140 /**
141 * The leftmost column number in the visible area. 0-based.
142 */
143 public int leftColumn = 0;
144
145 }
146
147 // ------------------------------------------------------------------------
148 // Constructors -----------------------------------------------------------
149 // ------------------------------------------------------------------------
150
151 /**
152 * Public constructor.
153 *
154 * @param parent parent widget
155 * @param text text on the screen
156 * @param x column relative to parent
157 * @param y row relative to parent
158 * @param width width of text area
159 * @param height height of text area
160 */
161 public TEditorWidget(final TWidget parent, final String text, final int x,
162 final int y, final int width, final int height) {
163
164 // Set parent and window
165 super(parent, x, y, width, height);
166
167 setCursorVisible(true);
168
169 defaultColor = getTheme().getColor("teditor");
170 document = new Document(text, defaultColor);
171 }
172
173 // ------------------------------------------------------------------------
174 // Event handlers ---------------------------------------------------------
175 // ------------------------------------------------------------------------
176
177 /**
178 * Handle mouse press events.
179 *
180 * @param mouse mouse button press event
181 */
182 @Override
183 public void onMouseDown(final TMouseEvent mouse) {
184 if (mouse.isMouseWheelUp()) {
185 for (int i = 0; i < wheelScrollSize; i++) {
186 if (topLine > 0) {
187 topLine--;
188 alignDocument(false);
189 }
190 }
191 return;
192 }
193 if (mouse.isMouseWheelDown()) {
194 for (int i = 0; i < wheelScrollSize; i++) {
195 if (topLine < document.getLineCount() - 1) {
196 topLine++;
197 alignDocument(true);
198 }
199 }
200 return;
201 }
202
203 if (mouse.isMouse1()) {
204 // Selection.
205 int newLine = topLine + mouse.getY();
206 int newX = leftColumn + mouse.getX();
207
208 inSelection = true;
209 if (newLine > document.getLineCount() - 1) {
210 selectionLine0 = document.getLineCount() - 1;
211 } else {
212 selectionLine0 = topLine + mouse.getY();
213 }
214 selectionColumn0 = leftColumn + mouse.getX();
215 selectionColumn0 = Math.max(0, Math.min(selectionColumn0,
216 document.getLine(selectionLine0).getDisplayLength() - 1));
217 selectionColumn1 = selectionColumn0;
218 selectionLine1 = selectionLine0;
219
220 // Set the row and column
221 if (newLine > document.getLineCount() - 1) {
222 // Go to the end
223 document.setLineNumber(document.getLineCount() - 1);
224 document.end();
225 if (newLine > document.getLineCount() - 1) {
226 setCursorY(document.getLineCount() - 1 - topLine);
227 } else {
228 setCursorY(mouse.getY());
229 }
230 alignCursor();
231 if (inSelection) {
232 selectionColumn1 = document.getCursor();
233 selectionLine1 = document.getLineNumber();
234 }
235 return;
236 }
237
238 document.setLineNumber(newLine);
239 setCursorY(mouse.getY());
240 if (newX >= document.getCurrentLine().getDisplayLength()) {
241 document.end();
242 alignCursor();
243 } else {
244 document.setCursor(newX);
245 setCursorX(mouse.getX());
246 }
247 if (inSelection) {
248 selectionColumn1 = document.getCursor();
249 selectionLine1 = document.getLineNumber();
250 }
251 return;
252 } else {
253 inSelection = false;
254 }
255
256 // Pass to children
257 super.onMouseDown(mouse);
258 }
259
260 /**
261 * Handle mouse motion events.
262 *
263 * @param mouse mouse motion event
264 */
265 @Override
266 public void onMouseMotion(final TMouseEvent mouse) {
267
268 if (mouse.isMouse1()) {
269 // Set the row and column
270 int newLine = topLine + mouse.getY();
271 int newX = leftColumn + mouse.getX();
272 if ((newLine < 0) || (newX < 0)) {
273 return;
274 }
275
276 // Selection.
277 if (inSelection) {
278 selectionColumn1 = newX;
279 selectionLine1 = newLine;
280 } else {
281 inSelection = true;
282 selectionColumn0 = newX;
283 selectionLine0 = newLine;
284 selectionColumn1 = selectionColumn0;
285 selectionLine1 = selectionLine0;
286 }
287
288 if (newLine > document.getLineCount() - 1) {
289 // Go to the end
290 document.setLineNumber(document.getLineCount() - 1);
291 document.end();
292 if (newLine > document.getLineCount() - 1) {
293 setCursorY(document.getLineCount() - 1 - topLine);
294 } else {
295 setCursorY(mouse.getY());
296 }
297 alignCursor();
298 if (inSelection) {
299 selectionColumn1 = document.getCursor();
300 selectionLine1 = document.getLineNumber();
301 }
302 return;
303 }
304 document.setLineNumber(newLine);
305 setCursorY(mouse.getY());
306 if (newX >= document.getCurrentLine().getDisplayLength()) {
307 document.end();
308 alignCursor();
309 } else {
310 document.setCursor(newX);
311 setCursorX(mouse.getX());
312 }
313 if (inSelection) {
314 selectionColumn1 = document.getCursor();
315 selectionLine1 = document.getLineNumber();
316 }
317 return;
318 }
319
320 // Pass to children
321 super.onMouseDown(mouse);
322 }
323
324 /**
325 * Handle keystrokes.
326 *
327 * @param keypress keystroke event
328 */
329 @Override
330 public void onKeypress(final TKeypressEvent keypress) {
331 if (keypress.getKey().isShift()) {
332 if (keypress.equals(kbShiftLeft)
333 || keypress.equals(kbShiftRight)
334 || keypress.equals(kbShiftUp)
335 || keypress.equals(kbShiftDown)
336 || keypress.equals(kbShiftPgDn)
337 || keypress.equals(kbShiftPgUp)
338 || keypress.equals(kbShiftHome)
339 || keypress.equals(kbShiftEnd)
340 ) {
341 // Shifted navigation keys enable selection
342 if (!inSelection) {
343 inSelection = true;
344 selectionColumn0 = document.getCursor();
345 selectionLine0 = document.getLineNumber();
346 selectionColumn1 = selectionColumn0;
347 selectionLine1 = selectionLine0;
348 }
349 }
350 } else {
351 if (keypress.equals(kbLeft)
352 || keypress.equals(kbRight)
353 || keypress.equals(kbUp)
354 || keypress.equals(kbDown)
355 || keypress.equals(kbPgDn)
356 || keypress.equals(kbPgUp)
357 || keypress.equals(kbHome)
358 || keypress.equals(kbEnd)
359 ) {
360 // Non-shifted navigation keys disable selection.
361 inSelection = false;
362 }
363 if ((selectionColumn0 == selectionColumn1)
364 && (selectionLine0 == selectionLine1)
365 ) {
366 // The user clicked a spot and started typing.
367 inSelection = false;
368 }
369 }
370
371 if (keypress.equals(kbLeft)
372 || keypress.equals(kbShiftLeft)
373 ) {
374 document.left();
375 alignTopLine(false);
376 } else if (keypress.equals(kbRight)
377 || keypress.equals(kbShiftRight)
378 ) {
379 document.right();
380 alignTopLine(true);
381 } else if (keypress.equals(kbAltLeft)
382 || keypress.equals(kbCtrlLeft)
383 || keypress.equals(kbAltShiftLeft)
384 || keypress.equals(kbCtrlShiftLeft)
385 ) {
386 document.backwardsWord();
387 alignTopLine(false);
388 } else if (keypress.equals(kbAltRight)
389 || keypress.equals(kbCtrlRight)
390 || keypress.equals(kbAltShiftRight)
391 || keypress.equals(kbCtrlShiftRight)
392 ) {
393 document.forwardsWord();
394 alignTopLine(true);
395 } else if (keypress.equals(kbUp)
396 || keypress.equals(kbShiftUp)
397 ) {
398 document.up();
399 alignTopLine(false);
400 } else if (keypress.equals(kbDown)
401 || keypress.equals(kbShiftDown)
402 ) {
403 document.down();
404 alignTopLine(true);
405 } else if (keypress.equals(kbPgUp)
406 || keypress.equals(kbShiftPgUp)
407 ) {
408 document.up(getHeight() - 1);
409 alignTopLine(false);
410 } else if (keypress.equals(kbPgDn)
411 || keypress.equals(kbShiftPgDn)
412 ) {
413 document.down(getHeight() - 1);
414 alignTopLine(true);
415 } else if (keypress.equals(kbHome)
416 || keypress.equals(kbShiftHome)
417 ) {
418 if (document.home()) {
419 leftColumn = 0;
420 if (leftColumn < 0) {
421 leftColumn = 0;
422 }
423 setCursorX(0);
424 }
425 } else if (keypress.equals(kbEnd)
426 || keypress.equals(kbShiftEnd)
427 ) {
428 if (document.end()) {
429 alignCursor();
430 }
431 } else if (keypress.equals(kbCtrlHome)
432 || keypress.equals(kbCtrlShiftHome)
433 ) {
434 document.setLineNumber(0);
435 document.home();
436 topLine = 0;
437 leftColumn = 0;
438 setCursorX(0);
439 setCursorY(0);
440 } else if (keypress.equals(kbCtrlEnd)
441 || keypress.equals(kbCtrlShiftEnd)
442 ) {
443 document.setLineNumber(document.getLineCount() - 1);
444 document.end();
445 alignTopLine(false);
446 } else if (keypress.equals(kbIns)) {
447 document.setOverwrite(!document.isOverwrite());
448 } else if (keypress.equals(kbDel)) {
449 if (inSelection) {
450 deleteSelection();
451 alignCursor();
452 } else {
453 saveUndo();
454 document.del();
455 alignCursor();
456 }
457 } else if (keypress.equals(kbBackspace)
458 || keypress.equals(kbBackspaceDel)
459 ) {
460 if (inSelection) {
461 deleteSelection();
462 alignTopLine(false);
463 } else {
464 saveUndo();
465 document.backspace();
466 alignTopLine(false);
467 }
468 } else if (keypress.equals(kbTab)) {
469 deleteSelection();
470 saveUndo();
471 document.tab();
472 alignCursor();
473 } else if (keypress.equals(kbShiftTab)) {
474 deleteSelection();
475 saveUndo();
476 document.backTab();
477 alignCursor();
478 } else if (keypress.equals(kbEnter)) {
479 deleteSelection();
480 saveUndo();
481 document.enter();
482 alignTopLine(true);
483 } else if (!keypress.getKey().isFnKey()
484 && !keypress.getKey().isAlt()
485 && !keypress.getKey().isCtrl()
486 ) {
487 // Plain old keystroke, process it
488 deleteSelection();
489 saveUndo();
490 document.addChar(keypress.getKey().getChar());
491 alignCursor();
492 } else {
493 // Pass other keys (tab etc.) on to TWidget
494 super.onKeypress(keypress);
495 }
496
497 if (inSelection) {
498 selectionColumn1 = document.getCursor();
499 selectionLine1 = document.getLineNumber();
500 }
501 }
502
503 /**
504 * Method that subclasses can override to handle window/screen resize
505 * events.
506 *
507 * @param resize resize event
508 */
509 @Override
510 public void onResize(final TResizeEvent resize) {
511 // Change my width/height, and pull the cursor in as needed.
512 if (resize.getType() == TResizeEvent.Type.WIDGET) {
513 setWidth(resize.getWidth());
514 setHeight(resize.getHeight());
515 // See if the cursor is now outside the window, and if so move
516 // things.
517 if (getCursorX() >= getWidth()) {
518 leftColumn += getCursorX() - (getWidth() - 1);
519 setCursorX(getWidth() - 1);
520 }
521 if (getCursorY() >= getHeight()) {
522 topLine += getCursorY() - (getHeight() - 1);
523 setCursorY(getHeight() - 1);
524 }
525 } else {
526 // Let superclass handle it
527 super.onResize(resize);
528 }
529 }
530
531 /**
532 * Handle posted command events.
533 *
534 * @param command command event
535 */
536 @Override
537 public void onCommand(final TCommandEvent command) {
538 if (command.equals(cmCut)) {
539 // Copy text to clipboard, and then remove it.
540 copySelection();
541 deleteSelection();
542 return;
543 }
544
545 if (command.equals(cmCopy)) {
546 // Copy text to clipboard.
547 copySelection();
548 return;
549 }
550
551 if (command.equals(cmPaste)) {
552 // Delete selected text, then paste text from clipboard.
553 deleteSelection();
554
555 String text = getClipboard().pasteText();
556 if (text != null) {
557 for (int i = 0; i < text.length(); ) {
558 int ch = text.codePointAt(i);
559 switch (ch) {
560 case '\n':
561 onKeypress(new TKeypressEvent(kbEnter));
562 break;
563 case '\t':
564 onKeypress(new TKeypressEvent(kbTab));
565 break;
566 default:
567 if ((ch >= 0x20) && (ch != 0x7F)) {
568 onKeypress(new TKeypressEvent(false, 0, ch,
569 false, false, false));
570 }
571 break;
572 }
573
574 i += Character.charCount(ch);
575 }
576 }
577 return;
578 }
579
580 if (command.equals(cmClear)) {
581 // Remove text.
582 deleteSelection();
583 return;
584 }
585
586 }
587
588 // ------------------------------------------------------------------------
589 // TWidget ----------------------------------------------------------------
590 // ------------------------------------------------------------------------
591
592 /**
593 * Draw the text box.
594 */
595 @Override
596 public void draw() {
597 CellAttributes selectedColor = getTheme().getColor("teditor.selected");
598
599 boolean drawSelection = true;
600
601 int startCol = selectionColumn0;
602 int startRow = selectionLine0;
603 int endCol = selectionColumn1;
604 int endRow = selectionLine1;
605
606 if (((selectionColumn1 < selectionColumn0)
607 && (selectionLine1 == selectionLine0))
608 || (selectionLine1 < selectionLine0)
609 ) {
610 // The user selected from bottom-to-top and/or right-to-left.
611 // Reverse the coordinates for the inverted section.
612 startCol = selectionColumn1;
613 startRow = selectionLine1;
614 endCol = selectionColumn0;
615 endRow = selectionLine0;
616 }
617 if ((startCol == endCol) && (startRow == endRow)) {
618 drawSelection = false;
619 }
620
621 for (int i = 0; i < getHeight(); i++) {
622 // Background line
623 getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
624
625 // Now draw document's line
626 if (topLine + i < document.getLineCount()) {
627 Line line = document.getLine(topLine + i);
628 int x = 0;
629 for (Word word: line.getWords()) {
630 // For now, we are cheating: draw outside the left region
631 // if needed and let screen do the clipping.
632 getScreen().putStringXY(x - leftColumn, i, word.getText(),
633 word.getColor());
634 x += word.getDisplayLength();
635 if (x - leftColumn > getWidth()) {
636 break;
637 }
638 }
639
640 // Highlight selected region
641 if (inSelection && drawSelection) {
642 if (startRow == endRow) {
643 if (topLine + i == startRow) {
644 for (x = startCol; x <= endCol; x++) {
645 putAttrXY(x - leftColumn, i, selectedColor);
646 }
647 }
648 } else {
649 if (topLine + i == startRow) {
650 for (x = startCol; x < line.getDisplayLength(); x++) {
651 putAttrXY(x - leftColumn, i, selectedColor);
652 }
653 } else if (topLine + i == endRow) {
654 for (x = 0; x <= endCol; x++) {
655 putAttrXY(x - leftColumn, i, selectedColor);
656 }
657 } else if ((topLine + i >= startRow)
658 && (topLine + i <= endRow)
659 ) {
660 for (x = 0; x < getWidth(); x++) {
661 putAttrXY(x, i, selectedColor);
662 }
663 }
664 }
665 }
666
667 }
668 }
669 }
670
671 // ------------------------------------------------------------------------
672 // TEditorWidget ----------------------------------------------------------
673 // ------------------------------------------------------------------------
674
675 /**
676 * Set the undo level.
677 *
678 * @param undoLevel the maximum number of undo operations
679 */
680 public void setUndoLevel(final int undoLevel) {
681 this.undoLevel = undoLevel;
682 }
683
684 /**
685 * Align visible area with document current line.
686 *
687 * @param topLineIsTop if true, make the top visible line the document
688 * current line if it was off-screen. If false, make the bottom visible
689 * line the document current line.
690 */
691 private void alignTopLine(final boolean topLineIsTop) {
692 int line = document.getLineNumber();
693
694 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
695 // Need to move topLine to bring document back into view.
696 if (topLineIsTop) {
697 topLine = line - (getHeight() - 1);
698 if (topLine < 0) {
699 topLine = 0;
700 }
701 assert (topLine >= 0);
702 } else {
703 topLine = line;
704 assert (topLine >= 0);
705 }
706 }
707
708 /*
709 System.err.println("line " + line + " topLine " + topLine);
710 */
711
712 // Document is in view, let's set cursorY
713 assert (line >= topLine);
714 setCursorY(line - topLine);
715 alignCursor();
716 }
717
718 /**
719 * Align document current line with visible area.
720 *
721 * @param topLineIsTop if true, make the top visible line the document
722 * current line if it was off-screen. If false, make the bottom visible
723 * line the document current line.
724 */
725 private void alignDocument(final boolean topLineIsTop) {
726 int line = document.getLineNumber();
727 int cursor = document.getCursor();
728
729 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
730 // Need to move document to ensure it fits view.
731 if (topLineIsTop) {
732 document.setLineNumber(topLine);
733 } else {
734 document.setLineNumber(topLine + (getHeight() - 1));
735 }
736 if (cursor < document.getCurrentLine().getDisplayLength()) {
737 document.setCursor(cursor);
738 }
739 }
740
741 /*
742 System.err.println("getLineNumber() " + document.getLineNumber() +
743 " topLine " + topLine);
744 */
745
746 // Document is in view, let's set cursorY
747 setCursorY(document.getLineNumber() - topLine);
748 alignCursor();
749 }
750
751 /**
752 * Align visible cursor with document cursor.
753 */
754 private void alignCursor() {
755 int width = getWidth();
756
757 int desiredX = document.getCursor() - leftColumn;
758 if (desiredX < 0) {
759 // We need to push the screen to the left.
760 leftColumn = document.getCursor();
761 } else if (desiredX > width - 1) {
762 // We need to push the screen to the right.
763 leftColumn = document.getCursor() - (width - 1);
764 }
765
766 /*
767 System.err.println("document cursor " + document.getCursor() +
768 " leftColumn " + leftColumn);
769 */
770
771
772 setCursorX(document.getCursor() - leftColumn);
773 }
774
775 /**
776 * Get the number of lines in the underlying Document.
777 *
778 * @return the number of lines
779 */
780 public int getLineCount() {
781 return document.getLineCount();
782 }
783
784 /**
785 * Get the current visible top row number. 1-based.
786 *
787 * @return the visible top row number. Row 1 is the first row.
788 */
789 public int getVisibleRowNumber() {
790 return topLine + 1;
791 }
792
793 /**
794 * Set the current visible row number. 1-based.
795 *
796 * @param row the new visible row number. Row 1 is the first row.
797 */
798 public void setVisibleRowNumber(final int row) {
799 assert (row > 0);
800 if ((row > 0) && (row < document.getLineCount())) {
801 topLine = row - 1;
802 alignDocument(true);
803 }
804 }
805
806 /**
807 * Get the current editing row number. 1-based.
808 *
809 * @return the editing row number. Row 1 is the first row.
810 */
811 public int getEditingRowNumber() {
812 return document.getLineNumber() + 1;
813 }
814
815 /**
816 * Set the current editing row number. 1-based.
817 *
818 * @param row the new editing row number. Row 1 is the first row.
819 */
820 public void setEditingRowNumber(final int row) {
821 assert (row > 0);
822 if ((row > 0) && (row < document.getLineCount())) {
823 document.setLineNumber(row - 1);
824 alignTopLine(true);
825 }
826 }
827
828 /**
829 * Set the current visible column number. 1-based.
830 *
831 * @return the visible column number. Column 1 is the first column.
832 */
833 public int getVisibleColumnNumber() {
834 return leftColumn + 1;
835 }
836
837 /**
838 * Set the current visible column number. 1-based.
839 *
840 * @param column the new visible column number. Column 1 is the first
841 * column.
842 */
843 public void setVisibleColumnNumber(final int column) {
844 assert (column > 0);
845 if ((column > 0) && (column < document.getLineLengthMax())) {
846 leftColumn = column - 1;
847 alignDocument(true);
848 }
849 }
850
851 /**
852 * Get the current editing column number. 1-based.
853 *
854 * @return the editing column number. Column 1 is the first column.
855 */
856 public int getEditingColumnNumber() {
857 return document.getCursor() + 1;
858 }
859
860 /**
861 * Set the current editing column number. 1-based.
862 *
863 * @param column the new editing column number. Column 1 is the first
864 * column.
865 */
866 public void setEditingColumnNumber(final int column) {
867 if ((column > 0) && (column < document.getLineLength())) {
868 document.setCursor(column - 1);
869 alignCursor();
870 }
871 }
872
873 /**
874 * Get the maximum possible row number. 1-based.
875 *
876 * @return the maximum row number. Row 1 is the first row.
877 */
878 public int getMaximumRowNumber() {
879 return document.getLineCount() + 1;
880 }
881
882 /**
883 * Get the maximum possible column number. 1-based.
884 *
885 * @return the maximum column number. Column 1 is the first column.
886 */
887 public int getMaximumColumnNumber() {
888 return document.getLineLengthMax() + 1;
889 }
890
891 /**
892 * Get the current editing row plain text. 1-based.
893 *
894 * @param row the editing row number. Row 1 is the first row.
895 * @return the plain text of the row
896 */
897 public String getEditingRawLine(final int row) {
898 Line line = document.getLine(row - 1);
899 return line.getRawString();
900 }
901
902 /**
903 * Get the dirty value.
904 *
905 * @return true if the buffer is dirty
906 */
907 public boolean isDirty() {
908 return document.isDirty();
909 }
910
911 /**
912 * Unset the dirty flag.
913 */
914 public void setNotDirty() {
915 document.setNotDirty();
916 }
917
918 /**
919 * Get the overwrite value.
920 *
921 * @return true if new text will overwrite old text
922 */
923 public boolean isOverwrite() {
924 return document.isOverwrite();
925 }
926
927 /**
928 * Save contents to file.
929 *
930 * @param filename file to save to
931 * @throws IOException if a java.io operation throws
932 */
933 public void saveToFilename(final String filename) throws IOException {
934 document.saveToFilename(filename);
935 }
936
937 /**
938 * Delete text within the selection bounds.
939 */
940 private void deleteSelection() {
941 if (!inSelection) {
942 return;
943 }
944
945 saveUndo();
946
947 inSelection = false;
948
949 int startCol = selectionColumn0;
950 int startRow = selectionLine0;
951 int endCol = selectionColumn1;
952 int endRow = selectionLine1;
953
954 /*
955 System.err.println("INITIAL: " + startRow + " " + startCol + " " +
956 endRow + " " + endCol + " " +
957 document.getLineNumber() + " " + document.getCursor());
958 */
959
960 if (((selectionColumn1 < selectionColumn0)
961 && (selectionLine1 == selectionLine0))
962 || (selectionLine1 < selectionLine0)
963 ) {
964 // The user selected from bottom-to-top and/or right-to-left.
965 // Reverse the coordinates for the inverted section.
966 startCol = selectionColumn1;
967 startRow = selectionLine1;
968 endCol = selectionColumn0;
969 endRow = selectionLine0;
970
971 if (endRow >= document.getLineCount()) {
972 // The selection started beyond EOF, trim it to EOF.
973 endRow = document.getLineCount() - 1;
974 endCol = document.getLine(endRow).getDisplayLength();
975 } else if (endRow == document.getLineCount() - 1) {
976 // The selection started beyond EOF, trim it to EOF.
977 if (endCol >= document.getLine(endRow).getDisplayLength()) {
978 endCol = document.getLine(endRow).getDisplayLength() - 1;
979 }
980 }
981 }
982 /*
983 System.err.println("FLIP: " + startRow + " " + startCol + " " +
984 endRow + " " + endCol + " " +
985 document.getLineNumber() + " " + document.getCursor());
986 System.err.println(" --END: " + endRow + " " + document.getLineCount() +
987 " " + document.getLine(endRow).getDisplayLength());
988 */
989
990 assert (endRow < document.getLineCount());
991 if (endCol >= document.getLine(endRow).getDisplayLength()) {
992 endCol = document.getLine(endRow).getDisplayLength() - 1;
993 }
994 if (endCol < 0) {
995 endCol = 0;
996 }
997 if (startCol >= document.getLine(startRow).getDisplayLength()) {
998 startCol = document.getLine(startRow).getDisplayLength() - 1;
999 }
1000 if (startCol < 0) {
1001 startCol = 0;
1002 }
1003
1004 // Place the cursor on the selection end, and "press backspace" until
1005 // the cursor matches the selection start.
1006 /*
1007 System.err.println("BEFORE: " + startRow + " " + startCol + " " +
1008 endRow + " " + endCol + " " +
1009 document.getLineNumber() + " " + document.getCursor());
1010 */
1011 document.setLineNumber(endRow);
1012 document.setCursor(endCol + 1);
1013 while (!((document.getLineNumber() == startRow)
1014 && (document.getCursor() == startCol))
1015 ) {
1016 /*
1017 System.err.println("DURING: " + startRow + " " + startCol + " " +
1018 endRow + " " + endCol + " " +
1019 document.getLineNumber() + " " + document.getCursor());
1020 */
1021
1022 document.backspace();
1023 }
1024 alignTopLine(true);
1025 }
1026
1027 /**
1028 * Copy text within the selection bounds to clipboard.
1029 */
1030 private void copySelection() {
1031 if (!inSelection) {
1032 return;
1033 }
1034 getClipboard().copyText(getSelection());
1035 }
1036
1037 /**
1038 * Set the selection.
1039 *
1040 * @param startRow the starting row number. 0-based: row 0 is the first
1041 * row.
1042 * @param startColumn the starting column number. 0-based: column 0 is
1043 * the first column.
1044 * @param endRow the ending row number. 0-based: row 0 is the first row.
1045 * @param endColumn the ending column number. 0-based: column 0 is the
1046 * first column.
1047 */
1048 public void setSelection(final int startRow, final int startColumn,
1049 final int endRow, final int endColumn) {
1050
1051 inSelection = true;
1052 selectionLine0 = startRow;
1053 selectionColumn0 = startColumn;
1054 selectionLine1 = endRow;
1055 selectionColumn1 = endColumn;
1056 }
1057
1058 /**
1059 * Copy text within the selection bounds to a string.
1060 *
1061 * @return the selection as a string, or null if there is no selection
1062 */
1063 public String getSelection() {
1064 if (!inSelection) {
1065 return null;
1066 }
1067
1068 int startCol = selectionColumn0;
1069 int startRow = selectionLine0;
1070 int endCol = selectionColumn1;
1071 int endRow = selectionLine1;
1072
1073 if (((selectionColumn1 < selectionColumn0)
1074 && (selectionLine1 == selectionLine0))
1075 || (selectionLine1 < selectionLine0)
1076 ) {
1077 // The user selected from bottom-to-top and/or right-to-left.
1078 // Reverse the coordinates for the inverted section.
1079 startCol = selectionColumn1;
1080 startRow = selectionLine1;
1081 endCol = selectionColumn0;
1082 endRow = selectionLine0;
1083 }
1084
1085 StringBuilder sb = new StringBuilder();
1086
1087 if (endRow > startRow) {
1088 // First line
1089 String line = document.getLine(startRow).getRawString();
1090 int x = 0;
1091 for (int i = 0; i < line.length(); ) {
1092 int ch = line.codePointAt(i);
1093
1094 if (x >= startCol) {
1095 sb.append(Character.toChars(ch));
1096 }
1097 x += StringUtils.width(ch);
1098 i += Character.charCount(ch);
1099 }
1100 sb.append("\n");
1101
1102 // Middle lines
1103 for (int y = startRow + 1; y < endRow; y++) {
1104 sb.append(document.getLine(y).getRawString());
1105 sb.append("\n");
1106 }
1107
1108 // Final line
1109 line = document.getLine(endRow).getRawString();
1110 x = 0;
1111 for (int i = 0; i < line.length(); ) {
1112 int ch = line.codePointAt(i);
1113
1114 if (x > endCol) {
1115 break;
1116 }
1117
1118 sb.append(Character.toChars(ch));
1119 x += StringUtils.width(ch);
1120 i += Character.charCount(ch);
1121 }
1122 } else {
1123 assert (startRow == endRow);
1124
1125 // Only one line
1126 String line = document.getLine(startRow).getRawString();
1127 int x = 0;
1128 for (int i = 0; i < line.length(); ) {
1129 int ch = line.codePointAt(i);
1130
1131 if ((x >= startCol) && (x <= endCol)) {
1132 sb.append(Character.toChars(ch));
1133 }
1134
1135 x += StringUtils.width(ch);
1136 i += Character.charCount(ch);
1137 }
1138 }
1139 return sb.toString();
1140 }
1141
1142 /**
1143 * Get the selection starting row number.
1144 *
1145 * @return the starting row number, or -1 if there is no selection.
1146 * 0-based: row 0 is the first row.
1147 */
1148 public int getSelectionStartRow() {
1149 if (!inSelection) {
1150 return -1;
1151 }
1152
1153 int startCol = selectionColumn0;
1154 int startRow = selectionLine0;
1155 int endCol = selectionColumn1;
1156 int endRow = selectionLine1;
1157
1158 if (((selectionColumn1 < selectionColumn0)
1159 && (selectionLine1 == selectionLine0))
1160 || (selectionLine1 < selectionLine0)
1161 ) {
1162 // The user selected from bottom-to-top and/or right-to-left.
1163 // Reverse the coordinates for the inverted section.
1164 startCol = selectionColumn1;
1165 startRow = selectionLine1;
1166 endCol = selectionColumn0;
1167 endRow = selectionLine0;
1168 }
1169 return startRow;
1170 }
1171
1172 /**
1173 * Get the selection starting column number.
1174 *
1175 * @return the starting column number, or -1 if there is no selection.
1176 * 0-based: column 0 is the first column.
1177 */
1178 public int getSelectionStartColumn() {
1179 if (!inSelection) {
1180 return -1;
1181 }
1182
1183 int startCol = selectionColumn0;
1184 int startRow = selectionLine0;
1185 int endCol = selectionColumn1;
1186 int endRow = selectionLine1;
1187
1188 if (((selectionColumn1 < selectionColumn0)
1189 && (selectionLine1 == selectionLine0))
1190 || (selectionLine1 < selectionLine0)
1191 ) {
1192 // The user selected from bottom-to-top and/or right-to-left.
1193 // Reverse the coordinates for the inverted section.
1194 startCol = selectionColumn1;
1195 startRow = selectionLine1;
1196 endCol = selectionColumn0;
1197 endRow = selectionLine0;
1198 }
1199 return startCol;
1200 }
1201
1202 /**
1203 * Get the selection ending row number.
1204 *
1205 * @return the ending row number, or -1 if there is no selection.
1206 * 0-based: row 0 is the first row.
1207 */
1208 public int getSelectionEndRow() {
1209 if (!inSelection) {
1210 return -1;
1211 }
1212
1213 int startCol = selectionColumn0;
1214 int startRow = selectionLine0;
1215 int endCol = selectionColumn1;
1216 int endRow = selectionLine1;
1217
1218 if (((selectionColumn1 < selectionColumn0)
1219 && (selectionLine1 == selectionLine0))
1220 || (selectionLine1 < selectionLine0)
1221 ) {
1222 // The user selected from bottom-to-top and/or right-to-left.
1223 // Reverse the coordinates for the inverted section.
1224 startCol = selectionColumn1;
1225 startRow = selectionLine1;
1226 endCol = selectionColumn0;
1227 endRow = selectionLine0;
1228 }
1229 return endRow;
1230 }
1231
1232 /**
1233 * Get the selection ending column number.
1234 *
1235 * @return the ending column number, or -1 if there is no selection.
1236 * 0-based: column 0 is the first column.
1237 */
1238 public int getSelectionEndColumn() {
1239 if (!inSelection) {
1240 return -1;
1241 }
1242
1243 int startCol = selectionColumn0;
1244 int startRow = selectionLine0;
1245 int endCol = selectionColumn1;
1246 int endRow = selectionLine1;
1247
1248 if (((selectionColumn1 < selectionColumn0)
1249 && (selectionLine1 == selectionLine0))
1250 || (selectionLine1 < selectionLine0)
1251 ) {
1252 // The user selected from bottom-to-top and/or right-to-left.
1253 // Reverse the coordinates for the inverted section.
1254 startCol = selectionColumn1;
1255 startRow = selectionLine1;
1256 endCol = selectionColumn0;
1257 endRow = selectionLine0;
1258 }
1259 return endCol;
1260 }
1261
1262 /**
1263 * Unset the selection.
1264 */
1265 public void unsetSelection() {
1266 inSelection = false;
1267 }
1268
1269 /**
1270 * Replace whatever is being selected with new text. If not in
1271 * selection, nothing is replaced.
1272 *
1273 * @param text the new replacement text
1274 */
1275 public void replaceSelection(final String text) {
1276 if (!inSelection) {
1277 return;
1278 }
1279
1280 // Delete selected text, then paste text from clipboard.
1281 deleteSelection();
1282
1283 for (int i = 0; i < text.length(); ) {
1284 int ch = text.codePointAt(i);
1285 switch (ch) {
1286 case '\n':
1287 onKeypress(new TKeypressEvent(kbEnter));
1288 break;
1289 case '\t':
1290 onKeypress(new TKeypressEvent(kbTab));
1291 break;
1292 default:
1293 if ((ch >= 0x20) && (ch != 0x7F)) {
1294 onKeypress(new TKeypressEvent(false, 0, ch,
1295 false, false, false));
1296 }
1297 break;
1298 }
1299 i += Character.charCount(ch);
1300 }
1301 }
1302
1303 /**
1304 * Check if selection is available.
1305 *
1306 * @return true if a selection has been made
1307 */
1308 public boolean hasSelection() {
1309 return inSelection;
1310 }
1311
1312 /**
1313 * Get the entire contents of the editor as one string.
1314 *
1315 * @return the editor contents
1316 */
1317 public String getText() {
1318 return document.getText();
1319 }
1320
1321 /**
1322 * Set the entire contents of the editor from one string.
1323 *
1324 * @param text the new contents
1325 */
1326 public void setText(final String text) {
1327 document = new Document(text, defaultColor);
1328 unsetSelection();
1329 topLine = 0;
1330 leftColumn = 0;
1331 }
1332
1333 // ------------------------------------------------------------------------
1334 // EditMenuUser -----------------------------------------------------------
1335 // ------------------------------------------------------------------------
1336
1337 /**
1338 * Check if the cut menu item should be enabled.
1339 *
1340 * @return true if the cut menu item should be enabled
1341 */
1342 public boolean isEditMenuCut() {
1343 return true;
1344 }
1345
1346 /**
1347 * Check if the copy menu item should be enabled.
1348 *
1349 * @return true if the copy menu item should be enabled
1350 */
1351 public boolean isEditMenuCopy() {
1352 return true;
1353 }
1354
1355 /**
1356 * Check if the paste menu item should be enabled.
1357 *
1358 * @return true if the paste menu item should be enabled
1359 */
1360 public boolean isEditMenuPaste() {
1361 return true;
1362 }
1363
1364 /**
1365 * Check if the clear menu item should be enabled.
1366 *
1367 * @return true if the clear menu item should be enabled
1368 */
1369 public boolean isEditMenuClear() {
1370 return true;
1371 }
1372
1373 /**
1374 * Save undo state.
1375 */
1376 private void saveUndo() {
1377 SavedState state = new SavedState();
1378 state.document = document.dup();
1379 state.topLine = topLine;
1380 state.leftColumn = leftColumn;
1381 if (undoLevel > 0) {
1382 while (undoList.size() > undoLevel) {
1383 undoList.remove(0);
1384 }
1385 }
1386 undoList.add(state);
1387 undoListI = undoList.size() - 1;
1388 }
1389
1390 /**
1391 * Undo an edit.
1392 */
1393 public void undo() {
1394 inSelection = false;
1395 if ((undoListI >= 0) && (undoListI < undoList.size())) {
1396 SavedState state = undoList.get(undoListI);
1397 document = state.document.dup();
1398 topLine = state.topLine;
1399 leftColumn = state.leftColumn;
1400 undoListI--;
1401 setCursorY(document.getLineNumber() - topLine);
1402 alignCursor();
1403 }
1404 }
1405
1406 /**
1407 * Redo an edit.
1408 */
1409 public void redo() {
1410 inSelection = false;
1411 if ((undoListI >= 0) && (undoListI < undoList.size())) {
1412 SavedState state = undoList.get(undoListI);
1413 document = state.document.dup();
1414 topLine = state.topLine;
1415 leftColumn = state.leftColumn;
1416 undoListI++;
1417 setCursorY(document.getLineNumber() - topLine);
1418 alignCursor();
1419 }
1420 }
1421
1422 /**
1423 * Trim trailing whitespace from lines and trailing empty
1424 * lines from the document.
1425 */
1426 public void cleanWhitespace() {
1427 document.cleanWhitespace();
1428 setCursorY(document.getLineNumber() - topLine);
1429 alignCursor();
1430 }
1431
1432 /**
1433 * Set keyword highlighting.
1434 *
1435 * @param enabled if true, enable keyword highlighting
1436 */
1437 public void setHighlighting(final boolean enabled) {
1438 document.setHighlighting(enabled);
1439 }
1440
1441 }