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