/*
 * This file is part of lanterna (https://github.com/mabe02/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-2020 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 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 AWTTerminal and 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
/** * 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.
Params:
  • 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
  • deviceConfiguration – Device configuration to use for this SwingTerminal
  • colorConfiguration – Color configuration to use for this SwingTerminal
  • scrollController – Controller to use for scrolling, the object passed in will be notified whenever the scrollable area has changed
/** * 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<>(); 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()); } TerminalEmulatorDeviceConfiguration getDeviceConfiguration() { return deviceConfiguration; } TerminalEmulatorColorConfiguration getColorConfiguration() { return colorConfiguration; } /////////// // First abstract methods that are implemented in AWTTerminalImplementation and SwingTerminalImplementation ///////////
Used to find out the font height, in pixels
Returns:Terminal font height in pixels
/** * 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
Returns:Terminal font width in pixels
/** * 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
Returns:Height of the terminal component, in pixels
/** * 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
Returns:Width of the terminal component, in pixels
/** * 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.
Params:
  • character – Character to get the font for
Returns:Font to be used for this character
/** * 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 true if anti-aliasing is enabled, false otherwise
Returns:true if anti-aliasing is enabled, false otherwise
/** * 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 GraphicalTerminalImplementation when it would like the OS to schedule a repaint of the window
/** * 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
/** * 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
/** * 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
Returns:Preferred size of this terminal
/** * 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
Params:
  • componentGraphics – Object to use when drawing to the component's surface
/** * 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, (rowNumber, 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 double-width 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 && textCharacter.isDoubleWidth()) { 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 * (textCharacter.isDoubleWidth() ? 2 : 1); Color foregroundColor = deriveTrueForegroundColor(textCharacter, atCursorLocation); Color backgroundColor = deriveTrueBackgroundColor(textCharacter, atCursorLocation); //Always draw if the cursor isn't blinking boolean drawCursor = atCursorLocation && (!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(textCharacter.isDoubleWidth()) { 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.getCharacterString(), 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); } } void addInput(KeyStroke keyStroke) { keyQueue.add(keyStroke); } /////////// // 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
/** * 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 void putString(String string) { virtualTerminal.putString(string); } @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<>(Arrays.asList('\n', '\t', '\r', '\b', '\33', (char) 127));
Class that translates AWT key events into Lanterna KeyStroke
/** * 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, false)); } 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, true, true, 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<>(); 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; } } } }