0cd379cdd9ad2032985c2bec02100037502c8b81
[sixth-3d.git] / src / main / java / eu / svjatoslav / sixth / e3d / gui / textEditorComponent / TextEditComponent.java
1 /*
2  * Sixth 3D engine. Author: Svjatoslav Agejenko.
3  * This project is released under Creative Commons Zero (CC0) license.
4  */
5 package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
6
7 import eu.svjatoslav.sixth.e3d.geometry.Point2D;
8 import eu.svjatoslav.sixth.e3d.gui.GuiComponent;
9 import eu.svjatoslav.sixth.e3d.gui.TextPointer;
10 import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
11 import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardHelper;
12 import eu.svjatoslav.sixth.e3d.math.Transform;
13 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas;
14
15 import java.awt.*;
16 import java.awt.datatransfer.*;
17 import java.awt.event.KeyEvent;
18 import java.io.IOException;
19 import java.util.HashSet;
20 import java.util.Set;
21
22 public class TextEditComponent extends GuiComponent implements ClipboardOwner {
23
24     private static final long serialVersionUID = -7118833957783600630L;
25
26     /**
27      * Text rows that need to be repainted.
28      */
29     private final Set<Integer> dirtyRows = new HashSet<>();
30
31
32     private final TextCanvas textCanvas;
33     public int scrolledCharacters = 0, scrolledLines = 0;
34     public boolean selecting = false;
35
36     /**
37      * Selection start and end pointers.
38      */
39     public TextPointer selectionStart = new TextPointer(0, 0);
40     public TextPointer selectionEnd = new TextPointer(0, 0);
41
42
43     public TextPointer cursorLocation = new TextPointer(0, 0);
44     Page page = new Page();
45     LookAndFeel lookAndFeel;
46
47     /**
48      * If true, the page will be repainted on the next update.
49      */
50     boolean repaintPage = false;
51
52     public TextEditComponent(final Transform transform,
53                              final ViewPanel viewPanel,
54                              final Point2D sizeInWorldCoordinates,
55                              LookAndFeel lookAndFeel) {
56         super(transform, viewPanel, sizeInWorldCoordinates.to3D());
57
58         this.lookAndFeel = lookAndFeel;
59         final int columns = (int) (sizeInWorldCoordinates.x / TextCanvas.FONT_CHAR_WIDTH);
60         final int rows = (int) (sizeInWorldCoordinates.y / TextCanvas.FONT_CHAR_HEIGHT);
61
62         textCanvas = new TextCanvas(
63                 new Transform(),
64                 new TextPointer(rows, columns),
65                 lookAndFeel.foreground, lookAndFeel.background);
66
67         textCanvas.setMouseInteractionController(this);
68
69         repaintPage();
70         addShape(textCanvas);
71     }
72
73     private void checkCursorBoundaries() {
74         if (cursorLocation.column < 0)
75             cursorLocation.column = 0;
76         if (cursorLocation.row < 0)
77             cursorLocation.row = 0;
78
79         // ensure chat cursor stays within vertical editor boundaries by
80         // vertical scrolling
81         if ((cursorLocation.row - scrolledLines) < 0)
82             scroll(0, cursorLocation.row - scrolledLines);
83
84         if ((((cursorLocation.row - scrolledLines) + 1)) > textCanvas.getSize().row)
85             scroll(0,
86                     ((((((cursorLocation.row - scrolledLines) + 1) - textCanvas
87                             .getSize().row)))));
88
89         // ensure chat cursor stays within horizontal editor boundaries by
90         // horizontal scrolling
91         if ((cursorLocation.column - scrolledCharacters) < 0)
92             scroll(cursorLocation.column - scrolledCharacters, 0);
93
94         if ((((cursorLocation.column - scrolledCharacters) + 1)) > textCanvas
95                 .getSize().column)
96             scroll((((((cursorLocation.column - scrolledCharacters) + 1) - textCanvas
97                     .getSize().column))), 0);
98     }
99
100     /**
101      * Clear text selection.
102      */
103     public void clearSelection() {
104         selectionEnd = new TextPointer(selectionStart);
105         repaintPage = true;
106     }
107
108     /**
109      * Copies selected text to the clipboard.
110      */
111     public void copyToClipboard() {
112         if (selectionStart.compareTo(selectionEnd) == 0)
113             return;
114         // System.out.println("Copy action.");
115         final StringBuilder msg = new StringBuilder();
116
117         ensureSelectionOrder();
118
119         for (int row = selectionStart.row; row <= selectionEnd.row; row++) {
120             final TextLine textLine = page.getLine(row);
121
122             if (row == selectionStart.row) {
123                 if (row == selectionEnd.row)
124                     msg.append(textLine.getSubString(selectionStart.column,
125                             selectionEnd.column + 1));
126                 else
127                     msg.append(textLine.getSubString(selectionStart.column,
128                             textLine.getLength()));
129             } else {
130                 msg.append('\n');
131                 if (row == selectionEnd.row)
132                     msg.append(textLine
133                             .getSubString(0, selectionEnd.column + 1));
134                 else
135                     msg.append(textLine.toString());
136             }
137         }
138
139         setClipboardContents(msg.toString());
140     }
141
142     public void cutToClipboard() {
143         copyToClipboard();
144         deleteSelection();
145         repaintPage();
146     }
147
148     public void deleteSelection() {
149         ensureSelectionOrder();
150         int ym = 0;
151
152         for (int line = selectionStart.row; line <= selectionEnd.row; line++) {
153             final TextLine currentLine = page.getLine(line - ym);
154
155             if (line == selectionStart.row) {
156                 if (line == selectionEnd.row)
157
158                     currentLine.cutSubString(selectionStart.column,
159                             selectionEnd.column);
160                 else if (selectionStart.column == 0) {
161                     page.removeLine(line - ym);
162                     ym++;
163                 } else
164                     currentLine.cutSubString(selectionStart.column,
165                             currentLine.getLength() + 1);
166             } else if (line == selectionEnd.row)
167                 currentLine.cutSubString(0, selectionEnd.column);
168             else {
169                 page.removeLine(line - ym);
170                 ym++;
171             }
172         }
173
174         clearSelection();
175         cursorLocation = new TextPointer(selectionStart);
176     }
177
178     /**
179      * Ensures that {@link #selectionStart} is smaller than
180      * {@link #selectionEnd}.
181      */
182     public void ensureSelectionOrder() {
183         if (selectionStart.compareTo(selectionEnd) > 0) {
184             final TextPointer temp = selectionEnd;
185             selectionEnd = selectionStart;
186             selectionStart = temp;
187         }
188     }
189
190     public String getClipboardContents() {
191         String result = "";
192         final Clipboard clipboard = Toolkit.getDefaultToolkit()
193                 .getSystemClipboard();
194         // odd: the Object param of getContents is not currently used
195         final Transferable contents = clipboard.getContents(null);
196         final boolean hasTransferableText = (contents != null)
197                 && contents.isDataFlavorSupported(DataFlavor.stringFlavor);
198         if (hasTransferableText)
199             try {
200                 result = (String) contents
201                         .getTransferData(DataFlavor.stringFlavor);
202             } catch (final UnsupportedFlavorException | IOException ex) {
203                 // highly unlikely since we are using a standard DataFlavor
204                 System.out.println(ex);
205             }
206         // System.out.println(result);
207         return result;
208     }
209
210     /**
211      * Place string into system clipboard so that it can be pasted into other
212      * applications.
213      */
214     public void setClipboardContents(final String contents) {
215         final StringSelection stringSelection = new StringSelection(contents);
216         final Clipboard clipboard = Toolkit.getDefaultToolkit()
217                 .getSystemClipboard();
218         clipboard.setContents(stringSelection, stringSelection);
219     }
220
221     public void goToLine(final int Line) {
222         // markNavigationLocation(Line);
223         scrolledLines = Line + 1;
224         cursorLocation.row = Line + 1;
225         cursorLocation.column = 0;
226         repaintPage();
227     }
228
229     public void insertText(final String txt) {
230         if (txt == null)
231             return;
232
233         for (final char c : txt.toCharArray()) {
234
235             if (c == KeyboardHelper.DEL) {
236                 processDel();
237                 continue;
238             }
239
240             if (c == KeyboardHelper.ENTER) {
241                 processEnter();
242                 continue;
243             }
244
245             if (c == KeyboardHelper.BACKSPACE) {
246                 processBackspace();
247                 continue;
248             }
249
250             // type character
251             if (KeyboardHelper.isText(c)) {
252                 page.insertCharacter(cursorLocation.row, cursorLocation.column,
253                         c);
254                 cursorLocation.column++;
255             }
256         }
257     }
258
259     /**
260      * Parse key presses.
261      */
262     @Override
263     public boolean keyPressed(final KeyEvent event, final ViewPanel viewPanel) {
264         super.keyPressed(event, viewPanel);
265
266         processKeyEvent(event);
267
268         markRowDirty();
269
270         checkCursorBoundaries();
271
272         repaintWhatNeeded();
273         return true;
274     }
275
276     /**
277      * Empty implementation of the ClipboardOwner interface.
278      */
279     @Override
280     public void lostOwnership(final Clipboard aClipboard,
281                               final Transferable aContents) {
282         // do nothing
283     }
284
285     public void markRowDirty() {
286         dirtyRows.add(cursorLocation.row);
287     }
288
289     public void pasteFromClipboard() {
290         insertText(getClipboardContents());
291     }
292
293     private void processBackspace() {
294         if (selectionStart.compareTo(selectionEnd) == 0) {
295             // erase single character
296             if (cursorLocation.column > 0) {
297                 cursorLocation.column--;
298                 page.removeCharacter(cursorLocation.row, cursorLocation.column);
299                 // System.out.println(lines.get(currentCursor.line).toString());
300             } else if (cursorLocation.row > 0) {
301                 cursorLocation.row--;
302                 final int currentLineLength = page
303                         .getLineLength(cursorLocation.row);
304                 cursorLocation.column = currentLineLength;
305                 page.getLine(cursorLocation.row)
306                         .insertTextLine(currentLineLength,
307                                 page.getLine(cursorLocation.row + 1));
308                 page.removeLine(cursorLocation.row + 1);
309                 repaintPage = true;
310             }
311         } else {
312             // dedent multiple lines
313             ensureSelectionOrder();
314             // scan if enough space exists
315             for (int y = selectionStart.row; y < selectionEnd.row; y++)
316                 if (page.getLine(y).getIdent() < 4)
317                     return;
318
319             for (int y = selectionStart.row; y < selectionEnd.row; y++)
320                 page.getLine(y).cutFromBeginning(4);
321
322             repaintPage = true;
323         }
324     }
325
326     private void processCtrlCombinations(final int keyCode) {
327
328         if ((char) keyCode == 'A') { // CTRL + A -- select all
329             final int lastLineIndex = page.getLinesCount() - 1;
330             selectionStart = new TextPointer(0, 0);
331             selectionEnd = new TextPointer(lastLineIndex,
332                     page.getLineLength(lastLineIndex));
333             repaintPage();
334         }
335
336         // CTRL + X -- cut
337         if ((char) keyCode == 'X')
338             cutToClipboard();
339
340         // CTRL + C -- copy
341         if ((char) keyCode == 'C')
342             copyToClipboard();
343
344         // CTRL + V -- paste
345         if ((char) keyCode == 'V')
346             pasteFromClipboard();
347
348         if (keyCode == 39) { // RIGHT
349             // skip to the beginning of the next word
350
351             for (int x = cursorLocation.column; x < (page
352                     .getLineLength(cursorLocation.row) - 1); x++)
353                 if ((page.getChar(cursorLocation.row, x) == ' ')
354                         && (page.getChar(cursorLocation.row, x + 1) != ' ')) {
355                     // beginning of the next word is found
356                     cursorLocation.column = x + 1;
357                     return;
358                 }
359
360             cursorLocation.column = page.getLineLength(cursorLocation.row);
361             return;
362         }
363
364         if (keyCode == 37) { // Left
365
366             // skip to the beginning of the previous word
367             for (int x = cursorLocation.column - 2; x >= 0; x--)
368                 if ((page.getChar(cursorLocation.row, x) == ' ')
369                         & (page.getChar(cursorLocation.row, x + 1) != ' ')) {
370                     cursorLocation.column = x + 1;
371                     return;
372                 }
373
374             cursorLocation.column = 0;
375         }
376     }
377
378     public void processDel() {
379         if (selectionStart.compareTo(selectionEnd) == 0) {
380             // is there still some text right to the cursor ?
381             if (cursorLocation.column < page.getLineLength(cursorLocation.row))
382                 page.removeCharacter(cursorLocation.row, cursorLocation.column);
383             else {
384                 page.getLine(cursorLocation.row).insertTextLine(
385                         cursorLocation.column,
386                         page.getLine(cursorLocation.row + 1));
387                 page.removeLine(cursorLocation.row + 1);
388                 repaintPage = true;
389             }
390         } else {
391             deleteSelection();
392             repaintPage = true;
393         }
394     }
395
396     private void processEnter() {
397         final TextLine currentLine = page.getLine(cursorLocation.row);
398         // move everything right to the cursor into new line
399         final TextLine newLine = currentLine.getSubLine(cursorLocation.column,
400                 currentLine.getLength());
401         page.insertLine(cursorLocation.row + 1, newLine);
402
403         // trim existing line
404         page.getLine(cursorLocation.row).cutUntilEnd(cursorLocation.column);
405         repaintPage = true;
406
407         cursorLocation.row++;
408         cursorLocation.column = 0;
409     }
410
411     private void processKeyEvent(final KeyEvent event) {
412         final int modifiers = event.getModifiersEx();
413         final int keyCode = event.getKeyCode();
414         final char keyChar = event.getKeyChar();
415
416         // System.out.println("Keycode:" + keyCode s+ ", keychar:" + keyChar);
417
418         if (KeyboardHelper.isAltPressed(modifiers))
419             return;
420
421         if (KeyboardHelper.isCtrlPressed(modifiers)) {
422             processCtrlCombinations(keyCode);
423             return;
424         }
425
426         if (keyCode == KeyboardHelper.TAB) {
427             processTab(modifiers);
428             return;
429         }
430
431         clearSelection();
432
433         if (KeyboardHelper.isText(keyCode)) {
434             insertText(String.valueOf(keyChar));
435             return;
436         }
437
438         if (KeyboardHelper.isShiftPressed(modifiers)) {
439             if (!selecting)
440                 attemptSelectionStart:{
441
442                     if (keyChar == 65535)
443                         if (keyCode == 16)
444                             break attemptSelectionStart;
445                     if (((keyChar >= 32) & (keyChar <= 128)) | (keyChar == 10)
446                             | (keyChar == 8) | (keyChar == 9))
447                         break attemptSelectionStart;
448
449                     selectionStart = new TextPointer(cursorLocation);
450                     selectionEnd = selectionStart;
451                     selecting = true;
452                     repaintPage();
453                 }
454         } else
455             selecting = false;
456
457         if (keyCode == KeyboardHelper.HOME) {
458             cursorLocation.column = 0;
459             return;
460         }
461         if (keyCode == KeyboardHelper.END) {
462             cursorLocation.column = page.getLineLength(cursorLocation.row);
463             return;
464         }
465
466         // process cursor keys
467         if (keyCode == KeyboardHelper.DOWN) {
468             markRowDirty();
469             cursorLocation.row++;
470             return;
471         }
472
473         if (keyCode == KeyboardHelper.UP) {
474             markRowDirty();
475             cursorLocation.row--;
476             return;
477         }
478
479         if (keyCode == KeyboardHelper.RIGHT) {
480             cursorLocation.column++;
481             return;
482         }
483
484         if (keyCode == KeyboardHelper.LEFT) {
485             cursorLocation.column--;
486             return;
487         }
488
489         if (keyCode == KeyboardHelper.PGDOWN) {
490             cursorLocation.row += textCanvas.getSize().row;
491             repaintPage();
492             return;
493         }
494
495         if (keyCode == KeyboardHelper.PGUP) {
496             cursorLocation.row -= textCanvas.getSize().row;
497             repaintPage = true;
498         }
499
500     }
501
502     private void processTab(final int modifiers) {
503         if (KeyboardHelper.isShiftPressed(modifiers)) {
504             if (selectionStart.compareTo(selectionEnd) != 0) {
505                 // dedent multiple lines
506                 ensureSelectionOrder();
507
508                 identSelection:
509                 {
510                     // check that indentation is possible
511                     for (int y = selectionStart.row; y < selectionEnd.row; y++) {
512                         final TextLine textLine = page.getLine(y);
513
514                         if (!textLine.isEmpty())
515                             if (textLine.getIdent() < 4)
516                                 break identSelection;
517                     }
518
519                     for (int y = selectionStart.row; y < selectionEnd.row; y++)
520                         page.getLine(y).cutFromBeginning(4);
521                 }
522             } else {
523                 // dedent current line
524                 final TextLine textLine = page.getLine(cursorLocation.row);
525
526                 if (cursorLocation.column >= 4)
527                     if (textLine.isEmpty())
528                         cursorLocation.column -= 4;
529                     else if (textLine.getIdent() >= 4) {
530                         cursorLocation.column -= 4;
531                         textLine.cutFromBeginning(4);
532                     }
533
534             }
535
536             repaintPage();
537
538         } else if (selectionStart.compareTo(selectionEnd) != 0) {
539             // ident multiple lines
540             ensureSelectionOrder();
541             for (int y = selectionStart.row; y < selectionEnd.row; y++)
542                 page.getLine(y).addIdent(4);
543
544             repaintPage();
545         }
546     }
547
548     public void repaintPage() {
549
550         final int columnCount = textCanvas.getSize().column + 2;
551         final int rowCount = textCanvas.getSize().row + 2;
552
553         for (int row = 0; row < rowCount; row++)
554             for (int column = 0; column < columnCount; column++) {
555                 final boolean isTabMargin = ((column + scrolledCharacters) % 4) == 0;
556
557                 if ((column == (cursorLocation.column - scrolledCharacters))
558                         & (row == (cursorLocation.row - scrolledLines))) {
559                     // cursor
560                     textCanvas.setBackgroundColor(lookAndFeel.cursorBackground);
561                     textCanvas.setForegroundColor(lookAndFeel.cursorForeground);
562                 } else if (new TextPointer(row + scrolledLines, column).isBetween(
563                         selectionStart, selectionEnd)) {
564                     // selected text
565                     textCanvas.setBackgroundColor(lookAndFeel.selectionBackground);
566                     textCanvas.setForegroundColor(lookAndFeel.selectionForeground);
567                 } else {
568                     // normal text
569                     textCanvas.setBackgroundColor(lookAndFeel.background);
570                     textCanvas.setForegroundColor(lookAndFeel.foreground);
571
572                     if (isTabMargin)
573                         textCanvas
574                                 .setBackgroundColor(lookAndFeel.tabStopBackground);
575
576                 }
577
578                 final char charUnderCursor = page.getChar(row + scrolledLines,
579                         column + scrolledCharacters);
580
581                 textCanvas.putChar(row, column, charUnderCursor);
582             }
583
584     }
585
586     public void repaintRow(final int rowNumber) {
587         // TODO: Optimize this. No need to repaint entire page.
588         repaintPage();
589     }
590
591     private void repaintWhatNeeded() {
592         if (repaintPage) {
593             dirtyRows.clear();
594             repaintPage();
595             return;
596         }
597
598         dirtyRows.forEach(this::repaintRow);
599         dirtyRows.clear();
600     }
601
602     /**
603      * Scroll full page to given amount of lines or charancters.
604      */
605     public void scroll(final int charactersToScroll, final int linesToScroll) {
606         scrolledLines += linesToScroll;
607         scrolledCharacters += charactersToScroll;
608
609         if (scrolledLines < 0)
610             scrolledLines = 0;
611
612         if (scrolledCharacters < 0)
613             scrolledCharacters = 0;
614
615         repaintPage = true;
616     }
617
618     public void setText(final String text) {
619         // System.out.println("Set text:" + text);
620         cursorLocation = new TextPointer(0, 0);
621         scrolledCharacters = 0;
622         scrolledLines = 0;
623         selectionStart = new TextPointer(0, 0);
624         selectionEnd = new TextPointer(0, 0);
625         page = new Page();
626         insertText(text);
627         repaintPage();
628     }
629
630 }