X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTTerminalWindow.java;h=d17a2c4cdd53b3564fbb952276834f35822146b3;hb=d11152032d9342cc178ba20adab40083a9f37081;hp=74f71edc7c0af8b9387dddca88a246d275810a9a;hpb=107bba162165a2e6fc88913065f52c14d04b3883;p=fanfix.git diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 74f71ed..d17a2c4 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * Copyright (C) 2019 Kevin Lamonte * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -28,14 +28,24 @@ */ package jexer; +import java.awt.image.BufferedImage; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; + +import java.io.InputStream; import java.io.IOException; import java.lang.reflect.Field; import java.text.MessageFormat; -import java.util.LinkedList; +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. */ @@ -77,10 +86,52 @@ public class TTerminalWindow extends TScrollableWindow /** * If true, we are using the ptypipe utility to support dynamic window * resizing. ptypipe is available at - * https://github.com/klamonte/ptypipe . + * https://gitlab.com/klamonte/ptypipe . */ private boolean ptypipe = false; + /** + * If true, close the window when the shell exits. + */ + 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; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -96,7 +147,25 @@ 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")); + } + + /** + * Public constructor spawns a custom command line. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + */ + 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+"), + closeOnExit); } /** @@ -111,9 +180,30 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final int flags, final String [] command) { + this(application, x, y, flags, command, + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); + } + + /** + * Public constructor spawns a custom command line. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param command the command line to execute + * @param closeOnExit if true, close the window when the command exits + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final int flags, final String [] command, + final boolean closeOnExit) { + super(application, i18n.getString("windowTitle"), x, y, 80 + 2, 24 + 2, flags); + this.closeOnExit = closeOnExit; + String [] fullCommand; // Spawn a shell and pass its I/O to the other constructor. @@ -161,9 +251,29 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final int flags) { + this(application, x, y, flags, + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); + + } + + /** + * Public constructor spawns a shell. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param closeOnExit if true, close the window when the shell exits + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final int flags, final boolean closeOnExit) { + super(application, i18n.getString("windowTitle"), x, y, 80 + 2, 24 + 2, flags); + this.closeOnExit = closeOnExit; + String cmdShellWindows = "cmd.exe"; // You cannot run a login shell in a bare Process interactively, due @@ -183,16 +293,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+")); } } @@ -224,7 +334,7 @@ public class TTerminalWindow extends TScrollableWindow + getVerticalValue(); assert (visibleBottom >= 0); - List preceedingBlankLines = new LinkedList(); + List preceedingBlankLines = new ArrayList(); int visibleTop = visibleBottom - visibleHeight; if (visibleTop < 0) { for (int i = visibleTop; i < 0; i++) { @@ -234,11 +344,11 @@ public class TTerminalWindow extends TScrollableWindow } assert (visibleTop >= 0); - List displayLines = new LinkedList(); + List displayLines = new ArrayList(); displayLines.addAll(scrollback); displayLines.addAll(display); - List visibleLines = new LinkedList(); + List visibleLines = new ArrayList(); visibleLines.addAll(preceedingBlankLines); visibleLines.addAll(displayLines.subList(visibleTop, visibleBottom)); @@ -277,10 +387,9 @@ public class TTerminalWindow extends TScrollableWindow } } if (line.isDoubleWidth()) { - getScreen().putCharXY((i * 2) + 1, row, newCell); - getScreen().putCharXY((i * 2) + 2, row, ' ', newCell); + putDoubleWidthCharXY(line, (i * 2) + 1, row, newCell); } else { - getScreen().putCharXY(i + 1, row, newCell); + putCharXY(i + 1, row, newCell); } } row++; @@ -292,8 +401,7 @@ public class TTerminalWindow extends TScrollableWindow CellAttributes background = new CellAttributes(); // Fill in the blank lines on bottom for (int i = 0; i < visibleHeight; i++) { - getScreen().hLineXY(1, i + row, getWidth() - 2, ' ', - background); + hLineXY(1, i + row, getWidth() - 2, ' ', background); } } // synchronized (emulator) @@ -711,6 +819,9 @@ public class TTerminalWindow extends TScrollableWindow * Hook for subclasses to be notified of the shell termination. */ public void onShellExit() { + if (closeOnExit) { + close(); + } getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT)); } @@ -722,6 +833,7 @@ public class TTerminalWindow extends TScrollableWindow // Synchronize against the emulator so we don't stomp on its reader // thread. synchronized (emulator) { + setHiddenMouse(emulator.hasHiddenMousePointer()); setCursorX(emulator.getCursorX() + 1); setCursorY(emulator.getCursorY() + 1 @@ -787,7 +899,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()) { @@ -805,4 +917,157 @@ 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 = 16; + int textHeight = 20; + + if (getScreen() instanceof SwingTerminal) { + SwingTerminal terminal = (SwingTerminal) getScreen(); + + textWidth = terminal.getTextWidth(); + textHeight = terminal.getTextHeight(); + } else if (getScreen() instanceof ECMA48Terminal) { + ECMA48Terminal terminal = (ECMA48Terminal) getScreen(); + + textWidth = terminal.getTextWidth(); + textHeight = terminal.getTextHeight(); + } 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; + + 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. + if (getScreen() instanceof ECMA48Terminal) { + // BUG: the background color is coming in the same as the + // foreground color. For now, don't draw it. + } else { + gr2.setColor(SwingTerminal.attrToBackgroundColor(cell)); + gr2.fillRect(0, 0, image.getWidth(), image.getHeight()); + } + 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); + 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); + 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 cache. + glyphCache = new HashMap(); + } + }