/* * AwtTerminal.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 awt; //import app.session.Session; import gui.Redrawable; import java.awt.Canvas; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import terminal.VT320; /** * A rewrite of {@link terminal.Terminal} that uses the AWT UI widgets. * * @author VDP <vdp DOT kindle AT gmail.com> */ public class AwtTerminal extends Canvas implements Redrawable, KeyListener { // ------------------------------- 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 = new Color(0x000000); public Color bgcolor = new Color(0xffffff); 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; Rectangle[] dirtyRects = new Rectangle[UPDATE_THRESHOLD]; /** * @param buffer */ public AwtTerminal(VT320 buffer, AwtSession session) { this.buffer = buffer; buffer.setDisplay(this); rotated = ROT_NORMAL; initFont(); //top = 0; //left = 0; this.session = session; 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(); } public void update(Graphics g) { 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); } private void doDisconnect() { session.disconnect(); } /** * 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 nDirty = 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 + nDirty >= UPDATE_THRESHOLD) { searchFurther = false; // (probably) doesn't make sense to continue continue; } this.dirtyRects[nDirty++].setBounds(startDirty, r, endDirty-startDirty+1, 1); //System.out.println("Dirty rect: " + nDirty + " cursorCountDn: " + cursorCountDn); } } if (!isPrevCursorUpdated) { this.dirtyRects[nDirty++].setBounds(prevCursorX, prevCursorY, 1, 1); } if (!isCursorUpdated) { this.dirtyRects[nDirty++].setBounds(buffer.cursorX, buffer.cursorY, 1, 1); } prevCursorX = buffer.cursorX; prevCursorY = buffer.cursorY; return nDirty; } 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 = prevAttrs[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, fontWidth, 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[]) prevChars[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) { int nDirty = findDirtyRects(); Graphics dblBuf = this.backingStore.getGraphics(); if (nDirty >= UPDATE_THRESHOLD) { redrawAll(dblBuf); g.drawImage(backingStore, 0, 0, null); } else redrawFast(g, dblBuf, nDirty); invalid = false; } else { g.drawImage(backingStore, 0, 0, null); } } } public void redraw() { synchronized (paintMutex) { invalid = true; repaint(); } } private void initFont() { initSystemFont(FONT_SIZE); } private void initSystemFont(int size) { font = new Font("Monospaced", Font.PLAIN, size); 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 == KeyEvent.VK_ENTER) { buffer.keyPressed(VT320.VK_ENTER, 0); System.out.println("VK_ENTER: " + code); } else if (code == KeyEvent.VK_BACK_SPACE) { buffer.keyPressed(VT320.VK_BACK_SPACE, 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."); } }