a329dfa347b770dcb279140a1c5ef170f72aab64
[fanfix.git] / 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
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.*;
44
45 /**
46 * TEditorWidget displays an editable text document. It is unaware of
47 * scrolling behavior, but can respond to mouse and keyboard events.
48 */
49 public class TEditorWidget extends TWidget implements EditMenuUser {
50
51 // ------------------------------------------------------------------------
52 // Constants --------------------------------------------------------------
53 // ------------------------------------------------------------------------
54
55 /**
56 * The number of lines to scroll on mouse wheel up/down.
57 */
58 private static final int wheelScrollSize = 3;
59
60 // ------------------------------------------------------------------------
61 // Variables --------------------------------------------------------------
62 // ------------------------------------------------------------------------
63
64 /**
65 * The document being edited.
66 */
67 private Document document;
68
69 /**
70 * The default color for the TEditor class.
71 */
72 private CellAttributes defaultColor = null;
73
74 /**
75 * The topmost line number in the visible area. 0-based.
76 */
77 private int topLine = 0;
78
79 /**
80 * The leftmost column number in the visible area. 0-based.
81 */
82 private int leftColumn = 0;
83
84 /**
85 * If true, the mouse is dragging a selection.
86 */
87 private boolean inSelection = false;
88
89 /**
90 * Selection starting column.
91 */
92 private int selectionColumn0;
93
94 /**
95 * Selection starting line.
96 */
97 private int selectionLine0;
98
99 /**
100 * Selection ending column.
101 */
102 private int selectionColumn1;
103
104 /**
105 * Selection ending line.
106 */
107 private int selectionLine1;
108
109 // ------------------------------------------------------------------------
110 // Constructors -----------------------------------------------------------
111 // ------------------------------------------------------------------------
112
113 /**
114 * Public constructor.
115 *
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
122 */
123 public TEditorWidget(final TWidget parent, final String text, final int x,
124 final int y, final int width, final int height) {
125
126 // Set parent and window
127 super(parent, x, y, width, height);
128
129 setCursorVisible(true);
130
131 defaultColor = getTheme().getColor("teditor");
132 document = new Document(text, defaultColor);
133 }
134
135 // ------------------------------------------------------------------------
136 // Event handlers ---------------------------------------------------------
137 // ------------------------------------------------------------------------
138
139 /**
140 * Handle mouse press events.
141 *
142 * @param mouse mouse button press event
143 */
144 @Override
145 public void onMouseDown(final TMouseEvent mouse) {
146 if (mouse.isMouseWheelUp()) {
147 for (int i = 0; i < wheelScrollSize; i++) {
148 if (topLine > 0) {
149 topLine--;
150 alignDocument(false);
151 }
152 }
153 return;
154 }
155 if (mouse.isMouseWheelDown()) {
156 for (int i = 0; i < wheelScrollSize; i++) {
157 if (topLine < document.getLineCount() - 1) {
158 topLine++;
159 alignDocument(true);
160 }
161 }
162 return;
163 }
164
165 if (mouse.isMouse1()) {
166 // Selection.
167 inSelection = true;
168 selectionColumn0 = leftColumn + mouse.getX();
169 selectionLine0 = topLine + mouse.getY();
170 selectionColumn1 = selectionColumn0;
171 selectionLine1 = selectionLine0;
172
173 // Set the row and column
174 int newLine = topLine + mouse.getY();
175 int newX = leftColumn + mouse.getX();
176 if (newLine > document.getLineCount() - 1) {
177 // Go to the end
178 document.setLineNumber(document.getLineCount() - 1);
179 document.end();
180 if (newLine > document.getLineCount() - 1) {
181 setCursorY(document.getLineCount() - 1 - topLine);
182 } else {
183 setCursorY(mouse.getY());
184 }
185 alignCursor();
186 if (inSelection) {
187 selectionColumn1 = document.getCursor();
188 selectionLine1 = document.getLineNumber();
189 }
190 return;
191 }
192
193 document.setLineNumber(newLine);
194 setCursorY(mouse.getY());
195 if (newX >= document.getCurrentLine().getDisplayLength()) {
196 document.end();
197 alignCursor();
198 } else {
199 document.setCursor(newX);
200 setCursorX(mouse.getX());
201 }
202 if (inSelection) {
203 selectionColumn1 = document.getCursor();
204 selectionLine1 = document.getLineNumber();
205 }
206 return;
207 } else {
208 inSelection = false;
209 }
210
211 // Pass to children
212 super.onMouseDown(mouse);
213 }
214
215 /**
216 * Handle mouse motion events.
217 *
218 * @param mouse mouse motion event
219 */
220 @Override
221 public void onMouseMotion(final TMouseEvent mouse) {
222
223 if (mouse.isMouse1()) {
224 // Selection.
225 if (inSelection) {
226 selectionColumn1 = leftColumn + mouse.getX();
227 selectionLine1 = topLine + mouse.getY();
228 } else {
229 inSelection = true;
230 selectionColumn0 = leftColumn + mouse.getX();
231 selectionLine0 = topLine + mouse.getY();
232 selectionColumn1 = selectionColumn0;
233 selectionLine1 = selectionLine0;
234 }
235
236 // Set the row and column
237 int newLine = topLine + mouse.getY();
238 int newX = leftColumn + mouse.getX();
239 if (newLine > document.getLineCount() - 1) {
240 // Go to the end
241 document.setLineNumber(document.getLineCount() - 1);
242 document.end();
243 if (newLine > document.getLineCount() - 1) {
244 setCursorY(document.getLineCount() - 1 - topLine);
245 } else {
246 setCursorY(mouse.getY());
247 }
248 alignCursor();
249 if (inSelection) {
250 selectionColumn1 = document.getCursor();
251 selectionLine1 = document.getLineNumber();
252 }
253 return;
254 }
255
256 document.setLineNumber(newLine);
257 setCursorY(mouse.getY());
258 if (newX >= document.getCurrentLine().getDisplayLength()) {
259 document.end();
260 alignCursor();
261 } else {
262 document.setCursor(newX);
263 setCursorX(mouse.getX());
264 }
265 if (inSelection) {
266 selectionColumn1 = document.getCursor();
267 selectionLine1 = document.getLineNumber();
268 }
269 return;
270 }
271
272 // Pass to children
273 super.onMouseDown(mouse);
274 }
275
276 /**
277 * Handle keystrokes.
278 *
279 * @param keypress keystroke event
280 */
281 @Override
282 public void onKeypress(final TKeypressEvent keypress) {
283 if (keypress.getKey().isShift()) {
284 // Selection.
285 if (!inSelection) {
286 inSelection = true;
287 selectionColumn0 = document.getCursor();
288 selectionLine0 = document.getLineNumber();
289 selectionColumn1 = selectionColumn0;
290 selectionLine1 = selectionLine0;
291 }
292 } else {
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)
301 ) {
302 // Non-shifted navigation keys disable selection.
303 inSelection = false;
304 }
305 }
306
307 if (keypress.equals(kbLeft)
308 || keypress.equals(kbShiftLeft)
309 ) {
310 document.left();
311 alignTopLine(false);
312 } else if (keypress.equals(kbRight)
313 || keypress.equals(kbShiftRight)
314 ) {
315 document.right();
316 alignTopLine(true);
317 } else if (keypress.equals(kbAltLeft)
318 || keypress.equals(kbCtrlLeft)
319 || keypress.equals(kbAltShiftLeft)
320 || keypress.equals(kbCtrlShiftLeft)
321 ) {
322 document.backwardsWord();
323 alignTopLine(false);
324 } else if (keypress.equals(kbAltRight)
325 || keypress.equals(kbCtrlRight)
326 || keypress.equals(kbAltShiftRight)
327 || keypress.equals(kbCtrlShiftRight)
328 ) {
329 document.forwardsWord();
330 alignTopLine(true);
331 } else if (keypress.equals(kbUp)
332 || keypress.equals(kbShiftUp)
333 ) {
334 document.up();
335 alignTopLine(false);
336 } else if (keypress.equals(kbDown)
337 || keypress.equals(kbShiftDown)
338 ) {
339 document.down();
340 alignTopLine(true);
341 } else if (keypress.equals(kbPgUp)
342 || keypress.equals(kbShiftPgUp)
343 ) {
344 document.up(getHeight() - 1);
345 alignTopLine(false);
346 } else if (keypress.equals(kbPgDn)
347 || keypress.equals(kbShiftPgDn)
348 ) {
349 document.down(getHeight() - 1);
350 alignTopLine(true);
351 } else if (keypress.equals(kbHome)
352 || keypress.equals(kbShiftHome)
353 ) {
354 if (document.home()) {
355 leftColumn = 0;
356 if (leftColumn < 0) {
357 leftColumn = 0;
358 }
359 setCursorX(0);
360 }
361 } else if (keypress.equals(kbEnd)
362 || keypress.equals(kbShiftEnd)
363 ) {
364 if (document.end()) {
365 alignCursor();
366 }
367 } else if (keypress.equals(kbCtrlHome)
368 || keypress.equals(kbCtrlShiftHome)
369 ) {
370 document.setLineNumber(0);
371 document.home();
372 topLine = 0;
373 leftColumn = 0;
374 setCursorX(0);
375 setCursorY(0);
376 } else if (keypress.equals(kbCtrlEnd)
377 || keypress.equals(kbCtrlShiftEnd)
378 ) {
379 document.setLineNumber(document.getLineCount() - 1);
380 document.end();
381 alignTopLine(false);
382 } else if (keypress.equals(kbIns)) {
383 document.setOverwrite(!document.getOverwrite());
384 } else if (keypress.equals(kbDel)) {
385 if (inSelection) {
386 deleteSelection();
387 } else {
388 document.del();
389 }
390 alignCursor();
391 } else if (keypress.equals(kbBackspace)
392 || keypress.equals(kbBackspaceDel)
393 ) {
394 if (inSelection) {
395 deleteSelection();
396 } else {
397 document.backspace();
398 }
399 alignTopLine(false);
400 } else if (keypress.equals(kbTab)) {
401 deleteSelection();
402 // Add spaces until we hit modulo 8.
403 for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
404 document.addChar(' ');
405 }
406 alignCursor();
407 } else if (keypress.equals(kbEnter)) {
408 deleteSelection();
409 document.enter();
410 alignTopLine(true);
411 } else if (!keypress.getKey().isFnKey()
412 && !keypress.getKey().isAlt()
413 && !keypress.getKey().isCtrl()
414 ) {
415 // Plain old keystroke, process it
416 deleteSelection();
417 document.addChar(keypress.getKey().getChar());
418 alignCursor();
419 } else {
420 // Pass other keys (tab etc.) on to TWidget
421 super.onKeypress(keypress);
422 }
423
424 if (inSelection) {
425 selectionColumn1 = document.getCursor();
426 selectionLine1 = document.getLineNumber();
427 }
428 }
429
430 /**
431 * Method that subclasses can override to handle window/screen resize
432 * events.
433 *
434 * @param resize resize event
435 */
436 @Override
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
443 // things.
444 if (getCursorX() >= getWidth()) {
445 leftColumn += getCursorX() - (getWidth() - 1);
446 setCursorX(getWidth() - 1);
447 }
448 if (getCursorY() >= getHeight()) {
449 topLine += getCursorY() - (getHeight() - 1);
450 setCursorY(getHeight() - 1);
451 }
452 } else {
453 // Let superclass handle it
454 super.onResize(resize);
455 }
456 }
457
458 /**
459 * Handle posted command events.
460 *
461 * @param command command event
462 */
463 @Override
464 public void onCommand(final TCommandEvent command) {
465 if (command.equals(cmCut)) {
466 // Copy text to clipboard, and then remove it.
467 copySelection();
468 deleteSelection();
469 return;
470 }
471
472 if (command.equals(cmCopy)) {
473 // Copy text to clipboard.
474 copySelection();
475 return;
476 }
477
478 if (command.equals(cmPaste)) {
479 // Delete selected text, then paste text from clipboard.
480 deleteSelection();
481
482 String text = getClipboard().pasteText();
483 if (text != null) {
484 for (int i = 0; i < text.length(); ) {
485 int ch = text.codePointAt(i);
486 onKeypress(new TKeypressEvent(false, 0, ch, false, false,
487 false));
488 i += Character.charCount(ch);
489 }
490 }
491 return;
492 }
493
494 if (command.equals(cmClear)) {
495 // Remove text.
496 deleteSelection();
497 return;
498 }
499
500 }
501
502 // ------------------------------------------------------------------------
503 // TWidget ----------------------------------------------------------------
504 // ------------------------------------------------------------------------
505
506 /**
507 * Draw the text box.
508 */
509 @Override
510 public void draw() {
511 CellAttributes selectedColor = getTheme().getColor("teditor.selected");
512
513 int startCol = selectionColumn0;
514 int startRow = selectionLine0;
515 int endCol = selectionColumn1;
516 int endRow = selectionLine1;
517
518 if (((selectionColumn1 < selectionColumn0)
519 && (selectionLine1 <= selectionLine0))
520 || ((selectionColumn1 <= selectionColumn0)
521 && (selectionLine1 < selectionLine0))
522 ) {
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;
529 }
530
531 for (int i = 0; i < getHeight(); i++) {
532 // Background line
533 getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
534
535 // Now draw document's line
536 if (topLine + i < document.getLineCount()) {
537 Line line = document.getLine(topLine + i);
538 int x = 0;
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(),
543 word.getColor());
544 x += word.getDisplayLength();
545 if (x - leftColumn > getWidth()) {
546 break;
547 }
548 }
549
550 // Highlight selected region
551 if (inSelection) {
552 if (startRow == endRow) {
553 if (topLine + i == startRow) {
554 for (x = startCol; x <= endCol; x++) {
555 putAttrXY(x - leftColumn, i, selectedColor);
556 }
557 }
558 } else {
559 if (topLine + i == startRow) {
560 for (x = startCol; x < line.getDisplayLength(); x++) {
561 putAttrXY(x - leftColumn, i, selectedColor);
562 }
563 } else if (topLine + i == endRow) {
564 for (x = 0; x <= endCol; x++) {
565 putAttrXY(x - leftColumn, i, selectedColor);
566 }
567 } else if ((topLine + i >= startRow)
568 && (topLine + i <= endRow)
569 ) {
570 for (x = 0; x < getWidth(); x++) {
571 putAttrXY(x, i, selectedColor);
572 }
573 }
574 }
575 }
576
577 }
578 }
579 }
580
581 // ------------------------------------------------------------------------
582 // TEditorWidget ----------------------------------------------------------
583 // ------------------------------------------------------------------------
584
585 /**
586 * Align visible area with document current line.
587 *
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.
591 */
592 private void alignTopLine(final boolean topLineIsTop) {
593 int line = document.getLineNumber();
594
595 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
596 // Need to move topLine to bring document back into view.
597 if (topLineIsTop) {
598 topLine = line - (getHeight() - 1);
599 if (topLine < 0) {
600 topLine = 0;
601 }
602 assert (topLine >= 0);
603 } else {
604 topLine = line;
605 assert (topLine >= 0);
606 }
607 }
608
609 /*
610 System.err.println("line " + line + " topLine " + topLine);
611 */
612
613 // Document is in view, let's set cursorY
614 assert (line >= topLine);
615 setCursorY(line - topLine);
616 alignCursor();
617 }
618
619 /**
620 * Align document current line with visible area.
621 *
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.
625 */
626 private void alignDocument(final boolean topLineIsTop) {
627 int line = document.getLineNumber();
628 int cursor = document.getCursor();
629
630 if ((line < topLine) || (line > topLine + getHeight() - 1)) {
631 // Need to move document to ensure it fits view.
632 if (topLineIsTop) {
633 document.setLineNumber(topLine);
634 } else {
635 document.setLineNumber(topLine + (getHeight() - 1));
636 }
637 if (cursor < document.getCurrentLine().getDisplayLength()) {
638 document.setCursor(cursor);
639 }
640 }
641
642 /*
643 System.err.println("getLineNumber() " + document.getLineNumber() +
644 " topLine " + topLine);
645 */
646
647 // Document is in view, let's set cursorY
648 setCursorY(document.getLineNumber() - topLine);
649 alignCursor();
650 }
651
652 /**
653 * Align visible cursor with document cursor.
654 */
655 private void alignCursor() {
656 int width = getWidth();
657
658 int desiredX = document.getCursor() - leftColumn;
659 if (desiredX < 0) {
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);
665 }
666
667 /*
668 System.err.println("document cursor " + document.getCursor() +
669 " leftColumn " + leftColumn);
670 */
671
672
673 setCursorX(document.getCursor() - leftColumn);
674 }
675
676 /**
677 * Get the number of lines in the underlying Document.
678 *
679 * @return the number of lines
680 */
681 public int getLineCount() {
682 return document.getLineCount();
683 }
684
685 /**
686 * Get the current visible top row number. 1-based.
687 *
688 * @return the visible top row number. Row 1 is the first row.
689 */
690 public int getVisibleRowNumber() {
691 return topLine + 1;
692 }
693
694 /**
695 * Set the current visible row number. 1-based.
696 *
697 * @param row the new visible row number. Row 1 is the first row.
698 */
699 public void setVisibleRowNumber(final int row) {
700 assert (row > 0);
701 if ((row > 0) && (row < document.getLineCount())) {
702 topLine = row - 1;
703 alignDocument(true);
704 }
705 }
706
707 /**
708 * Get the current editing row number. 1-based.
709 *
710 * @return the editing row number. Row 1 is the first row.
711 */
712 public int getEditingRowNumber() {
713 return document.getLineNumber() + 1;
714 }
715
716 /**
717 * Set the current editing row number. 1-based.
718 *
719 * @param row the new editing row number. Row 1 is the first row.
720 */
721 public void setEditingRowNumber(final int row) {
722 assert (row > 0);
723 if ((row > 0) && (row < document.getLineCount())) {
724 document.setLineNumber(row - 1);
725 alignTopLine(true);
726 }
727 }
728
729 /**
730 * Set the current visible column number. 1-based.
731 *
732 * @return the visible column number. Column 1 is the first column.
733 */
734 public int getVisibleColumnNumber() {
735 return leftColumn + 1;
736 }
737
738 /**
739 * Set the current visible column number. 1-based.
740 *
741 * @param column the new visible column number. Column 1 is the first
742 * column.
743 */
744 public void setVisibleColumnNumber(final int column) {
745 assert (column > 0);
746 if ((column > 0) && (column < document.getLineLengthMax())) {
747 leftColumn = column - 1;
748 alignDocument(true);
749 }
750 }
751
752 /**
753 * Get the current editing column number. 1-based.
754 *
755 * @return the editing column number. Column 1 is the first column.
756 */
757 public int getEditingColumnNumber() {
758 return document.getCursor() + 1;
759 }
760
761 /**
762 * Set the current editing column number. 1-based.
763 *
764 * @param column the new editing column number. Column 1 is the first
765 * column.
766 */
767 public void setEditingColumnNumber(final int column) {
768 if ((column > 0) && (column < document.getLineLength())) {
769 document.setCursor(column - 1);
770 alignCursor();
771 }
772 }
773
774 /**
775 * Get the maximum possible row number. 1-based.
776 *
777 * @return the maximum row number. Row 1 is the first row.
778 */
779 public int getMaximumRowNumber() {
780 return document.getLineCount() + 1;
781 }
782
783 /**
784 * Get the maximum possible column number. 1-based.
785 *
786 * @return the maximum column number. Column 1 is the first column.
787 */
788 public int getMaximumColumnNumber() {
789 return document.getLineLengthMax() + 1;
790 }
791
792 /**
793 * Get the dirty value.
794 *
795 * @return true if the buffer is dirty
796 */
797 public boolean isDirty() {
798 return document.isDirty();
799 }
800
801 /**
802 * Save contents to file.
803 *
804 * @param filename file to save to
805 * @throws IOException if a java.io operation throws
806 */
807 public void saveToFilename(final String filename) throws IOException {
808 document.saveToFilename(filename);
809 }
810
811 /**
812 * Delete text within the selection bounds.
813 */
814 private void deleteSelection() {
815 if (inSelection == false) {
816 return;
817 }
818 inSelection = false;
819
820 int startCol = selectionColumn0;
821 int startRow = selectionLine0;
822 int endCol = selectionColumn1;
823 int endRow = selectionLine1;
824
825 if (((selectionColumn1 < selectionColumn0)
826 && (selectionLine1 <= selectionLine0))
827 || ((selectionColumn1 <= selectionColumn0)
828 && (selectionLine1 < selectionLine0))
829 ) {
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;
836 }
837
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))
844 ) {
845 document.backspace();
846 }
847 alignTopLine(true);
848 }
849
850 /**
851 * Copy text within the selection bounds to clipboard.
852 */
853 private void copySelection() {
854 if (inSelection == false) {
855 return;
856 }
857
858 int startCol = selectionColumn0;
859 int startRow = selectionLine0;
860 int endCol = selectionColumn1;
861 int endRow = selectionLine1;
862
863 if (((selectionColumn1 < selectionColumn0)
864 && (selectionLine1 <= selectionLine0))
865 || ((selectionColumn1 <= selectionColumn0)
866 && (selectionLine1 < selectionLine0))
867 ) {
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;
874 }
875
876 StringBuilder sb = new StringBuilder();
877
878 if (endRow > startRow) {
879 // First line
880 String line = document.getLine(startRow).getRawString();
881 int x = 0;
882 for (int i = 0; i < line.length(); ) {
883 int ch = line.codePointAt(i);
884
885 if (x >= startCol) {
886 sb.append(Character.toChars(ch));
887 }
888 x += StringUtils.width(ch);
889 i += Character.charCount(ch);
890 }
891 sb.append("\n");
892
893 // Middle lines
894 for (int y = startRow + 1; y < endRow; y++) {
895 sb.append(document.getLine(y).getRawString());
896 sb.append("\n");
897 }
898
899 // Final line
900 line = document.getLine(endRow).getRawString();
901 x = 0;
902 for (int i = 0; i < line.length(); ) {
903 int ch = line.codePointAt(i);
904
905 if (x > endCol) {
906 break;
907 }
908
909 sb.append(Character.toChars(ch));
910 x += StringUtils.width(ch);
911 i += Character.charCount(ch);
912 }
913 } else {
914 assert (startRow == endRow);
915
916 // Only one line
917 String line = document.getLine(startRow).getRawString();
918 int x = 0;
919 for (int i = 0; i < line.length(); ) {
920 int ch = line.codePointAt(i);
921
922 if ((x >= startCol) && (x <= endCol)) {
923 sb.append(Character.toChars(ch));
924 }
925
926 x += StringUtils.width(ch);
927 i += Character.charCount(ch);
928 }
929 }
930
931 getClipboard().copyText(sb.toString());
932 }
933
934 // ------------------------------------------------------------------------
935 // EditMenuUser -----------------------------------------------------------
936 // ------------------------------------------------------------------------
937
938 /**
939 * Check if the cut menu item should be enabled.
940 *
941 * @return true if the cut menu item should be enabled
942 */
943 public boolean isEditMenuCut() {
944 return true;
945 }
946
947 /**
948 * Check if the copy menu item should be enabled.
949 *
950 * @return true if the copy menu item should be enabled
951 */
952 public boolean isEditMenuCopy() {
953 return true;
954 }
955
956 /**
957 * Check if the paste menu item should be enabled.
958 *
959 * @return true if the paste menu item should be enabled
960 */
961 public boolean isEditMenuPaste() {
962 return true;
963 }
964
965 /**
966 * Check if the clear menu item should be enabled.
967 *
968 * @return true if the clear menu item should be enabled
969 */
970 public boolean isEditMenuClear() {
971 return true;
972 }
973
974 }