X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTTerminalWindow.java;h=945536d57f3e01849a0dc42b2808680707e63c33;hb=49380c21bc74308370dd85d30f7c0c689eb1cab8;hp=ec4fd07ec7957cffe92943d001798ae2df79a41e;hpb=978a5d8f650488c8840d54ccc3032599ca50a084;p=fanfix.git diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index ec4fd07..945536d 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -28,14 +28,24 @@ */ package jexer; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +import java.io.InputStream; import java.io.IOException; import java.lang.reflect.Field; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ResourceBundle; +import jexer.backend.ECMA48Terminal; +import jexer.backend.MultiScreen; +import jexer.backend.SwingTerminal; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.event.TKeypressEvent; @@ -54,7 +64,6 @@ import static jexer.TKeypress.*; public class TTerminalWindow extends TScrollableWindow implements DisplayListener { - /** * Translated strings. */ @@ -86,6 +95,55 @@ public class TTerminalWindow extends TScrollableWindow */ private boolean closeOnExit = false; + /** + * System-dependent Y adjustment for text in the character cell + * (double-height). + */ + private int doubleTextAdjustY = 0; + + /** + * System-dependent X adjustment for text in the character cell + * (double-height). + */ + private int doubleTextAdjustX = 0; + + /** + * Descent of a character cell in pixels (double-height). + */ + private int doubleMaxDescent = 0; + + /** + * Double-width font. + */ + private Font doubleFont = null; + + /** + * Last text width value. + */ + private int lastTextWidth = -1; + + /** + * Last text height value. + */ + private int lastTextHeight = -1; + + /** + * A cache of previously-rendered double-width glyphs. + */ + private Map glyphCache; + + /** + * A cache of previously-rendered double-width glyphs for blinking text, + * when it is not visible. + */ + private Map glyphCacheBlink; + + /** + * The blink state, used only by ECMA48 backend and when double-width + * chars must be drawn. + */ + private boolean blinkState = true; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -101,7 +159,7 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final String commandLine) { - this(application, x, y, RESIZABLE, commandLine.split("\\s"), + this(application, x, y, RESIZABLE, commandLine.split("\\s+"), System.getProperty("jexer.TTerminal.closeOnExit", "false").equals("true")); } @@ -118,7 +176,7 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final String commandLine, final boolean closeOnExit) { - this(application, x, y, RESIZABLE, commandLine.split("\\s"), + this(application, x, y, RESIZABLE, commandLine.split("\\s+"), closeOnExit); } @@ -247,16 +305,16 @@ public class TTerminalWindow extends TScrollableWindow equals("true")) ) { ptypipe = true; - spawnShell(cmdShellPtypipe.split("\\s")); + spawnShell(cmdShellPtypipe.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Windows")) { - spawnShell(cmdShellWindows.split("\\s")); + spawnShell(cmdShellWindows.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Mac")) { - spawnShell(cmdShellBSD.split("\\s")); + spawnShell(cmdShellBSD.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Linux")) { - spawnShell(cmdShellGNU.split("\\s")); + spawnShell(cmdShellGNU.split("\\s+")); } else { // When all else fails, assume GNU. - spawnShell(cmdShellGNU.split("\\s")); + spawnShell(cmdShellGNU.split("\\s+")); } } @@ -341,8 +399,7 @@ public class TTerminalWindow extends TScrollableWindow } } if (line.isDoubleWidth()) { - putCharXY((i * 2) + 1, row, newCell); - putCharXY((i * 2) + 2, row, ' ', newCell); + putDoubleWidthCharXY(line, (i * 2) + 1, row, newCell); } else { putCharXY(i + 1, row, newCell); } @@ -493,13 +550,17 @@ public class TTerminalWindow extends TScrollableWindow return; } - if (mouse.isMouseWheelUp()) { - verticalDecrement(); - return; - } - if (mouse.isMouseWheelDown()) { - verticalIncrement(); - return; + // If the emulator is tracking mouse buttons, it needs to see wheel + // events. + if (emulator.getMouseProtocol() == ECMA48.MouseProtocol.OFF) { + if (mouse.isMouseWheelUp()) { + verticalDecrement(); + return; + } + if (mouse.isMouseWheelDown()) { + verticalIncrement(); + return; + } } if (mouseOnEmulator(mouse)) { synchronized (emulator) { @@ -704,6 +765,10 @@ public class TTerminalWindow extends TScrollableWindow // Add shortcut text newStatusBar(i18n.getString("statusBarRunning")); + + // Pass the correct text cell width/height to the emulator + emulator.setTextWidth(getScreen().getTextWidth()); + emulator.setTextHeight(getScreen().getTextHeight()); } /** @@ -789,7 +854,7 @@ public class TTerminalWindow extends TScrollableWindow // thread. synchronized (emulator) { setHiddenMouse(emulator.hasHiddenMousePointer()); - + setCursorX(emulator.getCursorX() + 1); setCursorY(emulator.getCursorY() + 1 + (getHeight() - 2 - emulator.getHeight()) @@ -854,7 +919,7 @@ public class TTerminalWindow extends TScrollableWindow * @param mouse a mouse-based event * @return whether or not the mouse is on the emulator */ - private final boolean mouseOnEmulator(final TMouseEvent mouse) { + private boolean mouseOnEmulator(final TMouseEvent mouse) { synchronized (emulator) { if (!emulator.isReading()) { @@ -872,4 +937,190 @@ public class TTerminalWindow extends TScrollableWindow return false; } + /** + * Draw glyphs for a double-width or double-height VT100 cell to two + * screen cells. + * + * @param line the line this VT100 cell is in + * @param x the X position to draw the left half to + * @param y the Y position to draw to + * @param cell the cell to draw + */ + private void putDoubleWidthCharXY(final DisplayLine line, final int x, + final int y, final Cell cell) { + + int textWidth = getScreen().getTextWidth(); + int textHeight = getScreen().getTextHeight(); + boolean cursorBlinkVisible = true; + + if (getScreen() instanceof SwingTerminal) { + SwingTerminal terminal = (SwingTerminal) getScreen(); + cursorBlinkVisible = terminal.getCursorBlinkVisible(); + } else if (getScreen() instanceof ECMA48Terminal) { + ECMA48Terminal terminal = (ECMA48Terminal) getScreen(); + + if (!terminal.hasSixel()) { + // The backend does not have sixel support, draw this as text + // and bail out. + putCharXY(x, y, cell); + putCharXY(x + 1, y, ' ', cell); + return; + } + cursorBlinkVisible = blinkState; + } else { + // We don't know how to dray glyphs to this screen, draw them as + // text and bail out. + putCharXY(x, y, cell); + putCharXY(x + 1, y, ' ', cell); + return; + } + + if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) { + // Screen size has changed, reset all fonts. + setupFonts(textHeight); + lastTextWidth = textWidth; + lastTextHeight = textHeight; + } + assert (doubleFont != null); + + BufferedImage image = null; + if (cell.isBlink() && !cursorBlinkVisible) { + image = glyphCacheBlink.get(cell); + } else { + image = glyphCache.get(cell); + } + if (image == null) { + // Generate glyph and draw it to an image. + image = new BufferedImage(textWidth * 2, textHeight * 2, + BufferedImage.TYPE_INT_ARGB); + Graphics2D gr2 = image.createGraphics(); + gr2.setFont(doubleFont); + + // Draw the background rectangle, then the foreground character. + gr2.setColor(SwingTerminal.attrToBackgroundColor(cell)); + gr2.fillRect(0, 0, image.getWidth(), image.getHeight()); + if (!cell.isBlink() + || (cell.isBlink() && cursorBlinkVisible) + ) { + gr2.setColor(SwingTerminal.attrToForegroundColor(cell)); + char [] chars = new char[1]; + chars[0] = cell.getChar(); + gr2.drawChars(chars, 0, 1, doubleTextAdjustX, + (textHeight * 2) - doubleMaxDescent + doubleTextAdjustY); + + if (cell.isUnderline() && (line.getDoubleHeight() != 1)) { + gr2.fillRect(0, textHeight - 2, textWidth, 2); + } + } + gr2.dispose(); + + // Now save this generated image, using 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); + } + } + + // Now that we have the double-wide glyph drawn, copy the right + // pieces of it to the cells. + Cell left = new Cell(); + Cell right = new Cell(); + left.setTo(cell); + right.setTo(cell); + right.setChar(' '); + BufferedImage leftImage = null; + BufferedImage rightImage = null; + switch (line.getDoubleHeight()) { + case 1: + // Top half double height + leftImage = image.getSubimage(0, 0, textWidth, textHeight); + rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight); + break; + case 2: + // Bottom half double height + leftImage = image.getSubimage(0, textHeight, textWidth, textHeight); + rightImage = image.getSubimage(textWidth, textHeight, + textWidth, textHeight); + break; + default: + // Either single height double-width, or error fallback + BufferedImage wideImage = new BufferedImage(textWidth * 2, + textHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D grWide = wideImage.createGraphics(); + grWide.drawImage(image, 0, 0, wideImage.getWidth(), + wideImage.getHeight(), null); + grWide.dispose(); + leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight); + rightImage = wideImage.getSubimage(textWidth, 0, textWidth, + textHeight); + break; + } + left.setImage(leftImage); + right.setImage(rightImage); + // Since we have image data, ditch the character here. Otherwise, a + // drawBoxShadow() over the terminal window will show the characters + // which looks wrong. + left.setChar(' '); + right.setChar(' '); + putCharXY(x, y, left); + putCharXY(x + 1, y, right); + } + + /** + * Set up the single and double-width fonts. + * + * @param fontSize the size of font to request for the single-width font. + * The double-width font will be 2x this value. + */ + private void setupFonts(final int fontSize) { + try { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + InputStream in = loader.getResourceAsStream(SwingTerminal.FONTFILE); + Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in); + Font terminusDouble = terminusRoot.deriveFont(Font.PLAIN, + fontSize * 2); + doubleFont = terminusDouble; + } catch (java.awt.FontFormatException e) { + new TExceptionDialog(getApplication(), e); + doubleFont = new Font(Font.MONOSPACED, Font.PLAIN, fontSize * 2); + } catch (java.io.IOException e) { + new TExceptionDialog(getApplication(), e); + doubleFont = new Font(Font.MONOSPACED, Font.PLAIN, fontSize * 2); + } + + // Get font descent. + BufferedImage image = new BufferedImage(fontSize * 10, fontSize * 10, + BufferedImage.TYPE_INT_ARGB); + Graphics2D gr = image.createGraphics(); + gr.setFont(doubleFont); + FontMetrics fm = gr.getFontMetrics(); + doubleMaxDescent = fm.getMaxDescent(); + + gr.dispose(); + + // (Re)create the glyph caches. + glyphCache = new HashMap(); + glyphCacheBlink = new HashMap(); + + // Special case: the ECMA48 backend needs to have a timer to drive + // its blink state. + if (getScreen() instanceof jexer.backend.ECMA48Terminal) { + // Blink every 500 millis. + long millis = 500; + getApplication().addTimer(millis, true, + new TAction() { + public void DO() { + blinkState = !blinkState; + getApplication().doRepaint(); + } + } + ); + } + + } + }