2 * Jexer - Java Text User Interface
4 * License: LGPLv3 or later
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.
10 * Copyright (C) 2015 Kevin Lamonte
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.
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.
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
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
33 import java
.awt
.Color
;
34 import java
.awt
.Cursor
;
36 import java
.awt
.FontMetrics
;
37 import java
.awt
.Graphics
;
38 import java
.awt
.Insets
;
39 import java
.awt
.Point
;
40 import java
.awt
.Rectangle
;
41 import java
.awt
.Toolkit
;
42 import java
.awt
.geom
.Rectangle2D
;
43 import java
.awt
.image
.BufferedImage
;
44 import java
.awt
.image
.BufferStrategy
;
45 import java
.io
.InputStream
;
46 import java
.util
.Date
;
47 import javax
.swing
.JFrame
;
48 import javax
.swing
.SwingUtilities
;
50 import jexer
.bits
.Cell
;
51 import jexer
.bits
.CellAttributes
;
52 import jexer
.session
.SwingSessionInfo
;
55 * This Screen implementation draws to a Java Swing JFrame.
57 public final class SwingScreen
extends Screen
{
60 * If true, use double buffering thread.
62 private static final boolean doubleBuffer
= true;
65 * Cursor style to draw.
67 public enum CursorStyle
{
69 * Use an underscore for the cursor.
74 * Use a solid block for the cursor.
79 * Use an outlined block for the cursor.
84 private static Color MYBLACK
;
85 private static Color MYRED
;
86 private static Color MYGREEN
;
87 private static Color MYYELLOW
;
88 private static Color MYBLUE
;
89 private static Color MYMAGENTA
;
90 private static Color MYCYAN
;
91 private static Color MYWHITE
;
93 private static Color MYBOLD_BLACK
;
94 private static Color MYBOLD_RED
;
95 private static Color MYBOLD_GREEN
;
96 private static Color MYBOLD_YELLOW
;
97 private static Color MYBOLD_BLUE
;
98 private static Color MYBOLD_MAGENTA
;
99 private static Color MYBOLD_CYAN
;
100 private static Color MYBOLD_WHITE
;
102 private static boolean dosColors
= false;
105 * Setup Swing colors to match DOS color palette.
107 private static void setDOSColors() {
111 MYBLACK
= new Color(0x00, 0x00, 0x00);
112 MYRED
= new Color(0xa8, 0x00, 0x00);
113 MYGREEN
= new Color(0x00, 0xa8, 0x00);
114 MYYELLOW
= new Color(0xa8, 0x54, 0x00);
115 MYBLUE
= new Color(0x00, 0x00, 0xa8);
116 MYMAGENTA
= new Color(0xa8, 0x00, 0xa8);
117 MYCYAN
= new Color(0x00, 0xa8, 0xa8);
118 MYWHITE
= new Color(0xa8, 0xa8, 0xa8);
119 MYBOLD_BLACK
= new Color(0x54, 0x54, 0x54);
120 MYBOLD_RED
= new Color(0xfc, 0x54, 0x54);
121 MYBOLD_GREEN
= new Color(0x54, 0xfc, 0x54);
122 MYBOLD_YELLOW
= new Color(0xfc, 0xfc, 0x54);
123 MYBOLD_BLUE
= new Color(0x54, 0x54, 0xfc);
124 MYBOLD_MAGENTA
= new Color(0xfc, 0x54, 0xfc);
125 MYBOLD_CYAN
= new Color(0x54, 0xfc, 0xfc);
126 MYBOLD_WHITE
= new Color(0xfc, 0xfc, 0xfc);
132 * SwingFrame is our top-level hook into the Swing system.
134 class SwingFrame
extends JFrame
{
137 * Serializable version.
139 private static final long serialVersionUID
= 1;
142 * The terminus font resource filename.
144 private static final String FONTFILE
= "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
147 * The BufferStrategy object needed for double-buffering.
149 private BufferStrategy bufferStrategy
;
152 * The TUI Screen data.
157 * Width of a character cell.
159 private int textWidth
= 1;
162 * Height of a character cell.
164 private int textHeight
= 1;
167 * Descent of a character cell.
169 private int maxDescent
= 0;
172 * Top pixel absolute location.
174 private int top
= 30;
177 * Left pixel absolute location.
179 private int left
= 30;
182 * The cursor style to draw.
184 private CursorStyle cursorStyle
= CursorStyle
.UNDERLINE
;
187 * The number of millis to wait before switching the blink from
188 * visible to invisible.
190 private long blinkMillis
= 500;
193 * If true, the cursor should be visible right now based on the blink
196 private boolean cursorBlinkVisible
= true;
199 * The time that the blink last flipped from visible to invisible or
200 * from invisible to visible.
202 private long lastBlinkTime
= 0;
205 * Convert a CellAttributes foreground color to an Swing Color.
207 * @param attr the text attributes
208 * @return the Swing Color
210 private Color
attrToForegroundColor(final CellAttributes attr
) {
212 if (attr
.getForeColor().equals(jexer
.bits
.Color
.BLACK
)) {
214 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.RED
)) {
216 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.BLUE
)) {
218 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.GREEN
)) {
220 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.YELLOW
)) {
221 return MYBOLD_YELLOW
;
222 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.CYAN
)) {
224 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.MAGENTA
)) {
225 return MYBOLD_MAGENTA
;
226 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.WHITE
)) {
230 if (attr
.getForeColor().equals(jexer
.bits
.Color
.BLACK
)) {
232 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.RED
)) {
234 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.BLUE
)) {
236 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.GREEN
)) {
238 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.YELLOW
)) {
240 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.CYAN
)) {
242 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.MAGENTA
)) {
244 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.WHITE
)) {
248 throw new IllegalArgumentException("Invalid color: " + attr
.getForeColor().getValue());
252 * Convert a CellAttributes background color to an Swing Color.
254 * @param attr the text attributes
255 * @return the Swing Color
257 private Color
attrToBackgroundColor(final CellAttributes attr
) {
258 if (attr
.getBackColor().equals(jexer
.bits
.Color
.BLACK
)) {
260 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.RED
)) {
262 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.BLUE
)) {
264 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.GREEN
)) {
266 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.YELLOW
)) {
268 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.CYAN
)) {
270 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.MAGENTA
)) {
272 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.WHITE
)) {
275 throw new IllegalArgumentException("Invalid color: " + attr
.getBackColor().getValue());
279 * Public constructor.
281 * @param screen the Screen that Backend talks to
283 public SwingFrame(final SwingScreen screen
) {
284 this.screen
= screen
;
287 // Figure out my cursor style
288 String cursorStyleString
= System
.getProperty("jexer.Swing.cursorStyle",
289 "underline").toLowerCase();
291 if (cursorStyleString
.equals("underline")) {
292 cursorStyle
= CursorStyle
.UNDERLINE
;
293 } else if (cursorStyleString
.equals("outline")) {
294 cursorStyle
= CursorStyle
.OUTLINE
;
295 } else if (cursorStyleString
.equals("block")) {
296 cursorStyle
= CursorStyle
.BLOCK
;
299 setTitle("Jexer Application");
300 setBackground(Color
.black
);
303 // Always try to use Terminus, the one decent font.
304 ClassLoader loader
= Thread
.currentThread().getContextClassLoader();
305 InputStream in
= loader
.getResourceAsStream(FONTFILE
);
306 Font terminusRoot
= Font
.createFont(Font
.TRUETYPE_FONT
, in
);
307 Font terminus
= terminusRoot
.deriveFont(Font
.PLAIN
, 22);
309 } catch (Exception e
) {
311 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
312 setFont(new Font(Font
.MONOSPACED
, Font
.PLAIN
, 24));
316 // Kill the X11 cursor
317 // Transparent 16 x 16 pixel cursor image.
318 BufferedImage cursorImg
= new BufferedImage(16, 16,
319 BufferedImage
.TYPE_INT_ARGB
);
320 // Create a new blank cursor.
321 Cursor blankCursor
= Toolkit
.getDefaultToolkit().createCustomCursor(
322 cursorImg
, new Point(0, 0), "blank cursor");
323 setCursor(blankCursor
);
325 // Be capable of seeing Tab / Shift-Tab
326 setFocusTraversalKeysEnabled(false);
328 // Save the text cell width/height
331 // Setup double-buffering
332 if (screen
.doubleBuffer
) {
333 setIgnoreRepaint(true);
334 createBufferStrategy(2);
335 bufferStrategy
= getBufferStrategy();
340 * Figure out my font dimensions.
342 private void getFontDimensions() {
343 Graphics gr
= getGraphics();
344 FontMetrics fm
= gr
.getFontMetrics();
345 maxDescent
= fm
.getMaxDescent();
346 Rectangle2D bounds
= fm
.getMaxCharBounds(gr
);
347 int leading
= fm
.getLeading();
348 textWidth
= (int)Math
.round(bounds
.getWidth());
349 textHeight
= (int)Math
.round(bounds
.getHeight()) - maxDescent
;
350 // This also produces the same number, but works better for ugly
352 textHeight
= fm
.getMaxAscent() + maxDescent
- leading
;
356 * Resize to font dimensions.
358 public void resizeToScreen() {
359 // Figure out the thickness of borders and use that to set the
361 Insets insets
= getInsets();
365 setSize(textWidth
* screen
.width
+ insets
.left
+ insets
.right
,
366 textHeight
* screen
.height
+ insets
.top
+ insets
.bottom
);
370 * Update redraws the whole screen.
372 * @param gr the Swing Graphics context
375 public void update(final Graphics gr
) {
376 // The default update clears the area. Don't do that, instead
377 // just paint it directly.
382 * Paint redraws the whole screen.
384 * @param gr the Swing Graphics context
387 public void paint(final Graphics gr
) {
388 // Do nothing until the screen reference has been set.
389 if (screen
== null) {
392 if (screen
.frame
== null) {
396 // See if it is time to flip the blink time.
397 long nowTime
= (new Date()).getTime();
398 if (nowTime
> blinkMillis
+ lastBlinkTime
) {
399 lastBlinkTime
= nowTime
;
400 cursorBlinkVisible
= !cursorBlinkVisible
;
404 int xCellMax
= screen
.width
;
406 int yCellMax
= screen
.height
;
408 Rectangle bounds
= gr
.getClipBounds();
409 if (bounds
!= null) {
410 // Only update what is in the bounds
411 xCellMin
= screen
.textColumn(bounds
.x
);
412 xCellMax
= screen
.textColumn(bounds
.x
+ bounds
.width
);
413 if (xCellMax
> screen
.width
) {
414 xCellMax
= screen
.width
;
416 if (xCellMin
>= xCellMax
) {
417 xCellMin
= xCellMax
- 2;
422 yCellMin
= screen
.textRow(bounds
.y
);
423 yCellMax
= screen
.textRow(bounds
.y
+ bounds
.height
);
424 if (yCellMax
> screen
.height
) {
425 yCellMax
= screen
.height
;
427 if (yCellMin
>= yCellMax
) {
428 yCellMin
= yCellMax
- 2;
434 // We need a total repaint
435 reallyCleared
= true;
438 // Prevent updates to the screen's data from the TApplication
440 synchronized (screen
) {
442 System.err.printf("bounds %s X %d %d Y %d %d\n",
443 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
446 for (int y
= yCellMin
; y
< yCellMax
; y
++) {
447 for (int x
= xCellMin
; x
< xCellMax
; x
++) {
449 int xPixel
= x
* textWidth
+ left
;
450 int yPixel
= y
* textHeight
+ top
;
452 Cell lCell
= screen
.logical
[x
][y
];
453 Cell pCell
= screen
.physical
[x
][y
];
455 if (!lCell
.equals(pCell
)
459 Cell lCellColor
= new Cell();
460 lCellColor
.setTo(lCell
);
463 if (lCell
.isReverse()) {
464 lCellColor
.setForeColor(lCell
.getBackColor());
465 lCellColor
.setBackColor(lCell
.getForeColor());
468 // Draw the background rectangle, then the
469 // foreground character.
470 gr
.setColor(attrToBackgroundColor(lCellColor
));
471 gr
.fillRect(xPixel
, yPixel
, textWidth
, textHeight
);
473 // Handle blink and underline
475 || (lCell
.isBlink() && cursorBlinkVisible
)
477 gr
.setColor(attrToForegroundColor(lCellColor
));
478 char [] chars
= new char[1];
479 chars
[0] = lCell
.getChar();
480 gr
.drawChars(chars
, 0, 1, xPixel
,
481 yPixel
+ textHeight
- maxDescent
);
482 if (lCell
.isUnderline()) {
483 gr
.fillRect(xPixel
, yPixel
+ textHeight
- 2,
488 // Physical is always updated
489 physical
[x
][y
].setTo(lCell
);
494 // Draw the cursor if it is visible
496 && (cursorY
<= screen
.height
- 1)
497 && (cursorX
<= screen
.width
- 1)
498 && cursorBlinkVisible
500 int xPixel
= cursorX
* textWidth
+ left
;
501 int yPixel
= cursorY
* textHeight
+ top
;
502 Cell lCell
= screen
.logical
[cursorX
][cursorY
];
503 gr
.setColor(attrToForegroundColor(lCell
));
504 switch (cursorStyle
) {
506 gr
.fillRect(xPixel
, yPixel
+ textHeight
- 2,
510 gr
.fillRect(xPixel
, yPixel
, textWidth
, textHeight
);
513 gr
.drawRect(xPixel
, yPixel
, textWidth
- 1,
520 reallyCleared
= false;
521 } // synchronized (screen)
524 } // class SwingFrame
527 * The raw Swing JFrame. Note package private access.
532 * Restore terminal to normal state.
534 public void shutdown() {
539 * Public constructor.
541 public SwingScreen() {
543 SwingUtilities
.invokeAndWait(new Runnable() {
545 SwingScreen
.this.frame
= new SwingFrame(SwingScreen
.this);
546 SwingScreen
.this.sessionInfo
=
547 new SwingSessionInfo(SwingScreen
.this.frame
,
551 SwingScreen
.this.setDimensions(sessionInfo
.getWindowWidth(),
552 sessionInfo
.getWindowHeight());
554 SwingScreen
.this.frame
.resizeToScreen();
555 SwingScreen
.this.frame
.setVisible(true);
558 } catch (Exception e
) {
566 private SwingSessionInfo sessionInfo
;
569 * Create the SwingSessionInfo. Note package private access.
571 * @return the sessionInfo
573 SwingSessionInfo
getSessionInfo() {
578 * Push the logical screen to the physical device.
581 public void flushPhysical() {
584 // Really refreshed, do it all
586 Graphics gr
= frame
.bufferStrategy
.getDrawGraphics();
589 frame
.bufferStrategy
.show();
590 Toolkit
.getDefaultToolkit().sync();
597 // Do nothing if nothing happened.
602 // Request a repaint, let the frame's repaint/update methods do the
605 // Find the minimum-size damaged region.
606 int xMin
= frame
.getWidth();
608 int yMin
= frame
.getHeight();
611 synchronized (this) {
612 for (int y
= 0; y
< height
; y
++) {
613 for (int x
= 0; x
< width
; x
++) {
614 Cell lCell
= logical
[x
][y
];
615 Cell pCell
= physical
[x
][y
];
617 int xPixel
= x
* frame
.textWidth
+ frame
.left
;
618 int yPixel
= y
* frame
.textHeight
+ frame
.top
;
620 if (!lCell
.equals(pCell
)
629 if (xPixel
+ frame
.textWidth
> xMax
) {
630 xMax
= xPixel
+ frame
.textWidth
;
635 if (yPixel
+ frame
.textHeight
> yMax
) {
636 yMax
= yPixel
+ frame
.textHeight
;
642 if (xMin
+ frame
.textWidth
>= xMax
) {
643 xMax
+= frame
.textWidth
;
645 if (yMin
+ frame
.textHeight
>= yMax
) {
646 yMax
+= frame
.textHeight
;
649 // Repaint the desired area
650 // System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
653 Graphics gr
= frame
.bufferStrategy
.getDrawGraphics();
654 Rectangle bounds
= new Rectangle(xMin
, yMin
, xMax
- xMin
,
659 frame
.bufferStrategy
.show();
660 Toolkit
.getDefaultToolkit().sync();
662 frame
.repaint(xMin
, yMin
, xMax
- xMin
, yMax
- yMin
);
667 * Put the cursor at (x,y).
669 * @param visible if true, the cursor should be visible
670 * @param x column coordinate to put the cursor on
671 * @param y row coordinate to put the cursor on
674 public void putCursor(final boolean visible
, final int x
, final int y
) {
676 if ((visible
== cursorVisible
) && ((x
== cursorX
) && (y
== cursorY
))) {
677 // See if it is time to flip the blink time.
678 long nowTime
= (new Date()).getTime();
679 if (nowTime
< frame
.blinkMillis
+ frame
.lastBlinkTime
) {
680 // Nothing has changed, so don't do anything.
686 && (cursorY
<= height
- 1)
687 && (cursorX
<= width
- 1)
689 // Make the current cursor position dirty
690 if (physical
[cursorX
][cursorY
].getChar() == 'Q') {
691 physical
[cursorX
][cursorY
].setChar('X');
693 physical
[cursorX
][cursorY
].setChar('Q');
697 super.putCursor(visible
, x
, y
);
701 * Convert pixel column position to text cell column position.
703 * @param x pixel column position
704 * @return text cell column position
706 public int textColumn(final int x
) {
707 return ((x
- frame
.left
) / frame
.textWidth
);
711 * Convert pixel row position to text cell row position.
713 * @param y pixel row position
714 * @return text cell row position
716 public int textRow(final int y
) {
717 return ((y
- frame
.top
) / frame
.textHeight
);