e3f72191809b73565397c98c8b8523c800b9f8be
[fanfix.git] / Screen.java
1 /**
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer.io;
32
33 import jexer.bits.Cell;
34 import jexer.bits.CellAttributes;
35 import jexer.bits.GraphicsChars;
36
37 /**
38 * This class represents a text-based screen. Drawing operations write to a
39 * logical screen.
40 */
41 public abstract class Screen {
42
43 /**
44 * Width of the visible window.
45 */
46 protected int width;
47
48 /**
49 * Height of the visible window.
50 */
51 protected int height;
52
53 /**
54 * Drawing offset for x.
55 */
56 private int offsetX;
57
58 /**
59 * Set drawing offset for x.
60 *
61 * @param offsetX new drawing offset
62 */
63 public final void setOffsetX(final int offsetX) {
64 this.offsetX = offsetX;
65 }
66
67 /**
68 * Drawing offset for y.
69 */
70 private int offsetY;
71
72 /**
73 * Set drawing offset for y.
74 *
75 * @param offsetY new drawing offset
76 */
77 public final void setOffsetY(final int offsetY) {
78 this.offsetY = offsetY;
79 }
80
81 /**
82 * Ignore anything drawn right of clipRight.
83 */
84 private int clipRight;
85
86 /**
87 * Get right drawing clipping boundary.
88 *
89 * @return drawing boundary
90 */
91 public final int getClipRight() {
92 return clipRight;
93 }
94
95 /**
96 * Set right drawing clipping boundary.
97 *
98 * @param clipRight new boundary
99 */
100 public final void setClipRight(final int clipRight) {
101 this.clipRight = clipRight;
102 }
103
104 /**
105 * Ignore anything drawn below clipBottom.
106 */
107 private int clipBottom;
108
109 /**
110 * Get bottom drawing clipping boundary.
111 *
112 * @return drawing boundary
113 */
114 public final int getClipBottom() {
115 return clipBottom;
116 }
117
118 /**
119 * Set bottom drawing clipping boundary.
120 *
121 * @param clipBottom new boundary
122 */
123 public final void setClipBottom(final int clipBottom) {
124 this.clipBottom = clipBottom;
125 }
126
127 /**
128 * Ignore anything drawn left of clipLeft.
129 */
130 private int clipLeft;
131
132 /**
133 * Get left drawing clipping boundary.
134 *
135 * @return drawing boundary
136 */
137 public final int getClipLeft() {
138 return clipLeft;
139 }
140
141 /**
142 * Set left drawing clipping boundary.
143 *
144 * @param clipLeft new boundary
145 */
146 public final void setClipLeft(final int clipLeft) {
147 this.clipLeft = clipLeft;
148 }
149
150 /**
151 * Ignore anything drawn above clipTop.
152 */
153 private int clipTop;
154
155 /**
156 * Get top drawing clipping boundary.
157 *
158 * @return drawing boundary
159 */
160 public final int getClipTop() {
161 return clipTop;
162 }
163
164 /**
165 * Set top drawing clipping boundary.
166 *
167 * @param clipTop new boundary
168 */
169 public final void setClipTop(final int clipTop) {
170 this.clipTop = clipTop;
171 }
172
173 /**
174 * The physical screen last sent out on flush().
175 */
176 protected Cell [][] physical;
177
178 /**
179 * The logical screen being rendered to.
180 */
181 protected Cell [][] logical;
182
183 /**
184 * When true, logical != physical.
185 */
186 protected boolean dirty;
187
188 /**
189 * Get dirty flag.
190 *
191 * @return if true, the logical screen is not in sync with the physical
192 * screen
193 */
194 public final boolean isDirty() {
195 return dirty;
196 }
197
198 /**
199 * Set if the user explicitly wants to redraw everything starting with a
200 * ECMATerminal.clearAll().
201 */
202 protected boolean reallyCleared;
203
204 /**
205 * If true, the cursor is visible and should be placed onscreen at
206 * (cursorX, cursorY) during a call to flushPhysical().
207 */
208 protected boolean cursorVisible;
209
210 /**
211 * Cursor X position if visible.
212 */
213 protected int cursorX;
214
215 /**
216 * Cursor Y position if visible.
217 */
218 protected int cursorY;
219
220 /**
221 * Get the attributes at one location.
222 *
223 * @param x column coordinate. 0 is the left-most column.
224 * @param y row coordinate. 0 is the top-most row.
225 * @return attributes at (x, y)
226 */
227 public final CellAttributes getAttrXY(final int x, final int y) {
228
229 CellAttributes attr = new CellAttributes();
230 if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
231 attr.setTo(logical[x][y]);
232 }
233 return attr;
234 }
235
236 /**
237 * Set the attributes at one location.
238 *
239 * @param x column coordinate. 0 is the left-most column.
240 * @param y row coordinate. 0 is the top-most row.
241 * @param attr attributes to use (bold, foreColor, backColor)
242 */
243 public final void putAttrXY(final int x, final int y,
244 final CellAttributes attr) {
245
246 putAttrXY(x, y, attr, true);
247 }
248
249 /**
250 * Set the attributes at one location.
251 *
252 * @param x column coordinate. 0 is the left-most column.
253 * @param y row coordinate. 0 is the top-most row.
254 * @param attr attributes to use (bold, foreColor, backColor)
255 * @param clip if true, honor clipping/offset
256 */
257 public final void putAttrXY(final int x, final int y
258 , final CellAttributes attr, final boolean clip) {
259
260 int X = x;
261 int Y = y;
262
263 if (clip) {
264 if ((x < clipLeft)
265 || (x >= clipRight)
266 || (y < clipTop)
267 || (y >= clipBottom)
268 ) {
269 return;
270 }
271 X += offsetX;
272 Y += offsetY;
273 }
274
275 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
276 dirty = true;
277 logical[X][Y].setForeColor(attr.getForeColor());
278 logical[X][Y].setBackColor(attr.getBackColor());
279 logical[X][Y].setBold(attr.getBold());
280 logical[X][Y].setBlink(attr.getBlink());
281 logical[X][Y].setReverse(attr.getReverse());
282 logical[X][Y].setUnderline(attr.getUnderline());
283 logical[X][Y].setProtect(attr.getProtect());
284 }
285 }
286
287 /**
288 * Fill the entire screen with one character with attributes.
289 *
290 * @param ch character to draw
291 * @param attr attributes to use (bold, foreColor, backColor)
292 */
293 public final void putAll(final char ch, final CellAttributes attr) {
294
295 for (int x = 0; x < width; x++) {
296 for (int y = 0; y < height; y++) {
297 putCharXY(x, y, ch, attr);
298 }
299 }
300 }
301
302 /**
303 * Render one character with attributes.
304 *
305 * @param x column coordinate. 0 is the left-most column.
306 * @param y row coordinate. 0 is the top-most row.
307 * @param ch character + attributes to draw
308 */
309 public final void putCharXY(final int x, final int y, final Cell ch) {
310 putCharXY(x, y, ch.getChar(), ch);
311 }
312
313 /**
314 * Render one character with attributes.
315 *
316 * @param x column coordinate. 0 is the left-most column.
317 * @param y row coordinate. 0 is the top-most row.
318 * @param ch character to draw
319 * @param attr attributes to use (bold, foreColor, backColor)
320 */
321 public final void putCharXY(final int x, final int y, final char ch,
322 final CellAttributes attr) {
323
324 if ((x < clipLeft)
325 || (x >= clipRight)
326 || (y < clipTop)
327 || (y >= clipBottom)
328 ) {
329 return;
330 }
331
332 int X = x + offsetX;
333 int Y = y + offsetY;
334
335 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
336
337 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
338 dirty = true;
339
340 // Do not put control characters on the display
341 assert (ch >= 0x20);
342 assert (ch != 0x7F);
343
344 logical[X][Y].setChar(ch);
345 logical[X][Y].setForeColor(attr.getForeColor());
346 logical[X][Y].setBackColor(attr.getBackColor());
347 logical[X][Y].setBold(attr.getBold());
348 logical[X][Y].setBlink(attr.getBlink());
349 logical[X][Y].setReverse(attr.getReverse());
350 logical[X][Y].setUnderline(attr.getUnderline());
351 logical[X][Y].setProtect(attr.getProtect());
352 }
353 }
354
355 /**
356 * Render one character without changing the underlying attributes.
357 *
358 * @param x column coordinate. 0 is the left-most column.
359 * @param y row coordinate. 0 is the top-most row.
360 * @param ch character to draw
361 */
362 public final void putCharXY(final int x, final int y, final char ch) {
363
364 if ((x < clipLeft)
365 || (x >= clipRight)
366 || (y < clipTop)
367 || (y >= clipBottom)
368 ) {
369 return;
370 }
371
372 int X = x + offsetX;
373 int Y = y + offsetY;
374
375 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
376
377 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
378 dirty = true;
379 logical[X][Y].setChar(ch);
380 }
381 }
382
383 /**
384 * Render a string. Does not wrap if the string exceeds the line.
385 *
386 * @param x column coordinate. 0 is the left-most column.
387 * @param y row coordinate. 0 is the top-most row.
388 * @param str string to draw
389 * @param attr attributes to use (bold, foreColor, backColor)
390 */
391 public final void putStrXY(final int x, final int y, final String str,
392 final CellAttributes attr) {
393
394 int i = x;
395 for (int j = 0; j < str.length(); j++) {
396 char ch = str.charAt(j);
397 putCharXY(i, y, ch, attr);
398 i++;
399 if (i == width) {
400 break;
401 }
402 }
403 }
404
405 /**
406 * Render a string without changing the underlying attribute. Does not
407 * wrap if the string exceeds the line.
408 *
409 * @param x column coordinate. 0 is the left-most column.
410 * @param y row coordinate. 0 is the top-most row.
411 * @param str string to draw
412 */
413 public final void putStrXY(final int x, final int y, final String str) {
414
415 int i = x;
416 for (int j = 0; j < str.length(); j++) {
417 char ch = str.charAt(j);
418 putCharXY(i, y, ch);
419 i++;
420 if (i == width) {
421 break;
422 }
423 }
424 }
425
426 /**
427 * Draw a vertical line from (x, y) to (x, y + n).
428 *
429 * @param x column coordinate. 0 is the left-most column.
430 * @param y row coordinate. 0 is the top-most row.
431 * @param n number of characters to draw
432 * @param ch character to draw
433 * @param attr attributes to use (bold, foreColor, backColor)
434 */
435 public final void vLineXY(final int x, final int y, final int n,
436 final char ch, final CellAttributes attr) {
437
438 for (int i = y; i < y + n; i++) {
439 putCharXY(x, i, ch, attr);
440 }
441 }
442
443 /**
444 * Draw a horizontal line from (x, y) to (x + n, y).
445 *
446 * @param x column coordinate. 0 is the left-most column.
447 * @param y row coordinate. 0 is the top-most row.
448 * @param n number of characters to draw
449 * @param ch character to draw
450 * @param attr attributes to use (bold, foreColor, backColor)
451 */
452 public final void hLineXY(final int x, final int y, final int n,
453 final char ch, final CellAttributes attr) {
454
455 for (int i = x; i < x + n; i++) {
456 putCharXY(i, y, ch, attr);
457 }
458 }
459
460 /**
461 * Reallocate screen buffers.
462 *
463 * @param width new width
464 * @param height new height
465 */
466 private synchronized void reallocate(final int width, final int height) {
467 if (logical != null) {
468 for (int row = 0; row < this.height; row++) {
469 for (int col = 0; col < this.width; col++) {
470 logical[col][row] = null;
471 }
472 }
473 logical = null;
474 }
475 logical = new Cell[width][height];
476 if (physical != null) {
477 for (int row = 0; row < this.height; row++) {
478 for (int col = 0; col < this.width; col++) {
479 physical[col][row] = null;
480 }
481 }
482 physical = null;
483 }
484 physical = new Cell[width][height];
485
486 for (int row = 0; row < height; row++) {
487 for (int col = 0; col < width; col++) {
488 physical[col][row] = new Cell();
489 logical[col][row] = new Cell();
490 }
491 }
492
493 this.width = width;
494 this.height = height;
495
496 clipLeft = 0;
497 clipTop = 0;
498 clipRight = width;
499 clipBottom = height;
500
501 reallyCleared = true;
502 dirty = true;
503 }
504
505 /**
506 * Change the width. Everything on-screen will be destroyed and must be
507 * redrawn.
508 *
509 * @param width new screen width
510 */
511 public final synchronized void setWidth(final int width) {
512 reallocate(width, this.height);
513 }
514
515 /**
516 * Change the height. Everything on-screen will be destroyed and must be
517 * redrawn.
518 *
519 * @param height new screen height
520 */
521 public final synchronized void setHeight(final int height) {
522 reallocate(this.width, height);
523 }
524
525 /**
526 * Change the width and height. Everything on-screen will be destroyed
527 * and must be redrawn.
528 *
529 * @param width new screen width
530 * @param height new screen height
531 */
532 public final void setDimensions(final int width, final int height) {
533 reallocate(width, height);
534 }
535
536 /**
537 * Get the height.
538 *
539 * @return current screen height
540 */
541 public final synchronized int getHeight() {
542 return this.height;
543 }
544
545 /**
546 * Get the width.
547 *
548 * @return current screen width
549 */
550 public final synchronized int getWidth() {
551 return this.width;
552 }
553
554 /**
555 * Public constructor. Sets everything to not-bold, white-on-black.
556 */
557 protected Screen() {
558 offsetX = 0;
559 offsetY = 0;
560 width = 80;
561 height = 24;
562 logical = null;
563 physical = null;
564 reallocate(width, height);
565 }
566
567 /**
568 * Reset screen to not-bold, white-on-black. Also flushes the offset and
569 * clip variables.
570 */
571 public final synchronized void reset() {
572 dirty = true;
573 for (int row = 0; row < height; row++) {
574 for (int col = 0; col < width; col++) {
575 logical[col][row].reset();
576 }
577 }
578 resetClipping();
579 }
580
581 /**
582 * Flush the offset and clip variables.
583 */
584 public final void resetClipping() {
585 offsetX = 0;
586 offsetY = 0;
587 clipLeft = 0;
588 clipTop = 0;
589 clipRight = width;
590 clipBottom = height;
591 }
592
593 /**
594 * Force the screen to be fully cleared and redrawn on the next flush().
595 */
596 public final void clear() {
597 reset();
598 }
599
600 /**
601 * Draw a box with a border and empty background.
602 *
603 * @param left left column of box. 0 is the left-most row.
604 * @param top top row of the box. 0 is the top-most row.
605 * @param right right column of box
606 * @param bottom bottom row of the box
607 * @param border attributes to use for the border
608 * @param background attributes to use for the background
609 */
610 public final void drawBox(final int left, final int top,
611 final int right, final int bottom,
612 final CellAttributes border, final CellAttributes background) {
613
614 drawBox(left, top, right, bottom, border, background, 1, false);
615 }
616
617 /**
618 * Draw a box with a border and empty background.
619 *
620 * @param left left column of box. 0 is the left-most row.
621 * @param top top row of the box. 0 is the top-most row.
622 * @param right right column of box
623 * @param bottom bottom row of the box
624 * @param border attributes to use for the border
625 * @param background attributes to use for the background
626 * @param borderType if 1, draw a single-line border; if 2, draw a
627 * double-line border; if 3, draw double-line top/bottom edges and
628 * single-line left/right edges (like Qmodem)
629 * @param shadow if true, draw a "shadow" on the box
630 */
631 public final void drawBox(final int left, final int top,
632 final int right, final int bottom,
633 final CellAttributes border, final CellAttributes background,
634 final int borderType, final boolean shadow) {
635
636 int boxTop = top;
637 int boxLeft = left;
638 int boxWidth = right - left;
639 int boxHeight = bottom - top;
640
641 char cTopLeft;
642 char cTopRight;
643 char cBottomLeft;
644 char cBottomRight;
645 char cHSide;
646 char cVSide;
647
648 switch (borderType) {
649 case 1:
650 cTopLeft = GraphicsChars.ULCORNER;
651 cTopRight = GraphicsChars.URCORNER;
652 cBottomLeft = GraphicsChars.LLCORNER;
653 cBottomRight = GraphicsChars.LRCORNER;
654 cHSide = GraphicsChars.SINGLE_BAR;
655 cVSide = GraphicsChars.WINDOW_SIDE;
656 break;
657
658 case 2:
659 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP_DOUBLE;
660 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP_DOUBLE;
661 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM_DOUBLE;
662 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM_DOUBLE;
663 cHSide = GraphicsChars.DOUBLE_BAR;
664 cVSide = GraphicsChars.WINDOW_SIDE_DOUBLE;
665 break;
666
667 case 3:
668 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP;
669 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP;
670 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM;
671 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM;
672 cHSide = GraphicsChars.WINDOW_TOP;
673 cVSide = GraphicsChars.WINDOW_SIDE;
674 break;
675 default:
676 throw new IllegalArgumentException("Invalid border type: "
677 + borderType);
678 }
679
680 // Place the corner characters
681 putCharXY(left, top, cTopLeft, border);
682 putCharXY(left + boxWidth - 1, top, cTopRight, border);
683 putCharXY(left, top + boxHeight - 1, cBottomLeft, border);
684 putCharXY(left + boxWidth - 1, top + boxHeight - 1, cBottomRight,
685 border);
686
687 // Draw the box lines
688 hLineXY(left + 1, top, boxWidth - 2, cHSide, border);
689 vLineXY(left, top + 1, boxHeight - 2, cVSide, border);
690 hLineXY(left + 1, top + boxHeight - 1, boxWidth - 2, cHSide, border);
691 vLineXY(left + boxWidth - 1, top + 1, boxHeight - 2, cVSide, border);
692
693 // Fill in the interior background
694 for (int i = 1; i < boxHeight - 1; i++) {
695 hLineXY(1 + left, i + top, boxWidth - 2, ' ', background);
696 }
697
698 if (shadow) {
699 // Draw a shadow
700 drawBoxShadow(left, top, right, bottom);
701 }
702 }
703
704 /**
705 * Draw a box shadow.
706 *
707 * @param left left column of box. 0 is the left-most row.
708 * @param top top row of the box. 0 is the top-most row.
709 * @param right right column of box
710 * @param bottom bottom row of the box
711 */
712 public final void drawBoxShadow(final int left, final int top,
713 final int right, final int bottom) {
714
715 int boxTop = top;
716 int boxLeft = left;
717 int boxWidth = right - left;
718 int boxHeight = bottom - top;
719 CellAttributes shadowAttr = new CellAttributes();
720
721 // Shadows do not honor clipping but they DO honor offset.
722 int oldClipRight = clipRight;
723 int oldClipBottom = clipBottom;
724 /*
725 clipRight = boxWidth + 2;
726 clipBottom = boxHeight + 1;
727 */
728 clipRight = width;
729 clipBottom = height;
730
731 for (int i = 0; i < boxHeight; i++) {
732 putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
733 putAttrXY(boxLeft + boxWidth + 1, boxTop + 1 + i, shadowAttr);
734 }
735 for (int i = 0; i < boxWidth; i++) {
736 putAttrXY(boxLeft + 2 + i, boxTop + boxHeight, shadowAttr);
737 }
738 clipRight = oldClipRight;
739 clipBottom = oldClipBottom;
740 }
741
742 /**
743 * Subclasses must provide an implementation to push the logical screen
744 * to the physical device.
745 */
746 public abstract void flushPhysical();
747
748 /**
749 * Put the cursor at (x,y).
750 *
751 * @param visible if true, the cursor should be visible
752 * @param x column coordinate to put the cursor on
753 * @param y row coordinate to put the cursor on
754 */
755 public void putCursor(final boolean visible, final int x, final int y) {
756
757 cursorVisible = visible;
758 cursorX = x;
759 cursorY = y;
760 }
761
762 /**
763 * Hide the cursor.
764 */
765 public final void hideCursor() {
766 cursorVisible = false;
767 }
768 }