stub maven support
[nikiroo-utils.git] / src / jexer / io / SwingScreen.java
index 1b3d8a409c26fdc3bd8129916ae201aeb74dee0f..ee8467dd076008970784d1225609f516cbc9a469 100644 (file)
@@ -1,29 +1,27 @@
-/**
+/*
  * Jexer - Java Text User Interface
  *
- * License: LGPLv3 or later
- *
- * This module is licensed under the GNU Lesser General Public License
- * Version 3.  Please see the file "COPYING" in this directory for more
- * information about the GNU Lesser General Public License Version 3.
+ * The MIT License (MIT)
  *
- *     Copyright (C) 2015  Kevin Lamonte
+ * Copyright (C) 2017 Kevin Lamonte
  *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation; either version 3 of
- * the License, or (at your option) any later version.
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
  *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
  *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program; if not, see
- * http://www.gnu.org/licenses/, or write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
  *
  * @author Kevin Lamonte [kevin.lamonte@gmail.com]
  * @version 1
@@ -35,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;
@@ -44,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;
 
@@ -57,9 +57,9 @@ import jexer.session.SwingSessionInfo;
 public final class SwingScreen extends Screen {
 
     /**
-     * If true, use double buffering thread.
+     * If true, use triple buffering thread.
      */
-    private static final boolean doubleBuffer = true;
+    private static boolean tripleBuffer = true;
 
     /**
      * Cursor style to draw.
@@ -144,15 +144,32 @@ public final class SwingScreen extends Screen {
         private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
 
         /**
-         * The BufferStrategy object needed for double-buffering.
+         * The BufferStrategy object needed for triple-buffering.
          */
         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.
          */
@@ -255,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());
         }
 
         /**
@@ -282,21 +300,24 @@ 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());
         }
 
         /**
          * Public constructor.
          *
          * @param screen the Screen that Backend talks to
+         * @param fontSize the size in points.  Good values to pick are: 16,
+         * 20, 22, and 24.
          */
-        public SwingFrame(final SwingScreen screen) {
+        public SwingFrame(final SwingScreen screen, final int fontSize) {
             this.screen = 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;
@@ -306,20 +327,30 @@ public final class SwingScreen extends Screen {
                 cursorStyle = CursorStyle.BLOCK;
             }
 
+            if (System.getProperty("jexer.Swing.tripleBuffer") != null) {
+                if (System.getProperty("jexer.Swing.tripleBuffer").
+                    equals("false")) {
+
+                    SwingScreen.tripleBuffer = false;
+                }
+            }
+
             setTitle("Jexer Application");
             setBackground(Color.black);
 
             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, fontSize);
                 setFont(terminus);
+                gotTerminus = true;
             } catch (Exception e) {
                 e.printStackTrace();
                 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
-                setFont(new Font(Font.MONOSPACED, Font.PLAIN, 24));
+                setFont(new Font(Font.MONOSPACED, Font.PLAIN, fontSize));
             }
             pack();
 
@@ -338,14 +369,99 @@ public final class SwingScreen extends Screen {
             // Save the text cell width/height
             getFontDimensions();
 
-            // Setup double-buffering
-            if (SwingScreen.doubleBuffer) {
+            // Cache glyphs as they are rendered
+            glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+            glyphCache = new HashMap<Cell, BufferedImage>();
+
+            // Setup triple-buffering
+            if (SwingScreen.tripleBuffer) {
                 setIgnoreRepaint(true);
-                createBufferStrategy(2);
+                createBufferStrategy(3);
                 bufferStrategy = getBufferStrategy();
             }
         }
 
+        /**
+         * 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
+         */
+        private boolean getFontAdjustments() {
+            BufferedImage image = null;
+
+            // 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.
+
+            Graphics2D gr2 = null;
+            int gr2x = 3;
+            int gr2y = 3;
+            image = new BufferedImage(textWidth * 2, textHeight * 2,
+                BufferedImage.TYPE_INT_ARGB);
+
+            gr2 = image.createGraphics();
+            gr2.setFont(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();
+
+            for (int x = 0; x < textWidth; x++) {
+                for (int y = 0; y < textHeight; y++) {
+
+                    /*
+                    System.err.println("X: " + x + " Y: " + y + " " +
+                        image.getRGB(x, y));
+                     */
+
+                    if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
+                        textAdjustY = (gr2y - y);
+
+                        // System.err.println("textAdjustY: " + textAdjustY);
+                        x = textWidth;
+                        break;
+                    }
+                }
+            }
+
+            gr2 = image.createGraphics();
+            gr2.setFont(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();
+
+            for (int x = 0; x < textWidth; x++) {
+                for (int y = 0; y < textHeight; y++) {
+
+                    /*
+                    System.err.println("X: " + x + " Y: " + y + " " +
+                        image.getRGB(x, y));
+                     */
+
+                    if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
+                        textAdjustX = (gr2x - x);
+
+                        // System.err.println("textAdjustX: " + textAdjustX);
+                        return true;
+                    }
+                }
+            }
+
+            // Something weird happened, don't rely on this function.
+            // System.err.println("getFontAdjustments: false");
+            return false;
+        }
+
+
         /**
          * Figure out my font dimensions.
          */
@@ -356,14 +472,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 (System.getProperty("os.name").startsWith("Windows")) {
-                textAdjustY = -1;
-                textAdjustX = 0;
+            if (gotTerminus == true) {
+                textHeight++;
+            }
+
+            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;
+                }
             }
         }
 
@@ -393,6 +519,123 @@ 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.
+            Graphics2D gr2 = null;
+            int gr2x = xPixel;
+            int gr2y = yPixel;
+            if (tripleBuffer) {
+                image = new BufferedImage(textWidth, textHeight,
+                    BufferedImage.TYPE_INT_ARGB);
+                gr2 = image.createGraphics();
+                gr2.setFont(getFont());
+                gr2x = 0;
+                gr2y = 0;
+            } else {
+                gr2 = (Graphics2D) gr;
+            }
+
+            Cell cellColor = new Cell();
+            cellColor.setTo(cell);
+
+            // Check for reverse
+            if (cell.isReverse()) {
+                cellColor.setForeColor(cell.getBackColor());
+                cellColor.setBackColor(cell.getForeColor());
+            }
+
+            // Draw the background rectangle, then the foreground character.
+            gr2.setColor(attrToBackgroundColor(cellColor));
+            gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
+
+            // Handle blink and underline
+            if (!cell.isBlink()
+                || (cell.isBlink() && cursorBlinkVisible)
+            ) {
+                gr2.setColor(attrToForegroundColor(cellColor));
+                char [] chars = new char[1];
+                chars[0] = cell.getChar();
+                gr2.drawChars(chars, 0, 1, gr2x + textAdjustX,
+                    gr2y + textHeight - maxDescent + textAdjustY);
+
+                if (cell.isUnderline()) {
+                    gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2);
+                }
+            }
+
+            if (tripleBuffer) {
+                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.
          *
@@ -471,67 +714,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) {
-                    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;
@@ -554,16 +744,24 @@ public final class SwingScreen extends Screen {
 
     /**
      * Public constructor.
+     *
+     * @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.
      */
-    public SwingScreen() {
+    public SwingScreen(final int windowWidth, final int windowHeight,
+        final int fontSize) {
+
         try {
             SwingUtilities.invokeAndWait(new Runnable() {
                 public void run() {
-                    SwingScreen.this.frame = new SwingFrame(SwingScreen.this);
+                    SwingScreen.this.frame = new SwingFrame(SwingScreen.this,
+                        fontSize);
                     SwingScreen.this.sessionInfo =
                         new SwingSessionInfo(SwingScreen.this.frame,
-                            frame.textWidth,
-                            frame.textHeight);
+                            frame.textWidth, frame.textHeight,
+                            windowWidth, windowHeight);
 
                     SwingScreen.this.setDimensions(sessionInfo.getWindowWidth(),
                         sessionInfo.getWindowHeight());
@@ -571,7 +769,7 @@ public final class SwingScreen extends Screen {
                     SwingScreen.this.frame.resizeToScreen();
                     SwingScreen.this.frame.setVisible(true);
                 }
-            } );
+            });
         } catch (Exception e) {
             e.printStackTrace();
         }
@@ -597,17 +795,24 @@ public final class SwingScreen extends Screen {
     @Override
     public void flushPhysical() {
 
-        if (reallyCleared) {
-            // Really refreshed, do it all
-            if (SwingScreen.doubleBuffer) {
-                Graphics gr = frame.bufferStrategy.getDrawGraphics();
-                frame.paint(gr);
-                gr.dispose();
-                frame.bufferStrategy.show();
-                Toolkit.getDefaultToolkit().sync();
-            } else {
-                frame.repaint();
-            }
+        /*
+        System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n",
+            reallyCleared, dirty);
+         */
+
+        // If reallyCleared is set, we have to draw everything.
+        if ((frame.bufferStrategy != null) && (reallyCleared == true)) {
+            // Triple-buffering: we have to redraw everything on this thread.
+            Graphics gr = frame.bufferStrategy.getDrawGraphics();
+            frame.paint(gr);
+            gr.dispose();
+            frame.bufferStrategy.show();
+            // sync() doesn't seem to help the tearing for me.
+            // Toolkit.getDefaultToolkit().sync();
+            return;
+        } else if ((frame.bufferStrategy == null) && (reallyCleared == true)) {
+            // Repaint everything on the Swing thread.
+            frame.repaint();
             return;
         }
 
@@ -616,8 +821,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();
@@ -664,9 +909,13 @@ public final class SwingScreen extends Screen {
         }
 
         // Repaint the desired area
-        // System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
-        //     yMin, yMax);
-        if (SwingScreen.doubleBuffer) {
+        /*
+        System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
+            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);
@@ -674,8 +923,10 @@ public final class SwingScreen extends Screen {
             frame.paint(gr);
             gr.dispose();
             frame.bufferStrategy.show();
-            Toolkit.getDefaultToolkit().sync();
+            // 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);
         }
     }
@@ -734,4 +985,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);
+    }
+
 }