/*
* 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-2017 Martin Berglund
*/
package com.googlecode.lanterna.terminal.swing;
import com.googlecode.lanterna.*;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.input.DefaultKeyDecodingProfile;
import com.googlecode.lanterna.input.InputDecoder;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.terminal.IOSafeTerminal;
import com.googlecode.lanterna.terminal.TerminalResizeListener;
import com.googlecode.lanterna.terminal.virtual.DefaultVirtualTerminal;
import com.googlecode.lanterna.terminal.virtual.VirtualTerminal;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.StringReader;
import java.util.*;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This is the class that does the heavy lifting for both {@link AWTTerminal} and {@link SwingTerminal}. It maintains
* most of the external terminal state and also the main back buffer that is copied to the components area on draw
* operations.
*
* @author martin
*/
abstract class GraphicalTerminalImplementation implements IOSafeTerminal {
private final TerminalEmulatorDeviceConfiguration deviceConfiguration;
private final TerminalEmulatorColorConfiguration colorConfiguration;
private final DefaultVirtualTerminal virtualTerminal;
private final BlockingQueue<KeyStroke> keyQueue;
private final TerminalScrollController scrollController;
private final DirtyCellsLookupTable dirtyCellsLookupTable;
private final String enquiryString;
private boolean cursorIsVisible;
private boolean enableInput;
private Timer blinkTimer;
private boolean hasBlinkingText;
private boolean blinkOn;
private boolean bellOn;
private boolean needFullRedraw;
private TerminalPosition lastDrawnCursorPosition;
private int lastBufferUpdateScrollPosition;
private int lastComponentWidth;
private int lastComponentHeight;
// We use two different data structures to optimize drawing
// * A list of modified characters since the last draw (stored in VirtualTerminal)
// * A backbuffer with the graphics content
//
// The buffer is the most important one as it allows us to re-use what was drawn earlier. It is not reset on every
// drawing operation but updates just in those places where the map tells us the character has changed.
private BufferedImage backbuffer;
// Used as a middle-ground when copying large segments when scrolling
private BufferedImage copybuffer;
/**
* Creates a new GraphicalTerminalImplementation component using custom settings and a custom scroll controller. The
* scrolling controller will be notified when the terminal's history size grows and will be called when this class
* needs to figure out the current scrolling position.
* @param initialTerminalSize Initial size of the terminal, which will be used when calculating the preferred size
* of the component. If null, it will default to 80x25. If the AWT layout manager forces
* the component to a different size, the value of this parameter won't have any meaning
* @param deviceConfiguration Device configuration to use for this SwingTerminal
* @param colorConfiguration Color configuration to use for this SwingTerminal
* @param scrollController Controller to use for scrolling, the object passed in will be notified whenever the
* scrollable area has changed
*/
GraphicalTerminalImplementation(
TerminalSize initialTerminalSize,
TerminalEmulatorDeviceConfiguration deviceConfiguration,
TerminalEmulatorColorConfiguration colorConfiguration,
TerminalScrollController scrollController) {
//This is kind of meaningless since we don't know how large the
//component is at this point, but we should set it to something
if(initialTerminalSize == null) {
initialTerminalSize = new TerminalSize(80, 24);
}
this.virtualTerminal = new DefaultVirtualTerminal(initialTerminalSize);
this.keyQueue = new LinkedBlockingQueue<KeyStroke>();
this.deviceConfiguration = deviceConfiguration;
this.colorConfiguration = colorConfiguration;
this.scrollController = scrollController;
this.dirtyCellsLookupTable = new DirtyCellsLookupTable();
this.cursorIsVisible = true; //Always start with an activate and visible cursor
this.enableInput = false; //Start with input disabled and activate it once the window is visible
this.enquiryString = "TerminalEmulator";
this.lastDrawnCursorPosition = null;
this.lastBufferUpdateScrollPosition = 0;
this.lastComponentHeight = 0;
this.lastComponentWidth = 0;
this.backbuffer = null; // We don't know the dimensions yet
this.copybuffer = null;
this.blinkTimer = null;
this.hasBlinkingText = false; // Assume initial content doesn't have any blinking text
this.blinkOn = true;
this.needFullRedraw = false;
virtualTerminal.setBacklogSize(deviceConfiguration.getLineBufferScrollbackSize());
}
///////////
// First abstract methods that are implemented in AWTTerminalImplementation and SwingTerminalImplementation
///////////
/**
* Used to find out the font height, in pixels
* @return Terminal font height in pixels
*/
abstract int getFontHeight();
/**
* Used to find out the font width, in pixels
* @return Terminal font width in pixels
*/
abstract int getFontWidth();
/**
* Used when requiring the total height of the terminal component, in pixels
* @return Height of the terminal component, in pixels
*/
abstract int getHeight();
/**
* Used when requiring the total width of the terminal component, in pixels
* @return Width of the terminal component, in pixels
*/
abstract int getWidth();
/**
* Returning the AWT font to use for the specific character. This might not always be the same, in case a we are
* trying to draw an unusual character (probably CJK) which isn't contained in the standard terminal font.
* @param character Character to get the font for
* @return Font to be used for this character
*/
abstract Font getFontForCharacter(TextCharacter character);
/**
* Returns {@code true} if anti-aliasing is enabled, {@code false} otherwise
* @return {@code true} if anti-aliasing is enabled, {@code false} otherwise
*/
abstract boolean isTextAntiAliased();
/**
* Called by the {@code GraphicalTerminalImplementation} when it would like the OS to schedule a repaint of the
* window
*/
abstract void repaint();
synchronized void onCreated() {
startBlinkTimer();
enableInput = true;
// Reset the queue, just be to sure
keyQueue.clear();
}
synchronized void onDestroyed() {
stopBlinkTimer();
enableInput = false;
// If a thread is blocked, waiting on something in the keyQueue...
keyQueue.add(new KeyStroke(KeyType.EOF));
}
/**
* Start the timer that triggers blinking
*/
synchronized void startBlinkTimer() {
if(blinkTimer != null) {
// Already on!
return;
}
blinkTimer = new Timer("LanternaTerminalBlinkTimer", true);
blinkTimer.schedule(new TimerTask() {
@Override
public void run() {
blinkOn = !blinkOn;
if(hasBlinkingText) {
repaint();
}
}
}, deviceConfiguration.getBlinkLengthInMilliSeconds(), deviceConfiguration.getBlinkLengthInMilliSeconds());
}
/**
* Stops the timer the triggers blinking
*/
synchronized void stopBlinkTimer() {
if(blinkTimer == null) {
// Already off!
return;
}
blinkTimer.cancel();
blinkTimer = null;
}
///////////
// Implement all the Swing-related methods
///////////
/**
* Calculates the preferred size of this terminal
* @return Preferred size of this terminal
*/
synchronized Dimension getPreferredSize() {
return new Dimension(getFontWidth() * virtualTerminal.getTerminalSize().getColumns(),
getFontHeight() * virtualTerminal.getTerminalSize().getRows());
}
/**
* Updates the back buffer (if necessary) and draws it to the component's surface
* @param componentGraphics Object to use when drawing to the component's surface
*/
synchronized void paintComponent(Graphics componentGraphics) {
int width = getWidth();
int height = getHeight();
this.scrollController.updateModel(
virtualTerminal.getBufferLineCount() * getFontHeight(),
height);
boolean needToUpdateBackBuffer =
// User has used the scrollbar, we need to update the back buffer to reflect this
lastBufferUpdateScrollPosition != scrollController.getScrollingOffset() ||
// There is blinking text to update
hasBlinkingText ||
// We simply have a hint that we should update everything
needFullRedraw;
// Detect resize
if(width != lastComponentWidth || height != lastComponentHeight) {
int columns = width / getFontWidth();
int rows = height / getFontHeight();
TerminalSize terminalSize = virtualTerminal.getTerminalSize().withColumns(columns).withRows(rows);
virtualTerminal.setTerminalSize(terminalSize);
// Back buffer needs to be updated since the component size has changed
needToUpdateBackBuffer = true;
}
if(needToUpdateBackBuffer) {
updateBackBuffer(scrollController.getScrollingOffset());
}
ensureGraphicBufferHasRightSize();
Rectangle clipBounds = componentGraphics.getClipBounds();
if(clipBounds == null) {
clipBounds = new Rectangle(0, 0, getWidth(), getHeight());
}
componentGraphics.drawImage(
backbuffer,
// Destination coordinates
clipBounds.x,
clipBounds.y,
clipBounds.width,
clipBounds.height,
// Source coordinates
clipBounds.x,
clipBounds.y,
clipBounds.width,
clipBounds.height,
null);
// Take care of the left-over area at the bottom and right of the component where no character can fit
//int leftoverHeight = getHeight() % getFontHeight();
int leftoverWidth = getWidth() % getFontWidth();
componentGraphics.setColor(Color.BLACK);
if(leftoverWidth > 0) {
componentGraphics.fillRect(getWidth() - leftoverWidth, 0, leftoverWidth, getHeight());
}
//0, 0, getWidth(), getHeight(), 0, 0, getWidth(), getHeight(), null);
this.lastComponentWidth = width;
this.lastComponentHeight = height;
componentGraphics.dispose();
notifyAll();
}
private synchronized void updateBackBuffer(final int scrollOffsetFromTopInPixels) {
//long startTime = System.currentTimeMillis();
final int fontWidth = getFontWidth();
final int fontHeight = getFontHeight();
//Retrieve the position of the cursor, relative to the scrolling state
final TerminalPosition cursorPosition = virtualTerminal.getCursorBufferPosition();
final TerminalSize viewportSize = virtualTerminal.getTerminalSize();
final int firstVisibleRowIndex = scrollOffsetFromTopInPixels / fontHeight;
final int lastVisibleRowIndex = (scrollOffsetFromTopInPixels + getHeight()) / fontHeight;
//Setup the graphics object
ensureGraphicBufferHasRightSize();
final Graphics2D backbufferGraphics = backbuffer.createGraphics();
if(isTextAntiAliased()) {
backbufferGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
backbufferGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
}
final AtomicBoolean foundBlinkingCharacters = new AtomicBoolean(deviceConfiguration.isCursorBlinking());
buildDirtyCellsLookupTable(firstVisibleRowIndex, lastVisibleRowIndex);
// Detect scrolling
if(lastBufferUpdateScrollPosition < scrollOffsetFromTopInPixels) {
int gap = scrollOffsetFromTopInPixels - lastBufferUpdateScrollPosition;
if(gap / fontHeight < viewportSize.getRows()) {
Graphics2D graphics = copybuffer.createGraphics();
graphics.setClip(0, 0, getWidth(), getHeight() - gap);
graphics.drawImage(backbuffer, 0, -gap, null);
graphics.dispose();
backbufferGraphics.drawImage(copybuffer, 0, 0, getWidth(), getHeight(), 0, 0, getWidth(), getHeight(), null);
if(!dirtyCellsLookupTable.isAllDirty()) {
//Mark bottom rows as dirty so they are repainted
int previousLastVisibleRowIndex = (lastBufferUpdateScrollPosition + getHeight()) / fontHeight;
for(int row = previousLastVisibleRowIndex; row <= lastVisibleRowIndex; row++) {
dirtyCellsLookupTable.setRowDirty(row);
}
}
}
else {
dirtyCellsLookupTable.setAllDirty();
}
}
else if(lastBufferUpdateScrollPosition > scrollOffsetFromTopInPixels) {
int gap = lastBufferUpdateScrollPosition - scrollOffsetFromTopInPixels;
if(gap / fontHeight < viewportSize.getRows()) {
Graphics2D graphics = copybuffer.createGraphics();
graphics.setClip(0, 0, getWidth(), getHeight() - gap);
graphics.drawImage(backbuffer, 0, 0, null);
graphics.dispose();
backbufferGraphics.drawImage(copybuffer, 0, gap, getWidth(), getHeight(), 0, 0, getWidth(), getHeight() - gap, null);
if(!dirtyCellsLookupTable.isAllDirty()) {
//Mark top rows as dirty so they are repainted
int previousFirstVisibleRowIndex = lastBufferUpdateScrollPosition / fontHeight;
for(int row = firstVisibleRowIndex; row <= previousFirstVisibleRowIndex; row++) {
dirtyCellsLookupTable.setRowDirty(row);
}
}
}
else {
dirtyCellsLookupTable.setAllDirty();
}
}
// Detect component resize
if(lastComponentWidth < getWidth()) {
if(!dirtyCellsLookupTable.isAllDirty()) {
//Mark right columns as dirty so they are repainted
int lastVisibleColumnIndex = getWidth() / fontWidth;
int previousLastVisibleColumnIndex = lastComponentWidth / fontWidth;
for(int column = previousLastVisibleColumnIndex; column <= lastVisibleColumnIndex; column++) {
dirtyCellsLookupTable.setColumnDirty(column);
}
}
}
if(lastComponentHeight < getHeight()) {
if(!dirtyCellsLookupTable.isAllDirty()) {
//Mark bottom rows as dirty so they are repainted
int previousLastVisibleRowIndex = (scrollOffsetFromTopInPixels + lastComponentHeight) / fontHeight;
for(int row = previousLastVisibleRowIndex; row <= lastVisibleRowIndex; row++) {
dirtyCellsLookupTable.setRowDirty(row);
}
}
}
virtualTerminal.forEachLine(firstVisibleRowIndex, lastVisibleRowIndex, new VirtualTerminal.BufferWalker() {
@Override
public void onLine(int rowNumber, VirtualTerminal.BufferLine bufferLine) {
for(int column = 0; column < viewportSize.getColumns(); column++) {
TextCharacter textCharacter = bufferLine.getCharacterAt(column);
boolean atCursorLocation = cursorPosition.equals(column, rowNumber);
//If next position is the cursor location and this is a CJK character (i.e. cursor is on the padding),
//consider this location the cursor position since otherwise the cursor will be skipped
if(!atCursorLocation &&
cursorPosition.getColumn() == column + 1 &&
cursorPosition.getRow() == rowNumber &&
TerminalTextUtils.isCharCJK(textCharacter.getCharacter())) {
atCursorLocation = true;
}
boolean isBlinking = textCharacter.getModifiers().contains(SGR.BLINK);
if(isBlinking) {
foundBlinkingCharacters.set(true);
}
if(dirtyCellsLookupTable.isAllDirty() || dirtyCellsLookupTable.isDirty(rowNumber, column) || isBlinking) {
int characterWidth = fontWidth * (TerminalTextUtils.isCharCJK(textCharacter.getCharacter()) ? 2 : 1);
Color foregroundColor = deriveTrueForegroundColor(textCharacter, atCursorLocation);
Color backgroundColor = deriveTrueBackgroundColor(textCharacter, atCursorLocation);
boolean drawCursor = atCursorLocation &&
(!deviceConfiguration.isCursorBlinking() || //Always draw if the cursor isn't blinking
(deviceConfiguration.isCursorBlinking() && blinkOn)); //If the cursor is blinking, only draw when blinkOn is true
// Visualize bell as all colors inverted
if(bellOn) {
Color temp = foregroundColor;
foregroundColor = backgroundColor;
backgroundColor = temp;
}
drawCharacter(backbufferGraphics,
textCharacter,
column,
rowNumber,
foregroundColor,
backgroundColor,
fontWidth,
fontHeight,
characterWidth,
scrollOffsetFromTopInPixels,
drawCursor);
}
if(TerminalTextUtils.isCharCJK(textCharacter.getCharacter())) {
column++; //Skip the trailing space after a CJK character
}
}
}
});
backbufferGraphics.dispose();
// Update the blink status according to if there were any blinking characters or not
this.hasBlinkingText = foundBlinkingCharacters.get();
this.lastDrawnCursorPosition = cursorPosition;
this.lastBufferUpdateScrollPosition = scrollOffsetFromTopInPixels;
this.needFullRedraw = false;
//System.out.println("Updated backbuffer in " + (System.currentTimeMillis() - startTime) + " ms");
}
private void buildDirtyCellsLookupTable(int firstRowOffset, int lastRowOffset) {
if(virtualTerminal.isWholeBufferDirtyThenReset() || needFullRedraw) {
dirtyCellsLookupTable.setAllDirty();
return;
}
TerminalSize viewportSize = virtualTerminal.getTerminalSize();
TerminalPosition cursorPosition = virtualTerminal.getCursorBufferPosition();
dirtyCellsLookupTable.resetAndInitialize(firstRowOffset, lastRowOffset, viewportSize.getColumns());
dirtyCellsLookupTable.setDirty(cursorPosition);
if(lastDrawnCursorPosition != null && !lastDrawnCursorPosition.equals(cursorPosition)) {
if(virtualTerminal.getCharacter(lastDrawnCursorPosition).isDoubleWidth()) {
dirtyCellsLookupTable.setDirty(lastDrawnCursorPosition.withRelativeColumn(1));
}
if(lastDrawnCursorPosition.getColumn() > 0 && virtualTerminal.getCharacter(lastDrawnCursorPosition.withRelativeColumn(-1)).isDoubleWidth()) {
dirtyCellsLookupTable.setDirty(lastDrawnCursorPosition.withRelativeColumn(-1));
}
dirtyCellsLookupTable.setDirty(lastDrawnCursorPosition);
}
TreeSet<TerminalPosition> dirtyCells = virtualTerminal.getAndResetDirtyCells();
for(TerminalPosition position: dirtyCells) {
dirtyCellsLookupTable.setDirty(position);
}
}
private void ensureGraphicBufferHasRightSize() {
if(backbuffer == null) {
backbuffer = new BufferedImage(getWidth() * 2, getHeight() * 2, BufferedImage.TYPE_INT_RGB);
copybuffer = new BufferedImage(getWidth() * 2, getHeight() * 2, BufferedImage.TYPE_INT_RGB);
// We only need to set the content of the backbuffer during initialization time
Graphics2D graphics = backbuffer.createGraphics();
graphics.setColor(colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false));
graphics.fillRect(0, 0, getWidth() * 2, getHeight() * 2);
graphics.dispose();
}
if(backbuffer.getWidth() < getWidth() || backbuffer.getWidth() > getWidth() * 4 ||
backbuffer.getHeight() < getHeight() || backbuffer.getHeight() > getHeight() * 4) {
BufferedImage newBackbuffer = new BufferedImage(Math.max(getWidth(), 1) * 2, Math.max(getHeight(), 1) * 2, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = newBackbuffer.createGraphics();
graphics.fillRect(0, 0, newBackbuffer.getWidth(), newBackbuffer.getHeight());
graphics.drawImage(backbuffer, 0, 0, null);
graphics.dispose();
backbuffer = newBackbuffer;
// Re-initialize the copy buffer, but we don't need to set any content
copybuffer = new BufferedImage(Math.max(getWidth(), 1) * 2, Math.max(getHeight(), 1) * 2, BufferedImage.TYPE_INT_RGB);
}
}
private void drawCharacter(
Graphics g,
TextCharacter character,
int columnIndex,
int rowIndex,
Color foregroundColor,
Color backgroundColor,
int fontWidth,
int fontHeight,
int characterWidth,
int scrollingOffsetInPixels,
boolean drawCursor) {
int x = columnIndex * fontWidth;
int y = rowIndex * fontHeight - scrollingOffsetInPixels;
g.setColor(backgroundColor);
g.setClip(x, y, characterWidth, fontHeight);
g.fillRect(x, y, characterWidth, fontHeight);
g.setColor(foregroundColor);
Font font = getFontForCharacter(character);
g.setFont(font);
FontMetrics fontMetrics = g.getFontMetrics();
g.drawString(Character.toString(character.getCharacter()), x, y + fontHeight - fontMetrics.getDescent() + 1);
if(character.isCrossedOut()) {
//noinspection UnnecessaryLocalVariable
int lineStartX = x;
int lineStartY = y + (fontHeight / 2);
int lineEndX = lineStartX + characterWidth;
g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
}
if(character.isUnderlined()) {
//noinspection UnnecessaryLocalVariable
int lineStartX = x;
int lineStartY = y + fontHeight - fontMetrics.getDescent() + 1;
int lineEndX = lineStartX + characterWidth;
g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
}
if(drawCursor) {
if(deviceConfiguration.getCursorColor() == null) {
g.setColor(foregroundColor);
}
else {
g.setColor(colorConfiguration.toAWTColor(deviceConfiguration.getCursorColor(), false, false));
}
if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.UNDER_BAR) {
g.fillRect(x, y + fontHeight - 3, characterWidth, 2);
}
else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.VERTICAL_BAR) {
g.fillRect(x, y + 1, 2, fontHeight - 2);
}
}
}
private Color deriveTrueForegroundColor(TextCharacter character, boolean atCursorLocation) {
TextColor foregroundColor = character.getForegroundColor();
TextColor backgroundColor = character.getBackgroundColor();
boolean reverse = character.isReversed();
boolean blink = character.isBlinking();
if(cursorIsVisible && atCursorLocation) {
if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
(!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
reverse = true;
}
}
if(reverse && (!blink || !blinkOn)) {
return colorConfiguration.toAWTColor(backgroundColor, backgroundColor != TextColor.ANSI.DEFAULT, character.isBold());
}
else if(!reverse && blink && blinkOn) {
return colorConfiguration.toAWTColor(backgroundColor, false, character.isBold());
}
else {
return colorConfiguration.toAWTColor(foregroundColor, true, character.isBold());
}
}
private Color deriveTrueBackgroundColor(TextCharacter character, boolean atCursorLocation) {
TextColor foregroundColor = character.getForegroundColor();
TextColor backgroundColor = character.getBackgroundColor();
boolean reverse = character.isReversed();
if(cursorIsVisible && atCursorLocation) {
if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
(!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
reverse = true;
}
else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.FIXED_BACKGROUND) {
backgroundColor = deviceConfiguration.getCursorColor();
}
}
if(reverse) {
return colorConfiguration.toAWTColor(foregroundColor, backgroundColor == TextColor.ANSI.DEFAULT, character.isBold());
}
else {
return colorConfiguration.toAWTColor(backgroundColor, false, false);
}
}
///////////
// Then delegate all Terminal interface methods to the virtual terminal implementation
//
// Some of these methods we need to pass to the AWT-thread, which makes the call asynchronous. Hopefully this isn't
// causing too much problem...
///////////
@Override
public KeyStroke pollInput() {
if(!enableInput) {
return new KeyStroke(KeyType.EOF);
}
return keyQueue.poll();
}
@Override
public KeyStroke readInput() {
// Synchronize on keyQueue here so only one thread is inside keyQueue.take()
synchronized(keyQueue) {
if(!enableInput) {
return new KeyStroke(KeyType.EOF);
}
try {
return keyQueue.take();
}
catch(InterruptedException ignore) {
throw new RuntimeException("Blocking input was interrupted");
}
}
}
@Override
public synchronized void enterPrivateMode() {
virtualTerminal.enterPrivateMode();
clearBackBuffer();
flush();
}
@Override
public synchronized void exitPrivateMode() {
virtualTerminal.exitPrivateMode();
clearBackBuffer();
flush();
}
@Override
public synchronized void clearScreen() {
virtualTerminal.clearScreen();
clearBackBuffer();
}
/**
* Clears out the back buffer and the resets the visual state so next paint operation will do a full repaint of
* everything
*/
private void clearBackBuffer() {
// Manually clear the backbuffer
if(backbuffer != null) {
Graphics2D graphics = backbuffer.createGraphics();
Color backgroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false);
graphics.setColor(backgroundColor);
graphics.fillRect(0, 0, getWidth(), getHeight());
graphics.dispose();
}
}
@Override
public synchronized void setCursorPosition(int x, int y) {
setCursorPosition(new TerminalPosition(x, y));
}
@Override
public synchronized void setCursorPosition(TerminalPosition position) {
if(position.getColumn() < 0) {
position = position.withColumn(0);
}
if(position.getRow() < 0) {
position = position.withRow(0);
}
virtualTerminal.setCursorPosition(position);
}
@Override
public TerminalPosition getCursorPosition() {
return virtualTerminal.getCursorPosition();
}
@Override
public void setCursorVisible(final boolean visible) {
cursorIsVisible = visible;
}
@Override
public synchronized void putCharacter(final char c) {
virtualTerminal.putCharacter(c);
}
@Override
public TextGraphics newTextGraphics() {
return virtualTerminal.newTextGraphics();
}
@Override
public void enableSGR(final SGR sgr) {
virtualTerminal.enableSGR(sgr);
}
@Override
public void disableSGR(final SGR sgr) {
virtualTerminal.disableSGR(sgr);
}
@Override
public void resetColorAndSGR() {
virtualTerminal.resetColorAndSGR();
}
@Override
public void setForegroundColor(final TextColor color) {
virtualTerminal.setForegroundColor(color);
}
@Override
public void setBackgroundColor(final TextColor color) {
virtualTerminal.setBackgroundColor(color);
}
@Override
public synchronized TerminalSize getTerminalSize() {
return virtualTerminal.getTerminalSize();
}
@Override
public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
return enquiryString.getBytes();
}
@Override
public void bell() {
if(bellOn) {
return;
}
// Flash the screen...
bellOn = true;
needFullRedraw = true;
updateBackBuffer(scrollController.getScrollingOffset());
repaint();
// Unify this with the blink timer and just do the whole timer logic ourselves?
new Thread("BellSilencer") {
@Override
public void run() {
try {
Thread.sleep(100);
}
catch(InterruptedException ignore) {}
bellOn = false;
needFullRedraw = true;
updateBackBuffer(scrollController.getScrollingOffset());
repaint();
}
}.start();
// ...and make a sound
Toolkit.getDefaultToolkit().beep();
}
@Override
public synchronized void flush() {
updateBackBuffer(scrollController.getScrollingOffset());
repaint();
}
@Override
public void close() {
// No action
}
@Override
public void addResizeListener(TerminalResizeListener listener) {
virtualTerminal.addResizeListener(listener);
}
@Override
public void removeResizeListener(TerminalResizeListener listener) {
virtualTerminal.removeResizeListener(listener);
}
///////////
// Remaining are private internal classes used by SwingTerminal
///////////
private static final Set<Character> TYPED_KEYS_TO_IGNORE = new HashSet<Character>(Arrays.asList('\n', '\t', '\r', '\b', '\33', (char)127));
/**
* Class that translates AWT key events into Lanterna {@link KeyStroke}
*/
protected class TerminalInputListener extends KeyAdapter {
@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;
boolean shiftDown = (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0;
if(!TYPED_KEYS_TO_IGNORE.contains(character)) {
//We need to re-adjust alphabet characters if ctrl was pressed, just like for the AnsiTerminal
if(ctrlDown && character > 0 && character < 0x1a) {
character = (char) ('a' - 1 + character);
if(shiftDown) {
character = Character.toUpperCase(character);
}
}
// Check if clipboard is avavilable and this was a paste (ctrl + shift + v) before
// adding the key to the input queue
if(!altDown && ctrlDown && shiftDown && character == 'V' && deviceConfiguration.isClipboardAvailable()) {
pasteClipboardContent();
}
else {
keyQueue.add(new KeyStroke(character, ctrlDown, altDown, shiftDown));
}
}
}
@Override
public void keyPressed(KeyEvent e) {
boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
boolean shiftDown = (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0;
if(e.getKeyCode() == KeyEvent.VK_ENTER) {
keyQueue.add(new KeyStroke(KeyType.Enter, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
keyQueue.add(new KeyStroke(KeyType.Escape, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
keyQueue.add(new KeyStroke(KeyType.Backspace, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_LEFT) {
keyQueue.add(new KeyStroke(KeyType.ArrowLeft, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_RIGHT) {
keyQueue.add(new KeyStroke(KeyType.ArrowRight, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_UP) {
keyQueue.add(new KeyStroke(KeyType.ArrowUp, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
keyQueue.add(new KeyStroke(KeyType.ArrowDown, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_INSERT) {
// This could be a paste (shift+insert) if the clipboard is available
if(!altDown && !ctrlDown && shiftDown && deviceConfiguration.isClipboardAvailable()) {
pasteClipboardContent();
}
else {
keyQueue.add(new KeyStroke(KeyType.Insert, ctrlDown, altDown, shiftDown));
}
}
else if(e.getKeyCode() == KeyEvent.VK_DELETE) {
keyQueue.add(new KeyStroke(KeyType.Delete, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_HOME) {
keyQueue.add(new KeyStroke(KeyType.Home, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_END) {
keyQueue.add(new KeyStroke(KeyType.End, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
keyQueue.add(new KeyStroke(KeyType.PageUp, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
keyQueue.add(new KeyStroke(KeyType.PageDown, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F1) {
keyQueue.add(new KeyStroke(KeyType.F1, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F2) {
keyQueue.add(new KeyStroke(KeyType.F2, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F3) {
keyQueue.add(new KeyStroke(KeyType.F3, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F4) {
keyQueue.add(new KeyStroke(KeyType.F4, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F5) {
keyQueue.add(new KeyStroke(KeyType.F5, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F6) {
keyQueue.add(new KeyStroke(KeyType.F6, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F7) {
keyQueue.add(new KeyStroke(KeyType.F7, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F8) {
keyQueue.add(new KeyStroke(KeyType.F8, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F9) {
keyQueue.add(new KeyStroke(KeyType.F9, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F10) {
keyQueue.add(new KeyStroke(KeyType.F10, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F11) {
keyQueue.add(new KeyStroke(KeyType.F11, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F12) {
keyQueue.add(new KeyStroke(KeyType.F12, ctrlDown, altDown, shiftDown));
}
else if(e.getKeyCode() == KeyEvent.VK_TAB) {
if(e.isShiftDown()) {
keyQueue.add(new KeyStroke(KeyType.ReverseTab, ctrlDown, altDown, shiftDown));
}
else {
keyQueue.add(new KeyStroke(KeyType.Tab, ctrlDown, altDown, shiftDown));
}
}
else {
//keyTyped doesn't catch this scenario (for whatever reason...) so we have to do it here
if(altDown && ctrlDown && e.getKeyCode() >= 'A' && e.getKeyCode() <= 'Z') {
char character = (char) e.getKeyCode();
if(!shiftDown) {
character = Character.toLowerCase(character);
}
keyQueue.add(new KeyStroke(character, ctrlDown, altDown, shiftDown));
}
}
}
}
// This is mostly unimplemented, we could hook more of this into ExtendedTerminal's mouse functions
protected class TerminalMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if(MouseInfo.getNumberOfButtons() > 2 &&
e.getButton() == MouseEvent.BUTTON2 &&
deviceConfiguration.isClipboardAvailable()) {
pasteSelectionContent();
}
}
}
private void pasteClipboardContent() {
try {
Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
if(systemClipboard != null) {
injectStringAsKeyStrokes((String) systemClipboard.getData(DataFlavor.stringFlavor));
}
}
catch(Exception ignore) {
}
}
private void pasteSelectionContent() {
try {
Clipboard systemSelection = Toolkit.getDefaultToolkit().getSystemSelection();
if(systemSelection != null) {
injectStringAsKeyStrokes((String) systemSelection.getData(DataFlavor.stringFlavor));
}
}
catch(Exception ignore) {
}
}
private void injectStringAsKeyStrokes(String string) {
StringReader stringReader = new StringReader(string);
InputDecoder inputDecoder = new InputDecoder(stringReader);
inputDecoder.addProfile(new DefaultKeyDecodingProfile());
try {
KeyStroke keyStroke = inputDecoder.getNextCharacter(false);
while (keyStroke != null && keyStroke.getKeyType() != KeyType.EOF) {
keyQueue.add(keyStroke);
keyStroke = inputDecoder.getNextCharacter(false);
}
}
catch(IOException ignore) {
}
}
private static class DirtyCellsLookupTable {
private final List<BitSet> table;
private int firstRowIndex;
private boolean allDirty;
DirtyCellsLookupTable() {
table = new ArrayList<BitSet>();
firstRowIndex = -1;
allDirty = false;
}
void resetAndInitialize(int firstRowIndex, int lastRowIndex, int columns) {
this.firstRowIndex = firstRowIndex;
this.allDirty = false;
int rows = lastRowIndex - firstRowIndex + 1;
while(table.size() < rows) {
table.add(new BitSet(columns));
}
while(table.size() > rows) {
table.remove(table.size() - 1);
}
for(int index = 0; index < table.size(); index++) {
if(table.get(index).size() != columns) {
table.set(index, new BitSet(columns));
}
else {
table.get(index).clear();
}
}
}
void setAllDirty() {
allDirty = true;
}
boolean isAllDirty() {
return allDirty;
}
void setDirty(TerminalPosition position) {
if(position.getRow() < firstRowIndex ||
position.getRow() >= firstRowIndex + table.size()) {
return;
}
BitSet tableRow = table.get(position.getRow() - firstRowIndex);
if(position.getColumn() < tableRow.size()) {
tableRow.set(position.getColumn());
}
}
void setRowDirty(int rowNumber) {
BitSet row = table.get(rowNumber - firstRowIndex);
row.set(0, row.size());
}
void setColumnDirty(int column) {
for(BitSet row: table) {
if(column < row.size()) {
row.set(column);
}
}
}
boolean isDirty(int row, int column) {
if(row < firstRowIndex || row >= firstRowIndex + table.size()) {
return false;
}
BitSet tableRow = table.get(row - firstRowIndex);
if(column < tableRow.size()) {
return tableRow.get(column);
}
else {
return false;
}
}
}
}