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