Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[fanfix.git] / src / jexer / backend / SwingTerminal.java
index a98063867e75479c2b18d91e2872fb2fdadec4a5..f0ba3552fd52b812a91a06be0f97c9adb96604e7 100644 (file)
@@ -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"),
@@ -51,11 +51,13 @@ import java.awt.event.WindowListener;
 import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
 import java.io.InputStream;
+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 +80,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 +117,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<Cell, BufferedImage> 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<Cell, BufferedImage> glyphCache;
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     // Colors to map DOS colors to AWT colors.
     private static Color MYBLACK;
@@ -149,41 +153,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<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.
@@ -203,12 +187,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 +219,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.
      */
@@ -246,17 +250,6 @@ public final class SwingTerminal extends LogicalScreen
      */
     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.
@@ -270,507 +263,301 @@ 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<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);
-        }
+    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);
+                        }
 
-                    // 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(true);
+                    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 ArrayList<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;
-        }
+        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) {
+                    // Load the font so that we can set sessionInfo.
+                    setDefaultFont();
 
-        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;
-
-        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 ArrayList<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);
     }
 
     /**
@@ -791,6 +578,7 @@ public final class SwingTerminal extends LogicalScreen
         ) {
             do {
                 do {
+                    clearPhysical();
                     drawToSwing();
                 } while (swing.getBufferStrategy().contentsRestored());
 
@@ -804,520 +592,1120 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // 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() {
-
-        /*
-        System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
-            reallyCleared, dirty);
-        */
-
-        // 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;
+    public boolean hasEvents() {
+        synchronized (eventQueue) {
+            return (eventQueue.size() > 0);
         }
+    }
 
-        if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
-            Graphics gr = swing.getBufferStrategy().getDrawGraphics();
+    /**
+     * 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();
+            }
+        }
+    }
 
-            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];
+    /**
+     * Restore terminal to normal state.
+     */
+    public void closeTerminal() {
+        shutdown();
+    }
 
-                        int xPixel = x * textWidth + left;
-                        int yPixel = y * textHeight + top;
+    /**
+     * 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;
+    }
 
-                        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)
+    /**
+     * 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;
+        }
 
-            gr.dispose();
-            swing.getBufferStrategy().show();
-            Toolkit.getDefaultToolkit().sync();
-            return;
+        // Pull the system property for triple buffering.
+        if (System.getProperty("jexer.Swing.tripleBuffer",
+                "true").equals("true")
+        ) {
+            SwingComponent.tripleBuffer = true;
+        } else {
+            SwingComponent.tripleBuffer = false;
         }
 
-        // Swing thread version: request a repaint, but limit it to the area
-        // that has changed.
+        // Set custom colors
+        setCustomSystemColors();
+    }
 
-        // Find the minimum-size damaged region.
-        int xMin = swing.getWidth();
-        int xMax = 0;
-        int yMin = swing.getHeight();
-        int yMax = 0;
+    // ------------------------------------------------------------------------
+    // SwingTerminal ----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
-        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 width of a character cell in pixels.
+     *
+     * @return the width in pixels of a character cell
+     */
+    public int getTextWidth() {
+        return textWidth;
+    }
 
-                    int xPixel = x * textWidth + left;
-                    int yPixel = y * textHeight + top;
+    /**
+     * Get the height of a character cell in pixels.
+     *
+     * @return the height in pixels of a character cell
+     */
+    public int getTextHeight() {
+        return 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;
-                        }
-                    }
-                }
-            }
-        }
-        if (xMin + textWidth >= xMax) {
-            xMax += textWidth;
+    /**
+     * Setup Swing colors to match DOS color palette.
+     */
+    private static void setDOSColors() {
+        if (dosColors) {
+            return;
         }
-        if (yMin + textHeight >= yMax) {
-            yMax += textHeight;
+        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;
+    }
+
+    /**
+     * 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);
         }
+    }
 
-        // Repaint the desired area
-        /*
-        System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
-            yMin, yMax);
-        */
+    /**
+     * 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) {
 
-        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);
+        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));
+
+        return color;
     }
 
     /**
-     * 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 current status of the blink flag.
      *
-     * @param y pixel row position
-     * @return text cell row position
+     * @return true if the cursor and blinking text should be visible
      */
-    public int textRow(final int y) {
-        return ((y - top) / textHeight);
+    public boolean getCursorBlinkVisible() {
+        return cursorBlinkVisible;
     }
 
     /**
-     * Set the window title.
+     * Get the font size in points.
      *
-     * @param title the new title
+     * @return font size in points
      */
-    public void setTitle(final String title) {
-        swing.setTitle(title);
+    public int getFontSize() {
+        return fontSize;
     }
 
-    // ------------------------------------------------------------------------
-    // TerminalReader ---------------------------------------------------------
-    // ------------------------------------------------------------------------
+    /**
+     * 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);
+    }
 
     /**
-     * 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) {
+        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<Cell, BufferedImage>();
+                            glyphCache = new HashMap<Cell, BufferedImage>();
+                            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<Cell, BufferedImage>();
+                glyphCache = new HashMap<Cell, BufferedImage>();
+                resizeToScreen(true);
+            }
+        }
+    }
 
     /**
-     * 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;
+    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();
+        }
+    }
 
-        setDOSColors();
+    /**
+     * 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;
 
-        // 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;
+            return new Color(red, green, blue);
         }
 
-        // 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;
+        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());
+    }
 
-        try {
-            SwingUtilities.invokeAndWait(new Runnable() {
-                public void run() {
+    /**
+     * 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;
 
-                    JFrame frame = new JFrame() {
+            return new Color(red, green, blue);
+        }
 
-                        /**
-                         * Serializable version.
-                         */
-                        private static final long serialVersionUID = 1;
+        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());
+    }
 
-                        /**
-                         * The code that performs the actual drawing.
-                         */
-                        public SwingTerminal screen = null;
+    /**
+     * 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;
 
-                        /*
-                         * Anonymous class initializer saves the screen
-                         * reference, so that paint() and the like call out
-                         * to SwingTerminal.
-                         */
-                        {
-                            this.screen = SwingTerminal.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.
 
-                        /**
-                         * 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);
-                        }
+        Graphics2D gr2 = null;
+        int gr2x = 3;
+        int gr2y = 3;
+        image = new BufferedImage(fontTextWidth * 2, fontTextHeight * 2,
+            BufferedImage.TYPE_INT_ARGB);
 
-                        /**
-                         * Paint redraws the whole screen.
-                         *
-                         * @param gr the Swing Graphics context
-                         */
-                        @Override
-                        public void paint(final Graphics gr) {
-                            if (screen != null) {
-                                screen.paint(gr);
-                            }
-                        }
-                    };
+        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 Swing component
-                    SwingTerminal.this.swing = new SwingComponent(frame);
+        int top = fontTextHeight * 2;
+        int bottom = -1;
+        int left = fontTextWidth * 2;
+        int right = -1;
+        textAdjustX = 0;
+        textAdjustY = 0;
+        textAdjustHeight = 0;
+        textAdjustWidth = 0;
 
-                    // Hang onto top and left for drawing.
-                    Insets insets = SwingTerminal.this.swing.getInsets();
-                    SwingTerminal.this.left = insets.left;
-                    SwingTerminal.this.top = insets.top;
+        for (int x = 0; x < fontTextWidth * 2; x++) {
+            for (int y = 0; y < fontTextHeight * 2; y++) {
 
-                    // Load the font so that we can set sessionInfo.
-                    getDefaultFont();
+                /*
+                System.err.println("H X: " + x + " Y: " + y + " " +
+                    image.getRGB(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,
-                            windowWidth, windowHeight);
+                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 (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);
 
-                    SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(),
-                        sessionInfo.getWindowHeight());
+        // 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;
+        }
+    }
 
-                    SwingTerminal.this.resizeToScreen();
-                    SwingTerminal.this.swing.setVisible(true);
-                }
-            });
-        } catch (Exception e) {
-            e.printStackTrace();
+    /**
+     * 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);
+    }
 
-        this.listener    = listener;
-        mouse1           = false;
-        mouse2           = false;
-        mouse3           = false;
-        eventQueue       = new LinkedList<TInputEvent>();
+    /**
+     * 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();
+        fontTextWidth = (int)Math.round(bounds.getWidth());
+        // fontTextHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
 
-        // Add listeners to Swing.
-        swing.addKeyListener(this);
-        swing.addWindowListener(this);
-        swing.addComponentListener(this);
-        swing.addMouseListener(this);
-        swing.addMouseMotionListener(this);
-        swing.addMouseWheelListener(this);
+        // This produces the same number, but works better for ugly
+        // monospace.
+        fontTextHeight = fm.getMaxAscent() + maxDescent - leading;
+
+        getFontAdjustments();
+        textHeight = fontTextHeight + textAdjustHeight;
+        textWidth = fontTextWidth + textAdjustWidth;
+
+        if (sessionInfo != null) {
+            sessionInfo.setTextCellDimensions(textWidth, textHeight);
+        }
+        gotFontDimensions = true;
     }
 
     /**
-     * Public constructor renders to an existing JComponent.
+     * Resize the physical screen to match the logical screen dimensions.
      *
-     * @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 resizeComponent if true, resize the Swing component
      */
-    public SwingTerminal(final JComponent component, final int windowWidth,
-        final int windowHeight, final int fontSize, final Object listener) {
+    private void resizeToScreen(final boolean resizeComponent) {
+        if (resizeComponent) {
+            swing.setDimensions(textWidth * width, textHeight * height);
+        }
+        clearPhysical();
+    }
 
-        this.fontSize = fontSize;
+    /**
+     * Resize the physical screen to match the logical screen dimensions.
+     */
+    @Override
+    public void resizeToScreen() {
+        resizeToScreen(false);
+    }
 
-        setDOSColors();
+    /**
+     * 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) {
 
-        // 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;
+        /*
+        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(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 = 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());
+            }
         }
 
-        try {
-            SwingUtilities.invokeAndWait(new Runnable() {
-                public void run() {
+    }
+
+    /**
+     * 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];
+            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;
+            }
+        }
+    }
+
+    /**
+     * 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);
+             */
+        }
+
+        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) + 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;
+        }
+
+        // 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)
+    }
+
+    /**
+     * Restore terminal to normal state.
+     */
+    public void shutdown() {
+        swing.dispose();
+    }
 
-                    JComponent newComponent = new JComponent() {
+    /**
+     * Push the logical screen to the physical device.
+     */
+    private void drawToSwing() {
 
-                        /**
-                         * Serializable version.
-                         */
-                        private static final long serialVersionUID = 1;
+        /*
+        System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
+            reallyCleared, dirty);
+        */
 
-                        /**
-                         * The code that performs the actual drawing.
-                         */
-                        public SwingTerminal screen = null;
+        // 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;
+        }
 
-                        /*
-                         * Anonymous class initializer saves the screen
-                         * reference, so that paint() and the like call out
-                         * to SwingTerminal.
-                         */
-                        {
-                            this.screen = SwingTerminal.this;
-                        }
+        if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
+            Graphics gr = swing.getBufferStrategy().getDrawGraphics();
 
-                        /**
-                         * 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);
-                        }
+            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];
 
-                        /**
-                         * Paint redraws the whole screen.
-                         *
-                         * @param gr the Swing Graphics context
-                         */
-                        @Override
-                        public void paint(final Graphics gr) {
-                            if (screen != null) {
-                                screen.paint(gr);
+                        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.
      *
@@ -1531,6 +1919,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
@@ -1550,6 +1939,10 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // WindowListener ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass window events into the event queue.
      *
@@ -1577,9 +1970,9 @@ 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) {
@@ -1625,6 +2018,10 @@ public final class SwingTerminal extends LogicalScreen
         // Ignore
     }
 
+    // ------------------------------------------------------------------------
+    // ComponentListener ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass component events into the event queue.
      *
@@ -1665,6 +2062,12 @@ 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) {
@@ -1672,6 +2075,10 @@ public final class SwingTerminal extends LogicalScreen
                 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
             eventQueue.add(windowResize);
             resetBlinkTimer();
+            /*
+            System.err.println("Add resize event: " + windowResize.getWidth() +
+                " x " + windowResize.getHeight());
+             */
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1680,6 +2087,10 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // MouseMotionListener ----------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass mouse events into the event queue.
      *
@@ -1748,6 +2159,10 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // MouseListener ----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass mouse events into the event queue.
      *
@@ -1763,7 +2178,7 @@ public final class SwingTerminal extends LogicalScreen
      * @param mouse mouse event received
      */
     public void mouseEntered(final MouseEvent mouse) {
-        // Ignore
+        swing.requestFocusInWindow();
     }
 
     /**
@@ -1862,6 +2277,10 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // MouseWheelListener -----------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass mouse events into the event queue.
      *