Refactoring. Ability to specify look and feel for text editor.
[sixth-3d.git] / src / main / java / eu / svjatoslav / sixth / e3d / gui / textEditorComponent / TextEditComponent.java
1 /*
2  * Sixth 3D engine. Author: Svjatoslav Agejenko. 
3  * This project is released under Creative Commons Zero (CC0) license.
4  */
5 package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
6
7 import eu.svjatoslav.sixth.e3d.geometry.Point2D;
8 import eu.svjatoslav.sixth.e3d.gui.GuiComponent;
9 import eu.svjatoslav.sixth.e3d.gui.TextPointer;
10 import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
11 import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardHelper;
12 import eu.svjatoslav.sixth.e3d.math.Transform;
13 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas;
14
15 import java.awt.*;
16 import java.awt.datatransfer.*;
17 import java.awt.event.KeyEvent;
18 import java.io.IOException;
19 import java.util.HashSet;
20 import java.util.Set;
21
22 public class TextEditComponent extends GuiComponent implements ClipboardOwner {
23
24     private static final long serialVersionUID = -7118833957783600630L;
25
26     /**
27      * Text rows that need to be repainted.
28      */
29     private final Set<Integer> dirtyRows = new HashSet<>();
30
31     private final TextCanvas textCanvas;
32     public int scrolledCharacters = 0, scrolledLines = 0;
33     public boolean selecting = false;
34     public TextPointer selectionStart = new TextPointer(0, 0);
35     public TextPointer selectionEnd = new TextPointer(0, 0);
36     public TextPointer cursorLocation = new TextPointer(0, 0);
37     Page page = new Page();
38     LookAndFeel lookAndFeel;
39     boolean repaintPage = false;
40
41     public TextEditComponent(final Transform transform,
42                              final ViewPanel viewPanel,
43                              final Point2D sizeInWorldCoordinates,
44                              LookAndFeel lookAndFeel) {
45         super(transform, viewPanel, sizeInWorldCoordinates.to3D());
46
47         this.lookAndFeel = lookAndFeel;
48         final int columns = (int) (sizeInWorldCoordinates.x / TextCanvas.FONT_CHAR_WIDTH);
49         final int rows = (int) (sizeInWorldCoordinates.y / TextCanvas.FONT_CHAR_HEIGHT);
50
51         textCanvas = new TextCanvas(
52                 new Transform(),
53                 new TextPointer(rows, columns),
54                 lookAndFeel.foreground, lookAndFeel.background);
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.getModifiersEx();
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.isAltPressed(modifiers))
408             return;
409
410         if (KeyboardHelper.isCtrlPressed(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         if (KeyboardHelper.isShiftPressed(modifiers)) {
428             if (!selecting)
429                 attemptSelectionStart:{
430
431                     if (keyChar == 65535)
432                         if (keyCode == 16)
433                             break attemptSelectionStart;
434                     if (((keyChar >= 32) & (keyChar <= 128)) | (keyChar == 10)
435                             | (keyChar == 8) | (keyChar == 9))
436                         break attemptSelectionStart;
437
438                     selectionStart = new TextPointer(cursorLocation);
439                     selectionEnd = selectionStart;
440                     selecting = true;
441                     repaintPage();
442                 }
443         } else
444             selecting = false;
445
446         if (keyCode == KeyboardHelper.HOME) {
447             cursorLocation.column = 0;
448             return;
449         }
450         if (keyCode == KeyboardHelper.END) {
451             cursorLocation.column = page.getLineLength(cursorLocation.row);
452             return;
453         }
454
455         // process cursor keys
456         if (keyCode == KeyboardHelper.DOWN) {
457             markRowDirty();
458             cursorLocation.row++;
459             return;
460         }
461
462         if (keyCode == KeyboardHelper.UP) {
463             markRowDirty();
464             cursorLocation.row--;
465             return;
466         }
467
468         if (keyCode == KeyboardHelper.RIGHT) {
469             cursorLocation.column++;
470             return;
471         }
472
473         if (keyCode == KeyboardHelper.LEFT) {
474             cursorLocation.column--;
475             return;
476         }
477
478         if (keyCode == KeyboardHelper.PGDOWN) {
479             cursorLocation.row += textCanvas.getSize().row;
480             repaintPage();
481             return;
482         }
483
484         if (keyCode == KeyboardHelper.PGUP) {
485             cursorLocation.row -= textCanvas.getSize().row;
486             repaintPage = true;
487             return;
488         }
489
490     }
491
492     private void processTab(final int modifiers) {
493         if (KeyboardHelper.isShiftPressed(modifiers)) {
494             if (selectionStart.compareTo(selectionEnd) != 0) {
495                 // dedent multiple lines
496                 ensureSelectionOrder();
497
498                 identSelection:
499                 {
500                     // check that indentation is possible
501                     for (int y = selectionStart.row; y < selectionEnd.row; y++) {
502                         final TextLine textLine = page.getLine(y);
503
504                         if (!textLine.isEmpty())
505                             if (textLine.getIdent() < 4)
506                                 break identSelection;
507                     }
508
509                     for (int y = selectionStart.row; y < selectionEnd.row; y++)
510                         page.getLine(y).cutFromBeginning(4);
511                 }
512             } else {
513                 // dedent current line
514                 final TextLine textLine = page.getLine(cursorLocation.row);
515
516                 if (cursorLocation.column >= 4)
517                     if (textLine.isEmpty())
518                         cursorLocation.column -= 4;
519                     else if (textLine.getIdent() >= 4) {
520                         cursorLocation.column -= 4;
521                         textLine.cutFromBeginning(4);
522                     }
523
524             }
525
526             repaintPage();
527
528         } else if (selectionStart.compareTo(selectionEnd) != 0) {
529             // ident multiple lines
530             ensureSelectionOrder();
531             for (int y = selectionStart.row; y < selectionEnd.row; y++)
532                 page.getLine(y).addIdent(4);
533
534             repaintPage();
535         }
536     }
537
538     public void repaintPage() {
539
540         final int columnCount = textCanvas.getSize().column + 2;
541         final int rowCount = textCanvas.getSize().row + 2;
542
543         for (int row = 0; row < rowCount; row++)
544             for (int column = 0; column < columnCount; column++) {
545                 final boolean isTabMargin = ((column + scrolledCharacters) % 4) == 0;
546
547                 if ((column == (cursorLocation.column - scrolledCharacters))
548                         & (row == (cursorLocation.row - scrolledLines))) {
549                     // cursor
550                     textCanvas.setBackgroundColor(lookAndFeel.cursorBackground);
551                     textCanvas.setForegroundColor(lookAndFeel.cursorForeground);
552                 } else if (new TextPointer(row + scrolledLines, column).isBetween(
553                         selectionStart, selectionEnd)) {
554                     // selected text
555                     textCanvas.setBackgroundColor(lookAndFeel.selectionBackground);
556                     textCanvas.setForegroundColor(lookAndFeel.selectionForeground);
557                 } else {
558                     // normal text
559                     textCanvas.setBackgroundColor(lookAndFeel.background);
560                     textCanvas.setForegroundColor(lookAndFeel.foreground);
561
562                     if (isTabMargin)
563                         textCanvas
564                                 .setBackgroundColor(lookAndFeel.tabStopBackground);
565
566                 }
567
568                 final char charUnderCursor = page.getChar(row + scrolledLines,
569                         column + scrolledCharacters);
570
571                 textCanvas.putChar(row, column, charUnderCursor);
572             }
573
574     }
575
576     public void repaintRow(final int rowNumber) {
577         // TODO: Optimize this. No need to repaint entire page.
578         repaintPage();
579     }
580
581     private void repaintWhatNeeded() {
582         if (repaintPage) {
583             dirtyRows.clear();
584             repaintPage();
585             return;
586         }
587
588         dirtyRows.forEach(this::repaintRow);
589         dirtyRows.clear();
590     }
591
592     // public void setCaret(final int x, final int y) {
593     // selecting = false;
594     // cursorLocation.column = (x / characterWidth) + scrolledCharacters;
595     // cursorLocation.row = (y / characterHeight) + scrolledLines;
596     // repaintPage();
597     // }
598
599     /**
600      * Scroll full page to given amount of lines or charancters.
601      */
602     public void scroll(final int charactersToScroll, final int linesToScroll) {
603         scrolledLines += linesToScroll;
604         scrolledCharacters += charactersToScroll;
605
606         if (scrolledLines < 0)
607             scrolledLines = 0;
608
609         if (scrolledCharacters < 0)
610             scrolledCharacters = 0;
611
612         repaintPage = true;
613     }
614
615     public void setText(final String text) {
616         // System.out.println("Set text:" + text);
617         cursorLocation = new TextPointer(0, 0);
618         scrolledCharacters = 0;
619         scrolledLines = 0;
620         selectionStart = new TextPointer(0, 0);
621         selectionEnd = new TextPointer(0, 0);
622         page = new Page();
623         insertText(text);
624         repaintPage();
625     }
626
627 }