#19 TField fixes
[nikiroo-utils.git] / src / jexer / backend / LogicalScreen.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.backend;
30
31 import jexer.bits.Cell;
32 import jexer.bits.CellAttributes;
33 import jexer.bits.GraphicsChars;
34
35 /**
36 * A logical screen composed of a 2D array of Cells.
37 */
38 public class LogicalScreen implements Screen {
39
40 /**
41 * Width of the visible window.
42 */
43 protected int width;
44
45 /**
46 * Height of the visible window.
47 */
48 protected int height;
49
50 /**
51 * Drawing offset for x.
52 */
53 private int offsetX;
54
55 /**
56 * Set drawing offset for x.
57 *
58 * @param offsetX new drawing offset
59 */
60 public final void setOffsetX(final int offsetX) {
61 this.offsetX = offsetX;
62 }
63
64 /**
65 * Drawing offset for y.
66 */
67 private int offsetY;
68
69 /**
70 * Set drawing offset for y.
71 *
72 * @param offsetY new drawing offset
73 */
74 public final void setOffsetY(final int offsetY) {
75 this.offsetY = offsetY;
76 }
77
78 /**
79 * Ignore anything drawn right of clipRight.
80 */
81 private int clipRight;
82
83 /**
84 * Get right drawing clipping boundary.
85 *
86 * @return drawing boundary
87 */
88 public final int getClipRight() {
89 return clipRight;
90 }
91
92 /**
93 * Set right drawing clipping boundary.
94 *
95 * @param clipRight new boundary
96 */
97 public final void setClipRight(final int clipRight) {
98 this.clipRight = clipRight;
99 }
100
101 /**
102 * Ignore anything drawn below clipBottom.
103 */
104 private int clipBottom;
105
106 /**
107 * Get bottom drawing clipping boundary.
108 *
109 * @return drawing boundary
110 */
111 public final int getClipBottom() {
112 return clipBottom;
113 }
114
115 /**
116 * Set bottom drawing clipping boundary.
117 *
118 * @param clipBottom new boundary
119 */
120 public final void setClipBottom(final int clipBottom) {
121 this.clipBottom = clipBottom;
122 }
123
124 /**
125 * Ignore anything drawn left of clipLeft.
126 */
127 private int clipLeft;
128
129 /**
130 * Get left drawing clipping boundary.
131 *
132 * @return drawing boundary
133 */
134 public final int getClipLeft() {
135 return clipLeft;
136 }
137
138 /**
139 * Set left drawing clipping boundary.
140 *
141 * @param clipLeft new boundary
142 */
143 public final void setClipLeft(final int clipLeft) {
144 this.clipLeft = clipLeft;
145 }
146
147 /**
148 * Ignore anything drawn above clipTop.
149 */
150 private int clipTop;
151
152 /**
153 * Get top drawing clipping boundary.
154 *
155 * @return drawing boundary
156 */
157 public final int getClipTop() {
158 return clipTop;
159 }
160
161 /**
162 * Set top drawing clipping boundary.
163 *
164 * @param clipTop new boundary
165 */
166 public final void setClipTop(final int clipTop) {
167 this.clipTop = clipTop;
168 }
169
170 /**
171 * The physical screen last sent out on flush().
172 */
173 protected Cell [][] physical;
174
175 /**
176 * The logical screen being rendered to.
177 */
178 protected Cell [][] logical;
179
180 /**
181 * Get dirty flag.
182 *
183 * @return if true, the logical screen is not in sync with the physical
184 * screen
185 */
186 public final boolean isDirty() {
187 for (int x = 0; x < width; x++) {
188 for (int y = 0; y < height; y++) {
189 if (!logical[x][y].equals(physical[x][y])) {
190 return true;
191 }
192 if (logical[x][y].isBlink()) {
193 // Blinking screens are always dirty. There is
194 // opportunity for a Netscape blink tag joke here...
195 return true;
196 }
197 }
198 }
199
200 return false;
201 }
202
203 /**
204 * Set if the user explicitly wants to redraw everything starting with a
205 * ECMATerminal.clearAll().
206 */
207 protected boolean reallyCleared;
208
209 /**
210 * If true, the cursor is visible and should be placed onscreen at
211 * (cursorX, cursorY) during a call to flushPhysical().
212 */
213 protected boolean cursorVisible;
214
215 /**
216 * Cursor X position if visible.
217 */
218 protected int cursorX;
219
220 /**
221 * Cursor Y position if visible.
222 */
223 protected int cursorY;
224
225 /**
226 * Get the attributes at one location.
227 *
228 * @param x column coordinate. 0 is the left-most column.
229 * @param y row coordinate. 0 is the top-most row.
230 * @return attributes at (x, y)
231 */
232 public final CellAttributes getAttrXY(final int x, final int y) {
233 CellAttributes attr = new CellAttributes();
234 if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
235 attr.setTo(logical[x][y]);
236 }
237 return attr;
238 }
239
240 /**
241 * Get the cell at one location.
242 *
243 * @param x column coordinate. 0 is the left-most column.
244 * @param y row coordinate. 0 is the top-most row.
245 * @return the character + attributes
246 */
247 public Cell getCharXY(final int x, final int y) {
248 Cell cell = new Cell();
249 if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
250 cell.setTo(logical[x][y]);
251 }
252 return cell;
253 }
254
255 /**
256 * Set the attributes at one location.
257 *
258 * @param x column coordinate. 0 is the left-most column.
259 * @param y row coordinate. 0 is the top-most row.
260 * @param attr attributes to use (bold, foreColor, backColor)
261 */
262 public final void putAttrXY(final int x, final int y,
263 final CellAttributes attr) {
264
265 putAttrXY(x, y, attr, true);
266 }
267
268 /**
269 * Set the attributes at one location.
270 *
271 * @param x column coordinate. 0 is the left-most column.
272 * @param y row coordinate. 0 is the top-most row.
273 * @param attr attributes to use (bold, foreColor, backColor)
274 * @param clip if true, honor clipping/offset
275 */
276 public final void putAttrXY(final int x, final int y,
277 final CellAttributes attr, final boolean clip) {
278
279 int X = x;
280 int Y = y;
281
282 if (clip) {
283 if ((x < clipLeft)
284 || (x >= clipRight)
285 || (y < clipTop)
286 || (y >= clipBottom)
287 ) {
288 return;
289 }
290 X += offsetX;
291 Y += offsetY;
292 }
293
294 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
295 logical[X][Y].setTo(attr);
296
297 // If this happens to be the cursor position, make the position
298 // dirty.
299 if ((cursorX == X) && (cursorY == Y)) {
300 if (physical[cursorX][cursorY].getChar() == 'Q') {
301 physical[cursorX][cursorY].setChar('X');
302 } else {
303 physical[cursorX][cursorY].setChar('Q');
304 }
305 }
306 }
307 }
308
309 /**
310 * Fill the entire screen with one character with attributes.
311 *
312 * @param ch character to draw
313 * @param attr attributes to use (bold, foreColor, backColor)
314 */
315 public final void putAll(final char ch, final CellAttributes attr) {
316
317 for (int x = 0; x < width; x++) {
318 for (int y = 0; y < height; y++) {
319 putCharXY(x, y, ch, attr);
320 }
321 }
322 }
323
324 /**
325 * Render one character with attributes.
326 *
327 * @param x column coordinate. 0 is the left-most column.
328 * @param y row coordinate. 0 is the top-most row.
329 * @param ch character + attributes to draw
330 */
331 public final void putCharXY(final int x, final int y, final Cell ch) {
332 putCharXY(x, y, ch.getChar(), ch);
333 }
334
335 /**
336 * Render one character with attributes.
337 *
338 * @param x column coordinate. 0 is the left-most column.
339 * @param y row coordinate. 0 is the top-most row.
340 * @param ch character to draw
341 * @param attr attributes to use (bold, foreColor, backColor)
342 */
343 public final void putCharXY(final int x, final int y, final char ch,
344 final CellAttributes attr) {
345
346 if ((x < clipLeft)
347 || (x >= clipRight)
348 || (y < clipTop)
349 || (y >= clipBottom)
350 ) {
351 return;
352 }
353
354 int X = x + offsetX;
355 int Y = y + offsetY;
356
357 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
358
359 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
360
361 // Do not put control characters on the display
362 assert (ch >= 0x20);
363 assert (ch != 0x7F);
364
365 logical[X][Y].setTo(attr);
366 logical[X][Y].setChar(ch);
367
368 // If this happens to be the cursor position, make the position
369 // dirty.
370 if ((cursorX == X) && (cursorY == Y)) {
371 if (physical[cursorX][cursorY].getChar() == 'Q') {
372 physical[cursorX][cursorY].setChar('X');
373 } else {
374 physical[cursorX][cursorY].setChar('Q');
375 }
376 }
377 }
378 }
379
380 /**
381 * Render one character without changing the underlying attributes.
382 *
383 * @param x column coordinate. 0 is the left-most column.
384 * @param y row coordinate. 0 is the top-most row.
385 * @param ch character to draw
386 */
387 public final void putCharXY(final int x, final int y, final char ch) {
388
389 if ((x < clipLeft)
390 || (x >= clipRight)
391 || (y < clipTop)
392 || (y >= clipBottom)
393 ) {
394 return;
395 }
396
397 int X = x + offsetX;
398 int Y = y + offsetY;
399
400 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
401
402 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
403 logical[X][Y].setChar(ch);
404
405 // If this happens to be the cursor position, make the position
406 // dirty.
407 if ((cursorX == X) && (cursorY == Y)) {
408 if (physical[cursorX][cursorY].getChar() == 'Q') {
409 physical[cursorX][cursorY].setChar('X');
410 } else {
411 physical[cursorX][cursorY].setChar('Q');
412 }
413 }
414 }
415 }
416
417 /**
418 * Render a string. Does not wrap if the string exceeds the line.
419 *
420 * @param x column coordinate. 0 is the left-most column.
421 * @param y row coordinate. 0 is the top-most row.
422 * @param str string to draw
423 * @param attr attributes to use (bold, foreColor, backColor)
424 */
425 public final void putStringXY(final int x, final int y, final String str,
426 final CellAttributes attr) {
427
428 int i = x;
429 for (int j = 0; j < str.length(); j++) {
430 char ch = str.charAt(j);
431 putCharXY(i, y, ch, attr);
432 i++;
433 if (i == width) {
434 break;
435 }
436 }
437 }
438
439 /**
440 * Render a string without changing the underlying attribute. Does not
441 * wrap if the string exceeds the line.
442 *
443 * @param x column coordinate. 0 is the left-most column.
444 * @param y row coordinate. 0 is the top-most row.
445 * @param str string to draw
446 */
447 public final void putStringXY(final int x, final int y, final String str) {
448
449 int i = x;
450 for (int j = 0; j < str.length(); j++) {
451 char ch = str.charAt(j);
452 putCharXY(i, y, ch);
453 i++;
454 if (i == width) {
455 break;
456 }
457 }
458 }
459
460 /**
461 * Draw a vertical line from (x, y) to (x, y + n).
462 *
463 * @param x column coordinate. 0 is the left-most column.
464 * @param y row coordinate. 0 is the top-most row.
465 * @param n number of characters to draw
466 * @param ch character to draw
467 * @param attr attributes to use (bold, foreColor, backColor)
468 */
469 public final void vLineXY(final int x, final int y, final int n,
470 final char ch, final CellAttributes attr) {
471
472 for (int i = y; i < y + n; i++) {
473 putCharXY(x, i, ch, attr);
474 }
475 }
476
477 /**
478 * Draw a horizontal line from (x, y) to (x + n, y).
479 *
480 * @param x column coordinate. 0 is the left-most column.
481 * @param y row coordinate. 0 is the top-most row.
482 * @param n number of characters to draw
483 * @param ch character to draw
484 * @param attr attributes to use (bold, foreColor, backColor)
485 */
486 public final void hLineXY(final int x, final int y, final int n,
487 final char ch, final CellAttributes attr) {
488
489 for (int i = x; i < x + n; i++) {
490 putCharXY(i, y, ch, attr);
491 }
492 }
493
494 /**
495 * Reallocate screen buffers.
496 *
497 * @param width new width
498 * @param height new height
499 */
500 private synchronized void reallocate(final int width, final int height) {
501 if (logical != null) {
502 for (int row = 0; row < this.height; row++) {
503 for (int col = 0; col < this.width; col++) {
504 logical[col][row] = null;
505 }
506 }
507 logical = null;
508 }
509 logical = new Cell[width][height];
510 if (physical != null) {
511 for (int row = 0; row < this.height; row++) {
512 for (int col = 0; col < this.width; col++) {
513 physical[col][row] = null;
514 }
515 }
516 physical = null;
517 }
518 physical = new Cell[width][height];
519
520 for (int row = 0; row < height; row++) {
521 for (int col = 0; col < width; col++) {
522 physical[col][row] = new Cell();
523 logical[col][row] = new Cell();
524 }
525 }
526
527 this.width = width;
528 this.height = height;
529
530 clipLeft = 0;
531 clipTop = 0;
532 clipRight = width;
533 clipBottom = height;
534
535 reallyCleared = true;
536 }
537
538 /**
539 * Change the width. Everything on-screen will be destroyed and must be
540 * redrawn.
541 *
542 * @param width new screen width
543 */
544 public final synchronized void setWidth(final int width) {
545 reallocate(width, this.height);
546 }
547
548 /**
549 * Change the height. Everything on-screen will be destroyed and must be
550 * redrawn.
551 *
552 * @param height new screen height
553 */
554 public final synchronized void setHeight(final int height) {
555 reallocate(this.width, height);
556 }
557
558 /**
559 * Change the width and height. Everything on-screen will be destroyed
560 * and must be redrawn.
561 *
562 * @param width new screen width
563 * @param height new screen height
564 */
565 public final void setDimensions(final int width, final int height) {
566 reallocate(width, height);
567 }
568
569 /**
570 * Get the height.
571 *
572 * @return current screen height
573 */
574 public final synchronized int getHeight() {
575 return this.height;
576 }
577
578 /**
579 * Get the width.
580 *
581 * @return current screen width
582 */
583 public final synchronized int getWidth() {
584 return this.width;
585 }
586
587 /**
588 * Public constructor. Sets everything to not-bold, white-on-black.
589 */
590 protected LogicalScreen() {
591 offsetX = 0;
592 offsetY = 0;
593 width = 80;
594 height = 24;
595 logical = null;
596 physical = null;
597 reallocate(width, height);
598 }
599
600 /**
601 * Reset screen to not-bold, white-on-black. Also flushes the offset and
602 * clip variables.
603 */
604 public final synchronized void reset() {
605 for (int row = 0; row < height; row++) {
606 for (int col = 0; col < width; col++) {
607 logical[col][row].reset();
608 }
609 }
610 resetClipping();
611 }
612
613 /**
614 * Flush the offset and clip variables.
615 */
616 public final void resetClipping() {
617 offsetX = 0;
618 offsetY = 0;
619 clipLeft = 0;
620 clipTop = 0;
621 clipRight = width;
622 clipBottom = height;
623 }
624
625 /**
626 * Clear the logical screen.
627 */
628 public final void clear() {
629 reset();
630 }
631
632 /**
633 * Clear the physical screen.
634 */
635 public final void clearPhysical() {
636 for (int row = 0; row < height; row++) {
637 for (int col = 0; col < width; col++) {
638 physical[col][row].reset();
639 }
640 }
641 }
642
643 /**
644 * Draw a box with a border and empty background.
645 *
646 * @param left left column of box. 0 is the left-most row.
647 * @param top top row of the box. 0 is the top-most row.
648 * @param right right column of box
649 * @param bottom bottom row of the box
650 * @param border attributes to use for the border
651 * @param background attributes to use for the background
652 */
653 public final void drawBox(final int left, final int top,
654 final int right, final int bottom,
655 final CellAttributes border, final CellAttributes background) {
656
657 drawBox(left, top, right, bottom, border, background, 1, false);
658 }
659
660 /**
661 * Draw a box with a border and empty background.
662 *
663 * @param left left column of box. 0 is the left-most row.
664 * @param top top row of the box. 0 is the top-most row.
665 * @param right right column of box
666 * @param bottom bottom row of the box
667 * @param border attributes to use for the border
668 * @param background attributes to use for the background
669 * @param borderType if 1, draw a single-line border; if 2, draw a
670 * double-line border; if 3, draw double-line top/bottom edges and
671 * single-line left/right edges (like Qmodem)
672 * @param shadow if true, draw a "shadow" on the box
673 */
674 public final void drawBox(final int left, final int top,
675 final int right, final int bottom,
676 final CellAttributes border, final CellAttributes background,
677 final int borderType, final boolean shadow) {
678
679 int boxWidth = right - left;
680 int boxHeight = bottom - top;
681
682 char cTopLeft;
683 char cTopRight;
684 char cBottomLeft;
685 char cBottomRight;
686 char cHSide;
687 char cVSide;
688
689 switch (borderType) {
690 case 1:
691 cTopLeft = GraphicsChars.ULCORNER;
692 cTopRight = GraphicsChars.URCORNER;
693 cBottomLeft = GraphicsChars.LLCORNER;
694 cBottomRight = GraphicsChars.LRCORNER;
695 cHSide = GraphicsChars.SINGLE_BAR;
696 cVSide = GraphicsChars.WINDOW_SIDE;
697 break;
698
699 case 2:
700 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP_DOUBLE;
701 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP_DOUBLE;
702 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM_DOUBLE;
703 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM_DOUBLE;
704 cHSide = GraphicsChars.DOUBLE_BAR;
705 cVSide = GraphicsChars.WINDOW_SIDE_DOUBLE;
706 break;
707
708 case 3:
709 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP;
710 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP;
711 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM;
712 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM;
713 cHSide = GraphicsChars.WINDOW_TOP;
714 cVSide = GraphicsChars.WINDOW_SIDE;
715 break;
716 default:
717 throw new IllegalArgumentException("Invalid border type: "
718 + borderType);
719 }
720
721 // Place the corner characters
722 putCharXY(left, top, cTopLeft, border);
723 putCharXY(left + boxWidth - 1, top, cTopRight, border);
724 putCharXY(left, top + boxHeight - 1, cBottomLeft, border);
725 putCharXY(left + boxWidth - 1, top + boxHeight - 1, cBottomRight,
726 border);
727
728 // Draw the box lines
729 hLineXY(left + 1, top, boxWidth - 2, cHSide, border);
730 vLineXY(left, top + 1, boxHeight - 2, cVSide, border);
731 hLineXY(left + 1, top + boxHeight - 1, boxWidth - 2, cHSide, border);
732 vLineXY(left + boxWidth - 1, top + 1, boxHeight - 2, cVSide, border);
733
734 // Fill in the interior background
735 for (int i = 1; i < boxHeight - 1; i++) {
736 hLineXY(1 + left, i + top, boxWidth - 2, ' ', background);
737 }
738
739 if (shadow) {
740 // Draw a shadow
741 drawBoxShadow(left, top, right, bottom);
742 }
743 }
744
745 /**
746 * Draw a box shadow.
747 *
748 * @param left left column of box. 0 is the left-most row.
749 * @param top top row of the box. 0 is the top-most row.
750 * @param right right column of box
751 * @param bottom bottom row of the box
752 */
753 public final void drawBoxShadow(final int left, final int top,
754 final int right, final int bottom) {
755
756 int boxTop = top;
757 int boxLeft = left;
758 int boxWidth = right - left;
759 int boxHeight = bottom - top;
760 CellAttributes shadowAttr = new CellAttributes();
761
762 // Shadows do not honor clipping but they DO honor offset.
763 int oldClipRight = clipRight;
764 int oldClipBottom = clipBottom;
765 /*
766 clipRight = boxWidth + 2;
767 clipBottom = boxHeight + 1;
768 */
769 clipRight = width;
770 clipBottom = height;
771
772 for (int i = 0; i < boxHeight; i++) {
773 putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
774 putAttrXY(boxLeft + boxWidth + 1, boxTop + 1 + i, shadowAttr);
775 }
776 for (int i = 0; i < boxWidth; i++) {
777 putAttrXY(boxLeft + 2 + i, boxTop + boxHeight, shadowAttr);
778 }
779 clipRight = oldClipRight;
780 clipBottom = oldClipBottom;
781 }
782
783 /**
784 * Default implementation does nothing.
785 */
786 public void flushPhysical() {}
787
788 /**
789 * Put the cursor at (x,y).
790 *
791 * @param visible if true, the cursor should be visible
792 * @param x column coordinate to put the cursor on
793 * @param y row coordinate to put the cursor on
794 */
795 public void putCursor(final boolean visible, final int x, final int y) {
796 if ((cursorY >= 0)
797 && (cursorX >= 0)
798 && (cursorY <= height - 1)
799 && (cursorX <= width - 1)
800 ) {
801 // Make the current cursor position dirty
802 if (physical[cursorX][cursorY].getChar() == 'Q') {
803 physical[cursorX][cursorY].setChar('X');
804 } else {
805 physical[cursorX][cursorY].setChar('Q');
806 }
807 }
808
809 cursorVisible = visible;
810 cursorX = x;
811 cursorY = y;
812 }
813
814 /**
815 * Hide the cursor.
816 */
817 public final void hideCursor() {
818 cursorVisible = false;
819 }
820
821 /**
822 * Get the cursor visibility.
823 *
824 * @return true if the cursor is visible
825 */
826 public boolean isCursorVisible() {
827 return cursorVisible;
828 }
829
830 /**
831 * Get the cursor X position.
832 *
833 * @return the cursor x column position
834 */
835 public int getCursorX() {
836 return cursorX;
837 }
838
839 /**
840 * Get the cursor Y position.
841 *
842 * @return the cursor y row position
843 */
844 public int getCursorY() {
845 return cursorY;
846 }
847
848 /**
849 * Set the window title. Default implementation does nothing.
850 *
851 * @param title the new title
852 */
853 public void setTitle(final String title) {}
854
855 }