2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2016 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
31 import java
.awt
.Color
;
32 import java
.awt
.Cursor
;
34 import java
.awt
.FontMetrics
;
35 import java
.awt
.Graphics
;
36 import java
.awt
.Insets
;
37 import java
.awt
.Point
;
38 import java
.awt
.Rectangle
;
39 import java
.awt
.Toolkit
;
40 import java
.awt
.geom
.Rectangle2D
;
41 import java
.awt
.image
.BufferedImage
;
42 import java
.awt
.image
.BufferStrategy
;
43 import java
.io
.InputStream
;
44 import java
.util
.Date
;
45 import javax
.swing
.JFrame
;
46 import javax
.swing
.SwingUtilities
;
48 import jexer
.bits
.Cell
;
49 import jexer
.bits
.CellAttributes
;
50 import jexer
.session
.SwingSessionInfo
;
53 * This Screen implementation draws to a Java Swing JFrame.
55 public final class SwingScreen
extends Screen
{
58 * If true, use triple buffering thread.
60 private static final boolean tripleBuffer
= true;
63 * Cursor style to draw.
65 public enum CursorStyle
{
67 * Use an underscore for the cursor.
72 * Use a solid block for the cursor.
77 * Use an outlined block for the cursor.
82 private static Color MYBLACK
;
83 private static Color MYRED
;
84 private static Color MYGREEN
;
85 private static Color MYYELLOW
;
86 private static Color MYBLUE
;
87 private static Color MYMAGENTA
;
88 private static Color MYCYAN
;
89 private static Color MYWHITE
;
91 private static Color MYBOLD_BLACK
;
92 private static Color MYBOLD_RED
;
93 private static Color MYBOLD_GREEN
;
94 private static Color MYBOLD_YELLOW
;
95 private static Color MYBOLD_BLUE
;
96 private static Color MYBOLD_MAGENTA
;
97 private static Color MYBOLD_CYAN
;
98 private static Color MYBOLD_WHITE
;
100 private static boolean dosColors
= false;
103 * Setup Swing colors to match DOS color palette.
105 private static void setDOSColors() {
109 MYBLACK
= new Color(0x00, 0x00, 0x00);
110 MYRED
= new Color(0xa8, 0x00, 0x00);
111 MYGREEN
= new Color(0x00, 0xa8, 0x00);
112 MYYELLOW
= new Color(0xa8, 0x54, 0x00);
113 MYBLUE
= new Color(0x00, 0x00, 0xa8);
114 MYMAGENTA
= new Color(0xa8, 0x00, 0xa8);
115 MYCYAN
= new Color(0x00, 0xa8, 0xa8);
116 MYWHITE
= new Color(0xa8, 0xa8, 0xa8);
117 MYBOLD_BLACK
= new Color(0x54, 0x54, 0x54);
118 MYBOLD_RED
= new Color(0xfc, 0x54, 0x54);
119 MYBOLD_GREEN
= new Color(0x54, 0xfc, 0x54);
120 MYBOLD_YELLOW
= new Color(0xfc, 0xfc, 0x54);
121 MYBOLD_BLUE
= new Color(0x54, 0x54, 0xfc);
122 MYBOLD_MAGENTA
= new Color(0xfc, 0x54, 0xfc);
123 MYBOLD_CYAN
= new Color(0x54, 0xfc, 0xfc);
124 MYBOLD_WHITE
= new Color(0xfc, 0xfc, 0xfc);
130 * SwingFrame is our top-level hook into the Swing system.
132 class SwingFrame
extends JFrame
{
135 * Serializable version.
137 private static final long serialVersionUID
= 1;
140 * The terminus font resource filename.
142 private static final String FONTFILE
= "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
145 * The BufferStrategy object needed for triple-buffering.
147 private BufferStrategy bufferStrategy
;
150 * The TUI Screen data.
155 * Width of a character cell.
157 private int textWidth
= 1;
160 * Height of a character cell.
162 private int textHeight
= 1;
165 * Descent of a character cell.
167 private int maxDescent
= 0;
170 * System-dependent Y adjustment for text in the character cell.
172 private int textAdjustY
= 0;
175 * System-dependent X adjustment for text in the character cell.
177 private int textAdjustX
= 0;
180 * Top pixel absolute location.
182 private int top
= 30;
185 * Left pixel absolute location.
187 private int left
= 30;
190 * The cursor style to draw.
192 private CursorStyle cursorStyle
= CursorStyle
.UNDERLINE
;
195 * The number of millis to wait before switching the blink from
196 * visible to invisible.
198 private long blinkMillis
= 500;
201 * If true, the cursor should be visible right now based on the blink
204 private boolean cursorBlinkVisible
= true;
207 * The time that the blink last flipped from visible to invisible or
208 * from invisible to visible.
210 private long lastBlinkTime
= 0;
213 * Convert a CellAttributes foreground color to an Swing Color.
215 * @param attr the text attributes
216 * @return the Swing Color
218 private Color
attrToForegroundColor(final CellAttributes attr
) {
220 if (attr
.getForeColor().equals(jexer
.bits
.Color
.BLACK
)) {
222 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.RED
)) {
224 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.BLUE
)) {
226 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.GREEN
)) {
228 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.YELLOW
)) {
229 return MYBOLD_YELLOW
;
230 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.CYAN
)) {
232 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.MAGENTA
)) {
233 return MYBOLD_MAGENTA
;
234 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.WHITE
)) {
238 if (attr
.getForeColor().equals(jexer
.bits
.Color
.BLACK
)) {
240 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.RED
)) {
242 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.BLUE
)) {
244 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.GREEN
)) {
246 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.YELLOW
)) {
248 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.CYAN
)) {
250 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.MAGENTA
)) {
252 } else if (attr
.getForeColor().equals(jexer
.bits
.Color
.WHITE
)) {
256 throw new IllegalArgumentException("Invalid color: " + attr
.getForeColor().getValue());
260 * Convert a CellAttributes background color to an Swing Color.
262 * @param attr the text attributes
263 * @return the Swing Color
265 private Color
attrToBackgroundColor(final CellAttributes attr
) {
266 if (attr
.getBackColor().equals(jexer
.bits
.Color
.BLACK
)) {
268 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.RED
)) {
270 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.BLUE
)) {
272 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.GREEN
)) {
274 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.YELLOW
)) {
276 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.CYAN
)) {
278 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.MAGENTA
)) {
280 } else if (attr
.getBackColor().equals(jexer
.bits
.Color
.WHITE
)) {
283 throw new IllegalArgumentException("Invalid color: " + attr
.getBackColor().getValue());
287 * Public constructor.
289 * @param screen the Screen that Backend talks to
291 public SwingFrame(final SwingScreen screen
) {
292 this.screen
= screen
;
295 // Figure out my cursor style
296 String cursorStyleString
= System
.getProperty("jexer.Swing.cursorStyle",
297 "underline").toLowerCase();
299 if (cursorStyleString
.equals("underline")) {
300 cursorStyle
= CursorStyle
.UNDERLINE
;
301 } else if (cursorStyleString
.equals("outline")) {
302 cursorStyle
= CursorStyle
.OUTLINE
;
303 } else if (cursorStyleString
.equals("block")) {
304 cursorStyle
= CursorStyle
.BLOCK
;
307 setTitle("Jexer Application");
308 setBackground(Color
.black
);
311 // Always try to use Terminus, the one decent font.
312 ClassLoader loader
= Thread
.currentThread().getContextClassLoader();
313 InputStream in
= loader
.getResourceAsStream(FONTFILE
);
314 Font terminusRoot
= Font
.createFont(Font
.TRUETYPE_FONT
, in
);
315 Font terminus
= terminusRoot
.deriveFont(Font
.PLAIN
, 22);
317 } catch (Exception e
) {
319 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
320 setFont(new Font(Font
.MONOSPACED
, Font
.PLAIN
, 24));
324 // Kill the X11 cursor
325 // Transparent 16 x 16 pixel cursor image.
326 BufferedImage cursorImg
= new BufferedImage(16, 16,
327 BufferedImage
.TYPE_INT_ARGB
);
328 // Create a new blank cursor.
329 Cursor blankCursor
= Toolkit
.getDefaultToolkit().createCustomCursor(
330 cursorImg
, new Point(0, 0), "blank cursor");
331 setCursor(blankCursor
);
333 // Be capable of seeing Tab / Shift-Tab
334 setFocusTraversalKeysEnabled(false);
336 // Save the text cell width/height
339 // Setup triple-buffering
340 if (SwingScreen
.tripleBuffer
) {
341 setIgnoreRepaint(true);
342 createBufferStrategy(3);
343 bufferStrategy
= getBufferStrategy();
348 * Figure out my font dimensions.
350 private void getFontDimensions() {
351 Graphics gr
= getGraphics();
352 FontMetrics fm
= gr
.getFontMetrics();
353 maxDescent
= fm
.getMaxDescent();
354 Rectangle2D bounds
= fm
.getMaxCharBounds(gr
);
355 int leading
= fm
.getLeading();
356 textWidth
= (int)Math
.round(bounds
.getWidth());
357 textHeight
= (int)Math
.round(bounds
.getHeight()) - maxDescent
;
358 // This also produces the same number, but works better for ugly
360 textHeight
= fm
.getMaxAscent() + maxDescent
- leading
;
362 if (System
.getProperty("os.name").startsWith("Windows")) {
369 * Resize to font dimensions.
371 public void resizeToScreen() {
372 // Figure out the thickness of borders and use that to set the
374 Insets insets
= getInsets();
378 setSize(textWidth
* screen
.width
+ insets
.left
+ insets
.right
,
379 textHeight
* screen
.height
+ insets
.top
+ insets
.bottom
);
383 * Update redraws the whole screen.
385 * @param gr the Swing Graphics context
388 public void update(final Graphics gr
) {
389 // The default update clears the area. Don't do that, instead
390 // just paint it directly.
395 * Paint redraws the whole screen.
397 * @param gr the Swing Graphics context
400 public void paint(final Graphics gr
) {
401 // Do nothing until the screen reference has been set.
402 if (screen
== null) {
405 if (screen
.frame
== null) {
409 // See if it is time to flip the blink time.
410 long nowTime
= (new Date()).getTime();
411 if (nowTime
> blinkMillis
+ lastBlinkTime
) {
412 lastBlinkTime
= nowTime
;
413 cursorBlinkVisible
= !cursorBlinkVisible
;
417 int xCellMax
= screen
.width
;
419 int yCellMax
= screen
.height
;
421 Rectangle bounds
= gr
.getClipBounds();
422 if (bounds
!= null) {
423 // Only update what is in the bounds
424 xCellMin
= screen
.textColumn(bounds
.x
);
425 xCellMax
= screen
.textColumn(bounds
.x
+ bounds
.width
);
426 if (xCellMax
> screen
.width
) {
427 xCellMax
= screen
.width
;
429 if (xCellMin
>= xCellMax
) {
430 xCellMin
= xCellMax
- 2;
435 yCellMin
= screen
.textRow(bounds
.y
);
436 yCellMax
= screen
.textRow(bounds
.y
+ bounds
.height
);
437 if (yCellMax
> screen
.height
) {
438 yCellMax
= screen
.height
;
440 if (yCellMin
>= yCellMax
) {
441 yCellMin
= yCellMax
- 2;
447 // We need a total repaint
448 reallyCleared
= true;
451 // Prevent updates to the screen's data from the TApplication
453 synchronized (screen
) {
456 System.err.printf("bounds %s X %d %d Y %d %d\n",
457 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
459 Cell lCellColor
= new Cell();
461 for (int y
= yCellMin
; y
< yCellMax
; y
++) {
462 for (int x
= xCellMin
; x
< xCellMax
; x
++) {
464 int xPixel
= x
* textWidth
+ left
;
465 int yPixel
= y
* textHeight
+ top
;
467 Cell lCell
= screen
.logical
[x
][y
];
468 Cell pCell
= screen
.physical
[x
][y
];
470 if (!lCell
.equals(pCell
)
474 lCellColor
.setTo(lCell
);
477 if (lCell
.isReverse()) {
478 lCellColor
.setForeColor(lCell
.getBackColor());
479 lCellColor
.setBackColor(lCell
.getForeColor());
482 // Draw the background rectangle, then the
483 // foreground character.
484 gr
.setColor(attrToBackgroundColor(lCellColor
));
485 gr
.fillRect(xPixel
, yPixel
, textWidth
, textHeight
);
487 // Handle blink and underline
489 || (lCell
.isBlink() && cursorBlinkVisible
)
491 gr
.setColor(attrToForegroundColor(lCellColor
));
492 char [] chars
= new char[1];
493 chars
[0] = lCell
.getChar();
494 gr
.drawChars(chars
, 0, 1, xPixel
+ textAdjustX
,
495 yPixel
+ textHeight
- maxDescent
498 if (lCell
.isUnderline()) {
499 gr
.fillRect(xPixel
, yPixel
+ textHeight
- 2,
504 // Physical is always updated
505 physical
[x
][y
].setTo(lCell
);
510 // Draw the cursor if it is visible
512 && (cursorY
<= screen
.height
- 1)
513 && (cursorX
<= screen
.width
- 1)
514 && cursorBlinkVisible
516 int xPixel
= cursorX
* textWidth
+ left
;
517 int yPixel
= cursorY
* textHeight
+ top
;
518 Cell lCell
= screen
.logical
[cursorX
][cursorY
];
519 gr
.setColor(attrToForegroundColor(lCell
));
520 switch (cursorStyle
) {
524 gr
.fillRect(xPixel
, yPixel
+ textHeight
- 2,
528 gr
.fillRect(xPixel
, yPixel
, textWidth
, textHeight
);
531 gr
.drawRect(xPixel
, yPixel
, textWidth
- 1,
538 reallyCleared
= false;
539 } // synchronized (screen)
542 } // class SwingFrame
545 * The raw Swing JFrame. Note package private access.
550 * Restore terminal to normal state.
552 public void shutdown() {
557 * Public constructor.
559 public SwingScreen() {
561 SwingUtilities
.invokeAndWait(new Runnable() {
563 SwingScreen
.this.frame
= new SwingFrame(SwingScreen
.this);
564 SwingScreen
.this.sessionInfo
=
565 new SwingSessionInfo(SwingScreen
.this.frame
,
569 SwingScreen
.this.setDimensions(sessionInfo
.getWindowWidth(),
570 sessionInfo
.getWindowHeight());
572 SwingScreen
.this.frame
.resizeToScreen();
573 SwingScreen
.this.frame
.setVisible(true);
576 } catch (Exception e
) {
584 private SwingSessionInfo sessionInfo
;
587 * Create the SwingSessionInfo. Note package private access.
589 * @return the sessionInfo
591 SwingSessionInfo
getSessionInfo() {
596 * Push the logical screen to the physical device.
599 public void flushPhysical() {
602 System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n",
603 reallyCleared, dirty);
606 // If reallyCleared is set, we have to draw everything.
607 if ((frame
.bufferStrategy
!= null) && (reallyCleared
== true)) {
608 // Triple-buffering: we have to redraw everything on this thread.
609 Graphics gr
= frame
.bufferStrategy
.getDrawGraphics();
612 frame
.bufferStrategy
.show();
613 // sync() doesn't seem to help the tearing for me.
614 // Toolkit.getDefaultToolkit().sync();
616 } else if ((frame
.bufferStrategy
== null) && (reallyCleared
== true)) {
617 // Repaint everything on the Swing thread.
622 // Do nothing if nothing happened.
627 // Request a repaint, let the frame's repaint/update methods do the
630 // Find the minimum-size damaged region.
631 int xMin
= frame
.getWidth();
633 int yMin
= frame
.getHeight();
636 synchronized (this) {
637 for (int y
= 0; y
< height
; y
++) {
638 for (int x
= 0; x
< width
; x
++) {
639 Cell lCell
= logical
[x
][y
];
640 Cell pCell
= physical
[x
][y
];
642 int xPixel
= x
* frame
.textWidth
+ frame
.left
;
643 int yPixel
= y
* frame
.textHeight
+ frame
.top
;
645 if (!lCell
.equals(pCell
)
654 if (xPixel
+ frame
.textWidth
> xMax
) {
655 xMax
= xPixel
+ frame
.textWidth
;
660 if (yPixel
+ frame
.textHeight
> yMax
) {
661 yMax
= yPixel
+ frame
.textHeight
;
667 if (xMin
+ frame
.textWidth
>= xMax
) {
668 xMax
+= frame
.textWidth
;
670 if (yMin
+ frame
.textHeight
>= yMax
) {
671 yMax
+= frame
.textHeight
;
674 // Repaint the desired area
676 System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
679 if (frame
.bufferStrategy
!= null) {
680 Graphics gr
= frame
.bufferStrategy
.getDrawGraphics();
681 Rectangle bounds
= new Rectangle(xMin
, yMin
, xMax
- xMin
,
686 frame
.bufferStrategy
.show();
687 // sync() doesn't seem to help the tearing for me.
688 // Toolkit.getDefaultToolkit().sync();
690 frame
.repaint(xMin
, yMin
, xMax
- xMin
, yMax
- yMin
);
695 * Put the cursor at (x,y).
697 * @param visible if true, the cursor should be visible
698 * @param x column coordinate to put the cursor on
699 * @param y row coordinate to put the cursor on
702 public void putCursor(final boolean visible
, final int x
, final int y
) {
704 if ((visible
== cursorVisible
) && ((x
== cursorX
) && (y
== cursorY
))) {
705 // See if it is time to flip the blink time.
706 long nowTime
= (new Date()).getTime();
707 if (nowTime
< frame
.blinkMillis
+ frame
.lastBlinkTime
) {
708 // Nothing has changed, so don't do anything.
714 && (cursorY
<= height
- 1)
715 && (cursorX
<= width
- 1)
717 // Make the current cursor position dirty
718 if (physical
[cursorX
][cursorY
].getChar() == 'Q') {
719 physical
[cursorX
][cursorY
].setChar('X');
721 physical
[cursorX
][cursorY
].setChar('Q');
725 super.putCursor(visible
, x
, y
);
729 * Convert pixel column position to text cell column position.
731 * @param x pixel column position
732 * @return text cell column position
734 public int textColumn(final int x
) {
735 return ((x
- frame
.left
) / frame
.textWidth
);
739 * Convert pixel row position to text cell row position.
741 * @param y pixel row position
742 * @return text cell row position
744 public int textRow(final int y
) {
745 return ((y
- frame
.top
) / frame
.textHeight
);