/*
* 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 .
*
* Copyright (C) 2010-2015 Martin
*/
package com.googlecode.lanterna.terminal.swing;
import com.googlecode.lanterna.*;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.terminal.IOSafeTerminal;
import com.googlecode.lanterna.terminal.ResizeListener;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 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
*/
@SuppressWarnings("serial")
abstract class GraphicalTerminalImplementation implements IOSafeTerminal {
private final TerminalEmulatorDeviceConfiguration deviceConfiguration;
private final TerminalEmulatorColorConfiguration colorConfiguration;
private final VirtualTerminal virtualTerminal;
private final BlockingQueue keyQueue;
private final List resizeListeners;
private final String enquiryString;
private final EnumSet activeSGRs;
private TextColor foregroundColor;
private TextColor backgroundColor;
private volatile boolean cursorIsVisible;
private volatile Timer blinkTimer;
private volatile boolean hasBlinkingText;
private volatile boolean blinkOn;
private volatile boolean flushed;
// We use two different data structures to optimize drawing
// * A map (as a two-dimensional array) of all characters currently visible inside this component
// * 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. Note that
// when the component is resized, we always update the whole buffer.
//
// DON'T RELY ON THESE FOR SIZE! We make it a big bigger than necessary to make resizing smoother. Use the AWT/Swing
// methods to get the correct dimensions or use {@code getTerminalSize()} to get the size in terminal space.
private CharacterState[][] visualState;
private BufferedImage backbuffer;
/**
* 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
*/
public 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 VirtualTerminal(
deviceConfiguration.getLineBufferScrollbackSize(),
initialTerminalSize,
scrollController);
this.keyQueue = new LinkedBlockingQueue();
this.resizeListeners = new CopyOnWriteArrayList();
this.deviceConfiguration = deviceConfiguration;
this.colorConfiguration = colorConfiguration;
this.activeSGRs = EnumSet.noneOf(SGR.class);
this.foregroundColor = TextColor.ANSI.DEFAULT;
this.backgroundColor = TextColor.ANSI.DEFAULT;
this.cursorIsVisible = true; //Always start with an activate and visible cursor
this.enquiryString = "TerminalEmulator";
this.visualState = new CharacterState[48][160];
this.backbuffer = null; // We don't know the dimensions yet
this.blinkTimer = null;
this.hasBlinkingText = false; // Assume initial content doesn't have any blinking text
this.blinkOn = true;
this.flushed = false;
//Set the initial scrollable size
//scrollObserver.newScrollableLength(fontConfiguration.getFontHeight() * terminalSize.getRows());
}
///////////
// 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
*/
protected abstract int getFontHeight();
/**
* Used to find out the font width, in pixels
* @return Terminal font width in pixels
*/
protected abstract int getFontWidth();
/**
* Used when requiring the total height of the terminal component, in pixels
* @return Height of the terminal component, in pixels
*/
protected abstract int getHeight();
/**
* Used when requiring the total width of the terminal component, in pixels
* @return Width of the terminal component, in pixels
*/
protected 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
*/
protected 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
*/
protected abstract boolean isTextAntiAliased();
/**
* Called by the {@code GraphicalTerminalImplementation} when it would like the OS to schedule a repaint of the
* window
*/
protected abstract void repaint();
/**
* Start the timer that triggers blinking
*/
protected 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
*/
protected synchronized void stopBlinkTimer() {
if(blinkTimer == null) {
// Already off!
return;
}
blinkTimer.cancel();
blinkTimer = null;
}
///////////
// First 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.getSize().getColumns(),
getFontHeight() * virtualTerminal.getSize().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
*/
protected synchronized void paintComponent(Graphics componentGraphics) {
//First, resize the buffer width/height if necessary
int fontWidth = getFontWidth();
int fontHeight = getFontHeight();
//boolean antiAliasing = fontConfiguration.isAntiAliased();
int widthInNumberOfCharacters = getWidth() / fontWidth;
int visibleRows = getHeight() / fontHeight;
boolean terminalResized = false;
//Don't let size be less than 1
widthInNumberOfCharacters = Math.max(1, widthInNumberOfCharacters);
visibleRows = Math.max(1, visibleRows);
//scrollObserver.updateModel(currentBuffer.getNumberOfLines(), visibleRows);
TerminalSize terminalSize = virtualTerminal.getSize().withColumns(widthInNumberOfCharacters).withRows(visibleRows);
if(!terminalSize.equals(virtualTerminal.getSize())) {
virtualTerminal.resize(terminalSize);
for(ResizeListener listener: resizeListeners) {
listener.onResized(this, terminalSize);
}
terminalResized = true;
ensureVisualStateHasRightSize(terminalSize);
}
ensureBackbufferHasRightSize();
// At this point, if the user hasn't asked for an explicit flush, just paint the backbuffer. It's prone to
// problems if the user isn't flushing properly but it reduces flickering when resizing the window and the code
// is asynchronously responding to the resize
if(flushed) {
updateBackBuffer(fontWidth, fontHeight, terminalResized, terminalSize);
flushed = false;
}
componentGraphics.drawImage(backbuffer, 0, 0, getWidth(), getHeight(), 0, 0, getWidth(), getHeight(), null);
// Dispose the graphic objects
componentGraphics.dispose();
// Tell anyone waiting on us that drawing is complete
notifyAll();
}
private void updateBackBuffer(int fontWidth, int fontHeight, boolean terminalResized, TerminalSize terminalSize) {
//Retrieve the position of the cursor, relative to the scrolling state
TerminalPosition translatedCursorPosition = virtualTerminal.getTranslatedCursorPosition();
//Setup the graphics object
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);
}
// Draw line by line, character by character
// Initiate the blink state to whatever the cursor is using, since if the cursor is blinking then we always want
// to do the blink repaint
boolean foundBlinkingCharacters = deviceConfiguration.isCursorBlinking();
int rowIndex = 0;
for(List row: virtualTerminal.getLines()) {
for(int columnIndex = 0; columnIndex < row.size(); columnIndex++) {
//Any extra characters from the virtual terminal that doesn't fit can be discarded
if(columnIndex >= terminalSize.getColumns()) {
continue;
}
TextCharacter character = row.get(columnIndex);
boolean atCursorLocation = translatedCursorPosition.equals(columnIndex, rowIndex);
//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 &&
translatedCursorPosition.getColumn() == columnIndex + 1 &&
translatedCursorPosition.getRow() == rowIndex &&
TerminalTextUtils.isCharCJK(character.getCharacter())) {
atCursorLocation = true;
}
int characterWidth = fontWidth * (TerminalTextUtils.isCharCJK(character.getCharacter()) ? 2 : 1);
Color foregroundColor = deriveTrueForegroundColor(character, atCursorLocation);
Color backgroundColor = deriveTrueBackgroundColor(character, 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
CharacterState characterState = new CharacterState(character, foregroundColor, backgroundColor, drawCursor);
if(!characterState.equals(visualState[rowIndex][columnIndex]) || terminalResized) {
drawCharacter(backbufferGraphics,
character,
columnIndex,
rowIndex,
foregroundColor,
backgroundColor,
fontWidth,
fontHeight,
characterWidth,
drawCursor);
visualState[rowIndex][columnIndex] = characterState;
if(TerminalTextUtils.isCharCJK(character.getCharacter())) {
visualState[rowIndex][columnIndex+1] = characterState;
}
}
if(character.getModifiers().contains(SGR.BLINK)) {
foundBlinkingCharacters = true;
}
if(TerminalTextUtils.isCharCJK(character.getCharacter())) {
columnIndex++; //Skip the trailing space after a CJK character
}
}
rowIndex++;
}
// Take care of the left-over area at the bottom and right of the component where no character can fit
int leftoverHeight = getHeight() % fontHeight;
int leftoverWidth = getWidth() % fontWidth;
backbufferGraphics.setColor(Color.BLACK);
if(leftoverWidth > 0) {
backbufferGraphics.fillRect(getWidth() - leftoverWidth, 0, leftoverWidth, getHeight());
}
if(leftoverHeight > 0) {
backbufferGraphics.fillRect(0, getHeight() - leftoverHeight, getWidth(), leftoverHeight);
}
backbufferGraphics.dispose();
// Update the blink status according to if there were any blinking characters or not
this.hasBlinkingText = foundBlinkingCharacters;
}
private void ensureBackbufferHasRightSize() {
if(backbuffer == null) {
backbuffer = new BufferedImage(getWidth() * 2, getHeight() * 2, BufferedImage.TYPE_INT_RGB);
}
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.drawImage(backbuffer, 0, 0, null);
graphics.dispose();
backbuffer = newBackbuffer;
}
}
private void ensureVisualStateHasRightSize(TerminalSize terminalSize) {
if(visualState == null) {
visualState = new CharacterState[terminalSize.getRows() * 2][terminalSize.getColumns() * 2];
}
if(visualState.length < terminalSize.getRows() || visualState.length > Math.max(terminalSize.getRows(), 1) * 4) {
visualState = Arrays.copyOf(visualState, terminalSize.getRows() * 2);
}
for(int rowIndex = 0; rowIndex < visualState.length; rowIndex++) {
CharacterState[] row = visualState[rowIndex];
if(row == null) {
row = new CharacterState[terminalSize.getColumns() * 2];
visualState[rowIndex] = row;
}
if(row.length < terminalSize.getColumns() || row.length > Math.max(terminalSize.getColumns(), 1) * 4) {
row = Arrays.copyOf(row, terminalSize.getColumns() * 2);
visualState[rowIndex] = row;
}
// Make sure all items outside the 'real' terminal size are null
if(rowIndex < terminalSize.getRows()) {
Arrays.fill(row, terminalSize.getColumns(), row.length, null);
}
else {
Arrays.fill(row, null);
}
}
}
private void drawCharacter(
Graphics g,
TextCharacter character,
int columnIndex,
int rowIndex,
Color foregroundColor,
Color backgroundColor,
int fontWidth,
int fontHeight,
int characterWidth,
boolean drawCursor) {
int x = columnIndex * fontWidth;
int y = rowIndex * fontHeight;
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, ((rowIndex + 1) * fontHeight) - fontMetrics.getDescent());
if(character.isCrossedOut()) {
int lineStartX = x;
int lineStartY = y + (fontHeight / 2);
int lineEndX = lineStartX + characterWidth;
g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
}
if(character.isUnderlined()) {
int lineStartX = x;
int lineStartY = ((rowIndex + 1) * 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() {
return keyQueue.poll();
}
@Override
public KeyStroke readInput() throws IOException {
try {
return keyQueue.take();
}
catch(InterruptedException ignore) {
throw new IOException("Blocking input was interrupted");
}
}
@Override
public synchronized void enterPrivateMode() {
virtualTerminal.switchToPrivateMode();
clearBackBufferAndVisualState();
flush();
}
@Override
public synchronized void exitPrivateMode() {
virtualTerminal.switchToNormalMode();
clearBackBufferAndVisualState();
flush();
}
@Override
public synchronized void clearScreen() {
virtualTerminal.clear();
clearBackBufferAndVisualState();
flush();
}
/**
* Clears out the back buffer and the resets the visual state so next paint operation will do a full repaint of
* everything
*/
protected void clearBackBufferAndVisualState() {
// Manually clear the backbuffer and visual state
if(backbuffer != null) {
Graphics2D graphics = backbuffer.createGraphics();
Color foregroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, true, false);
Color backgroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false);
graphics.setColor(backgroundColor);
graphics.fillRect(0, 0, getWidth(), getHeight());
graphics.dispose();
for(CharacterState[] line : visualState) {
Arrays.fill(line, new CharacterState(new TextCharacter(' '), foregroundColor, backgroundColor, false));
}
}
}
@Override
public synchronized void setCursorPosition(final int x, final int y) {
virtualTerminal.setCursorPosition(new TerminalPosition(x, y));
}
@Override
public void setCursorVisible(final boolean visible) {
cursorIsVisible = visible;
}
@Override
public synchronized void putCharacter(final char c) {
virtualTerminal.putCharacter(new TextCharacter(c, foregroundColor, backgroundColor, activeSGRs));
}
@Override
public TextGraphics newTextGraphics() throws IOException {
return new VirtualTerminalTextGraphics(virtualTerminal);
}
@Override
public void enableSGR(final SGR sgr) {
activeSGRs.add(sgr);
}
@Override
public void disableSGR(final SGR sgr) {
activeSGRs.remove(sgr);
}
@Override
public void resetColorAndSGR() {
foregroundColor = TextColor.ANSI.DEFAULT;
backgroundColor = TextColor.ANSI.DEFAULT;
activeSGRs.clear();
}
@Override
public void setForegroundColor(final TextColor color) {
foregroundColor = color;
}
@Override
public void setBackgroundColor(final TextColor color) {
backgroundColor = color;
}
@Override
public synchronized TerminalSize getTerminalSize() {
return virtualTerminal.getSize();
}
@Override
public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
return enquiryString.getBytes();
}
@Override
public void flush() {
flushed = true;
repaint();
}
@Override
public void addResizeListener(ResizeListener listener) {
resizeListeners.add(listener);
}
@Override
public void removeResizeListener(ResizeListener listener) {
resizeListeners.remove(listener);
}
///////////
// Remaining are private internal classes used by SwingTerminal
///////////
private static final Set TYPED_KEYS_TO_IGNORE = new HashSet(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;
if(!TYPED_KEYS_TO_IGNORE.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 KeyStroke(character, ctrlDown, altDown));
}
}
@Override
public void keyPressed(KeyEvent e) {
boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
if(e.getKeyCode() == KeyEvent.VK_ENTER) {
keyQueue.add(new KeyStroke(KeyType.Enter, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
keyQueue.add(new KeyStroke(KeyType.Escape, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
keyQueue.add(new KeyStroke(KeyType.Backspace, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_LEFT) {
keyQueue.add(new KeyStroke(KeyType.ArrowLeft, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_RIGHT) {
keyQueue.add(new KeyStroke(KeyType.ArrowRight, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_UP) {
keyQueue.add(new KeyStroke(KeyType.ArrowUp, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
keyQueue.add(new KeyStroke(KeyType.ArrowDown, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_INSERT) {
keyQueue.add(new KeyStroke(KeyType.Insert, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_DELETE) {
keyQueue.add(new KeyStroke(KeyType.Delete, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_HOME) {
keyQueue.add(new KeyStroke(KeyType.Home, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_END) {
keyQueue.add(new KeyStroke(KeyType.End, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
keyQueue.add(new KeyStroke(KeyType.PageUp, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
keyQueue.add(new KeyStroke(KeyType.PageDown, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F1) {
keyQueue.add(new KeyStroke(KeyType.F1, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F2) {
keyQueue.add(new KeyStroke(KeyType.F2, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F3) {
keyQueue.add(new KeyStroke(KeyType.F3, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F4) {
keyQueue.add(new KeyStroke(KeyType.F4, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F5) {
keyQueue.add(new KeyStroke(KeyType.F5, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F6) {
keyQueue.add(new KeyStroke(KeyType.F6, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F7) {
keyQueue.add(new KeyStroke(KeyType.F7, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F8) {
keyQueue.add(new KeyStroke(KeyType.F8, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F9) {
keyQueue.add(new KeyStroke(KeyType.F9, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F10) {
keyQueue.add(new KeyStroke(KeyType.F10, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F11) {
keyQueue.add(new KeyStroke(KeyType.F11, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_F12) {
keyQueue.add(new KeyStroke(KeyType.F12, ctrlDown, altDown));
}
else if(e.getKeyCode() == KeyEvent.VK_TAB) {
if(e.isShiftDown()) {
keyQueue.add(new KeyStroke(KeyType.ReverseTab, ctrlDown, altDown));
}
else {
keyQueue.add(new KeyStroke(KeyType.Tab, ctrlDown, altDown));
}
}
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 asLowerCase = Character.toLowerCase((char) e.getKeyCode());
keyQueue.add(new KeyStroke(asLowerCase, true, true));
}
}
}
}
private static class CharacterState {
private final TextCharacter textCharacter;
private final Color foregroundColor;
private final Color backgroundColor;
private final boolean drawCursor;
CharacterState(TextCharacter textCharacter, Color foregroundColor, Color backgroundColor, boolean drawCursor) {
this.textCharacter = textCharacter;
this.foregroundColor = foregroundColor;
this.backgroundColor = backgroundColor;
this.drawCursor = drawCursor;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
CharacterState that = (CharacterState) o;
if(drawCursor != that.drawCursor) {
return false;
}
if(!textCharacter.equals(that.textCharacter)) {
return false;
}
if(!foregroundColor.equals(that.foregroundColor)) {
return false;
}
return backgroundColor.equals(that.backgroundColor);
}
@Override
public int hashCode() {
int result = textCharacter.hashCode();
result = 31 * result + foregroundColor.hashCode();
result = 31 * result + backgroundColor.hashCode();
result = 31 * result + (drawCursor ? 1 : 0);
return result;
}
@Override
public String toString() {
return "CharacterState{" +
"textCharacter=" + textCharacter +
", foregroundColor=" + foregroundColor +
", backgroundColor=" + backgroundColor +
", drawCursor=" + drawCursor +
'}';
}
}
}