Merge branch 'master' of https://github.com/klamonte/jexer
[nikiroo-utils.git] / src / jexer / io / AWTScreen.java
CommitLineData
1ac2ccb1
KL
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 */
31package jexer.io;
32
33import jexer.bits.Cell;
34import jexer.bits.CellAttributes;
30bd4abd 35import jexer.session.AWTSessionInfo;
1ac2ccb1
KL
36
37import java.awt.Color;
38import java.awt.Cursor;
39import java.awt.Font;
40import java.awt.FontMetrics;
41import java.awt.Frame;
42import java.awt.Graphics;
84614868 43import java.awt.Insets;
30bd4abd
KL
44import java.awt.Point;
45import java.awt.Rectangle;
46import java.awt.Toolkit;
1ac2ccb1 47import java.awt.geom.Rectangle2D;
30bd4abd 48import java.awt.image.BufferedImage;
84614868 49import java.io.InputStream;
aed33687
KL
50import javax.swing.JFrame;
51import javax.swing.SwingUtilities;
1ac2ccb1
KL
52
53/**
54 * This Screen implementation draws to a Java AWT Frame.
55 */
56public final class AWTScreen extends Screen {
57
84614868
KL
58 private static Color MYBLACK;
59 private static Color MYRED;
60 private static Color MYGREEN;
61 private static Color MYYELLOW;
62 private static Color MYBLUE;
63 private static Color MYMAGENTA;
64 private static Color MYCYAN;
65 private static Color MYWHITE;
66
67 private static Color MYBOLD_BLACK;
68 private static Color MYBOLD_RED;
69 private static Color MYBOLD_GREEN;
70 private static Color MYBOLD_YELLOW;
71 private static Color MYBOLD_BLUE;
72 private static Color MYBOLD_MAGENTA;
73 private static Color MYBOLD_CYAN;
74 private static Color MYBOLD_WHITE;
75
76 private static boolean dosColors = false;
77
78 /**
79 * Setup AWT colors to match DOS color palette.
80 */
81 private static void setDOSColors() {
82 if (dosColors) {
83 return;
84 }
85 MYBLACK = new Color(0x00, 0x00, 0x00);
86 MYRED = new Color(0xa8, 0x00, 0x00);
87 MYGREEN = new Color(0x00, 0xa8, 0x00);
88 MYYELLOW = new Color(0xa8, 0x54, 0x00);
89 MYBLUE = new Color(0x00, 0x00, 0xa8);
90 MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
91 MYCYAN = new Color(0x00, 0xa8, 0xa8);
92 MYWHITE = new Color(0xa8, 0xa8, 0xa8);
93 MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
94 MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
95 MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
96 MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
97 MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
98 MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
99 MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
100 MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
101
102 dosColors = true;
103 }
104
1ac2ccb1
KL
105 /**
106 * AWTFrame is our top-level hook into the AWT system.
107 */
aed33687 108 class AWTFrame extends JFrame {
1ac2ccb1 109
84614868
KL
110 /**
111 * The terminus font resource filename.
112 */
113 private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
114
1ac2ccb1
KL
115 /**
116 * The TUI Screen data.
117 */
118 AWTScreen screen;
119
120 /**
121 * Width of a character cell.
122 */
123 private int textWidth = 1;
124
125 /**
126 * Height of a character cell.
127 */
128 private int textHeight = 1;
129
84614868
KL
130 /**
131 * Descent of a character cell.
132 */
133 private int maxDescent = 0;
134
1ac2ccb1
KL
135 /**
136 * Top pixel value.
137 */
138 private int top = 30;
84614868 139
1ac2ccb1
KL
140 /**
141 * Left pixel value.
142 */
143 private int left = 30;
84614868
KL
144
145 /**
146 * Convert a CellAttributes foreground color to an AWT Color.
147 *
148 * @param attr the text attributes
149 * @return the AWT Color
150 */
151 private Color attrToForegroundColor(final CellAttributes attr) {
152 /*
153 * TODO:
154 * reverse
155 * blink
156 * underline
157 */
158 if (attr.getBold()) {
159 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
160 return MYBOLD_BLACK;
161 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
162 return MYBOLD_RED;
163 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
164 return MYBOLD_BLUE;
165 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
166 return MYBOLD_GREEN;
167 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
168 return MYBOLD_YELLOW;
169 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
170 return MYBOLD_CYAN;
171 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
172 return MYBOLD_MAGENTA;
173 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
174 return MYBOLD_WHITE;
175 }
176 } else {
177 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
178 return MYBLACK;
179 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
180 return MYRED;
181 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
182 return MYBLUE;
183 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
184 return MYGREEN;
185 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
186 return MYYELLOW;
187 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
188 return MYCYAN;
189 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
190 return MYMAGENTA;
191 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
192 return MYWHITE;
193 }
194 }
195 throw new IllegalArgumentException("Invalid color: " + attr.getForeColor().getValue());
196 }
197
198 /**
199 * Convert a CellAttributes background color to an AWT Color.
200 *
201 * @param attr the text attributes
202 * @return the AWT Color
203 */
204 private Color attrToBackgroundColor(final CellAttributes attr) {
205 /*
206 * TODO:
207 * reverse
208 * blink
209 * underline
210 */
211 if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
212 return MYBLACK;
213 } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
214 return MYRED;
215 } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
216 return MYBLUE;
217 } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
218 return MYGREEN;
219 } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
220 return MYYELLOW;
221 } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
222 return MYCYAN;
223 } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
224 return MYMAGENTA;
225 } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
226 return MYWHITE;
227 }
228 throw new IllegalArgumentException("Invalid color: " + attr.getBackColor().getValue());
229 }
230
1ac2ccb1
KL
231 /**
232 * Public constructor.
84614868
KL
233 *
234 * @param screen the Screen that Backend talks to
1ac2ccb1 235 */
84614868
KL
236 public AWTFrame(final AWTScreen screen) {
237 this.screen = screen;
238 setDOSColors();
239
1ac2ccb1 240 setTitle("Jexer Application");
aed33687 241 setBackground(Color.black);
30bd4abd 242 // setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
84614868 243 // setFont(new Font("Liberation Mono", Font.BOLD, 16));
1ac2ccb1 244 // setFont(new Font(Font.MONOSPACED, Font.PLAIN, 16));
84614868
KL
245
246 try {
30bd4abd 247 // Always try to use Terminus, the one decent font.
84614868
KL
248 ClassLoader loader = Thread.currentThread().getContextClassLoader();
249 InputStream in = loader.getResourceAsStream(FONTFILE);
250 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
251 Font terminus = terminusRoot.deriveFont(Font.PLAIN, 22);
252 setFont(terminus);
253 } catch (Exception e) {
254 e.printStackTrace();
255 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
256 setFont(new Font(Font.MONOSPACED, Font.PLAIN, 24));
257 }
aed33687 258 pack();
30bd4abd
KL
259
260 // Kill the X11 cursor
261 // Transparent 16 x 16 pixel cursor image.
262 BufferedImage cursorImg = new BufferedImage(16, 16,
263 BufferedImage.TYPE_INT_ARGB);
264
265 // Create a new blank cursor.
266 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
267 cursorImg, new Point(0, 0), "blank cursor");
268 setCursor(blankCursor);
bd8d51fa 269 getFontDimensions();
1ac2ccb1
KL
270 }
271
272 /**
bd8d51fa 273 * Figure out my font dimensions.
1ac2ccb1 274 */
bd8d51fa 275 private void getFontDimensions() {
84614868
KL
276 Graphics gr = getGraphics();
277 FontMetrics fm = gr.getFontMetrics();
278 maxDescent = fm.getMaxDescent();
279 Rectangle2D bounds = fm.getMaxCharBounds(gr);
280 int leading = fm.getLeading();
281 textWidth = (int)Math.round(bounds.getWidth());
282 textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
283 // This also produces the same number, but works better for ugly
284 // monospace.
285 textHeight = fm.getMaxAscent() + maxDescent - leading;
bd8d51fa 286 }
84614868 287
bd8d51fa
KL
288 /**
289 * Resize to font dimensions.
290 */
291 public void resizeToScreen() {
84614868
KL
292 // Figure out the thickness of borders and use that to set the
293 // final size.
294 Insets insets = getInsets();
295 left = insets.left;
296 top = insets.top;
297
298 setSize(textWidth * screen.width + insets.left + insets.right,
299 textHeight * screen.height + insets.top + insets.bottom);
1ac2ccb1
KL
300 }
301
30bd4abd
KL
302 /**
303 * Update redraws the whole screen.
304 *
305 * @param gr the AWT Graphics context
306 */
307 @Override
308 public void update(final Graphics gr) {
309 // The default update clears the area. Don't do that, instead
310 // just paint it directly.
311 paint(gr);
312 }
313
1ac2ccb1
KL
314 /**
315 * Paint redraws the whole screen.
316 *
317 * @param gr the AWT Graphics context
318 */
319 @Override
84614868 320 public void paint(final Graphics gr) {
87a17f3c
KL
321 // Do nothing until the screen reference has been set.
322 if (screen == null) {
323 return;
324 }
325 if (screen.frame == null) {
326 return;
327 }
328
329 int xCellMin = 0;
330 int xCellMax = screen.width;
331 int yCellMin = 0;
332 int yCellMax = screen.height;
333
30bd4abd 334 Rectangle bounds = gr.getClipBounds();
87a17f3c
KL
335 if (bounds != null) {
336 // Only update what is in the bounds
337 xCellMin = screen.textColumn(bounds.x);
bd8d51fa 338 xCellMax = screen.textColumn(bounds.x + bounds.width);
87a17f3c
KL
339 if (xCellMax > screen.width) {
340 xCellMax = screen.width;
341 }
342 if (xCellMin >= xCellMax) {
343 xCellMin = xCellMax - 2;
344 }
345 if (xCellMin < 0) {
346 xCellMin = 0;
347 }
348 yCellMin = screen.textRow(bounds.y);
bd8d51fa 349 yCellMax = screen.textRow(bounds.y + bounds.height);
87a17f3c
KL
350 if (yCellMax > screen.height) {
351 yCellMax = screen.height;
352 }
353 if (yCellMin >= yCellMax) {
354 yCellMin = yCellMax - 2;
355 }
356 if (yCellMin < 0) {
357 yCellMin = 0;
358 }
bd8d51fa
KL
359 } else {
360 // We need a total repaint
361 reallyCleared = true;
87a17f3c 362 }
1ac2ccb1 363
87a17f3c
KL
364 // Prevent updates to the screen's data from the TApplication
365 // threads.
366 synchronized (screen) {
367 /*
368 System.err.printf("bounds %s X %d %d Y %d %d\n",
369 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
370 */
371
372 for (int y = yCellMin; y < yCellMax; y++) {
373 for (int x = xCellMin; x < xCellMax; x++) {
374
375 int xPixel = x * textWidth + left;
376 int yPixel = y * textHeight + top;
377
378 Cell lCell = screen.logical[x][y];
379 Cell pCell = screen.physical[x][y];
380
381 if (!lCell.equals(pCell) || reallyCleared) {
382 // Draw the background rectangle, then the
383 // foreground character.
384 gr.setColor(attrToBackgroundColor(lCell));
385 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
386 gr.setColor(attrToForegroundColor(lCell));
387 char [] chars = new char[1];
388 chars[0] = lCell.getChar();
389 gr.drawChars(chars, 0, 1, xPixel,
390 yPixel + textHeight - maxDescent);
391
392 // Physical is always updated
393 physical[x][y].setTo(lCell);
30bd4abd
KL
394 }
395 }
87a17f3c 396 }
84614868 397
87a17f3c
KL
398 // Draw the cursor if it is visible
399 if ((cursorVisible)
400 && (cursorY <= screen.height - 1)
401 && (cursorX <= screen.width - 1)
402 ) {
403 int xPixel = cursorX * textWidth + left;
404 int yPixel = cursorY * textHeight + top;
405 Cell lCell = screen.logical[cursorX][cursorY];
406 gr.setColor(attrToForegroundColor(lCell));
407 gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2);
1ac2ccb1 408 }
30bd4abd 409
87a17f3c
KL
410 dirty = false;
411 reallyCleared = false;
412 } // synchronized (screen)
1ac2ccb1 413 }
bd8d51fa
KL
414
415 } // class AWTFrame
1ac2ccb1
KL
416
417 /**
84614868 418 * The raw AWT Frame. Note package private access.
1ac2ccb1 419 */
84614868 420 AWTFrame frame;
1ac2ccb1
KL
421
422 /**
423 * Public constructor.
424 */
425 public AWTScreen() {
aed33687
KL
426 try {
427 SwingUtilities.invokeAndWait(new Runnable() {
428 public void run() {
429 AWTScreen.this.frame = new AWTFrame(AWTScreen.this);
bd8d51fa
KL
430 AWTScreen.this.sessionInfo =
431 new AWTSessionInfo(AWTScreen.this.frame,
432 frame.textWidth,
433 frame.textHeight);
434
435 AWTScreen.this.setDimensions(sessionInfo.getWindowWidth(),
436 sessionInfo.getWindowHeight());
437
438 AWTScreen.this.frame.resizeToScreen();
439 AWTScreen.this.frame.setVisible(true);
aed33687
KL
440 }
441 } );
442 } catch (Exception e) {
443 e.printStackTrace();
444 }
1ac2ccb1
KL
445 }
446
bd8d51fa
KL
447 /**
448 * The sessionInfo.
449 */
450 private AWTSessionInfo sessionInfo;
451
30bd4abd
KL
452 /**
453 * Create the AWTSessionInfo. Note package private access.
454 *
455 * @return the sessionInfo
456 */
457 AWTSessionInfo getSessionInfo() {
30bd4abd
KL
458 return sessionInfo;
459 }
460
1ac2ccb1
KL
461 /**
462 * Push the logical screen to the physical device.
463 */
464 @Override
465 public void flushPhysical() {
87a17f3c
KL
466
467 if (reallyCleared) {
468 // Really refreshed, do it all
469 frame.repaint();
470 return;
471 }
472
473 // Do nothing if nothing happened.
474 if (!dirty) {
475 return;
476 }
477
30bd4abd
KL
478 // Request a repaint, let the frame's repaint/update methods do the
479 // right thing.
87a17f3c 480
30bd4abd
KL
481 // Find the minimum-size damaged region.
482 int xMin = frame.getWidth();
483 int xMax = 0;
484 int yMin = frame.getHeight();
485 int yMax = 0;
30bd4abd 486
87a17f3c
KL
487 synchronized (this) {
488 for (int y = 0; y < height; y++) {
489 for (int x = 0; x < width; x++) {
490 Cell lCell = logical[x][y];
491 Cell pCell = physical[x][y];
492
493 int xPixel = x * frame.textWidth + frame.left;
494 int yPixel = y * frame.textHeight + frame.top;
495
496 if (!lCell.equals(pCell)
497 || ((x == cursorX)
498 && (y == cursorY)
499 && cursorVisible)
500 ) {
501 if (xPixel < xMin) {
502 xMin = xPixel;
503 }
504 if (xPixel + frame.textWidth > xMax) {
505 xMax = xPixel + frame.textWidth;
506 }
507 if (yPixel < yMin) {
508 yMin = yPixel;
509 }
510 if (yPixel + frame.textHeight > yMax) {
511 yMax = yPixel + frame.textHeight;
512 }
30bd4abd
KL
513 }
514 }
515 }
516 }
87a17f3c
KL
517 if (xMin + frame.textWidth >= xMax) {
518 xMax += frame.textWidth;
519 }
520 if (yMin + frame.textHeight >= yMax) {
521 yMax += frame.textHeight;
522 }
30bd4abd 523
87a17f3c
KL
524 // Repaint the desired area
525 frame.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
526 // System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax, yMin, yMax);
1ac2ccb1 527 }
30bd4abd
KL
528
529 /**
530 * Put the cursor at (x,y).
531 *
532 * @param visible if true, the cursor should be visible
533 * @param x column coordinate to put the cursor on
534 * @param y row coordinate to put the cursor on
535 */
536 @Override
537 public void putCursor(final boolean visible, final int x, final int y) {
538 if ((cursorVisible)
539 && (cursorY <= height - 1)
540 && (cursorX <= width - 1)
541 ) {
542 // Make the current cursor position dirty
87a17f3c 543 if (physical[cursorX][cursorY].getChar() == 'Q') {
30bd4abd
KL
544 physical[cursorX][cursorY].setChar('X');
545 } else {
87a17f3c 546 physical[cursorX][cursorY].setChar('Q');
30bd4abd
KL
547 }
548 }
549
550 super.putCursor(visible, x, y);
551 }
552
87a17f3c
KL
553 /**
554 * Convert pixel column position to text cell column position.
555 *
556 * @param x pixel column position
557 * @return text cell column position
558 */
559 public int textColumn(final int x) {
560 return ((x - frame.left) / frame.textWidth);
561 }
562
563 /**
564 * Convert pixel row position to text cell row position.
565 *
566 * @param y pixel row position
567 * @return text cell row position
568 */
569 public int textRow(final int y) {
570 return ((y - frame.top) / frame.textHeight);
571 }
572
1ac2ccb1 573}