/*
* This file is part of lanterna (http://code.google.com/p/lanterna/).
*
* lanterna 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.
*
* 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 Lesser General Public License for more details.
*
* 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/>.
*
* Copyright (C) 2010-2012 Martin
*/
package com.googlecode.lanterna.terminal.swing;
import com.googlecode.lanterna.input.InputProvider;
import com.googlecode.lanterna.input.Key;
import com.googlecode.lanterna.input.KeyMappingProfile;
import com.googlecode.lanterna.terminal.AbstractTerminal;
import com.googlecode.lanterna.terminal.TerminalPosition;
import com.googlecode.lanterna.terminal.TerminalSize;
import com.googlecode.lanterna.terminal.XTerm8bitIndexedColorUtils;
import java.awt.*;
import java.awt.event.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.WindowConstants;
/**
* A Swing-based text terminal emulator
* @author Martin
*/
public class SwingTerminal extends AbstractTerminal implements InputProvider
{
private final TerminalRenderer terminalRenderer;
private final Timer blinkTimer;
private JFrame terminalFrame;
private TerminalAppearance appearance;
private TerminalCharacter [][]characterMap;
private TerminalPosition textPosition;
private TerminalCharacterColor currentForegroundColor;
private TerminalCharacterColor currentBackgroundColor;
private boolean currentlyBold;
private boolean currentlyBlinking;
private boolean currentlyUnderlined;
private boolean blinkVisible;
private boolean cursorVisible;
private Queue<Key> keyQueue;
private final Object resizeMutex;
public SwingTerminal()
{
this(160, 40); //By default, create a 160x40 terminal (normal size * 2)
}
public SwingTerminal(TerminalSize terminalSize)
{
this(terminalSize.getColumns(), terminalSize.getRows());
}
public SwingTerminal(int widthInColumns, int heightInRows)
{
this(TerminalAppearance.DEFAULT_APPEARANCE, widthInColumns, heightInRows);
}
public SwingTerminal(TerminalAppearance appearance)
{
this(appearance, 160, 40);
}
public SwingTerminal(TerminalAppearance appearance, int widthInColumns, int heightInRows)
{
this.appearance = appearance;
this.terminalRenderer = new TerminalRenderer();
this.blinkTimer = new Timer(500, new BlinkAction());
this.textPosition = new TerminalPosition(0, 0);
this.characterMap = new TerminalCharacter[heightInRows][widthInColumns];
this.currentForegroundColor = new CharacterANSIColor(Color.WHITE);
this.currentBackgroundColor = new CharacterANSIColor(Color.BLACK);
this.currentlyBold = false;
this.currentlyBlinking = false;
this.currentlyUnderlined = false;
this.blinkVisible = false;
this.cursorVisible = true;
this.keyQueue = new ConcurrentLinkedQueue<Key>();
this.resizeMutex = new Object();
onResized(widthInColumns, heightInRows);
clearScreen();
}
/**
* This method will give you the underlying JFrame for this terminal.
* Please be careful when calling methods on the JFrame, especially if
* you modify the rendering or content pane, since that may cause the
* terminal rendering to malfunction.
*
* <p>Good uses of this method is to set the window title, window icon list
* and so on. If you add more logic to this JFrame, you should probably
* ask yourself why you are using Lanterna to begin with.
*
* @return JFrame object used by this SwingTerminal
*/
public JFrame getJFrame() {
return terminalFrame;
}
private class BlinkAction implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
blinkVisible = !blinkVisible;
terminalRenderer.repaint();
}
}
@Override
public void addInputProfile(KeyMappingProfile profile)
{
}
@Override
public void applyBackgroundColor(Color color) {
currentBackgroundColor = new CharacterANSIColor(color);
}
@Override
public void applyBackgroundColor(int r, int g, int b) {
currentBackgroundColor = new Character24bitColor(new java.awt.Color(r, g, b));
}
@Override
public void applyBackgroundColor(int index) {
currentBackgroundColor = new CharacterIndexedColor(index);
}
@Override
public void applyForegroundColor(Color color) {
currentForegroundColor = new CharacterANSIColor(color);
}
@Override
public void applyForegroundColor(int r, int g, int b) {
currentForegroundColor = new Character24bitColor(new java.awt.Color(r, g, b));
}
@Override
public void applyForegroundColor(int index) {
currentForegroundColor = new CharacterIndexedColor(index);
}
@Override
public void applySGR(SGR... options)
{
for(SGR sgr: options)
{
if(sgr == SGR.RESET_ALL) {
currentlyBold = false;
currentlyBlinking = false;
currentlyUnderlined = false;
currentForegroundColor = new CharacterANSIColor(Color.DEFAULT);
currentBackgroundColor = new CharacterANSIColor(Color.BLACK);
}
else if(sgr == SGR.ENTER_BOLD)
currentlyBold = true;
else if(sgr == SGR.EXIT_BOLD)
currentlyBold = false;
else if(sgr == SGR.ENTER_BLINK)
currentlyBlinking = true;
else if(sgr == SGR.EXIT_BLINK)
currentlyBlinking = false;
else if(sgr == SGR.ENTER_UNDERLINE)
currentlyUnderlined = true;
else if(sgr == SGR.EXIT_UNDERLINE)
currentlyUnderlined = false;
}
}
@Override
public void clearScreen()
{
synchronized(resizeMutex) {
for(int y = 0; y < size().getRows(); y++)
for(int x = 0; x < size().getColumns(); x++)
this.characterMap[y][x] = new TerminalCharacter(
' ',
new CharacterANSIColor(Color.DEFAULT),
new CharacterANSIColor(Color.BLACK),
false,
false,
false);
moveCursor(0,0);
}
}
@Override
public void enterPrivateMode()
{
terminalFrame = new JFrame("Terminal");
terminalFrame.addComponentListener(new FrameResizeListener());
terminalFrame.getContentPane().setLayout(new BorderLayout());
terminalFrame.getContentPane().add(terminalRenderer, BorderLayout.CENTER);
terminalFrame.addKeyListener(new KeyCapturer());
terminalFrame.pack();
terminalFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
terminalFrame.setLocationByPlatform(true);
terminalFrame.setVisible(true);
terminalFrame.setFocusTraversalKeysEnabled(false);
//terminalEmulator.setSize(terminalEmulator.getPreferredSize());
terminalFrame.pack();
blinkTimer.start();
}
@Override
public void exitPrivateMode()
{
if(terminalFrame == null)
return;
blinkTimer.stop();
terminalFrame.setVisible(false);
terminalFrame.dispose();
}
@Override
public void moveCursor(int x, int y)
{
if(x < 0)
x = 0;
if(x >= size().getColumns())
x = size().getColumns() - 1;
if(y < 0)
y = 0;
if(y >= size().getRows())
y = size().getRows() - 1;
textPosition.setColumn(x);
textPosition.setRow(y);
refreshScreen();
}
@Override
public void setCursorVisible(boolean visible) {
this.cursorVisible = visible;
refreshScreen();
}
@Override
public synchronized void putCharacter(char c)
{
characterMap[textPosition.getRow()][textPosition.getColumn()] =
new TerminalCharacter(c, currentForegroundColor, currentBackgroundColor, currentlyBold, currentlyBlinking, currentlyUnderlined);
if(textPosition.getColumn() == size().getColumns() - 1 &&
textPosition.getRow() == size().getRows() - 1)
moveCursor(0, textPosition.getRow());
if(textPosition.getColumn() == size().getColumns() - 1)
moveCursor(0, textPosition.getRow() + 1);
else
moveCursor(textPosition.getColumn() + 1, textPosition.getRow());
}
@Override
public TerminalSize queryTerminalSize()
{
//Just bypass to getTerminalSize()
return getTerminalSize();
}
@Override
public TerminalSize getTerminalSize() {
return size();
}
private synchronized void resize(int newSizeColumns, int newSizeRows)
{
TerminalCharacter [][]newCharacterMap = new TerminalCharacter[newSizeRows][newSizeColumns];
for(int y = 0; y < newSizeRows; y++)
for(int x = 0; x < newSizeColumns; x++)
newCharacterMap[y][x] = new TerminalCharacter(
' ',
new CharacterANSIColor(Color.WHITE),
new CharacterANSIColor(Color.BLACK),
false,
false,
false);
synchronized(resizeMutex) {
for(int y = 0; y < size().getRows() && y < newSizeRows; y++) {
for(int x = 0; x < size().getColumns() && x < newSizeColumns; x++) {
newCharacterMap[y][x] = this.characterMap[y][x];
}
}
this.characterMap = newCharacterMap;
SwingUtilities.invokeLater(new Runnable() {
public void run()
{
terminalFrame.pack();
terminalFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
});
onResized(newSizeColumns, newSizeRows);
}
}
@Override
public Key readInput()
{
return keyQueue.poll();
}
@Override
public void flush() {
//Not needed
}
/**
* Changes the current color palett to a new one supplied
* @param palette Palett to use
*/
public void setTerminalPalette(TerminalPalette palette) {
appearance = appearance.withPalette(palette);
refreshScreen();
}
private void refreshScreen()
{
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run()
{
terminalRenderer.repaint();
}
});
}
/**
* Returns the size of the terminal, which will always be same as calling
* getLastKnownSize(), but since that could be confusing when reading the
* code, I added this helper method.
*/
private TerminalSize size()
{
return getLastKnownSize();
}
private class KeyCapturer extends KeyAdapter
{
private Set<Character> typedIgnore = new HashSet<Character>(
Arrays.asList('\n', '\t', '\r', '\b'));
@Override
public void keyTyped(KeyEvent e)
{
char character = e.getKeyChar();
boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
if(!typedIgnore.contains(character)) {
if(ctrlDown) {
//We need to re-adjust the character if ctrl is pressed, just like for the AnsiTerminal
character = (char)('a' - 1 + character);
}
keyQueue.add(new Key(character, ctrlDown, altDown));
}
}
@Override
public void keyPressed(KeyEvent e)
{
if(e.getKeyCode() == KeyEvent.VK_ENTER)
keyQueue.add(new Key(Key.Kind.Enter));
else if(e.getKeyCode() == KeyEvent.VK_ESCAPE)
keyQueue.add(new Key(Key.Kind.Escape));
else if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE)
keyQueue.add(new Key(Key.Kind.Backspace));
else if(e.getKeyCode() == KeyEvent.VK_LEFT)
keyQueue.add(new Key(Key.Kind.ArrowLeft));
else if(e.getKeyCode() == KeyEvent.VK_RIGHT)
keyQueue.add(new Key(Key.Kind.ArrowRight));
else if(e.getKeyCode() == KeyEvent.VK_UP)
keyQueue.add(new Key(Key.Kind.ArrowUp));
else if(e.getKeyCode() == KeyEvent.VK_DOWN)
keyQueue.add(new Key(Key.Kind.ArrowDown));
else if(e.getKeyCode() == KeyEvent.VK_INSERT)
keyQueue.add(new Key(Key.Kind.Insert));
else if(e.getKeyCode() == KeyEvent.VK_DELETE)
keyQueue.add(new Key(Key.Kind.Delete));
else if(e.getKeyCode() == KeyEvent.VK_HOME)
keyQueue.add(new Key(Key.Kind.Home));
else if(e.getKeyCode() == KeyEvent.VK_END)
keyQueue.add(new Key(Key.Kind.End));
else if(e.getKeyCode() == KeyEvent.VK_PAGE_UP)
keyQueue.add(new Key(Key.Kind.PageUp));
else if(e.getKeyCode() == KeyEvent.VK_PAGE_DOWN)
keyQueue.add(new Key(Key.Kind.PageDown));
else if(e.getKeyCode() == KeyEvent.VK_TAB) {
if(e.isShiftDown())
keyQueue.add(new Key(Key.Kind.ReverseTab));
else
keyQueue.add(new Key(Key.Kind.Tab));
}
else {
//keyTyped doesn't catch this scenario (for whatever reason...) so we have to do it here
boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
if(altDown && ctrlDown && e.getKeyCode() >= 'A' && e.getKeyCode() <= 'Z') {
char asLowerCase = Character.toLowerCase((char)e.getKeyCode());
keyQueue.add(new Key(asLowerCase, true, true));
}
}
}
}
private class FrameResizeListener extends ComponentAdapter
{
private int lastWidth = -1;
private int lastHeight = -1;
@Override
public void componentResized(ComponentEvent e)
{
if(e.getComponent() == null || e.getComponent() instanceof JFrame == false)
return;
JFrame frame = (JFrame)e.getComponent();
Container contentPane = frame.getContentPane();
int newWidth = contentPane.getWidth();
int newHeight = contentPane.getHeight();
FontMetrics fontMetrics = frame.getGraphics().getFontMetrics(appearance.getNormalTextFont());
int consoleWidth = newWidth / fontMetrics.charWidth(' ');
int consoleHeight = newHeight / fontMetrics.getHeight();
if(consoleWidth == lastWidth && consoleHeight == lastHeight)
return;
lastWidth = consoleWidth;
lastHeight = consoleHeight;
resize(consoleWidth, consoleHeight);
}
}
private class TerminalRenderer extends JComponent
{
public TerminalRenderer()
{
}
@Override
public Dimension getPreferredSize()
{
FontMetrics fontMetrics = getGraphics().getFontMetrics(appearance.getNormalTextFont());
final int screenWidth = SwingTerminal.this.size().getColumns() * fontMetrics.charWidth(' ');
final int screenHeight = SwingTerminal.this.size().getRows() * fontMetrics.getHeight();
return new Dimension(screenWidth, screenHeight);
}
@Override
protected void paintComponent(Graphics g)
{
final Graphics2D graphics2D = (Graphics2D)g.create();
graphics2D.setFont(appearance.getNormalTextFont());
graphics2D.setColor(java.awt.Color.BLACK);
graphics2D.fillRect(0, 0, getWidth(), getHeight());
final FontMetrics fontMetrics = getGraphics().getFontMetrics(appearance.getNormalTextFont());
final int charWidth = fontMetrics.charWidth(' ');
final int charHeight = fontMetrics.getHeight();
for(int row = 0; row < SwingTerminal.this.size().getRows(); row++) {
for(int col = 0; col < SwingTerminal.this.size().getColumns(); col++) {
TerminalCharacter character = characterMap[row][col];
if(cursorVisible && row == textPosition.getRow() && col == textPosition.getColumn())
graphics2D.setColor(character.getForegroundAsAWTColor(appearance.useBrightColorsOnBold()));
else
graphics2D.setColor(character.getBackgroundAsAWTColor());
graphics2D.fillRect(col * charWidth, row * charHeight, charWidth, charHeight);
if((cursorVisible && row == textPosition.getRow() && col == textPosition.getColumn()) ||
(character.isBlinking() && !blinkVisible))
graphics2D.setColor(character.getBackgroundAsAWTColor());
else
graphics2D.setColor(character.getForegroundAsAWTColor(appearance.useBrightColorsOnBold()));
if(character.isBold())
graphics2D.setFont(appearance.getBoldTextFont());
if(character.isUnderlined())
graphics2D.drawLine(
col * charWidth, ((row + 1) * charHeight) - 1,
(col+1) * charWidth, ((row + 1) * charHeight) - 1);
graphics2D.drawString(character.toString(), col * charWidth, ((row + 1) * charHeight) - fontMetrics.getDescent());
if(character.isBold())
graphics2D.setFont(appearance.getNormalTextFont()); //Restore the original font
}
}
graphics2D.dispose();
}
}
private static class TerminalCharacter {
private final char character;
private final TerminalCharacterColor foreground;
private final TerminalCharacterColor background;
private final boolean bold;
private final boolean blinking;
private final boolean underlined;
TerminalCharacter(
char character,
TerminalCharacterColor foreground,
TerminalCharacterColor background,
boolean bold,
boolean blinking,
boolean underlined) {
this.character = character;
this.foreground = foreground;
this.background = background;
this.bold = bold;
this.blinking = blinking;
this.underlined = underlined;
}
public boolean isBold() {
return bold;
}
public boolean isBlinking() {
return blinking;
}
public boolean isUnderlined() {
return underlined;
}
private java.awt.Color getForegroundAsAWTColor(boolean useBrightOnBold) {
return foreground.getColor(isBold() && useBrightOnBold, true);
}
private java.awt.Color getBackgroundAsAWTColor() {
return background.getColor(false, false);
}
@Override
public String toString() {
return Character.toString(character);
}
}
private static abstract class TerminalCharacterColor {
public abstract java.awt.Color getColor(boolean brightHint, boolean foregroundHint);
}
private class CharacterANSIColor extends TerminalCharacterColor {
private final Color color;
CharacterANSIColor(Color color) {
this.color = color;
}
@Override
public java.awt.Color getColor(boolean brightHint, boolean foregroundHint) {
switch(color) {
case BLACK:
if(brightHint)
return appearance.getColorPalette().getBrightBlack();
else
return appearance.getColorPalette().getNormalBlack();
case BLUE:
if(brightHint)
return appearance.getColorPalette().getBrightBlue();
else
return appearance.getColorPalette().getNormalBlue();
case CYAN:
if(brightHint)
return appearance.getColorPalette().getBrightCyan();
else
return appearance.getColorPalette().getNormalCyan();
case DEFAULT:
if(brightHint)
return appearance.getColorPalette().getDefaultBrightColor();
else
return appearance.getColorPalette().getDefaultColor();
case GREEN:
if(brightHint)
return appearance.getColorPalette().getBrightGreen();
else
return appearance.getColorPalette().getNormalGreen();
case MAGENTA:
if(brightHint)
return appearance.getColorPalette().getBrightMagenta();
else
return appearance.getColorPalette().getNormalMagenta();
case RED:
if(brightHint)
return appearance.getColorPalette().getBrightRed();
else
return appearance.getColorPalette().getNormalRed();
case WHITE:
if(brightHint)
return appearance.getColorPalette().getBrightWhite();
else
return appearance.getColorPalette().getNormalWhite();
case YELLOW:
if(brightHint)
return appearance.getColorPalette().getBrightYellow();
else
return appearance.getColorPalette().getNormalYellow();
}
return java.awt.Color.PINK;
}
}
private class CharacterIndexedColor extends TerminalCharacterColor {
private final int index;
CharacterIndexedColor(int index) {
this.index = index;
}
@Override
public java.awt.Color getColor(boolean brightHint, boolean foregroundHint) {
return XTerm8bitIndexedColorUtils.getAWTColor(index, appearance.getColorPalette());
}
}
private static class Character24bitColor extends TerminalCharacterColor {
private final java.awt.Color c;
Character24bitColor(java.awt.Color c) {
this.c = c;
}
@Override
public java.awt.Color getColor(boolean brightHint, boolean foregroundHint) {
return c;
}
}
}