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