/* Copyright (C) 2002-2005 RealVNC Ltd. 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.
*/
//
// CConn
//
// Methods on CConn are called from both the GUI thread and the thread which
// processes incoming RFB messages ("the RFB thread"). This means we need to
// be careful with synchronization here.
//
// Any access to writer() must not only be synchronized, but we must also make
// sure that the connection is in RFBSTATE_NORMAL. We are guaranteed this for
// any code called after serverInit() has been called. Since the DesktopWindow
// isn't created until then, any methods called only from DesktopWindow can
// assume that we are in RFBSTATE_NORMAL.
package vncviewer;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.Frame;
import java.awt.ScrollPane;
import rfb.SecTypes;
@SuppressWarnings({"unchecked", "deprecation", "serial"}) class ViewportFrame extends Frame {
public ViewportFrame(String name, CConn cc_) {
super(name);
cc = cc_;
sp = new ScrollPane();
add(sp);
}
public void addChild(Component child) {
sp.add(child);
}
public void setGeometry(int x, int y, int w, int h) {
sp.setSize(w, h);
pack();
setLocation(x, y);
}
public boolean handleEvent(Event event) {
if (event.id == Event.WINDOW_DESTROY) {
cc.close();
}
return super.handleEvent(event);
}
CConn cc;
ScrollPane sp;
}
@SuppressWarnings({"unchecked", "deprecation", "serial"}) public class CConn extends rfb.CConnection
implements rfb.UserPasswdGetter, OptionsDialogCallback
{
////////////////////////////////////////////////////////////////////
// The following methods are all called from the RFB thread
public CConn(VNCViewer viewer_) {
viewer = viewer_;
// Set the rest of the defaults
currentEncoding = rfb.Encodings.ZRLE;
lastUsedEncoding = rfb.Encodings.max;
fullColour = viewer.fullColour.getValue();
autoSelect = viewer.autoSelect.getValue();
shared = viewer.shared.getValue();
options = new OptionsDialog(this);
about = new AboutDialog();
info = new InfoDialog();
clipboardDialog = new ClipboardDialog(this);
setShared(shared);
addSecType(rfb.SecTypes.none);
addSecType(rfb.SecTypes.vncAuth);
String encStr = viewer.preferredEncoding.getValue();
if (encStr != null) {
int encNum = rfb.Encodings.num(encStr);
if (encNum != -1) {
currentEncoding = encNum;
autoSelect = false;
}
}
cp.supportsDesktopResize = true;
cp.supportsLocalCursor = viewer.useLocalCursor.getValue();
menu = new F8Menu(this);
}
// init() gets the name of the VNC server (if necessary), connects to it and
// initiates the RFB protocol. It returns false if no server was entered in
// the dialog box.
public boolean init(java.net.Socket sock_, String vncServerName,
boolean alwaysShowServerDialog)
throws java.io.IOException
{
sock = sock_;
if (sock != null) {
String name = sock.getInetAddress().getHostAddress()+"::"+sock.getPort();
vlog.info("Accepted connection from "+name);
} else {
if (alwaysShowServerDialog || vncServerName == null) {
ServerDialog dlg = new ServerDialog(options, about, vncServerName);
if (!dlg.showDialog() || dlg.server.getText().equals(""))
return false;
vncServerName = dlg.server.getText();
}
serverHost = rfb.Hostname.getHost(vncServerName);
serverPort = rfb.Hostname.getPort(vncServerName);
sock = new java.net.Socket(serverHost, serverPort);
vlog.info("connected to host "+serverHost+" port "+serverPort);
}
setServerName(sock.getInetAddress().getHostAddress()+"::"+sock.getPort());
jis = new rdr.JavaInStream(sock.getInputStream());
jos = new rdr.JavaOutStream(sock.getOutputStream());
setStreams(jis, jos);
initialiseProtocol();
return true;
}
// removeWindow() destroys the viewport and desktop windows.
void removeWindow() {
if (viewport != null)
viewport.dispose();
viewport = null;
}
// getUserPasswd() is called by the CSecurity object when it needs us to read
// a password from the user.
public boolean getUserPasswd(StringBuffer user, StringBuffer passwd) {
String title = ("VNC Authentication ["
+ getCurrentCSecurity().description() + "]");
PasswdDialog dlg = new PasswdDialog(title, (user == null), (passwd == null));
if (!dlg.showDialog()) return false;
if (user != null)
user.append(dlg.userEntry.getText());
if (passwd != null)
passwd.append(dlg.passwdEntry.getText());
return true;
}
// CConnection callback methods
// getCSecurity() gets the appropriate CSecurity object for the security
// types which we support.
public rfb.CSecurity getCSecurity(int secType) {
switch (secType) {
case rfb.SecTypes.none:
return new rfb.CSecurityNone();
case rfb.SecTypes.vncAuth:
return new rfb.CSecurityVncAuth(this);
default:
throw new rfb.Exception("Unsupported secType?");
}
}
// serverInit() is called when the serverInit message has been received. At
// this point we create the desktop window and display it. We also tell the
// server the pixel format and encodings to use and request the first update.
public void serverInit() {
super.serverInit();
serverPF = cp.pf();
desktop = new DesktopWindow(serverPF, this);
desktop.add(menu);
fullColourPF = desktop.getPF();
if (!serverPF.trueColour)
fullColour = true;
recreateViewport();
formatChange = encodingChange = true;
requestNewUpdate();
}
// setDesktopSize() is called when the desktop size changes (including when
// it is set initially).
public void setDesktopSize(int w, int h) {
super.setDesktopSize(w,h);
if (desktop != null) {
desktop.resize();
recreateViewport();
}
}
// framebufferUpdateStart() and framebufferUpdateEnd() are called at the
// beginning and end of an update. We use the speed of the connection,
// computed within beginRect() and endRect() to select the format and
// encoding appropriately, and then request another incremental update.
public void framebufferUpdateStart() {
}
public void framebufferUpdateEnd() {
if (autoSelect)
autoSelectFormatAndEncoding();
requestNewUpdate();
}
// The rest of the callbacks are fairly self-explanatory...
public void setColourMapEntries(int firstColour, int nColours, int[] rgbs) {
desktop.setColourMapEntries(firstColour, nColours, rgbs);
}
public void bell() { desktop.getToolkit().beep(); }
public void serverCutText(String str) {
if (viewer.acceptClipboard.getValue())
clipboardDialog.serverCutText(str);
}
public void beginRect(int x, int y, int w, int h, int encoding) {
jis.startTiming();
desktop.beginRect(x, y, w, h, encoding);
}
public void endRect(int x, int y, int w, int h, int encoding) {
desktop.endRect(x, y, w, h, encoding);
jis.stopTiming();
if ( encoding <= rfb.Encodings.max )
lastUsedEncoding = encoding;
}
public void fillRect(int x, int y, int w, int h, int p) {
desktop.fillRect(x, y, w, h, p);
}
public void imageRect(int x, int y, int w, int h, byte[] p, int offset) {
desktop.imageRect(x, y, w, h, p, offset);
}
public void copyRect(int x, int y, int w, int h, int sx, int sy) {
desktop.copyRect(x, y, w, h, sx, sy);
}
public void setCursor(int hotspotX, int hotspotY, int w, int h,
byte[] data, byte[] mask) {
desktop.setCursor(hotspotX, hotspotY, w, h, data, mask);
}
// recreateViewport() recreates our top-level window. This seems to be
// better than attempting to resize the existing window, at least with
// various X window managers.
void recreateViewport()
{
if (viewport != null) viewport.dispose();
viewport = new ViewportFrame("VNC: "+cp.name, this);
viewport.addChild(desktop);
reconfigureViewport();
viewport.show();
desktop.initGraphics();
}
void reconfigureViewport()
{
//viewport->setMaxSize(cp.width, cp.height);
int w = cp.width + 4; // 4 is due to bizarre ScrollPane border
int h = cp.height + 4;
Dimension dpySize = viewport.getToolkit().getScreenSize();
int wmDecorationWidth = 6;
int wmDecorationHeight = 24;
if (w + wmDecorationWidth >= dpySize.width)
w = dpySize.width - wmDecorationWidth;
if (h + wmDecorationHeight >= dpySize.height)
h = dpySize.height - wmDecorationHeight;
int x = (dpySize.width - w - wmDecorationWidth) / 2;
int y = (dpySize.height - h - wmDecorationHeight)/2;
viewport.setGeometry(x, y, w, h);
}
// autoSelectFormatAndEncoding() chooses the format and encoding appropriate
// to the connection speed:
// Above 3Mbps, switch to hextile
// Below 1.5Mbps, switch to ZRLE
// Above 1Mbps, switch to full colour mode
void autoSelectFormatAndEncoding() {
long kbitsPerSecond = jis.kbitsPerSecond();
int newEncoding = currentEncoding;
if (kbitsPerSecond > 3000) {
newEncoding = rfb.Encodings.hextile;
} else if (kbitsPerSecond < 1500) {
newEncoding = rfb.Encodings.ZRLE;
}
if (newEncoding != currentEncoding) {
vlog.info("Throughput "+kbitsPerSecond+" kbit/s - changing to "+
rfb.Encodings.name(newEncoding)+" encoding");
currentEncoding = newEncoding;
encodingChange = true;
}
// if (kbitsPerSecond > 1000) {
// if (!fullColour) {
// vlog.info("Throughput "+kbitsPerSecond+
// " kbit/s - changing to full colour");
// fullColour = true;
// formatChange = true;
// }
// }
}
// requestNewUpdate() requests an update from the server, having set the
// format and encoding appropriately.
void requestNewUpdate()
{
if (formatChange) {
if (fullColour) {
//desktop.setPF(fullColourPF);
} else {
//desktop.setPF(rfb.PixelFormat(8,6,0,1,3,3,3,4,2,0));
}
String str = desktop.getPF().print();
vlog.info("Using pixel format "+str);
cp.setPF(desktop.getPF());
synchronized (this) {
writer().writeSetPixelFormat(cp.pf());
}
}
checkEncodings();
synchronized (this) {
writer().writeFramebufferUpdateRequest(0, 0, cp.width, cp.height,
!formatChange);
}
formatChange = false;
}
////////////////////////////////////////////////////////////////////
// The following methods are all called from the GUI thread
// close() closes the socket, thus waking up the RFB thread.
public void close() {
try {
shuttingDown = true;
sock.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// Menu callbacks. These are guaranteed only to be called after serverInit()
// has been called, since the menu is only accessible from the DesktopWindow
public void showMenu(int x, int y) {
menu.show(desktop, x, y);
}
public void showInfo() {
info.setDetails(cp.name, serverHost+":"+serverPort, cp.width+"x"+cp.height,
cp.pf().print(), serverPF.print(),
rfb.Encodings.name(currentEncoding),
rfb.Encodings.name(lastUsedEncoding),
jis.kbitsPerSecond()+" kbit/s",
cp.majorVersion+"."+cp.minorVersion,
SecTypes.name(getCurrentCSecurity().getType()),
getCurrentCSecurity().description());
info.showDialog();
}
synchronized public void refresh() {
writer().writeFramebufferUpdateRequest(0, 0, cp.width, cp.height, false);
}
// OptionsDialogCallback. setOptions() sets the options dialog's checkboxes
// etc to reflect our flags. getOptions() sets our flags according to the
// options dialog's checkboxes. They are both called from the GUI thread.
// Some of the flags are also accessed by the RFB thread. I believe that
// reading and writing boolean and int values in java is atomic, so there is
// no need for synchronization.
public void setOptions() {
options.autoSelect.setState(autoSelect);
options.fullColour.setState(false/*fullColour*/);
options.veryLowColour.setState(false/*!fullColour && lowColourLevel==0*/);
options.lowColour.setState(false/*!fullColour && lowColourLevel == 1*/);
options.mediumColour.setState(true/*!fullColour && lowColourLevel == 2*/);
options.fullColour.setEnabled(false);
options.veryLowColour.setEnabled(false);
options.lowColour.setEnabled(false);
options.zrle.setState(currentEncoding == rfb.Encodings.ZRLE);
options.hextile.setState(currentEncoding == rfb.Encodings.hextile);
options.raw.setState(currentEncoding == rfb.Encodings.raw);
options.viewOnly.setState(viewer.viewOnly.getValue());
options.acceptClipboard.setState(viewer.acceptClipboard.getValue());
options.sendClipboard.setState(viewer.sendClipboard.getValue());
if (state() == RFBSTATE_NORMAL)
options.shared.setEnabled(false);
else
options.shared.setState(shared);
options.useLocalCursor.setState(viewer.useLocalCursor.getValue());
options.fastCopyRect.setState(viewer.fastCopyRect.getValue());
}
public void getOptions() {
autoSelect = options.autoSelect.getState();
// if (fullColour != options.fullColour.getState())
// formatChange = true;
fullColour = options.fullColour.getState();
int newEncoding = (options.zrle.getState() ? rfb.Encodings.ZRLE :
options.hextile.getState() ? rfb.Encodings.hextile :
rfb.Encodings.raw);
if (newEncoding != currentEncoding) {
currentEncoding = newEncoding;
encodingChange = true;
}
viewer.viewOnly.setParam(options.viewOnly.getState());
viewer.acceptClipboard.setParam(options.acceptClipboard.getState());
viewer.sendClipboard.setParam(options.sendClipboard.getState());
clipboardDialog.setSendingEnabled(viewer.sendClipboard.getValue());
shared = options.shared.getState();
setShared(shared);
viewer.useLocalCursor.setParam(options.useLocalCursor.getState());
if (cp.supportsLocalCursor != viewer.useLocalCursor.getValue()) {
cp.supportsLocalCursor = viewer.useLocalCursor.getValue();
encodingChange = true;
if (desktop != null)
desktop.resetLocalCursor();
}
viewer.fastCopyRect.setParam(options.fastCopyRect.getState());
checkEncodings();
}
// writeClientCutText() is called from the clipboard dialog
synchronized public void writeClientCutText(String str) {
if (state() != RFBSTATE_NORMAL) return;
writer().writeClientCutText(str);
}
synchronized public void writeKeyEvent(int keysym, boolean down) {
if (state() != RFBSTATE_NORMAL) return;
writer().writeKeyEvent(keysym, down);
}
synchronized public void writeKeyEvent(Event ev) {
if (ev.id != Event.KEY_PRESS && ev.id != Event.KEY_ACTION)
return;
int keysym;
if (ev.id == Event.KEY_PRESS) {
vlog.debug("key press "+ev.key);
if (ev.key < 32) {
// if the ctrl modifier key is down, send the equivalent ASCII since we
// will send the ctrl modifier anyway
if ((ev.modifiers & Event.CTRL_MASK) != 0) {
keysym = ev.key + 96;
if (keysym == 127) keysym = 95;
} else {
switch (ev.key) {
case Event.BACK_SPACE: keysym = rfb.Keysyms.BackSpace; break;
case Event.TAB: keysym = rfb.Keysyms.Tab; break;
case Event.ENTER: keysym = rfb.Keysyms.Return; break;
case Event.ESCAPE: keysym = rfb.Keysyms.Escape; break;
default: return;
}
}
} else if (ev.key == 127) {
keysym = rfb.Keysyms.Delete;
} else {
keysym = rfb.UnicodeToKeysym.translate(ev.key);
if (keysym == -1)
return;
}
} else {
// KEY_ACTION
vlog.debug("key action "+ev.key);
switch (ev.key) {
case Event.HOME: keysym = rfb.Keysyms.Home; break;
case Event.END: keysym = rfb.Keysyms.End; break;
case Event.PGUP: keysym = rfb.Keysyms.Page_Up; break;
case Event.PGDN: keysym = rfb.Keysyms.Page_Down; break;
case Event.UP: keysym = rfb.Keysyms.Up; break;
case Event.DOWN: keysym = rfb.Keysyms.Down; break;
case Event.LEFT: keysym = rfb.Keysyms.Left; break;
case Event.RIGHT: keysym = rfb.Keysyms.Right; break;
case Event.F1: keysym = rfb.Keysyms.F1; break;
case Event.F2: keysym = rfb.Keysyms.F2; break;
case Event.F3: keysym = rfb.Keysyms.F3; break;
case Event.F4: keysym = rfb.Keysyms.F4; break;
case Event.F5: keysym = rfb.Keysyms.F5; break;
case Event.F6: keysym = rfb.Keysyms.F6; break;
case Event.F7: keysym = rfb.Keysyms.F7; break;
case Event.F8: keysym = rfb.Keysyms.F8; break;
case Event.F9: keysym = rfb.Keysyms.F9; break;
case Event.F10: keysym = rfb.Keysyms.F10; break;
case Event.F11: keysym = rfb.Keysyms.F11; break;
case Event.F12: keysym = rfb.Keysyms.F12; break;
case Event.PRINT_SCREEN: keysym = rfb.Keysyms.Print; break;
case Event.PAUSE: keysym = rfb.Keysyms.Pause; break;
case Event.INSERT: keysym = rfb.Keysyms.Insert; break;
default: return;
}
}
writeModifiers(ev.modifiers);
writeKeyEvent(keysym, true);
writeKeyEvent(keysym, false);
writeModifiers(0);
}
synchronized public void writePointerEvent(Event ev) {
if (state() != RFBSTATE_NORMAL) return;
switch (ev.id) {
case Event.MOUSE_DOWN:
buttonMask = 1;
if ((ev.modifiers & Event.ALT_MASK) != 0) buttonMask = 2;
if ((ev.modifiers & Event.META_MASK) != 0) buttonMask = 4;
break;
case Event.MOUSE_UP:
buttonMask = 0;
break;
}
writeModifiers(ev.modifiers & ~Event.ALT_MASK & ~Event.META_MASK);
if (ev.x < 0) ev.x = 0;
if (ev.x > cp.width-1) ev.x = cp.width-1;
if (ev.y < 0) ev.y = 0;
if (ev.y > cp.height-1) ev.y = cp.height-1;
writer().writePointerEvent(ev.x, ev.y, buttonMask);
if (buttonMask == 0) writeModifiers(0);
}
void writeModifiers(int m) {
if ((m & Event.SHIFT_MASK) != (pressedModifiers & Event.SHIFT_MASK))
writeKeyEvent(rfb.Keysyms.Shift_L, (m & Event.SHIFT_MASK) != 0);
if ((m & Event.CTRL_MASK) != (pressedModifiers & Event.CTRL_MASK))
writeKeyEvent(rfb.Keysyms.Control_L, (m & Event.CTRL_MASK) != 0);
if ((m & Event.ALT_MASK) != (pressedModifiers & Event.ALT_MASK))
writeKeyEvent(rfb.Keysyms.Alt_L, (m & Event.ALT_MASK) != 0);
if ((m & Event.META_MASK) != (pressedModifiers & Event.META_MASK))
writeKeyEvent(rfb.Keysyms.Meta_L, (m & Event.META_MASK) != 0);
pressedModifiers = m;
}
////////////////////////////////////////////////////////////////////
// The following methods are called from both RFB and GUI threads
// checkEncodings() sends a setEncodings message if one is needed.
synchronized private void checkEncodings() {
if (encodingChange && state() == RFBSTATE_NORMAL) {
vlog.info("Using "+rfb.Encodings.name(currentEncoding)+" encoding");
writer().writeSetEncodings(currentEncoding, true);
encodingChange = false;
}
}
// the following never change so need no synchronization:
String serverHost;
int serverPort;
java.net.Socket sock;
rdr.JavaInStream jis;
rdr.JavaOutStream jos;
// viewer object is only ever accessed by the GUI thread so needs no
// synchronization (except for one test in DesktopWindow - see comment
// there).
VNCViewer viewer;
// access to desktop by different threads is specified in DesktopWindow
DesktopWindow desktop;
// the following need no synchronization:
rfb.PixelFormat serverPF;
ViewportFrame viewport;
rfb.PixelFormat fullColourPF;
// shuttingDown is set by the GUI thread and only ever tested by the RFB
// thread after the window has been destroyed.
boolean shuttingDown;
// reading and writing int and boolean is atomic in java, so no
// synchronization of the following flags is needed:
int currentEncoding, lastUsedEncoding;
boolean fullColour;
boolean autoSelect;
boolean shared;
boolean formatChange;
boolean encodingChange;
boolean sameMachine;
// All menu, options, about and info stuff is done in the GUI thread (apart
// from when constructed).
F8Menu menu;
OptionsDialog options;
AboutDialog about;
InfoDialog info;
// clipboard sync issues?
ClipboardDialog clipboardDialog;
// the following are only ever accessed by the GUI thread:
int buttonMask;
int pressedModifiers;
static rfb.LogWriter vlog = new rfb.LogWriter("CConn");
}