Refactoring.
[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 boolean 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         return true;
263     }
264
265     /**
266      * Empty implementation of the ClipboardOwner interface.
267      */
268     @Override
269     public void lostOwnership(final Clipboard aClipboard,
270                               final Transferable aContents) {
271         // do nothing
272     }
273
274     public void markRowDirty() {
275         dirtyRows.add(cursorLocation.row);
276     }
277
278     public void pasteFromClipboard() {
279         insertText(getClipboardContents());
280     }
281
282     private void processBackspace() {
283         if (selectionStart.compareTo(selectionEnd) == 0) {
284             // erase single character
285             if (cursorLocation.column > 0) {
286                 cursorLocation.column--;
287                 page.removeCharacter(cursorLocation.row, cursorLocation.column);
288                 // System.out.println(lines.get(currentCursor.line).toString());
289             } else if (cursorLocation.row > 0) {
290                 cursorLocation.row--;
291                 final int currentLineLength = page
292                         .getLineLength(cursorLocation.row);
293                 cursorLocation.column = currentLineLength;
294                 page.getLine(cursorLocation.row)
295                         .insertTextLine(currentLineLength,
296                                 page.getLine(cursorLocation.row + 1));
297                 page.removeLine(cursorLocation.row + 1);
298                 repaintPage = true;
299             }
300         } else {
301             // dedent multiple lines
302             ensureSelectionOrder();
303             // scan if enough space exists
304             for (int y = selectionStart.row; y < selectionEnd.row; y++)
305                 if (page.getLine(y).getIdent() < 4)
306                     return;
307
308             for (int y = selectionStart.row; y < selectionEnd.row; y++)
309                 page.getLine(y).cutFromBeginning(4);
310
311             repaintPage = true;
312         }
313     }
314
315     private void processCtrlCombinations(final int keyCode) {
316
317         if ((char) keyCode == 'A') { // CTRL + A -- select all
318             final int lastLineIndex = page.getLinesCount() - 1;
319             selectionStart = new TextPointer(0, 0);
320             selectionEnd = new TextPointer(lastLineIndex,
321                     page.getLineLength(lastLineIndex));
322             repaintPage();
323         }
324
325         // CTRL + X -- cut
326         if ((char) keyCode == 'X')
327             cutToClipboard();
328
329         // CTRL + C -- copy
330         if ((char) keyCode == 'C')
331             copyToClipboard();
332
333         // CTRL + V -- paste
334         if ((char) keyCode == 'V')
335             pasteFromClipboard();
336
337         if (keyCode == 39) { // RIGHT
338             // skip to the beginning of the next word
339
340             for (int x = cursorLocation.column; x < (page
341                     .getLineLength(cursorLocation.row) - 1); x++)
342                 if ((page.getChar(cursorLocation.row, x) == ' ')
343                         && (page.getChar(cursorLocation.row, x + 1) != ' ')) {
344                     // beginning of the next word is found
345                     cursorLocation.column = x + 1;
346                     return;
347                 }
348
349             cursorLocation.column = page.getLineLength(cursorLocation.row);
350             return;
351         }
352
353         if (keyCode == 37) { // Left
354
355             // skip to the beginning of the previous word
356             for (int x = cursorLocation.column - 2; x >= 0; x--)
357                 if ((page.getChar(cursorLocation.row, x) == ' ')
358                         & (page.getChar(cursorLocation.row, x + 1) != ' ')) {
359                     cursorLocation.column = x + 1;
360                     return;
361                 }
362
363             cursorLocation.column = 0;
364         }
365     }
366
367     public void processDel() {
368         if (selectionStart.compareTo(selectionEnd) == 0) {
369             // is there still some text right to the cursor ?
370             if (cursorLocation.column < page.getLineLength(cursorLocation.row))
371                 page.removeCharacter(cursorLocation.row, cursorLocation.column);
372             else {
373                 page.getLine(cursorLocation.row).insertTextLine(
374                         cursorLocation.column,
375                         page.getLine(cursorLocation.row + 1));
376                 page.removeLine(cursorLocation.row + 1);
377                 repaintPage = true;
378             }
379         } else {
380             deleteSelection();
381             repaintPage = true;
382         }
383     }
384
385     private void processEnter() {
386         final TextLine currentLine = page.getLine(cursorLocation.row);
387         // move everything right to the cursor into new line
388         final TextLine newLine = currentLine.getSubLine(cursorLocation.column,
389                 currentLine.getLength());
390         page.insertLine(cursorLocation.row + 1, newLine);
391
392         // trim existing line
393         page.getLine(cursorLocation.row).cutUntilEnd(cursorLocation.column);
394         repaintPage = true;
395
396         cursorLocation.row++;
397         cursorLocation.column = 0;
398     }
399
400     private void processKeyEvent(final KeyEvent event) {
401         final int modifiers = event.getModifiers();
402
403         final int keyCode = event.getKeyCode();
404         final char keyChar = event.getKeyChar();
405
406         // System.out.println("Keycode:" + keyCode s+ ", keychar:" + keyChar);
407
408         if (KeyboardHelper.isAlt(modifiers))
409             return;
410
411         if (KeyboardHelper.isCtrl(modifiers)) {
412             processCtrlCombinations(keyCode);
413             return;
414         }
415
416         if (keyCode == KeyboardHelper.TAB) {
417             processTab(modifiers);
418             return;
419         }
420
421         clearSelection();
422
423         if (KeyboardHelper.isText(keyCode)) {
424             insertText(String.valueOf(keyChar));
425             return;
426         }
427
428         // System.out.println("Co:" + String.valueOf(code) + "  Ch:" +
429         // String.valueOf(keyChar));
430
431         if (KeyboardHelper.isShift(modifiers)) {
432             if (!selecting)
433                 attemptSelectionStart:{
434
435                     if (keyChar == 65535)
436                         if (keyCode == 16)
437                             break attemptSelectionStart;
438                     if (((keyChar >= 32) & (keyChar <= 128)) | (keyChar == 10)
439                             | (keyChar == 8) | (keyChar == 9))
440                         break attemptSelectionStart;
441
442                     // System.out.println("Selection started:" + keyChar + " "
443                     // + keyCode);
444
445                     selectionStart = new TextPointer(cursorLocation);
446                     selectionEnd = selectionStart;
447                     selecting = true;
448                     repaintPage();
449                 }
450         } else
451             selecting = false;
452
453         if (keyCode == KeyboardHelper.HOME) {
454             cursorLocation.column = 0;
455             return;
456         }
457         if (keyCode == KeyboardHelper.END) {
458             cursorLocation.column = page.getLineLength(cursorLocation.row);
459             return;
460         }
461
462         // process cursor keys
463         if (keyCode == KeyboardHelper.DOWN) {
464             markRowDirty();
465             cursorLocation.row++;
466             return;
467         }
468
469         if (keyCode == KeyboardHelper.UP) {
470             markRowDirty();
471             cursorLocation.row--;
472             return;
473         }
474
475         if (keyCode == KeyboardHelper.RIGHT) {
476             cursorLocation.column++;
477             return;
478         }
479
480         if (keyCode == KeyboardHelper.LEFT) {
481             cursorLocation.column--;
482             return;
483         }
484
485         if (keyCode == KeyboardHelper.PGDOWN) {
486             cursorLocation.row += textCanvas.getSize().row;
487             repaintPage();
488             return;
489         }
490
491         if (keyCode == KeyboardHelper.PGUP) {
492             cursorLocation.row -= textCanvas.getSize().row;
493             repaintPage = true;
494             return;
495         }
496
497     }
498
499     private void processTab(final int modifiers) {
500         if (KeyboardHelper.isShift(modifiers)) {
501             if (selectionStart.compareTo(selectionEnd) != 0) {
502                 // dedent multiple lines
503                 ensureSelectionOrder();
504
505                 identSelection:
506                 {
507                     // check that identation is possible
508                     for (int y = selectionStart.row; y < selectionEnd.row; y++) {
509                         final TextLine textLine = page.getLine(y);
510
511                         if (!textLine.isEmpty())
512                             if (textLine.getIdent() < 4)
513                                 break identSelection;
514                     }
515
516                     for (int y = selectionStart.row; y < selectionEnd.row; y++)
517                         page.getLine(y).cutFromBeginning(4);
518                 }
519             } else {
520                 // dedent current line
521                 final TextLine textLine = page.getLine(cursorLocation.row);
522
523                 if (cursorLocation.column >= 4)
524                     if (textLine.isEmpty())
525                         cursorLocation.column -= 4;
526                     else if (textLine.getIdent() >= 4) {
527                         cursorLocation.column -= 4;
528                         textLine.cutFromBeginning(4);
529                     }
530
531             }
532
533             repaintPage();
534
535         } else if (selectionStart.compareTo(selectionEnd) != 0) {
536             // ident multiple lines
537             ensureSelectionOrder();
538             for (int y = selectionStart.row; y < selectionEnd.row; y++)
539                 page.getLine(y).addIdent(4);
540
541             repaintPage();
542         }
543     }
544
545     public void repaintPage() {
546
547         final int chXe = textCanvas.getSize().column + 2;
548         final int chYe = textCanvas.getSize().row + 2;
549
550         for (int cy = 0; cy < chYe; cy++)
551             for (int cx = 0; cx < chXe; cx++) {
552                 final boolean isTabMargin = ((cx + scrolledCharacters) % 4) == 0;
553
554                 if ((cx == (cursorLocation.column - scrolledCharacters))
555                         & (cy == (cursorLocation.row - scrolledLines))) {
556                     // cursor
557                     textCanvas.setBackgroundColor(colorConfig.cursorBack);
558                     textCanvas.setForegroundColor(colorConfig.cursorText);
559                 } else if (new TextPointer(cy + scrolledLines, cx).isBetween(
560                         selectionStart, selectionEnd)) {
561                     // selected text
562                     textCanvas.setBackgroundColor(colorConfig.selectedBack);
563                     textCanvas.setForegroundColor(colorConfig.selectedText);
564                 } else {
565                     // normal text
566                     textCanvas.setBackgroundColor(colorConfig.normalBack);
567                     textCanvas.setForegroundColor(colorConfig.normalText);
568
569                     if (isTabMargin)
570                         textCanvas
571                                 .setBackgroundColor(colorConfig.tabulatorBack);
572
573                 }
574
575                 final char charUnderCursor = page.getChar(cy + scrolledLines,
576                         cx + scrolledCharacters);
577
578                 textCanvas.putChar(cy, cx, charUnderCursor);
579             }
580
581     }
582
583     public void repaintRow(final int rowNumber) {
584         // TODO: fix this
585         repaintPage();
586     }
587
588     private void repaintWhatNeeded() {
589         if (repaintPage) {
590             dirtyRows.clear();
591             repaintPage();
592             return;
593         }
594
595         dirtyRows.forEach(this::repaintRow);
596         dirtyRows.clear();
597     }
598
599     // public void setCaret(final int x, final int y) {
600     // selecting = false;
601     // cursorLocation.column = (x / characterWidth) + scrolledCharacters;
602     // cursorLocation.row = (y / characterHeight) + scrolledLines;
603     // repaintPage();
604     // }
605
606     /**
607      * Scroll full page to given amount of lines or charancters.
608      */
609     public void scroll(final int charactersToScroll, final int linesToScroll) {
610         scrolledLines += linesToScroll;
611         scrolledCharacters += charactersToScroll;
612
613         if (scrolledLines < 0)
614             scrolledLines = 0;
615
616         if (scrolledCharacters < 0)
617             scrolledCharacters = 0;
618
619         repaintPage = true;
620     }
621
622     public void setText(final String text) {
623         // System.out.println("Set text:" + text);
624         cursorLocation = new TextPointer(0, 0);
625         scrolledCharacters = 0;
626         scrolledLines = 0;
627         selectionStart = new TextPointer(0, 0);
628         selectionEnd = new TextPointer(0, 0);
629         page = new Page();
630         insertText(text);
631         repaintPage();
632     }
633
634 }