/* * 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 + '}'; } } }