make Windows look like X11, why does this work
[fanfix.git] / src / jexer / io / 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 volatile 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 CellAttributes attr = new CellAttributes();
229 if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
230 attr.setTo(logical[x][y]);
231 }
232 return attr;
233 }
234
235 /**
236 * Set the attributes at one location.
237 *
238 * @param x column coordinate. 0 is the left-most column.
239 * @param y row coordinate. 0 is the top-most row.
240 * @param attr attributes to use (bold, foreColor, backColor)
241 */
242 public final void putAttrXY(final int x, final int y,
243 final CellAttributes attr) {
244
245 putAttrXY(x, y, attr, true);
246 }
247
248 /**
249 * Set the attributes at one location.
250 *
251 * @param x column coordinate. 0 is the left-most column.
252 * @param y row coordinate. 0 is the top-most row.
253 * @param attr attributes to use (bold, foreColor, backColor)
254 * @param clip if true, honor clipping/offset
255 */
256 public final void putAttrXY(final int x, final int y
257 , final CellAttributes attr, final boolean clip) {
258
259 int X = x;
260 int Y = y;
261
262 if (clip) {
263 if ((x < clipLeft)
264 || (x >= clipRight)
265 || (y < clipTop)
266 || (y >= clipBottom)
267 ) {
268 return;
269 }
270 X += offsetX;
271 Y += offsetY;
272 }
273
274 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
275 dirty = true;
276 logical[X][Y].setForeColor(attr.getForeColor());
277 logical[X][Y].setBackColor(attr.getBackColor());
278 logical[X][Y].setBold(attr.isBold());
279 logical[X][Y].setBlink(attr.isBlink());
280 logical[X][Y].setReverse(attr.isReverse());
281 logical[X][Y].setUnderline(attr.isUnderline());
282 logical[X][Y].setProtect(attr.isProtect());
283 }
284 }
285
286 /**
287 * Fill the entire screen with one character with attributes.
288 *
289 * @param ch character to draw
290 * @param attr attributes to use (bold, foreColor, backColor)
291 */
292 public final void putAll(final char ch, final CellAttributes attr) {
293
294 for (int x = 0; x < width; x++) {
295 for (int y = 0; y < height; y++) {
296 putCharXY(x, y, ch, attr);
297 }
298 }
299 }
300
301 /**
302 * Render one character with attributes.
303 *
304 * @param x column coordinate. 0 is the left-most column.
305 * @param y row coordinate. 0 is the top-most row.
306 * @param ch character + attributes to draw
307 */
308 public final void putCharXY(final int x, final int y, final Cell ch) {
309 putCharXY(x, y, ch.getChar(), ch);
310 }
311
312 /**
313 * Render one character with attributes.
314 *
315 * @param x column coordinate. 0 is the left-most column.
316 * @param y row coordinate. 0 is the top-most row.
317 * @param ch character to draw
318 * @param attr attributes to use (bold, foreColor, backColor)
319 */
320 public final void putCharXY(final int x, final int y, final char ch,
321 final CellAttributes attr) {
322
323 if ((x < clipLeft)
324 || (x >= clipRight)
325 || (y < clipTop)
326 || (y >= clipBottom)
327 ) {
328 return;
329 }
330
331 int X = x + offsetX;
332 int Y = y + offsetY;
333
334 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
335
336 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
337 dirty = true;
338
339 // Do not put control characters on the display
340 assert (ch >= 0x20);
341 assert (ch != 0x7F);
342
343 logical[X][Y].setChar(ch);
344 logical[X][Y].setForeColor(attr.getForeColor());
345 logical[X][Y].setBackColor(attr.getBackColor());
346 logical[X][Y].setBold(attr.isBold());
347 logical[X][Y].setBlink(attr.isBlink());
348 logical[X][Y].setReverse(attr.isReverse());
349 logical[X][Y].setUnderline(attr.isUnderline());
350 logical[X][Y].setProtect(attr.isProtect());
351 }
352 }
353
354 /**
355 * Render one character without changing the underlying attributes.
356 *
357 * @param x column coordinate. 0 is the left-most column.
358 * @param y row coordinate. 0 is the top-most row.
359 * @param ch character to draw
360 */
361 public final void putCharXY(final int x, final int y, final char ch) {
362
363 if ((x < clipLeft)
364 || (x >= clipRight)
365 || (y < clipTop)
366 || (y >= clipBottom)
367 ) {
368 return;
369 }
370
371 int X = x + offsetX;
372 int Y = y + offsetY;
373
374 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
375
376 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
377 dirty = true;
378 logical[X][Y].setChar(ch);
379 }
380 }
381
382 /**
383 * Render a string. Does not wrap if the string exceeds the line.
384 *
385 * @param x column coordinate. 0 is the left-most column.
386 * @param y row coordinate. 0 is the top-most row.
387 * @param str string to draw
388 * @param attr attributes to use (bold, foreColor, backColor)
389 */
390 public final void putStringXY(final int x, final int y, final String str,
391 final CellAttributes attr) {
392
393 int i = x;
394 for (int j = 0; j < str.length(); j++) {
395 char ch = str.charAt(j);
396 putCharXY(i, y, ch, attr);
397 i++;
398 if (i == width) {
399 break;
400 }
401 }
402 }
403
404 /**
405 * Render a string without changing the underlying attribute. Does not
406 * wrap if the string exceeds the line.
407 *
408 * @param x column coordinate. 0 is the left-most column.
409 * @param y row coordinate. 0 is the top-most row.
410 * @param str string to draw
411 */
412 public final void putStringXY(final int x, final int y, final String str) {
413
414 int i = x;
415 for (int j = 0; j < str.length(); j++) {
416 char ch = str.charAt(j);
417 putCharXY(i, y, ch);
418 i++;
419 if (i == width) {
420 break;
421 }
422 }
423 }
424
425 /**
426 * Draw a vertical line from (x, y) to (x, y + n).
427 *
428 * @param x column coordinate. 0 is the left-most column.
429 * @param y row coordinate. 0 is the top-most row.
430 * @param n number of characters to draw
431 * @param ch character to draw
432 * @param attr attributes to use (bold, foreColor, backColor)
433 */
434 public final void vLineXY(final int x, final int y, final int n,
435 final char ch, final CellAttributes attr) {
436
437 for (int i = y; i < y + n; i++) {
438 putCharXY(x, i, ch, attr);
439 }
440 }
441
442 /**
443 * Draw a horizontal line from (x, y) to (x + n, y).
444 *
445 * @param x column coordinate. 0 is the left-most column.
446 * @param y row coordinate. 0 is the top-most row.
447 * @param n number of characters to draw
448 * @param ch character to draw
449 * @param attr attributes to use (bold, foreColor, backColor)
450 */
451 public final void hLineXY(final int x, final int y, final int n,
452 final char ch, final CellAttributes attr) {
453
454 for (int i = x; i < x + n; i++) {
455 putCharXY(i, y, ch, attr);
456 }
457 }
458
459 /**
460 * Reallocate screen buffers.
461 *
462 * @param width new width
463 * @param height new height
464 */
465 private synchronized void reallocate(final int width, final int height) {
466 if (logical != null) {
467 for (int row = 0; row < this.height; row++) {
468 for (int col = 0; col < this.width; col++) {
469 logical[col][row] = null;
470 }
471 }
472 logical = null;
473 }
474 logical = new Cell[width][height];
475 if (physical != null) {
476 for (int row = 0; row < this.height; row++) {
477 for (int col = 0; col < this.width; col++) {
478 physical[col][row] = null;
479 }
480 }
481 physical = null;
482 }
483 physical = new Cell[width][height];
484
485 for (int row = 0; row < height; row++) {
486 for (int col = 0; col < width; col++) {
487 physical[col][row] = new Cell();
488 logical[col][row] = new Cell();
489 }
490 }
491
492 this.width = width;
493 this.height = height;
494
495 clipLeft = 0;
496 clipTop = 0;
497 clipRight = width;
498 clipBottom = height;
499
500 reallyCleared = true;
501 dirty = true;
502 }
503
504 /**
505 * Change the width. Everything on-screen will be destroyed and must be
506 * redrawn.
507 *
508 * @param width new screen width
509 */
510 public final synchronized void setWidth(final int width) {
511 reallocate(width, this.height);
512 }
513
514 /**
515 * Change the height. Everything on-screen will be destroyed and must be
516 * redrawn.
517 *
518 * @param height new screen height
519 */
520 public final synchronized void setHeight(final int height) {
521 reallocate(this.width, height);
522 }
523
524 /**
525 * Change the width and height. Everything on-screen will be destroyed
526 * and must be redrawn.
527 *
528 * @param width new screen width
529 * @param height new screen height
530 */
531 public final void setDimensions(final int width, final int height) {
532 reallocate(width, height);
533 }
534
535 /**
536 * Get the height.
537 *
538 * @return current screen height
539 */
540 public final synchronized int getHeight() {
541 return this.height;
542 }
543
544 /**
545 * Get the width.
546 *
547 * @return current screen width
548 */
549 public final synchronized int getWidth() {
550 return this.width;
551 }
552
553 /**
554 * Public constructor. Sets everything to not-bold, white-on-black.
555 */
556 protected Screen() {
557 offsetX = 0;
558 offsetY = 0;
559 width = 80;
560 height = 24;
561 logical = null;
562 physical = null;
563 reallocate(width, height);
564 }
565
566 /**
567 * Reset screen to not-bold, white-on-black. Also flushes the offset and
568 * clip variables.
569 */
570 public final synchronized void reset() {
571 dirty = true;
572 for (int row = 0; row < height; row++) {
573 for (int col = 0; col < width; col++) {
574 logical[col][row].reset();
575 }
576 }
577 resetClipping();
578 }
579
580 /**
581 * Flush the offset and clip variables.
582 */
583 public final void resetClipping() {
584 offsetX = 0;
585 offsetY = 0;
586 clipLeft = 0;
587 clipTop = 0;
588 clipRight = width;
589 clipBottom = height;
590 }
591
592 /**
593 * Clear the logical screen.
594 */
595 public final void clear() {
596 reset();
597 }
598
599 /**
600 * Clear the physical screen.
601 */
602 public final void clearPhysical() {
603 dirty = true;
604 for (int row = 0; row < height; row++) {
605 for (int col = 0; col < width; col++) {
606 physical[col][row].reset();
607 }
608 }
609 }
610
611 /**
612 * Draw a box with a border and empty background.
613 *
614 * @param left left column of box. 0 is the left-most row.
615 * @param top top row of the box. 0 is the top-most row.
616 * @param right right column of box
617 * @param bottom bottom row of the box
618 * @param border attributes to use for the border
619 * @param background attributes to use for the background
620 */
621 public final void drawBox(final int left, final int top,
622 final int right, final int bottom,
623 final CellAttributes border, final CellAttributes background) {
624
625 drawBox(left, top, right, bottom, border, background, 1, false);
626 }
627
628 /**
629 * Draw a box with a border and empty background.
630 *
631 * @param left left column of box. 0 is the left-most row.
632 * @param top top row of the box. 0 is the top-most row.
633 * @param right right column of box
634 * @param bottom bottom row of the box
635 * @param border attributes to use for the border
636 * @param background attributes to use for the background
637 * @param borderType if 1, draw a single-line border; if 2, draw a
638 * double-line border; if 3, draw double-line top/bottom edges and
639 * single-line left/right edges (like Qmodem)
640 * @param shadow if true, draw a "shadow" on the box
641 */
642 public final void drawBox(final int left, final int top,
643 final int right, final int bottom,
644 final CellAttributes border, final CellAttributes background,
645 final int borderType, final boolean shadow) {
646
647 int boxWidth = right - left;
648 int boxHeight = bottom - top;
649
650 char cTopLeft;
651 char cTopRight;
652 char cBottomLeft;
653 char cBottomRight;
654 char cHSide;
655 char cVSide;
656
657 switch (borderType) {
658 case 1:
659 cTopLeft = GraphicsChars.ULCORNER;
660 cTopRight = GraphicsChars.URCORNER;
661 cBottomLeft = GraphicsChars.LLCORNER;
662 cBottomRight = GraphicsChars.LRCORNER;
663 cHSide = GraphicsChars.SINGLE_BAR;
664 cVSide = GraphicsChars.WINDOW_SIDE;
665 break;
666
667 case 2:
668 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP_DOUBLE;
669 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP_DOUBLE;
670 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM_DOUBLE;
671 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM_DOUBLE;
672 cHSide = GraphicsChars.DOUBLE_BAR;
673 cVSide = GraphicsChars.WINDOW_SIDE_DOUBLE;
674 break;
675
676 case 3:
677 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP;
678 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP;
679 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM;
680 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM;
681 cHSide = GraphicsChars.WINDOW_TOP;
682 cVSide = GraphicsChars.WINDOW_SIDE;
683 break;
684 default:
685 throw new IllegalArgumentException("Invalid border type: "
686 + borderType);
687 }
688
689 // Place the corner characters
690 putCharXY(left, top, cTopLeft, border);
691 putCharXY(left + boxWidth - 1, top, cTopRight, border);
692 putCharXY(left, top + boxHeight - 1, cBottomLeft, border);
693 putCharXY(left + boxWidth - 1, top + boxHeight - 1, cBottomRight,
694 border);
695
696 // Draw the box lines
697 hLineXY(left + 1, top, boxWidth - 2, cHSide, border);
698 vLineXY(left, top + 1, boxHeight - 2, cVSide, border);
699 hLineXY(left + 1, top + boxHeight - 1, boxWidth - 2, cHSide, border);
700 vLineXY(left + boxWidth - 1, top + 1, boxHeight - 2, cVSide, border);
701
702 // Fill in the interior background
703 for (int i = 1; i < boxHeight - 1; i++) {
704 hLineXY(1 + left, i + top, boxWidth - 2, ' ', background);
705 }
706
707 if (shadow) {
708 // Draw a shadow
709 drawBoxShadow(left, top, right, bottom);
710 }
711 }
712
713 /**
714 * Draw a box shadow.
715 *
716 * @param left left column of box. 0 is the left-most row.
717 * @param top top row of the box. 0 is the top-most row.
718 * @param right right column of box
719 * @param bottom bottom row of the box
720 */
721 public final void drawBoxShadow(final int left, final int top,
722 final int right, final int bottom) {
723
724 int boxTop = top;
725 int boxLeft = left;
726 int boxWidth = right - left;
727 int boxHeight = bottom - top;
728 CellAttributes shadowAttr = new CellAttributes();
729
730 // Shadows do not honor clipping but they DO honor offset.
731 int oldClipRight = clipRight;
732 int oldClipBottom = clipBottom;
733 /*
734 clipRight = boxWidth + 2;
735 clipBottom = boxHeight + 1;
736 */
737 clipRight = width;
738 clipBottom = height;
739
740 for (int i = 0; i < boxHeight; i++) {
741 putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
742 putAttrXY(boxLeft + boxWidth + 1, boxTop + 1 + i, shadowAttr);
743 }
744 for (int i = 0; i < boxWidth; i++) {
745 putAttrXY(boxLeft + 2 + i, boxTop + boxHeight, shadowAttr);
746 }
747 clipRight = oldClipRight;
748 clipBottom = oldClipBottom;
749 }
750
751 /**
752 * Subclasses must provide an implementation to push the logical screen
753 * to the physical device.
754 */
755 public abstract void flushPhysical();
756
757 /**
758 * Put the cursor at (x,y).
759 *
760 * @param visible if true, the cursor should be visible
761 * @param x column coordinate to put the cursor on
762 * @param y row coordinate to put the cursor on
763 */
764 public void putCursor(final boolean visible, final int x, final int y) {
765
766 cursorVisible = visible;
767 cursorX = x;
768 cursorY = y;
769 }
770
771 /**
772 * Hide the cursor.
773 */
774 public final void hideCursor() {
775 cursorVisible = false;
776 }
777 }