Add versioned about box, set title
[fanfix.git] / src / jexer / io / SwingScreen.java
index 24710101ee5b1fb44ebe25ee126390e523ccb244..3d0729e4feecd719b78431d93e1e54da46c9e30a 100644 (file)
@@ -33,6 +33,7 @@ import java.awt.Cursor;
 import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
+import java.awt.Graphics2D;
 import java.awt.Insets;
 import java.awt.Point;
 import java.awt.Rectangle;
@@ -42,6 +43,7 @@ import java.awt.image.BufferedImage;
 import java.awt.image.BufferStrategy;
 import java.io.InputStream;
 import java.util.Date;
+import java.util.HashMap;
 import javax.swing.JFrame;
 import javax.swing.SwingUtilities;
 
@@ -146,11 +148,28 @@ public final class SwingScreen extends Screen {
          */
         private BufferStrategy bufferStrategy;
 
+        /**
+         * A cache of previously-rendered glyphs for blinking text, when it
+         * is not visible.
+         */
+        private HashMap<Cell, BufferedImage> glyphCacheBlink;
+
+        /**
+         * A cache of previously-rendered glyphs for non-blinking, or
+         * blinking-and-visible, text.
+         */
+        private HashMap<Cell, BufferedImage> glyphCache;
+
         /**
          * The TUI Screen data.
          */
         SwingScreen screen;
 
+        /**
+         * If true, we were successful getting Terminus.
+         */
+        private boolean gotTerminus = false;
+
         /**
          * Width of a character cell.
          */
@@ -253,7 +272,8 @@ public final class SwingScreen extends Screen {
                     return MYWHITE;
                 }
             }
-            throw new IllegalArgumentException("Invalid color: " + attr.getForeColor().getValue());
+            throw new IllegalArgumentException("Invalid color: " +
+                attr.getForeColor().getValue());
         }
 
         /**
@@ -280,7 +300,8 @@ public final class SwingScreen extends Screen {
             } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
                 return MYWHITE;
             }
-            throw new IllegalArgumentException("Invalid color: " + attr.getBackColor().getValue());
+            throw new IllegalArgumentException("Invalid color: " +
+                attr.getBackColor().getValue());
         }
 
         /**
@@ -293,8 +314,8 @@ public final class SwingScreen extends Screen {
             setDOSColors();
 
             // Figure out my cursor style
-            String cursorStyleString = System.getProperty("jexer.Swing.cursorStyle",
-                "underline").toLowerCase();
+            String cursorStyleString = System.getProperty(
+                "jexer.Swing.cursorStyle", "underline").toLowerCase();
 
             if (cursorStyleString.equals("underline")) {
                 cursorStyle = CursorStyle.UNDERLINE;
@@ -309,11 +330,13 @@ public final class SwingScreen extends Screen {
 
             try {
                 // Always try to use Terminus, the one decent font.
-                ClassLoader loader = Thread.currentThread().getContextClassLoader();
+                ClassLoader loader = Thread.currentThread().
+                        getContextClassLoader();
                 InputStream in = loader.getResourceAsStream(FONTFILE);
                 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
-                Font terminus = terminusRoot.deriveFont(Font.PLAIN, 22);
+                Font terminus = terminusRoot.deriveFont(Font.PLAIN, 20);
                 setFont(terminus);
+                gotTerminus = true;
             } catch (Exception e) {
                 e.printStackTrace();
                 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
@@ -336,6 +359,10 @@ public final class SwingScreen extends Screen {
             // Save the text cell width/height
             getFontDimensions();
 
+            // Cache glyphs as they are rendered
+            glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+            glyphCache = new HashMap<Cell, BufferedImage>();
+
             // Setup triple-buffering
             if (SwingScreen.tripleBuffer) {
                 setIgnoreRepaint(true);
@@ -354,15 +381,24 @@ public final class SwingScreen extends Screen {
             Rectangle2D bounds = fm.getMaxCharBounds(gr);
             int leading = fm.getLeading();
             textWidth = (int)Math.round(bounds.getWidth());
-            textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
-            // This also produces the same number, but works better for ugly
+            // textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
+
+            // This produces the same number, but works better for ugly
             // monospace.
             textHeight = fm.getMaxAscent() + maxDescent - leading;
 
+            if (gotTerminus == true) {
+                textHeight++;
+            }
+
             if (System.getProperty("os.name").startsWith("Windows")) {
                 textAdjustY = -1;
                 textAdjustX = 0;
             }
+            if (System.getProperty("os.name").startsWith("Mac")) {
+                textAdjustY = -1;
+                textAdjustX = 0;
+            }
         }
 
         /**
@@ -391,6 +427,110 @@ public final class SwingScreen extends Screen {
             paint(gr);
         }
 
+        /**
+         * 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) {
+
+            BufferedImage image = null;
+            if (cell.isBlink() && !cursorBlinkVisible) {
+                image = glyphCacheBlink.get(cell);
+            } else {
+                image = glyphCache.get(cell);
+            }
+            if (image != null) {
+                gr.drawImage(image, xPixel, yPixel, this);
+                return;
+            }
+
+            // Generate glyph and draw it.
+
+            image = new BufferedImage(textWidth, textHeight,
+                BufferedImage.TYPE_INT_ARGB);
+            Graphics2D gr2 = image.createGraphics();
+            gr2.setFont(getFont());
+
+            Cell cellColor = new Cell();
+            cellColor.setTo(cell);
+
+            // Check for reverse
+            if (cell.isReverse()) {
+                cellColor.setForeColor(cell.getBackColor());
+                cellColor.setBackColor(cell.getForeColor());
+            }
+
+            // Draw the background rectangle, then the foreground character.
+            gr2.setColor(attrToBackgroundColor(cellColor));
+            gr2.fillRect(0, 0, textWidth, textHeight);
+
+            // Handle blink and underline
+            if (!cell.isBlink()
+                || (cell.isBlink() && cursorBlinkVisible)
+            ) {
+                gr2.setColor(attrToForegroundColor(cellColor));
+                char [] chars = new char[1];
+                chars[0] = cell.getChar();
+                gr2.drawChars(chars, 0, 1, 0 + textAdjustX,
+                    0 + textHeight - maxDescent + textAdjustY);
+
+                if (cell.isUnderline()) {
+                    gr2.fillRect(0, 0 + textHeight - 2, textWidth, 2);
+                }
+            }
+            gr2.dispose();
+
+            // We need a new key that will not be mutated by invertCell().
+            Cell key = new Cell();
+            key.setTo(cell);
+            if (cell.isBlink() && !cursorBlinkVisible) {
+                glyphCacheBlink.put(key, image);
+            } else {
+                glyphCache.put(key, image);
+            }
+
+            gr.drawImage(image, xPixel, yPixel, this);
+        }
+
+        /**
+         * 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 <= screen.height - 1)
+                && (cursorX <= screen.width - 1)
+                && cursorBlinkVisible
+            ) {
+                int xPixel = cursorX * textWidth + left;
+                int yPixel = cursorY * textHeight + top;
+                Cell lCell = screen.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;
+                }
+            }
+        }
+
         /**
          * Paint redraws the whole screen.
          *
@@ -469,69 +609,14 @@ public final class SwingScreen extends Screen {
                             || lCell.isBlink()
                             || reallyCleared) {
 
-                            Cell lCellColor = new Cell();
-                            lCellColor.setTo(lCell);
-
-                            // Check for reverse
-                            if (lCell.isReverse()) {
-                                lCellColor.setForeColor(lCell.getBackColor());
-                                lCellColor.setBackColor(lCell.getForeColor());
-                            }
-
-                            // Draw the background rectangle, then the
-                            // foreground character.
-                            gr.setColor(attrToBackgroundColor(lCellColor));
-                            gr.fillRect(xPixel, yPixel, textWidth, textHeight);
-
-                            // Handle blink and underline
-                            if (!lCell.isBlink()
-                                || (lCell.isBlink() && cursorBlinkVisible)
-                            ) {
-                                gr.setColor(attrToForegroundColor(lCellColor));
-                                char [] chars = new char[1];
-                                chars[0] = lCell.getChar();
-                                gr.drawChars(chars, 0, 1, xPixel + textAdjustX,
-                                    yPixel + textHeight - maxDescent
-                                    + textAdjustY);
-
-                                if (lCell.isUnderline()) {
-                                    gr.fillRect(xPixel, yPixel + textHeight - 2,
-                                        textWidth, 2);
-                                }
-                            }
+                            drawGlyph(gr, lCell, xPixel, yPixel);
 
                             // Physical is always updated
                             physical[x][y].setTo(lCell);
                         }
                     }
                 }
-
-                // Draw the cursor if it is visible
-                if (cursorVisible
-                    && (cursorY <= screen.height - 1)
-                    && (cursorX <= screen.width - 1)
-                    && cursorBlinkVisible
-                ) {
-                    int xPixel = cursorX * textWidth + left;
-                    int yPixel = cursorY * textHeight + top;
-                    Cell lCell = screen.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;
-                    }
-                }
+                drawCursor(gr);
 
                 dirty = false;
                 reallyCleared = false;
@@ -623,8 +708,48 @@ public final class SwingScreen extends Screen {
             return;
         }
 
-        // Request a repaint, let the frame's repaint/update methods do the
-        // right thing.
+        if (frame.bufferStrategy != null) {
+            // See if it is time to flip the blink time.
+            long nowTime = (new Date()).getTime();
+            if (nowTime > frame.blinkMillis + frame.lastBlinkTime) {
+                frame.lastBlinkTime = nowTime;
+                frame.cursorBlinkVisible = !frame.cursorBlinkVisible;
+            }
+
+            Graphics gr = frame.bufferStrategy.getDrawGraphics();
+
+            synchronized (this) {
+                for (int y = 0; y < height; y++) {
+                    for (int x = 0; x < width; x++) {
+                        Cell lCell = logical[x][y];
+                        Cell pCell = physical[x][y];
+
+                        int xPixel = x * frame.textWidth + frame.left;
+                        int yPixel = y * frame.textHeight + frame.top;
+
+                        if (!lCell.equals(pCell)
+                            || ((x == cursorX)
+                                && (y == cursorY)
+                                && cursorVisible)
+                            || (lCell.isBlink())
+                        ) {
+                            frame.drawGlyph(gr, lCell, xPixel, yPixel);
+                            physical[x][y].setTo(lCell);
+                        }
+                    }
+                }
+                frame.drawCursor(gr);
+            } // synchronized (this)
+
+            gr.dispose();
+            frame.bufferStrategy.show();
+            // sync() doesn't seem to help the tearing for me.
+            // Toolkit.getDefaultToolkit().sync();
+            return;
+        }
+
+        // Swing thread version: request a repaint, but limit it to the area
+        // that has changed.
 
         // Find the minimum-size damaged region.
         int xMin = frame.getWidth();
@@ -676,6 +801,8 @@ public final class SwingScreen extends Screen {
             yMin, yMax);
          */
         if (frame.bufferStrategy != null) {
+            // This path should never be taken, but is left here for
+            // completeness.
             Graphics gr = frame.bufferStrategy.getDrawGraphics();
             Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
                 yMax - yMin);
@@ -686,6 +813,7 @@ public final class SwingScreen extends Screen {
             // sync() doesn't seem to help the tearing for me.
             // Toolkit.getDefaultToolkit().sync();
         } else {
+            // Repaint on the Swing thread.
             frame.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
         }
     }
@@ -744,4 +872,13 @@ public final class SwingScreen extends Screen {
         return ((y - frame.top) / frame.textHeight);
     }
 
+    /**
+     * Set the window title.
+     *
+     * @param title the new title
+     */
+    public void setTitle(final String title) {
+        frame.setTitle(title);
+    }
+
 }