*
* The MIT License (MIT)
*
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import javax.swing.JComponent;
import javax.swing.JFrame;
+import javax.swing.ImageIcon;
import javax.swing.SwingUtilities;
import jexer.TKeypress;
* and uses a SwingComponent wrapper class to call the JFrame or JComponent
* methods.
*/
-public final class SwingTerminal extends LogicalScreen
- implements TerminalReader,
- ComponentListener, KeyListener,
- MouseListener, MouseMotionListener,
- MouseWheelListener, WindowListener {
+public class SwingTerminal extends LogicalScreen
+ implements TerminalReader,
+ ComponentListener, KeyListener,
+ MouseListener, MouseMotionListener,
+ MouseWheelListener, WindowListener {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
/**
- * The Swing component or frame to draw to.
+ * The icon image location.
*/
- private SwingComponent swing;
+ private static final String ICONFILE = "jexer_logo_128.png";
- // ------------------------------------------------------------------------
- // Screen -----------------------------------------------------------------
- // ------------------------------------------------------------------------
+ /**
+ * The terminus font resource filename.
+ */
+ private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
/**
* Cursor style to draw.
OUTLINE
}
- /**
- * A cache of previously-rendered glyphs for blinking text, when it is
- * not visible.
- */
- private HashMap<Cell, BufferedImage> glyphCacheBlink;
-
- /**
- * A cache of previously-rendered glyphs for non-blinking, or
- * blinking-and-visible, text.
- */
- private HashMap<Cell, BufferedImage> glyphCache;
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
// Colors to map DOS colors to AWT colors.
private static Color MYBLACK;
private static boolean dosColors = false;
/**
- * Setup Swing colors to match DOS color palette.
+ * The Swing component or frame to draw to.
*/
- private static void setDOSColors() {
- if (dosColors) {
- return;
- }
- MYBLACK = new Color(0x00, 0x00, 0x00);
- MYRED = new Color(0xa8, 0x00, 0x00);
- MYGREEN = new Color(0x00, 0xa8, 0x00);
- MYYELLOW = new Color(0xa8, 0x54, 0x00);
- MYBLUE = new Color(0x00, 0x00, 0xa8);
- MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
- MYCYAN = new Color(0x00, 0xa8, 0xa8);
- MYWHITE = new Color(0xa8, 0xa8, 0xa8);
- MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
- MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
- MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
- MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
- MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
- MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
- MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
- MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
-
- dosColors = true;
- }
+ private SwingComponent swing;
/**
- * The terminus font resource filename.
+ * A cache of previously-rendered glyphs for blinking text, when it is
+ * not visible.
*/
- private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
+ private Map<Cell, BufferedImage> glyphCacheBlink;
/**
- * If true, we were successful getting Terminus.
+ * A cache of previously-rendered glyphs for non-blinking, or
+ * blinking-and-visible, text.
*/
- private boolean gotTerminus = false;
+ private Map<Cell, BufferedImage> glyphCache;
/**
* If true, we were successful at getting the font dimensions.
*/
private int textHeight = 1;
+ /**
+ * Width of a character cell in pixels, as reported by font.
+ */
+ private int fontTextWidth = 1;
+
+ /**
+ * Height of a character cell in pixels, as reported by font.
+ */
+ private int fontTextHeight = 1;
+
/**
* Descent of a character cell in pixels.
*/
*/
private int textAdjustX = 0;
+ /**
+ * System-dependent height adjustment for text in the character cell.
+ */
+ private int textAdjustHeight = 0;
+
+ /**
+ * System-dependent width adjustment for text in the character cell.
+ */
+ private int textAdjustWidth = 0;
+
/**
* Top pixel absolute location.
*/
*/
private long blinkMillis = 500;
- /**
- * Get the number of millis to wait before switching the blink from
- * visible to invisible.
- *
- * @return the number of milli to wait before switching the blink from
- * visible to invisible
- */
- public long getBlinkMillis() {
- return blinkMillis;
- }
-
/**
* If true, the cursor should be visible right now based on the blink
* time.
private long lastBlinkTime = 0;
/**
- * Get the font size in points.
- *
- * @return font size in points
+ * The session information.
*/
- public int getFontSize() {
- return fontSize;
- }
+ private SwingSessionInfo sessionInfo;
/**
- * Set the font size in points.
- *
- * @param fontSize font size in points
+ * The listening object that run() wakes up on new input.
*/
- public void setFontSize(final int fontSize) {
- this.fontSize = fontSize;
- Font newFont = font.deriveFont((float) fontSize);
- setFont(newFont);
- }
+ private Object listener;
/**
- * Set to a new font, and resize the screen to match its dimensions.
- *
- * @param font the new font
+ * The event queue, filled up by a thread reading on input.
*/
- public void setFont(final Font font) {
- this.font = font;
- getFontDimensions();
- swing.setFont(font);
- glyphCacheBlink = new HashMap<Cell, BufferedImage>();
- glyphCache = new HashMap<Cell, BufferedImage>();
- resizeToScreen();
- }
+ private List<TInputEvent> eventQueue;
/**
- * Set the font to Terminus, the best all-around font for both CP437 and
- * ISO8859-1.
+ * The last reported mouse X position.
*/
- public void getDefaultFont() {
- try {
- ClassLoader loader = Thread.currentThread().
- getContextClassLoader();
- InputStream in = loader.getResourceAsStream(FONTFILE);
- Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
- Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize);
- gotTerminus = true;
- font = terminus;
- } catch (Exception e) {
- e.printStackTrace();
- font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
- }
-
- setFont(font);
- }
+ private int oldMouseX = -1;
/**
- * Convert a CellAttributes foreground color to an Swing Color.
- *
- * @param attr the text attributes
- * @return the Swing Color
+ * The last reported mouse Y position.
*/
- private Color attrToForegroundColor(final CellAttributes attr) {
- if (attr.isBold()) {
- if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
- return MYBOLD_BLACK;
- } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
- return MYBOLD_RED;
- } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
- return MYBOLD_BLUE;
- } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
- return MYBOLD_GREEN;
- } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
- return MYBOLD_YELLOW;
- } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
- return MYBOLD_CYAN;
- } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
- return MYBOLD_MAGENTA;
- } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
- return MYBOLD_WHITE;
- }
- } else {
- if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
- return MYBLACK;
- } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
- return MYRED;
- } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
- return MYBLUE;
- } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
- return MYGREEN;
- } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
- return MYYELLOW;
- } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
- return MYCYAN;
- } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
- return MYMAGENTA;
- } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
- return MYWHITE;
- }
- }
- throw new IllegalArgumentException("Invalid color: " +
- attr.getForeColor().getValue());
- }
+ private int oldMouseY = -1;
/**
- * Convert a CellAttributes background color to an Swing Color.
- *
- * @param attr the text attributes
- * @return the Swing Color
+ * true if mouse1 was down. Used to report mouse1 on the release event.
*/
- private Color attrToBackgroundColor(final CellAttributes attr) {
- if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
- return MYBLACK;
- } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
- return MYRED;
- } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
- return MYBLUE;
- } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
- return MYGREEN;
- } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
- return MYYELLOW;
- } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
- return MYCYAN;
- } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
- return MYMAGENTA;
- } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
- return MYWHITE;
- }
- throw new IllegalArgumentException("Invalid color: " +
- attr.getBackColor().getValue());
- }
+ private boolean mouse1 = false;
/**
- * Figure out what textAdjustX and textAdjustY should be, based on the
- * location of a vertical bar (to find textAdjustY) and a horizontal bar
- * (to find textAdjustX).
- *
- * @return true if textAdjustX and textAdjustY were guessed at correctly
+ * true if mouse2 was down. Used to report mouse2 on the release event.
*/
- private boolean getFontAdjustments() {
- BufferedImage image = null;
+ private boolean mouse2 = false;
- // What SHOULD happen is that the topmost/leftmost white pixel is at
- // position (gr2x, gr2y). But it might also be off by a pixel in
- // either direction.
+ /**
+ * true if mouse3 was down. Used to report mouse3 on the release event.
+ */
+ private boolean mouse3 = false;
- Graphics2D gr2 = null;
- int gr2x = 3;
- int gr2y = 3;
- image = new BufferedImage(textWidth * 2, textHeight * 2,
- BufferedImage.TYPE_INT_ARGB);
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
- gr2 = image.createGraphics();
- gr2.setFont(swing.getFont());
- gr2.setColor(java.awt.Color.BLACK);
- gr2.fillRect(0, 0, textWidth * 2, textHeight * 2);
- gr2.setColor(java.awt.Color.WHITE);
- char [] chars = new char[1];
- chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR;
- gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent);
- gr2.dispose();
+ /**
+ * Public constructor creates a new JFrame to render to.
+ *
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points. Good values to pick are: 16, 20,
+ * 22, and 24.
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ */
+ public SwingTerminal(final int windowWidth, final int windowHeight,
+ final int fontSize, final Object listener) {
- for (int x = 0; x < textWidth; x++) {
- for (int y = 0; y < textHeight; y++) {
+ this.fontSize = fontSize;
- /*
- System.err.println("X: " + x + " Y: " + y + " " +
- image.getRGB(x, y));
- */
+ setDOSColors();
+ reloadOptions();
- if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
- textAdjustY = (gr2y - y);
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
- // System.err.println("textAdjustY: " + textAdjustY);
- x = textWidth;
- break;
- }
- }
- }
+ JFrame frame = new JFrame() {
- gr2 = image.createGraphics();
- gr2.setFont(swing.getFont());
- gr2.setColor(java.awt.Color.BLACK);
- gr2.fillRect(0, 0, textWidth * 2, textHeight * 2);
- gr2.setColor(java.awt.Color.WHITE);
- chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR;
- gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent);
- gr2.dispose();
+ /**
+ * Serializable version.
+ */
+ private static final long serialVersionUID = 1;
- for (int x = 0; x < textWidth; x++) {
- for (int y = 0; y < textHeight; y++) {
+ /**
+ * The code that performs the actual drawing.
+ */
+ public SwingTerminal screen = null;
- /*
- System.err.println("X: " + x + " Y: " + y + " " +
- image.getRGB(x, y));
- */
+ /*
+ * Anonymous class initializer saves the screen
+ * reference, so that paint() and the like call out
+ * to SwingTerminal.
+ */
+ {
+ this.screen = SwingTerminal.this;
+ }
- if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
- textAdjustX = (gr2x - x);
+ /**
+ * Update redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ @Override
+ public void update(final Graphics gr) {
+ // The default update clears the area. Don't do
+ // that, instead just paint it directly.
+ paint(gr);
+ }
- // System.err.println("textAdjustX: " + textAdjustX);
- return true;
- }
- }
- }
+ /**
+ * Paint redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ @Override
+ public void paint(final Graphics gr) {
+ if (screen != null) {
+ screen.paint(gr);
+ }
+ }
+ };
- // Something weird happened, don't rely on this function.
- // System.err.println("getFontAdjustments: false");
- return false;
- }
+ // Set icon
+ ClassLoader loader = Thread.currentThread().
+ getContextClassLoader();
+ frame.setIconImage((new ImageIcon(loader.
+ getResource(ICONFILE))).getImage());
- /**
- * Figure out my font dimensions. This code path works OK for the JFrame
- * case, and can be called immediately after JFrame creation.
- */
- private void getFontDimensions() {
- swing.setFont(font);
- Graphics gr = swing.getGraphics();
- if (gr == null) {
- return;
- }
- getFontDimensions(gr);
- }
+ // Get the Swing component
+ SwingTerminal.this.swing = new SwingComponent(frame);
- /**
- * Figure out my font dimensions. This code path is needed to lazy-load
- * the information inside paint().
- *
- * @param gr Graphics object to use
- */
- private void getFontDimensions(final Graphics gr) {
- swing.setFont(font);
- FontMetrics fm = gr.getFontMetrics();
- maxDescent = fm.getMaxDescent();
- Rectangle2D bounds = fm.getMaxCharBounds(gr);
- int leading = fm.getLeading();
- textWidth = (int)Math.round(bounds.getWidth());
- // textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
+ // Hang onto top and left for drawing.
+ Insets insets = SwingTerminal.this.swing.getInsets();
+ SwingTerminal.this.left = insets.left;
+ SwingTerminal.this.top = insets.top;
- // This produces the same number, but works better for ugly
- // monospace.
- textHeight = fm.getMaxAscent() + maxDescent - leading;
+ // Load the font so that we can set sessionInfo.
+ setDefaultFont();
- if (gotTerminus == true) {
- textHeight++;
- }
+ // Get the default cols x rows and set component size
+ // accordingly.
+ SwingTerminal.this.sessionInfo =
+ new SwingSessionInfo(SwingTerminal.this.swing,
+ SwingTerminal.this.textWidth,
+ SwingTerminal.this.textHeight,
+ windowWidth, windowHeight);
- if (getFontAdjustments() == false) {
- // We were unable to programmatically determine textAdjustX and
- // textAdjustY, so try some guesses based on VM vendor.
- String runtime = System.getProperty("java.runtime.name");
- if ((runtime != null) && (runtime.contains("Java(TM)"))) {
- textAdjustY = -1;
- textAdjustX = 0;
- }
- }
+ SwingTerminal.this.setDimensions(sessionInfo.
+ getWindowWidth(), sessionInfo.getWindowHeight());
- if (sessionInfo != null) {
- sessionInfo.setTextCellDimensions(textWidth, textHeight);
+ SwingTerminal.this.resizeToScreen();
+ SwingTerminal.this.swing.setVisible(true);
+ }
+ });
+ } catch (java.lang.reflect.InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
}
- gotFontDimensions = true;
- }
- /**
- * Resize to font dimensions.
- */
- public void resizeToScreen() {
- swing.setDimensions(textWidth * width, textHeight * height);
+ this.listener = listener;
+ mouse1 = false;
+ mouse2 = false;
+ mouse3 = false;
+ eventQueue = new LinkedList<TInputEvent>();
+
+ // Add listeners to Swing.
+ swing.addKeyListener(this);
+ swing.addWindowListener(this);
+ swing.addComponentListener(this);
+ swing.addMouseListener(this);
+ swing.addMouseMotionListener(this);
+ swing.addMouseWheelListener(this);
}
/**
- * Draw one glyph to the screen.
+ * Public constructor renders to an existing JComponent.
*
- * @param gr the Swing Graphics context
- * @param cell the Cell to draw
- * @param xPixel the x-coordinate to render to. 0 means the
- * left-most pixel column.
- * @param yPixel the y-coordinate to render to. 0 means the top-most
- * pixel row.
+ * @param component the Swing component to render to
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points. Good values to pick are: 16, 20,
+ * 22, and 24.
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
*/
- private void drawGlyph(final Graphics gr, final Cell cell,
- final int xPixel, final int yPixel) {
+ public SwingTerminal(final JComponent component, final int windowWidth,
+ final int windowHeight, final int fontSize, final Object listener) {
- /*
- System.err.println("drawGlyph(): " + xPixel + " " + yPixel +
- " " + cell);
- */
+ this.fontSize = fontSize;
- BufferedImage image = null;
- if (cell.isBlink() && !cursorBlinkVisible) {
- image = glyphCacheBlink.get(cell);
- } else {
- image = glyphCache.get(cell);
- }
- if (image != null) {
- if (swing.getFrame() != null) {
- gr.drawImage(image, xPixel, yPixel, swing.getFrame());
- } else {
- gr.drawImage(image, xPixel, yPixel, swing.getComponent());
- }
- return;
- }
+ setDOSColors();
+ reloadOptions();
- // Generate glyph and draw it.
- Graphics2D gr2 = null;
- int gr2x = xPixel;
- int gr2y = yPixel;
- if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
- image = new BufferedImage(textWidth, textHeight,
- BufferedImage.TYPE_INT_ARGB);
- gr2 = image.createGraphics();
- gr2.setFont(swing.getFont());
- gr2x = 0;
- gr2y = 0;
- } else {
- gr2 = (Graphics2D) gr;
- }
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
- Cell cellColor = new Cell();
- cellColor.setTo(cell);
+ JComponent newComponent = new JComponent() {
- // Check for reverse
- if (cell.isReverse()) {
- cellColor.setForeColor(cell.getBackColor());
- cellColor.setBackColor(cell.getForeColor());
- }
+ /**
+ * Serializable version.
+ */
+ private static final long serialVersionUID = 1;
- // Draw the background rectangle, then the foreground character.
- gr2.setColor(attrToBackgroundColor(cellColor));
- gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
+ /**
+ * The code that performs the actual drawing.
+ */
+ public SwingTerminal screen = null;
- // Handle blink and underline
- if (!cell.isBlink()
- || (cell.isBlink() && cursorBlinkVisible)
- ) {
- gr2.setColor(attrToForegroundColor(cellColor));
- char [] chars = new char[1];
- chars[0] = cell.getChar();
- gr2.drawChars(chars, 0, 1, gr2x + textAdjustX,
- gr2y + textHeight - maxDescent + textAdjustY);
+ /*
+ * Anonymous class initializer saves the screen
+ * reference, so that paint() and the like call out
+ * to SwingTerminal.
+ */
+ {
+ this.screen = SwingTerminal.this;
+ }
- if (cell.isUnderline()) {
- gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2);
- }
- }
+ /**
+ * Update redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ @Override
+ public void update(final Graphics gr) {
+ // The default update clears the area. Don't do
+ // that, instead just paint it directly.
+ paint(gr);
+ }
- if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
- gr2.dispose();
+ /**
+ * Paint redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ @Override
+ public void paint(final Graphics gr) {
+ if (screen != null) {
+ screen.paint(gr);
+ }
+ }
+ };
+ component.setLayout(new BorderLayout());
+ component.add(newComponent);
- // We need a new key that will not be mutated by
- // invertCell().
- Cell key = new Cell();
- key.setTo(cell);
- if (cell.isBlink() && !cursorBlinkVisible) {
- glyphCacheBlink.put(key, image);
- } else {
- glyphCache.put(key, image);
- }
+ // Allow key events to be received
+ component.setFocusable(true);
- if (swing.getFrame() != null) {
- gr.drawImage(image, xPixel, yPixel, swing.getFrame());
- } else {
- gr.drawImage(image, xPixel, yPixel, swing.getComponent());
- }
- }
+ // Get the Swing component
+ SwingTerminal.this.swing = new SwingComponent(component);
- }
+ // Hang onto top and left for drawing.
+ Insets insets = SwingTerminal.this.swing.getInsets();
+ SwingTerminal.this.left = insets.left;
+ SwingTerminal.this.top = insets.top;
- /**
- * Check if the cursor is visible, and if so draw it.
- *
- * @param gr the Swing Graphics context
- */
- private void drawCursor(final Graphics gr) {
-
- if (cursorVisible
- && (cursorY >= 0)
- && (cursorX >= 0)
- && (cursorY <= height - 1)
- && (cursorX <= width - 1)
- && cursorBlinkVisible
- ) {
- int xPixel = cursorX * textWidth + left;
- int yPixel = cursorY * textHeight + top;
- Cell lCell = logical[cursorX][cursorY];
- gr.setColor(attrToForegroundColor(lCell));
- switch (cursorStyle) {
- default:
- // Fall through...
- case UNDERLINE:
- gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2);
- break;
- case BLOCK:
- gr.fillRect(xPixel, yPixel, textWidth, textHeight);
- break;
- case OUTLINE:
- gr.drawRect(xPixel, yPixel, textWidth - 1, textHeight - 1);
- break;
- }
- }
- }
-
- /**
- * Reset the blink timer.
- */
- private void resetBlinkTimer() {
- lastBlinkTime = System.currentTimeMillis();
- cursorBlinkVisible = true;
- }
-
- /**
- * Paint redraws the whole screen.
- *
- * @param gr the Swing Graphics context
- */
- public void paint(final Graphics gr) {
-
- if (gotFontDimensions == false) {
- // Lazy-load the text width/height
- getFontDimensions(gr);
- /*
- System.err.println("textWidth " + textWidth +
- " textHeight " + textHeight);
- System.err.println("FONT: " + swing.getFont() + " font " + font);
- */
- }
-
- int xCellMin = 0;
- int xCellMax = width;
- int yCellMin = 0;
- int yCellMax = height;
+ // Load the font so that we can set sessionInfo.
+ setDefaultFont();
- Rectangle bounds = gr.getClipBounds();
- if (bounds != null) {
- // Only update what is in the bounds
- xCellMin = textColumn(bounds.x);
- xCellMax = textColumn(bounds.x + bounds.width);
- if (xCellMax > width) {
- xCellMax = width;
- }
- if (xCellMin >= xCellMax) {
- xCellMin = xCellMax - 2;
- }
- if (xCellMin < 0) {
- xCellMin = 0;
- }
- yCellMin = textRow(bounds.y);
- yCellMax = textRow(bounds.y + bounds.height);
- if (yCellMax > height) {
- yCellMax = height;
- }
- if (yCellMin >= yCellMax) {
- yCellMin = yCellMax - 2;
- }
- if (yCellMin < 0) {
- yCellMin = 0;
- }
- } else {
- // We need a total repaint
- reallyCleared = true;
+ // Get the default cols x rows and set component size
+ // accordingly.
+ SwingTerminal.this.sessionInfo =
+ new SwingSessionInfo(SwingTerminal.this.swing,
+ SwingTerminal.this.textWidth,
+ SwingTerminal.this.textHeight);
+ }
+ });
+ } catch (java.lang.reflect.InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
}
- // Prevent updates to the screen's data from the TApplication
- // threads.
- synchronized (this) {
-
- /*
- System.err.printf("bounds %s X %d %d Y %d %d\n",
- bounds, xCellMin, xCellMax, yCellMin, yCellMax);
- */
-
- for (int y = yCellMin; y < yCellMax; y++) {
- for (int x = xCellMin; x < xCellMax; x++) {
-
- int xPixel = x * textWidth + left;
- int yPixel = y * textHeight + top;
-
- Cell lCell = logical[x][y];
- Cell pCell = physical[x][y];
-
- if (!lCell.equals(pCell)
- || lCell.isBlink()
- || reallyCleared
- || (swing.getFrame() == null)) {
-
- drawGlyph(gr, lCell, xPixel, yPixel);
-
- // Physical is always updated
- physical[x][y].setTo(lCell);
- }
- }
- }
- drawCursor(gr);
+ this.listener = listener;
+ mouse1 = false;
+ mouse2 = false;
+ mouse3 = false;
+ eventQueue = new LinkedList<TInputEvent>();
- reallyCleared = false;
- } // synchronized (this)
+ // Add listeners to Swing.
+ swing.addKeyListener(this);
+ swing.addWindowListener(this);
+ swing.addComponentListener(this);
+ swing.addMouseListener(this);
+ swing.addMouseMotionListener(this);
+ swing.addMouseWheelListener(this);
}
+ // ------------------------------------------------------------------------
+ // LogicalScreen ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
- * Restore terminal to normal state.
+ * Set the window title.
+ *
+ * @param title the new title
*/
- public void shutdown() {
- swing.dispose();
+ @Override
+ public void setTitle(final String title) {
+ swing.setTitle(title);
}
/**
}
}
+ // ------------------------------------------------------------------------
+ // TerminalReader ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
- * Push the logical screen to the physical device.
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the backend
*/
- private void drawToSwing() {
+ public boolean hasEvents() {
+ synchronized (eventQueue) {
+ return (eventQueue.size() > 0);
+ }
+ }
- /*
- System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
- reallyCleared, dirty);
- */
+ /**
+ * Return any events in the IO queue.
+ *
+ * @param queue list to append new events to
+ */
+ public void getEvents(final List<TInputEvent> queue) {
+ synchronized (eventQueue) {
+ if (eventQueue.size() > 0) {
+ synchronized (queue) {
+ queue.addAll(eventQueue);
+ }
+ eventQueue.clear();
+ }
+ }
+ }
- // If reallyCleared is set, we have to draw everything.
- if ((swing.getFrame() != null)
- && (swing.getBufferStrategy() != null)
- && (reallyCleared == true)
- ) {
- // Triple-buffering: we have to redraw everything on this thread.
- Graphics gr = swing.getBufferStrategy().getDrawGraphics();
- swing.paint(gr);
- gr.dispose();
- swing.getBufferStrategy().show();
- Toolkit.getDefaultToolkit().sync();
- return;
- } else if (((swing.getFrame() != null)
- && (swing.getBufferStrategy() == null))
- || (reallyCleared == true)
- ) {
- // Repaint everything on the Swing thread.
- // System.err.println("REPAINT ALL");
- swing.repaint();
- return;
+ /**
+ * Restore terminal to normal state.
+ */
+ public void closeTerminal() {
+ shutdown();
+ }
+
+ /**
+ * Set listener to a different Object.
+ *
+ * @param listener the new listening object that run() wakes up on new
+ * input
+ */
+ public void setListener(final Object listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Reload options from System properties.
+ */
+ public void reloadOptions() {
+ // Figure out my cursor style.
+ String cursorStyleString = System.getProperty(
+ "jexer.Swing.cursorStyle", "underline").toLowerCase();
+ if (cursorStyleString.equals("underline")) {
+ cursorStyle = CursorStyle.UNDERLINE;
+ } else if (cursorStyleString.equals("outline")) {
+ cursorStyle = CursorStyle.OUTLINE;
+ } else if (cursorStyleString.equals("block")) {
+ cursorStyle = CursorStyle.BLOCK;
}
- if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
- Graphics gr = swing.getBufferStrategy().getDrawGraphics();
+ // Pull the system property for triple buffering.
+ if (System.getProperty("jexer.Swing.tripleBuffer",
+ "true").equals("true")
+ ) {
+ SwingComponent.tripleBuffer = true;
+ } else {
+ SwingComponent.tripleBuffer = false;
+ }
+ }
- synchronized (this) {
- for (int y = 0; y < height; y++) {
- for (int x = 0; x < width; x++) {
- Cell lCell = logical[x][y];
- Cell pCell = physical[x][y];
+ // ------------------------------------------------------------------------
+ // SwingTerminal ----------------------------------------------------------
+ // ------------------------------------------------------------------------
- int xPixel = x * textWidth + left;
- int yPixel = y * textHeight + top;
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ public int getTextWidth() {
+ return textWidth;
+ }
- if (!lCell.equals(pCell)
- || ((x == cursorX)
- && (y == cursorY)
- && cursorVisible)
- || (lCell.isBlink())
- ) {
- drawGlyph(gr, lCell, xPixel, yPixel);
- physical[x][y].setTo(lCell);
- }
- }
- }
- drawCursor(gr);
- } // synchronized (this)
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ public int getTextHeight() {
+ return textHeight;
+ }
- gr.dispose();
- swing.getBufferStrategy().show();
- Toolkit.getDefaultToolkit().sync();
+ /**
+ * Setup Swing colors to match DOS color palette.
+ */
+ private static void setDOSColors() {
+ if (dosColors) {
return;
}
+ MYBLACK = new Color(0x00, 0x00, 0x00);
+ MYRED = new Color(0xa8, 0x00, 0x00);
+ MYGREEN = new Color(0x00, 0xa8, 0x00);
+ MYYELLOW = new Color(0xa8, 0x54, 0x00);
+ MYBLUE = new Color(0x00, 0x00, 0xa8);
+ MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
+ MYCYAN = new Color(0x00, 0xa8, 0xa8);
+ MYWHITE = new Color(0xa8, 0xa8, 0xa8);
+ MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
+ MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
+ MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
+ MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
+ MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
+ MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
+ MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
+ MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
- // Swing thread version: request a repaint, but limit it to the area
- // that has changed.
-
- // Find the minimum-size damaged region.
- int xMin = swing.getWidth();
- int xMax = 0;
- int yMin = swing.getHeight();
- int yMax = 0;
-
- synchronized (this) {
- for (int y = 0; y < height; y++) {
- for (int x = 0; x < width; x++) {
- Cell lCell = logical[x][y];
- Cell pCell = physical[x][y];
-
- int xPixel = x * textWidth + left;
- int yPixel = y * textHeight + top;
-
- if (!lCell.equals(pCell)
- || ((x == cursorX)
- && (y == cursorY)
- && cursorVisible)
- || lCell.isBlink()
- ) {
- if (xPixel < xMin) {
- xMin = xPixel;
- }
- if (xPixel + textWidth > xMax) {
- xMax = xPixel + textWidth;
- }
- if (yPixel < yMin) {
- yMin = yPixel;
- }
- if (yPixel + textHeight > yMax) {
- yMax = yPixel + textHeight;
- }
- }
- }
- }
- }
- if (xMin + textWidth >= xMax) {
- xMax += textWidth;
- }
- if (yMin + textHeight >= yMax) {
- yMax += textHeight;
- }
-
- // Repaint the desired area
- /*
- System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
- yMin, yMax);
- */
-
- if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
- // This path should never be taken, but is left here for
- // completeness.
- Graphics gr = swing.getBufferStrategy().getDrawGraphics();
- Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
- yMax - yMin);
- gr.setClip(bounds);
- swing.paint(gr);
- gr.dispose();
- swing.getBufferStrategy().show();
- Toolkit.getDefaultToolkit().sync();
- } else {
- // Repaint on the Swing thread.
- swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
- }
+ dosColors = true;
}
/**
- * Convert pixel column position to text cell column position.
+ * Get the number of millis to wait before switching the blink from
+ * visible to invisible.
*
- * @param x pixel column position
- * @return text cell column position
+ * @return the number of milli to wait before switching the blink from
+ * visible to invisible
*/
- public int textColumn(final int x) {
- return ((x - left) / textWidth);
+ public long getBlinkMillis() {
+ return blinkMillis;
}
/**
- * Convert pixel row position to text cell row position.
+ * Get the font size in points.
*
- * @param y pixel row position
- * @return text cell row position
+ * @return font size in points
*/
- public int textRow(final int y) {
- return ((y - top) / textHeight);
+ public int getFontSize() {
+ return fontSize;
}
/**
- * Set the window title.
+ * Set the font size in points.
*
- * @param title the new title
+ * @param fontSize font size in points
*/
- public void setTitle(final String title) {
- swing.setTitle(title);
+ public void setFontSize(final int fontSize) {
+ this.fontSize = fontSize;
+ Font newFont = font.deriveFont((float) fontSize);
+ setFont(newFont);
}
- // ------------------------------------------------------------------------
- // TerminalReader ---------------------------------------------------------
- // ------------------------------------------------------------------------
-
/**
- * The session information.
+ * Set to a new font, and resize the screen to match its dimensions.
+ *
+ * @param font the new font
*/
- private SwingSessionInfo sessionInfo;
+ public void setFont(final Font font) {
+ synchronized (this) {
+ this.font = font;
+ getFontDimensions();
+ swing.setFont(font);
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ resizeToScreen();
+ }
+ }
/**
- * Getter for sessionInfo.
+ * Get the font this screen was last set to.
*
- * @return the SessionInfo
+ * @return the font
*/
- public SessionInfo getSessionInfo() {
- return sessionInfo;
+ public Font getFont() {
+ return font;
}
/**
- * The listening object that run() wakes up on new input.
+ * Set the font to Terminus, the best all-around font for both CP437 and
+ * ISO8859-1.
*/
- private Object listener;
+ public void setDefaultFont() {
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ InputStream in = loader.getResourceAsStream(FONTFILE);
+ Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
+ Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize);
+ font = terminus;
+ } catch (java.awt.FontFormatException e) {
+ e.printStackTrace();
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ } catch (java.io.IOException e) {
+ e.printStackTrace();
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ }
+
+ setFont(font);
+ }
/**
- * Set listener to a different Object.
+ * Get the X text adjustment.
*
- * @param listener the new listening object that run() wakes up on new
- * input
+ * @return X text adjustment
*/
- public void setListener(final Object listener) {
- this.listener = listener;
+ public int getTextAdjustX() {
+ return textAdjustX;
}
/**
- * The event queue, filled up by a thread reading on input.
+ * Set the X text adjustment.
+ *
+ * @param textAdjustX the X text adjustment
*/
- private List<TInputEvent> eventQueue;
+ public void setTextAdjustX(final int textAdjustX) {
+ synchronized (this) {
+ this.textAdjustX = textAdjustX;
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ clearPhysical();
+ }
+ }
/**
- * The last reported mouse X position.
+ * Get the Y text adjustment.
+ *
+ * @return Y text adjustment
*/
- private int oldMouseX = -1;
+ public int getTextAdjustY() {
+ return textAdjustY;
+ }
/**
- * The last reported mouse Y position.
+ * Set the Y text adjustment.
+ *
+ * @param textAdjustY the Y text adjustment
*/
- private int oldMouseY = -1;
+ public void setTextAdjustY(final int textAdjustY) {
+ synchronized (this) {
+ this.textAdjustY = textAdjustY;
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ clearPhysical();
+ }
+ }
/**
- * true if mouse1 was down. Used to report mouse1 on the release event.
+ * Get the height text adjustment.
+ *
+ * @return height text adjustment
*/
- private boolean mouse1 = false;
+ public int getTextAdjustHeight() {
+ return textAdjustHeight;
+ }
/**
- * true if mouse2 was down. Used to report mouse2 on the release event.
+ * Set the height text adjustment.
+ *
+ * @param textAdjustHeight the height text adjustment
*/
- private boolean mouse2 = false;
+ public void setTextAdjustHeight(final int textAdjustHeight) {
+ synchronized (this) {
+ this.textAdjustHeight = textAdjustHeight;
+ textHeight = fontTextHeight + textAdjustHeight;
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ clearPhysical();
+ }
+ }
/**
- * true if mouse3 was down. Used to report mouse3 on the release event.
+ * Get the width text adjustment.
+ *
+ * @return width text adjustment
*/
- private boolean mouse3 = false;
+ public int getTextAdjustWidth() {
+ return textAdjustWidth;
+ }
/**
- * Public constructor creates a new JFrame to render to.
+ * Set the width text adjustment.
*
- * @param windowWidth the number of text columns to start with
- * @param windowHeight the number of text rows to start with
- * @param fontSize the size in points. Good values to pick are: 16, 20,
- * 22, and 24.
- * @param listener the object this backend needs to wake up when new
- * input comes in
+ * @param textAdjustWidth the width text adjustment
*/
- public SwingTerminal(final int windowWidth, final int windowHeight,
- final int fontSize, final Object listener) {
-
- this.fontSize = fontSize;
-
- setDOSColors();
-
- // Figure out my cursor style.
- String cursorStyleString = System.getProperty(
- "jexer.Swing.cursorStyle", "underline").toLowerCase();
- if (cursorStyleString.equals("underline")) {
- cursorStyle = CursorStyle.UNDERLINE;
- } else if (cursorStyleString.equals("outline")) {
- cursorStyle = CursorStyle.OUTLINE;
- } else if (cursorStyleString.equals("block")) {
- cursorStyle = CursorStyle.BLOCK;
- }
-
- // Pull the system property for triple buffering.
- if (System.getProperty("jexer.Swing.tripleBuffer") != null) {
- if (System.getProperty("jexer.Swing.tripleBuffer").equals("true")) {
- SwingComponent.tripleBuffer = true;
- } else {
- SwingComponent.tripleBuffer = false;
- }
+ public void setTextAdjustWidth(final int textAdjustWidth) {
+ synchronized (this) {
+ this.textAdjustWidth = textAdjustWidth;
+ textWidth = fontTextWidth + textAdjustWidth;
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ clearPhysical();
}
+ }
- try {
- SwingUtilities.invokeAndWait(new Runnable() {
- public void run() {
+ /**
+ * Convert a CellAttributes foreground color to an Swing Color.
+ *
+ * @param attr the text attributes
+ * @return the Swing Color
+ */
+ private Color attrToForegroundColor(final CellAttributes attr) {
+ int rgb = attr.getForeColorRGB();
+ if (rgb >= 0) {
+ int red = (rgb >> 16) & 0xFF;
+ int green = (rgb >> 8) & 0xFF;
+ int blue = rgb & 0xFF;
- JFrame frame = new JFrame() {
+ return new Color(red, green, blue);
+ }
- /**
- * Serializable version.
- */
- private static final long serialVersionUID = 1;
+ if (attr.isBold()) {
+ if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
+ return MYBOLD_BLACK;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
+ return MYBOLD_RED;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
+ return MYBOLD_BLUE;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
+ return MYBOLD_GREEN;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
+ return MYBOLD_YELLOW;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
+ return MYBOLD_CYAN;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
+ return MYBOLD_MAGENTA;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
+ return MYBOLD_WHITE;
+ }
+ } else {
+ if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
+ return MYBLACK;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
+ return MYRED;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
+ return MYBLUE;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
+ return MYGREEN;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
+ return MYYELLOW;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
+ return MYCYAN;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
+ return MYMAGENTA;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
+ return MYWHITE;
+ }
+ }
+ throw new IllegalArgumentException("Invalid color: " +
+ attr.getForeColor().getValue());
+ }
- /**
- * The code that performs the actual drawing.
- */
- public SwingTerminal screen = null;
+ /**
+ * Convert a CellAttributes background color to an Swing Color.
+ *
+ * @param attr the text attributes
+ * @return the Swing Color
+ */
+ private Color attrToBackgroundColor(final CellAttributes attr) {
+ int rgb = attr.getBackColorRGB();
+ if (rgb >= 0) {
+ int red = (rgb >> 16) & 0xFF;
+ int green = (rgb >> 8) & 0xFF;
+ int blue = rgb & 0xFF;
- /*
- * Anonymous class initializer saves the screen
- * reference, so that paint() and the like call out
- * to SwingTerminal.
- */
- {
- this.screen = SwingTerminal.this;
- }
+ return new Color(red, green, blue);
+ }
- /**
- * Update redraws the whole screen.
- *
- * @param gr the Swing Graphics context
- */
- @Override
- public void update(final Graphics gr) {
- // The default update clears the area. Don't do
- // that, instead just paint it directly.
- paint(gr);
- }
+ if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
+ return MYBLACK;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
+ return MYRED;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
+ return MYBLUE;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
+ return MYGREEN;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
+ return MYYELLOW;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
+ return MYCYAN;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
+ return MYMAGENTA;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
+ return MYWHITE;
+ }
+ throw new IllegalArgumentException("Invalid color: " +
+ attr.getBackColor().getValue());
+ }
- /**
- * Paint redraws the whole screen.
- *
- * @param gr the Swing Graphics context
- */
- @Override
- public void paint(final Graphics gr) {
- if (screen != null) {
- screen.paint(gr);
- }
- }
- };
+ /**
+ * Figure out what textAdjustX, textAdjustY, textAdjustHeight, and
+ * textAdjustWidth should be, based on the location of a vertical bar and
+ * a horizontal bar.
+ */
+ private void getFontAdjustments() {
+ BufferedImage image = null;
- // Get the Swing component
- SwingTerminal.this.swing = new SwingComponent(frame);
+ // What SHOULD happen is that the topmost/leftmost white pixel is at
+ // position (gr2x, gr2y). But it might also be off by a pixel in
+ // either direction.
- // Hang onto top and left for drawing.
- Insets insets = SwingTerminal.this.swing.getInsets();
- SwingTerminal.this.left = insets.left;
- SwingTerminal.this.top = insets.top;
+ Graphics2D gr2 = null;
+ int gr2x = 3;
+ int gr2y = 3;
+ image = new BufferedImage(fontTextWidth * 2, fontTextHeight * 2,
+ BufferedImage.TYPE_INT_ARGB);
- // Load the font so that we can set sessionInfo.
- getDefaultFont();
+ gr2 = image.createGraphics();
+ gr2.setFont(swing.getFont());
+ gr2.setColor(java.awt.Color.BLACK);
+ gr2.fillRect(0, 0, fontTextWidth * 2, fontTextHeight * 2);
+ gr2.setColor(java.awt.Color.WHITE);
+ char [] chars = new char[1];
+ chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR;
+ gr2.drawChars(chars, 0, 1, gr2x, gr2y + fontTextHeight - maxDescent);
+ chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR;
+ gr2.drawChars(chars, 0, 1, gr2x, gr2y + fontTextHeight - maxDescent);
+ gr2.dispose();
- // Get the default cols x rows and set component size
- // accordingly.
- SwingTerminal.this.sessionInfo =
- new SwingSessionInfo(SwingTerminal.this.swing,
- SwingTerminal.this.textWidth,
- SwingTerminal.this.textHeight,
- windowWidth, windowHeight);
+ int top = fontTextHeight * 2;
+ int bottom = -1;
+ int left = fontTextWidth * 2;
+ int right = -1;
+ textAdjustX = 0;
+ textAdjustY = 0;
+ textAdjustHeight = 0;
+ textAdjustWidth = 0;
- SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(),
- sessionInfo.getWindowHeight());
+ for (int x = 0; x < fontTextWidth * 2; x++) {
+ for (int y = 0; y < fontTextHeight * 2; y++) {
- SwingTerminal.this.resizeToScreen();
- SwingTerminal.this.swing.setVisible(true);
+ /*
+ System.err.println("H X: " + x + " Y: " + y + " " +
+ image.getRGB(x, y));
+ */
+
+ if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
+ // Pixel is present.
+ if (y < top) {
+ top = y;
+ }
+ if (y > bottom) {
+ bottom = y;
+ }
+ if (x < left) {
+ left = x;
+ }
+ if (x > right) {
+ right = x;
+ }
}
- });
- } catch (Exception e) {
- e.printStackTrace();
+ }
}
+ if (left < right) {
+ textAdjustX = (gr2x - left);
+ textAdjustWidth = fontTextWidth - (right - left + 1);
+ }
+ if (top < bottom) {
+ textAdjustY = (gr2y - top);
+ textAdjustHeight = fontTextHeight - (bottom - top + 1);
+ }
+ // System.err.println("top " + top + " bottom " + bottom);
+ // System.err.println("left " + left + " right " + right);
- this.listener = listener;
- mouse1 = false;
- mouse2 = false;
- mouse3 = false;
- eventQueue = new LinkedList<TInputEvent>();
+ // Special case: do not believe fonts that claim to be wider than
+ // they are tall.
+ if (fontTextWidth >= fontTextHeight) {
+ textAdjustX = 0;
+ textAdjustWidth = 0;
+ fontTextWidth = fontTextHeight / 2;
+ }
+ }
- // Add listeners to Swing.
- swing.addKeyListener(this);
- swing.addWindowListener(this);
- swing.addComponentListener(this);
- swing.addMouseListener(this);
- swing.addMouseMotionListener(this);
- swing.addMouseWheelListener(this);
+ /**
+ * Figure out my font dimensions. This code path works OK for the JFrame
+ * case, and can be called immediately after JFrame creation.
+ */
+ private void getFontDimensions() {
+ swing.setFont(font);
+ Graphics gr = swing.getGraphics();
+ if (gr == null) {
+ return;
+ }
+ getFontDimensions(gr);
}
/**
- * Public constructor renders to an existing JComponent.
+ * Figure out my font dimensions. This code path is needed to lazy-load
+ * the information inside paint().
*
- * @param component the Swing component to render to
- * @param windowWidth the number of text columns to start with
- * @param windowHeight the number of text rows to start with
- * @param fontSize the size in points. Good values to pick are: 16, 20,
- * 22, and 24.
- * @param listener the object this backend needs to wake up when new
- * input comes in
+ * @param gr Graphics object to use
*/
- public SwingTerminal(final JComponent component, final int windowWidth,
- final int windowHeight, final int fontSize, final Object listener) {
+ private void getFontDimensions(final Graphics gr) {
+ swing.setFont(font);
+ FontMetrics fm = gr.getFontMetrics();
+ maxDescent = fm.getMaxDescent();
+ Rectangle2D bounds = fm.getMaxCharBounds(gr);
+ int leading = fm.getLeading();
+ fontTextWidth = (int)Math.round(bounds.getWidth());
+ // fontTextHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
- this.fontSize = fontSize;
+ // This produces the same number, but works better for ugly
+ // monospace.
+ fontTextHeight = fm.getMaxAscent() + maxDescent - leading;
- setDOSColors();
+ getFontAdjustments();
+ textHeight = fontTextHeight + textAdjustHeight;
+ textWidth = fontTextWidth + textAdjustWidth;
- // Figure out my cursor style.
- String cursorStyleString = System.getProperty(
- "jexer.Swing.cursorStyle", "underline").toLowerCase();
- if (cursorStyleString.equals("underline")) {
- cursorStyle = CursorStyle.UNDERLINE;
- } else if (cursorStyleString.equals("outline")) {
- cursorStyle = CursorStyle.OUTLINE;
- } else if (cursorStyleString.equals("block")) {
- cursorStyle = CursorStyle.BLOCK;
+ if (sessionInfo != null) {
+ sessionInfo.setTextCellDimensions(textWidth, textHeight);
+ }
+ gotFontDimensions = true;
+ }
+
+ /**
+ * Resize to font dimensions.
+ */
+ public void resizeToScreen() {
+ swing.setDimensions(textWidth * (width + 1), textHeight * (height + 1));
+ }
+
+ /**
+ * Draw one cell's image to the screen.
+ *
+ * @param gr the Swing Graphics context
+ * @param cell the Cell to draw
+ * @param xPixel the x-coordinate to render to. 0 means the
+ * left-most pixel column.
+ * @param yPixel the y-coordinate to render to. 0 means the top-most
+ * pixel row.
+ */
+ private void drawImage(final Graphics gr, final Cell cell,
+ final int xPixel, final int yPixel) {
+
+ /*
+ System.err.println("drawImage(): " + xPixel + " " + yPixel +
+ " " + cell);
+ */
+
+ // Draw the background rectangle, then the foreground character.
+ assert (cell.isImage());
+ gr.setColor(cell.getBackground());
+ gr.fillRect(xPixel, yPixel, textWidth, textHeight);
+
+ BufferedImage image = cell.getImage();
+ if (image != null) {
+ if (swing.getFrame() != null) {
+ gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+ } else {
+ gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+ }
+ return;
+ }
+ }
+
+ /**
+ * Draw one glyph to the screen.
+ *
+ * @param gr the Swing Graphics context
+ * @param cell the Cell to draw
+ * @param xPixel the x-coordinate to render to. 0 means the
+ * left-most pixel column.
+ * @param yPixel the y-coordinate to render to. 0 means the top-most
+ * pixel row.
+ */
+ private void drawGlyph(final Graphics gr, final Cell cell,
+ final int xPixel, final int yPixel) {
+
+ /*
+ System.err.println("drawGlyph(): " + xPixel + " " + yPixel +
+ " " + cell);
+ */
+
+ BufferedImage image = null;
+ if (cell.isBlink() && !cursorBlinkVisible) {
+ image = glyphCacheBlink.get(cell);
+ } else {
+ image = glyphCache.get(cell);
+ }
+ if (image != null) {
+ if (swing.getFrame() != null) {
+ gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+ } else {
+ gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+ }
+ return;
+ }
+
+ // Generate glyph and draw it.
+ Graphics2D gr2 = null;
+ int gr2x = xPixel;
+ int gr2y = yPixel;
+ if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
+ image = new BufferedImage(textWidth, textHeight,
+ BufferedImage.TYPE_INT_ARGB);
+ gr2 = image.createGraphics();
+ gr2.setFont(swing.getFont());
+ gr2x = 0;
+ gr2y = 0;
+ } else {
+ gr2 = (Graphics2D) gr;
+ }
+
+ Cell cellColor = new Cell();
+ cellColor.setTo(cell);
+
+ // Check for reverse
+ if (cell.isReverse()) {
+ cellColor.setForeColor(cell.getBackColor());
+ cellColor.setBackColor(cell.getForeColor());
+ }
+
+ // Draw the background rectangle, then the foreground character.
+ gr2.setColor(attrToBackgroundColor(cellColor));
+ gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
+
+ // Handle blink and underline
+ if (!cell.isBlink()
+ || (cell.isBlink() && cursorBlinkVisible)
+ ) {
+ gr2.setColor(attrToForegroundColor(cellColor));
+ char [] chars = new char[1];
+ chars[0] = cell.getChar();
+ gr2.drawChars(chars, 0, 1, gr2x + textAdjustX,
+ gr2y + textHeight - maxDescent + textAdjustY);
+
+ if (cell.isUnderline()) {
+ gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2);
+ }
+ }
+
+ if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
+ gr2.dispose();
+
+ // We need a new key that will not be mutated by
+ // invertCell().
+ Cell key = new Cell();
+ key.setTo(cell);
+ if (cell.isBlink() && !cursorBlinkVisible) {
+ glyphCacheBlink.put(key, image);
+ } else {
+ glyphCache.put(key, image);
+ }
+
+ if (swing.getFrame() != null) {
+ gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+ } else {
+ gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+ }
+ }
+
+ }
+
+ /**
+ * Check if the cursor is visible, and if so draw it.
+ *
+ * @param gr the Swing Graphics context
+ */
+ private void drawCursor(final Graphics gr) {
+
+ if (cursorVisible
+ && (cursorY >= 0)
+ && (cursorX >= 0)
+ && (cursorY <= height - 1)
+ && (cursorX <= width - 1)
+ && cursorBlinkVisible
+ ) {
+ int xPixel = cursorX * textWidth + left;
+ int yPixel = cursorY * textHeight + top;
+ Cell lCell = logical[cursorX][cursorY];
+ gr.setColor(attrToForegroundColor(lCell));
+ switch (cursorStyle) {
+ default:
+ // Fall through...
+ case UNDERLINE:
+ gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2);
+ break;
+ case BLOCK:
+ gr.fillRect(xPixel, yPixel, textWidth, textHeight);
+ break;
+ case OUTLINE:
+ gr.drawRect(xPixel, yPixel, textWidth - 1, textHeight - 1);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Reset the blink timer.
+ */
+ private void resetBlinkTimer() {
+ lastBlinkTime = System.currentTimeMillis();
+ cursorBlinkVisible = true;
+ }
+
+ /**
+ * Paint redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ public void paint(final Graphics gr) {
+
+ if (gotFontDimensions == false) {
+ // Lazy-load the text width/height
+ getFontDimensions(gr);
+ /*
+ System.err.println("textWidth " + textWidth +
+ " textHeight " + textHeight);
+ System.err.println("FONT: " + swing.getFont() + " font " + font);
+ */
}
- try {
- SwingUtilities.invokeAndWait(new Runnable() {
- public void run() {
+ if ((swing.getFrame() != null)
+ && (swing.getBufferStrategy() != null)
+ && (SwingUtilities.isEventDispatchThread())
+ ) {
+ // System.err.println("paint(), skip first paint on swing thread");
+ return;
+ }
+
+ int xCellMin = 0;
+ int xCellMax = width;
+ int yCellMin = 0;
+ int yCellMax = height;
+
+ Rectangle bounds = gr.getClipBounds();
+ if (bounds != null) {
+ // Only update what is in the bounds
+ xCellMin = textColumn(bounds.x);
+ xCellMax = textColumn(bounds.x + bounds.width);
+ if (xCellMax > width) {
+ xCellMax = width;
+ }
+ if (xCellMin >= xCellMax) {
+ xCellMin = xCellMax - 2;
+ }
+ if (xCellMin < 0) {
+ xCellMin = 0;
+ }
+ yCellMin = textRow(bounds.y);
+ yCellMax = textRow(bounds.y + bounds.height);
+ if (yCellMax > height) {
+ yCellMax = height;
+ }
+ if (yCellMin >= yCellMax) {
+ yCellMin = yCellMax - 2;
+ }
+ if (yCellMin < 0) {
+ yCellMin = 0;
+ }
+ } else {
+ // We need a total repaint
+ reallyCleared = true;
+ }
+
+ // Prevent updates to the screen's data from the TApplication
+ // threads.
+ synchronized (this) {
+
+ /*
+ System.err.printf("bounds %s X %d %d Y %d %d\n",
+ bounds, xCellMin, xCellMax, yCellMin, yCellMax);
+ */
+
+ for (int y = yCellMin; y < yCellMax; y++) {
+ for (int x = xCellMin; x < xCellMax; x++) {
+
+ int xPixel = x * textWidth + left;
+ int yPixel = y * textHeight + top;
+
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+
+ if (!lCell.equals(pCell)
+ || lCell.isBlink()
+ || reallyCleared
+ || (swing.getFrame() == null)) {
+
+ if (lCell.isImage()) {
+ drawImage(gr, lCell, xPixel, yPixel);
+ } else {
+ drawGlyph(gr, lCell, xPixel, yPixel);
+ }
+
+ // Physical is always updated
+ physical[x][y].setTo(lCell);
+ }
+ }
+ }
+ drawCursor(gr);
+
+ reallyCleared = false;
+ } // synchronized (this)
+ }
- JComponent newComponent = new JComponent() {
+ /**
+ * Restore terminal to normal state.
+ */
+ public void shutdown() {
+ swing.dispose();
+ }
- /**
- * Serializable version.
- */
- private static final long serialVersionUID = 1;
+ /**
+ * Push the logical screen to the physical device.
+ */
+ private void drawToSwing() {
- /**
- * The code that performs the actual drawing.
- */
- public SwingTerminal screen = null;
+ /*
+ System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
+ reallyCleared, dirty);
+ */
- /*
- * Anonymous class initializer saves the screen
- * reference, so that paint() and the like call out
- * to SwingTerminal.
- */
- {
- this.screen = SwingTerminal.this;
- }
+ // If reallyCleared is set, we have to draw everything.
+ if ((swing.getFrame() != null)
+ && (swing.getBufferStrategy() != null)
+ && (reallyCleared == true)
+ ) {
+ // Triple-buffering: we have to redraw everything on this thread.
+ Graphics gr = swing.getBufferStrategy().getDrawGraphics();
+ swing.paint(gr);
+ gr.dispose();
+ swing.getBufferStrategy().show();
+ Toolkit.getDefaultToolkit().sync();
+ return;
+ } else if (((swing.getFrame() != null)
+ && (swing.getBufferStrategy() == null))
+ || (reallyCleared == true)
+ ) {
+ // Repaint everything on the Swing thread.
+ // System.err.println("REPAINT ALL");
+ swing.repaint();
+ return;
+ }
- /**
- * Update redraws the whole screen.
- *
- * @param gr the Swing Graphics context
- */
- @Override
- public void update(final Graphics gr) {
- // The default update clears the area. Don't do
- // that, instead just paint it directly.
- paint(gr);
- }
+ if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
+ Graphics gr = swing.getBufferStrategy().getDrawGraphics();
- /**
- * Paint redraws the whole screen.
- *
- * @param gr the Swing Graphics context
- */
- @Override
- public void paint(final Graphics gr) {
- if (screen != null) {
- screen.paint(gr);
+ synchronized (this) {
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+
+ int xPixel = x * textWidth + left;
+ int yPixel = y * textHeight + top;
+
+ if (!lCell.equals(pCell)
+ || ((x == cursorX)
+ && (y == cursorY)
+ && cursorVisible)
+ || (lCell.isBlink())
+ ) {
+ if (lCell.isImage()) {
+ drawImage(gr, lCell, xPixel, yPixel);
+ } else {
+ drawGlyph(gr, lCell, xPixel, yPixel);
}
+ physical[x][y].setTo(lCell);
}
- };
- component.setLayout(new BorderLayout());
- component.add(newComponent);
+ }
+ }
+ drawCursor(gr);
+ } // synchronized (this)
- // Allow key events to be received
- component.setFocusable(true);
+ gr.dispose();
+ swing.getBufferStrategy().show();
+ Toolkit.getDefaultToolkit().sync();
+ return;
+ }
- // Get the Swing component
- SwingTerminal.this.swing = new SwingComponent(component);
+ // Swing thread version: request a repaint, but limit it to the area
+ // that has changed.
- // Hang onto top and left for drawing.
- Insets insets = SwingTerminal.this.swing.getInsets();
- SwingTerminal.this.left = insets.left;
- SwingTerminal.this.top = insets.top;
+ // Find the minimum-size damaged region.
+ int xMin = swing.getWidth();
+ int xMax = 0;
+ int yMin = swing.getHeight();
+ int yMax = 0;
- // Load the font so that we can set sessionInfo.
- getDefaultFont();
+ synchronized (this) {
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
- // Get the default cols x rows and set component size
- // accordingly.
- SwingTerminal.this.sessionInfo =
- new SwingSessionInfo(SwingTerminal.this.swing,
- SwingTerminal.this.textWidth,
- SwingTerminal.this.textHeight);
+ int xPixel = x * textWidth + left;
+ int yPixel = y * textHeight + top;
+
+ if (!lCell.equals(pCell)
+ || ((x == cursorX)
+ && (y == cursorY)
+ && cursorVisible)
+ || lCell.isBlink()
+ ) {
+ if (xPixel < xMin) {
+ xMin = xPixel;
+ }
+ if (xPixel + textWidth > xMax) {
+ xMax = xPixel + textWidth;
+ }
+ if (yPixel < yMin) {
+ yMin = yPixel;
+ }
+ if (yPixel + textHeight > yMax) {
+ yMax = yPixel + textHeight;
+ }
+ }
}
- });
- } catch (Exception e) {
- e.printStackTrace();
+ }
+ }
+ if (xMin + textWidth >= xMax) {
+ xMax += textWidth;
+ }
+ if (yMin + textHeight >= yMax) {
+ yMax += textHeight;
}
- this.listener = listener;
- mouse1 = false;
- mouse2 = false;
- mouse3 = false;
- eventQueue = new LinkedList<TInputEvent>();
+ // Repaint the desired area
+ /*
+ System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
+ yMin, yMax);
+ */
- // Add listeners to Swing.
- swing.addKeyListener(this);
- swing.addWindowListener(this);
- swing.addComponentListener(this);
- swing.addMouseListener(this);
- swing.addMouseMotionListener(this);
- swing.addMouseWheelListener(this);
+ if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
+ // This path should never be taken, but is left here for
+ // completeness.
+ Graphics gr = swing.getBufferStrategy().getDrawGraphics();
+ Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
+ yMax - yMin);
+ gr.setClip(bounds);
+ swing.paint(gr);
+ gr.dispose();
+ swing.getBufferStrategy().show();
+ Toolkit.getDefaultToolkit().sync();
+ } else {
+ // Repaint on the Swing thread.
+ swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
+ }
}
/**
- * Check if there are events in the queue.
+ * Convert pixel column position to text cell column position.
*
- * @return if true, getEvents() has something to return to the backend
+ * @param x pixel column position
+ * @return text cell column position
*/
- public boolean hasEvents() {
- synchronized (eventQueue) {
- return (eventQueue.size() > 0);
+ public int textColumn(final int x) {
+ int column = ((x - left) / textWidth);
+ if (column < 0) {
+ column = 0;
+ }
+ if (column > width - 1) {
+ column = width - 1;
}
+ return column;
}
/**
- * Return any events in the IO queue.
+ * Convert pixel row position to text cell row position.
*
- * @param queue list to append new events to
+ * @param y pixel row position
+ * @return text cell row position
*/
- public void getEvents(final List<TInputEvent> queue) {
- synchronized (eventQueue) {
- if (eventQueue.size() > 0) {
- synchronized (queue) {
- queue.addAll(eventQueue);
- }
- eventQueue.clear();
- }
+ public int textRow(final int y) {
+ int row = ((y - top) / textHeight);
+ if (row < 0) {
+ row = 0;
}
+ if (row > height - 1) {
+ row = height - 1;
+ }
+ return row;
}
/**
- * Restore terminal to normal state.
+ * Getter for sessionInfo.
+ *
+ * @return the SessionInfo
*/
- public void closeTerminal() {
- shutdown();
+ public SessionInfo getSessionInfo() {
+ return sessionInfo;
+ }
+
+ /**
+ * Getter for the underlying Swing component.
+ *
+ * @return the SwingComponent
+ */
+ public SwingComponent getSwingComponent() {
+ return swing;
}
+ // ------------------------------------------------------------------------
+ // KeyListener ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
* Pass Swing keystrokes into the event queue.
*
}
}
+ // ------------------------------------------------------------------------
+ // WindowListener ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
* Pass window events into the event queue.
*
// Ignore
}
+ // ------------------------------------------------------------------------
+ // ComponentListener ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
* Pass component events into the event queue.
*
sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
eventQueue.add(windowResize);
resetBlinkTimer();
+ /*
+ System.err.println("Add resize event: " + windowResize.getWidth() +
+ " x " + windowResize.getHeight());
+ */
}
if (listener != null) {
synchronized (listener) {
}
}
+ // ------------------------------------------------------------------------
+ // MouseMotionListener ----------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
* Pass mouse events into the event queue.
*
}
}
+ // ------------------------------------------------------------------------
+ // MouseListener ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
* Pass mouse events into the event queue.
*
}
}
+ // ------------------------------------------------------------------------
+ // MouseWheelListener -----------------------------------------------------
+ // ------------------------------------------------------------------------
+
/**
* Pass mouse events into the event queue.
*