blob: b7764819c4e5831b95e0c389093f52d480e8deb8 [file] [log] [blame]
//
// 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.
//
package com.tigervnc.vncviewer;
import com.tigervnc.decoder.CoRREDecoder;
import com.tigervnc.decoder.CopyRectDecoder;
import com.tigervnc.decoder.HextileDecoder;
import com.tigervnc.decoder.RREDecoder;
import com.tigervnc.decoder.RawDecoder;
import com.tigervnc.decoder.TightDecoder;
import com.tigervnc.decoder.ZRLEDecoder;
import com.tigervnc.decoder.ZlibDecoder;
import com.tigervnc.decoder.common.Repaintable;
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, Repaintable, Runnable {
VncViewer viewer;
RfbProto rfb;
ColorModel cm8, cm24;
int bytesPixel;
int maxWidth = 0, maxHeight = 0;
int scalingFactor;
int scaledWidth, scaledHeight;
Image memImage;
Graphics memGraphics;
//
// Decoders
//
RawDecoder rawDecoder;
RREDecoder rreDecoder;
CoRREDecoder correDecoder;
ZlibDecoder zlibDecoder;
HextileDecoder hextileDecoder;
ZRLEDecoder zrleDecoder;
TightDecoder tightDecoder;
CopyRectDecoder copyRectDecoder;
// Base decoder decoders array
RawDecoder []decoders = null;
// Update statistics.
long statStartTime; // time on first framebufferUpdateRequest
long statNumUpdates; // counter for FramebufferUpdate messages
long statNumTotalRects; // rectangles in FramebufferUpdate messages
long statNumPixelRects; // the same, but excluding pseudo-rectangles
long statNumRectsTight; // Tight-encoded rectangles (including JPEG)
long statNumRectsTightJPEG; // JPEG-compressed Tight-encoded rectangles
long statNumRectsZRLE; // ZRLE-encoded rectangles
long statNumRectsHextile; // Hextile-encoded rectangles
long statNumRectsRaw; // Raw-encoded rectangles
long statNumRectsCopy; // CopyRect rectangles
long statNumBytesEncoded; // number of bytes in updates, as received
long statNumBytesDecoded; // number of bytes, as if Raw encoding was used
// True if we process keyboard and mouse events.
boolean inputEnabled;
// True if was no one auto resize of canvas
boolean isFirstSizeAutoUpdate = true;
// Members for limiting sending mouse events to server
long lastMouseEventSendTime = System.currentTimeMillis();
long mouseMaxFreq = 20;
//
// 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;
cm8 = new DirectColorModel(8, 7, (7 << 3), (3 << 6));
cm24 = new DirectColorModel(24, 0xFF0000, 0x00FF00, 0x0000FF);
//
// Create decoders
//
// Input stream for decoders
RfbInputStream rfbis = new RfbInputStream(rfb);
// Create output stream for session recording
RecordOutputStream ros = new RecordOutputStream(rfb);
rawDecoder = new RawDecoder(memGraphics, rfbis);
rreDecoder = new RREDecoder(memGraphics, rfbis);
correDecoder = new CoRREDecoder(memGraphics, rfbis);
hextileDecoder = new HextileDecoder(memGraphics, rfbis);
tightDecoder = new TightDecoder(memGraphics, rfbis);
zlibDecoder = new ZlibDecoder(memGraphics, rfbis);
zrleDecoder = new ZRLEDecoder(memGraphics, rfbis);
copyRectDecoder = new CopyRectDecoder(memGraphics, rfbis);
//
// Set data for decoders that needs extra parameters
//
hextileDecoder.setRepainableControl(this);
tightDecoder.setRepainableControl(this);
//
// Create array that contains our decoders
//
decoders = new RawDecoder[8];
decoders[0] = rawDecoder;
decoders[1] = rreDecoder;
decoders[2] = correDecoder;
decoders[3] = hextileDecoder;
decoders[4] = zlibDecoder;
decoders[5] = tightDecoder;
decoders[6] = zrleDecoder;
decoders[7] = copyRectDecoder;
//
// Set session recorder for decoders
//
for (int i = 0; i < decoders.length; i++) {
decoders[i].setDataOutputStream(ros);
}
setPixelFormat();
inputEnabled = false;
if (!viewer.options.viewOnly)
enableInput(true);
// Enable mouse and keyboard event listeners.
addKeyListener(this);
addMouseListener(this);
addMouseMotionListener(this);
// Create thread, that will send mouse movement events
// to VNC server.
Thread mouseThread = new Thread(this);
mouseThread.start();
}
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);
}
//
// 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;
if (viewer.showControls) {
viewer.buttonPanel.enableRemoteAccessControls(true);
}
createSoftCursor(); // scaled cursor
} else if (!enable && inputEnabled) {
inputEnabled = false;
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 setScalingFactor(int sf) {
scalingFactor = sf;
updateFramebufferSize();
invalidate();
}
void updateFramebufferSize() {
// Useful shortcuts.
int fbWidth = rfb.framebufferWidth;
int fbHeight = rfb.framebufferHeight;
// FIXME: This part of code must be in VncViewer i think
if (viewer.options.autoScale) {
if (viewer.inAnApplet) {
maxWidth = viewer.getWidth();
maxHeight = viewer.getHeight();
} else {
if (viewer.vncFrame != null) {
if (isFirstSizeAutoUpdate) {
isFirstSizeAutoUpdate = false;
Dimension screenSize = viewer.vncFrame.getToolkit().getScreenSize();
maxWidth = (int)screenSize.getWidth() - 100;
maxHeight = (int)screenSize.getHeight() - 100;
viewer.vncFrame.setSize(maxWidth, maxHeight);
} else {
viewer.desktopScrollPane.doLayout();
maxWidth = viewer.desktopScrollPane.getWidth();
maxHeight = viewer.desktopScrollPane.getHeight();
}
} else {
maxWidth = fbWidth;
maxHeight = fbHeight;
}
}
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();
}
}
//
// Update decoders
//
//
// FIXME: Why decoders can be null here?
//
if (decoders != null) {
for (int i = 0; i < decoders.length; i++) {
//
// Set changes to every decoder that we can use
//
decoders[i].setBPP(bytesPixel);
decoders[i].setFrameBufferSize(fbWidth, fbHeight);
decoders[i].setGraphics(memGraphics);
//
// Update decoder
//
decoders[i].update();
}
}
// FIXME: This part of code must be in VncViewer i think
// Update the size of desktop containers.
if (viewer.inSeparateFrame) {
if (viewer.desktopScrollPane != null) {
if (!viewer.options.autoScale) {
resizeDesktopFrame();
} else {
setSize(scaledWidth, scaledHeight);
viewer.desktopScrollPane.setSize(maxWidth + 200,
maxHeight + 200);
}
}
} 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);
resetStats();
boolean statsRestarted = 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:
if (statNumUpdates == viewer.debugStatsExcludeUpdates &&
!statsRestarted) {
resetStats();
statsRestarted = true;
} else if (statNumUpdates == viewer.debugStatsMeasureUpdates &&
statsRestarted) {
viewer.disconnect();
}
rfb.readFramebufferUpdate();
statNumUpdates++;
boolean cursorPosReceived = false;
for (int i = 0; i < rfb.updateNRects; i++) {
rfb.readFramebufferUpdateRectHdr();
statNumTotalRects++;
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;
}
long numBytesReadBefore = rfb.getNumBytesRead();
rfb.startTiming();
switch (rfb.updateRectEncoding) {
case RfbProto.EncodingRaw:
statNumRectsRaw++;
handleRawRect(rx, ry, rw, rh);
break;
case RfbProto.EncodingCopyRect:
statNumRectsCopy++;
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:
statNumRectsHextile++;
handleHextileRect(rx, ry, rw, rh);
break;
case RfbProto.EncodingZRLE:
statNumRectsZRLE++;
handleZRLERect(rx, ry, rw, rh);
break;
case RfbProto.EncodingZlib:
handleZlibRect(rx, ry, rw, rh);
break;
case RfbProto.EncodingTight:
if (tightDecoder != null) {
statNumRectsTightJPEG = tightDecoder.getNumJPEGRects();
//statNumRectsTight = tightDecoder.getNumTightRects();
}
statNumRectsTight++;
handleTightRect(rx, ry, rw, rh);
break;
default:
throw new Exception("Unknown RFB rectangle encoding " +
rfb.updateRectEncoding);
}
rfb.stopTiming();
statNumPixelRects++;
statNumBytesDecoded += rw * rh * bytesPixel;
statNumBytesEncoded +=
(int)(rfb.getNumBytesRead() - numBytesReadBefore);
}
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.available() == 0 && !cursorPosReceived) {
synchronized(rfb) {
try {
rfb.wait(viewer.deferUpdateRequests);
} catch (InterruptedException e) {
}
}
}
viewer.autoSelectEncodings();
// Before requesting framebuffer update, check if the pixel
// format should be changed.
if (viewer.options.eightBitColors != (bytesPixel == 1)) {
// Pixel format should be changed.
setPixelFormat();
fullUpdateNeeded = true;
}
// Finally, request framebuffer update if needed.
if (fullUpdateNeeded) {
rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth,
rfb.framebufferHeight, false);
} else {
rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth,
rfb.framebufferHeight, true);
}
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, Exception {
handleRawRect(x, y, w, h, true);
}
void handleRawRect(int x, int y, int w, int h, boolean paint)
throws IOException , Exception{
rawDecoder.handleRect(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 {
copyRectDecoder.handleRect(x, y, w, h);
scheduleRepaint(x, y, w, h);
}
//
// Handle an RRE-encoded rectangle.
//
void handleRRERect(int x, int y, int w, int h) throws IOException {
rreDecoder.handleRect(x, y, w, h);
scheduleRepaint(x, y, w, h);
}
//
// Handle a CoRRE-encoded rectangle.
//
void handleCoRRERect(int x, int y, int w, int h) throws IOException {
correDecoder.handleRect(x, y, w, h);
scheduleRepaint(x, y, w, h);
}
//
// Handle a Hextile-encoded rectangle.
//
void handleHextileRect(int x, int y, int w, int h) throws IOException,
Exception {
hextileDecoder.handleRect(x, y, w, h);
}
//
// 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 {
zrleDecoder.handleRect(x, y, w, h);
scheduleRepaint(x, y, w, h);
}
//
// Handle a Zlib-encoded rectangle.
//
void handleZlibRect(int x, int y, int w, int h) throws Exception {
zlibDecoder.handleRect(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 {
tightDecoder.handleRect(x, y, w, h);
scheduleRepaint(x, y, w, h);
}
//
// Tell JVM to repaint specified desktop area.
//
public 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);
}
private synchronized void trySendPointerEvent() {
if ((needToSendMouseEvent) && (mouseEvent!=null)) {
sendMouseEvent(mouseEvent, false);
needToSendMouseEvent = false;
lastMouseEventSendTime = System.currentTimeMillis();
}
}
public void run() {
while (true) {
// Send mouse movement if we have it
trySendPointerEvent();
// Sleep for some time
try {
Thread.sleep(1000 / mouseMaxFreq);
} catch (InterruptedException ex) {
}
}
}
//
// Ignored events.
//
public void mouseClicked(MouseEvent evt) {}
public void mouseEntered(MouseEvent evt) {}
public void mouseExited(MouseEvent evt) {}
//
// Actual event processing.
//
private 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 {
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();
}
private void processLocalMouseEvent(MouseEvent evt, boolean moved) {
if (viewer.rfb != null && rfb.inNormalProtocol) {
if (inputEnabled) {
// If mouse not moved, but it's click event then
// send it to server immideanlty.
// Else, it's mouse movement - we can send it in
// our thread later.
if (!moved) {
sendMouseEvent(evt, moved);
} else {
mouseEvent = evt;
needToSendMouseEvent = true;
}
}
}
}
private void sendMouseEvent(MouseEvent evt, boolean moved) {
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();
lastMouseEventSendTime = System.currentTimeMillis();
}
}
//
// Reset update statistics.
//
void resetStats() {
statStartTime = System.currentTimeMillis();
statNumUpdates = 0;
statNumTotalRects = 0;
statNumPixelRects = 0;
statNumRectsTight = 0;
statNumRectsTightJPEG = 0;
statNumRectsZRLE = 0;
statNumRectsHextile = 0;
statNumRectsRaw = 0;
statNumRectsCopy = 0;
statNumBytesEncoded = 0;
statNumBytesDecoded = 0;
if (tightDecoder != null) {
tightDecoder.setNumJPEGRects(0);
tightDecoder.setNumTightRects(0);
}
}
//////////////////////////////////////////////////////////////////
//
// Handle cursor shape updates (XCursor and RichCursor encodings).
//
boolean showSoftCursor = false;
MemoryImageSource softCursorSource;
Image softCursor;
MouseEvent mouseEvent = null;
boolean needToSendMouseEvent = false;
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.skipBytes(6 + bytesMaskData * 2);
} else {
// rfb.EncodingRichCursor
rfb.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 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);
}
}
}