/* * KindleTerminal.java * * Copyright (c) 2010 VDP <vdp DOT kindle AT gmail.com>. * * This file is part of MidpSSH. * * MidpSSH is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * MidpSSH 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with MidpSSH. If not, see <http ://www.gnu.org/licenses/>. */ package kindle; import awt.AwtSession; import com.amazon.kindle.kindlet.event.KindleKeyCodes; import com.amazon.kindle.kindlet.ui.KRepaintManager; import com.amazon.kindle.kindlet.ui.KTextArea; import com.amazon.kindle.kindlet.ui.KindletUIResources; import com.amazon.kindle.kindlet.ui.KindletUIResources.KColorName; import com.amazon.kindle.kindlet.ui.KindletUIResources.KFontFamilyName; import com.amazon.kindle.kindlet.ui.KindletUIResources.KFontStyle; import gui.Redrawable; import java.awt.Color; import java.awt.Component; import java.awt.EventQueue; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Image; import java.awt.Rectangle; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import org.apache.log4j.Logger; import terminal.VT320; /** * The terminal emulation window. * * This is the component that renders the emulation buffer * and accepts the user keyboard. * * @author VDP <vdp DOT kindle AT gmail.com> */ public class KindleTerminal extends KTextArea implements Redrawable, KeyListener { private static final int DIRTY_UNDEFINED = -1; // ------------------------------- Constants private static final int ROT_NORMAL = 0; private static final int ROT_90 = 2; private static final int FONT_SIZE = 20; // If more than UPDATE_THRESHOLD paint operations are needed the // whole screen is refreshed instead. private static final int UPDATE_THRESHOLD = 4; // ------------------------------- Data fields private AwtSession session; private Font font; /** the VDU buffer */ protected VT320 buffer; /** first top and left character in buffer, that is displayed */ protected int top, left; protected int width, height; private int fontWidth, fontHeight, fontAscent; protected int rotated; /** display size in characters */ public int rows, cols; private Image backingStore = null; public Color fgcolor; public Color bgcolor; private final Object paintMutex = new Object(); /** local copy of the lines that were displayed on the previous refresh */ private char[][] prevChars; private int[][] prevAttrs; /** A buffer to hold the characters of the line currently rendered */ private char[] renderChars; /** previous cursor positions */ int prevCursorX, prevCursorY; private boolean invalid = true; private boolean symbolActive = false; int nDirty; Rectangle[] dirtyRects = new Rectangle[UPDATE_THRESHOLD]; int currentDirty = DIRTY_UNDEFINED; private KindletUIResources resources; private Logger log; private Redrawer redrawer; /** * @param buffer */ public KindleTerminal(VT320 buffer, AwtSession session, KindletUIResources rsrc) { this.log = Logger.getLogger(KindleTerminal.class.getName()); this.buffer = buffer; this.resources = rsrc; rotated = ROT_NORMAL; initFont(); this.session = session; fgcolor = resources.getColor(KColorName.BLACK); bgcolor = resources.getColor(KColorName.WHITE); this.addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { sizeChanged(); redraw(); } }); this.prevCursorX = buffer.cursorX; this.prevCursorY = buffer.cursorY; this.addKeyListener(this); //setBackground(bgcolor); sizeChanged(); for (int d = 0; d < dirtyRects.length; d++) dirtyRects[d] = new Rectangle(); this.redrawer = new Redrawer(50, 1000, this); redrawer.start(); buffer.setDisplay(this); } public void update(Graphics g) { log.debug("update() called"); paint(g); //super.update(g); } protected void sizeChanged() { width = getWidth(); height = getHeight(); if (rotated != ROT_NORMAL) { width = getHeight(); height = getWidth(); } cols = width / fontWidth; rows = height / fontHeight; if (width > 0 && height > 0) { //System.out.println("Backing store created"); backingStore = createImage(width, height); } int virtualCols = cols; int virtualRows = rows; this.renderChars = new char[cols]; buffer.setScreenSize(virtualCols, virtualRows); } /** * Finds all screen regions that needs an update * * Those rectangles (un)covered by the cursor are also included. * * @param rects the result will be recorded here * @return the number of valid dirty rectangles */ private int findDirtyRects() { boolean isPrevCursorUpdated = false; boolean isCursorUpdated = false; int cursorCountDn = 2; if (buffer.cursorX == prevCursorX && buffer.cursorY == prevCursorY) { cursorCountDn = 0; isPrevCursorUpdated = true; isCursorUpdated = true; } int numDirty = 0; int nRows = buffer.charArray.length; int nCols = buffer.charArray[0].length; //int startVisible = buffer.windowBase; //int endVisible = buffer.windowBase + buffer.height; // Init 'previous' buffers if (prevChars == null || prevChars.length != nRows || prevChars[0].length != nCols) { prevChars = new char[nRows][]; prevAttrs = new int[nRows][]; for (int r=0; r < nRows; r++) { prevChars[r] = (char[]) buffer.charArray[r].clone(); prevAttrs[r] = (int[]) buffer.charAttributes[r].clone(); } } boolean searchFurther = true; for (int r=0; r < nRows; r++) { int startDirty = nCols; int endDirty = 0; char[] prevLine = prevChars[r]; int[] prevLineAttrs = prevAttrs[r]; char[] curLine = buffer.charArray[r]; int[] curLineAttrs = buffer.charAttributes[r]; for (int c=0; c < nCols; c++) { if(prevLine[c] != curLine[c] || prevLineAttrs[c] != curLineAttrs[c]) { if (c < startDirty) startDirty = c; endDirty = c; prevLine[c] = curLine[c]; prevLineAttrs[c] = curLineAttrs[c]; } } if (searchFurther && startDirty <= endDirty) { // prevCursorY == r-startVisible ??? (is it absolute or relative) if (cursorCountDn != 0 && prevCursorY == r && prevCursorX <= endDirty && prevCursorX >= startDirty) { isPrevCursorUpdated = true; --cursorCountDn; } if (cursorCountDn != 0 && buffer.cursorY == r && buffer.cursorX <= endDirty && buffer.cursorX >= startDirty) { isCursorUpdated = true; --cursorCountDn; } if (cursorCountDn + numDirty >= UPDATE_THRESHOLD) { searchFurther = false; // (probably) doesn't make sense to continue continue; } this.dirtyRects[numDirty++].setBounds(startDirty, r, endDirty-startDirty+1, 1); //System.out.println("Dirty rect: " + nDirty + " cursorCountDn: " + cursorCountDn); } } if (!isPrevCursorUpdated) { this.dirtyRects[numDirty++].setBounds(prevCursorX, prevCursorY, 1, 1); } if (!isCursorUpdated) { this.dirtyRects[numDirty++].setBounds(buffer.cursorX, buffer.cursorY, 1, 1); } prevCursorX = buffer.cursorX; prevCursorY = buffer.cursorY; return numDirty; } private String attrString(int attr) { StringBuffer sb = new StringBuffer(); sb.append("[FG:").append(Integer.toString((attr&VT320.COLOR_FG) >> 4)); sb.append(" BG:").append(Integer.toString((attr&VT320.COLOR_BG) >> 8)); if (0 != (attr&VT320.BOLD)) sb.append(',').append("BLD"); if (0 != (attr&VT320.INVERT)) sb.append(',').append("INV"); if (0 != (attr&VT320.NORMAL)) sb.append(',').append("NRML"); if (0 != (attr&VT320.UNDERLINE)) sb.append(',').append("ULN"); if (0 != (attr&VT320.LOW)) sb.append(',').append("LOW"); sb.append(',').append("RAW:").append(Integer.toHexString(attr)); sb.append(']'); return sb.toString(); } /** * Draws a string with homogenous attributes */ private void drawSpan(Graphics g, int line, int start, int end) { //String as = attrString(buffer.charAttributes[line][start]); //System.out.println("Span[" + line + ":" + start + "-" + end + as + "] " + new String(renderChars)); int attr = buffer.charAttributes[line][start]; Color fg = fgcolor; Color bg = bgcolor; Font curFont = font; if ((attr & VT320.INVERT) != 0) { bg = fgcolor; fg = bgcolor; } // clear the background int x1 = start * fontWidth; int y1 = line * fontHeight; int w = (end - start + 1) * fontWidth; int h = fontHeight; g.setColor(bg); g.fillRect(x1, y1, w, h); g.setColor(fg); g.setFont(font); g.drawChars(renderChars, start, end - start + 1, x1, y1 + fontAscent); if (buffer.cursorY == line && buffer.cursorX >= start && buffer.cursorX <= end) g.fillRect((buffer.cursorX - left) * fontWidth, (buffer.cursorY - top + buffer.screenBase - buffer.windowBase) * fontHeight, 4, fontHeight); } /** * Draws a single line of text. * * @param line The line from the buffer to be rendered * @param offset offset of the first character from the line to rendered * @param len the length of the subsring to be rendered */ private void drawLine(Graphics g, int line, int offset, int len) { renderChars = (char[]) buffer.charArray[line].clone(); int[] curAttrs = buffer.charAttributes[line]; int attr; int start = offset; int end = offset; while (end < offset + len) { attr = curAttrs[start]; while (curAttrs[end] == attr) { if (renderChars[end] < ' ') renderChars[end] = ' '; if (end >= offset+len-1) break; end++; } //System.out.println("Span[" + line + ":" + start + "-" + end + "] " + new String(renderChars)); drawSpan(g, line, start, end); start = end+1; end = start; } } private void redrawAll(Graphics dblBuf) { int nRows = buffer.charArray.length; int nCols = buffer.charArray[0].length; for (int r=0; r < nRows; r++) { drawLine(dblBuf, r, 0, nCols); } } // private void redrawFast(Graphics gScreen, Graphics gBuff, int nDirty) { // for (int d = 0; d < nDirty; d++) { // Rectangle rc = dirtyRects[d]; // renderChars = (char[]) prevChars[rc.y].clone(); // for (int i = rc.x; i < rc.x + rc.width; i++) { // if (renderChars[i] < ' ') // renderChars[i] = ' '; // } // drawLine(gBuff, rc.y, rc.x, rc.width); // // int x1 = rc.x * fontWidth; // int x2 = (rc.x + rc.width)*fontWidth + 1; // int y1 = rc.y * fontHeight; // int y2 = y1 + fontHeight + 1; // //System.out.println("Fast line " + rc.y + " [" + rc.x + ":" + (rc.x+rc.width-1) + "]" + new String(prevChars[rc.y])); // //System.out.println("x1:" + x1 + " x2:" + x2 + " y1:" + y1 + " y2:" + y2); // gScreen.drawImage(backingStore, x1, y1, x2, y2, x1, y1, x2, y2, null); // } // } public void paint(Graphics g) { synchronized (paintMutex) { if (invalid) { Graphics dblBuf = this.backingStore.getGraphics(); if (currentDirty == DIRTY_UNDEFINED) { log.debug("Fullscreen update"); redrawAll(dblBuf); g.drawImage(backingStore, 0, 0, null); } else { log.debug("Fast redrawing rectangle " + currentDirty); Rectangle rc = dirtyRects[currentDirty]; renderChars = (char[]) prevChars[rc.y].clone(); for (int i = rc.x; i < rc.x + rc.width; i++) { if (renderChars[i] < ' ') { renderChars[i] = ' '; } } drawLine(dblBuf, rc.y, rc.x, rc.width); int x1 = rc.x * fontWidth; int x2 = (rc.x + rc.width) * fontWidth + 1; int y1 = rc.y * fontHeight; int y2 = y1 + fontHeight + 1; //System.out.println("Fast line " + rc.y + " [" + rc.x + ":" + (rc.x+rc.width-1) + "]" + new String(prevChars[rc.y])); //System.out.println("x1:" + x1 + " x2:" + x2 + " y1:" + y1 + " y2:" + y2); g.drawImage(backingStore, x1, y1, x2, y2, x1, y1, x2, y2, null); } paintMutex.notifyAll(); invalid = false; } else { log.debug("System re-paint()"); g.drawImage(backingStore, 0, 0, null); } } } public void redraw() { redrawer.requestRedraw(); } private void initFont() { font = resources.getFont(KFontFamilyName.MONOSPACE, FONT_SIZE, KFontStyle.BOLD); FontMetrics fm = getToolkit().getFontMetrics(font); fontHeight = fm.getHeight(); fontWidth = fm.charWidth('W'); fontAscent = fm.getAscent(); } public void keyTyped(KeyEvent e) { buffer.keyTyped(0, e.getKeyChar(), 0); //System.out.println("Code: " + e.getKeyCode()); } public void keyPressed(KeyEvent e) { //System.out.println(e.getKeyText(e.getKeyCode())); int code = e.getKeyCode(); if (code == KindleKeyCodes.VK_SYMBOL) { symbolActive = !symbolActive; } else if (code == KindleKeyCodes.VK_BACK && symbolActive) { symbolActive = false; } else if (code == KindleKeyCodes.VK_FIVE_WAY_SELECT && symbolActive) { symbolActive = false; } else if (symbolActive) { return; } else if (code == KeyEvent.VK_BACK_SPACE) { buffer.keyPressed(VT320.VK_BACK_SPACE, 0); } else if (code == KindleKeyCodes.VK_FIVE_WAY_UP) { buffer.keyPressed(VT320.VK_UP, 0); } else if (code == KeyEvent.VK_LEFT) { buffer.keyPressed(VT320.VK_LEFT, 0); } else if (code == KeyEvent.VK_RIGHT) { buffer.keyPressed(VT320.VK_RIGHT, 0); } else if (code == KeyEvent.VK_UP) { buffer.keyPressed(VT320.VK_UP, 0); } else if (code == KeyEvent.VK_DOWN) { buffer.keyPressed(VT320.VK_DOWN, 0); } else if (code == KeyEvent.VK_TAB) { // setFocusTraversalKeysEnabled(false) should be set in order to work buffer.keyPressed(VT320.VK_TAB, 0); } else if (code == KeyEvent.VK_PAGE_DOWN) { buffer.keyPressed(VT320.VK_PAGE_DOWN, 0); } else if (code == KeyEvent.VK_PAGE_UP) { buffer.keyPressed(VT320.VK_PAGE_UP, 0); } } public void keyReleased(KeyEvent arg0) { //throw new UnsupportedOperationException("Not supported yet."); } public void setText(String arg0) { throw new UnsupportedOperationException("Not supported yet."); } public String getText() { throw new UnsupportedOperationException("Not supported yet."); } public boolean isEditable() { //throw new UnsupportedOperationException("Not supported yet."); return true; } public void setEditable(boolean arg0) { throw new UnsupportedOperationException("Not supported yet."); } public void kill() { redrawer.kill(); } /** * Rate limits the repaints and makes them sequential. */ private class Redrawer extends Thread { private final static long SCHED_UNDEFINED = -1; private boolean killed; private Component component; private long squelchTime; private long maxSquelch; private long lastRepaint; private long lastRequest; private final Object scheduleLock = new Object(); private long scheduledTime; private Logger log; /** * Constructor * * @param squelchTime if new redraw request is received within squelchTime * milliseconds from the previous redraw request * the repaint is delayed for another squelchTime ms, * unless maxSquelch milliseconds are already passed * since the last repaint * * @param maxSquelch new repaint request is issued if maxSquelch ms are * passed since the last repaint even if the requests * should be rate-limited according to squelchTime */ public Redrawer(int squelchTime, int maxSquelch, Component component) { this.squelchTime = squelchTime; this.maxSquelch = maxSquelch; this.component = component; lastRepaint = SCHED_UNDEFINED; lastRequest = SCHED_UNDEFINED; scheduledTime = SCHED_UNDEFINED; killed = false; this.log = Logger.getLogger(KindleTerminal.Redrawer.class.getName()); } public void requestRedraw() { synchronized (scheduleLock) { long time = System.currentTimeMillis(); long dPaint = time - lastRepaint; long dRequest = time - lastRequest; // if (lastRepaint == SCHED_UNDEFINED || dPaint > maxSquelch) { // log.debug("Redraw request should be satisfied NOW"); // scheduledTime = time; // repaint NOW // scheduleLock.notifyAll(); // } // else if (lastRequest == SCHED_UNDEFINED || dRequest < squelchTime) { // log.debug("Redraw request is subject to rate limit"); // scheduledTime = time + squelchTime; // scheduleLock.notifyAll(); // } if (lastRequest == SCHED_UNDEFINED || dRequest < squelchTime) { if (lastRepaint == SCHED_UNDEFINED || dPaint > maxSquelch) { log.debug("Redraw request cannot be squelched anymore. Redrawing NOW"); scheduledTime = time; // repaint NOW scheduleLock.notifyAll(); } else { log.debug("Redraw request is subject to rate limit"); scheduledTime = time + squelchTime; scheduleLock.notifyAll(); } } else { log.debug("Redraw request is not subject to rate limit. Redrawing NOW"); scheduledTime = time; // repaint NOW scheduleLock.notifyAll(); } lastRequest = time; } } public void kill() { synchronized (scheduleLock) { log.debug("Redrawer kill requested"); this.killed = true; scheduleLock.notifyAll(); } } private void processRepaint() { synchronized (paintMutex) { nDirty = findDirtyRects(); if (nDirty >= UPDATE_THRESHOLD) { log.debug("Fullscreen repaint scheduled"); currentDirty = KindleTerminal.DIRTY_UNDEFINED; KindleTerminal.this.invalid = true; EventQueue.invokeLater(new Runnable() { public void run() { synchronized (paintMutex) { //log.debug("Fullscreen updater"); KRepaintManager rm = KRepaintManager.currentManager(component); rm.addDirtyRegion(component, 0, 0, width, height); rm.paintDirtyRegions(false); paintMutex.notifyAll(); } } }); try { paintMutex.wait(); } catch (InterruptedException ie) { log.warn(ie.toString()); } log.debug("Fullscreen repaint finished"); } // nDirty >= UPDATE_THRESHOLD else if (nDirty > 0) { log.debug("Partial update scheduled. Dirty rects: " + nDirty); final KRepaintManager rm = KRepaintManager.currentManager(component); for (int i = 0; i < nDirty; i++) { KindleTerminal.this.invalid = true; KindleTerminal.this.currentDirty = i; log.debug("Current dirty rect: " + dirtyRects[currentDirty]); EventQueue.invokeLater(new Runnable() { public void run() { Rectangle rect = dirtyRects[KindleTerminal.this.currentDirty]; int x = rect.x * fontWidth; int y = rect.y * fontHeight; int w = rect.width * fontWidth; int h = rect.height * fontHeight; rm.addDirtyRegion(component, x, y, w, h); rm.paintDirtyRegions(false); } }); try { paintMutex.wait(); } catch (InterruptedException ie) { log.warn(ie.toString()); } log.debug("Dirty rect " + dirtyRects[currentDirty] + " repaint finished"); } } currentDirty = DIRTY_UNDEFINED; } // synchronized (paintMutex) synchronized (scheduleLock) { lastRepaint = System.currentTimeMillis(); } } public void run() { MAINLOOP: while (!killed) { synchronized (scheduleLock) { long time = System.currentTimeMillis(); while (scheduledTime == SCHED_UNDEFINED || time < scheduledTime) { if (killed) break MAINLOOP; try { if (scheduledTime == SCHED_UNDEFINED) { log.debug("No repaint is currently scheduled. Going to sleep"); scheduleLock.wait(); } else { long waitTime = scheduledTime - time; log.debug("Repaint cheduled in " + waitTime); scheduleLock.wait(waitTime); } } catch (InterruptedException ie) { log.warn(ie.toString()); } time = System.currentTimeMillis(); } // while scheduledTime = SCHED_UNDEFINED; } // synchronized (scheduleLock) processRepaint(); } } } }