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