X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fbackend%2FSwingTerminal.java;h=0727efc894d5832dc515fa1342d622a2eac885df;hb=c4cefaa04ec122fc02efb6542451a31fdf722c32;hp=6e902195fa59d40a8306fc1f27a713440992c364;hpb=3e0743556d1f31723a11a6019b5c2b018b4b2104;p=nikiroo-utils.git diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index 6e90219..0727efc 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -3,7 +3,7 @@ * * 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"), @@ -36,6 +36,8 @@ import java.awt.Graphics2D; import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Toolkit; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.KeyEvent; @@ -50,12 +52,13 @@ import java.awt.event.WindowListener; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.InputStream; -import java.util.Date; +import java.util.ArrayList; 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; @@ -78,20 +81,25 @@ import static 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. + */ + public static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf"; /** * Cursor style to draw. @@ -110,20 +118,17 @@ public final class SwingTerminal extends LogicalScreen /** * Use an outlined block for the cursor. */ - OUTLINE - } + OUTLINE, - /** - * A cache of previously-rendered glyphs for blinking text, when it is - * not visible. - */ - private HashMap glyphCacheBlink; + /** + * Use a vertical bar for the cursor. + */ + VERTICAL_BAR, + } - /** - * A cache of previously-rendered glyphs for non-blinking, or - * blinking-and-visible, text. - */ - private HashMap glyphCache; + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ // Colors to map DOS colors to AWT colors. private static Color MYBLACK; @@ -149,41 +154,21 @@ public final class SwingTerminal extends LogicalScreen 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 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 glyphCache; /** * If true, we were successful at getting the font dimensions. @@ -203,12 +188,22 @@ public final class SwingTerminal extends LogicalScreen /** * Width of a character cell in pixels. */ - private int textWidth = 1; + private int textWidth = 16; /** * Height of a character cell in pixels. */ - private int textHeight = 1; + private int textHeight = 20; + + /** + * 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. @@ -225,6 +220,16 @@ public final class SwingTerminal extends LogicalScreen */ 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. */ @@ -241,8 +246,8 @@ public final class SwingTerminal extends LogicalScreen private CursorStyle cursorStyle = CursorStyle.UNDERLINE; /** - * The number of millis to wait before switching the blink from - * visible to invisible. + * The number of millis to wait before switching the blink from visible + * to invisible. Set to 0 or negative to disable blinking. */ private long blinkMillis = 500; @@ -259,1070 +264,1469 @@ public final class SwingTerminal extends LogicalScreen 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(); - glyphCache = new HashMap(); - resizeToScreen(); - } + private List 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); - } + private int oldMouseX = -1; - setFont(font); - } + /** + * The last reported mouse Y position. + */ + private int oldMouseY = -1; /** - * Convert a CellAttributes foreground 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 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 boolean mouse1 = false; /** - * Convert a CellAttributes background color to an Swing Color. - * - * @param attr the text attributes - * @return the Swing Color + * true if mouse2 was down. Used to report mouse2 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 mouse2 = 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 mouse3 was down. Used to report mouse3 on the release event. */ - private boolean getFontAdjustments() { - BufferedImage image = null; + private boolean mouse3 = 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. + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ - Graphics2D gr2 = null; - int gr2x = 3; - int gr2y = 3; - image = new BufferedImage(textWidth * 2, textHeight * 2, - BufferedImage.TYPE_INT_ARGB); + /** + * Static constructor. + */ + static { + setDOSColors(); + } - 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)); - */ + 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); + } + + /** + * Paint redraws the whole screen. + * + * @param gr the Swing Graphics context + */ + @Override + public void paint(final Graphics gr) { + if (screen != null) { + screen.paint(gr); + } + } + }; + + // Set icon + ClassLoader loader = Thread.currentThread(). + getContextClassLoader(); + frame.setIconImage((new ImageIcon(loader. + getResource(ICONFILE))).getImage()); + + // Get the Swing component + SwingTerminal.this.swing = new SwingComponent(frame); - // System.err.println("textAdjustX: " + textAdjustX); - return true; + // Hang onto top and left for drawing. + Insets insets = SwingTerminal.this.swing.getInsets(); + SwingTerminal.this.left = insets.left; + SwingTerminal.this.top = insets.top; + + // Load the font so that we can set sessionInfo. + setDefaultFont(); + + // 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); + + SwingTerminal.this.setDimensions(sessionInfo. + getWindowWidth(), sessionInfo.getWindowHeight()); + + SwingTerminal.this.resizeToScreen(true); + SwingTerminal.this.swing.setVisible(true); } - } + }); + } catch (java.lang.reflect.InvocationTargetException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); } - // Something weird happened, don't rely on this function. - // System.err.println("getFontAdjustments: false"); - return false; + this.listener = listener; + mouse1 = false; + mouse2 = false; + mouse3 = false; + eventQueue = new ArrayList(); + + // 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. + * Public constructor renders to an existing JComponent. + * + * @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 getFontDimensions() { - swing.setFont(font); - Graphics gr = swing.getGraphics(); - if (gr == null) { - return; + public SwingTerminal(final JComponent component, final int windowWidth, + final int windowHeight, final int fontSize, final Object listener) { + + this.fontSize = fontSize; + + reloadOptions(); + + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + + JComponent newComponent = new JComponent() { + + /** + * Serializable version. + */ + private static final long serialVersionUID = 1; + + /** + * The code that performs the actual drawing. + */ + public SwingTerminal screen = null; + + /* + * Anonymous class initializer saves the screen + * reference, so that paint() and the like call out + * to SwingTerminal. + */ + { + this.screen = SwingTerminal.this; + } + + /** + * 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); + } + + /** + * 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); + + // Allow key events to be received + component.setFocusable(true); + + // 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; + + // Load the font so that we can set sessionInfo. + setDefaultFont(); + + // 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(); } - getFontDimensions(gr); + + this.listener = listener; + mouse1 = false; + mouse2 = false; + mouse3 = false; + eventQueue = new ArrayList(); + + // Add listeners to Swing. + swing.addKeyListener(this); + swing.addWindowListener(this); + swing.addComponentListener(this); + swing.addMouseListener(this); + swing.addMouseMotionListener(this); + swing.addMouseWheelListener(this); } + // ------------------------------------------------------------------------ + // LogicalScreen ---------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Figure out my font dimensions. This code path is needed to lazy-load - * the information inside paint(). + * Set the window title. * - * @param gr Graphics object to use + * @param title the new title */ - 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; + @Override + public void setTitle(final String title) { + swing.setTitle(title); + } - // This produces the same number, but works better for ugly - // monospace. - textHeight = fm.getMaxAscent() + maxDescent - leading; + /** + * Push the logical screen to the physical device. + */ + @Override + public void flushPhysical() { + // See if it is time to flip the blink time. + long nowTime = System.currentTimeMillis(); + if (nowTime >= blinkMillis + lastBlinkTime) { + lastBlinkTime = nowTime; + cursorBlinkVisible = !cursorBlinkVisible; + // System.err.println("New lastBlinkTime: " + lastBlinkTime); + } - if (gotTerminus == true) { - textHeight++; + if ((swing.getFrame() != null) + && (swing.getBufferStrategy() != null) + ) { + do { + do { + drawToSwing(); + } while (swing.getBufferStrategy().contentsRestored()); + + swing.getBufferStrategy().show(); + Toolkit.getDefaultToolkit().sync(); + } while (swing.getBufferStrategy().contentsLost()); + } else { + // Non-triple-buffered, call drawToSwing() once + drawToSwing(); } + } - 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; - } + // ------------------------------------------------------------------------ + // TerminalReader --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the backend + */ + public boolean hasEvents() { + synchronized (eventQueue) { + return (eventQueue.size() > 0); } + } - if (sessionInfo != null) { - sessionInfo.setTextCellDimensions(textWidth, textHeight); + /** + * Return any events in the IO queue. + * + * @param queue list to append new events to + */ + public void getEvents(final List queue) { + synchronized (eventQueue) { + if (eventQueue.size() > 0) { + synchronized (queue) { + queue.addAll(eventQueue); + } + eventQueue.clear(); + } } - gotFontDimensions = true; } /** - * Resize to font dimensions. + * Restore terminal to normal state. */ - public void resizeToScreen() { - swing.setDimensions(textWidth * width, textHeight * height); + public void closeTerminal() { + shutdown(); } /** - * Draw one glyph to the screen. + * Set listener to a different Object. * - * @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 listener the new listening object that run() wakes up on new + * input */ - private void drawGlyph(final Graphics gr, final Cell cell, - final int xPixel, final int yPixel) { + public void setListener(final Object listener) { + this.listener = listener; + } - /* - System.err.println("drawGlyph(): " + xPixel + " " + yPixel + - " " + cell); - */ + /** + * 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; + } else if (cursorStyleString.equals("verticalbar")) { + cursorStyle = CursorStyle.VERTICAL_BAR; + } - BufferedImage image = null; - if (cell.isBlink() && !cursorBlinkVisible) { - image = glyphCacheBlink.get(cell); + // Pull the system property for triple buffering. + if (System.getProperty("jexer.Swing.tripleBuffer", + "true").equals("true") + ) { + SwingComponent.tripleBuffer = true; } else { - image = glyphCache.get(cell); + SwingComponent.tripleBuffer = false; } - if (image != null) { - if (swing.getFrame() != null) { - gr.drawImage(image, xPixel, yPixel, swing.getFrame()); - } else { - gr.drawImage(image, xPixel, yPixel, swing.getComponent()); - } + + // Set custom colors + setCustomSystemColors(); + } + + // ------------------------------------------------------------------------ + // SwingTerminal ---------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Get the width of a character cell in pixels. + * + * @return the width in pixels of a character cell + */ + public int getTextWidth() { + return textWidth; + } + + /** + * Get the height of a character cell in pixels. + * + * @return the height in pixels of a character cell + */ + public int getTextHeight() { + return textHeight; + } + + /** + * 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); - // 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; + dosColors = true; + } + + /** + * Setup Swing colors to match those provided in system properties. + */ + private static void setCustomSystemColors() { + synchronized (SwingTerminal.class) { + MYBLACK = getCustomColor("jexer.Swing.color0", MYBLACK); + MYRED = getCustomColor("jexer.Swing.color1", MYRED); + MYGREEN = getCustomColor("jexer.Swing.color2", MYGREEN); + MYYELLOW = getCustomColor("jexer.Swing.color3", MYYELLOW); + MYBLUE = getCustomColor("jexer.Swing.color4", MYBLUE); + MYMAGENTA = getCustomColor("jexer.Swing.color5", MYMAGENTA); + MYCYAN = getCustomColor("jexer.Swing.color6", MYCYAN); + MYWHITE = getCustomColor("jexer.Swing.color7", MYWHITE); + MYBOLD_BLACK = getCustomColor("jexer.Swing.color8", MYBOLD_BLACK); + MYBOLD_RED = getCustomColor("jexer.Swing.color9", MYBOLD_RED); + MYBOLD_GREEN = getCustomColor("jexer.Swing.color10", MYBOLD_GREEN); + MYBOLD_YELLOW = getCustomColor("jexer.Swing.color11", MYBOLD_YELLOW); + MYBOLD_BLUE = getCustomColor("jexer.Swing.color12", MYBOLD_BLUE); + MYBOLD_MAGENTA = getCustomColor("jexer.Swing.color13", MYBOLD_MAGENTA); + MYBOLD_CYAN = getCustomColor("jexer.Swing.color14", MYBOLD_CYAN); + MYBOLD_WHITE = getCustomColor("jexer.Swing.color15", MYBOLD_WHITE); } + } - Cell cellColor = new Cell(); - cellColor.setTo(cell); + /** + * Setup one Swing color to match the RGB value provided in system + * properties. + * + * @param key the system property key + * @param defaultColor the default color to return if key is not set, or + * incorrect + * @return a color from the RGB string, or defaultColor + */ + private static Color getCustomColor(final String key, + final Color defaultColor) { - // Check for reverse - if (cell.isReverse()) { - cellColor.setForeColor(cell.getBackColor()); - cellColor.setBackColor(cell.getForeColor()); + String rgb = System.getProperty(key); + if (rgb == null) { + return defaultColor; } + if (rgb.startsWith("#")) { + rgb = rgb.substring(1); + } + int rgbInt = 0; + try { + rgbInt = Integer.parseInt(rgb, 16); + } catch (NumberFormatException e) { + return defaultColor; + } + Color color = new Color((rgbInt & 0xFF0000) >>> 16, + (rgbInt & 0x00FF00) >>> 8, + (rgbInt & 0x0000FF)); - // Draw the background rectangle, then the foreground character. - gr2.setColor(attrToBackgroundColor(cellColor)); - gr2.fillRect(gr2x, gr2y, textWidth, textHeight); + return color; + } - // 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); + /** + * 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 (cell.isUnderline()) { - gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2); - } - } + /** + * Get the current status of the blink flag. + * + * @return true if the cursor and blinking text should be visible + */ + public boolean getCursorBlinkVisible() { + return cursorBlinkVisible; + } - if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) { - gr2.dispose(); + /** + * Get the font size in points. + * + * @return font size in points + */ + public int getFontSize() { + return fontSize; + } - // 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); - } + /** + * Set the font size in points. + * + * @param fontSize font size in points + */ + public void setFontSize(final int fontSize) { + this.fontSize = fontSize; + Font newFont = font.deriveFont((float) fontSize); + setFont(newFont); + } - if (swing.getFrame() != null) { - gr.drawImage(image, xPixel, yPixel, swing.getFrame()); - } else { - gr.drawImage(image, xPixel, yPixel, swing.getComponent()); + /** + * Set to a new font, and resize the screen to match its dimensions. + * + * @param font the new font + */ + public void setFont(final Font font) { + if (!SwingUtilities.isEventDispatchThread()) { + // Not in the Swing thread: force this inside the Swing thread. + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + synchronized (this) { + SwingTerminal.this.font = font; + getFontDimensions(); + swing.setFont(font); + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + resizeToScreen(true); + } + } + }); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (java.lang.reflect.InvocationTargetException e) { + e.printStackTrace(); + } + } else { + synchronized (this) { + SwingTerminal.this.font = font; + getFontDimensions(); + swing.setFont(font); + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + resizeToScreen(true); } } - } /** - * Check if the cursor is visible, and if so draw it. + * Get the font this screen was last set to. * - * @param gr the Swing Graphics context + * @return the font */ - private void drawCursor(final Graphics gr) { + public Font getFont() { + return font; + } - if (cursorVisible - && (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; - } + /** + * Set the font to Terminus, the best all-around font for both CP437 and + * ISO8859-1. + */ + 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); } /** - * Paint redraws the whole screen. + * Get the X text adjustment. * - * @param gr the Swing Graphics context + * @return X text adjustment */ - public void paint(final Graphics gr) { - - if (gotFontDimensions == false) { - // Lazy-load the text width/height - // System.err.println("calling getFontDimensions..."); - getFontDimensions(gr); - /* - System.err.println("textWidth " + textWidth + - " textHeight " + textHeight); - System.err.println("FONT: " + swing.getFont() + " font " + font); - */ - // resizeToScreen(); - } + public int getTextAdjustX() { + return textAdjustX; + } - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime > blinkMillis + lastBlinkTime) { - lastBlinkTime = nowTime; - cursorBlinkVisible = !cursorBlinkVisible; + /** + * Set the X text adjustment. + * + * @param textAdjustX the X text adjustment + */ + public void setTextAdjustX(final int textAdjustX) { + synchronized (this) { + this.textAdjustX = textAdjustX; + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + clearPhysical(); } + } - 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; - } + /** + * Get the Y text adjustment. + * + * @return Y text adjustment + */ + public int getTextAdjustY() { + return textAdjustY; + } - // Prevent updates to the screen's data from the TApplication - // threads. + /** + * Set the Y text adjustment. + * + * @param textAdjustY the Y text adjustment + */ + public void setTextAdjustY(final int textAdjustY) { synchronized (this) { + this.textAdjustY = textAdjustY; + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + clearPhysical(); + } + } - /* - 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); + /** + * Get the height text adjustment. + * + * @return height text adjustment + */ + public int getTextAdjustHeight() { + return textAdjustHeight; + } - dirty = false; - reallyCleared = false; - } // synchronized (this) + /** + * Set the height text adjustment. + * + * @param textAdjustHeight the height text adjustment + */ + public void setTextAdjustHeight(final int textAdjustHeight) { + synchronized (this) { + this.textAdjustHeight = textAdjustHeight; + textHeight = fontTextHeight + textAdjustHeight; + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + clearPhysical(); + } } /** - * Restore terminal to normal state. + * Get the width text adjustment. + * + * @return width text adjustment */ - public void shutdown() { - swing.dispose(); + public int getTextAdjustWidth() { + return textAdjustWidth; } /** - * Push the logical screen to the physical device. + * Set the width text adjustment. + * + * @param textAdjustWidth the width text adjustment */ - @Override - public void flushPhysical() { + public void setTextAdjustWidth(final int textAdjustWidth) { + synchronized (this) { + this.textAdjustWidth = textAdjustWidth; + textWidth = fontTextWidth + textAdjustWidth; + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + clearPhysical(); + } + } - /* - System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n", - reallyCleared, dirty); - */ + /** + * Convert a CellAttributes foreground color to an Swing Color. + * + * @param attr the text attributes + * @return the Swing Color + */ + public static 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; - // 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(); - // sync() doesn't seem to help the tearing for me. - // 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; + return new Color(red, green, blue); } - // Do nothing if nothing happened. - if (!dirty) { - return; + 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()); + } - if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) { - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime > blinkMillis + lastBlinkTime) { - lastBlinkTime = nowTime; - cursorBlinkVisible = !cursorBlinkVisible; - } + /** + * Convert a CellAttributes background color to an Swing Color. + * + * @param attr the text attributes + * @return the Swing Color + */ + public static 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; - Graphics gr = swing.getBufferStrategy().getDrawGraphics(); + return new Color(red, green, blue); + } - 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]; + 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()); + } - int xPixel = x * textWidth + left; - int yPixel = y * textHeight + top; + /** + * 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; - 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) + // 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. - gr.dispose(); - swing.getBufferStrategy().show(); - // sync() doesn't seem to help the tearing for me. - // Toolkit.getDefaultToolkit().sync(); - return; - } + Graphics2D gr2 = null; + int gr2x = 3; + int gr2y = 3; + image = new BufferedImage(fontTextWidth * 2, fontTextHeight * 2, + BufferedImage.TYPE_INT_ARGB); - // Swing thread version: request a repaint, but limit it to the area - // that has changed. + 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(); - // Find the minimum-size damaged region. - int xMin = swing.getWidth(); - int xMax = 0; - int yMin = swing.getHeight(); - int yMax = 0; + int top = fontTextHeight * 2; + int bottom = -1; + int left = fontTextWidth * 2; + int right = -1; + textAdjustX = 0; + textAdjustY = 0; + textAdjustHeight = 0; + textAdjustWidth = 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]; + for (int x = 0; x < fontTextWidth * 2; x++) { + for (int y = 0; y < fontTextHeight * 2; y++) { - int xPixel = x * textWidth + left; - int yPixel = y * textHeight + top; + /* + System.err.println("H X: " + x + " Y: " + y + " " + + image.getRGB(x, y)); + */ - 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 ((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; } } } } - if (xMin + textWidth >= xMax) { - xMax += textWidth; + if (left < right) { + textAdjustX = (gr2x - left); + textAdjustWidth = fontTextWidth - (right - left + 1); } - if (yMin + textHeight >= yMax) { - yMax += textHeight; + 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); - // Repaint the desired area - /* - System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax, - yMin, yMax); - */ + // 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; + } + } - 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(); - // sync() doesn't seem to help the tearing for me. - // Toolkit.getDefaultToolkit().sync(); - } else { - // Repaint on the Swing thread. - swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin); + /** + * 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); } /** - * Put the cursor at (x,y). + * Figure out my font dimensions. This code path is needed to lazy-load + * the information inside paint(). * - * @param visible if true, the cursor should be visible - * @param x column coordinate to put the cursor on - * @param y row coordinate to put the cursor on + * @param gr Graphics object to use */ - @Override - public void putCursor(final boolean visible, final int x, final int y) { + 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; - if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) { - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime < blinkMillis + lastBlinkTime) { - // Nothing has changed, so don't do anything. - return; - } - } + // This produces the same number, but works better for ugly + // monospace. + fontTextHeight = fm.getMaxAscent() + maxDescent - leading; - if (cursorVisible - && (cursorY <= height - 1) - && (cursorX <= width - 1) - ) { - // Make the current cursor position dirty - if (physical[cursorX][cursorY].getChar() == 'Q') { - physical[cursorX][cursorY].setChar('X'); - } else { - physical[cursorX][cursorY].setChar('Q'); - } - } + getFontAdjustments(); + textHeight = fontTextHeight + textAdjustHeight; + textWidth = fontTextWidth + textAdjustWidth; - super.putCursor(visible, x, y); + if (sessionInfo != null) { + sessionInfo.setTextCellDimensions(textWidth, textHeight); + } + gotFontDimensions = true; } /** - * Convert pixel column position to text cell column position. + * Resize the physical screen to match the logical screen dimensions. * - * @param x pixel column position - * @return text cell column position + * @param resizeComponent if true, resize the Swing component */ - public int textColumn(final int x) { - return ((x - left) / textWidth); + private void resizeToScreen(final boolean resizeComponent) { + if (resizeComponent) { + swing.setDimensions(textWidth * width, textHeight * height); + } + clearPhysical(); } /** - * Convert pixel row position to text cell row position. - * - * @param y pixel row position - * @return text cell row position + * Resize the physical screen to match the logical screen dimensions. */ - public int textRow(final int y) { - return ((y - top) / textHeight); + @Override + public void resizeToScreen() { + resizeToScreen(false); } /** - * Set the window title. + * Draw one cell's image to the screen. * - * @param title the new title + * @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. */ - public void setTitle(final String title) { - swing.setTitle(title); - } + private void drawImage(final Graphics gr, final Cell cell, + final int xPixel, final int yPixel) { - // ------------------------------------------------------------------------ - // TerminalReader --------------------------------------------------------- - // ------------------------------------------------------------------------ + /* + System.err.println("drawImage(): " + xPixel + " " + yPixel + + " " + cell); + */ - /** - * The session information. - */ - private SwingSessionInfo sessionInfo; + // Draw the background rectangle, then the foreground character. + assert (cell.isImage()); - /** - * Getter for sessionInfo. - * - * @return the SessionInfo - */ - public SessionInfo getSessionInfo() { - return sessionInfo; - } + // Enable anti-aliasing + if (gr instanceof Graphics2D) { + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + } - /** - * The listening object that run() wakes up on new input. - */ - private Object listener; + gr.setColor(cell.getBackground()); + gr.fillRect(xPixel, yPixel, textWidth, textHeight); - /** - * 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; + BufferedImage image = cell.getImage(); + if (image != null) { + if (swing.getFrame() != null) { + gr.drawImage(image, xPixel, yPixel, getTextWidth(), + getTextHeight(), swing.getFrame()); + } else { + gr.drawImage(image, xPixel, yPixel, getTextWidth(), + getTextHeight(),swing.getComponent()); + } + return; + } } /** - * The event queue, filled up by a thread reading on input. + * 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 List eventQueue; + private void drawGlyph(final Graphics gr, final Cell cell, + final int xPixel, final int yPixel) { - /** - * The last reported mouse X position. - */ - private int oldMouseX = -1; + /* + System.err.println("drawGlyph(): " + xPixel + " " + yPixel + + " " + cell); + */ - /** - * The last reported mouse Y position. - */ - private int oldMouseY = -1; + 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(cell); + + // Check for reverse + if (cell.isReverse()) { + cellColor.setForeColor(cell.getBackColor()); + cellColor.setBackColor(cell.getForeColor()); + } + + // Enable anti-aliasing + if ((gr instanceof Graphics2D) && (swing.getFrame() != null)) { + // Anti-aliasing on JComponent makes the hash character disappear + // for Terminus font, and also kills performance. Only enable it + // for JFrame. + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + } + + // 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 = Character.toChars(cell.getChar()); + gr2.drawChars(chars, 0, chars.length, 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(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()); + } + } - /** - * true if mouse1 was down. Used to report mouse1 on the release event. - */ - private boolean mouse1 = false; + } /** - * true if mouse2 was down. Used to report mouse2 on the release event. + * Check if the cursor is visible, and if so draw it. + * + * @param gr the Swing Graphics context */ - private boolean mouse2 = false; + 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]; + int cursorWidth = textWidth; + switch (lCell.getWidth()) { + case SINGLE: + // NOP + break; + case LEFT: + cursorWidth *= 2; + break; + case RIGHT: + cursorWidth *= 2; + xPixel -= textWidth; + break; + } + gr.setColor(attrToForegroundColor(lCell)); + switch (cursorStyle) { + default: + // Fall through... + case UNDERLINE: + gr.fillRect(xPixel, yPixel + textHeight - 2, cursorWidth, 2); + break; + case BLOCK: + gr.fillRect(xPixel, yPixel, cursorWidth, textHeight); + break; + case OUTLINE: + gr.drawRect(xPixel, yPixel, cursorWidth - 1, textHeight - 1); + break; + case VERTICAL_BAR: + gr.fillRect(xPixel, yPixel, 2, textHeight); + break; + } + } + } /** - * true if mouse3 was down. Used to report mouse3 on the release event. + * Reset the blink timer. */ - private boolean mouse3 = false; + private void resetBlinkTimer() { + lastBlinkTime = System.currentTimeMillis(); + cursorBlinkVisible = true; + } /** - * Public constructor creates a new JFrame to render to. + * Paint redraws the whole screen. * - * @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 the Swing Graphics context */ - public SwingTerminal(final int windowWidth, final int windowHeight, - final int fontSize, final Object listener) { - - this.fontSize = fontSize; + public void paint(final Graphics gr) { - setDOSColors(); + 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); + */ + } - // 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) + && (SwingUtilities.isEventDispatchThread()) + ) { + // System.err.println("paint(), skip first paint on swing thread"); + return; } - // 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; + 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) + 1; + 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) + 1; + if (yCellMax > height) { + yCellMax = height; + } + if (yCellMin >= yCellMax) { + yCellMin = yCellMax - 2; + } + if (yCellMin < 0) { + yCellMin = 0; + } + } else { + // We need a total repaint + reallyCleared = true; } - try { - SwingUtilities.invokeAndWait(new Runnable() { - public void run() { + // Prevent updates to the screen's data from the TApplication + // threads. + synchronized (this) { - JFrame frame = new JFrame() { + /* + System.err.printf("bounds %s X %d %d Y %d %d\n", + bounds, xCellMin, xCellMax, yCellMin, yCellMax); + */ - /** - * Serializable version. - */ - private static final long serialVersionUID = 1; + for (int y = yCellMin; y < yCellMax; y++) { + for (int x = xCellMin; x < xCellMax; x++) { - /** - * The code that performs the actual drawing. - */ - public SwingTerminal screen = null; + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; - /* - * Anonymous class initializer saves the screen - * reference, so that paint() and the like call out - * to SwingTerminal. - */ - { - this.screen = SwingTerminal.this; - } + Cell lCell = logical[x][y]; + Cell pCell = physical[x][y]; - /** - * 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 (!lCell.equals(pCell) + || lCell.isBlink() + || reallyCleared + || (swing.getFrame() == null)) { - /** - * Paint redraws the whole screen. - * - * @param gr the Swing Graphics context - */ - @Override - public void paint(final Graphics gr) { - if (screen != null) { - screen.paint(gr); - } + if (lCell.isImage()) { + drawImage(gr, lCell, xPixel, yPixel); + } else { + drawGlyph(gr, lCell, xPixel, yPixel); } - }; - - // Get the Swing component - SwingTerminal.this.swing = new SwingComponent(frame); - - // Hang onto top and left for drawing. - Insets insets = SwingTerminal.this.swing.getInsets(); - SwingTerminal.this.left = insets.left; - SwingTerminal.this.top = insets.top; - - // Load the font so that we can set sessionInfo. - getDefaultFont(); - - // 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); - SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(), - sessionInfo.getWindowHeight()); - - SwingTerminal.this.resizeToScreen(); - SwingTerminal.this.swing.setVisible(true); + // Physical is always updated + physical[x][y].setTo(lCell); + } } - }); - } catch (Exception e) { - e.printStackTrace(); - } - - this.listener = listener; - mouse1 = false; - mouse2 = false; - mouse3 = false; - eventQueue = new LinkedList(); + } + drawCursor(gr); - // Add listeners to Swing. - swing.addKeyListener(this); - swing.addWindowListener(this); - swing.addComponentListener(this); - swing.addMouseListener(this); - swing.addMouseMotionListener(this); - swing.addMouseWheelListener(this); + reallyCleared = false; + } // synchronized (this) } /** - * Public constructor renders to an existing JComponent. - * - * @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 + * Restore terminal to normal state. */ - public SwingTerminal(final JComponent component, final int windowWidth, - final int windowHeight, final int fontSize, final Object listener) { + public void shutdown() { + swing.dispose(); + } - this.fontSize = fontSize; + /** + * Push the logical screen to the physical device. + */ + private void drawToSwing() { - setDOSColors(); + /* + System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n", + reallyCleared, dirty); + */ - // 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 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; } - try { - SwingUtilities.invokeAndWait(new Runnable() { - public void run() { - - JComponent newComponent = new JComponent() { + if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) { + Graphics gr = swing.getBufferStrategy().getDrawGraphics(); - /** - * Serializable version. - */ - private static final long serialVersionUID = 1; + 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]; - /** - * The code that performs the actual drawing. - */ - public SwingTerminal screen = null; + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; - /* - * Anonymous class initializer saves the screen - * reference, so that paint() and the like call out - * to SwingTerminal. - */ - { - this.screen = SwingTerminal.this; + 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); } + } + } + drawCursor(gr); + } // synchronized (this) - /** - * 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); - } + gr.dispose(); + swing.getBufferStrategy().show(); + Toolkit.getDefaultToolkit().sync(); + return; + } - /** - * 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); + // Swing thread version: request a repaint, but limit it to the area + // that has changed. - // Get the Swing component - SwingTerminal.this.swing = new SwingComponent(component); + // Find the minimum-size damaged region. + int xMin = swing.getWidth(); + int xMax = 0; + int yMin = swing.getHeight(); + int yMax = 0; - // Hang onto top and left for drawing. - Insets insets = SwingTerminal.this.swing.getInsets(); - SwingTerminal.this.left = insets.left; - SwingTerminal.this.top = insets.top; + 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]; - // Load the font so that we can set sessionInfo. - getDefaultFont(); + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; - // 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); + 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(); + // 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 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. * @@ -1357,13 +1761,16 @@ public final class SwingTerminal extends LogicalScreen } else { ch = key.getKeyChar(); } - alt = key.isAltDown(); + // Both meta and alt count as alt, thanks to Mac using alt for + // "symbols" so meta ("command") is the only other modifier left. + alt = key.isAltDown() | key.isMetaDown(); ctrl = key.isControlDown(); shift = key.isShiftDown(); /* System.err.printf("Swing Key: %s\n", key); System.err.printf(" isKey: %s\n", isKey); + System.err.printf(" meta: %s\n", key.isMetaDown()); System.err.printf(" alt: %s\n", alt); System.err.printf(" ctrl: %s\n", ctrl); System.err.printf(" shift: %s\n", shift); @@ -1494,8 +1901,7 @@ public final class SwingTerminal extends LogicalScreen alt, ctrl, shift); break; case KeyEvent.VK_BACK_SPACE: - // Special case: return it as kbBackspace (Ctrl-H) - keypress = new TKeypress(false, 0, 'H', false, true, false); + keypress = kbBackspace; break; default: // Unsupported, ignore @@ -1506,7 +1912,15 @@ public final class SwingTerminal extends LogicalScreen if (keypress == null) { switch (ch) { case 0x08: - keypress = kbBackspace; + // Disambiguate ^H from Backspace. + if (KeyEvent.getKeyText(key.getKeyCode()).equals("H")) { + // This is ^H. + keypress = kbBackspace; + } else { + // We are emulating Xterm here, where the backspace key + // on the keyboard returns ^?. + keypress = kbBackspaceDel; + } break; case 0x0A: keypress = kbEnter; @@ -1529,6 +1943,7 @@ public final class SwingTerminal extends LogicalScreen break; default: if (!alt && ctrl && !shift) { + // Control character, replace ch with 'A', 'B', etc. ch = KeyEvent.getKeyText(key.getKeyCode()).charAt(0); } // Not a special key, put it together @@ -1539,6 +1954,7 @@ public final class SwingTerminal extends LogicalScreen // Save it and we are done. synchronized (eventQueue) { eventQueue.add(new TKeypressEvent(keypress)); + resetBlinkTimer(); } if (listener != null) { synchronized (listener) { @@ -1547,6 +1963,10 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // WindowListener --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Pass window events into the event queue. * @@ -1574,9 +1994,10 @@ public final class SwingTerminal extends LogicalScreen * @param event window event received */ public void windowClosing(final WindowEvent event) { - // Drop a cmAbort and walk away + // Drop a cmBackendDisconnect and walk away synchronized (eventQueue) { - eventQueue.add(new TCommandEvent(cmAbort)); + eventQueue.add(new TCommandEvent(cmBackendDisconnect)); + resetBlinkTimer(); } if (listener != null) { synchronized (listener) { @@ -1621,6 +2042,10 @@ public final class SwingTerminal extends LogicalScreen // Ignore } + // ------------------------------------------------------------------------ + // ComponentListener ------------------------------------------------------ + // ------------------------------------------------------------------------ + /** * Pass component events into the event queue. * @@ -1661,12 +2086,23 @@ public final class SwingTerminal extends LogicalScreen return; } + if (sessionInfo == null) { + // This is the initial component resize in construction, bail + // out. + return; + } + // Drop a new TResizeEvent into the queue sessionInfo.queryWindowSize(); synchronized (eventQueue) { TResizeEvent windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); eventQueue.add(windowResize); + resetBlinkTimer(); + /* + System.err.println("Add resize event: " + windowResize.getWidth() + + " x " + windowResize.getHeight()); + */ } if (listener != null) { synchronized (listener) { @@ -1675,6 +2111,10 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // MouseMotionListener ---------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Pass mouse events into the event queue. * @@ -1685,6 +2125,10 @@ public final class SwingTerminal extends LogicalScreen boolean eventMouse1 = false; boolean eventMouse2 = false; boolean eventMouse3 = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { eventMouse1 = true; } @@ -1694,6 +2138,16 @@ public final class SwingTerminal extends LogicalScreen if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { eventMouse3 = true; } + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + mouse1 = eventMouse1; mouse2 = eventMouse2; mouse3 = eventMouse3; @@ -1701,10 +2155,12 @@ public final class SwingTerminal extends LogicalScreen int y = textRow(mouse.getY()); TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION, - x, y, x, y, mouse1, mouse2, mouse3, false, false); + x, y, x, y, mouse1, mouse2, mouse3, false, false, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); + resetBlinkTimer(); } if (listener != null) { synchronized (listener) { @@ -1728,11 +2184,28 @@ public final class SwingTerminal extends LogicalScreen oldMouseX = x; oldMouseY = y; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + + int modifiers = mouse.getModifiersEx(); + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION, - x, y, x, y, mouse1, mouse2, mouse3, false, false); + x, y, x, y, mouse1, mouse2, mouse3, false, false, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); + resetBlinkTimer(); } if (listener != null) { synchronized (listener) { @@ -1741,6 +2214,10 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // MouseListener ---------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Pass mouse events into the event queue. * @@ -1756,7 +2233,7 @@ public final class SwingTerminal extends LogicalScreen * @param mouse mouse event received */ public void mouseEntered(final MouseEvent mouse) { - // Ignore + swing.requestFocusInWindow(); } /** @@ -1778,6 +2255,10 @@ public final class SwingTerminal extends LogicalScreen boolean eventMouse1 = false; boolean eventMouse2 = false; boolean eventMouse3 = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { eventMouse1 = true; } @@ -1787,6 +2268,16 @@ public final class SwingTerminal extends LogicalScreen if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { eventMouse3 = true; } + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + mouse1 = eventMouse1; mouse2 = eventMouse2; mouse3 = eventMouse3; @@ -1794,10 +2285,12 @@ public final class SwingTerminal extends LogicalScreen int y = textRow(mouse.getY()); TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN, - x, y, x, y, mouse1, mouse2, mouse3, false, false); + x, y, x, y, mouse1, mouse2, mouse3, false, false, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); + resetBlinkTimer(); } if (listener != null) { synchronized (listener) { @@ -1816,6 +2309,10 @@ public final class SwingTerminal extends LogicalScreen boolean eventMouse1 = false; boolean eventMouse2 = false; boolean eventMouse3 = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { eventMouse1 = true; } @@ -1825,6 +2322,16 @@ public final class SwingTerminal extends LogicalScreen if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { eventMouse3 = true; } + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + if (mouse1) { mouse1 = false; eventMouse1 = true; @@ -1841,10 +2348,12 @@ public final class SwingTerminal extends LogicalScreen int y = textRow(mouse.getY()); TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP, - x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false); + x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); + resetBlinkTimer(); } if (listener != null) { synchronized (listener) { @@ -1853,6 +2362,10 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // MouseWheelListener ----------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Pass mouse events into the event queue. * @@ -1865,6 +2378,10 @@ public final class SwingTerminal extends LogicalScreen boolean eventMouse3 = false; boolean mouseWheelUp = false; boolean mouseWheelDown = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { eventMouse1 = true; } @@ -1874,6 +2391,16 @@ public final class SwingTerminal extends LogicalScreen if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { eventMouse3 = true; } + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + mouse1 = eventMouse1; mouse2 = eventMouse2; mouse3 = eventMouse3; @@ -1887,10 +2414,12 @@ public final class SwingTerminal extends LogicalScreen } TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN, - x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown); + x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); + resetBlinkTimer(); } if (listener != null) { synchronized (listener) {