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