// // Copyright (C) 2004 Horizon Wimba. All Rights Reserved. // Copyright (C) 2001-2003 HorizonLive.com, Inc. All Rights Reserved. // Copyright (C) 2001,2002 Constantin Kaplinsky. All Rights Reserved. // Copyright (C) 2000 Tridia Corporation. All Rights Reserved. // Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. // // This 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 2 of the License, or // (at your option) any later version. // // This software 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 this software; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, // USA. // import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.io.*; import java.lang.*; import java.util.zip.*; // // VncCanvas is a subclass of Canvas which draws a VNC desktop on it. // class VncCanvas extends Canvas implements KeyListener, MouseListener, MouseMotionListener { VncViewer viewer; RfbProto rfb; ColorModel cm8, cm24; Color[] colors; int bytesPixel; int maxWidth = 0, maxHeight = 0; int scalingFactor; int scaledWidth, scaledHeight; Image memImage; Graphics memGraphics; Image rawPixelsImage; MemoryImageSource pixelsSource; byte[] pixels8; int[] pixels24; // ZRLE encoder's data. byte[] zrleBuf; int zrleBufLen = 0; byte[] zrleTilePixels8; int[] zrleTilePixels24; ZlibInStream zrleInStream; boolean zrleRecWarningShown = false; // Zlib encoder's data. byte[] zlibBuf; int zlibBufLen = 0; Inflater zlibInflater; // Tight encoder's data. final static int tightZlibBufferSize = 512; Inflater[] tightInflaters; // Since JPEG images are loaded asynchronously, we have to remember // their position in the framebuffer. Also, this jpegRect object is // used for synchronization between the rfbThread and a JVM's thread // which decodes and loads JPEG images. Rectangle jpegRect; // True if we process keyboard and mouse events. boolean inputEnabled; int extraModifiers = 0; // // The constructors. // public VncCanvas(VncViewer v, int maxWidth_, int maxHeight_) throws IOException { viewer = v; maxWidth = maxWidth_; maxHeight = maxHeight_; rfb = viewer.rfb; scalingFactor = viewer.options.scalingFactor; tightInflaters = new Inflater[4]; cm8 = new DirectColorModel(8, 7, (7 << 3), (3 << 6)); cm24 = new DirectColorModel(24, 0xFF0000, 0x00FF00, 0x0000FF); colors = new Color[256]; for (int i = 0; i < 256; i++) colors[i] = new Color(cm8.getRGB(i)); setPixelFormat(); inputEnabled = false; if (!viewer.options.viewOnly) enableInput(true); // Keyboard listener is enabled even in view-only mode, to catch // 'r' or 'R' key presses used to request screen update. addKeyListener(this); } public VncCanvas(VncViewer v) throws IOException { this(v, 0, 0); } // // Callback methods to determine geometry of our Component. // public Dimension getPreferredSize() { return new Dimension(scaledWidth, scaledHeight); } public Dimension getMinimumSize() { return new Dimension(scaledWidth, scaledHeight); } public Dimension getMaximumSize() { return new Dimension(scaledWidth, scaledHeight); } // // All painting is performed here. // public void update(Graphics g) { paint(g); } public void paint(Graphics g) { synchronized(memImage) { if (rfb.framebufferWidth == scaledWidth) { g.drawImage(memImage, 0, 0, null); } else { paintScaledFrameBuffer(g); } } if (showSoftCursor) { int x0 = cursorX - hotX, y0 = cursorY - hotY; Rectangle r = new Rectangle(x0, y0, cursorWidth, cursorHeight); if (r.intersects(g.getClipBounds())) { g.drawImage(softCursor, x0, y0, null); } } } public void paintScaledFrameBuffer(Graphics g) { g.drawImage(memImage, 0, 0, scaledWidth, scaledHeight, null); } // // Override the ImageObserver interface method to handle drawing of // JPEG-encoded data. // public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { if ((infoflags & (ALLBITS | ABORT)) == 0) { return true; // We need more image data. } else { // If the whole image is available, draw it now. if ((infoflags & ALLBITS) != 0) { if (jpegRect != null) { synchronized(jpegRect) { memGraphics.drawImage(img, jpegRect.x, jpegRect.y, null); scheduleRepaint(jpegRect.x, jpegRect.y, jpegRect.width, jpegRect.height); jpegRect.notify(); } } } return false; // All image data was processed. } } // // Start/stop receiving mouse events. Keyboard events are received // even in view-only mode, because we want to map the 'r' key to the // screen refreshing function. // public synchronized void enableInput(boolean enable) { if (enable && !inputEnabled) { inputEnabled = true; addMouseListener(this); addMouseMotionListener(this); if (viewer.showControls) { viewer.buttonPanel.enableRemoteAccessControls(true); } createSoftCursor(); // scaled cursor } else if (!enable && inputEnabled) { inputEnabled = false; removeMouseListener(this); removeMouseMotionListener(this); if (viewer.showControls) { viewer.buttonPanel.enableRemoteAccessControls(false); } createSoftCursor(); // non-scaled cursor } } public void setPixelFormat() throws IOException { if (viewer.options.eightBitColors) { rfb.writeSetPixelFormat(8, 8, false, true, 7, 7, 3, 0, 3, 6); bytesPixel = 1; } else { rfb.writeSetPixelFormat(32, 24, false, true, 255, 255, 255, 16, 8, 0); bytesPixel = 4; } updateFramebufferSize(); } void updateFramebufferSize() { // Useful shortcuts. int fbWidth = rfb.framebufferWidth; int fbHeight = rfb.framebufferHeight; // Calculate scaling factor for auto scaling. if (maxWidth > 0 && maxHeight > 0) { int f1 = maxWidth * 100 / fbWidth; int f2 = maxHeight * 100 / fbHeight; scalingFactor = Math.min(f1, f2); if (scalingFactor > 100) scalingFactor = 100; System.out.println("Scaling desktop at " + scalingFactor + "%"); } // Update scaled framebuffer geometry. scaledWidth = (fbWidth * scalingFactor + 50) / 100; scaledHeight = (fbHeight * scalingFactor + 50) / 100; // Create new off-screen image either if it does not exist, or if // its geometry should be changed. It's not necessary to replace // existing image if only pixel format should be changed. if (memImage == null) { memImage = viewer.vncContainer.createImage(fbWidth, fbHeight); memGraphics = memImage.getGraphics(); } else if (memImage.getWidth(null) != fbWidth || memImage.getHeight(null) != fbHeight) { synchronized(memImage) { memImage = viewer.vncContainer.createImage(fbWidth, fbHeight); memGraphics = memImage.getGraphics(); } } // Images with raw pixels should be re-allocated on every change // of geometry or pixel format. if (bytesPixel == 1) { pixels24 = null; pixels8 = new byte[fbWidth * fbHeight]; pixelsSource = new MemoryImageSource(fbWidth, fbHeight, cm8, pixels8, 0, fbWidth); zrleTilePixels24 = null; zrleTilePixels8 = new byte[64 * 64]; } else { pixels8 = null; pixels24 = new int[fbWidth * fbHeight]; pixelsSource = new MemoryImageSource(fbWidth, fbHeight, cm24, pixels24, 0, fbWidth); zrleTilePixels8 = null; zrleTilePixels24 = new int[64 * 64]; } pixelsSource.setAnimated(true); rawPixelsImage = Toolkit.getDefaultToolkit().createImage(pixelsSource); // Update the size of desktop containers. if (viewer.inSeparateFrame) { if (viewer.desktopScrollPane != null) resizeDesktopFrame(); } else { setSize(scaledWidth, scaledHeight); } viewer.moveFocusToDesktop(); } void resizeDesktopFrame() { setSize(scaledWidth, scaledHeight); // FIXME: Find a better way to determine correct size of a // ScrollPane. -- const Insets insets = viewer.desktopScrollPane.getInsets(); viewer.desktopScrollPane.setSize(scaledWidth + 2 * Math.min(insets.left, insets.right), scaledHeight + 2 * Math.min(insets.top, insets.bottom)); viewer.vncFrame.pack(); // Try to limit the frame size to the screen size. Dimension screenSize = viewer.vncFrame.getToolkit().getScreenSize(); Dimension frameSize = viewer.vncFrame.getSize(); Dimension newSize = frameSize; // Reduce Screen Size by 30 pixels in each direction; // This is a (poor) attempt to account for // 1) Menu bar on Macintosh (should really also account for // Dock on OSX). Usually 22px on top of screen. // 2) Taxkbar on Windows (usually about 28 px on bottom) // 3) Other obstructions. screenSize.height -= 30; screenSize.width -= 30; boolean needToResizeFrame = false; if (frameSize.height > screenSize.height) { newSize.height = screenSize.height; needToResizeFrame = true; } if (frameSize.width > screenSize.width) { newSize.width = screenSize.width; needToResizeFrame = true; } if (needToResizeFrame) { viewer.vncFrame.setSize(newSize); } viewer.desktopScrollPane.doLayout(); } // // processNormalProtocol() - executed by the rfbThread to deal with the // RFB socket. // public void processNormalProtocol() throws Exception { // Start/stop session recording if necessary. viewer.checkRecordingStatus(); rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth, rfb.framebufferHeight, false); // // main dispatch loop // while (true) { // Read message type from the server. int msgType = rfb.readServerMessageType(); // Process the message depending on its type. switch (msgType) { case RfbProto.FramebufferUpdate: rfb.readFramebufferUpdate(); boolean cursorPosReceived = false; for (int i = 0; i < rfb.updateNRects; i++) { rfb.readFramebufferUpdateRectHdr(); int rx = rfb.updateRectX, ry = rfb.updateRectY; int rw = rfb.updateRectW, rh = rfb.updateRectH; if (rfb.updateRectEncoding == rfb.EncodingLastRect) break; if (rfb.updateRectEncoding == rfb.EncodingNewFBSize) { rfb.setFramebufferSize(rw, rh); updateFramebufferSize(); break; } if (rfb.updateRectEncoding == rfb.EncodingXCursor || rfb.updateRectEncoding == rfb.EncodingRichCursor) { handleCursorShapeUpdate(rfb.updateRectEncoding, rx, ry, rw, rh); continue; } if (rfb.updateRectEncoding == rfb.EncodingPointerPos) { softCursorMove(rx, ry); cursorPosReceived = true; continue; } rfb.startTiming(); switch (rfb.updateRectEncoding) { case RfbProto.EncodingRaw: handleRawRect(rx, ry, rw, rh); break; case RfbProto.EncodingCopyRect: handleCopyRect(rx, ry, rw, rh); break; case RfbProto.EncodingRRE: handleRRERect(rx, ry, rw, rh); break; case RfbProto.EncodingCoRRE: handleCoRRERect(rx, ry, rw, rh); break; case RfbProto.EncodingHextile: handleHextileRect(rx, ry, rw, rh); break; case RfbProto.EncodingZRLE: handleZRLERect(rx, ry, rw, rh); break; case RfbProto.EncodingZlib: handleZlibRect(rx, ry, rw, rh); break; case RfbProto.EncodingTight: handleTightRect(rx, ry, rw, rh); break; default: throw new Exception("Unknown RFB rectangle encoding " + rfb.updateRectEncoding); } rfb.stopTiming(); } boolean fullUpdateNeeded = false; // Start/stop session recording if necessary. Request full // update if a new session file was opened. if (viewer.checkRecordingStatus()) fullUpdateNeeded = true; // Defer framebuffer update request if necessary. But wake up // immediately on keyboard or mouse event. Also, don't sleep // if there is some data to receive, or if the last update // included a PointerPos message. if (viewer.deferUpdateRequests > 0 && rfb.is.available() == 0 && !cursorPosReceived) { synchronized(rfb) { try { rfb.wait(viewer.deferUpdateRequests); } catch (InterruptedException e) { } } } // Before requesting framebuffer update, check if the pixel // format should be changed. If it should, request full update // instead of an incremental one. if (viewer.options.eightBitColors != (bytesPixel == 1)) { setPixelFormat(); fullUpdateNeeded = true; } viewer.autoSelectEncodings(); rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth, rfb.framebufferHeight, !fullUpdateNeeded); break; case RfbProto.SetColourMapEntries: throw new Exception("Can't handle SetColourMapEntries message"); case RfbProto.Bell: Toolkit.getDefaultToolkit().beep(); break; case RfbProto.ServerCutText: String s = rfb.readServerCutText(); viewer.clipboard.setCutText(s); break; default: throw new Exception("Unknown RFB message type " + msgType); } } } // // Handle a raw rectangle. The second form with paint==false is used // by the Hextile decoder for raw-encoded tiles. // void handleRawRect(int x, int y, int w, int h) throws IOException { handleRawRect(x, y, w, h, true); } void handleRawRect(int x, int y, int w, int h, boolean paint) throws IOException { if (bytesPixel == 1) { for (int dy = y; dy < y + h; dy++) { rfb.readFully(pixels8, dy * rfb.framebufferWidth + x, w); if (rfb.rec != null) { rfb.rec.write(pixels8, dy * rfb.framebufferWidth + x, w); } } } else { byte[] buf = new byte[w * 4]; int i, offset; for (int dy = y; dy < y + h; dy++) { rfb.readFully(buf); if (rfb.rec != null) { rfb.rec.write(buf); } offset = dy * rfb.framebufferWidth + x; for (i = 0; i < w; i++) { pixels24[offset + i] = (buf[i * 4 + 2] & 0xFF) << 16 | (buf[i * 4 + 1] & 0xFF) << 8 | (buf[i * 4] & 0xFF); } } } handleUpdatedPixels(x, y, w, h); if (paint) scheduleRepaint(x, y, w, h); } // // Handle a CopyRect rectangle. // void handleCopyRect(int x, int y, int w, int h) throws IOException { rfb.readCopyRect(); memGraphics.copyArea(rfb.copyRectSrcX, rfb.copyRectSrcY, w, h, x - rfb.copyRectSrcX, y - rfb.copyRectSrcY); scheduleRepaint(x, y, w, h); } // // Handle an RRE-encoded rectangle. // void handleRRERect(int x, int y, int w, int h) throws IOException { int nSubrects = rfb.is.readInt(); byte[] bg_buf = new byte[bytesPixel]; rfb.readFully(bg_buf); Color pixel; if (bytesPixel == 1) { pixel = colors[bg_buf[0] & 0xFF]; } else { pixel = new Color(bg_buf[2] & 0xFF, bg_buf[1] & 0xFF, bg_buf[0] & 0xFF); } memGraphics.setColor(pixel); memGraphics.fillRect(x, y, w, h); byte[] buf = new byte[nSubrects * (bytesPixel + 8)]; rfb.readFully(buf); DataInputStream ds = new DataInputStream(new ByteArrayInputStream(buf)); if (rfb.rec != null) { rfb.rec.writeIntBE(nSubrects); rfb.rec.write(bg_buf); rfb.rec.write(buf); } int sx, sy, sw, sh; for (int j = 0; j < nSubrects; j++) { if (bytesPixel == 1) { pixel = colors[ds.readUnsignedByte()]; } else { ds.skip(4); pixel = new Color(buf[j*12+2] & 0xFF, buf[j*12+1] & 0xFF, buf[j*12] & 0xFF); } sx = x + ds.readUnsignedShort(); sy = y + ds.readUnsignedShort(); sw = ds.readUnsignedShort(); sh = ds.readUnsignedShort(); memGraphics.setColor(pixel); memGraphics.fillRect(sx, sy, sw, sh); } scheduleRepaint(x, y, w, h); } // // Handle a CoRRE-encoded rectangle. // void handleCoRRERect(int x, int y, int w, int h) throws IOException { int nSubrects = rfb.is.readInt(); byte[] bg_buf = new byte[bytesPixel]; rfb.readFully(bg_buf); Color pixel; if (bytesPixel == 1) { pixel = colors[bg_buf[0] & 0xFF]; } else { pixel = new Color(bg_buf[2] & 0xFF, bg_buf[1] & 0xFF, bg_buf[0] & 0xFF); } memGraphics.setColor(pixel); memGraphics.fillRect(x, y, w, h); byte[] buf = new byte[nSubrects * (bytesPixel + 4)]; rfb.readFully(buf); if (rfb.rec != null) { rfb.rec.writeIntBE(nSubrects); rfb.rec.write(bg_buf); rfb.rec.write(buf); } int sx, sy, sw, sh; int i = 0; for (int j = 0; j < nSubrects; j++) { if (bytesPixel == 1) { pixel = colors[buf[i++] & 0xFF]; } else { pixel = new Color(buf[i+2] & 0xFF, buf[i+1] & 0xFF, buf[i] & 0xFF); i += 4; } sx = x + (buf[i++] & 0xFF); sy = y + (buf[i++] & 0xFF); sw = buf[i++] & 0xFF; sh = buf[i++] & 0xFF; memGraphics.setColor(pixel); memGraphics.fillRect(sx, sy, sw, sh); } scheduleRepaint(x, y, w, h); } // // Handle a Hextile-encoded rectangle. // // These colors should be kept between handleHextileSubrect() calls. private Color hextile_bg, hextile_fg; void handleHextileRect(int x, int y, int w, int h) throws IOException { hextile_bg = new Color(0); hextile_fg = new Color(0); for (int ty = y; ty < y + h; ty += 16) { int th = 16; if (y + h - ty < 16) th = y + h - ty; for (int tx = x; tx < x + w; tx += 16) { int tw = 16; if (x + w - tx < 16) tw = x + w - tx; handleHextileSubrect(tx, ty, tw, th); } // Finished with a row of tiles, now let's show it. scheduleRepaint(x, y, w, h); } } // // Handle one tile in the Hextile-encoded data. // void handleHextileSubrect(int tx, int ty, int tw, int th) throws IOException { int subencoding = rfb.is.readUnsignedByte(); if (rfb.rec != null) { rfb.rec.writeByte(subencoding); } // Is it a raw-encoded sub-rectangle? if ((subencoding & rfb.HextileRaw) != 0) { handleRawRect(tx, ty, tw, th, false); return; } // Read and draw the background if specified. byte[] cbuf = new byte[bytesPixel]; if ((subencoding & rfb.HextileBackgroundSpecified) != 0) { rfb.readFully(cbuf); if (bytesPixel == 1) { hextile_bg = colors[cbuf[0] & 0xFF]; } else { hextile_bg = new Color(cbuf[2] & 0xFF, cbuf[1] & 0xFF, cbuf[0] & 0xFF); } if (rfb.rec != null) { rfb.rec.write(cbuf); } } memGraphics.setColor(hextile_bg); memGraphics.fillRect(tx, ty, tw, th); // Read the foreground color if specified. if ((subencoding & rfb.HextileForegroundSpecified) != 0) { rfb.readFully(cbuf); if (bytesPixel == 1) { hextile_fg = colors[cbuf[0] & 0xFF]; } else { hextile_fg = new Color(cbuf[2] & 0xFF, cbuf[1] & 0xFF, cbuf[0] & 0xFF); } if (rfb.rec != null) { rfb.rec.write(cbuf); } } // Done with this tile if there is no sub-rectangles. if ((subencoding & rfb.HextileAnySubrects) == 0) return; int nSubrects = rfb.is.readUnsignedByte(); int bufsize = nSubrects * 2; if ((subencoding & rfb.HextileSubrectsColoured) != 0) { bufsize += nSubrects * bytesPixel; } byte[] buf = new byte[bufsize]; rfb.readFully(buf); if (rfb.rec != null) { rfb.rec.writeByte(nSubrects); rfb.rec.write(buf); } int b1, b2, sx, sy, sw, sh; int i = 0; if ((subencoding & rfb.HextileSubrectsColoured) == 0) { // Sub-rectangles are all of the same color. memGraphics.setColor(hextile_fg); for (int j = 0; j < nSubrects; j++) { b1 = buf[i++] & 0xFF; b2 = buf[i++] & 0xFF; sx = tx + (b1 >> 4); sy = ty + (b1 & 0xf); sw = (b2 >> 4) + 1; sh = (b2 & 0xf) + 1; memGraphics.fillRect(sx, sy, sw, sh); } } else if (bytesPixel == 1) { // BGR233 (8-bit color) version for colored sub-rectangles. for (int j = 0; j < nSubrects; j++) { hextile_fg = colors[buf[i++] & 0xFF]; b1 = buf[i++] & 0xFF; b2 = buf[i++] & 0xFF; sx = tx + (b1 >> 4); sy = ty + (b1 & 0xf); sw = (b2 >> 4) + 1; sh = (b2 & 0xf) + 1; memGraphics.setColor(hextile_fg); memGraphics.fillRect(sx, sy, sw, sh); } } else { // Full-color (24-bit) version for colored sub-rectangles. for (int j = 0; j < nSubrects; j++) { hextile_fg = new Color(buf[i+2] & 0xFF, buf[i+1] & 0xFF, buf[i] & 0xFF); i += 4; b1 = buf[i++] & 0xFF; b2 = buf[i++] & 0xFF; sx = tx + (b1 >> 4); sy = ty + (b1 & 0xf); sw = (b2 >> 4) + 1; sh = (b2 & 0xf) + 1; memGraphics.setColor(hextile_fg); memGraphics.fillRect(sx, sy, sw, sh); } } } // // Handle a ZRLE-encoded rectangle. // // FIXME: Currently, session recording is not fully supported for ZRLE. // void handleZRLERect(int x, int y, int w, int h) throws Exception { if (zrleInStream == null) zrleInStream = new ZlibInStream(); int nBytes = rfb.is.readInt(); if (nBytes > 64 * 1024 * 1024) throw new Exception("ZRLE decoder: illegal compressed data size"); if (zrleBuf == null || zrleBufLen < nBytes) { zrleBufLen = nBytes + 4096; zrleBuf = new byte[zrleBufLen]; } // FIXME: Do not wait for all the data before decompression. rfb.readFully(zrleBuf, 0, nBytes); if (rfb.rec != null) { if (rfb.recordFromBeginning) { rfb.rec.writeIntBE(nBytes); rfb.rec.write(zrleBuf, 0, nBytes); } else if (!zrleRecWarningShown) { System.out.println("Warning: ZRLE session can be recorded" + " only from the beginning"); System.out.println("Warning: Recorded file may be corrupted"); zrleRecWarningShown = true; } } zrleInStream.setUnderlying(new MemInStream(zrleBuf, 0, nBytes), nBytes); for (int ty = y; ty < y+h; ty += 64) { int th = Math.min(y+h-ty, 64); for (int tx = x; tx < x+w; tx += 64) { int tw = Math.min(x+w-tx, 64); int mode = zrleInStream.readU8(); boolean rle = (mode & 128) != 0; int palSize = mode & 127; int[] palette = new int[128]; readZrlePalette(palette, palSize); if (palSize == 1) { int pix = palette[0]; Color c = (bytesPixel == 1) ? colors[pix] : new Color(0xFF000000 | pix); memGraphics.setColor(c); memGraphics.fillRect(tx, ty, tw, th); continue; } if (!rle) { if (palSize == 0) { readZrleRawPixels(tw, th); } else { readZrlePackedPixels(tw, th, palette, palSize); } } else { if (palSize == 0) { readZrlePlainRLEPixels(tw, th); } else { readZrlePackedRLEPixels(tw, th, palette); } } handleUpdatedZrleTile(tx, ty, tw, th); } } zrleInStream.reset(); scheduleRepaint(x, y, w, h); } int readPixel(InStream is) throws Exception { int pix; if (bytesPixel == 1) { pix = is.readU8(); } else { int p1 = is.readU8(); int p2 = is.readU8(); int p3 = is.readU8(); pix = (p3 & 0xFF) << 16 | (p2 & 0xFF) << 8 | (p1 & 0xFF); } return pix; } void readPixels(InStream is, int[] dst, int count) throws Exception { int pix; if (bytesPixel == 1) { byte[] buf = new byte[count]; is.readBytes(buf, 0, count); for (int i = 0; i < count; i++) { dst[i] = (int)buf[i] & 0xFF; } } else { byte[] buf = new byte[count * 3]; is.readBytes(buf, 0, count * 3); for (int i = 0; i < count; i++) { dst[i] = ((buf[i*3+2] & 0xFF) << 16 | (buf[i*3+1] & 0xFF) << 8 | (buf[i*3] & 0xFF)); } } } void readZrlePalette(int[] palette, int palSize) throws Exception { readPixels(zrleInStream, palette, palSize); } void readZrleRawPixels(int tw, int th) throws Exception { if (bytesPixel == 1) { zrleInStream.readBytes(zrleTilePixels8, 0, tw * th); } else { readPixels(zrleInStream, zrleTilePixels24, tw * th); /// } } void readZrlePackedPixels(int tw, int th, int[] palette, int palSize) throws Exception { int bppp = ((palSize > 16) ? 8 : ((palSize > 4) ? 4 : ((palSize > 2) ? 2 : 1))); int ptr = 0; for (int i = 0; i < th; i++) { int eol = ptr + tw; int b = 0; int nbits = 0; while (ptr < eol) { if (nbits == 0) { b = zrleInStream.readU8(); nbits = 8; } nbits -= bppp; int index = (b >> nbits) & ((1 << bppp) - 1) & 127; if (bytesPixel == 1) { zrleTilePixels8[ptr++] = (byte)palette[index]; } else { zrleTilePixels24[ptr++] = palette[index]; } } } } void readZrlePlainRLEPixels(int tw, int th) throws Exception { int ptr = 0; int end = ptr + tw * th; while (ptr < end) { int pix = readPixel(zrleInStream); int len = 1; int b; do { b = zrleInStream.readU8(); len += b; } while (b == 255); if (!(len <= end - ptr)) throw new Exception("ZRLE decoder: assertion failed" + " (len <= end-ptr)"); if (bytesPixel == 1) { while (len-- > 0) zrleTilePixels8[ptr++] = (byte)pix; } else { while (len-- > 0) zrleTilePixels24[ptr++] = pix; } } } void readZrlePackedRLEPixels(int tw, int th, int[] palette) throws Exception { int ptr = 0; int end = ptr + tw * th; while (ptr < end) { int index = zrleInStream.readU8(); int len = 1; if ((index & 128) != 0) { int b; do { b = zrleInStream.readU8(); len += b; } while (b == 255); if (!(len <= end - ptr)) throw new Exception("ZRLE decoder: assertion failed" + " (len <= end - ptr)"); } index &= 127; int pix = palette[index]; if (bytesPixel == 1) { while (len-- > 0) zrleTilePixels8[ptr++] = (byte)pix; } else { while (len-- > 0) zrleTilePixels24[ptr++] = pix; } } } // // Copy pixels from zrleTilePixels8 or zrleTilePixels24, then update. // void handleUpdatedZrleTile(int x, int y, int w, int h) { Object src, dst; if (bytesPixel == 1) { src = zrleTilePixels8; dst = pixels8; } else { src = zrleTilePixels24; dst = pixels24; } int offsetSrc = 0; int offsetDst = (y * rfb.framebufferWidth + x); for (int j = 0; j < h; j++) { System.arraycopy(src, offsetSrc, dst, offsetDst, w); offsetSrc += w; offsetDst += rfb.framebufferWidth; } handleUpdatedPixels(x, y, w, h); } // // Handle a Zlib-encoded rectangle. // void handleZlibRect(int x, int y, int w, int h) throws Exception { int nBytes = rfb.is.readInt(); if (zlibBuf == null || zlibBufLen < nBytes) { zlibBufLen = nBytes * 2; zlibBuf = new byte[zlibBufLen]; } rfb.readFully(zlibBuf, 0, nBytes); if (rfb.rec != null && rfb.recordFromBeginning) { rfb.rec.writeIntBE(nBytes); rfb.rec.write(zlibBuf, 0, nBytes); } if (zlibInflater == null) { zlibInflater = new Inflater(); } zlibInflater.setInput(zlibBuf, 0, nBytes); if (bytesPixel == 1) { for (int dy = y; dy < y + h; dy++) { zlibInflater.inflate(pixels8, dy * rfb.framebufferWidth + x, w); if (rfb.rec != null && !rfb.recordFromBeginning) rfb.rec.write(pixels8, dy * rfb.framebufferWidth + x, w); } } else { byte[] buf = new byte[w * 4]; int i, offset; for (int dy = y; dy < y + h; dy++) { zlibInflater.inflate(buf); offset = dy * rfb.framebufferWidth + x; for (i = 0; i < w; i++) { pixels24[offset + i] = (buf[i * 4 + 2] & 0xFF) << 16 | (buf[i * 4 + 1] & 0xFF) << 8 | (buf[i * 4] & 0xFF); } if (rfb.rec != null && !rfb.recordFromBeginning) rfb.rec.write(buf); } } handleUpdatedPixels(x, y, w, h); scheduleRepaint(x, y, w, h); } // // Handle a Tight-encoded rectangle. // void handleTightRect(int x, int y, int w, int h) throws Exception { int comp_ctl = rfb.is.readUnsignedByte(); if (rfb.rec != null) { if (rfb.recordFromBeginning || comp_ctl == (rfb.TightFill << 4) || comp_ctl == (rfb.TightJpeg << 4)) { // Send data exactly as received. rfb.rec.writeByte(comp_ctl); } else { // Tell the decoder to flush each of the four zlib streams. rfb.rec.writeByte(comp_ctl | 0x0F); } } // Flush zlib streams if we are told by the server to do so. for (int stream_id = 0; stream_id < 4; stream_id++) { if ((comp_ctl & 1) != 0 && tightInflaters[stream_id] != null) { tightInflaters[stream_id] = null; } comp_ctl >>= 1; } // Check correctness of subencoding value. if (comp_ctl > rfb.TightMaxSubencoding) { throw new Exception("Incorrect tight subencoding: " + comp_ctl); } // Handle solid-color rectangles. if (comp_ctl == rfb.TightFill) { if (bytesPixel == 1) { int idx = rfb.is.readUnsignedByte(); memGraphics.setColor(colors[idx]); if (rfb.rec != null) { rfb.rec.writeByte(idx); } } else { byte[] buf = new byte[3]; rfb.readFully(buf); if (rfb.rec != null) { rfb.rec.write(buf); } Color bg = new Color(0xFF000000 | (buf[0] & 0xFF) << 16 | (buf[1] & 0xFF) << 8 | (buf[2] & 0xFF)); memGraphics.setColor(bg); } memGraphics.fillRect(x, y, w, h); scheduleRepaint(x, y, w, h); return; } if (comp_ctl == rfb.TightJpeg) { // Read JPEG data. byte[] jpegData = new byte[rfb.readCompactLen()]; rfb.readFully(jpegData); if (rfb.rec != null) { if (!rfb.recordFromBeginning) { rfb.recordCompactLen(jpegData.length); } rfb.rec.write(jpegData); } // Create an Image object from the JPEG data. Image jpegImage = Toolkit.getDefaultToolkit().createImage(jpegData); // Remember the rectangle where the image should be drawn. jpegRect = new Rectangle(x, y, w, h); // Let the imageUpdate() method do the actual drawing, here just // wait until the image is fully loaded and drawn. synchronized(jpegRect) { Toolkit.getDefaultToolkit().prepareImage(jpegImage, -1, -1, this); try { // Wait no longer than three seconds. jpegRect.wait(3000); } catch (InterruptedException e) { throw new Exception("Interrupted while decoding JPEG image"); } } // Done, jpegRect is not needed any more. jpegRect = null; return; } // Read filter id and parameters. int numColors = 0, rowSize = w; byte[] palette8 = new byte[2]; int[] palette24 = new int[256]; boolean useGradient = false; if ((comp_ctl & rfb.TightExplicitFilter) != 0) { int filter_id = rfb.is.readUnsignedByte(); if (rfb.rec != null) { rfb.rec.writeByte(filter_id); } if (filter_id == rfb.TightFilterPalette) { numColors = rfb.is.readUnsignedByte() + 1; if (rfb.rec != null) { rfb.rec.writeByte(numColors - 1); } if (bytesPixel == 1) { if (numColors != 2) { throw new Exception("Incorrect tight palette size: " + numColors); } rfb.readFully(palette8); if (rfb.rec != null) { rfb.rec.write(palette8); } } else { byte[] buf = new byte[numColors * 3]; rfb.readFully(buf); if (rfb.rec != null) { rfb.rec.write(buf); } for (int i = 0; i < numColors; i++) { palette24[i] = ((buf[i * 3] & 0xFF) << 16 | (buf[i * 3 + 1] & 0xFF) << 8 | (buf[i * 3 + 2] & 0xFF)); } } if (numColors == 2) rowSize = (w + 7) / 8; } else if (filter_id == rfb.TightFilterGradient) { useGradient = true; } else if (filter_id != rfb.TightFilterCopy) { throw new Exception("Incorrect tight filter id: " + filter_id); } } if (numColors == 0 && bytesPixel == 4) rowSize *= 3; // Read, optionally uncompress and decode data. int dataSize = h * rowSize; if (dataSize < rfb.TightMinToCompress) { // Data size is small - not compressed with zlib. if (numColors != 0) { // Indexed colors. byte[] indexedData = new byte[dataSize]; rfb.readFully(indexedData); if (rfb.rec != null) { rfb.rec.write(indexedData); } if (numColors == 2) { // Two colors. if (bytesPixel == 1) { decodeMonoData(x, y, w, h, indexedData, palette8); } else { decodeMonoData(x, y, w, h, indexedData, palette24); } } else { // 3..255 colors (assuming bytesPixel == 4). int i = 0; for (int dy = y; dy < y + h; dy++) { for (int dx = x; dx < x + w; dx++) { pixels24[dy * rfb.framebufferWidth + dx] = palette24[indexedData[i++] & 0xFF]; } } } } else if (useGradient) { // "Gradient"-processed data byte[] buf = new byte[w * h * 3]; rfb.readFully(buf); if (rfb.rec != null) { rfb.rec.write(buf); } decodeGradientData(x, y, w, h, buf); } else { // Raw truecolor data. if (bytesPixel == 1) { for (int dy = y; dy < y + h; dy++) { rfb.readFully(pixels8, dy * rfb.framebufferWidth + x, w); if (rfb.rec != null) { rfb.rec.write(pixels8, dy * rfb.framebufferWidth + x, w); } } } else { byte[] buf = new byte[w * 3]; int i, offset; for (int dy = y; dy < y + h; dy++) { rfb.readFully(buf); if (rfb.rec != null) { rfb.rec.write(buf); } offset = dy * rfb.framebufferWidth + x; for (i = 0; i < w; i++) { pixels24[offset + i] = (buf[i * 3] & 0xFF) << 16 | (buf[i * 3 + 1] & 0xFF) << 8 | (buf[i * 3 + 2] & 0xFF); } } } } } else { // Data was compressed with zlib. int zlibDataLen = rfb.readCompactLen(); byte[] zlibData = new byte[zlibDataLen]; rfb.readFully(zlibData); if (rfb.rec != null && rfb.recordFromBeginning) { rfb.rec.write(zlibData); } int stream_id = comp_ctl & 0x03; if (tightInflaters[stream_id] == null) { tightInflaters[stream_id] = new Inflater(); } Inflater myInflater = tightInflaters[stream_id]; myInflater.setInput(zlibData); byte[] buf = new byte[dataSize]; myInflater.inflate(buf); if (rfb.rec != null && !rfb.recordFromBeginning) { rfb.recordCompressedData(buf); } if (numColors != 0) { // Indexed colors. if (numColors == 2) { // Two colors. if (bytesPixel == 1) { decodeMonoData(x, y, w, h, buf, palette8); } else { decodeMonoData(x, y, w, h, buf, palette24); } } else { // More than two colors (assuming bytesPixel == 4). int i = 0; for (int dy = y; dy < y + h; dy++) { for (int dx = x; dx < x + w; dx++) { pixels24[dy * rfb.framebufferWidth + dx] = palette24[buf[i++] & 0xFF]; } } } } else if (useGradient) { // Compressed "Gradient"-filtered data (assuming bytesPixel == 4). decodeGradientData(x, y, w, h, buf); } else { // Compressed truecolor data. if (bytesPixel == 1) { int destOffset = y * rfb.framebufferWidth + x; for (int dy = 0; dy < h; dy++) { System.arraycopy(buf, dy * w, pixels8, destOffset, w); destOffset += rfb.framebufferWidth; } } else { int srcOffset = 0; int destOffset, i; for (int dy = 0; dy < h; dy++) { myInflater.inflate(buf); destOffset = (y + dy) * rfb.framebufferWidth + x; for (i = 0; i < w; i++) { pixels24[destOffset + i] = (buf[srcOffset] & 0xFF) << 16 | (buf[srcOffset + 1] & 0xFF) << 8 | (buf[srcOffset + 2] & 0xFF); srcOffset += 3; } } } } } handleUpdatedPixels(x, y, w, h); scheduleRepaint(x, y, w, h); } // // Decode 1bpp-encoded bi-color rectangle (8-bit and 24-bit versions). // void decodeMonoData(int x, int y, int w, int h, byte[] src, byte[] palette) { int dx, dy, n; int i = y * rfb.framebufferWidth + x; int rowBytes = (w + 7) / 8; byte b; for (dy = 0; dy < h; dy++) { for (dx = 0; dx < w / 8; dx++) { b = src[dy*rowBytes+dx]; for (n = 7; n >= 0; n--) pixels8[i++] = palette[b >> n & 1]; } for (n = 7; n >= 8 - w % 8; n--) { pixels8[i++] = palette[src[dy*rowBytes+dx] >> n & 1]; } i += (rfb.framebufferWidth - w); } } void decodeMonoData(int x, int y, int w, int h, byte[] src, int[] palette) { int dx, dy, n; int i = y * rfb.framebufferWidth + x; int rowBytes = (w + 7) / 8; byte b; for (dy = 0; dy < h; dy++) { for (dx = 0; dx < w / 8; dx++) { b = src[dy*rowBytes+dx]; for (n = 7; n >= 0; n--) pixels24[i++] = palette[b >> n & 1]; } for (n = 7; n >= 8 - w % 8; n--) { pixels24[i++] = palette[src[dy*rowBytes+dx] >> n & 1]; } i += (rfb.framebufferWidth - w); } } // // Decode data processed with the "Gradient" filter. // void decodeGradientData (int x, int y, int w, int h, byte[] buf) { int dx, dy, c; byte[] prevRow = new byte[w * 3]; byte[] thisRow = new byte[w * 3]; byte[] pix = new byte[3]; int[] est = new int[3]; int offset = y * rfb.framebufferWidth + x; for (dy = 0; dy < h; dy++) { /* First pixel in a row */ for (c = 0; c < 3; c++) { pix[c] = (byte)(prevRow[c] + buf[dy * w * 3 + c]); thisRow[c] = pix[c]; } pixels24[offset++] = (pix[0] & 0xFF) << 16 | (pix[1] & 0xFF) << 8 | (pix[2] & 0xFF); /* Remaining pixels of a row */ for (dx = 1; dx < w; dx++) { for (c = 0; c < 3; c++) { est[c] = ((prevRow[dx * 3 + c] & 0xFF) + (pix[c] & 0xFF) - (prevRow[(dx-1) * 3 + c] & 0xFF)); if (est[c] > 0xFF) { est[c] = 0xFF; } else if (est[c] < 0x00) { est[c] = 0x00; } pix[c] = (byte)(est[c] + buf[(dy * w + dx) * 3 + c]); thisRow[dx * 3 + c] = pix[c]; } pixels24[offset++] = (pix[0] & 0xFF) << 16 | (pix[1] & 0xFF) << 8 | (pix[2] & 0xFF); } System.arraycopy(thisRow, 0, prevRow, 0, w * 3); offset += (rfb.framebufferWidth - w); } } // // Display newly updated area of pixels. // void handleUpdatedPixels(int x, int y, int w, int h) { // Draw updated pixels of the off-screen image. pixelsSource.newPixels(x, y, w, h); memGraphics.setClip(x, y, w, h); memGraphics.drawImage(rawPixelsImage, 0, 0, null); memGraphics.setClip(0, 0, rfb.framebufferWidth, rfb.framebufferHeight); } // // Tell JVM to repaint specified desktop area. // void scheduleRepaint(int x, int y, int w, int h) { // Request repaint, deferred if necessary. if (rfb.framebufferWidth == scaledWidth) { repaint(viewer.deferScreenUpdates, x, y, w, h); } else { int sx = x * scalingFactor / 100; int sy = y * scalingFactor / 100; int sw = ((x + w) * scalingFactor + 49) / 100 - sx + 1; int sh = ((y + h) * scalingFactor + 49) / 100 - sy + 1; repaint(viewer.deferScreenUpdates, sx, sy, sw, sh); } } // // Handle events. // public void keyPressed(KeyEvent evt) { processLocalKeyEvent(evt); } public void keyReleased(KeyEvent evt) { processLocalKeyEvent(evt); } public void keyTyped(KeyEvent evt) { evt.consume(); } public void mousePressed(MouseEvent evt) { processLocalMouseEvent(evt, false); } public void mouseReleased(MouseEvent evt) { processLocalMouseEvent(evt, false); } public void mouseMoved(MouseEvent evt) { processLocalMouseEvent(evt, true); } public void mouseDragged(MouseEvent evt) { processLocalMouseEvent(evt, true); } public void processLocalKeyEvent(KeyEvent evt) { if (viewer.rfb != null && rfb.inNormalProtocol) { if (!inputEnabled) { if ((evt.getKeyChar() == 'r' || evt.getKeyChar() == 'R') && evt.getID() == KeyEvent.KEY_PRESSED ) { // Request screen update. try { rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth, rfb.framebufferHeight, false); } catch (IOException e) { e.printStackTrace(); } } } else { // Input enabled. synchronized(rfb) { try { if (extraModifiers != 0) { evt.setModifiers(evt.getModifiers() | extraModifiers); } rfb.writeKeyEvent(evt); } catch (Exception e) { e.printStackTrace(); } rfb.notify(); } } } // Don't ever pass keyboard events to AWT for default processing. // Otherwise, pressing Tab would switch focus to ButtonPanel etc. evt.consume(); } public void processLocalMouseEvent(MouseEvent evt, boolean moved) { if (viewer.rfb != null && rfb.inNormalProtocol) { if (moved) { softCursorMove(evt.getX(), evt.getY()); } if (rfb.framebufferWidth != scaledWidth) { int sx = (evt.getX() * 100 + scalingFactor/2) / scalingFactor; int sy = (evt.getY() * 100 + scalingFactor/2) / scalingFactor; evt.translatePoint(sx - evt.getX(), sy - evt.getY()); } synchronized(rfb) { try { rfb.writePointerEvent(evt); } catch (Exception e) { e.printStackTrace(); } rfb.notify(); } } } // // Ignored events. // public void mouseClicked(MouseEvent evt) {} public void mouseEntered(MouseEvent evt) {} public void mouseExited(MouseEvent evt) {} ////////////////////////////////////////////////////////////////// // // Handle cursor shape updates (XCursor and RichCursor encodings). // boolean showSoftCursor = false; MemoryImageSource softCursorSource; Image softCursor; int cursorX = 0, cursorY = 0; int cursorWidth, cursorHeight; int origCursorWidth, origCursorHeight; int hotX, hotY; int origHotX, origHotY; // // Handle cursor shape update (XCursor and RichCursor encodings). // synchronized void handleCursorShapeUpdate(int encodingType, int xhot, int yhot, int width, int height) throws IOException { softCursorFree(); if (width * height == 0) return; // Ignore cursor shape data if requested by user. if (viewer.options.ignoreCursorUpdates) { int bytesPerRow = (width + 7) / 8; int bytesMaskData = bytesPerRow * height; if (encodingType == rfb.EncodingXCursor) { rfb.is.skipBytes(6 + bytesMaskData * 2); } else { // rfb.EncodingRichCursor rfb.is.skipBytes(width * height + bytesMaskData); } return; } // Decode cursor pixel data. softCursorSource = decodeCursorShape(encodingType, width, height); // Set original (non-scaled) cursor dimensions. origCursorWidth = width; origCursorHeight = height; origHotX = xhot; origHotY = yhot; // Create off-screen cursor image. createSoftCursor(); // Show the cursor. showSoftCursor = true; repaint(viewer.deferCursorUpdates, cursorX - hotX, cursorY - hotY, cursorWidth, cursorHeight); } // // decodeCursorShape(). Decode cursor pixel data and return // corresponding MemoryImageSource instance. // synchronized MemoryImageSource decodeCursorShape(int encodingType, int width, int height) throws IOException { int bytesPerRow = (width + 7) / 8; int bytesMaskData = bytesPerRow * height; int[] softCursorPixels = new int[width * height]; if (encodingType == rfb.EncodingXCursor) { // Read foreground and background colors of the cursor. byte[] rgb = new byte[6]; rfb.readFully(rgb); int[] colors = { (0xFF000000 | (rgb[3] & 0xFF) << 16 | (rgb[4] & 0xFF) << 8 | (rgb[5] & 0xFF)), (0xFF000000 | (rgb[0] & 0xFF) << 16 | (rgb[1] & 0xFF) << 8 | (rgb[2] & 0xFF)) }; // Read pixel and mask data. byte[] pixBuf = new byte[bytesMaskData]; rfb.readFully(pixBuf); byte[] maskBuf = new byte[bytesMaskData]; rfb.readFully(maskBuf); // Decode pixel data into softCursorPixels[]. byte pixByte, maskByte; int x, y, n, result; int i = 0; for (y = 0; y < height; y++) { for (x = 0; x < width / 8; x++) { pixByte = pixBuf[y * bytesPerRow + x]; maskByte = maskBuf[y * bytesPerRow + x]; for (n = 7; n >= 0; n--) { if ((maskByte >> n & 1) != 0) { result = colors[pixByte >> n & 1]; } else { result = 0; // Transparent pixel } softCursorPixels[i++] = result; } } for (n = 7; n >= 8 - width % 8; n--) { if ((maskBuf[y * bytesPerRow + x] >> n & 1) != 0) { result = colors[pixBuf[y * bytesPerRow + x] >> n & 1]; } else { result = 0; // Transparent pixel } softCursorPixels[i++] = result; } } } else { // encodingType == rfb.EncodingRichCursor // Read pixel and mask data. byte[] pixBuf = new byte[width * height * bytesPixel]; rfb.readFully(pixBuf); byte[] maskBuf = new byte[bytesMaskData]; rfb.readFully(maskBuf); // Decode pixel data into softCursorPixels[]. byte pixByte, maskByte; int x, y, n, result; int i = 0; for (y = 0; y < height; y++) { for (x = 0; x < width / 8; x++) { maskByte = maskBuf[y * bytesPerRow + x]; for (n = 7; n >= 0; n--) { if ((maskByte >> n & 1) != 0) { if (bytesPixel == 1) { result = cm8.getRGB(pixBuf[i]); } else { result = 0xFF000000 | (pixBuf[i * 4 + 2] & 0xFF) << 16 | (pixBuf[i * 4 + 1] & 0xFF) << 8 | (pixBuf[i * 4] & 0xFF); } } else { result = 0; // Transparent pixel } softCursorPixels[i++] = result; } } for (n = 7; n >= 8 - width % 8; n--) { if ((maskBuf[y * bytesPerRow + x] >> n & 1) != 0) { if (bytesPixel == 1) { result = cm8.getRGB(pixBuf[i]); } else { result = 0xFF000000 | (pixBuf[i * 4 + 2] & 0xFF) << 16 | (pixBuf[i * 4 + 1] & 0xFF) << 8 | (pixBuf[i * 4] & 0xFF); } } else { result = 0; // Transparent pixel } softCursorPixels[i++] = result; } } } return new MemoryImageSource(width, height, softCursorPixels, 0, width); } // // createSoftCursor(). Assign softCursor new Image (scaled if necessary). // Uses softCursorSource as a source for new cursor image. // synchronized void createSoftCursor() { if (softCursorSource == null) return; int scaleCursor = viewer.options.scaleCursor; if (scaleCursor == 0 || !inputEnabled) scaleCursor = 100; // Save original cursor coordinates. int x = cursorX - hotX; int y = cursorY - hotY; int w = cursorWidth; int h = cursorHeight; cursorWidth = (origCursorWidth * scaleCursor + 50) / 100; cursorHeight = (origCursorHeight * scaleCursor + 50) / 100; hotX = (origHotX * scaleCursor + 50) / 100; hotY = (origHotY * scaleCursor + 50) / 100; softCursor = Toolkit.getDefaultToolkit().createImage(softCursorSource); if (scaleCursor != 100) { softCursor = softCursor.getScaledInstance(cursorWidth, cursorHeight, Image.SCALE_SMOOTH); } if (showSoftCursor) { // Compute screen area to update. x = Math.min(x, cursorX - hotX); y = Math.min(y, cursorY - hotY); w = Math.max(w, cursorWidth); h = Math.max(h, cursorHeight); repaint(viewer.deferCursorUpdates, x, y, w, h); } } // // softCursorMove(). Moves soft cursor into a particular location. // synchronized void softCursorMove(int x, int y) { int oldX = cursorX; int oldY = cursorY; cursorX = x; cursorY = y; if (showSoftCursor) { repaint(viewer.deferCursorUpdates, oldX - hotX, oldY - hotY, cursorWidth, cursorHeight); repaint(viewer.deferCursorUpdates, cursorX - hotX, cursorY - hotY, cursorWidth, cursorHeight); } } // // softCursorFree(). Remove soft cursor, dispose resources. // synchronized void softCursorFree() { if (showSoftCursor) { showSoftCursor = false; softCursor = null; softCursorSource = null; repaint(viewer.deferCursorUpdates, cursorX - hotX, cursorY - hotY, cursorWidth, cursorHeight); } } }